From 607a53f1bf8ab4658033dd47af682fcdb82786d3 Mon Sep 17 00:00:00 2001 From: Christian Nennemann Date: Mon, 6 Apr 2026 21:33:36 +0200 Subject: [PATCH] feat: add decision.point event type, decision logger, and run replay script - archeflow-decision.sh: convenience wrapper for logging PDCA decision points - archeflow-replay.sh: timeline view and weighted what-if replay for recorded runs - archeflow-event.sh: add decision.point usage example - archeflow-dag.sh: render decision.point events in DAG output --- lib/archeflow-dag.sh | 5 +- lib/archeflow-decision.sh | 48 ++++++++ lib/archeflow-event.sh | 3 + lib/archeflow-replay.sh | 228 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 283 insertions(+), 1 deletion(-) create mode 100755 lib/archeflow-decision.sh create mode 100755 lib/archeflow-replay.sh diff --git a/lib/archeflow-dag.sh b/lib/archeflow-dag.sh index d1212de..78d0634 100755 --- a/lib/archeflow-dag.sh +++ b/lib/archeflow-dag.sh @@ -87,6 +87,9 @@ EVENTS_PARSED=$(jq -r ' 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.point" then + (.data.archetype // .agent // "?") + " → " + (.data.decision // "?") + + " (conf " + ((.data.confidence // 0) | tostring) + ")" elif .type == "decision" then "decision: " + (.data.what // "unknown") + " → " + (.data.chosen // "unknown") elif .type == "phase.transition" then @@ -209,7 +212,7 @@ render_node() { local colored_label case "$type" in phase.transition) colored_label="${C_TRANS}${label}${C_RESET}" ;; - decision) colored_label="${C_DECISION}${label}${C_RESET}" ;; + decision|decision.point) colored_label="${C_DECISION}${label}${C_RESET}" ;; review.verdict) colored_label="${C_VERDICT}${label}${C_RESET}" ;; *) colored_label="${pc}${label}${C_RESET}" ;; esac diff --git a/lib/archeflow-decision.sh b/lib/archeflow-decision.sh new file mode 100755 index 0000000..1190f6e --- /dev/null +++ b/lib/archeflow-decision.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# archeflow-decision.sh — Log a PDCA decision point for run replay / effectiveness analysis. +# +# Appends a decision.point event to .archeflow/events/.jsonl with: +# phase, archetype (agent + data.archetype), input, decision, confidence, ts (via event layer) +# +# Usage: +# ./lib/archeflow-decision.sh '' '' [parent_seq] +# +# Examples: +# ./lib/archeflow-decision.sh 2026-04-06-auth check guardian \ +# 'diff + proposal risks' 'needs_changes' 0.82 7 +# ./lib/archeflow-decision.sh 2026-04-06-auth act "" 'route findings' 'send_to_maker' 0.9 +# +# confidence: 0.0–1.0 (orchestrator-estimated certainty in the recorded choice) +# +# Requires: jq (via archeflow-event.sh) + +set -euo pipefail + +LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +if [[ $# -lt 6 ]]; then + echo "Usage: $0 '' '' [parent_seq]" >&2 + exit 1 +fi + +RUN_ID="$1" +PHASE="$2" +ARCH="$3" +INPUT="$4" +DECISION="$5" +CONF_RAW="$6" +PARENT="${7:-}" + +if ! [[ "$CONF_RAW" =~ ^[0-9]*\.?[0-9]+$ ]]; then + echo "Error: confidence must be a number (e.g. 0.85)" >&2 + exit 1 +fi + +DATA=$(jq -cn \ + --arg a "$ARCH" \ + --arg i "$INPUT" \ + --arg d "$DECISION" \ + --argjson c "$CONF_RAW" \ + '{archetype:$a, input:$i, decision:$d, confidence:$c}') + +exec "$LIB_DIR/archeflow-event.sh" "$RUN_ID" decision.point "$PHASE" "$ARCH" "$DATA" "$PARENT" diff --git a/lib/archeflow-event.sh b/lib/archeflow-event.sh index 5a3859b..ab0a728 100755 --- a/lib/archeflow-event.sh +++ b/lib/archeflow-event.sh @@ -8,6 +8,9 @@ # ./lib/archeflow-event.sh 2026-04-03-der-huster agent.complete plan creator '{"duration_ms":167522}' 2 # ./lib/archeflow-event.sh 2026-04-03-der-huster phase.transition do "" '{"from":"plan","to":"do"}' 3,4 # ./lib/archeflow-event.sh 2026-04-03-der-huster fix.applied act "" '{"source":"guardian"}' 8 +# ./lib/archeflow-event.sh 2026-04-03-der-huster decision.point check guardian \ +# '{"archetype":"guardian","input":"diff","decision":"needs_changes","confidence":0.85}' 7 +# # Or use: ./lib/archeflow-decision.sh '' '' [parent] # # Parent seqs: comma-separated seq numbers of causal parent events (DAG). # "2" → single parent [2] diff --git a/lib/archeflow-replay.sh b/lib/archeflow-replay.sh new file mode 100755 index 0000000..a621d11 --- /dev/null +++ b/lib/archeflow-replay.sh @@ -0,0 +1,228 @@ +#!/usr/bin/env bash +# archeflow-replay.sh — Inspect recorded runs: decision timeline and weighted what-if replay. +# +# Usage: +# archeflow-replay.sh timeline +# archeflow-replay.sh whatif [--weights arch=w,arch2=w2] [--threshold 0.5] [--json] +# archeflow-replay.sh compare [--weights ...] [--threshold ...] [--json] +# +# Events file: .archeflow/events/.jsonl (relative to current working directory) +# +# whatif / compare: +# - Loads check-phase review.verdict events (last verdict per archetype). +# - Original gate (strict): BLOCK if any reviewer is not approved. +# - Replay gate (weighted): BLOCK if sum(weight * strict) / sum(weight) >= threshold, +# where strict=1 for non-approved verdicts, else 0. Default weight per archetype is 1.0. +# +# Requires: jq + +set -euo pipefail + +if [[ $# -lt 2 ]]; then + echo "Usage: $0 {timeline|whatif|compare} [options]" >&2 + echo "" >&2 + echo " timeline Decision timeline (decision.point + review.verdict)" >&2 + echo " whatif [--weights k=v,...] [--threshold 0.5] [--json]" >&2 + echo " compare (timeline + whatif summary)" >&2 + exit 1 +fi + +COMMAND="$1" +RUN_ID="$2" +shift 2 + +if ! command -v jq &>/dev/null; then + echo "Error: jq is required." >&2 + exit 1 +fi + +EVENT_FILE=".archeflow/events/${RUN_ID}.jsonl" + +resolve_event_file() { + if [[ ! -f "$EVENT_FILE" ]]; then + echo "Error: event file not found: $EVENT_FILE" >&2 + exit 1 + fi +} + +cmd_timeline() { + resolve_event_file + echo "## Decision timeline — run_id=${RUN_ID}" + echo "" + local cnt + cnt=$(jq -s '[.[] | select(.type == "decision.point")] | length' "$EVENT_FILE") + if [[ "$cnt" -gt 0 ]]; then + echo "### decision.point (${cnt})" + jq -r 'select(.type == "decision.point") + | "- \(.ts) [\(.phase)] \(.data.archetype // .agent // "?") \(.data.decision) conf=\(.data.confidence // "n/a") input=\(.data.input // "")"' \ + "$EVENT_FILE" + echo "" + else + echo "### decision.point" + echo "(none — emit with ./lib/archeflow-decision.sh during the run)" + echo "" + fi + + echo "### review.verdict (check phase)" + if jq -e -s '[.[] | select(.type == "review.verdict" and .phase == "check")] | length > 0' "$EVENT_FILE" >/dev/null 2>&1; then + jq -r 'select(.type == "review.verdict" and .phase == "check") + | "- \(.ts) \(.data.archetype // .agent // "?") verdict=\(.data.verdict) findings=\((.data.findings // []) | length)"' \ + "$EVENT_FILE" + else + echo "(none)" + fi + echo "" +} + +parse_weights_to_json() { + local raw="${1:-}" + local obj='{}' + if [[ -z "$raw" ]]; then + echo '{}' + return + fi + IFS=',' read -ra pairs <<< "$raw" + for pair in "${pairs[@]}"; do + [[ -z "$pair" ]] && continue + local k="${pair%%=*}" + local v="${pair#*=}" + k=$(echo "$k" | tr '[:upper:]' '[:lower:]' | xargs) + v=$(echo "$v" | xargs) + if [[ -z "$k" || "$k" == "$pair" ]]; then + echo "Error: invalid weight entry (use arch=1.5): $pair" >&2 + exit 1 + fi + obj=$(echo "$obj" | jq --arg k "$k" --argjson v "$v" '. + {($k): $v}') + done + echo "$obj" +} + +cmd_whatif() { + local weights_str="" + local threshold="0.5" + local json_out="false" + while [[ $# -gt 0 ]]; do + case "$1" in + --weights) + weights_str="$2" + shift 2 + ;; + --threshold) + threshold="$2" + shift 2 + ;; + --json) + json_out="true" + shift + ;; + *) + echo "Unknown option: $1" >&2 + exit 1 + ;; + esac + done + + resolve_event_file + local weights_json + weights_json="$(parse_weights_to_json "$weights_str")" + + local result + result=$(jq -s --argjson weights "$weights_json" --argjson thr "$threshold" --arg run_id "$RUN_ID" ' + def strict($v): + if $v == null then 1 + else ($v | ascii_downcase) as $lv + | if ($lv == "approved" or $lv == "approve") then 0 else 1 end + end; + + def norm_key: ascii_downcase; + + ([.[] | select(.type == "review.verdict" and .phase == "check")] + | sort_by(.seq) + | reduce .[] as $e ({}; . + { (($e.data.archetype // $e.agent // "unknown") | norm_key): $e }) + ) as $last | + + ($last | keys) as $keys | + if ($keys | length) == 0 then + { + run_id: $run_id, + error: "no check-phase review.verdict events; nothing to simulate" + } + else + [ $keys[] as $k | $last[$k] as $ev | + ($weights[($k | norm_key)] // 1.0) as $w + | strict($ev.data.verdict) as $s + | { + archetype: ($ev.data.archetype // $ev.agent // $k), + verdict: ($ev.data.verdict // "unknown"), + weight: $w, + strict: $s, + weighted_contrib: ($w * $s) + } + ] as $rows | + ($rows | map(.weighted_contrib) | add) as $num | + ($rows | map(.weight) | add) as $den | + (if $den > 0 then ($num / $den) else 0 end) as $ratio | + (if ($rows | map(.strict) | max) == 1 then "BLOCK" else "SHIP" end) as $strict_out | + (if $ratio >= $thr then "BLOCK" else "SHIP" end) as $replay_out | + { + run_id: $run_id, + threshold: $thr, + weights_used: $weights, + strict_any_veto: { + outcome: $strict_out, + description: "BLOCK if any reviewer verdict is not approved" + }, + weighted_replay: { + weighted_strictness: ($ratio * 1000 | round / 1000), + outcome: $replay_out, + description: ("BLOCK if weighted strictness >= " + ($thr | tostring)) + }, + reviewers: $rows + } + end + ' "$EVENT_FILE") + + if [[ "$json_out" == "true" ]]; then + echo "$result" + else + echo "$result" | jq -r ' + if .error then "Error: \(.error)" else + "# What-if replay — run_id=\(.run_id)\n", + "", + "## Outcomes", + "| Model | Result |", + "|-------|--------|", + "| Original (any non-approve → BLOCK) | \(.strict_any_veto.outcome) |", + "| Weighted replay (threshold=\(.threshold)) | \(.weighted_replay.outcome) |", + "", + "## Weighted strictness", + "\(.weighted_replay.weighted_strictness) (0 = all approved, 1 = all blocking)", + "", + "## Per reviewer", + "| Archetype | Verdict | Weight | Strict | w×strict |", + "|-----------|---------|--------|--------|----------|", + (.reviewers[] | "| \(.archetype) | \(.verdict) | \(.weight) | \(.strict) | \(.weighted_contrib) |"), + "", + (if (.weights_used | length) > 0 then + "## Custom weights applied\n" + (.weights_used | to_entries | map("- \(.key): \(.value)") | join("\n")) + "\n" + else empty end) + end + ' + fi +} + +cmd_compare() { + cmd_timeline + echo "" + cmd_whatif "$@" +} + +case "$COMMAND" in + timeline) cmd_timeline ;; + whatif) cmd_whatif "$@" ;; + compare) cmd_compare "$@" ;; + *) + echo "Unknown command: $COMMAND" >&2 + exit 1 + ;; +esac