2 Commits

Author SHA1 Message Date
506143d613 feat: add decision.point event, decision logger, and run replay 2026-04-06 21:33:42 +02:00
607a53f1bf 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
2026-04-06 21:33:36 +02:00
4 changed files with 283 additions and 1 deletions

View File

@@ -87,6 +87,9 @@ EVENTS_PARSED=$(jq -r '
elif .type == "agent.complete" then elif .type == "agent.complete" then
(.data.archetype // .agent // "unknown") + " (" + .phase + ")" + (.data.archetype // .agent // "unknown") + " (" + .phase + ")" +
(if (.data.tokens // 0) > 0 then " [" + (.data.tokens | tostring) + " tok]" else "" end) (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 elif .type == "decision" then
"decision: " + (.data.what // "unknown") + " → " + (.data.chosen // "unknown") "decision: " + (.data.what // "unknown") + " → " + (.data.chosen // "unknown")
elif .type == "phase.transition" then elif .type == "phase.transition" then
@@ -209,7 +212,7 @@ render_node() {
local colored_label local colored_label
case "$type" in case "$type" in
phase.transition) colored_label="${C_TRANS}${label}${C_RESET}" ;; 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}" ;; review.verdict) colored_label="${C_VERDICT}${label}${C_RESET}" ;;
*) colored_label="${pc}${label}${C_RESET}" ;; *) colored_label="${pc}${label}${C_RESET}" ;;
esac esac

48
lib/archeflow-decision.sh Executable file
View File

@@ -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/<run_id>.jsonl with:
# phase, archetype (agent + data.archetype), input, decision, confidence, ts (via event layer)
#
# Usage:
# ./lib/archeflow-decision.sh <run_id> <phase> <archetype> '<input>' '<decision>' <confidence> [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.01.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 <run_id> <phase> <archetype> '<input>' '<decision>' <confidence> [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"

View File

@@ -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 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 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 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 <run_id> <phase> <arch> '<input>' '<decision>' <confidence> [parent]
# #
# Parent seqs: comma-separated seq numbers of causal parent events (DAG). # Parent seqs: comma-separated seq numbers of causal parent events (DAG).
# "2" → single parent [2] # "2" → single parent [2]

228
lib/archeflow-replay.sh Executable file
View File

@@ -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 <run_id>
# archeflow-replay.sh whatif <run_id> [--weights arch=w,arch2=w2] [--threshold 0.5] [--json]
# archeflow-replay.sh compare <run_id> [--weights ...] [--threshold ...] [--json]
#
# Events file: .archeflow/events/<run_id>.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} <run_id> [options]" >&2
echo "" >&2
echo " timeline <run_id> Decision timeline (decision.point + review.verdict)" >&2
echo " whatif <run_id> [--weights k=v,...] [--threshold 0.5] [--json]" >&2
echo " compare <run_id> (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