- skills/run: automated PDCA execution loop with --start-from, --dry-run - skills/artifact-routing: inter-phase artifact protocol with context injection - skills/act-phase: structured review→fix pipeline with cycle feedback - skills/domains: domain adapter system (writing, code, research) - skills/cost-tracking: per-agent cost estimation, budget enforcement - lib/archeflow-dag.sh: ASCII DAG renderer from JSONL events - lib/archeflow-report.sh: updated with DAG section, cycle diff, --dag/--summary flags
262 lines
8.0 KiB
Bash
Executable File
262 lines
8.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# archeflow-dag.sh — Render an ASCII DAG from ArcheFlow JSONL events.
|
|
#
|
|
# Usage: ./lib/archeflow-dag.sh <events.jsonl> [--color] [--no-color]
|
|
#
|
|
# Reads a JSONL event file and renders the causal DAG as ASCII art.
|
|
# Each event shows: #seq description (phase) [metadata]
|
|
# Tree drawing uses Unicode box-drawing characters for branches.
|
|
#
|
|
# The rendering uses a "logical grouping" strategy: phase transitions and
|
|
# structural events appear as top-level siblings under root, with agents
|
|
# and sub-events nested beneath their phase section. This gives a readable
|
|
# timeline view while preserving DAG relationships.
|
|
#
|
|
# Requires: jq
|
|
|
|
set -euo pipefail
|
|
|
|
if [[ $# -lt 1 ]]; then
|
|
echo "Usage: $0 <events.jsonl> [--color] [--no-color]" >&2
|
|
exit 1
|
|
fi
|
|
|
|
EVENT_FILE="$1"
|
|
shift
|
|
|
|
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
|
|
|
|
# Color support: auto-detect terminal, allow override
|
|
USE_COLOR=auto
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--color) USE_COLOR=yes ;;
|
|
--no-color) USE_COLOR=no ;;
|
|
esac
|
|
done
|
|
|
|
if [[ "$USE_COLOR" == "auto" ]]; then
|
|
if [[ -t 1 ]]; then
|
|
USE_COLOR=yes
|
|
else
|
|
USE_COLOR=no
|
|
fi
|
|
fi
|
|
|
|
# ANSI color codes
|
|
if [[ "$USE_COLOR" == "yes" ]]; then
|
|
C_RESET="\033[0m"
|
|
C_SEQ="\033[1;37m" # bold white for seq numbers
|
|
C_PLAN="\033[1;34m" # blue for plan phase
|
|
C_DO="\033[1;32m" # green for do phase
|
|
C_CHECK="\033[1;33m" # yellow for check phase
|
|
C_ACT="\033[1;35m" # magenta for act phase
|
|
C_TRANS="\033[0;36m" # cyan for phase transitions
|
|
C_DIM="\033[0;90m" # dim for metadata
|
|
C_DECISION="\033[1;33m" # yellow for decisions
|
|
C_VERDICT="\033[1;31m" # red for verdicts
|
|
else
|
|
C_RESET="" C_SEQ="" C_PLAN="" C_DO="" C_CHECK="" C_ACT=""
|
|
C_TRANS="" C_DIM="" C_DECISION="" C_VERDICT=""
|
|
fi
|
|
|
|
phase_color() {
|
|
case "$1" in
|
|
plan) printf "%s" "$C_PLAN" ;;
|
|
do) printf "%s" "$C_DO" ;;
|
|
check) printf "%s" "$C_CHECK" ;;
|
|
act) printf "%s" "$C_ACT" ;;
|
|
*) printf "%s" "$C_RESET" ;;
|
|
esac
|
|
}
|
|
|
|
# Pre-process all events with jq into a structured format for bash consumption.
|
|
# Output: seq|type|phase|agent|parents_csv|label
|
|
# This avoids calling jq per-event in a loop.
|
|
EVENTS_PARSED=$(jq -r '
|
|
def mklabel:
|
|
if .type == "run.start" then "run.start"
|
|
elif .type == "agent.complete" then
|
|
(.data.archetype // .agent // "unknown") + " (" + .phase + ")" +
|
|
(if (.data.tokens // 0) > 0 then " [" + (.data.tokens | tostring) + " tok]" else "" end)
|
|
elif .type == "decision" then
|
|
"decision: " + (.data.what // "unknown") + " → " + (.data.chosen // "unknown")
|
|
elif .type == "phase.transition" then
|
|
"─── " + (.data.from // "?") + " → " + (.data.to // "?") + " ───"
|
|
elif .type == "review.verdict" then
|
|
(.data.archetype // .agent // "unknown") + " (" + .phase + ") → " +
|
|
((.data.verdict // "unknown") | ascii_upcase | gsub("_"; " "))
|
|
elif .type == "fix.applied" then
|
|
"fix (" + (.data.source // "unknown") + "): " + (.data.finding // "unknown")
|
|
elif .type == "cycle.boundary" then
|
|
"cycle " + ((.data.cycle // 0) | tostring) + "/" + ((.data.max_cycles // 0) | tostring) +
|
|
" → " + (.data.next_action // "continue")
|
|
elif .type == "shadow.detected" then
|
|
"shadow: " + (.data.archetype // "unknown") + " — " + (.data.shadow // "unknown")
|
|
elif .type == "run.complete" then
|
|
"run.complete [" + ((.data.agents_total // .data.agents // 0) | tostring) +
|
|
" agents, " + ((.data.fixes_total // .data.fixes // 0) | tostring) + " fixes]"
|
|
else .type
|
|
end;
|
|
[.seq, .type, .phase,
|
|
(.agent // "_NONE_"),
|
|
(((.parent // []) | map(tostring) | join(",")) | if . == "" then "_NONE_" else . end),
|
|
mklabel]
|
|
| join("§")
|
|
' "$EVENT_FILE")
|
|
|
|
# Parse into arrays
|
|
declare -A EVENT_TYPE EVENT_PHASE EVENT_LABEL EVENT_PARENTS
|
|
declare -A CHILDREN_OF # parent_seq -> space-separated child seqs
|
|
MAX_SEQ=0
|
|
|
|
while IFS='§' read -r seq type phase agent parents label; do
|
|
[[ "$agent" == "_NONE_" ]] && agent=""
|
|
[[ "$parents" == "_NONE_" ]] && parents=""
|
|
EVENT_TYPE[$seq]="$type"
|
|
EVENT_PHASE[$seq]="$phase"
|
|
EVENT_LABEL[$seq]="$label"
|
|
EVENT_PARENTS[$seq]="$parents"
|
|
|
|
# Register parent-child relationships
|
|
if [[ -z "$parents" ]]; then
|
|
CHILDREN_OF[0]="${CHILDREN_OF[0]:-} $seq"
|
|
else
|
|
IFS=',' read -ra parent_arr <<< "$parents"
|
|
for p in "${parent_arr[@]}"; do
|
|
CHILDREN_OF[$p]="${CHILDREN_OF[$p]:-} $seq"
|
|
done
|
|
fi
|
|
|
|
if (( seq > MAX_SEQ )); then
|
|
MAX_SEQ=$seq
|
|
fi
|
|
done <<< "$EVENTS_PARSED"
|
|
|
|
# Sort and deduplicate children
|
|
for key in "${!CHILDREN_OF[@]}"; do
|
|
CHILDREN_OF[$key]=$(echo "${CHILDREN_OF[$key]}" | tr ' ' '\n' | sort -un | tr '\n' ' ' | xargs)
|
|
done
|
|
|
|
# Determine display parent for each event.
|
|
# Strategy: structural events (phase.transition, cycle.boundary, run.complete) are promoted
|
|
# to be direct children of #1 (run.start), creating a flat timeline backbone.
|
|
# All other events use their first (lowest-numbered) parent for display.
|
|
declare -A DISPLAY_PARENT # seq -> parent seq for display (0 = root)
|
|
declare -A DISPLAY_CHILDREN # parent -> ordered children for display
|
|
|
|
for seq_i in $(seq 1 "$MAX_SEQ"); do
|
|
[[ -z "${EVENT_TYPE[$seq_i]:-}" ]] && continue
|
|
local_type="${EVENT_TYPE[$seq_i]}"
|
|
parents_csv="${EVENT_PARENTS[$seq_i]:-}"
|
|
|
|
if [[ -z "$parents_csv" ]]; then
|
|
# Root event (run.start)
|
|
DISPLAY_PARENT[$seq_i]=0
|
|
elif [[ "$local_type" == "phase.transition" || "$local_type" == "cycle.boundary" || "$local_type" == "run.complete" ]]; then
|
|
# Promote structural events to be children of run.start (#1)
|
|
DISPLAY_PARENT[$seq_i]=1
|
|
else
|
|
# Use first (lowest) parent as display parent
|
|
IFS=',' read -ra parr <<< "$parents_csv"
|
|
DISPLAY_PARENT[$seq_i]="${parr[0]}"
|
|
fi
|
|
|
|
dp="${DISPLAY_PARENT[$seq_i]}"
|
|
DISPLAY_CHILDREN[$dp]="${DISPLAY_CHILDREN[$dp]:-} $seq_i"
|
|
done
|
|
|
|
# Sort display children
|
|
for key in "${!DISPLAY_CHILDREN[@]}"; do
|
|
DISPLAY_CHILDREN[$key]=$(echo "${DISPLAY_CHILDREN[$key]}" | tr ' ' '\n' | sort -n | tr '\n' ' ' | xargs)
|
|
done
|
|
|
|
# Render the tree recursively using display hierarchy
|
|
render_node() {
|
|
local seq="$1"
|
|
local prefix="$2"
|
|
local is_last="$3"
|
|
|
|
local label="${EVENT_LABEL[$seq]:-unknown}"
|
|
local phase="${EVENT_PHASE[$seq]:-}"
|
|
local type="${EVENT_TYPE[$seq]:-}"
|
|
local pc
|
|
pc=$(phase_color "$phase")
|
|
|
|
# Format seq number with padding
|
|
local seq_str
|
|
seq_str=$(printf "#%-3s" "${seq}")
|
|
|
|
# Connector
|
|
local connector
|
|
if [[ -z "$prefix" && "$seq" == "1" ]]; then
|
|
connector=""
|
|
elif [[ "$is_last" == "true" ]]; then
|
|
connector="└── "
|
|
else
|
|
connector="├── "
|
|
fi
|
|
|
|
# Color the label based on type
|
|
local colored_label
|
|
case "$type" in
|
|
phase.transition) colored_label="${C_TRANS}${label}${C_RESET}" ;;
|
|
decision) colored_label="${C_DECISION}${label}${C_RESET}" ;;
|
|
review.verdict) colored_label="${C_VERDICT}${label}${C_RESET}" ;;
|
|
*) colored_label="${pc}${label}${C_RESET}" ;;
|
|
esac
|
|
|
|
if [[ "$seq" == "1" ]]; then
|
|
printf "%b\n" "${C_SEQ}#1${C_RESET} ${colored_label}"
|
|
else
|
|
printf "%b\n" "${prefix}${connector}${C_SEQ}${seq_str}${C_RESET}${colored_label}"
|
|
fi
|
|
|
|
# Render children
|
|
local children="${DISPLAY_CHILDREN[$seq]:-}"
|
|
if [[ -z "$children" ]]; then
|
|
return
|
|
fi
|
|
|
|
local child_arr=($children)
|
|
local count=${#child_arr[@]}
|
|
local i=0
|
|
|
|
for c in "${child_arr[@]}"; do
|
|
i=$((i + 1))
|
|
local child_is_last="false"
|
|
if [[ $i -eq $count ]]; then
|
|
child_is_last="true"
|
|
fi
|
|
|
|
local child_prefix
|
|
if [[ "$seq" == "1" ]]; then
|
|
child_prefix=""
|
|
elif [[ "$is_last" == "true" ]]; then
|
|
child_prefix="${prefix} "
|
|
else
|
|
child_prefix="${prefix}│ "
|
|
fi
|
|
|
|
render_node "$c" "$child_prefix" "$child_is_last"
|
|
done
|
|
}
|
|
|
|
# Find root nodes (display parent == 0 means top-level)
|
|
root_children="${DISPLAY_CHILDREN[0]:-}"
|
|
if [[ -z "$root_children" ]]; then
|
|
echo "No events found." >&2
|
|
exit 1
|
|
fi
|
|
|
|
# The first root child should be #1 (run.start), render from there
|
|
render_node 1 "" "true"
|