#!/usr/bin/env bash # archeflow-report.sh — Generate a Markdown process report from ArcheFlow JSONL events. # # Usage: ./lib/archeflow-report.sh [--output ] [--dag] [--summary] # # Reads a JSONL event file and produces a structured Markdown report showing # the full orchestration process: phases, decisions, reviews, fixes, metrics. # # Flags: # --output Write report to file instead of stdout # --dag Output ONLY the ASCII DAG (for quick terminal viewing) # --summary Output a one-line summary (for session logs) # # Requires: jq set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" if [[ $# -lt 1 ]]; then echo "Usage: $0 [--output ] [--dag] [--summary]" >&2 exit 1 fi EVENT_FILE="$1" shift OUTPUT="" MODE="full" # full | dag | summary while [[ $# -gt 0 ]]; do case "$1" in --output) OUTPUT="${2:-}" shift 2 ;; --dag) MODE="dag" shift ;; --summary) MODE="summary" shift ;; *) shift ;; esac done 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"') # --summary mode: one-line output and exit if [[ "$MODE" == "summary" ]]; then if [[ -n "$RUN_COMPLETE" ]]; then STATUS=$(echo "$RUN_COMPLETE" | jq -r '.data.status // "unknown"') CYCLES=$(echo "$RUN_COMPLETE" | jq -r '.data.cycles // "?"') # Handle both agents_total and agents field names AGENTS=$(echo "$RUN_COMPLETE" | jq -r '.data.agents_total // .data.agents // "?"') FIXES=$(echo "$RUN_COMPLETE" | jq -r '.data.fixes_total // .data.fixes // "?"') DURATION_MS=$(echo "$RUN_COMPLETE" | jq -r '.data.duration_ms // "0"') if [[ "$DURATION_MS" != "0" && "$DURATION_MS" != "null" ]]; then DURATION_MIN=$(( DURATION_MS / 60000 )) echo "[${STATUS}] ${TASK} — ${CYCLES} cycles, ${AGENTS} agents, ${FIXES} fixes (~${DURATION_MIN}min) [${RUN_ID}]" else echo "[${STATUS}] ${TASK} — ${CYCLES} cycles, ${AGENTS} agents, ${FIXES} fixes [${RUN_ID}]" fi else echo "[in-progress] ${TASK} [${RUN_ID}]" fi exit 0 fi # --dag mode: output DAG and exit if [[ "$MODE" == "dag" ]]; then if [[ -x "${SCRIPT_DIR}/archeflow-dag.sh" ]]; then "${SCRIPT_DIR}/archeflow-dag.sh" "$EVENT_FILE" "$@" else echo "Error: archeflow-dag.sh not found at ${SCRIPT_DIR}/archeflow-dag.sh" >&2 exit 1 fi exit 0 fi # --- Full report mode --- # Collect cycle data for cycle diff section CYCLE_BOUNDARIES=$(events_of_type "cycle.boundary" | jq -r '.data.cycle' 2>/dev/null || true) CYCLE_COUNT=0 if [[ -n "$CYCLE_BOUNDARIES" ]]; then CYCLE_COUNT=$(echo "$CYCLE_BOUNDARIES" | grep -c '[0-9]' 2>/dev/null || true) CYCLE_COUNT=${CYCLE_COUNT:-0} fi # Collect review findings per cycle for diff # A cycle's reviews are between two cycle.boundary events (or between start and first boundary) collect_cycle_findings() { # Returns JSON array of {cycle, archetype, findings[]} for all review.verdict events jq -s ' # Assign cycle number to each event based on cycle.boundary positions ( [.[] | select(.type == "cycle.boundary") | .seq] | sort ) as $boundaries | [.[] | select(.type == "review.verdict")] | [.[] | { seq: .seq, archetype: (.data.archetype // .agent // "unknown"), verdict: .data.verdict, findings: (.data.findings // []), cycle: ( .seq as $s | if ($boundaries | length) == 0 then 1 else ([1] + [$boundaries | to_entries[] | select(.value < $s) | .key + 2] | max) end ) }] ' "$EVENT_FILE" } generate_report() { cat <
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 // "?"') # Handle both agents_total and agents field names AGENTS=$(echo "$RUN_COMPLETE" | jq -r '.data.agents_total // .data.agents // "?"') FIXES=$(echo "$RUN_COMPLETE" | jq -r '.data.fixes_total // .data.fixes // "?"') SHADOWS=$(echo "$RUN_COMPLETE" | jq -r '.data.shadows // "0"') DURATION_MS=$(echo "$RUN_COMPLETE" | jq -r '.data.duration_ms // "0"') if [[ "$DURATION_MS" != "0" && "$DURATION_MS" != "null" ]]; then DURATION_MIN=$(( DURATION_MS / 60000 )) DURATION_DISPLAY="~${DURATION_MIN} min" else DURATION_DISPLAY="n/a" fi cat </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" # Cycle Comparison section (only if multiple cycles detected) if [[ "$CYCLE_COUNT" -ge 2 ]]; then echo "" echo "---" echo "" echo "## Cycle Comparison" echo "" # Collect all review findings with cycle assignment CYCLE_FINDINGS=$(collect_cycle_findings) # Get unique cycle numbers CYCLE_NUMS=$(echo "$CYCLE_FINDINGS" | jq -r '[.[].cycle] | unique | .[]') # Compare consecutive cycles PREV_CYCLE="" for CURR_CYCLE in $CYCLE_NUMS; do if [[ -n "$PREV_CYCLE" ]]; then echo "### Cycle ${PREV_CYCLE} → Cycle ${CURR_CYCLE}" echo "" # Get findings for each cycle as JSON arrays PREV_FINDINGS=$(echo "$CYCLE_FINDINGS" | jq --argjson c "$PREV_CYCLE" \ '[.[] | select(.cycle == $c) | .findings[] | {desc: .description, sev: .severity}]' 2>/dev/null || echo "[]") CURR_FINDINGS=$(echo "$CYCLE_FINDINGS" | jq --argjson c "$CURR_CYCLE" \ '[.[] | select(.cycle == $c) | .findings[] | {desc: .description, sev: .severity}]' 2>/dev/null || echo "[]") # Compute new, resolved, and persistent findings DIFF_OUTPUT=$(jq -rn --argjson prev "$PREV_FINDINGS" --argjson curr "$CURR_FINDINGS" ' def descs: [.[].desc]; ($prev | descs) as $pd | ($curr | descs) as $cd | ($curr | [.[] | select(.desc as $d | $pd | all(. != $d))]) as $new | ($prev | [.[] | select(.desc as $d | $cd | all(. != $d))]) as $resolved | ($curr | [.[] | select(.desc as $d | $pd | any(. == $d))]) as $persistent | ( (if ($new | length) > 0 then ["**New findings:**"] + [$new[] | "- [" + .sev + "] " + .desc] else [] end) + (if ($resolved | length) > 0 then ["", "**Resolved findings:**"] + [$resolved[] | "- [" + .sev + "] " + .desc] else [] end) + (if ($persistent | length) > 0 then ["", "**Persistent findings:**"] + [$persistent[] | "- [" + .sev + "] " + .desc] else [] end) ) | .[] ' 2>/dev/null || true) if [[ -n "$DIFF_OUTPUT" ]]; then echo "$DIFF_OUTPUT" else echo "(No findings to compare)" fi echo "" fi PREV_CYCLE="$CURR_CYCLE" done fi # 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