feat: add process logging with DAG-based event sourcing
Event-sourced orchestration logging: JSONL events with parent relationships form a DAG for causal reconstruction of agent flows. Includes bash event emitter (jq-based) and markdown report generator.
This commit is contained in:
222
lib/archeflow-report.sh
Executable file
222
lib/archeflow-report.sh
Executable file
@@ -0,0 +1,222 @@
|
||||
#!/usr/bin/env bash
|
||||
# archeflow-report.sh — Generate a Markdown process report from ArcheFlow JSONL events.
|
||||
#
|
||||
# Usage: ./lib/archeflow-report.sh <events.jsonl> [--output <file.md>]
|
||||
#
|
||||
# Reads a JSONL event file and produces a structured Markdown report showing
|
||||
# the full orchestration process: phases, decisions, reviews, fixes, metrics.
|
||||
#
|
||||
# Requires: jq
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -lt 1 ]]; then
|
||||
echo "Usage: $0 <events.jsonl> [--output <file.md>]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
EVENT_FILE="$1"
|
||||
OUTPUT=""
|
||||
|
||||
if [[ "${2:-}" == "--output" && -n "${3:-}" ]]; then
|
||||
OUTPUT="$3"
|
||||
fi
|
||||
|
||||
if ! command -v jq &> /dev/null; then
|
||||
echo "Error: jq is required but not installed." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$EVENT_FILE" ]]; then
|
||||
echo "Error: Event file not found: $EVENT_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Helper: extract events by type
|
||||
events_of_type() {
|
||||
jq -c "select(.type == \"$1\")" "$EVENT_FILE"
|
||||
}
|
||||
|
||||
# Extract run metadata
|
||||
RUN_START=$(events_of_type "run.start" | head -1)
|
||||
RUN_COMPLETE=$(events_of_type "run.complete" | head -1)
|
||||
RUN_ID=$(echo "$RUN_START" | jq -r '.run_id // "unknown"')
|
||||
TASK=$(echo "$RUN_START" | jq -r '.data.task // "unknown"')
|
||||
WORKFLOW=$(echo "$RUN_START" | jq -r '.data.workflow // "unknown"')
|
||||
TEAM=$(echo "$RUN_START" | jq -r '.data.team // "unknown"')
|
||||
|
||||
# Generate report
|
||||
generate_report() {
|
||||
cat <<HEADER
|
||||
# Process Report: ${TASK}
|
||||
|
||||
> Auto-generated from ArcheFlow event log.
|
||||
> Run: \`${RUN_ID}\` | Workflow: \`${WORKFLOW}\` | Team: \`${TEAM}\`
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
HEADER
|
||||
|
||||
# Overview table from run.complete
|
||||
if [[ -n "$RUN_COMPLETE" ]]; then
|
||||
STATUS=$(echo "$RUN_COMPLETE" | jq -r '.data.status // "unknown"')
|
||||
CYCLES=$(echo "$RUN_COMPLETE" | jq -r '.data.cycles // "?"')
|
||||
AGENTS=$(echo "$RUN_COMPLETE" | jq -r '.data.agents_total // "?"')
|
||||
FIXES=$(echo "$RUN_COMPLETE" | jq -r '.data.fixes_total // "?"')
|
||||
SHADOWS=$(echo "$RUN_COMPLETE" | jq -r '.data.shadows // "0"')
|
||||
DURATION_MS=$(echo "$RUN_COMPLETE" | jq -r '.data.duration_ms // "0"')
|
||||
DURATION_MIN=$(( DURATION_MS / 60000 ))
|
||||
|
||||
cat <<TABLE
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Status** | ${STATUS} |
|
||||
| **PDCA Cycles** | ${CYCLES} |
|
||||
| **Agents** | ${AGENTS} |
|
||||
| **Fixes** | ${FIXES} |
|
||||
| **Shadows** | ${SHADOWS} |
|
||||
| **Duration** | ~${DURATION_MIN} min |
|
||||
|
||||
TABLE
|
||||
fi
|
||||
|
||||
# Config from run.start
|
||||
CONFIG=$(echo "$RUN_START" | jq -r '.data.config // empty')
|
||||
if [[ -n "$CONFIG" ]]; then
|
||||
echo "### Configuration"
|
||||
echo '```json'
|
||||
echo "$CONFIG" | jq .
|
||||
echo '```'
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo "---"
|
||||
echo ""
|
||||
|
||||
# Phase sections — iterate through phase transitions
|
||||
echo "## Phases"
|
||||
echo ""
|
||||
|
||||
CURRENT_PHASE=""
|
||||
|
||||
# Process all events chronologically
|
||||
while IFS= read -r event; do
|
||||
TYPE=$(echo "$event" | jq -r '.type')
|
||||
PHASE=$(echo "$event" | jq -r '.phase')
|
||||
AGENT=$(echo "$event" | jq -r '.agent // ""')
|
||||
TS=$(echo "$event" | jq -r '.ts')
|
||||
|
||||
# Phase header on transition
|
||||
if [[ "$PHASE" != "$CURRENT_PHASE" && "$TYPE" != "run.start" && "$TYPE" != "run.complete" ]]; then
|
||||
CURRENT_PHASE="$PHASE"
|
||||
PHASE_UPPER=$(echo "$PHASE" | tr '[:lower:]' '[:upper:]')
|
||||
echo "### ${PHASE_UPPER}"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
case "$TYPE" in
|
||||
agent.complete)
|
||||
ARCHETYPE=$(echo "$event" | jq -r '.data.archetype // .agent // "unknown"')
|
||||
DURATION=$(echo "$event" | jq -r '.data.duration_ms // 0')
|
||||
TOKENS=$(echo "$event" | jq -r '.data.tokens // 0')
|
||||
SUMMARY=$(echo "$event" | jq -r '.data.summary // "no summary"')
|
||||
ARTIFACTS=$(echo "$event" | jq -r '(.data.artifacts // []) | join(", ")')
|
||||
DURATION_S=$(( DURATION / 1000 ))
|
||||
|
||||
echo "**${ARCHETYPE}** (${DURATION_S}s, ${TOKENS} tokens)"
|
||||
echo ": ${SUMMARY}"
|
||||
if [[ -n "$ARTIFACTS" ]]; then
|
||||
echo ": Artifacts: ${ARTIFACTS}"
|
||||
fi
|
||||
echo ""
|
||||
;;
|
||||
|
||||
decision)
|
||||
WHAT=$(echo "$event" | jq -r '.data.what // "unknown"')
|
||||
CHOSEN=$(echo "$event" | jq -r '.data.chosen // "unknown"')
|
||||
RATIONALE=$(echo "$event" | jq -r '.data.rationale // ""')
|
||||
|
||||
echo "**Decision: ${WHAT}**"
|
||||
echo ": Chosen: ${CHOSEN}"
|
||||
if [[ -n "$RATIONALE" ]]; then
|
||||
echo ": Rationale: ${RATIONALE}"
|
||||
fi
|
||||
|
||||
# List alternatives if present
|
||||
ALTS=$(echo "$event" | jq -r '(.data.alternatives // [])[] | " - ~" + .id + "~ " + .label + " — " + .reason_rejected')
|
||||
if [[ -n "$ALTS" ]]; then
|
||||
echo ": Rejected:"
|
||||
echo "$ALTS"
|
||||
fi
|
||||
echo ""
|
||||
;;
|
||||
|
||||
review.verdict)
|
||||
ARCHETYPE=$(echo "$event" | jq -r '.data.archetype // .agent // "unknown"')
|
||||
VERDICT=$(echo "$event" | jq -r '.data.verdict // "unknown"')
|
||||
VERDICT_UPPER=$(echo "$VERDICT" | tr '[:lower:]' '[:upper:]' | tr '_' ' ')
|
||||
|
||||
echo "**${ARCHETYPE}** → ${VERDICT_UPPER}"
|
||||
|
||||
# List findings
|
||||
echo "$event" | jq -r '(.data.findings // [])[] | " - [" + .severity + "] " + .description' 2>/dev/null || true
|
||||
echo ""
|
||||
;;
|
||||
|
||||
fix.applied)
|
||||
SOURCE=$(echo "$event" | jq -r '.data.source // "unknown"')
|
||||
FINDING=$(echo "$event" | jq -r '.data.finding // "unknown"')
|
||||
FILE=$(echo "$event" | jq -r '.data.file // ""')
|
||||
LINE=$(echo "$event" | jq -r '.data.line // ""')
|
||||
|
||||
if [[ -n "$FILE" && "$LINE" != "null" && -n "$LINE" ]]; then
|
||||
echo "- **Fix** (${SOURCE}): ${FINDING} — \`${FILE}:${LINE}\`"
|
||||
else
|
||||
echo "- **Fix** (${SOURCE}): ${FINDING}"
|
||||
fi
|
||||
;;
|
||||
|
||||
shadow.detected)
|
||||
ARCHETYPE=$(echo "$event" | jq -r '.data.archetype // "unknown"')
|
||||
SHADOW=$(echo "$event" | jq -r '.data.shadow // "unknown"')
|
||||
ACTION=$(echo "$event" | jq -r '.data.action // "unknown"')
|
||||
|
||||
echo "- **Shadow** ⚠️ ${ARCHETYPE}: ${SHADOW} → ${ACTION}"
|
||||
echo ""
|
||||
;;
|
||||
|
||||
cycle.boundary)
|
||||
CYCLE=$(echo "$event" | jq -r '.data.cycle // "?"')
|
||||
MAX=$(echo "$event" | jq -r '.data.max_cycles // "?"')
|
||||
MET=$(echo "$event" | jq -r '.data.met // false')
|
||||
NEXT=$(echo "$event" | jq -r '.data.next_action // "unknown"')
|
||||
|
||||
echo ""
|
||||
echo "---"
|
||||
echo ""
|
||||
echo "**Cycle ${CYCLE}/${MAX}** — exit condition met: ${MET} → ${NEXT}"
|
||||
echo ""
|
||||
;;
|
||||
esac
|
||||
|
||||
done < "$EVENT_FILE"
|
||||
|
||||
# Artifacts list from run.complete
|
||||
if [[ -n "$RUN_COMPLETE" ]]; then
|
||||
echo ""
|
||||
echo "---"
|
||||
echo ""
|
||||
echo "## Artifacts"
|
||||
echo ""
|
||||
echo "$RUN_COMPLETE" | jq -r '(.data.artifacts // [])[] | "- `" + . + "`"'
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ -n "$OUTPUT" ]]; then
|
||||
generate_report > "$OUTPUT"
|
||||
echo "Report written to: $OUTPUT" >&2
|
||||
else
|
||||
generate_report
|
||||
fi
|
||||
Reference in New Issue
Block a user