Files
claude-archeflow-plugin/lib/archeflow-report.sh
Christian Nennemann 1753e69a9f 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.
2026-04-03 11:06:02 +02:00

223 lines
6.5 KiB
Bash
Executable File

#!/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