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
This commit is contained in:
@@ -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
|
||||
|
||||
48
lib/archeflow-decision.sh
Executable file
48
lib/archeflow-decision.sh
Executable 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.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 <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"
|
||||
@@ -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 <run_id> <phase> <arch> '<input>' '<decision>' <confidence> [parent]
|
||||
#
|
||||
# Parent seqs: comma-separated seq numbers of causal parent events (DAG).
|
||||
# "2" → single parent [2]
|
||||
|
||||
228
lib/archeflow-replay.sh
Executable file
228
lib/archeflow-replay.sh
Executable 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
|
||||
Reference in New Issue
Block a user