forge-orkes 0.62.0 → 0.64.0

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.
Files changed (56) hide show
  1. package/bin/create-forge.js +250 -99
  2. package/experimental/conventions/README.md +87 -0
  3. package/experimental/conventions/install.sh +179 -0
  4. package/experimental/conventions/source/rules/code-quality.md +17 -0
  5. package/experimental/conventions/source/rules/laravel-http.md +21 -0
  6. package/experimental/conventions/source/rules/laravel-jobs.md +19 -0
  7. package/experimental/conventions/source/rules/laravel-migrations.md +19 -0
  8. package/experimental/conventions/source/rules/laravel-services.md +21 -0
  9. package/experimental/conventions/source/rules/python.md +14 -0
  10. package/experimental/conventions/source/rules/testing.md +15 -0
  11. package/experimental/conventions/source/rules/vue-inertia.md +24 -0
  12. package/experimental/conventions/source/skills/laravel-conventions/SKILL.md +124 -0
  13. package/experimental/conventions/source/skills/python-conventions/SKILL.md +100 -0
  14. package/experimental/conventions/source/skills/testing-standards/SKILL.md +142 -0
  15. package/experimental/conventions/source/skills/vue-conventions/SKILL.md +92 -0
  16. package/experimental/conventions/uninstall.sh +97 -0
  17. package/experimental/m10/README.md +130 -0
  18. package/experimental/m10/install.sh +198 -0
  19. package/experimental/m10/source/hooks/forge-branch-guard.sh +61 -0
  20. package/experimental/m10/source/hooks/forge-claim-check-doctor.sh +77 -0
  21. package/experimental/m10/source/hooks/forge-claim-check.sh +113 -0
  22. package/experimental/m10/source/hooks/forge-session-id.sh +22 -0
  23. package/experimental/m10/source/mcp-server/README.md +74 -0
  24. package/experimental/m10/source/mcp-server/example.mcp.json +8 -0
  25. package/experimental/m10/source/mcp-server/index.js +460 -0
  26. package/experimental/m10/source/mcp-server/package.json +17 -0
  27. package/experimental/m10/source/skills/orchestrating/SKILL.md +223 -0
  28. package/experimental/m10/source/skills/orchestrating/bootstrap-checks.md +70 -0
  29. package/experimental/m10/uninstall.sh +69 -0
  30. package/package.json +3 -2
  31. package/template/.claude/hooks/forge-release-fold.sh +9 -1
  32. package/template/.claude/settings.json +1 -7
  33. package/template/.claude/skills/jarvis/SKILL.md +204 -0
  34. package/template/.claude/skills/upgrading/SKILL.md +105 -176
  35. package/template/.forge/FORGE.md +2 -2
  36. package/template/.forge/checks/forge-board-render.sh +11 -5
  37. package/template/.forge/checks/forge-jarvis-answer.sh +79 -0
  38. package/template/.forge/checks/forge-jarvis-awareness.sh +128 -0
  39. package/template/.forge/checks/forge-jarvis-bridge-clean.sh +130 -0
  40. package/template/.forge/checks/forge-jarvis-dispatch.sh +176 -0
  41. package/template/.forge/checks/forge-jarvis-instruments.sh +146 -0
  42. package/template/.forge/checks/forge-jarvis-notify-logonly.sh +40 -0
  43. package/template/.forge/checks/forge-jarvis-promote.sh +103 -0
  44. package/template/.forge/checks/forge-jarvis-prwatch.sh +92 -0
  45. package/template/.forge/checks/forge-jarvis-relay.sh +215 -0
  46. package/template/.forge/checks/forge-jarvis.sh +160 -0
  47. package/template/.forge/checks/tests/forge-jarvis-answer.test.sh +75 -0
  48. package/template/.forge/checks/tests/forge-jarvis-bridge-clean.test.sh +94 -0
  49. package/template/.forge/checks/tests/forge-jarvis-dispatch.test.sh +162 -0
  50. package/template/.forge/checks/tests/forge-jarvis-instruments.test.sh +64 -0
  51. package/template/.forge/checks/tests/forge-jarvis-promote.test.sh +102 -0
  52. package/template/.forge/checks/tests/forge-jarvis-prwatch.test.sh +66 -0
  53. package/template/.forge/checks/tests/forge-jarvis-relay.test.sh +135 -0
  54. package/template/.forge/checks/tests/forge-jarvis.test.sh +122 -0
  55. package/template/.forge/migrations/0.63.0-origin-first-upgrading.md +49 -0
  56. package/template/.forge/templates/project.yml +11 -0
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env sh
2
+ # Fixture test for .forge/checks/forge-jarvis-bridge-clean.sh (m-37 B5; FR-206; B1 probe ii).
3
+ #
4
+ # Builds a REAL throwaway repo with two REAL bridge worktrees (one "live", one orphaned), then
5
+ # asserts the orphan is reaped from disk AND git's registry while the live one is untouched — and
6
+ # that --dry-run removes nothing and never flags the live one. Liveness is pinned deterministically
7
+ # via FORGE_JARVIS_BRIDGE_LIVECHECK so the test never depends on wall-clock mtime.
8
+ set -u
9
+
10
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
11
+ CLEAN="$SCRIPT_DIR/../forge-jarvis-bridge-clean.sh"
12
+ [ -f "$CLEAN" ] || printf 'NOTE: %s missing — RED phase, every case should FAIL\n' "$CLEAN" >&2
13
+
14
+ ROOT="$(mktemp -d)"
15
+ trap 'rm -rf "$ROOT"' EXIT INT TERM
16
+
17
+ PASSED=0; FAILED=0
18
+ ok() { PASSED=$((PASSED+1)); printf ' ok %s\n' "$1"; }
19
+ bad() { FAILED=$((FAILED+1)); printf ' FAIL %s\n %s\n' "$1" "$2"; }
20
+
21
+ # --- a real repo with two real bridge worktrees ------------------------------------------------
22
+ REPO="$ROOT/repo"; mkdir -p "$REPO"
23
+ git -C "$REPO" init -q
24
+ git -C "$REPO" config user.email t@t.test
25
+ git -C "$REPO" config user.name tester
26
+ printf 'seed\n' > "$REPO/f"; git -C "$REPO" add f; git -C "$REPO" commit -qm init
27
+
28
+ WTR="$REPO/.claude/worktrees"; mkdir -p "$WTR"
29
+ git -C "$REPO" worktree add -q "$WTR/bridge-alive" 2>/dev/null || bad "setup" "could not add bridge-alive worktree"
30
+ git -C "$REPO" worktree add -q "$WTR/bridge-dead" 2>/dev/null || bad "setup" "could not add bridge-dead worktree"
31
+
32
+ # livecheck stub: ONLY bridge-alive is live.
33
+ LIVE="$ROOT/livecheck.sh"
34
+ printf '#!/usr/bin/env sh\ncase "$1" in *bridge-alive*) exit 0 ;; *) exit 1 ;; esac\n' > "$LIVE"
35
+ chmod +x "$LIVE"
36
+
37
+ run() { FORGE_JARVIS_BRIDGE_LIVECHECK="$LIVE" sh "$CLEAN" --worktrees-root "$WTR" "$@" 2>/dev/null; }
38
+
39
+ # 1. dry-run flags the orphan, removes nothing
40
+ out="$(run --dry-run)"
41
+ case "$out" in *bridge-dead*candidate*) ok "dry-run flags bridge-dead as an orphan candidate" ;; *) bad "dry-run candidate" "$out" ;; esac
42
+ [ -d "$WTR/bridge-dead" ] && ok "dry-run removed nothing (bridge-dead still on disk)" || bad "dry-run non-removal" "bridge-dead gone after dry-run"
43
+
44
+ # 2. dry-run never flags the live one (line-scoped: the bridge-alive line must not say "candidate")
45
+ if printf '%s\n' "$out" | grep 'bridge-alive' | grep -q 'candidate'; then
46
+ bad "dry-run live safety" "flagged the live worktree: $out"
47
+ else
48
+ ok "dry-run keeps the live worktree out of candidates"
49
+ fi
50
+
51
+ # 3. real run reaps the orphan, keeps the live one
52
+ out="$(run)"
53
+ [ ! -d "$WTR/bridge-dead" ] && ok "orphan bridge-dead reaped (removed from disk)" || bad "orphan removal" "bridge-dead still present: $out"
54
+ [ -d "$WTR/bridge-alive" ] && ok "live bridge-alive untouched" || bad "live safety" "live worktree was removed"
55
+
56
+ # 4. reaped from git's worktree registry too (not just the directory)
57
+ if git -C "$REPO" worktree list --porcelain 2>/dev/null | grep -q 'bridge-dead'; then
58
+ bad "registry" "bridge-dead still in git worktree list"
59
+ else
60
+ ok "bridge-dead gone from git's worktree registry too"
61
+ fi
62
+
63
+ # 5. re-run is a clean no-op (only the live one remains, and it is kept)
64
+ out="$(run)"
65
+ case "$out" in *'0 reaped'*) ok "re-run reaps nothing (idempotent — only the live worktree left)" ;; *) bad "idempotent" "$out" ;; esac
66
+
67
+ # --- 6+7. LOCK-AWARE liveness — the REAL RC substrate: bridges are git-LOCKED under the server pid.
68
+ # No injected livecheck here, so this exercises default_livecheck's lock-pid path AND unlock-on-reap
69
+ # (the fixtures 1-5 used unlocked worktrees + a stubbed livecheck, which masked both — the gap the
70
+ # FR-207 human-verification walk surfaced against a live RC server).
71
+ REPO2="$ROOT/repo2"; mkdir -p "$REPO2"
72
+ git -C "$REPO2" init -q
73
+ git -C "$REPO2" config user.email t@t.test
74
+ git -C "$REPO2" config user.name tester
75
+ printf 'seed\n' > "$REPO2/f"; git -C "$REPO2" add f; git -C "$REPO2" commit -qm init
76
+ WTR2="$REPO2/.claude/worktrees"; mkdir -p "$WTR2"
77
+ git -C "$REPO2" worktree add -q "$WTR2/bridge-livepid" 2>/dev/null || bad "setup2" "add bridge-livepid"
78
+ git -C "$REPO2" worktree add -q "$WTR2/bridge-deadpid" 2>/dev/null || bad "setup2" "add bridge-deadpid"
79
+
80
+ # a definitely-DEAD pid (background a no-op, reap it, reuse its pid); a definitely-LIVE pid (this shell)
81
+ ( : ) & DEADPID=$!; wait "$DEADPID" 2>/dev/null
82
+ LIVEPID=$$
83
+ git -C "$REPO2" worktree lock --reason "claude agent bridge-livepid (pid $LIVEPID start now)" "$WTR2/bridge-livepid" 2>/dev/null || bad "setup2" "lock bridge-livepid"
84
+ git -C "$REPO2" worktree lock --reason "claude agent bridge-deadpid (pid $DEADPID start now)" "$WTR2/bridge-deadpid" 2>/dev/null || bad "setup2" "lock bridge-deadpid"
85
+
86
+ # Both are FRESH (mtime ~now): under the OLD mtime rule both would be "live". The fix classifies by
87
+ # the lock pid, so the dead-pid one must reap (despite fresh mtime) and the live-pid one must stay —
88
+ # AND the reap must succeed against a LOCKED worktree (unlock-before-remove).
89
+ out2="$(sh "$CLEAN" --worktrees-root "$WTR2" 2>/dev/null)"
90
+ [ ! -d "$WTR2/bridge-deadpid" ] && ok "dead-pid lock reaped despite fresh mtime (lock beats mtime; unlock-then-remove worked)" || bad "lock-pid orphan" "bridge-deadpid still present: $out2"
91
+ [ -d "$WTR2/bridge-livepid" ] && ok "live-pid lock kept (the server still owns it)" || bad "lock-pid live" "live-pid worktree was removed: $out2"
92
+
93
+ printf '\n%s: %d passed, %d failed\n' "$(basename "$0")" "$PASSED" "$FAILED"
94
+ [ "$FAILED" = 0 ]
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env sh
2
+ # Fixture test suite for .forge/checks/forge-jarvis-dispatch.sh --dry-run (m-37, step 4 B3).
3
+ #
4
+ # Contract under test (handoff-step4-jarvis.md REV2 B3(b)/(c); FR-197/FR-198; acceptance #5):
5
+ # --dry-run prints the exact `nohup env ... runner ...` command and NEVER dispatches:
6
+ # - FORGE_SLICE_NOTIFY_LOG lands INSIDE the slice worktree (<slice>/.forge/runner-work/
7
+ # notify.log), NEVER the invoker's cwd — the clean-main invariant (step 0; acceptance #5).
8
+ # - FORGE_SLICE_NOTIFY points at the log-only channel (forge-jarvis-notify-logonly.sh) —
9
+ # the relay is the one delivery path; the default channel's push stays quiet (probe x).
10
+ # - the runner is dispatched DETACHED (nohup-& — survives Jarvis's death, probe viii).
11
+ # - every runner arg (incl --config carrying model_by_phase_type) is forwarded VERBATIM;
12
+ # the helper carries NO model-selection logic (routing is step 5, FR-198).
13
+ # Refusal: missing --slice → non-zero exit, no command on stdout.
14
+ #
15
+ # Only ever invokes --dry-run (pure resolution — `claude`/the runner need not run); the real
16
+ # runner + channel are resolved relative to the real script dir, so both exist. Slices are
17
+ # throwaway mktemp dirs.
18
+ #
19
+ # Usage: ./.forge/checks/tests/forge-jarvis-dispatch.test.sh
20
+ # RED-phase friendly: a missing/non-executable script fails every case, exits non-zero.
21
+ set -u
22
+
23
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
24
+ DISPATCH="$SCRIPT_DIR/../forge-jarvis-dispatch.sh"
25
+
26
+ [ -f "$DISPATCH" ] || printf 'NOTE: %s missing — RED phase, every case should FAIL\n' "$DISPATCH" >&2
27
+
28
+ ROOT="$(mktemp -d)"
29
+ trap 'rm -rf "$ROOT"' EXIT INT TERM
30
+
31
+ PASSED=0; FAILED=0
32
+ ok() { PASSED=$((PASSED+1)); printf ' ok %s\n' "$1"; }
33
+ bad() { FAILED=$((FAILED+1)); printf ' FAIL %s\n %s\n' "$1" "$2"; }
34
+
35
+ # --- fixture slice worktree (just a dir — --dry-run needs [ -d ], not a real git worktree) ---
36
+ SLICE="$ROOT/slice-wt"
37
+ mkdir -p "$SLICE"
38
+ LOGWANT="$SLICE/.forge/runner-work/notify.log"
39
+ CHECKS_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" # resolve — the dispatch emits a canonical path
40
+ CHANWANT="$CHECKS_DIR/forge-jarvis-notify-logonly.sh"
41
+
42
+ # 1. notify-log lands INSIDE the slice worktree (not cwd) — the acceptance #5 core
43
+ out="$(cd "$ROOT" && sh "$DISPATCH" --slice "$SLICE" --dry-run 2>/dev/null)"; rc=$?
44
+ case "$out" in
45
+ *"FORGE_SLICE_NOTIFY_LOG=$LOGWANT "*) ok "notify-log points INTO the slice worktree ($LOGWANT)" ;;
46
+ *) bad "notify-log placement" "rc=$rc out=$out" ;;
47
+ esac
48
+
49
+ # 2. FORGE_SLICE_NOTIFY points at the log-only channel
50
+ case "$out" in
51
+ *"FORGE_SLICE_NOTIFY=$CHANWANT "*) ok "notify channel is the log-only channel (no double-buzz, probe x)" ;;
52
+ *) bad "log-only channel" "rc=$rc out=$out" ;;
53
+ esac
54
+
55
+ # 3. dispatched DETACHED — the command begins with nohup (survives Jarvis's death, probe viii)
56
+ case "$out" in
57
+ "nohup "*) ok "runner dispatched detached (nohup-&)" ;;
58
+ *) bad "nohup detach" "out=$out" ;;
59
+ esac
60
+
61
+ # 4. model map passed through untouched — a caller --config reaches the runner verbatim
62
+ out4="$(cd "$ROOT" && sh "$DISPATCH" --slice "$SLICE" --config /tmp/mymap.yml --dry-run 2>/dev/null)"; rc=$?
63
+ case "$out4" in
64
+ *"--config /tmp/mymap.yml"*) ok "model map: --config forwarded to the runner VERBATIM (FR-198)" ;;
65
+ *) bad "config passthrough" "rc=$rc out=$out4" ;;
66
+ esac
67
+
68
+ # 5. --slice forwarded to the runner (the runner needs it too, not just this helper)
69
+ case "$out" in
70
+ *"--slice $SLICE"*) ok "--slice forwarded to the runner" ;;
71
+ *) bad "slice forward" "out=$out" ;;
72
+ esac
73
+
74
+ # 6. NEG-CONTROL (step-0 invariant): run from a DIFFERENT cwd (a stand-in 'main checkout');
75
+ # the log must STILL be the slice's, never <cwd>/notify.log. Without the override the
76
+ # runner's default would drop notify.log in cwd — this is why the override exists (#5).
77
+ FAKEMAIN="$ROOT/fake-main-checkout"
78
+ mkdir -p "$FAKEMAIN"
79
+ out6="$(cd "$FAKEMAIN" && sh "$DISPATCH" --slice "$SLICE" --dry-run 2>/dev/null)"; rc=$?
80
+ case "$out6" in
81
+ *"FORGE_SLICE_NOTIFY_LOG=$FAKEMAIN/notify.log"*)
82
+ bad "step-0 neg-control" "log leaked into cwd: $out6" ;;
83
+ *"FORGE_SLICE_NOTIFY_LOG=$LOGWANT "*)
84
+ ok "step-0 neg-control: dispatched from another cwd, log STILL in the slice (not cwd)" ;;
85
+ *) bad "step-0 neg-control" "rc=$rc out=$out6" ;;
86
+ esac
87
+
88
+ # 7. NEG: missing --slice → clean refusal (non-zero, no command emitted)
89
+ out7="$(cd "$ROOT" && sh "$DISPATCH" --dry-run 2>/dev/null)"; rc=$?
90
+ if [ "$rc" != 0 ] && [ -z "$out7" ]; then
91
+ ok "neg-control: missing --slice → refusal, no command emitted"
92
+ else
93
+ bad "neg-control missing --slice" "rc=$rc out=$out7"
94
+ fi
95
+
96
+ # 8. SOURCE AUDIT: the helper carries NO model-selection logic (routing is step 5, FR-198).
97
+ # Analogue of the launcher's no-token case — grep the script itself, not the output.
98
+ if grep -inE 'choose.*model|select.*model|pick.*model|route.*model|model_by_phase_type[[:space:]]*=' "$DISPATCH" >/dev/null 2>&1; then
99
+ bad "no routing brain" "model-selection logic found in $DISPATCH"
100
+ else
101
+ ok "no model-selection logic in the dispatch helper (routing stays step 5)"
102
+ fi
103
+
104
+ # --- 9-12. WORK-ORDER MATERIALIZATION (FR-197; walk run 4: runner refused 'no slice.yml',
105
+ # refusal visible only in dispatch.out — phone silent). The helper bridges the Forge plan
106
+ # layout (.forge/phases/milestone-{id}/{phase}-{name}/plan-NN.md) to the runner contract
107
+ # (slice.yml + phases/<name>/plan.md), refusing loudly BEFORE the detach when it cannot.
108
+
109
+ # 9. materialize from milestone plans: ordered slice.yml + concatenated plan.md per phase
110
+ MSLICE="$ROOT/mat-wt"
111
+ mkdir -p "$MSLICE/.forge/phases/milestone-77/71-aaa" "$MSLICE/.forge/phases/milestone-77/72-bbb"
112
+ printf 'plan A part1\n' > "$MSLICE/.forge/phases/milestone-77/71-aaa/plan-01.md"
113
+ printf 'plan A part2\n' > "$MSLICE/.forge/phases/milestone-77/71-aaa/plan-02.md"
114
+ printf 'plan B\n' > "$MSLICE/.forge/phases/milestone-77/72-bbb/plan-01.md"
115
+ err9="$(sh "$DISPATCH" --slice "$MSLICE" --materialize-only 2>&1)"; rc=$?
116
+ if [ "$rc" = 0 ] && [ -f "$MSLICE/slice.yml" ] \
117
+ && grep -q '^ - 71-aaa$' "$MSLICE/slice.yml" && grep -q '^ - 72-bbb$' "$MSLICE/slice.yml" \
118
+ && grep -q 'plan A part1' "$MSLICE/phases/71-aaa/plan.md" && grep -q 'plan A part2' "$MSLICE/phases/71-aaa/plan.md" \
119
+ && grep -q 'plan B' "$MSLICE/phases/72-bbb/plan.md"; then
120
+ ok "materializes slice.yml (ordered) + phases/<name>/plan.md (plan-NN concatenated) from milestone plans"
121
+ else
122
+ bad "materialize" "rc=$rc err=$err9 slice.yml=$(cat "$MSLICE/slice.yml" 2>/dev/null)"
123
+ fi
124
+
125
+ # 10. an existing slice.yml is honored untouched (hand-authored work order wins)
126
+ HSLICE="$ROOT/hand-wt"
127
+ mkdir -p "$HSLICE/.forge/phases/milestone-88/1-x"
128
+ printf 'x\n' > "$HSLICE/.forge/phases/milestone-88/1-x/plan-01.md"
129
+ printf 'phases:\n - hand-rolled\n' > "$HSLICE/slice.yml"
130
+ before="$(cat "$HSLICE/slice.yml")"
131
+ sh "$DISPATCH" --slice "$HSLICE" --materialize-only >/dev/null 2>&1; rc=$?
132
+ after="$(cat "$HSLICE/slice.yml")"
133
+ if [ "$rc" = 0 ] && [ "$before" = "$after" ]; then
134
+ ok "existing slice.yml honored untouched (hand-authored wins)"
135
+ else
136
+ bad "hand-authored slice.yml" "rc=$rc before/after differ or refusal"
137
+ fi
138
+
139
+ # 11. NEG: nothing to dispatch (no slice.yml, no milestone plans) → loud SYNCHRONOUS refusal
140
+ ESLICE="$ROOT/empty-wt"
141
+ mkdir -p "$ESLICE"
142
+ err11="$(sh "$DISPATCH" --slice "$ESLICE" --materialize-only 2>&1)"; rc=$?
143
+ if [ "$rc" != 0 ] && [ ! -f "$ESLICE/slice.yml" ] && printf '%s' "$err11" | grep -q 'REFUSED'; then
144
+ ok "neg-control: nothing to dispatch → loud refusal BEFORE the detach (no silent phone)"
145
+ else
146
+ bad "empty refusal" "rc=$rc err=$err11"
147
+ fi
148
+
149
+ # 12. --dry-run stays side-effect-free (reports, but writes no slice.yml)
150
+ DSLICE="$ROOT/dry-wt"
151
+ mkdir -p "$DSLICE/.forge/phases/milestone-99/1-y"
152
+ printf 'y\n' > "$DSLICE/.forge/phases/milestone-99/1-y/plan-01.md"
153
+ sh "$DISPATCH" --slice "$DSLICE" --dry-run >/dev/null 2>&1
154
+ if [ ! -f "$DSLICE/slice.yml" ] && [ ! -d "$DSLICE/phases" ]; then
155
+ ok "--dry-run is side-effect-free (no slice.yml/phases written)"
156
+ else
157
+ bad "dry-run side effects" "dry-run wrote work-order files"
158
+ fi
159
+
160
+ # --- summary -----------------------------------------------------------------
161
+ printf '\n%s: %d passed, %d failed\n' "$(basename "$0")" "$PASSED" "$FAILED"
162
+ [ "$FAILED" = 0 ]
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env sh
2
+ # Fixture test for .forge/checks/forge-jarvis-instruments.sh (m-37 B5; FR-205).
3
+ #
4
+ # Builds a notify.log + a relay-markers dir BY HAND in the exact TSV formats the runner's
5
+ # notify channel (-notify-logonly.sh) and the relay (-relay.sh) write, then asserts the four
6
+ # week-zero numbers fall out. No real repo/slices/network needed — pure fixtures.
7
+ #
8
+ # Fixture design (4 pings emitted at :00,:01,:02,:03; only 1-3 relayed):
9
+ # offset 1 ambiguity, relayed +30s → live
10
+ # offset 2 done, relayed +5s → live, host says unmerged (no discrepancy)
11
+ # offset 3 halt, relayed +30min → landed-while-away, DISCREPANCY (payload lied about merge)
12
+ # offset 4 ambiguity, NOT relayed (no marker) → excluded from every number
13
+ # Expected: 3 relayed total; ambiguity=1/done=1/halt=1; latency max 1800s; discrepancy=1;
14
+ # presence 2/3 live, 1 landed while away.
15
+ set -u
16
+
17
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
18
+ INSTR="$SCRIPT_DIR/../forge-jarvis-instruments.sh"
19
+ [ -f "$INSTR" ] || printf 'NOTE: %s missing — RED phase, every case should FAIL\n' "$INSTR" >&2
20
+
21
+ ROOT="$(mktemp -d)"
22
+ trap 'rm -rf "$ROOT"' EXIT INT TERM
23
+
24
+ PASSED=0; FAILED=0
25
+ ok() { PASSED=$((PASSED+1)); printf ' ok %s\n' "$1"; }
26
+ bad() { FAILED=$((FAILED+1)); printf ' FAIL %s\n %s\n' "$1" "$2"; }
27
+ TAB="$(printf '\t')"
28
+
29
+ LOG="$ROOT/notify.log"
30
+ MK="$ROOT/relay-markers"; mkdir -p "$MK"
31
+
32
+ # notify.log — emit-ts <TAB> type <TAB> payload
33
+ printf '2026-07-21T10:00:00Z%sambiguity%sJWT or sessions?\n' "$TAB" "$TAB" >> "$LOG" # 1
34
+ printf '2026-07-21T10:01:00Z%sdone%sPhases complete, PR opened\n' "$TAB" "$TAB" >> "$LOG" # 2
35
+ printf '2026-07-21T10:02:00Z%shalt%sAll done and merged to main!\n' "$TAB" "$TAB" >> "$LOG" # 3
36
+ printf '2026-07-21T10:03:00Z%sambiguity%sretry or fail?\n' "$TAB" "$TAB" >> "$LOG" # 4 (unrelayed)
37
+
38
+ # relay-markers — relay-ts <TAB> offset=N <TAB> relay-text (only 1,2,3 relayed)
39
+ printf '2026-07-21T10:00:30Z%soffset=1%s❓ Needs you (ambiguity): JWT or sessions?\n' "$TAB" "$TAB" > "$MK/1.relayed"
40
+ printf '2026-07-21T10:01:05Z%soffset=2%s✅ Slice done — host: release_state=unmerged. PR opened\n' "$TAB" "$TAB" > "$MK/2.relayed"
41
+ printf '2026-07-21T10:32:00Z%soffset=3%s⚠️ DISCREPANCY — payload claims landed/merged, but host shows release_state=unmerged. 🛑 Slice HALTED (host truth wins). Payload said: All done and merged to main!\n' "$TAB" "$TAB" > "$MK/3.relayed"
42
+
43
+ OUT="$(sh "$INSTR" --log "$LOG" --marker-dir "$MK" --live-window 120 2>/dev/null)"
44
+
45
+ # 1. by type — only relayed pings; offset 4 (ambiguity, unrelayed) excluded
46
+ printf '%s\n' "$OUT" | grep -qE '3 total' && ok "total relayed = 3 (unrelayed offset 4 excluded)" || bad "total" "$OUT"
47
+ printf '%s\n' "$OUT" | grep -qE 'ambiguity[[:space:]]+1' && ok "type: ambiguity=1 (only the relayed one)" || bad "type ambiguity" "$OUT"
48
+ printf '%s\n' "$OUT" | grep -qE 'done[[:space:]]+1' && ok "type: done=1" || bad "type done" "$OUT"
49
+ printf '%s\n' "$OUT" | grep -qE 'halt[[:space:]]+1' && ok "type: halt=1" || bad "type halt" "$OUT"
50
+
51
+ # 2. latency — max is the +30min catch-up ping
52
+ printf '%s\n' "$OUT" | grep -qE 'max 1800s' && ok "latency max = 1800s (the catch-up ping)" || bad "latency max" "$OUT"
53
+
54
+ # 3. discrepancy — the lying halt
55
+ printf '%s\n' "$OUT" | grep -qE 'Discrepancies caught.*: 1' && ok "discrepancy count = 1 (payload claimed a merge the host denied)" || bad "discrepancy" "$OUT"
56
+
57
+ # 4. presence proxy — 1 & 2 within window, 3 landed while away
58
+ printf '%s\n' "$OUT" | grep -qE '2/3 relayed live' && ok "presence proxy: 2/3 live, 1 landed while away" || bad "presence proxy" "$OUT"
59
+
60
+ # 5. the presence line labels itself a proxy (never a measured SLA)
61
+ printf '%s\n' "$OUT" | grep -qiE 'proxy' && ok "presence figure is labelled a proxy, not an SLA" || bad "proxy label" "$OUT"
62
+
63
+ printf '\n%s: %d passed, %d failed\n' "$(basename "$0")" "$PASSED" "$FAILED"
64
+ [ "$FAILED" = 0 ]
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env sh
2
+ # Fixture test suite for .forge/checks/forge-jarvis-promote.sh (m-37, step 4 B6; plan 70-01).
3
+ #
4
+ # Contract under test (handoff-step4-jarvis.md REV2 B6; FR-208; NFR-038; operator ruling 3
5
+ # "trigger-not-approve"; acceptance #7 is the manual human gate, not here):
6
+ # - SHOWS host state (the board projection) BEFORE triggering — reuses the B4/B5 host-truth
7
+ # env contract (FORGE_JARVIS_BOARD), degrades cleanly when the board is unavailable.
8
+ # - TRIGGERS the step-2 seam via `gh workflow run promote.yml -f sha=<full-sha>` and nothing
9
+ # else — a real (non-dry-run) run actually invokes gh (proven with a recording stub).
10
+ # - NEG-CONTROL: the helper contains NO Environment-approval path (no --approve, no
11
+ # `gh api ... environments ... approve`, no approve-a-deployment call). Triggering != approving.
12
+ # - refuses a non-full sha (Jarvis never invents/truncates a sha).
13
+ #
14
+ # The board + gh are stubbed via env so no real repo/main/gh state is needed. Each assertion
15
+ # prints WHY. RED-friendly: a missing/broken script fails every case and exits non-zero.
16
+ #
17
+ # Usage: ./.forge/checks/tests/forge-jarvis-promote.test.sh
18
+ set -u
19
+
20
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
21
+ PROMOTE="$SCRIPT_DIR/../forge-jarvis-promote.sh"
22
+ [ -f "$PROMOTE" ] || printf 'NOTE: %s missing — RED phase, every case should FAIL\n' "$PROMOTE" >&2
23
+
24
+ ROOT="$(mktemp -d)"
25
+ trap 'rm -rf "$ROOT"' EXIT INT TERM
26
+
27
+ PASSED=0; FAILED=0
28
+ ok() { PASSED=$((PASSED+1)); printf ' ok %s\n' "$1"; }
29
+ bad() { FAILED=$((FAILED+1)); printf ' FAIL %s\n %s\n' "$1" "$2"; }
30
+
31
+ SHA="0123456789abcdef0123456789abcdef01234567" # a well-formed full 40-hex sha
32
+
33
+ # --- stub board: emit line-oriented JSON with one merged + one complete unit ------------------
34
+ BOARD_OK="$ROOT/board-ok.sh"
35
+ cat > "$BOARD_OK" <<'EOF'
36
+ #!/usr/bin/env sh
37
+ # emulate forge-board-render.sh --emit: one line per unit
38
+ printf '{"forge_id":"m-30","status":"complete","release_state":"merged"}\n'
39
+ printf '{"forge_id":"m-99","status":"complete","release_state":"complete"}\n'
40
+ EOF
41
+ chmod +x "$BOARD_OK"
42
+
43
+ # --- recording gh stub: append its args so we can prove the trigger actually fired ------------
44
+ GH_REC="$ROOT/gh-rec.sh"
45
+ GH_LOG="$ROOT/gh-args.log"
46
+ cat > "$GH_REC" <<EOF
47
+ #!/usr/bin/env sh
48
+ printf '%s\n' "\$*" >> "$GH_LOG"
49
+ exit 0
50
+ EOF
51
+ chmod +x "$GH_REC"
52
+
53
+ # 1. dry-run SHOWS the board state BEFORE the trigger line (show-then-trigger ordering)
54
+ out="$(FORGE_JARVIS_BOARD="$BOARD_OK" FORGE_JARVIS_GH=gh sh "$PROMOTE" "$SHA" --dry-run 2>/dev/null)"
55
+ show_ln="$(printf '%s\n' "$out" | grep -n 'm-30' | head -1 | cut -d: -f1)"
56
+ trig_ln="$(printf '%s\n' "$out" | grep -n 'would run' | head -1 | cut -d: -f1)"
57
+ if [ -n "$show_ln" ] && [ -n "$trig_ln" ] && [ "$show_ln" -lt "$trig_ln" ]; then
58
+ ok "shows host state (merged unit m-30) BEFORE the trigger command"
59
+ else
60
+ bad "show-then-trigger" "show line=$show_ln trigger line=$trig_ln; out: $out"
61
+ fi
62
+
63
+ # 2. dry-run prints the step-2 seam command: gh workflow run promote.yml -f sha=<sha>
64
+ case "$out" in
65
+ *"gh workflow run promote.yml -f sha=$SHA"*) ok "dry-run prints 'gh workflow run promote.yml -f sha=<sha>' (the step-2 seam)" ;;
66
+ *) bad "trigger command" "no 'gh workflow run promote.yml -f sha=$SHA' in: $out" ;;
67
+ esac
68
+
69
+ # 3. a real (non-dry-run) run ACTUALLY invokes gh with the workflow trigger (recording stub)
70
+ : > "$GH_LOG"
71
+ FORGE_JARVIS_BOARD="$BOARD_OK" FORGE_JARVIS_GH="$GH_REC" sh "$PROMOTE" "$SHA" >/dev/null 2>&1
72
+ if grep -qF "workflow run promote.yml -f sha=$SHA" "$GH_LOG"; then
73
+ ok "real run invokes gh: 'workflow run promote.yml -f sha=<sha>' recorded"
74
+ else
75
+ bad "real trigger" "gh stub not called with the trigger args; log: $(cat "$GH_LOG" 2>/dev/null)"
76
+ fi
77
+
78
+ # 4. NEG-CONTROL — the helper has NO Environment-approval path anywhere in the file
79
+ if grep -qiE 'gh api.*environments.*approve|approve.*deployment|--approve' "$PROMOTE"; then
80
+ bad "no-approval neg-control" "an approval path exists in $PROMOTE (triggering must never approve)"
81
+ else
82
+ ok "neg-control: no approval API path in the helper (triggers, never approves)"
83
+ fi
84
+
85
+ # 5. refuses a non-full sha (never invents/truncates one)
86
+ if FORGE_JARVIS_BOARD="$BOARD_OK" sh "$PROMOTE" "abc123" --dry-run >/dev/null 2>&1; then
87
+ bad "short-sha refusal" "a short sha was accepted; promotion needs the full 40-hex sha"
88
+ else
89
+ ok "refuses a non-full (short) sha — containment needs the full deploy sha"
90
+ fi
91
+
92
+ # 6. board unavailable degrades: dry-run still renders + still prints the trigger (exit 0)
93
+ out="$(FORGE_JARVIS_BOARD="$ROOT/does-not-exist.sh" FORGE_JARVIS_GH=gh sh "$PROMOTE" "$SHA" --dry-run 2>/dev/null)"
94
+ rc=$?
95
+ case "$out" in
96
+ *"projection unavailable"*"would run: gh workflow run promote.yml"*) [ "$rc" -eq 0 ] && ok "board-down degrades: show still renders and the trigger still prints (exit 0)" || bad "degrade exit" "rc=$rc" ;;
97
+ *) bad "board-down degrade" "expected a degraded projection note + the trigger; got: $out" ;;
98
+ esac
99
+
100
+ # --- summary -----------------------------------------------------------------
101
+ printf '\n%s: %d passed, %d failed\n' "$(basename "$0")" "$PASSED" "$FAILED"
102
+ [ "$FAILED" = 0 ]
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env sh
2
+ # Fixture test suite for .forge/checks/forge-jarvis-prwatch.sh (m-37, step 4 B4(c); plan 68-02).
3
+ #
4
+ # Contract under test (handoff-step4-jarvis.md REV2 B4(c); NFR-036/Fork 3; acceptance #1/#6):
5
+ # - merged + staging-deploy success → the come-try text fires ONCE (at-most-once marker).
6
+ # - bound reached (PR stays OPEN) → clean exit, NO fire (never a standing daemon).
7
+ # - PR closed without a live deploy → self-terminate, no fire.
8
+ # - host truth only: merge + deploy come from gh (stubbed here via FORGE_JARVIS_GH).
9
+ #
10
+ # All runs use --interval 0 + a tiny --max-iterations so nothing sleeps or hangs. Each assertion
11
+ # prints WHY. RED-friendly: a missing script fails every case and exits non-zero.
12
+ set -u
13
+
14
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
15
+ PRWATCH="$SCRIPT_DIR/../forge-jarvis-prwatch.sh"
16
+ [ -f "$PRWATCH" ] || printf 'NOTE: %s missing — RED phase, every case should FAIL\n' "$PRWATCH" >&2
17
+
18
+ ROOT="$(mktemp -d)"
19
+ trap 'rm -rf "$ROOT"' EXIT INT TERM
20
+ PASSED=0; FAILED=0
21
+ ok() { PASSED=$((PASSED+1)); printf ' ok %s\n' "$1"; }
22
+ bad() { FAILED=$((FAILED+1)); printf ' FAIL %s\n %s\n' "$1" "$2"; }
23
+
24
+ # stub gh: `pr view` → $STUB_STATE ; `api .../deployments` → statuses_url / state per $STUB_DEPLOY
25
+ STUB="$ROOT/gh-stub.sh"
26
+ cat > "$STUB" <<'EOF'
27
+ #!/usr/bin/env sh
28
+ case "$1" in
29
+ pr) printf '%s\n' "${STUB_STATE:-OPEN}" ;;
30
+ api) case "$2" in
31
+ *deployments\?environment*) [ "${STUB_DEPLOY:-no}" = yes ] && printf 'https://x/statuses\n' || printf '\n' ;;
32
+ *statuses*) [ "${STUB_DEPLOY:-no}" = yes ] && printf 'success\n' || printf 'pending\n' ;;
33
+ esac ;;
34
+ esac
35
+ EOF
36
+ chmod +x "$STUB"
37
+ MK="$ROOT/markers"
38
+
39
+ run() { FORGE_JARVIS_GH="$STUB" sh "$PRWATCH" --max-iterations 3 --interval 0 --marker-dir "$MK" "$@" 2>/dev/null; }
40
+
41
+ # 1. merged + deployed → come-try fires
42
+ out="$(STUB_STATE=MERGED STUB_DEPLOY=yes run --pr 42)"
43
+ case "$out" in *"come try"*"#42"*) ok "merged + staging deploy → come-try tap fires, names the PR" ;; *) bad "come-try fire" "$out" ;; esac
44
+
45
+ # 2. at-most-once: a second run does NOT fire again (marker suppresses)
46
+ out="$(STUB_STATE=MERGED STUB_DEPLOY=yes run --pr 42)"
47
+ [ -z "$out" ] && ok "come-try is at-most-once (marker suppresses the re-fire)" || bad "double come-try" "$out"
48
+
49
+ # 3. OPEN forever → bound reached, clean exit (rc 0), NO fire (not a daemon)
50
+ out="$(STUB_STATE=OPEN run --pr 99)"; rc=$?
51
+ if [ "$rc" = 0 ] && [ -z "$out" ]; then ok "OPEN → hard bound reached, clean exit, no fire (self-terminating)"; else bad "bound self-terminate" "rc=$rc out=$out"; fi
52
+
53
+ # 4. merged but NOT deployed → within the bound it does not fire (waits for the deploy, then stops)
54
+ out="$(STUB_STATE=MERGED STUB_DEPLOY=no run --pr 55)"; rc=$?
55
+ if [ "$rc" = 0 ] && [ -z "$out" ]; then ok "merged w/o deploy → no premature come-try; bound stops it cleanly"; else bad "merged-no-deploy" "rc=$rc out=$out"; fi
56
+
57
+ # 5. CLOSED → self-terminate immediately, no fire
58
+ out="$(STUB_STATE=CLOSED run --pr 77)"; rc=$?
59
+ if [ "$rc" = 0 ] && [ -z "$out" ]; then ok "closed PR → self-terminate, no fire"; else bad "closed terminate" "rc=$rc out=$out"; fi
60
+
61
+ # 6. --dry-run prints the resolved plan and never polls
62
+ out="$(sh "$PRWATCH" --pr 7 --staging-env prod --dry-run 2>/dev/null)"
63
+ case "$out" in *"pr=7"*"env=prod"*) ok "--dry-run prints the resolved plan (pr, env, bound), no polling" ;; *) bad "dry-run plan" "$out" ;; esac
64
+
65
+ printf '\n%s: %d passed, %d failed\n' "$(basename "$0")" "$PASSED" "$FAILED"
66
+ [ "$FAILED" = 0 ]
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env sh
2
+ # Fixture test suite for .forge/checks/forge-jarvis-relay.sh (m-37, step 4 B4; plan 68-01).
3
+ #
4
+ # Contract under test (handoff-step4-jarvis.md REV2 B4(a/b/e); FR-200..202; NFR-036/040;
5
+ # acceptance #2/#3/#4):
6
+ # - each §8 type + halt → correct TYPED text; ambiguity relays the question VERBATIM.
7
+ # - HOST-TRUTH recompute wins: a payload claiming a merge the fold DENIES relays a loud
8
+ # DISCREPANCY (the standing lesson) — the state claim is never sourced from the payload.
9
+ # - markers are at-most-once (a re-scan of unchanged log re-buzzes NOTHING; a new line still
10
+ # relays — no double-buzz, no silent loss), keyed by log-line offset.
11
+ # - --mode catchup surfaces pending pings as ONE "while you were away" line, then marks them.
12
+ # - --audit-sources passes (no state claim sourced from the payload).
13
+ #
14
+ # The fold is stubbed via FORGE_JARVIS_FOLD so no real repo/main state is needed. Each assertion
15
+ # prints WHY. RED-friendly: a missing/broken script fails every case and exits non-zero.
16
+ #
17
+ # Usage: ./.forge/checks/tests/forge-jarvis-relay.test.sh [--markers] (both run by default)
18
+ set -u
19
+
20
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
21
+ RELAY="$SCRIPT_DIR/../forge-jarvis-relay.sh"
22
+ [ -f "$RELAY" ] || printf 'NOTE: %s missing — RED phase, every case should FAIL\n' "$RELAY" >&2
23
+
24
+ ROOT="$(mktemp -d)"
25
+ trap 'rm -rf "$ROOT"' EXIT INT TERM
26
+
27
+ PASSED=0; FAILED=0
28
+ ok() { PASSED=$((PASSED+1)); printf ' ok %s\n' "$1"; }
29
+ bad() { FAILED=$((FAILED+1)); printf ' FAIL %s\n %s\n' "$1" "$2"; }
30
+
31
+ # --- stub folds: echo a fixed release_state so the relay recomputes against a KNOWN host truth --
32
+ FOLD_UNMERGED="$ROOT/fold-unmerged.sh"
33
+ printf '#!/usr/bin/env sh\necho "release_state=unmerged reason=not-on-main"\n' > "$FOLD_UNMERGED"
34
+ FOLD_VALIDATED="$ROOT/fold-validated.sh"
35
+ printf '#!/usr/bin/env sh\necho "release_state=validated reason=signed-off"\n' > "$FOLD_VALIDATED"
36
+ chmod +x "$FOLD_UNMERGED" "$FOLD_VALIDATED"
37
+
38
+ # one(): run --one with a given TSV line + stub fold; echo the relay text
39
+ one() { # one <fold> <tsv-line>
40
+ FORGE_JARVIS_FOLD="$1" sh "$RELAY" --one "$2" --milestone m-37 2>/dev/null
41
+ }
42
+ TAB="$(printf '\t')"
43
+
44
+ # 1. ambiguity → the QUESTION verbatim, no host state leaked in
45
+ out="$(one "$FOLD_UNMERGED" "2026-07-21T10:00:00Z${TAB}ambiguity${TAB}JWT or sessions?")"
46
+ case "$out" in
47
+ *"JWT or sessions?"*) case "$out" in *release_state*) bad "ambiguity verbatim" "leaked host state: $out" ;; *) ok "ambiguity relays the question verbatim (not a state claim)" ;; esac ;;
48
+ *) bad "ambiguity verbatim" "question missing: $out" ;;
49
+ esac
50
+
51
+ # 2. fork → names the sketch path
52
+ out="$(one "$FOLD_UNMERGED" "2026-07-21T10:00:00Z${TAB}fork${TAB}/tmp/sketch-a.png")"
53
+ case "$out" in *"/tmp/sketch-a.png"*) ok "fork names the sketch path to look at" ;; *) bad "fork path" "$out" ;; esac
54
+
55
+ # 3. irreversible → verbatim, named
56
+ out="$(one "$FOLD_UNMERGED" "2026-07-21T10:00:00Z${TAB}irreversible${TAB}force-push to main?")"
57
+ case "$out" in *"force-push to main?"*) ok "irreversible relays the action, named" ;; *) bad "irreversible" "$out" ;; esac
58
+
59
+ # 4. budget → verbatim, named (runner's own tally — no host source)
60
+ out="$(one "$FOLD_UNMERGED" "2026-07-21T10:00:00Z${TAB}budget${TAB}cap \$25 reached")"
61
+ case "$out" in *"cap \$25 reached"*) ok "budget relays the runner's tally, named" ;; *) bad "budget" "$out" ;; esac
62
+
63
+ # 5. honest done (payload makes NO merge claim), fold=unmerged → host truth, NO discrepancy
64
+ out="$(one "$FOLD_UNMERGED" "2026-07-21T10:00:00Z${TAB}done${TAB}Phases complete, PR opened")"
65
+ case "$out" in
66
+ *DISCREPANCY*) bad "honest done" "false discrepancy on an honest ping: $out" ;;
67
+ *"release_state=unmerged"*) ok "honest done phrased from host release_state (no false discrepancy)" ;;
68
+ *) bad "honest done" "no host state in text: $out" ;;
69
+ esac
70
+
71
+ # 6. LYING halt: payload claims merged, fold says unmerged → LOUD discrepancy, host wins
72
+ out="$(one "$FOLD_UNMERGED" "2026-07-21T10:00:00Z${TAB}halt${TAB}All done and merged to main!")"
73
+ case "$out" in
74
+ *DISCREPANCY*"release_state=unmerged"*) ok "lying halt → discrepancy; host truth (unmerged) wins over the payload's merge claim" ;;
75
+ *) bad "lying halt discrepancy" "expected DISCREPANCY + release_state=unmerged, got: $out" ;;
76
+ esac
77
+
78
+ # 7. done claims merged AND fold=validated → host CONFIRMS, no discrepancy
79
+ out="$(one "$FOLD_VALIDATED" "2026-07-21T10:00:00Z${TAB}done${TAB}merged and validated")"
80
+ case "$out" in
81
+ *DISCREPANCY*) bad "confirmed merge" "false discrepancy when host agrees: $out" ;;
82
+ *"release_state=validated"*) ok "merge claim + host agrees (validated) → confirmed, no discrepancy" ;;
83
+ *) bad "confirmed merge" "$out" ;;
84
+ esac
85
+
86
+ # 8. --audit-sources passes (no state claim sourced from the payload)
87
+ if FORGE_JARVIS_FOLD="$FOLD_UNMERGED" sh "$RELAY" --audit-sources >/dev/null 2>&1; then
88
+ ok "--audit-sources: relay speaks host truth only"
89
+ else
90
+ bad "--audit-sources" "static source audit failed"
91
+ fi
92
+
93
+ # --- markers: at-most-once, no double-buzz, no silent loss ------------------------------------
94
+ WT="$ROOT/slice-wt"; mkdir -p "$WT/.forge/runner-work"
95
+ LOG="$WT/.forge/runner-work/notify.log"; MK="$WT/.forge/runner-work/relay-markers"
96
+ printf '2026-07-21T10:00:00Z\tambiguity\tfirst?\n' >> "$LOG"
97
+ printf '2026-07-21T10:01:00Z\tdone\tPR opened\n' >> "$LOG"
98
+
99
+ scan() { FORGE_JARVIS_FOLD="$FOLD_UNMERGED" sh "$RELAY" --scan "$LOG" --marker-dir "$MK" --milestone m-37 2>/dev/null; }
100
+
101
+ # 9. first scan relays BOTH lines
102
+ out="$(scan)"; n="$(printf '%s\n' "$out" | grep -c .)"
103
+ [ "$n" -eq 2 ] && ok "first --scan relays all unmarked lines ($n)" || bad "first scan count" "expected 2 got $n: $out"
104
+
105
+ # 10. re-scan of UNCHANGED log relays NOTHING (at-most-once — no double-buzz)
106
+ out="$(scan)"; n="$(printf '%s\n' "$out" | grep -c .)"
107
+ [ "$n" -eq 0 ] && ok "re-scan of unchanged log re-buzzes nothing (at-most-once markers)" || bad "double-buzz" "expected 0 got $n: $out"
108
+
109
+ # 11. a NEW line still relays (no silent loss of a genuinely-new ping)
110
+ printf '2026-07-21T10:02:00Z\thalt\tstuck\n' >> "$LOG"
111
+ out="$(scan)"; n="$(printf '%s\n' "$out" | grep -c .)"
112
+ [ "$n" -eq 1 ] && ok "a new line still relays after prior lines were marked (no silent loss)" || bad "new-line relay" "expected 1 got $n: $out"
113
+
114
+ # 12. markers exist keyed by offset (line number)
115
+ if [ -f "$MK/1.relayed" ] && [ -f "$MK/2.relayed" ] && [ -f "$MK/3.relayed" ]; then
116
+ ok "markers written keyed by log-line offset (1,2,3)"
117
+ else
118
+ bad "offset markers" "missing marker(s): $(ls "$MK" 2>/dev/null | tr '\n' ' ')"
119
+ fi
120
+
121
+ # 13. --mode catchup surfaces pending pings as ONE while-you-were-away line
122
+ WT2="$ROOT/slice-wt2"; mkdir -p "$WT2/.forge/runner-work"
123
+ LOG2="$WT2/.forge/runner-work/notify.log"
124
+ printf '2026-07-21T09:00:00Z\tbudget\tcap hit\n' >> "$LOG2"
125
+ printf '2026-07-21T09:05:00Z\tirreversible\tdrop table?\n' >> "$LOG2"
126
+ out="$(FORGE_JARVIS_FOLD="$FOLD_UNMERGED" sh "$RELAY" --scan "$LOG2" --milestone m-37 --mode catchup 2>/dev/null)"
127
+ lines="$(printf '%s\n' "$out" | grep -c .)"
128
+ case "$out" in
129
+ *"While you were away"*) [ "$lines" -eq 1 ] && ok "catchup surfaces pending pings as ONE while-you-were-away line" || bad "catchup one-line" "got $lines lines: $out" ;;
130
+ *) bad "catchup summary" "no while-you-were-away line: $out" ;;
131
+ esac
132
+
133
+ # --- summary -----------------------------------------------------------------
134
+ printf '\n%s: %d passed, %d failed\n' "$(basename "$0")" "$PASSED" "$FAILED"
135
+ [ "$FAILED" = 0 ]