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
|
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
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 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
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