@manhquy/flow-skill 0.7.2 → 0.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +12 -5
- package/README_VN.md +12 -5
- package/package.json +1 -1
- package/skills/flow/SKILL.md +3 -2
- package/skills/flow/eval/fixtures/f06_injected/flow/05-contract.md +28 -0
- package/skills/flow/eval/fixtures/f06_sound/flow/05-contract.md +24 -0
- package/skills/flow/eval/manifest.tsv +2 -0
- package/skills/flow/harness/flow_harness.py +153 -1
- package/skills/flow/manifest-hashes.json +3 -0
- package/skills/flow/references/command-dispatch.md +8 -7
- package/skills/flow/references/concierge.md +5 -3
- package/skills/flow/references/gate-consistency.md +12 -0
- package/skills/flow/references/gate-eval.md +44 -10
- package/skills/flow/references/host-agnostic-parallel.md +79 -0
- package/skills/flow/runner/attestations.sh +1 -1
- package/skills/flow/runner/flow.sh +599 -76
- package/skills-manifest.json +4 -2
|
@@ -56,7 +56,8 @@ DEBT_FILE="$ROOT/DEBT.md"
|
|
|
56
56
|
PROJECT_TYPE_FILE="$ROOT/PROJECT_TYPE"
|
|
57
57
|
SKIPPED_FILE="$ROOT/flow/.skipped"
|
|
58
58
|
LOCK_FILE="$ROOT/flow/.lock"
|
|
59
|
-
LOCK_DIR="$ROOT/flow/.lock.d" # atomic
|
|
59
|
+
LOCK_DIR="$ROOT/flow/.lock.d" # atomic claim dir (W5 guard); winner then writes $LOCK_FILE metadata
|
|
60
|
+
LOCK_CLAIM="$ROOT/flow/.lock.claim" # atomic set -C / O_EXCL claim guard
|
|
60
61
|
HARNESS_PY="$SCRIPT_DIR/../harness/flow_harness.py"
|
|
61
62
|
|
|
62
63
|
# Attestations library (risk, receipts, auto policy) — optional-soft: missing file is fatal for v0.28+.
|
|
@@ -643,38 +644,52 @@ _write_lock() { # $1 = cmd label
|
|
|
643
644
|
> "$LOCK_FILE" 2>/dev/null || true
|
|
644
645
|
}
|
|
645
646
|
|
|
647
|
+
# Atomic test-and-set claim via bash noclobber (set -C / O_EXCL creat).
|
|
648
|
+
# Guarantees atomic mutual exclusion across all filesystems and bash environments,
|
|
649
|
+
# including systems where external mkdir binaries suffer from race bugs (uutils 0.10.0).
|
|
650
|
+
# Winner also creates LOCK_DIR for backward compatibility with inspection/mtime tooling.
|
|
651
|
+
_claim_lock() {
|
|
652
|
+
( set -C; : > "$LOCK_CLAIM" ) 2>/dev/null || return 1
|
|
653
|
+
mkdir "$LOCK_DIR" 2>/dev/null || true
|
|
654
|
+
return 0
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
_release_claim() {
|
|
658
|
+
rm -f "$LOCK_CLAIM" 2>/dev/null || true
|
|
659
|
+
rm -rf "$LOCK_DIR" 2>/dev/null || true
|
|
660
|
+
}
|
|
661
|
+
|
|
646
662
|
# Guard a MUTATING command. 0 = lock taken/refreshed (proceed); 1 = refused (caller aborts).
|
|
647
663
|
lock_acquire() { # $1 = cmd label
|
|
648
664
|
local me age; me="$(flow_lock_owner)"
|
|
649
|
-
# Ensure the lock's PARENT dir exists so the atomic
|
|
665
|
+
# Ensure the lock's PARENT dir exists so the atomic claim test-and-set below can
|
|
650
666
|
# succeed on a brand-new project (flow/ may not exist yet at stage-00 unlock / first run).
|
|
651
|
-
# This creates only the parent; LOCK_DIR itself is still created with plain mkdir (atomic guard).
|
|
652
667
|
mkdir -p "$FLOW_DIR" 2>/dev/null || true
|
|
653
668
|
|
|
654
|
-
# FLOW_FORCE: unconditional takeover; reset the atomic claim
|
|
669
|
+
# FLOW_FORCE: unconditional takeover; reset the atomic claim so the guard is bypassed cleanly.
|
|
655
670
|
if [ -n "${FLOW_FORCE:-}" ]; then
|
|
656
671
|
if _read_lock && [ -n "$LOCK_OWNER" ] && [ "$LOCK_OWNER" != "$me" ]; then
|
|
657
672
|
echo "NOTE: FLOW_FORCE set - taking over a lock held by [$LOCK_OWNER] (cmd '${LOCK_CMD:-?}')."
|
|
658
673
|
fi
|
|
659
|
-
|
|
660
|
-
|
|
674
|
+
_release_claim
|
|
675
|
+
_claim_lock || true
|
|
661
676
|
_write_lock "$1"; return 0
|
|
662
677
|
fi
|
|
663
678
|
|
|
664
679
|
# Same-session refresh: if the lock file names OUR owner, just update the timestamp.
|
|
665
|
-
# The session already owns
|
|
680
|
+
# The session already owns the claim from the original claim; no need to re-claim.
|
|
666
681
|
if _read_lock && [ "$LOCK_OWNER" = "$me" ]; then _write_lock "$1"; return 0; fi
|
|
667
682
|
|
|
668
|
-
# W5 — Atomic claim guard.
|
|
669
|
-
# Only attempt when neither the claim
|
|
670
|
-
# Legacy flat-file (LOCK_FILE without LOCK_DIR): tolerated — fall through to reclaim logic below
|
|
683
|
+
# W5 — Atomic claim guard. set -C / O_EXCL is POSIX test-and-set: exactly one racing caller wins.
|
|
684
|
+
# Only attempt when neither the claim nor a legacy flat-file lock exists (truly free state).
|
|
685
|
+
# Legacy flat-file (LOCK_FILE without LOCK_DIR/LOCK_CLAIM): tolerated — fall through to reclaim logic below
|
|
671
686
|
# so an in-flight upgrade from v0.11 does not strand a live lock.
|
|
672
|
-
if ! [ -d "$LOCK_DIR" ] && ! [ -f "$LOCK_FILE" ]; then
|
|
673
|
-
if
|
|
687
|
+
if ! [ -f "$LOCK_CLAIM" ] && ! [ -d "$LOCK_DIR" ] && ! [ -f "$LOCK_FILE" ]; then
|
|
688
|
+
if _claim_lock; then
|
|
674
689
|
_write_lock "$1"; return 0 # won the race; lock claimed
|
|
675
690
|
fi
|
|
676
|
-
# Lost the race:
|
|
677
|
-
# LOCK_FILE may not be written yet (tiny window); treat
|
|
691
|
+
# Lost the race: claim now owned by the winner. Fall through to BLOCK below.
|
|
692
|
+
# LOCK_FILE may not be written yet (tiny window); treat claim presence as lock-held.
|
|
678
693
|
fi
|
|
679
694
|
|
|
680
695
|
# --- v0.11 FR4 reclaim logic (runs for: race loser, legacy flat-file, any existing lock) ---
|
|
@@ -686,25 +701,25 @@ lock_acquire() { # $1 = cmd label
|
|
|
686
701
|
local myhost; myhost="$(uname -n 2>/dev/null || echo host)"
|
|
687
702
|
if [ -n "$LOCK_PID" ] && [ "$LOCK_HOST" = "$myhost" ] && ! kill -0 "$LOCK_PID" 2>/dev/null; then
|
|
688
703
|
echo "NOTE: reclaiming a flow lock from a dead session [$LOCK_OWNER] (pid $LOCK_PID no longer alive)."
|
|
689
|
-
|
|
690
|
-
# F2: check
|
|
691
|
-
if
|
|
704
|
+
_release_claim
|
|
705
|
+
# F2: check claim atomically — if another reclaimer beat us, do NOT write
|
|
706
|
+
if _claim_lock; then
|
|
692
707
|
_write_lock "$1"; return 0
|
|
693
708
|
fi
|
|
694
|
-
# Lost the
|
|
709
|
+
# Lost the reclaim race; another reclaimer already owns the slot — back off
|
|
695
710
|
echo "BLOCKED: another flow session claimed this project concurrently (concurrent /flow corrupts the plan)."
|
|
696
|
-
echo " (reclaim
|
|
711
|
+
echo " (reclaim claim race lost — another session already took the lock.)"
|
|
697
712
|
return 1
|
|
698
713
|
elif [ "$age" -ge "$FLOW_LOCK_TTL" ]; then
|
|
699
714
|
echo "NOTE: reclaiming a STALE flow lock from [$LOCK_OWNER] (${age}s old >= ${FLOW_LOCK_TTL}s TTL)."
|
|
700
|
-
|
|
701
|
-
# F2: check
|
|
702
|
-
if
|
|
715
|
+
_release_claim
|
|
716
|
+
# F2: check claim atomically — if another reclaimer beat us, do NOT write
|
|
717
|
+
if _claim_lock; then
|
|
703
718
|
_write_lock "$1"; return 0
|
|
704
719
|
fi
|
|
705
|
-
# Lost the
|
|
720
|
+
# Lost the reclaim race; another reclaimer already owns the slot — back off
|
|
706
721
|
echo "BLOCKED: another flow session claimed this project concurrently (concurrent /flow corrupts the plan)."
|
|
707
|
-
echo " (reclaim
|
|
722
|
+
echo " (reclaim claim race lost — another session already took the lock.)"
|
|
708
723
|
return 1
|
|
709
724
|
fi
|
|
710
725
|
if flow_owner_strong "$me" && flow_owner_strong "$LOCK_OWNER"; then # fresh + provably foreign
|
|
@@ -719,29 +734,31 @@ lock_acquire() { # $1 = cmd label
|
|
|
719
734
|
_write_lock "$1"; return 0
|
|
720
735
|
fi
|
|
721
736
|
|
|
722
|
-
# LOCK_DIR exists but LOCK_FILE not yet written: either a race loser in the tiny window
|
|
723
|
-
# after
|
|
724
|
-
# Distinguish via mtime: if
|
|
737
|
+
# LOCK_CLAIM or LOCK_DIR exists but LOCK_FILE not yet written: either a race loser in the tiny window
|
|
738
|
+
# after claim loss, or a crashed winner (F1: process died after claim but before _write_lock).
|
|
739
|
+
# Distinguish via mtime: if the claim target is older than FLOW_LOCK_TTL it is a crashed stale claim
|
|
725
740
|
# and safe to reclaim; if fresh, treat as live mid-claim and BLOCK.
|
|
726
|
-
if [ -d "$LOCK_DIR" ]; then
|
|
741
|
+
if [ -f "$LOCK_CLAIM" ] || [ -d "$LOCK_DIR" ]; then
|
|
727
742
|
local ttl_min; ttl_min=$(( (FLOW_LOCK_TTL + 59) / 60 )) # ceiling division to minutes
|
|
728
|
-
|
|
743
|
+
local claim_target="$LOCK_DIR"
|
|
744
|
+
[ -d "$claim_target" ] || claim_target="$LOCK_CLAIM"
|
|
745
|
+
if find "$claim_target" -maxdepth 0 -mmin +"$ttl_min" 2>/dev/null | grep -q .; then
|
|
729
746
|
echo "NOTE: reclaiming a stale crashed claim (LOCK_DIR old, no LOCK_FILE)."
|
|
730
|
-
|
|
747
|
+
_release_claim
|
|
731
748
|
# F2: atomic re-claim; if another reclaimer beat us, don't write
|
|
732
|
-
if
|
|
749
|
+
if _claim_lock; then
|
|
733
750
|
_write_lock "$1"; return 0
|
|
734
751
|
fi
|
|
735
|
-
# Lost re-
|
|
752
|
+
# Lost re-claim race; fall through to BLOCKED below
|
|
736
753
|
fi
|
|
737
754
|
echo "BLOCKED: another flow session is claiming this project (concurrent /flow corrupts the plan)."
|
|
738
755
|
echo " (lock dir exists; the other session is mid-claim. If it is truly gone: '/flow unlock'.)"
|
|
739
756
|
return 1
|
|
740
757
|
fi
|
|
741
758
|
|
|
742
|
-
# No
|
|
743
|
-
# F2: check
|
|
744
|
-
if
|
|
759
|
+
# No claim and no LOCK_FILE: truly free state reached after reclaim or first run.
|
|
760
|
+
# F2: check claim exit status — if another caller beats us here, refuse rather than double-write
|
|
761
|
+
if _claim_lock; then
|
|
745
762
|
_write_lock "$1"; return 0 # won the clean claim
|
|
746
763
|
fi
|
|
747
764
|
# Another caller won; fall through — will hit the BLOCKED path on next iteration.
|
|
@@ -886,6 +903,14 @@ _stage_dwell() { # $1 = current stage name -> "Xm/Xh/Xd" from genuine entry, or
|
|
|
886
903
|
}
|
|
887
904
|
|
|
888
905
|
cmd_status() {
|
|
906
|
+
if [ "${1:-}" = "--json" ]; then
|
|
907
|
+
if [ -n "${2:-}" ]; then usage; exit 2; fi
|
|
908
|
+
_emit_context_json
|
|
909
|
+
return 0
|
|
910
|
+
elif [ -n "${1:-}" ]; then
|
|
911
|
+
usage
|
|
912
|
+
exit 2
|
|
913
|
+
fi
|
|
889
914
|
local idx; idx="$(current_stage_idx)"
|
|
890
915
|
echo "flow status"
|
|
891
916
|
echo " project: $ROOT"
|
|
@@ -990,6 +1015,18 @@ _resume_valid_lines() {
|
|
|
990
1015
|
}
|
|
991
1016
|
|
|
992
1017
|
cmd_resume() {
|
|
1018
|
+
local budget=0
|
|
1019
|
+
if [ "${1:-}" = "--json" ]; then
|
|
1020
|
+
if [ -n "${2:-}" ]; then usage; exit 2; fi
|
|
1021
|
+
_emit_context_json
|
|
1022
|
+
return 0
|
|
1023
|
+
elif [ "${1:-}" = "--budget" ]; then
|
|
1024
|
+
if [ -n "${2:-}" ]; then usage; exit 2; fi
|
|
1025
|
+
budget=1
|
|
1026
|
+
elif [ -n "${1:-}" ]; then
|
|
1027
|
+
usage
|
|
1028
|
+
exit 2
|
|
1029
|
+
fi
|
|
993
1030
|
local idx; idx="$(current_stage_idx)"
|
|
994
1031
|
if [ "$idx" -lt 0 ]; then
|
|
995
1032
|
_na="$(_next_action)"
|
|
@@ -1086,7 +1123,12 @@ cmd_resume() {
|
|
|
1086
1123
|
fi
|
|
1087
1124
|
|
|
1088
1125
|
# ---- IN FLIGHT ----
|
|
1089
|
-
local infl_out
|
|
1126
|
+
local infl_out
|
|
1127
|
+
if [ "$budget" -eq 1 ]; then
|
|
1128
|
+
infl_out="$(inflight_display 5)"
|
|
1129
|
+
else
|
|
1130
|
+
infl_out="$(inflight_display)"
|
|
1131
|
+
fi
|
|
1090
1132
|
if [ -n "$infl_out" ]; then
|
|
1091
1133
|
echo "in flight:"
|
|
1092
1134
|
printf '%s\n' "$infl_out"
|
|
@@ -1235,15 +1277,24 @@ _elapsed_human() { # $1=start-epoch -> "Xm"/"Xh"/"Xd" (integer; GNU/BSD-portable
|
|
|
1235
1277
|
}
|
|
1236
1278
|
|
|
1237
1279
|
inflight_display() { # emit a line per todo card still marked in flight (stale ids self-skip)
|
|
1238
|
-
local
|
|
1280
|
+
local max="${1:-0}"
|
|
1281
|
+
local infl id ts f count=0 hidden=0; infl="$(_inflight_file)"
|
|
1239
1282
|
[ -f "$infl" ] || return 0
|
|
1240
1283
|
while read -r id ts; do
|
|
1241
1284
|
[ -n "$id" ] || continue
|
|
1242
1285
|
f="$(resolve_card_file "$id" 2>/dev/null)"
|
|
1243
1286
|
[ -n "$f" ] && [ -f "$f" ] || continue # card gone -> stale, skip
|
|
1244
1287
|
[ "$(card_status "$f")" = "todo" ] || continue # only todo cards are "in flight"
|
|
1245
|
-
|
|
1288
|
+
count=$((count + 1))
|
|
1289
|
+
if [ "$max" -gt 0 ] && [ "$count" -gt "$max" ]; then
|
|
1290
|
+
hidden=$((hidden + 1))
|
|
1291
|
+
else
|
|
1292
|
+
printf ' %s (in flight, started %s ago)\n' "$id" "$(_elapsed_human "$ts")"
|
|
1293
|
+
fi
|
|
1246
1294
|
done < "$infl"
|
|
1295
|
+
if [ "$hidden" -gt 0 ]; then
|
|
1296
|
+
printf " +%s more in flight - see 'flow card'\n" "$hidden"
|
|
1297
|
+
fi
|
|
1247
1298
|
}
|
|
1248
1299
|
|
|
1249
1300
|
# Single source of truth for "what should I do next" - used by BOTH `status` (Phase 3) and
|
|
@@ -1317,7 +1368,8 @@ _next_action() {
|
|
|
1317
1368
|
echo "run '/flow check' then ship per stage 09"
|
|
1318
1369
|
}
|
|
1319
1370
|
|
|
1320
|
-
# Closed advisory enum from _next_action prose only. Hosts must not auto-exec
|
|
1371
|
+
# Closed advisory enum from _next_action prose only. Hosts must not auto-exec
|
|
1372
|
+
# next/card/card-start/check/fix-gate/auto/skip.
|
|
1321
1373
|
_next_verb_from() { # $1 = _next_action text
|
|
1322
1374
|
local a="$1"
|
|
1323
1375
|
case "$a" in
|
|
@@ -1338,6 +1390,54 @@ _emit_next() {
|
|
|
1338
1390
|
echo "NEXT_VERB=$(_next_verb_from "$a")"
|
|
1339
1391
|
}
|
|
1340
1392
|
|
|
1393
|
+
# PTC-aligned load hint for JSON status/resume. Display-only — never a capability token.
|
|
1394
|
+
# Matches SKILL.md load table: card/build -> law/CLAUDE.md; fix-gate/next/none stay empty
|
|
1395
|
+
# (mechanical FAIL is the blocked stage file; next PASS few-shots are after unlock).
|
|
1396
|
+
_json_load_for_verb() { # $1 = next_verb
|
|
1397
|
+
case "${1:-}" in
|
|
1398
|
+
card|card-start|check) echo "law/CLAUDE.md" ;;
|
|
1399
|
+
*) echo "" ;;
|
|
1400
|
+
esac
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
# Closed flow_context/v1 printer. Scalars through _json_str. scan_gate never piped.
|
|
1404
|
+
# Hosts MUST NOT dispatch next_verb (next/card/card-start/check/fix-gate/auto/skip).
|
|
1405
|
+
_emit_context_json() {
|
|
1406
|
+
local idx cur f stage="" gate="none" next_verb card="" dwell="" load na infl id ts
|
|
1407
|
+
idx="$(current_stage_idx)"
|
|
1408
|
+
if [ "$idx" -ge 0 ]; then
|
|
1409
|
+
cur="$(stage_name_at "$idx")"
|
|
1410
|
+
stage="$cur"
|
|
1411
|
+
f="$FLOW_DIR/$cur.md"
|
|
1412
|
+
if [ -f "$f" ]; then
|
|
1413
|
+
if scan_gate "$f" >/dev/null 2>&1; then
|
|
1414
|
+
gate="PASS"
|
|
1415
|
+
else
|
|
1416
|
+
gate="BLOCKED"
|
|
1417
|
+
fi
|
|
1418
|
+
fi
|
|
1419
|
+
dwell="$(_stage_dwell "$cur")"
|
|
1420
|
+
fi
|
|
1421
|
+
na="$(_next_action)"
|
|
1422
|
+
next_verb="$(_next_verb_from "$na")"
|
|
1423
|
+
load="$(_json_load_for_verb "$next_verb")"
|
|
1424
|
+
infl="$(_inflight_file)"
|
|
1425
|
+
if [ -f "$infl" ]; then
|
|
1426
|
+
while read -r id ts; do
|
|
1427
|
+
[ -n "$id" ] || continue
|
|
1428
|
+
f="$(resolve_card_file "$id" 2>/dev/null)"
|
|
1429
|
+
[ -n "$f" ] && [ -f "$f" ] || continue
|
|
1430
|
+
[ "$(card_status "$f")" = "todo" ] || continue
|
|
1431
|
+
card="$id"
|
|
1432
|
+
break
|
|
1433
|
+
done < "$infl"
|
|
1434
|
+
fi
|
|
1435
|
+
printf '{"v":"%s","stage":"%s","gate":"%s","next_verb":"%s","card":"%s","dwell":"%s","load":"%s"}\n' \
|
|
1436
|
+
"$(_json_str "flow_context/v1")" "$(_json_str "$stage")" "$(_json_str "$gate")" \
|
|
1437
|
+
"$(_json_str "$next_verb")" "$(_json_str "$card")" "$(_json_str "$dwell")" "$(_json_str "$load")"
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
|
|
1341
1441
|
_set_card_status() { # $1=file $2=value -> rewrite the ^status: line (portable substitute; temp+mv)
|
|
1342
1442
|
local f="$1" v="$2"
|
|
1343
1443
|
sed "s/^status:.*/status: $v/" "$f" > "$f.tmp" 2>/dev/null || { rm -f "$f.tmp"; return 1; }
|
|
@@ -2051,10 +2151,10 @@ cmd_recall() {
|
|
|
2051
2151
|
}
|
|
2052
2152
|
|
|
2053
2153
|
cmd_unlock() {
|
|
2054
|
-
if [ ! -f "$LOCK_FILE" ] && ! [ -d "$LOCK_DIR" ]; then echo "no flow lock to clear ($LOCK_FILE)"; return 0; fi
|
|
2154
|
+
if [ ! -f "$LOCK_FILE" ] && ! [ -d "$LOCK_DIR" ] && ! [ -f "$LOCK_CLAIM" ]; then echo "no flow lock to clear ($LOCK_FILE)"; return 0; fi
|
|
2055
2155
|
_read_lock
|
|
2056
2156
|
rm -f "$LOCK_FILE" 2>/dev/null || true
|
|
2057
|
-
|
|
2157
|
+
_release_claim
|
|
2058
2158
|
echo "PASS: cleared flow lock (was [$LOCK_OWNER], cmd '${LOCK_CMD:-?}')."
|
|
2059
2159
|
return 0
|
|
2060
2160
|
}
|
|
@@ -2314,8 +2414,131 @@ prd_declares_fr() { # true if the PRD uses the FRn traceability convention (driv
|
|
|
2314
2414
|
# keep it loose so the tip reliably shows in the common case. Do not "tighten" to match consistency.
|
|
2315
2415
|
[ -f "$FLOW_DIR/03-prd.md" ] && grep -qE 'FR[0-9]+' "$FLOW_DIR/03-prd.md" 2>/dev/null
|
|
2316
2416
|
}
|
|
2417
|
+
_consistency_contract() {
|
|
2418
|
+
local contract="$FLOW_DIR/05-contract.md"
|
|
2419
|
+
if [ ! -f "$contract" ]; then
|
|
2420
|
+
echo "consistency --contract: no flow/05-contract.md found - skipped."
|
|
2421
|
+
return 0
|
|
2422
|
+
fi
|
|
2423
|
+
|
|
2424
|
+
local td; td="$(mktemp -d)"; _register_td "$td"
|
|
2425
|
+
trap 'rm -rf "${td:-}" 2>/dev/null' RETURN
|
|
2426
|
+
|
|
2427
|
+
# 1. Extract endpoint rows from 05-contract.md
|
|
2428
|
+
# Grounded in _templates/05-contract.md: table columns "| Method/Interface | Path/Name | ..."
|
|
2429
|
+
# Tolerates swapped Method/Path column order based on header row inspection.
|
|
2430
|
+
awk -F'|' '
|
|
2431
|
+
/^[[:space:]]*\|/ {
|
|
2432
|
+
line = $0
|
|
2433
|
+
gsub(/^[[:space:]]*\||\|[[:space:]]*$/, "", line)
|
|
2434
|
+
if (tolower(line) ~ /method/ && tolower(line) ~ /path/) {
|
|
2435
|
+
n = split(line, cols, "|")
|
|
2436
|
+
for (i = 1; i <= n; i++) {
|
|
2437
|
+
c = tolower(cols[i])
|
|
2438
|
+
gsub(/^[[:space:]]+|[[:space:]]+$/, "", c)
|
|
2439
|
+
if (c ~ /method/) m_col = i
|
|
2440
|
+
if (c ~ /path/) p_col = i
|
|
2441
|
+
}
|
|
2442
|
+
in_table = 1
|
|
2443
|
+
next
|
|
2444
|
+
}
|
|
2445
|
+
if (in_table) {
|
|
2446
|
+
if (line ~ /^[[:space:]]*-+/) next
|
|
2447
|
+
n = split(line, cols, "|")
|
|
2448
|
+
m = cols[m_col]; p = cols[p_col]
|
|
2449
|
+
gsub(/^[[:space:]]+|[[:space:]]+$|[`"]/, "", m)
|
|
2450
|
+
gsub(/^[[:space:]]+|[[:space:]]+$|[`"]/, "", p)
|
|
2451
|
+
if (m ~ /^\[FILL/ || p ~ /^\[FILL/ || m == "" || p == "") next
|
|
2452
|
+
print m "\t" p
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
!/^[[:space:]]*\|/ { in_table = 0 }
|
|
2456
|
+
' "$contract" > "$td/endpoints" 2>/dev/null
|
|
2457
|
+
|
|
2458
|
+
local n_ep
|
|
2459
|
+
n_ep="$(grep -c . "$td/endpoints" 2>/dev/null || echo 0)"
|
|
2460
|
+
if [ "$n_ep" -eq 0 ]; then
|
|
2461
|
+
echo "consistency --contract: no endpoint table found in flow/05-contract.md - skipped."
|
|
2462
|
+
rm -rf "$td" 2>/dev/null
|
|
2463
|
+
return 0
|
|
2464
|
+
fi
|
|
2465
|
+
|
|
2466
|
+
# 2. Extract ## Scope content from all cards/C-*.md
|
|
2467
|
+
: > "$td/cards_scope"
|
|
2468
|
+
if [ -d "$CARDS_DIR" ]; then
|
|
2469
|
+
local cf
|
|
2470
|
+
for cf in "$CARDS_DIR"/C-*.md; do
|
|
2471
|
+
[ -f "$cf" ] || continue
|
|
2472
|
+
awk '/^##[[:space:]]+[Ss]cope/{f=1;next} /^##[[:space:]]/{f=0} f' "$cf" 2>/dev/null
|
|
2473
|
+
done > "$td/cards_scope"
|
|
2474
|
+
fi
|
|
2475
|
+
|
|
2476
|
+
# 3. Match each endpoint path against cards Scope content
|
|
2477
|
+
echo "contract-to-card endpoint coverage audit (flow/05-contract.md vs cards/C-*.md ## Scope)"
|
|
2478
|
+
echo " (matching rule: exact path token in cards/C-*.md '## Scope' - an occurrence counts only when not"
|
|
2479
|
+
echo " preceded/followed by [A-Za-z0-9_] and not followed by [A-Za-z0-9_\/{}:.-]; /api/users/:id does not"
|
|
2480
|
+
echo " cover /api/users; a trailing '?' or end-of-line still counts)"
|
|
2481
|
+
local m p n_cov=0 n_uncov=0
|
|
2482
|
+
: > "$td/uncovered"
|
|
2483
|
+
while IFS="$(printf '\t')" read -r m p; do
|
|
2484
|
+
[ -n "$p" ] || continue
|
|
2485
|
+
# Exact-token boundary via index()/substr only (no regex interpolation): an occurrence of the
|
|
2486
|
+
# path counts as coverage only when it is not preceded by [A-Za-z0-9_] and not followed by
|
|
2487
|
+
# [A-Za-z0-9_\/{}:.-] (end-of-line after counts, so a trailing '?' or EOL is still covered).
|
|
2488
|
+
# awk exit 0 = found (covered); awk exit 1 = not found (uncovered).
|
|
2489
|
+
if awk -v p="$p" '
|
|
2490
|
+
{
|
|
2491
|
+
s = $0; start = 1
|
|
2492
|
+
while ((pos = index(substr(s, start), p)) > 0) {
|
|
2493
|
+
pos += start - 1
|
|
2494
|
+
before = (pos > 1) ? substr(s, pos - 1, 1) : ""
|
|
2495
|
+
after = substr(s, pos + length(p), 1)
|
|
2496
|
+
if (before !~ /[A-Za-z0-9_]/ && (after == "" || after !~ /[A-Za-z0-9_\/{}:.-]/)) { found = 1; exit }
|
|
2497
|
+
start = pos + 1
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
END { exit found ? 0 : 1 }
|
|
2501
|
+
' "$td/cards_scope" 2>/dev/null; then
|
|
2502
|
+
n_cov=$((n_cov + 1))
|
|
2503
|
+
else
|
|
2504
|
+
n_uncov=$((n_uncov + 1))
|
|
2505
|
+
printf ' %s %s\n' "$m" "$p" >> "$td/uncovered"
|
|
2506
|
+
fi
|
|
2507
|
+
done < "$td/endpoints"
|
|
2508
|
+
|
|
2509
|
+
if [ "$n_uncov" -gt 0 ]; then
|
|
2510
|
+
echo " [!] uncovered contract endpoint(s) (no card references path in ## Scope):"
|
|
2511
|
+
cat "$td/uncovered"
|
|
2512
|
+
echo " coverage: $n_ep endpoint(s), $n_cov covered, $n_uncov uncovered."
|
|
2513
|
+
trap - RETURN
|
|
2514
|
+
rm -rf "$td" 2>/dev/null
|
|
2515
|
+
echo "FLAGGED: contract endpoint(s) have no card covering them in ## Scope."
|
|
2516
|
+
return 1
|
|
2517
|
+
fi
|
|
2518
|
+
|
|
2519
|
+
echo " coverage: $n_ep endpoint(s), $n_cov covered, $n_uncov uncovered."
|
|
2520
|
+
echo " PASS: all $n_ep endpoint(s) covered across cards/ scope."
|
|
2521
|
+
trap - RETURN
|
|
2522
|
+
rm -rf "$td" 2>/dev/null
|
|
2523
|
+
return 0
|
|
2524
|
+
}
|
|
2317
2525
|
|
|
2318
2526
|
cmd_consistency() {
|
|
2527
|
+
case "${1:-}" in
|
|
2528
|
+
"") : ;;
|
|
2529
|
+
--contract)
|
|
2530
|
+
if [ $# -gt 1 ]; then
|
|
2531
|
+
echo "usage: /flow consistency [--contract]" >&2
|
|
2532
|
+
return 1
|
|
2533
|
+
fi
|
|
2534
|
+
_consistency_contract
|
|
2535
|
+
return $?
|
|
2536
|
+
;;
|
|
2537
|
+
*)
|
|
2538
|
+
echo "usage: /flow consistency [--contract]" >&2
|
|
2539
|
+
return 1
|
|
2540
|
+
;;
|
|
2541
|
+
esac
|
|
2319
2542
|
# Cross-artifact coverage + contradiction audit - the MECHANICAL complement to the human
|
|
2320
2543
|
# traceability challenges in gate-rules.md (03/05). The other probes each cover one axis:
|
|
2321
2544
|
# coherence=version drift, contract=URL-prefix drift, tokens=design-token drift. THIS axis is
|
|
@@ -2326,7 +2549,7 @@ cmd_consistency() {
|
|
|
2326
2549
|
# Traceability anchors (see _templates): PRD 'Features' entries tagged 'FRn:' (functional
|
|
2327
2550
|
# requirement id); cards declare 'implements: FR1, FR2' (or 'infra'/'none' for non-feature
|
|
2328
2551
|
# cards); 05-contract 'Feature -> interface map' references each 'FRn'.
|
|
2329
|
-
local prd="$FLOW_DIR/03-prd.md" contract="$FLOW_DIR/05-contract.md"
|
|
2552
|
+
local prd="$FLOW_DIR/03-prd.md" contract="$FLOW_DIR/05-contract.md" td=""
|
|
2330
2553
|
if [ ! -f "$prd" ]; then echo "consistency: no flow/03-prd.md yet - planning incomplete (skipped)."; return 0; fi
|
|
2331
2554
|
local td; td="$(mktemp -d)"; _register_td "$td"
|
|
2332
2555
|
trap 'rm -rf "$td" 2>/dev/null' RETURN
|
|
@@ -2418,16 +2641,30 @@ cmd_consistency() {
|
|
|
2418
2641
|
|
|
2419
2642
|
cmd_promote() {
|
|
2420
2643
|
# cross-project knowledge tier: copy a playbook/lesson into ~/.claude/flow/playbooks so its
|
|
2421
|
-
# lessons travel to every project (surfaced by '/flow recall' everywhere)
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2644
|
+
# lessons travel to every project (surfaced by '/flow recall' everywhere), or suggest
|
|
2645
|
+
# draft playbooks from repeated harness patterns via --suggest.
|
|
2646
|
+
case "${1:-}" in
|
|
2647
|
+
--suggest)
|
|
2648
|
+
if ! harness_available; then
|
|
2649
|
+
echo "promote --suggest: durable layer unavailable (python or harness missing, or FLOW_HARNESS_DISABLE set)."
|
|
2650
|
+
return 0
|
|
2651
|
+
fi
|
|
2652
|
+
local py; py="$(_python)"
|
|
2653
|
+
FLOW_PROJECT_ROOT="$ROOT" "$py" "$HARNESS_PY" promote-suggest
|
|
2654
|
+
return $?
|
|
2655
|
+
;;
|
|
2656
|
+
*)
|
|
2657
|
+
local src="${1:-}"
|
|
2658
|
+
if [ -z "$src" ] || [ ! -f "$src" ]; then echo "usage: /flow promote <path-to-playbook.md> | /flow promote --suggest"; return 1; fi
|
|
2659
|
+
mkdir -p "$GLOBAL_KB_DIR" 2>/dev/null || true
|
|
2660
|
+
if ! cp "$src" "$GLOBAL_KB_DIR/$(basename "$src")" 2>/dev/null; then
|
|
2661
|
+
echo "FAIL: could not copy into $GLOBAL_KB_DIR"; return 1
|
|
2662
|
+
fi
|
|
2663
|
+
echo "PASS: promoted $(basename "$src") -> $GLOBAL_KB_DIR"
|
|
2664
|
+
echo " '/flow recall' now surfaces it as a GLOBAL PLAYBOOK in every project."
|
|
2665
|
+
return 0
|
|
2666
|
+
;;
|
|
2667
|
+
esac
|
|
2431
2668
|
}
|
|
2432
2669
|
|
|
2433
2670
|
cmd_doctor() {
|
|
@@ -2471,6 +2708,35 @@ cmd_doctor() {
|
|
|
2471
2708
|
esac
|
|
2472
2709
|
fi
|
|
2473
2710
|
echo
|
|
2711
|
+
# Runner integrity (pack-time reference hash). Preferred source is the receipt sync.mjs writes
|
|
2712
|
+
# INSIDE the bundled skill (skills/flow/manifest-hashes.json) - present on real npm installs;
|
|
2713
|
+
# portable-manifest.json only exists in dev checkouts. First existing file wins; no file / no
|
|
2714
|
+
# hashes entry (older installs) => informational skip only.
|
|
2715
|
+
local mf="" _mf
|
|
2716
|
+
for _mf in "$SCRIPT_DIR/../manifest-hashes.json" "$SCRIPT_DIR/../../../portable-manifest.json" "$SCRIPT_DIR/../../portable-manifest.json"; do
|
|
2717
|
+
if [ -f "$_mf" ]; then mf="$_mf"; break; fi
|
|
2718
|
+
done
|
|
2719
|
+
local want=""
|
|
2720
|
+
[ -n "$mf" ] && want="$(sed -nE 's/.*"skills\/flow\/runner\/flow\.sh"[[:space:]]*:[[:space:]]*"([0-9a-f]{64})".*/\1/p' "$mf" 2>/dev/null | head -1)"
|
|
2721
|
+
if [ -z "$want" ]; then
|
|
2722
|
+
echo " runner integrity: no reference hash in manifest (older install) - skipped"
|
|
2723
|
+
else
|
|
2724
|
+
local have=""
|
|
2725
|
+
if command -v shasum >/dev/null 2>&1; then
|
|
2726
|
+
have="$(shasum -a 256 "$SCRIPT_DIR/flow.sh" 2>/dev/null | awk '{print $1}')"
|
|
2727
|
+
elif command -v sha256sum >/dev/null 2>&1; then
|
|
2728
|
+
have="$(sha256sum "$SCRIPT_DIR/flow.sh" 2>/dev/null | awk '{print $1}')"
|
|
2729
|
+
fi
|
|
2730
|
+
if [ -z "$have" ]; then
|
|
2731
|
+
echo " runner integrity: no shasum/sha256sum available - skipped"
|
|
2732
|
+
elif [ "$have" = "$want" ]; then
|
|
2733
|
+
echo " runner integrity: ok (${want%"${want#????????}"}...)"
|
|
2734
|
+
else
|
|
2735
|
+
echo " FAIL: runner integrity - flow.sh does not match the installed manifest hash (expected ${want%"${want#????????}"}..., got ${have%"${have#????????}"}...) - the harness may have been modified mid-project"
|
|
2736
|
+
ok=0
|
|
2737
|
+
fi
|
|
2738
|
+
fi
|
|
2739
|
+
echo
|
|
2474
2740
|
if [ "$ok" -eq 1 ]; then
|
|
2475
2741
|
echo "READY. Gate engine works$([ -n "$py" ] && echo '; durable layer on.' || echo '; durable layer off (no python).')"
|
|
2476
2742
|
return 0
|
|
@@ -3070,16 +3336,96 @@ cmd_converge() { # [--file <payload>] append-only remainder cards from a flow-c
|
|
|
3070
3336
|
echo " These are remainder stubs with [FILL] Verify/Done-evidence - build them via /flow ready + /flow check."
|
|
3071
3337
|
return 0
|
|
3072
3338
|
}
|
|
3339
|
+
_ws_port() { # <C-NNN>
|
|
3340
|
+
local card="${1:-}"
|
|
3341
|
+
if [ $# -ne 1 ]; then
|
|
3342
|
+
echo "usage: /flow workspace port <C-NNN> (e.g. C-012)" >&2
|
|
3343
|
+
return 2
|
|
3344
|
+
fi
|
|
3345
|
+
case "$card" in
|
|
3346
|
+
C-[0-9]*)
|
|
3347
|
+
local num="${card#C-}"
|
|
3348
|
+
case "$num" in
|
|
3349
|
+
''|*[!0-9]*)
|
|
3350
|
+
echo "usage: /flow workspace port <C-NNN> (e.g. C-012)" >&2
|
|
3351
|
+
return 2
|
|
3352
|
+
;;
|
|
3353
|
+
esac
|
|
3354
|
+
;;
|
|
3355
|
+
*)
|
|
3356
|
+
echo "usage: /flow workspace port <C-NNN> (e.g. C-012)" >&2
|
|
3357
|
+
return 2
|
|
3358
|
+
;;
|
|
3359
|
+
esac
|
|
3360
|
+
|
|
3361
|
+
local rec="" r c wt offset
|
|
3362
|
+
while IFS= read -r r; do
|
|
3363
|
+
[ -n "$r" ] || continue
|
|
3364
|
+
c="$(_ws_get "$r" card_id)"
|
|
3365
|
+
if [ "$c" = "$card" ]; then
|
|
3366
|
+
rec="$r"
|
|
3367
|
+
break
|
|
3368
|
+
fi
|
|
3369
|
+
done <<EOF
|
|
3370
|
+
$(_ws_active_records)
|
|
3371
|
+
EOF
|
|
3372
|
+
|
|
3373
|
+
if [ -z "$rec" ]; then
|
|
3374
|
+
echo "workspace port: FAIL - card '$card' has no registered workspace (run '/flow workspace add <branch> --card $card' first)" >&2
|
|
3375
|
+
return 2
|
|
3376
|
+
fi
|
|
3377
|
+
|
|
3378
|
+
wt="$(_ws_get "$rec" worktree_path)"
|
|
3379
|
+
if [ -z "$wt" ] || [ ! -d "$wt" ]; then
|
|
3380
|
+
echo "workspace port: FAIL - workspace directory for '$card' not found at '${wt:-<empty>}'" >&2
|
|
3381
|
+
return 2
|
|
3382
|
+
fi
|
|
3383
|
+
|
|
3384
|
+
offset="$(_ws_get_num "$rec" port_offset)"
|
|
3385
|
+
if [ -z "$offset" ]; then
|
|
3386
|
+
echo "workspace port: FAIL - workspace record for '$card' has no port_offset" >&2
|
|
3387
|
+
return 2
|
|
3388
|
+
fi
|
|
3389
|
+
|
|
3390
|
+
local baseport="${FLOW_WORKSPACE_BASEPORT:-3000}"
|
|
3391
|
+
case "$baseport" in
|
|
3392
|
+
''|*[!0-9]*)
|
|
3393
|
+
baseport=3000
|
|
3394
|
+
;;
|
|
3395
|
+
*)
|
|
3396
|
+
if [ "$baseport" -lt 1 ] || [ "$baseport" -gt 65535 ]; then
|
|
3397
|
+
echo "workspace port: FLOW_WORKSPACE_BASEPORT=$baseport out of range (1..65535)" >&2
|
|
3398
|
+
return 2
|
|
3399
|
+
fi
|
|
3400
|
+
;;
|
|
3401
|
+
esac
|
|
3402
|
+
|
|
3403
|
+
local raw_port port
|
|
3404
|
+
raw_port=$(( baseport + offset ))
|
|
3405
|
+
if [ "$raw_port" -gt 65535 ]; then
|
|
3406
|
+
echo "workspace port: FAIL - port range exhausted for '$card' (baseport $baseport + offset $offset = $raw_port > 65535); lower FLOW_WORKSPACE_BASEPORT" >&2
|
|
3407
|
+
return 2
|
|
3408
|
+
fi
|
|
3409
|
+
port="$raw_port"
|
|
3410
|
+
|
|
3411
|
+
local env_file="$wt/.env.flow.local"
|
|
3412
|
+
printf 'PORT=%s\nFLOW_CARD=%s\n' "$port" "$card" > "$env_file" || {
|
|
3413
|
+
echo "workspace port: FAIL - cannot write to '$env_file'" >&2
|
|
3414
|
+
return 1
|
|
3415
|
+
}
|
|
3416
|
+
echo "$card -> port $port (written to $env_file)"
|
|
3417
|
+
return 0
|
|
3418
|
+
}
|
|
3073
3419
|
|
|
3074
3420
|
cmd_workspace() {
|
|
3075
3421
|
local sub="${1:-}"; shift 2>/dev/null || true
|
|
3076
3422
|
case "$sub" in
|
|
3077
|
-
add|list|enter|remove|check|doctor) : ;;
|
|
3078
|
-
*) echo "usage: /flow workspace add|list|enter|remove|check|doctor"; return 1 ;;
|
|
3423
|
+
add|list|enter|remove|check|doctor|port) : ;;
|
|
3424
|
+
*) echo "usage: /flow workspace add|list|enter|remove|check|doctor|port"; return 1 ;;
|
|
3079
3425
|
esac
|
|
3080
3426
|
if ! command -v git >/dev/null 2>&1; then
|
|
3081
3427
|
echo "workspace: git not found - worktree isolation unavailable (serial builds still work)."
|
|
3082
|
-
case "$sub" in add|remove) return 1 ;; *) return 0 ;; esac
|
|
3428
|
+
case "$sub" in add|remove) return 1 ;; port) return 2 ;; *) return 0 ;; esac
|
|
3083
3429
|
fi
|
|
3084
3430
|
case "$sub" in
|
|
3085
3431
|
add) _ws_add "$@" ;;
|
|
@@ -3088,6 +3434,7 @@ cmd_workspace() {
|
|
|
3088
3434
|
remove) _ws_remove "$@" ;;
|
|
3089
3435
|
check) _ws_check "$@" ;;
|
|
3090
3436
|
doctor) _ws_doctor ;;
|
|
3437
|
+
port) _ws_port "$@" ;;
|
|
3091
3438
|
esac
|
|
3092
3439
|
}
|
|
3093
3440
|
|
|
@@ -3317,22 +3664,24 @@ _eval_heading_pattern() {
|
|
|
3317
3664
|
case "$1" in
|
|
3318
3665
|
01) echo '^## Stage 01' ;;
|
|
3319
3666
|
02) echo '^## Stage 02' ;;
|
|
3667
|
+
05) echo '^## Stage 05' ;;
|
|
3320
3668
|
card) echo '^## Card gate' ;;
|
|
3321
3669
|
*) return 1 ;;
|
|
3322
3670
|
esac
|
|
3323
3671
|
}
|
|
3324
3672
|
|
|
3325
3673
|
# Extract one stage's ritual text from the mapped file (01→gate-01.md, 02→gate-02.md,
|
|
3326
|
-
# card→gate-card.md): from its heading line up to (not including) the next
|
|
3327
|
-
# heading, or EOF. Prints nothing on a bad stage/missing file - caller MUST assert
|
|
3674
|
+
# 05→gate-05.md, card→gate-card.md): from its heading line up to (not including) the next
|
|
3675
|
+
# '## ' heading, or EOF. Prints nothing on a bad stage/missing file - caller MUST assert
|
|
3328
3676
|
# non-empty before any billable call (a silent empty extraction would judge against
|
|
3329
3677
|
# no rule at all).
|
|
3330
|
-
_eval_extract_section() { # $1 = stage (01|02|card)
|
|
3678
|
+
_eval_extract_section() { # $1 = stage (01|02|05|card)
|
|
3331
3679
|
local pat src
|
|
3332
3680
|
pat="$(_eval_heading_pattern "$1")" || return 1
|
|
3333
3681
|
case "$1" in
|
|
3334
3682
|
01) src="$SCRIPT_DIR/../references/gate-01.md" ;;
|
|
3335
3683
|
02) src="$SCRIPT_DIR/../references/gate-02.md" ;;
|
|
3684
|
+
05) src="$SCRIPT_DIR/../references/gate-05.md" ;;
|
|
3336
3685
|
card) src="$SCRIPT_DIR/../references/gate-card.md" ;;
|
|
3337
3686
|
*) return 1 ;;
|
|
3338
3687
|
esac
|
|
@@ -3402,8 +3751,9 @@ _eval_build_prompt() { # $1=outfile $2=stage $3=artifact-file $4=nonce
|
|
|
3402
3751
|
local outfile="$1" stage="$2" artifact="$3" nonce="$4" section shared
|
|
3403
3752
|
section="$(_eval_extract_section "$stage")" || return 1
|
|
3404
3753
|
[ -n "$section" ] || return 1
|
|
3405
|
-
# Shared OD/authority lives in 02/03/05 challenges. Extract maps 01/02/card
|
|
3406
|
-
#
|
|
3754
|
+
# Shared OD/authority lives in 02/03/05 challenges. Extract maps 01/02/05/card;
|
|
3755
|
+
# append shared onto 02 only. 05 extract may mention gate-shared.md; do not cat it
|
|
3756
|
+
# (shared OD on 05 false-FLAGs f05a — PASS fixture states bearer auth without an ADR).
|
|
3407
3757
|
if [ "$stage" = "02" ]; then
|
|
3408
3758
|
shared="$SCRIPT_DIR/../references/gate-shared.md"
|
|
3409
3759
|
[ -f "$shared" ] && section="$section
|
|
@@ -3679,8 +4029,9 @@ _eval_json_str_field() { # $1=line $2=field-name -> value or empty
|
|
|
3679
4029
|
# List complete run_ids (has both a start header and a done trailer, n_written >= n_expected),
|
|
3680
4030
|
# oldest first. An interrupted batch (Ctrl-C mid-run -> no trailer, per the EXIT-only trap) is
|
|
3681
4031
|
# silently excluded here - that absence IS the completeness signal, not a positive "torn" flag.
|
|
3682
|
-
|
|
3683
|
-
|
|
4032
|
+
_eval_file_complete_run_ids() { # $1=results-file
|
|
4033
|
+
local f="${1:-$EVAL_RESULTS_FILE}"
|
|
4034
|
+
[ -f "$f" ] || return 0
|
|
3684
4035
|
awk '
|
|
3685
4036
|
/"batch":"start"/ {
|
|
3686
4037
|
match($0, /"run_id":"[^"]*"/); rid = substr($0, RSTART+10, RLENGTH-11)
|
|
@@ -3701,7 +4052,11 @@ _eval_complete_run_ids() {
|
|
|
3701
4052
|
if (seen_start[r] && seen_done[r] && (nwritten[r]+0) >= (nexp[r]+0)) print r
|
|
3702
4053
|
}
|
|
3703
4054
|
}
|
|
3704
|
-
' "$
|
|
4055
|
+
' "$f"
|
|
4056
|
+
}
|
|
4057
|
+
|
|
4058
|
+
_eval_complete_run_ids() {
|
|
4059
|
+
_eval_file_complete_run_ids "$EVAL_RESULTS_FILE"
|
|
3705
4060
|
}
|
|
3706
4061
|
|
|
3707
4062
|
# Per-stage scorecard for one batch: hollow flag-rate, sound pass-rate, invalid/unreliable
|
|
@@ -3824,6 +4179,157 @@ _eval_print_drift() {
|
|
|
3824
4179
|
done
|
|
3825
4180
|
}
|
|
3826
4181
|
|
|
4182
|
+
# Extract per-fixture verdict records from a results file.
|
|
4183
|
+
# If complete batches exist, selects the latest complete batch; otherwise falls back
|
|
4184
|
+
# to the latest run_id present in fixture rows, or all fixture rows if no run_id exists.
|
|
4185
|
+
# Deduplicates by fixture id, keeping the latest verdict.
|
|
4186
|
+
_eval_diff_extract_fixtures() { # $1=results-file
|
|
4187
|
+
local file="$1"
|
|
4188
|
+
local c_ids last_id=""
|
|
4189
|
+
c_ids="$(_eval_file_complete_run_ids "$file")"
|
|
4190
|
+
if [ -n "$c_ids" ]; then
|
|
4191
|
+
last_id="$(printf '%s\n' "$c_ids" | tail -1)"
|
|
4192
|
+
else
|
|
4193
|
+
last_id="$(grep '"fixture":' "$file" 2>/dev/null | grep -oE '"run_id":"[^"]*"' | tail -1 | sed -E 's/"run_id":"([^"]*)"/\1/')"
|
|
4194
|
+
fi
|
|
4195
|
+
|
|
4196
|
+
local rows
|
|
4197
|
+
if [ -n "$last_id" ]; then
|
|
4198
|
+
rows="$(grep -F "\"run_id\":\"$last_id\"" "$file" 2>/dev/null | grep '"fixture":')"
|
|
4199
|
+
else
|
|
4200
|
+
rows="$(grep '"fixture":' "$file" 2>/dev/null)"
|
|
4201
|
+
fi
|
|
4202
|
+
[ -z "$rows" ] && return 0
|
|
4203
|
+
|
|
4204
|
+
printf '%s\n' "$rows" | while IFS= read -r line; do
|
|
4205
|
+
[ -z "$line" ] && continue
|
|
4206
|
+
local fid v exp
|
|
4207
|
+
fid="$(_eval_json_str_field "$line" fixture)"
|
|
4208
|
+
v="$(_eval_json_str_field "$line" verdict)"
|
|
4209
|
+
exp="$(_eval_json_str_field "$line" expected)"
|
|
4210
|
+
[ -n "$fid" ] && printf '%s\t%s\t%s\n' "$fid" "${v:-UNKNOWN}" "${exp:--}"
|
|
4211
|
+
done | awk -F'\t' '{ f[$1] = $2; e[$1] = $3 } END { for (k in f) print k "\t" f[k] "\t" e[k] }' | sort -k1,1
|
|
4212
|
+
}
|
|
4213
|
+
|
|
4214
|
+
# Longitudinal regression classifier: compares two eval results files offline.
|
|
4215
|
+
# Per fixture present in both: REGRESSED, IMPROVED, SATURATED, UNCHANGED (correctness-relative).
|
|
4216
|
+
# Per fixture only in old: removed. Only in new: added.
|
|
4217
|
+
# Unknown expected: unknown (does not count toward regressions).
|
|
4218
|
+
# Exit 1 if any REGRESSED, exit 2 on missing/unreadable or empty file, else 0.
|
|
4219
|
+
_eval_diff() { # $1=old_results $2=new_results
|
|
4220
|
+
local old_file="$1" new_file="$2"
|
|
4221
|
+
if [ -z "$old_file" ] || [ -z "$new_file" ]; then
|
|
4222
|
+
echo "eval --diff: requires two file arguments <old_results> <new_results>" >&2
|
|
4223
|
+
return 2
|
|
4224
|
+
fi
|
|
4225
|
+
if [ ! -f "$old_file" ] || [ ! -r "$old_file" ]; then
|
|
4226
|
+
echo "eval --diff: file not found or unreadable: $old_file" >&2
|
|
4227
|
+
return 2
|
|
4228
|
+
fi
|
|
4229
|
+
if [ ! -f "$new_file" ] || [ ! -r "$new_file" ]; then
|
|
4230
|
+
echo "eval --diff: file not found or unreadable: $new_file" >&2
|
|
4231
|
+
return 2
|
|
4232
|
+
fi
|
|
4233
|
+
|
|
4234
|
+
local td; td="$(mktemp -d)"; _register_td "$td"
|
|
4235
|
+
|
|
4236
|
+
_eval_diff_extract_fixtures "$old_file" > "$td/old.tsv"
|
|
4237
|
+
_eval_diff_extract_fixtures "$new_file" > "$td/new.tsv"
|
|
4238
|
+
|
|
4239
|
+
if [ ! -s "$td/old.tsv" ]; then
|
|
4240
|
+
echo "eval --diff: no fixture records found in $old_file" >&2
|
|
4241
|
+
rm -rf "$td" 2>/dev/null
|
|
4242
|
+
return 2
|
|
4243
|
+
fi
|
|
4244
|
+
if [ ! -s "$td/new.tsv" ]; then
|
|
4245
|
+
echo "eval --diff: no fixture records found in $new_file" >&2
|
|
4246
|
+
rm -rf "$td" 2>/dev/null
|
|
4247
|
+
return 2
|
|
4248
|
+
fi
|
|
4249
|
+
|
|
4250
|
+
local all_fids
|
|
4251
|
+
all_fids="$( { cut -f1 "$td/old.tsv" 2>/dev/null; cut -f1 "$td/new.tsv" 2>/dev/null; } | sort -u )"
|
|
4252
|
+
|
|
4253
|
+
local rc=0
|
|
4254
|
+
{
|
|
4255
|
+
printf '%s\n' "$all_fids" | while IFS= read -r fid; do
|
|
4256
|
+
[ -n "$fid" ] && printf 'ALL\t%s\n' "$fid"
|
|
4257
|
+
done
|
|
4258
|
+
if [ -f "${EVAL_MANIFEST:-}" ]; then
|
|
4259
|
+
awk -F'\t' 'NR>1 && $1!="" && $4!="" { print "MAN\t" $1 "\t" $4 }' "$EVAL_MANIFEST" 2>/dev/null
|
|
4260
|
+
fi
|
|
4261
|
+
awk -F'\t' '{ print "OLD\t" $1 "\t" $2 "\t" $3 }' "$td/old.tsv"
|
|
4262
|
+
awk -F'\t' '{ print "NEW\t" $1 "\t" $2 "\t" $3 }' "$td/new.tsv"
|
|
4263
|
+
} | awk -F'\t' '
|
|
4264
|
+
$1 == "ALL" { order[++n_fids] = $2 }
|
|
4265
|
+
$1 == "MAN" { if ($3 != "" && $3 != "-") exp_man[$2] = $3 }
|
|
4266
|
+
$1 == "OLD" {
|
|
4267
|
+
old_v[$2] = $3
|
|
4268
|
+
if ($4 != "" && $4 != "-") exp_old_row[$2] = $4
|
|
4269
|
+
}
|
|
4270
|
+
$1 == "NEW" {
|
|
4271
|
+
new_v[$2] = $3
|
|
4272
|
+
if ($4 != "" && $4 != "-") exp_new_row[$2] = $4
|
|
4273
|
+
}
|
|
4274
|
+
END {
|
|
4275
|
+
printf " %-12s %-12s %-10s %-10s %s\n", "id", "expected", "old", "new", "classification"
|
|
4276
|
+
n_reg = 0; n_imp = 0; n_sat = 0; n_unc = 0; n_expchg = 0
|
|
4277
|
+
for (i = 1; i <= n_fids; i++) {
|
|
4278
|
+
fid = order[i]
|
|
4279
|
+
has_old = (fid in old_v)
|
|
4280
|
+
has_new = (fid in new_v)
|
|
4281
|
+
eo = (fid in exp_old_row) ? exp_old_row[fid] : ((fid in exp_man) ? exp_man[fid] : "-")
|
|
4282
|
+
en = (fid in exp_new_row) ? exp_new_row[fid] : ((fid in exp_man) ? exp_man[fid] : "-")
|
|
4283
|
+
ov = has_old ? old_v[fid] : "-"
|
|
4284
|
+
nv = has_new ? new_v[fid] : "-"
|
|
4285
|
+
expected_disp = "-"
|
|
4286
|
+
if (has_old && has_new) {
|
|
4287
|
+
if (eo != "-" && en != "-" && eo != en) {
|
|
4288
|
+
cls = "expectation-changed"
|
|
4289
|
+
n_expchg++
|
|
4290
|
+
expected_disp = eo "→" en
|
|
4291
|
+
} else {
|
|
4292
|
+
expected_val = (en != "-") ? en : eo
|
|
4293
|
+
expected_disp = expected_val
|
|
4294
|
+
if (expected_val == "-") {
|
|
4295
|
+
cls = "unknown"
|
|
4296
|
+
} else {
|
|
4297
|
+
old_corr = (ov == expected_val)
|
|
4298
|
+
new_corr = (nv == expected_val)
|
|
4299
|
+
if (old_corr && new_corr) {
|
|
4300
|
+
cls = "UNCHANGED"
|
|
4301
|
+
n_unc++
|
|
4302
|
+
} else if (old_corr && !new_corr) {
|
|
4303
|
+
cls = "REGRESSED"
|
|
4304
|
+
n_reg++
|
|
4305
|
+
} else if (!old_corr && new_corr) {
|
|
4306
|
+
cls = "IMPROVED"
|
|
4307
|
+
n_imp++
|
|
4308
|
+
} else {
|
|
4309
|
+
cls = "SATURATED"
|
|
4310
|
+
n_sat++
|
|
4311
|
+
}
|
|
4312
|
+
}
|
|
4313
|
+
}
|
|
4314
|
+
} else if (has_old && !has_new) {
|
|
4315
|
+
cls = "removed"
|
|
4316
|
+
expected_disp = (eo != "-") ? eo : en
|
|
4317
|
+
} else {
|
|
4318
|
+
cls = "added"
|
|
4319
|
+
expected_disp = (en != "-") ? en : eo
|
|
4320
|
+
}
|
|
4321
|
+
printf " %-12s %-12s %-10s %-10s %s\n", fid, expected_disp, ov, nv, cls
|
|
4322
|
+
}
|
|
4323
|
+
printf "\n%d regressed, %d improved, %d saturated, %d unchanged, %d expectation-changed\n", n_reg, n_imp, n_sat, n_unc, n_expchg
|
|
4324
|
+
if (n_reg > 0) exit 1
|
|
4325
|
+
exit 0
|
|
4326
|
+
}
|
|
4327
|
+
'
|
|
4328
|
+
rc=$?
|
|
4329
|
+
rm -rf "$td" 2>/dev/null
|
|
4330
|
+
return "$rc"
|
|
4331
|
+
}
|
|
4332
|
+
|
|
3827
4333
|
## ---------------------------------------------------------------------------------------
|
|
3828
4334
|
## Routing eval (v0.22) - a SEPARATE judge modality from the artifact-vs-gate-rules eval
|
|
3829
4335
|
## above. Judges (state-snapshot + utterance) -> single action, not (artifact + gate
|
|
@@ -4459,7 +4965,7 @@ _eval_guard_unbounded_darwin() { # $1=replay_mode (0/1); return 1 = refused
|
|
|
4459
4965
|
# -positives on that exact byte sequence regardless of language. Purely a spelling dodge for a
|
|
4460
4966
|
# generic scanner; behavior is 100% identical to every other function in this file.
|
|
4461
4967
|
function cmd_eval {
|
|
4462
|
-
local stage_filter="" fixture_filter="" n=3 timeout=120 report_mode=0 keep_going=0 replay_mode=0 record_mode=0
|
|
4968
|
+
local stage_filter="" fixture_filter="" n=3 timeout=120 report_mode=0 keep_going=0 replay_mode=0 record_mode=0 diff_mode=0 diff_old="" diff_new=""
|
|
4463
4969
|
while [ $# -gt 0 ]; do
|
|
4464
4970
|
case "$1" in
|
|
4465
4971
|
--stage) shift; stage_filter="${1:-}" ;;
|
|
@@ -4470,10 +4976,25 @@ function cmd_eval {
|
|
|
4470
4976
|
--record) record_mode=1 ;;
|
|
4471
4977
|
--replay) replay_mode=1 ;;
|
|
4472
4978
|
--keep-going) keep_going=1 ;;
|
|
4473
|
-
|
|
4979
|
+
--diff)
|
|
4980
|
+
diff_mode=1
|
|
4981
|
+
shift
|
|
4982
|
+
diff_old="${1:-}"
|
|
4983
|
+
shift
|
|
4984
|
+
diff_new="${1:-}"
|
|
4985
|
+
;;
|
|
4986
|
+
*) echo "usage: /flow eval [--stage 01|02|05|card|routing|converge] [--fixture <id>] [--n 3] [--timeout <seconds>] [--keep-going] [--report] [--record|--replay] [--diff <old> <new>]"; return 1 ;;
|
|
4474
4987
|
esac
|
|
4475
4988
|
shift 2>/dev/null || true
|
|
4476
4989
|
done
|
|
4990
|
+
if [ "$diff_mode" -eq 1 ]; then
|
|
4991
|
+
if [ "$report_mode" -eq 1 ] || [ "$record_mode" -eq 1 ] || [ "$replay_mode" -eq 1 ]; then
|
|
4992
|
+
echo "eval: --diff cannot combine with --report, --record, or --replay" >&2
|
|
4993
|
+
return 1
|
|
4994
|
+
fi
|
|
4995
|
+
_eval_diff "$diff_old" "$diff_new"
|
|
4996
|
+
return $?
|
|
4997
|
+
fi
|
|
4477
4998
|
case "$n" in ''|*[!0-9]*|0) echo "eval: --n must be a positive integer (got '$n')"; return 1 ;; esac
|
|
4478
4999
|
case "$timeout" in ''|*[!0-9]*|0) echo "eval: --timeout must be a positive integer seconds value (got '$timeout')"; return 1 ;; esac
|
|
4479
5000
|
case "$stage_filter" in ""|01|02|05|card|routing|converge) : ;; *) echo "eval: --stage must be one of 01|02|05|card|routing|converge (got '$stage_filter')"; return 1 ;; esac
|
|
@@ -4641,7 +5162,7 @@ function cmd_eval {
|
|
|
4641
5162
|
[ -z "$fid" ] && continue
|
|
4642
5163
|
# manifest's stage column uses flow.sh's own full STAGES names ("01-research", "02-scope",
|
|
4643
5164
|
# "card") for consistency with the rest of the codebase; --stage/heading-map use the terse
|
|
4644
|
-
# 01|02|card form the plan's CLI spec documents - normalize by stripping to the leading
|
|
5165
|
+
# 01|02|05|card form the plan's CLI spec documents - normalize by stripping to the leading
|
|
4645
5166
|
# token before the first '-' (a bare "card" has none, so it passes through unchanged).
|
|
4646
5167
|
local fstage_short="${fstage%%-*}"
|
|
4647
5168
|
[ -n "$stage_filter" ] && [ "$fstage_short" != "$stage_filter" ] && continue
|
|
@@ -4667,7 +5188,7 @@ function cmd_eval {
|
|
|
4667
5188
|
local promptfile="$rundir/prompt.txt"
|
|
4668
5189
|
if ! _eval_build_prompt "$promptfile" "$fstage_short" "$artifact_path" "$nonce"; then
|
|
4669
5190
|
if [ "$replay_mode" -eq 1 ]; then
|
|
4670
|
-
echo " $fid: SKIP - no heading-mapped section (artifact-replay scope is 01|02|card)"
|
|
5191
|
+
echo " $fid: SKIP - no heading-mapped section (artifact-replay scope is 01|02|05|card)"
|
|
4671
5192
|
rm -rf "$rundir" 2>/dev/null
|
|
4672
5193
|
continue
|
|
4673
5194
|
fi
|
|
@@ -4898,10 +5419,12 @@ flow.sh - buildflow gate runner (mechanical layer)
|
|
|
4898
5419
|
|
|
4899
5420
|
usage: bash flow.sh <command> [args]
|
|
4900
5421
|
|
|
4901
|
-
status
|
|
4902
|
-
|
|
5422
|
+
status [--json] Where am I? What's blocking? (also: no command). --json prints
|
|
5423
|
+
one flow_context/v1 object (display-only; do not auto-exec next_verb).
|
|
5424
|
+
resume [--json|--budget] Session-story brief for a fresh agent entering mid-cycle: last session
|
|
4903
5425
|
(command names only, never raw args), in-flight card + dwell, gate state,
|
|
4904
5426
|
one NEXT-> line. Read-only, no lock. Run this FIRST when resuming a project.
|
|
5427
|
+
--json: same closed object as status --json.
|
|
4905
5428
|
next Check current gate; unlock next stage (or start at 00)
|
|
4906
5429
|
assess Brownfield: scaffold + gate a current-state assessment (flow/00-inspect.md) before planning
|
|
4907
5430
|
card Create the next build card (after planning complete)
|
|
@@ -4911,7 +5434,7 @@ usage: bash flow.sh <command> [args]
|
|
|
4911
5434
|
project-type [t] Show or set project type (web|cli|library|skill); adapts done-evidence
|
|
4912
5435
|
skip <stage> --reason Advance past a gate that has a matching open DEBT (non-security only)
|
|
4913
5436
|
ready List buildable todo cards + parallel-safety hint
|
|
4914
|
-
workspace <verb> Multi-agent worktree isolation: add|list|enter|remove|check|doctor (one worktree per agent)
|
|
5437
|
+
workspace <verb> Multi-agent worktree isolation: add|list|enter|remove|check|doctor|port (one worktree per agent)
|
|
4915
5438
|
loop-prep <card> Plumbing for ck-loop: isolated worktree + numeric Verify command + param block (thin wrapper; ck-loop is the engine)
|
|
4916
5439
|
loop-log <card> Record a finished ck-loop run (iterations/start/end/outcome) into usage-log telemetry
|
|
4917
5440
|
auto [stop] Activate attested auto (risk + Stage 05 receipt) or stop
|
|
@@ -4924,11 +5447,11 @@ usage: bash flow.sh <command> [args]
|
|
|
4924
5447
|
contract Check client base-URL vs served-path prefixes (path-resolution drift; web)
|
|
4925
5448
|
tokens Check DESIGN.md declared tokens vs CSS usage (design-system drift)
|
|
4926
5449
|
coherence Flag version drift across declared version fields (doc-vs-code coherence)
|
|
4927
|
-
consistency
|
|
5450
|
+
consistency [--contract] Audit cross-artifact coverage (PRD FR ids or --contract endpoint audit)
|
|
4928
5451
|
constitution Check operator-authored per-project invariants in flow/constitution.md (advisory; not a next-gate)
|
|
4929
5452
|
clarify List leftover ## Open decisions bullets on scope/PRD/contract (advisory; not a next-gate)
|
|
4930
5453
|
converge [--file] Append-only remainder cards from a flow-converge/v1 payload (plan vs present code)
|
|
4931
|
-
promote <file
|
|
5454
|
+
promote <file>|--suggest Copy a playbook into KB or suggest draft playbooks from harness patterns
|
|
4932
5455
|
doctor Check the environment (bash/python/grep/git) across macOS/Linux/Windows
|
|
4933
5456
|
eval [opts] Behavioral eval: does the LLM semantic gate flag a hollow-but-mechanically-clean
|
|
4934
5457
|
fixture? Opt-in, BILLABLE (live mode skips cleanly with zero calls if 'claude'
|
|
@@ -5110,8 +5633,8 @@ fi
|
|
|
5110
5633
|
FLOW_LOG_START="$(_now)"
|
|
5111
5634
|
trap '_log_on_exit' EXIT
|
|
5112
5635
|
case "$cmd" in
|
|
5113
|
-
status|"") cmd_status ;;
|
|
5114
|
-
resume) cmd_resume ;;
|
|
5636
|
+
status|"") cmd_status "$@" ;;
|
|
5637
|
+
resume) cmd_resume "$@" ;;
|
|
5115
5638
|
next) cmd_next ;;
|
|
5116
5639
|
assess) cmd_assess ;;
|
|
5117
5640
|
card) cmd_card "$@" ;;
|
|
@@ -5135,11 +5658,11 @@ case "$cmd" in
|
|
|
5135
5658
|
contract) cmd_contract ;;
|
|
5136
5659
|
tokens) cmd_tokens ;;
|
|
5137
5660
|
coherence) cmd_coherence ;;
|
|
5138
|
-
consistency) cmd_consistency ;;
|
|
5661
|
+
consistency) cmd_consistency "$@" ;;
|
|
5139
5662
|
constitution) cmd_constitution ;;
|
|
5140
5663
|
clarify) cmd_clarify ;;
|
|
5141
5664
|
converge) cmd_converge "$@" ;;
|
|
5142
|
-
promote) cmd_promote "
|
|
5665
|
+
promote) cmd_promote "$@" ;;
|
|
5143
5666
|
doctor) cmd_doctor ;;
|
|
5144
5667
|
eval) cmd_eval "$@" ;;
|
|
5145
5668
|
usage) cmd_usage "$@" ;;
|