forge-orkes 0.70.0 → 0.72.2
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/package.json +1 -1
- package/template/.claude/hooks/forge-model-outcome.sh +5 -3
- package/template/.claude/hooks/forge-reserve.sh +127 -15
- package/template/.claude/hooks/forge-slice-notify.sh +12 -3
- package/template/.claude/hooks/forge-slice-runner.sh +40 -19
- package/template/.claude/skills/jarvis/SKILL.md +2 -0
- package/template/.claude/skills/notion-integration/SKILL.md +1 -0
- package/template/.claude/skills/planning/SKILL.md +3 -3
- package/template/.forge/FORGE.md +2 -2
- package/template/.forge/checks/forge-board-render.sh +21 -18
- package/template/.forge/checks/forge-check-lint.sh +36 -2
- package/template/.forge/checks/forge-hold-check.sh +121 -2
- package/template/.forge/checks/forge-jarvis-dispatch.sh +236 -12
- package/template/.forge/checks/forge-jarvis.sh +28 -0
- package/template/.forge/checks/forge-roadmap-check.sh +106 -22
- package/template/.forge/checks/tests/forge-check-lint.test.sh +126 -4
- package/template/.forge/checks/tests/forge-hold-check.test.sh +166 -1
- package/template/.forge/checks/tests/forge-jarvis-dispatch.test.sh +211 -0
- package/template/.forge/checks/tests/forge-jarvis.test.sh +78 -4
- package/template/.forge/checks/tests/forge-reserve.test.sh +235 -0
- package/template/.forge/checks/tests/forge-roadmap-check.test.sh +487 -0
- package/template/.forge/templates/hold-config.yml +15 -0
- package/template/.forge/templates/project.yml +10 -0
- package/template/.forge/templates/waive-hold.yml +63 -0
|
@@ -192,6 +192,217 @@ else
|
|
|
192
192
|
bad "numeric-id branch pick" "rc=$rc err=$err14"
|
|
193
193
|
fi
|
|
194
194
|
|
|
195
|
+
# --- 15-18. --plan WORK-ORDER SCOPING (FR-234; canvaz desire path 2026-07-22 defect 2:
|
|
196
|
+
# materialization bundled ALL phase dirs + ALL plan-NN.md, so a mid-milestone re-dispatch
|
|
197
|
+
# re-bundled landed work). --plan <phase> takes a whole phase, <phase>/plan-NN one plan
|
|
198
|
+
# file; unknown specs and hand-authored slice.yml refuse loudly. mktemp fixtures — a
|
|
199
|
+
# single milestone dir exercises the (branch-free) fallback pick.
|
|
200
|
+
|
|
201
|
+
# 15. single-plan spec: only the named plan file lands in the phase's plan.md
|
|
202
|
+
PSLICE="$ROOT/plan-one-wt"
|
|
203
|
+
mkdir -p "$PSLICE/.forge/phases/milestone-77/1-aaa"
|
|
204
|
+
printf 'LANDED plan one\n' > "$PSLICE/.forge/phases/milestone-77/1-aaa/plan-01.md"
|
|
205
|
+
printf 'NEXT plan two\n' > "$PSLICE/.forge/phases/milestone-77/1-aaa/plan-02.md"
|
|
206
|
+
err15="$(sh "$DISPATCH" --slice "$PSLICE" --materialize-only --plan 1-aaa/plan-02 2>&1)"; rc=$?
|
|
207
|
+
if [ "$rc" = 0 ] && grep -q '^ - 1-aaa$' "$PSLICE/slice.yml" 2>/dev/null \
|
|
208
|
+
&& grep -q 'NEXT plan two' "$PSLICE/phases/1-aaa/plan.md" 2>/dev/null \
|
|
209
|
+
&& ! grep -q 'LANDED plan one' "$PSLICE/phases/1-aaa/plan.md" 2>/dev/null; then
|
|
210
|
+
ok "--plan <phase>/plan-NN scopes to exactly that plan (landed sibling NOT re-bundled)"
|
|
211
|
+
else
|
|
212
|
+
bad "--plan single-plan scoping" "rc=$rc err=$err15 plan.md=$(cat "$PSLICE/phases/1-aaa/plan.md" 2>/dev/null)"
|
|
213
|
+
fi
|
|
214
|
+
|
|
215
|
+
# 16. whole-phase specs + ordering: numeric slice order regardless of flag order; unselected
|
|
216
|
+
# phases do NOT materialize
|
|
217
|
+
OSLICE="$ROOT/plan-order-wt"
|
|
218
|
+
mkdir -p "$OSLICE/.forge/phases/milestone-77/71-aaa" "$OSLICE/.forge/phases/milestone-77/72-bbb" "$OSLICE/.forge/phases/milestone-77/73-ccc"
|
|
219
|
+
printf 'plan A\n' > "$OSLICE/.forge/phases/milestone-77/71-aaa/plan-01.md"
|
|
220
|
+
printf 'plan B\n' > "$OSLICE/.forge/phases/milestone-77/72-bbb/plan-01.md"
|
|
221
|
+
printf 'decoy\n' > "$OSLICE/.forge/phases/milestone-77/73-ccc/plan-01.md"
|
|
222
|
+
err16="$(sh "$DISPATCH" --slice "$OSLICE" --materialize-only --plan 72-bbb --plan 71-aaa 2>&1)"; rc=$?
|
|
223
|
+
if [ "$rc" = 0 ] \
|
|
224
|
+
&& [ "$(grep -E '^ - ' "$OSLICE/slice.yml" 2>/dev/null | tr -d ' -')" = "71aaa
|
|
225
|
+
72bbb" ] \
|
|
226
|
+
&& grep -q 'plan A' "$OSLICE/phases/71-aaa/plan.md" 2>/dev/null \
|
|
227
|
+
&& grep -q 'plan B' "$OSLICE/phases/72-bbb/plan.md" 2>/dev/null \
|
|
228
|
+
&& [ ! -d "$OSLICE/phases/73-ccc" ]; then
|
|
229
|
+
ok "--plan whole-phase specs keep numeric order (flag order ignored); unselected phase skipped"
|
|
230
|
+
else
|
|
231
|
+
bad "--plan ordering" "rc=$rc err=$err16 slice.yml=$(cat "$OSLICE/slice.yml" 2>/dev/null)"
|
|
232
|
+
fi
|
|
233
|
+
|
|
234
|
+
# 17. NEG: unknown spec → loud refusal, no slice.yml (a typo must not dispatch the wrong order)
|
|
235
|
+
USLICE="$ROOT/plan-unknown-wt"
|
|
236
|
+
mkdir -p "$USLICE/.forge/phases/milestone-77/1-aaa"
|
|
237
|
+
printf 'x\n' > "$USLICE/.forge/phases/milestone-77/1-aaa/plan-01.md"
|
|
238
|
+
err17="$(sh "$DISPATCH" --slice "$USLICE" --materialize-only --plan 2-nope 2>&1)"; rc=$?
|
|
239
|
+
if [ "$rc" != 0 ] && [ ! -f "$USLICE/slice.yml" ] && printf '%s' "$err17" | grep -q 'REFUSED'; then
|
|
240
|
+
ok "neg-control: unknown --plan spec → loud refusal, no slice.yml"
|
|
241
|
+
else
|
|
242
|
+
bad "unknown --plan spec" "rc=$rc err=$err17"
|
|
243
|
+
fi
|
|
244
|
+
|
|
245
|
+
# 18. NEG: --plan vs a HAND-AUTHORED slice.yml (no materializer header) → refusal; slice.yml
|
|
246
|
+
# byte-unchanged, seeded runner-work untouched (hand-authored work orders win, always)
|
|
247
|
+
HPSLICE="$ROOT/plan-hand-wt"
|
|
248
|
+
mkdir -p "$HPSLICE/.forge/phases/milestone-77/1-aaa" "$HPSLICE/.forge/runner-work/pings"
|
|
249
|
+
printf 'x\n' > "$HPSLICE/.forge/phases/milestone-77/1-aaa/plan-01.md"
|
|
250
|
+
printf 'phases:\n - hand-rolled\n' > "$HPSLICE/slice.yml"
|
|
251
|
+
touch "$HPSLICE/.forge/runner-work/pings/k-done.marker"
|
|
252
|
+
before18="$(cat "$HPSLICE/slice.yml")"
|
|
253
|
+
err18="$(sh "$DISPATCH" --slice "$HPSLICE" --materialize-only --plan 1-aaa 2>&1)"; rc=$?
|
|
254
|
+
if [ "$rc" != 0 ] && printf '%s' "$err18" | grep -q 'REFUSED' \
|
|
255
|
+
&& [ "$(cat "$HPSLICE/slice.yml")" = "$before18" ] \
|
|
256
|
+
&& [ -f "$HPSLICE/.forge/runner-work/pings/k-done.marker" ]; then
|
|
257
|
+
ok "neg-control: --plan refuses a hand-authored slice.yml; nothing touched"
|
|
258
|
+
else
|
|
259
|
+
bad "--plan vs hand-authored" "rc=$rc err=$err18"
|
|
260
|
+
fi
|
|
261
|
+
|
|
262
|
+
# 18b. NEG (path hygiene): a plan part carrying a separator (traversal shape) refuses —
|
|
263
|
+
# sh case patterns let * match /, so plan-../x.md would pass the plan-*.md form check
|
|
264
|
+
# and reach outside the milestone dir without this guard.
|
|
265
|
+
TSLICE="$ROOT/plan-traverse-wt"
|
|
266
|
+
mkdir -p "$TSLICE/.forge/phases/milestone-77/1-aaa"
|
|
267
|
+
printf 'x\n' > "$TSLICE/.forge/phases/milestone-77/1-aaa/plan-01.md"
|
|
268
|
+
err18b="$(sh "$DISPATCH" --slice "$TSLICE" --materialize-only --plan '1-aaa/plan-../x' 2>&1)"; rc=$?
|
|
269
|
+
if [ "$rc" != 0 ] && [ ! -f "$TSLICE/slice.yml" ] && printf '%s' "$err18b" | grep -q 'REFUSED'; then
|
|
270
|
+
ok "neg-control: --plan file part with a path separator → loud refusal (no traversal)"
|
|
271
|
+
else
|
|
272
|
+
bad "--plan traversal guard" "rc=$rc err=$err18b"
|
|
273
|
+
fi
|
|
274
|
+
|
|
275
|
+
# --- 19-21. RUNNER-WORK ROTATION (FR-235; canvaz desire path 2026-07-22 defect 3: stale
|
|
276
|
+
# pings/{key}-{kind}.marker silenced the new run's done ping and index-keyed
|
|
277
|
+
# phase-{idx}/phase-report.yml verdict:done short-circuited the new phase). A NEW work
|
|
278
|
+
# order rotates runner-work WHOLE (relay-markers are line-number-keyed siblings of
|
|
279
|
+
# notify.log — a partial reset would misalign offsets); resume paths keep slice.yml and
|
|
280
|
+
# are never rotated; archive, never delete.
|
|
281
|
+
|
|
282
|
+
# 19. materializing a NEW order rotates seeded state into runner-work-archive; a replaced
|
|
283
|
+
# helper-materialized slice.yml is archived alongside
|
|
284
|
+
RSLICE="$ROOT/rotate-wt"
|
|
285
|
+
mkdir -p "$RSLICE/.forge/phases/milestone-77/1-aaa"
|
|
286
|
+
printf 'LANDED\n' > "$RSLICE/.forge/phases/milestone-77/1-aaa/plan-01.md"
|
|
287
|
+
printf 'NEXT\n' > "$RSLICE/.forge/phases/milestone-77/1-aaa/plan-02.md"
|
|
288
|
+
sh "$DISPATCH" --slice "$RSLICE" --materialize-only >/dev/null 2>&1 # first order (all plans) — seeds a materialized slice.yml
|
|
289
|
+
# Seed the "run happened" state AFTER the first materialization (a dispatch with stale
|
|
290
|
+
# runner-work already present would rotate it — exactly the behavior under test in 19).
|
|
291
|
+
mkdir -p "$RSLICE/.forge/runner-work/pings" "$RSLICE/.forge/runner-work/phase-1" "$RSLICE/.forge/runner-work/relay-markers"
|
|
292
|
+
touch "$RSLICE/.forge/runner-work/pings/k-done.marker"
|
|
293
|
+
printf 'verdict: done\n' > "$RSLICE/.forge/runner-work/phase-1/phase-report.yml"
|
|
294
|
+
touch "$RSLICE/.forge/runner-work/relay-markers/1.relayed"
|
|
295
|
+
err19="$(sh "$DISPATCH" --slice "$RSLICE" --materialize-only --plan 1-aaa/plan-02 2>&1)"; rc=$?
|
|
296
|
+
if [ "$rc" = 0 ] \
|
|
297
|
+
&& [ ! -e "$RSLICE/.forge/runner-work/pings/k-done.marker" ] \
|
|
298
|
+
&& [ ! -e "$RSLICE/.forge/runner-work/phase-1/phase-report.yml" ] \
|
|
299
|
+
&& ls "$RSLICE/.forge/runner-work-archive/"*/pings/k-done.marker >/dev/null 2>&1 \
|
|
300
|
+
&& ls "$RSLICE/.forge/runner-work-archive/"*/phase-1/phase-report.yml >/dev/null 2>&1 \
|
|
301
|
+
&& ls "$RSLICE/.forge/runner-work-archive/"*/relay-markers/1.relayed >/dev/null 2>&1 \
|
|
302
|
+
&& ls "$RSLICE/.forge/runner-work-archive/"*/slice.yml >/dev/null 2>&1 \
|
|
303
|
+
&& grep -q 'NEXT' "$RSLICE/phases/1-aaa/plan.md" 2>/dev/null \
|
|
304
|
+
&& ! grep -q 'LANDED' "$RSLICE/phases/1-aaa/plan.md" 2>/dev/null; then
|
|
305
|
+
ok "new work order rotates runner-work WHOLE (pings+report+relay-markers) + archives replaced slice.yml"
|
|
306
|
+
else
|
|
307
|
+
bad "rotation on new order" "rc=$rc err=$err19"
|
|
308
|
+
fi
|
|
309
|
+
|
|
310
|
+
# 20. NEG (resume preserved): existing materialized slice.yml, NO --plan → honored as-is,
|
|
311
|
+
# runner-work untouched, no archive (crash-resume / park→answer→resume path)
|
|
312
|
+
KSLICE="$ROOT/keep-wt"
|
|
313
|
+
mkdir -p "$KSLICE/.forge/phases/milestone-77/1-aaa"
|
|
314
|
+
printf 'x\n' > "$KSLICE/.forge/phases/milestone-77/1-aaa/plan-01.md"
|
|
315
|
+
sh "$DISPATCH" --slice "$KSLICE" --materialize-only >/dev/null 2>&1 # materialized header slice.yml
|
|
316
|
+
mkdir -p "$KSLICE/.forge/runner-work/pings" # seed AFTER — see case 19 note
|
|
317
|
+
touch "$KSLICE/.forge/runner-work/pings/k-done.marker"
|
|
318
|
+
before20="$(cat "$KSLICE/slice.yml")"
|
|
319
|
+
err20="$(sh "$DISPATCH" --slice "$KSLICE" --materialize-only 2>&1)"; rc=$?
|
|
320
|
+
if [ "$rc" = 0 ] && [ "$(cat "$KSLICE/slice.yml")" = "$before20" ] \
|
|
321
|
+
&& [ -f "$KSLICE/.forge/runner-work/pings/k-done.marker" ] \
|
|
322
|
+
&& [ ! -d "$KSLICE/.forge/runner-work-archive" ]; then
|
|
323
|
+
ok "neg-control: existing slice.yml honored → NO rotation (resume state survives)"
|
|
324
|
+
else
|
|
325
|
+
bad "resume preserved" "rc=$rc err=$err20"
|
|
326
|
+
fi
|
|
327
|
+
|
|
328
|
+
# 21. NEG: --dry-run stays fully side-effect-free — no slice.yml, no rotation, even with
|
|
329
|
+
# stale runner-work present (extends case 12)
|
|
330
|
+
YSLICE="$ROOT/dry-rotate-wt"
|
|
331
|
+
mkdir -p "$YSLICE/.forge/phases/milestone-77/1-aaa" "$YSLICE/.forge/runner-work/pings"
|
|
332
|
+
printf 'x\n' > "$YSLICE/.forge/phases/milestone-77/1-aaa/plan-01.md"
|
|
333
|
+
touch "$YSLICE/.forge/runner-work/pings/k-done.marker"
|
|
334
|
+
sh "$DISPATCH" --slice "$YSLICE" --dry-run >/dev/null 2>&1
|
|
335
|
+
if [ ! -f "$YSLICE/slice.yml" ] && [ ! -d "$YSLICE/.forge/runner-work-archive" ] \
|
|
336
|
+
&& [ -f "$YSLICE/.forge/runner-work/pings/k-done.marker" ]; then
|
|
337
|
+
ok "neg-control: --dry-run rotates nothing, writes nothing"
|
|
338
|
+
else
|
|
339
|
+
bad "dry-run rotation" "dry-run had side effects"
|
|
340
|
+
fi
|
|
341
|
+
|
|
342
|
+
# 22. NEG: --work-dir refused loudly BEFORE the detach (R092) — a forwarded work dir
|
|
343
|
+
# would split-brain against the pinned notify.log/dispatch.out/rotation paths.
|
|
344
|
+
# Both arg forms; no slice.yml materialized, nothing dispatched.
|
|
345
|
+
WSLICE="$ROOT/workdir-wt"
|
|
346
|
+
mkdir -p "$WSLICE/.forge/phases/milestone-78/1-bbb"
|
|
347
|
+
printf 'x\n' > "$WSLICE/.forge/phases/milestone-78/1-bbb/plan-01.md"
|
|
348
|
+
err22="$(sh "$DISPATCH" --slice "$WSLICE" --work-dir "$ROOT/elsewhere" 2>&1 >/dev/null)"; rc=$?
|
|
349
|
+
err22b="$(sh "$DISPATCH" --slice "$WSLICE" --work-dir="$ROOT/elsewhere" 2>&1 >/dev/null)"; rc2=$?
|
|
350
|
+
if [ "$rc" = 2 ] && [ "$rc2" = 2 ] && [ ! -f "$WSLICE/slice.yml" ] \
|
|
351
|
+
&& printf '%s' "$err22" | grep -q 'REFUSED' && printf '%s' "$err22b" | grep -q 'REFUSED'; then
|
|
352
|
+
ok "neg-control: --work-dir refused (both forms), nothing materialized"
|
|
353
|
+
else
|
|
354
|
+
bad "work-dir refusal" "rc=$rc rc2=$rc2 err=$err22"
|
|
355
|
+
fi
|
|
356
|
+
|
|
357
|
+
# --- 23-24. STALENESS GUARD (live 2026-07-23: an amended source plan never reached the
|
|
358
|
+
# materialized work order — plain re-dispatch silently rebuilt the OLD spec). The
|
|
359
|
+
# materializer fingerprints each bundled plan-NN.md (`# source: ... cksum=N`); a plain
|
|
360
|
+
# re-dispatch re-checks them and refuses loudly on divergence. Refuse, never auto-rebuild
|
|
361
|
+
# (the honored-as-is branch serves resume paths whose runner-work must survive).
|
|
362
|
+
|
|
363
|
+
# 23. NEG: amend a source plan after materialization → plain re-dispatch REFUSES loudly;
|
|
364
|
+
# slice.yml + runner-work untouched (no rotation — the operator decides)
|
|
365
|
+
ZSLICE="$ROOT/stale-wt"
|
|
366
|
+
mkdir -p "$ZSLICE/.forge/phases/milestone-77/1-aaa"
|
|
367
|
+
printf 'original spec\n' > "$ZSLICE/.forge/phases/milestone-77/1-aaa/plan-01.md"
|
|
368
|
+
sh "$DISPATCH" --slice "$ZSLICE" --materialize-only >/dev/null 2>&1
|
|
369
|
+
mkdir -p "$ZSLICE/.forge/runner-work/pings" # seed AFTER — see case 19 note
|
|
370
|
+
touch "$ZSLICE/.forge/runner-work/pings/k-done.marker"
|
|
371
|
+
printf 'AMENDED spec\n' > "$ZSLICE/.forge/phases/milestone-77/1-aaa/plan-01.md"
|
|
372
|
+
before23="$(cat "$ZSLICE/slice.yml")"
|
|
373
|
+
err23="$(sh "$DISPATCH" --slice "$ZSLICE" --materialize-only 2>&1)"; rc=$?
|
|
374
|
+
if [ "$rc" != 0 ] && printf '%s' "$err23" | grep -q 'REFUSED' && printf '%s' "$err23" | grep -q 'STALE' \
|
|
375
|
+
&& printf '%s' "$err23" | grep -q '1-aaa/plan-01.md' \
|
|
376
|
+
&& [ "$(cat "$ZSLICE/slice.yml")" = "$before23" ] \
|
|
377
|
+
&& [ -f "$ZSLICE/.forge/runner-work/pings/k-done.marker" ] \
|
|
378
|
+
&& grep -q 'original spec' "$ZSLICE/phases/1-aaa/plan.md" 2>/dev/null; then
|
|
379
|
+
ok "staleness guard: amended source plan → plain re-dispatch refuses loudly, nothing touched"
|
|
380
|
+
else
|
|
381
|
+
bad "staleness guard" "rc=$rc err=$err23"
|
|
382
|
+
fi
|
|
383
|
+
|
|
384
|
+
# 23b. the remedy the refusal names: --plan re-materializes the amended spec (and case 20's
|
|
385
|
+
# unchanged-source plain re-dispatch already proves the guard stays quiet when fresh)
|
|
386
|
+
err23b="$(sh "$DISPATCH" --slice "$ZSLICE" --materialize-only --plan 1-aaa 2>&1)"; rc=$?
|
|
387
|
+
if [ "$rc" = 0 ] && grep -q 'AMENDED spec' "$ZSLICE/phases/1-aaa/plan.md" 2>/dev/null; then
|
|
388
|
+
ok "staleness remedy: --plan rebuilds the work order to the amended spec"
|
|
389
|
+
else
|
|
390
|
+
bad "staleness remedy" "rc=$rc err=$err23b"
|
|
391
|
+
fi
|
|
392
|
+
|
|
393
|
+
# 24. NEG (lazy migration): a pre-guard materialized slice.yml (helper header, NO
|
|
394
|
+
# fingerprint lines) is still honored as-is — the guard never bricks an in-flight order
|
|
395
|
+
GSLICE="$ROOT/legacy-wt"
|
|
396
|
+
mkdir -p "$GSLICE/.forge/phases/milestone-77/1-aaa" "$GSLICE/phases/1-aaa"
|
|
397
|
+
printf 'x\n' > "$GSLICE/.forge/phases/milestone-77/1-aaa/plan-01.md"
|
|
398
|
+
printf 'x\n' > "$GSLICE/phases/1-aaa/plan.md"
|
|
399
|
+
printf '# Materialized by forge-jarvis-dispatch from .forge/phases/milestone-77 (delete to re-materialize)\nphases:\n - 1-aaa\n' > "$GSLICE/slice.yml"
|
|
400
|
+
err24="$(sh "$DISPATCH" --slice "$GSLICE" --materialize-only 2>&1)"; rc=$?
|
|
401
|
+
if [ "$rc" = 0 ] && printf '%s' "$err24" | grep -q 'using it as-is'; then
|
|
402
|
+
ok "lazy migration: pre-guard materialized slice.yml (no fingerprints) honored as-is"
|
|
403
|
+
else
|
|
404
|
+
bad "legacy materialized order" "rc=$rc err=$err24"
|
|
405
|
+
fi
|
|
195
406
|
# --- summary -----------------------------------------------------------------
|
|
196
407
|
printf '\n%s: %d passed, %d failed\n' "$(basename "$0")" "$PASSED" "$FAILED"
|
|
197
408
|
[ "$FAILED" = 0 ]
|
|
@@ -13,11 +13,17 @@
|
|
|
13
13
|
# mode (post-UAT 2026-07-22): jarvis.spawn_permission_mode appends --permission-mode,
|
|
14
14
|
# env FORGE_JARVIS_SPAWN_PERMISSION_MODE beats config, unset = no flag. Refusals:
|
|
15
15
|
# jarvis.enabled: false, unknown spawn_permission_mode, or not a git repo →
|
|
16
|
-
# non-zero exit, NO command on stdout.
|
|
16
|
+
# non-zero exit, NO command on stdout. Double-start guard (firstmate-review
|
|
17
|
+
# adoption 2026-07-22): on REAL launch a live `claude remote-control` already
|
|
18
|
+
# holding the resolved sticky name → refusal naming the live pid; exact --name
|
|
19
|
+
# token match (a live prefix-sibling never blocks); unreadable ps → warn +
|
|
20
|
+
# proceed. --dry-run never runs the guard.
|
|
17
21
|
#
|
|
18
|
-
# Builds throwaway git repos in a mktemp dir, offline by construction;
|
|
19
|
-
#
|
|
20
|
-
#
|
|
22
|
+
# Builds throwaway git repos in a mktemp dir, offline by construction; dry-run
|
|
23
|
+
# cases need no `claude`/`caffeinate` (dry-run is pure resolution — that
|
|
24
|
+
# determinism is itself part of the contract), and the real-launch guard cases
|
|
25
|
+
# stub the ps read via FORGE_JARVIS_PS and put a fake `claude` first on PATH so
|
|
26
|
+
# fall-through provably reaches the launch without ever starting a real server.
|
|
21
27
|
#
|
|
22
28
|
# Usage: ./.forge/checks/tests/forge-jarvis.test.sh
|
|
23
29
|
# RED-phase friendly: a missing/non-executable script fails every case, exits non-zero.
|
|
@@ -176,6 +182,74 @@ else
|
|
|
176
182
|
bad "neg-control non-repo" "rc=$rc out=$out"
|
|
177
183
|
fi
|
|
178
184
|
|
|
185
|
+
# --- double-start guard (real launch; firstmate-review adoption 2026-07-22) ---
|
|
186
|
+
# Reset the fixture to a clean enabled config (sticky base is jarvis-proj again).
|
|
187
|
+
cat > "$REPO/.forge/project.yml" <<'EOF'
|
|
188
|
+
jarvis:
|
|
189
|
+
enabled: true
|
|
190
|
+
EOF
|
|
191
|
+
# Fake `claude` first on PATH: fall-through cases provably REACH the launch
|
|
192
|
+
# (marker file written) without ever starting a real server. caffeinate, when
|
|
193
|
+
# present, resolves `claude` through the same inherited PATH → still the fake.
|
|
194
|
+
FAKEBIN="$ROOT/fakebin"
|
|
195
|
+
mkdir -p "$FAKEBIN"
|
|
196
|
+
cat > "$FAKEBIN/claude" <<EOF
|
|
197
|
+
#!/bin/sh
|
|
198
|
+
printf '%s\n' "\$*" > "$ROOT/launch.args"
|
|
199
|
+
exit 0
|
|
200
|
+
EOF
|
|
201
|
+
chmod +x "$FAKEBIN/claude"
|
|
202
|
+
|
|
203
|
+
# 13. a live same-name server → refusal naming the live pid, nothing launched
|
|
204
|
+
STUB_DUP="$ROOT/stub-ps-dup"
|
|
205
|
+
cat > "$STUB_DUP" <<'EOF'
|
|
206
|
+
#!/bin/sh
|
|
207
|
+
printf ' 4242 caffeinate -s claude remote-control --name jarvis-proj --spawn worktree --capacity 4\n'
|
|
208
|
+
printf ' 4243 claude remote-control --name jarvis-proj --spawn worktree --capacity 4\n'
|
|
209
|
+
EOF
|
|
210
|
+
chmod +x "$STUB_DUP"
|
|
211
|
+
rm -f "$ROOT/launch.args"
|
|
212
|
+
out="$(cd "$REPO" && FORGE_JARVIS_PS="$STUB_DUP" PATH="$FAKEBIN:$PATH" sh "$JARVIS" 2>/dev/null)"; rc=$?
|
|
213
|
+
err="$(cd "$REPO" && FORGE_JARVIS_PS="$STUB_DUP" PATH="$FAKEBIN:$PATH" sh "$JARVIS" 2>&1 >/dev/null)"
|
|
214
|
+
if [ "$rc" != 0 ] && [ -z "$out" ] && [ ! -f "$ROOT/launch.args" ]; then
|
|
215
|
+
case "$err" in
|
|
216
|
+
*jarvis-proj*4242*) ok "double-start guard: live same-name server → refusal naming the live pid" ;;
|
|
217
|
+
*) bad "double-start refusal message" "err=$err" ;;
|
|
218
|
+
esac
|
|
219
|
+
else
|
|
220
|
+
bad "double-start guard" "rc=$rc out=$out launched=$([ -f "$ROOT/launch.args" ] && echo yes || echo no)"
|
|
221
|
+
fi
|
|
222
|
+
|
|
223
|
+
# 14. NEG: prefix near-miss (jarvis-proj-personal live) must NOT trip the guard —
|
|
224
|
+
# exact --name token match; fall-through provably reaches the launch
|
|
225
|
+
STUB_NEAR="$ROOT/stub-ps-nearmiss"
|
|
226
|
+
cat > "$STUB_NEAR" <<'EOF'
|
|
227
|
+
#!/bin/sh
|
|
228
|
+
printf ' 5151 caffeinate -s claude remote-control --name jarvis-proj-personal --spawn worktree --capacity 4\n'
|
|
229
|
+
printf ' 5152 vim notes-on-remote-control.md\n'
|
|
230
|
+
EOF
|
|
231
|
+
chmod +x "$STUB_NEAR"
|
|
232
|
+
rm -f "$ROOT/launch.args"
|
|
233
|
+
out="$(cd "$REPO" && FORGE_JARVIS_PS="$STUB_NEAR" PATH="$FAKEBIN:$PATH" sh "$JARVIS" 2>/dev/null)"; rc=$?
|
|
234
|
+
if [ "$rc" = 0 ] && [ -f "$ROOT/launch.args" ] && grep -q -- "--name jarvis-proj --spawn" "$ROOT/launch.args"; then
|
|
235
|
+
ok "neg-control: live prefix-sibling name does not block — exact token match, launch reached"
|
|
236
|
+
else
|
|
237
|
+
bad "neg-control prefix near-miss" "rc=$rc launched=$([ -f "$ROOT/launch.args" ] && cat "$ROOT/launch.args" || echo no)"
|
|
238
|
+
fi
|
|
239
|
+
|
|
240
|
+
# 15. unreadable ps → warn + proceed (advisory hygiene, never a hard fail)
|
|
241
|
+
rm -f "$ROOT/launch.args"
|
|
242
|
+
out="$(cd "$REPO" && FORGE_JARVIS_PS="$ROOT/no-such-stub" PATH="$FAKEBIN:$PATH" sh "$JARVIS" 2>/dev/null)"; rc=$?
|
|
243
|
+
err="$(cd "$REPO" && FORGE_JARVIS_PS="$ROOT/no-such-stub" PATH="$FAKEBIN:$PATH" sh "$JARVIS" 2>&1 >/dev/null)"
|
|
244
|
+
if [ "$rc" = 0 ] && [ -f "$ROOT/launch.args" ]; then
|
|
245
|
+
case "$err" in
|
|
246
|
+
*"duplicate-door check unavailable"*) ok "unreadable ps → stated warning + launch proceeds (fail-open, explicit)" ;;
|
|
247
|
+
*) bad "ps-failure warning wording" "err=$err" ;;
|
|
248
|
+
esac
|
|
249
|
+
else
|
|
250
|
+
bad "ps-failure fail-open" "rc=$rc launched=$([ -f "$ROOT/launch.args" ] && echo yes || echo no)"
|
|
251
|
+
fi
|
|
252
|
+
|
|
179
253
|
# --- summary -----------------------------------------------------------------
|
|
180
254
|
printf '\n%s: %d passed, %d failed\n' "$(basename "$0")" "$PASSED" "$FAILED"
|
|
181
255
|
[ "$FAILED" = 0 ]
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# Fixture-repo test suite for .claude/hooks/forge-reserve.sh (m-41, plan 81-01).
|
|
3
|
+
#
|
|
4
|
+
# Contract under test (ADR-021/023/029 + FR-236/237/238, NFR-045):
|
|
5
|
+
# phase — computed P-0NN; in-tree scan = live roadmap `- id: N` ints ∪ archived
|
|
6
|
+
# phase dirs .forge/archive/milestone-*/phases/<N>-* (burned ids never
|
|
7
|
+
# reallocated); ledger + main:reservations.yml floors as for all kinds.
|
|
8
|
+
# wave — computed W-0NN; in-tree scan = live waves-map KEYS only (never the
|
|
9
|
+
# phase ids inside the brackets).
|
|
10
|
+
# version — RECORD-ONLY (--value): clean claim dual-writes ledger +
|
|
11
|
+
# reservations.yml and echoes the value; the SAME value claimed by a
|
|
12
|
+
# DIFFERENT milestone → exit 1, stderr names the holder, nothing
|
|
13
|
+
# written; re-claim by the SAME milestone → exit 0, idempotent, no
|
|
14
|
+
# duplicate line; missing --value → usage exit 2, nothing written.
|
|
15
|
+
# regression (NFR-045) — the pre-existing kinds' ids and file shapes are
|
|
16
|
+
# unchanged; the lock releases so back-to-back calls never time out.
|
|
17
|
+
#
|
|
18
|
+
# Every case builds its own mktemp fixture repo (real `git init`) — the suite
|
|
19
|
+
# never touches the enclosing dev repo's state. Output: one line per case,
|
|
20
|
+
# `case N <tag> PASS` or `case N <tag> FAIL: <why>`; non-zero exit if any FAIL.
|
|
21
|
+
#
|
|
22
|
+
# Usage: sh .forge/checks/tests/forge-reserve.test.sh
|
|
23
|
+
set -u
|
|
24
|
+
|
|
25
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
26
|
+
RESERVE="$SCRIPT_DIR/../../../.claude/hooks/forge-reserve.sh"
|
|
27
|
+
[ -f "$RESERVE" ] || { printf 'FATAL: %s missing\n' "$RESERVE" >&2; exit 1; }
|
|
28
|
+
|
|
29
|
+
GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null
|
|
30
|
+
export GIT_CONFIG_GLOBAL GIT_CONFIG_SYSTEM
|
|
31
|
+
|
|
32
|
+
ROOT="$(mktemp -d)"
|
|
33
|
+
trap 'rm -rf "$ROOT"' EXIT INT TERM
|
|
34
|
+
|
|
35
|
+
fails=0
|
|
36
|
+
n=0
|
|
37
|
+
|
|
38
|
+
# new_fixture <name> → prints the fixture repo path
|
|
39
|
+
new_fixture() {
|
|
40
|
+
fx="$ROOT/$1"
|
|
41
|
+
mkdir -p "$fx"
|
|
42
|
+
git -C "$fx" init -q
|
|
43
|
+
printf '%s' "$fx"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
report() { # report <tag> <pass|fail> [why]
|
|
47
|
+
n=$((n + 1))
|
|
48
|
+
if [ "$2" = pass ]; then
|
|
49
|
+
printf 'case %d %s PASS\n' "$n" "$1"
|
|
50
|
+
else
|
|
51
|
+
printf 'case %d %s FAIL: %s\n' "$n" "$1" "${3:-unspecified}"
|
|
52
|
+
fails=$((fails + 1))
|
|
53
|
+
fi
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# ── FR-236: phase kind ────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
# case 1 phase-archive-aware — roadmap max 12, archived phase dir 14 → P-015
|
|
59
|
+
fx=$(new_fixture c1)
|
|
60
|
+
mkdir -p "$fx/.forge/archive/milestone-9/phases/14-old"
|
|
61
|
+
printf 'roadmap:\n phases:\n - id: 11\n - id: 12\n' > "$fx/.forge/roadmap.yml"
|
|
62
|
+
out=$(cd "$fx" && "$RESERVE" phase --milestone m-t 2>/dev/null)
|
|
63
|
+
if [ "$out" = "P-015" ]; then report phase-archive-aware pass
|
|
64
|
+
else report phase-archive-aware fail "expected P-015 (archived 14 must stay burned), got: $out"; fi
|
|
65
|
+
|
|
66
|
+
# case 2 phase-dual-write — case 1's claim landed in BOTH registries
|
|
67
|
+
if grep -q "phase P-015 m-t" "$fx/.git/forge/id-reservations" 2>/dev/null \
|
|
68
|
+
&& grep -q "id: P-015" "$fx/.forge/reservations.yml" 2>/dev/null; then
|
|
69
|
+
report phase-dual-write pass
|
|
70
|
+
else report phase-dual-write fail "P-015 missing from ledger or reservations.yml"; fi
|
|
71
|
+
|
|
72
|
+
# case 3 phase-sibling-ledger — uncommitted sibling ledger claim is visible
|
|
73
|
+
fx=$(new_fixture c3)
|
|
74
|
+
mkdir -p "$fx/.forge" "$fx/.git/forge"
|
|
75
|
+
printf 'roadmap:\n phases:\n - id: 12\n' > "$fx/.forge/roadmap.yml"
|
|
76
|
+
printf 'phase\tP-015\tm-sib\t2026-07-23T00:00:00Z\n' >> "$fx/.git/forge/id-reservations"
|
|
77
|
+
out=$(cd "$fx" && "$RESERVE" phase --milestone m-t 2>/dev/null)
|
|
78
|
+
if [ "$out" = "P-016" ]; then report phase-sibling-ledger pass
|
|
79
|
+
else report phase-sibling-ledger fail "sibling claim P-015 ignored — expected P-016, got: $out"; fi
|
|
80
|
+
|
|
81
|
+
# case 4 phase-fresh — no roadmap, no archive → P-001
|
|
82
|
+
fx=$(new_fixture c4)
|
|
83
|
+
out=$(cd "$fx" && "$RESERVE" phase --milestone m-t 2>/dev/null)
|
|
84
|
+
if [ "$out" = "P-001" ]; then report phase-fresh pass
|
|
85
|
+
else report phase-fresh fail "fresh repo expected P-001, got: $out"; fi
|
|
86
|
+
|
|
87
|
+
# case 5 phase-wave-keys-not-counted — waves KEYS and bracket ids must not leak
|
|
88
|
+
# into the phase max: roadmap phase max 3, waves key 9 pointing at [3] → P-004.
|
|
89
|
+
fx=$(new_fixture c5)
|
|
90
|
+
mkdir -p "$fx/.forge"
|
|
91
|
+
printf 'roadmap:\n phases:\n - id: 3\n waves:\n 9: [3]\n' > "$fx/.forge/roadmap.yml"
|
|
92
|
+
out=$(cd "$fx" && "$RESERVE" phase --milestone m-t 2>/dev/null)
|
|
93
|
+
if [ "$out" = "P-004" ]; then report phase-wave-keys-not-counted pass
|
|
94
|
+
else report phase-wave-keys-not-counted fail "expected P-004 (wave key 9 is not a phase id), got: $out"; fi
|
|
95
|
+
|
|
96
|
+
# ── FR-237: wave kind ────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
# case 6 wave-max-key — waves keys 1,7 (bracket ids larger) → W-008
|
|
99
|
+
fx=$(new_fixture c6)
|
|
100
|
+
mkdir -p "$fx/.forge"
|
|
101
|
+
printf 'roadmap:\n phases:\n - id: 1\n waves:\n 1: [40]\n 7: [41]\n' > "$fx/.forge/roadmap.yml"
|
|
102
|
+
out=$(cd "$fx" && "$RESERVE" wave --milestone m-t 2>/dev/null)
|
|
103
|
+
if [ "$out" = "W-008" ]; then report wave-max-key pass
|
|
104
|
+
else report wave-max-key fail "expected W-008 (max KEY 7, ids in brackets ignored), got: $out"; fi
|
|
105
|
+
|
|
106
|
+
# case 7 wave-fresh — no waves block anywhere → W-001
|
|
107
|
+
fx=$(new_fixture c7)
|
|
108
|
+
out=$(cd "$fx" && "$RESERVE" wave --milestone m-t 2>/dev/null)
|
|
109
|
+
if [ "$out" = "W-001" ]; then report wave-fresh pass
|
|
110
|
+
else report wave-fresh fail "expected W-001, got: $out"; fi
|
|
111
|
+
|
|
112
|
+
# ── FR-238: version kind (record-only) ───────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
# case 8 version-clean-claim — records in BOTH registries, echoes the value
|
|
115
|
+
fx=$(new_fixture c8)
|
|
116
|
+
out=$(cd "$fx" && "$RESERVE" version --value 0.9.0 --milestone m-a 2>/dev/null)
|
|
117
|
+
if [ "$out" = "0.9.0" ] \
|
|
118
|
+
&& grep -q "version 0.9.0 m-a" "$fx/.git/forge/id-reservations" 2>/dev/null \
|
|
119
|
+
&& grep -q 'id: 0.9.0' "$fx/.forge/reservations.yml" 2>/dev/null; then
|
|
120
|
+
report version-clean-claim pass
|
|
121
|
+
else report version-clean-claim fail "claim not recorded in both registries (stdout: $out)"; fi
|
|
122
|
+
|
|
123
|
+
# case 9 version-conflict-refused — different milestone, same value → exit 1,
|
|
124
|
+
# stderr names the holder, NOTHING written
|
|
125
|
+
err=$(cd "$fx" && "$RESERVE" version --value 0.9.0 --milestone m-b 2>&1 >/dev/null)
|
|
126
|
+
rc=$?
|
|
127
|
+
lines=$(grep -c '^version' "$fx/.git/forge/id-reservations" 2>/dev/null)
|
|
128
|
+
case "$err" in
|
|
129
|
+
*m-a*) named=yes ;;
|
|
130
|
+
*) named=no ;;
|
|
131
|
+
esac
|
|
132
|
+
if [ "$rc" -ne 0 ] && [ "$named" = yes ] && [ "$lines" = "1" ]; then
|
|
133
|
+
report version-conflict-refused pass
|
|
134
|
+
else report version-conflict-refused fail "rc=$rc named-holder=$named ledger-version-lines=$lines (want rc!=0, holder m-a named, 1 line)"; fi
|
|
135
|
+
|
|
136
|
+
# case 10 version-idempotent-reclaim — same milestone re-claims → exit 0, no dup
|
|
137
|
+
out=$(cd "$fx" && "$RESERVE" version --value 0.9.0 --milestone m-a 2>/dev/null)
|
|
138
|
+
rc=$?
|
|
139
|
+
lines=$(grep -c '^version' "$fx/.git/forge/id-reservations" 2>/dev/null)
|
|
140
|
+
if [ "$rc" -eq 0 ] && [ "$out" = "0.9.0" ] && [ "$lines" = "1" ]; then
|
|
141
|
+
report version-idempotent-reclaim pass
|
|
142
|
+
else report version-idempotent-reclaim fail "rc=$rc stdout=$out ledger-version-lines=$lines (want 0 / 0.9.0 / 1)"; fi
|
|
143
|
+
|
|
144
|
+
# case 11 version-missing-value — usage exit 2, nothing written
|
|
145
|
+
fx=$(new_fixture c11)
|
|
146
|
+
err=$(cd "$fx" && "$RESERVE" version --milestone m-a 2>&1 >/dev/null)
|
|
147
|
+
rc=$?
|
|
148
|
+
if [ "$rc" -eq 2 ] && [ ! -f "$fx/.git/forge/id-reservations" ] && [ ! -f "$fx/.forge/reservations.yml" ]; then
|
|
149
|
+
report version-missing-value pass
|
|
150
|
+
else report version-missing-value fail "rc=$rc (want 2) or files written on usage error"; fi
|
|
151
|
+
|
|
152
|
+
# ── NFR-045: existing kinds regression + lock release ────────────────────────
|
|
153
|
+
|
|
154
|
+
# case 12 regression-existing-kinds — fr x2 / milestone / refactor sequence and
|
|
155
|
+
# reservations.yml shape identical to pre-m-41 behavior
|
|
156
|
+
fx=$(new_fixture c12)
|
|
157
|
+
a=$(cd "$fx" && "$RESERVE" fr --milestone m-t 2>/dev/null)
|
|
158
|
+
b=$(cd "$fx" && "$RESERVE" fr --milestone m-t 2>/dev/null)
|
|
159
|
+
m=$(cd "$fx" && "$RESERVE" milestone --milestone m-new 2>/dev/null)
|
|
160
|
+
r=$(cd "$fx" && "$RESERVE" refactor --milestone m-t 2>/dev/null)
|
|
161
|
+
if [ "$a $b $m $r" = "FR-001 FR-002 M-1 R001" ] \
|
|
162
|
+
&& grep -q '^reservations:' "$fx/.forge/reservations.yml" 2>/dev/null \
|
|
163
|
+
&& grep -q 'kind: refactor' "$fx/.forge/reservations.yml" 2>/dev/null; then
|
|
164
|
+
report regression-existing-kinds pass
|
|
165
|
+
else report regression-existing-kinds fail "expected FR-001 FR-002 M-1 R001 + seeded header, got: $a $b $m $r"; fi
|
|
166
|
+
|
|
167
|
+
# case 13 regression-lock-release — back-to-back calls in one fixture must not
|
|
168
|
+
# time out (a leaked lock would make the second call exit 3)
|
|
169
|
+
fx=$(new_fixture c13)
|
|
170
|
+
(cd "$fx" && "$RESERVE" adr --milestone m-t >/dev/null 2>&1)
|
|
171
|
+
out=$(cd "$fx" && FORGE_RESERVE_WAIT=2 "$RESERVE" adr --milestone m-t 2>/dev/null)
|
|
172
|
+
rc=$?
|
|
173
|
+
if [ "$rc" -eq 0 ] && [ "$out" = "ADR-002" ]; then
|
|
174
|
+
report regression-lock-release pass
|
|
175
|
+
else report regression-lock-release fail "second call rc=$rc out=$out (leaked lock?)"; fi
|
|
176
|
+
|
|
177
|
+
# ── input hardening (review, m-41): validation before the lock, nothing written ─
|
|
178
|
+
|
|
179
|
+
# case 14 version-value-rejects-non-semver — --value must be X.Y.Z; a non-semver
|
|
180
|
+
# (here a YAML-injection payload) → exit 2, NOTHING written
|
|
181
|
+
fx=$(new_fixture c14)
|
|
182
|
+
err=$(cd "$fx" && "$RESERVE" version --value '9.9.9
|
|
183
|
+
injected: pwned' --milestone m-a 2>&1 >/dev/null)
|
|
184
|
+
rc=$?
|
|
185
|
+
if [ "$rc" -eq 2 ] && [ ! -f "$fx/.git/forge/id-reservations" ] && [ ! -f "$fx/.forge/reservations.yml" ]; then
|
|
186
|
+
report version-value-rejects-non-semver pass
|
|
187
|
+
else report version-value-rejects-non-semver fail "rc=$rc (want 2) or files written on bad --value"; fi
|
|
188
|
+
|
|
189
|
+
# case 15 version-value-accepts-prerelease — X.Y.Z-pre is valid semver → recorded
|
|
190
|
+
fx=$(new_fixture c15)
|
|
191
|
+
out=$(cd "$fx" && "$RESERVE" version --value 1.2.0-rc.1 --milestone m-a 2>/dev/null)
|
|
192
|
+
if [ "$out" = "1.2.0-rc.1" ]; then report version-value-accepts-prerelease pass
|
|
193
|
+
else report version-value-accepts-prerelease fail "expected 1.2.0-rc.1 recorded, got: $out"; fi
|
|
194
|
+
|
|
195
|
+
# case 16 milestone-rejects-control-char — a newline in --milestone would split
|
|
196
|
+
# the ledger row / inject a sibling YAML key → exit 2, NOTHING written
|
|
197
|
+
fx=$(new_fixture c16)
|
|
198
|
+
err=$(cd "$fx" && "$RESERVE" fr --milestone 'm-1
|
|
199
|
+
injected: pwned' 2>&1 >/dev/null)
|
|
200
|
+
rc=$?
|
|
201
|
+
if [ "$rc" -eq 2 ] && [ ! -f "$fx/.git/forge/id-reservations" ] && [ ! -f "$fx/.forge/reservations.yml" ]; then
|
|
202
|
+
report milestone-rejects-control-char pass
|
|
203
|
+
else report milestone-rejects-control-char fail "rc=$rc (want 2) or files written on control-char milestone"; fi
|
|
204
|
+
|
|
205
|
+
# case 17 symlink-target-refused — reservations.yml is a symlink → refuse to
|
|
206
|
+
# write through it, exit 2, and the symlink target stays untouched
|
|
207
|
+
fx=$(new_fixture c17)
|
|
208
|
+
mkdir -p "$fx/.forge"
|
|
209
|
+
: > "$ROOT/c17-victim.txt"
|
|
210
|
+
ln -s "$ROOT/c17-victim.txt" "$fx/.forge/reservations.yml"
|
|
211
|
+
err=$(cd "$fx" && "$RESERVE" fr --milestone m-t 2>&1 >/dev/null)
|
|
212
|
+
rc=$?
|
|
213
|
+
if [ "$rc" -eq 2 ] && [ ! -s "$ROOT/c17-victim.txt" ]; then
|
|
214
|
+
report symlink-target-refused pass
|
|
215
|
+
else report symlink-target-refused fail "rc=$rc (want 2) or victim written through symlink"; fi
|
|
216
|
+
|
|
217
|
+
# case 18 main-floor — cross-machine/cold-start floor: main:reservations.yml
|
|
218
|
+
# carries a merged P-020, empty ledger + roadmap → next phase is P-021
|
|
219
|
+
fx=$(new_fixture c18)
|
|
220
|
+
mkdir -p "$fx/.forge"
|
|
221
|
+
printf 'reservations:\n - kind: phase\n id: P-020\n milestone: m-old\n' > "$fx/.forge/reservations.yml"
|
|
222
|
+
git -C "$fx" add .forge/reservations.yml >/dev/null 2>&1
|
|
223
|
+
git -C "$fx" -c user.email=t@t -c user.name=t commit -qm seed >/dev/null 2>&1
|
|
224
|
+
git -C "$fx" branch -M main >/dev/null 2>&1
|
|
225
|
+
out=$(cd "$fx" && "$RESERVE" phase --milestone m-t 2>/dev/null)
|
|
226
|
+
if [ "$out" = "P-021" ]; then report main-floor pass
|
|
227
|
+
else report main-floor fail "main floor P-020 ignored — expected P-021, got: $out"; fi
|
|
228
|
+
|
|
229
|
+
# ── summary ──────────────────────────────────────────────────────────────────
|
|
230
|
+
if [ "$fails" -eq 0 ]; then
|
|
231
|
+
printf 'forge-reserve.test.sh: all %d cases PASS\n' "$n"
|
|
232
|
+
exit 0
|
|
233
|
+
fi
|
|
234
|
+
printf 'forge-reserve.test.sh: %d of %d cases FAILED\n' "$fails" "$n" >&2
|
|
235
|
+
exit 1
|