forge-orkes 0.71.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/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 +63 -7
- 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 +64 -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
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
# lint=fail + finding=<rule>:<file>[:<line>] lines
|
|
8
8
|
# lint=justified justify="<trailer line>"
|
|
9
9
|
# + "finding=<rule>:<file>[:<line>] justified" lines
|
|
10
|
-
# rule ∈ cannot-fail|silent-fail|existence-only|deleted-assertion|feature-removal|skip-marker
|
|
10
|
+
# rule ∈ cannot-fail|silent-fail|existence-only|deleted-assertion|feature-removal|skip-marker|driven-by-missing
|
|
11
11
|
# exit: 0 = clean or justified; 1 = fail; 2 = bad invocation.
|
|
12
12
|
# Range MUST be three-dot BASE...HEAD; scanned against merge-base(BASE, HEAD).
|
|
13
13
|
#
|
|
@@ -28,17 +28,24 @@
|
|
|
28
28
|
# allowlist: still fires, still needs the operator act, only the label
|
|
29
29
|
# changes (partial source shrink keeps the stricter deleted-assertion);
|
|
30
30
|
# skip-marker — added .only(/.skip(/it.skip/describe.only/xit(/xdescribe(/
|
|
31
|
-
# @pytest.mark.skip/t.Skip() line
|
|
31
|
+
# @pytest.mark.skip/t.Skip() line;
|
|
32
|
+
# driven-by-missing — CONFIG-GATED, default OFF: when the BASE-side
|
|
33
|
+
# .forge/project.yml carries attribution.driven_by: true, every
|
|
34
|
+
# non-merge commit of merge-base..head must carry a
|
|
35
|
+
# Driven-by: <email> trailer (optional trailing session ref);
|
|
36
|
+
# finding's file slot = the offending commit's short SHA. Absent/
|
|
37
|
+
# false config = rule fully off. Base-side read: a PR cannot
|
|
38
|
+
# disable it for itself.
|
|
32
39
|
# Justification: a "Lint-Justify: <one line>" trailer in ANY commit message of
|
|
33
40
|
# merge-base..head (pure git). Lifts b/c/deleted-assertion/feature-removal/
|
|
34
|
-
# skip-marker → exit 0 lint=justified. NEVER lifts (a).
|
|
41
|
+
# skip-marker/driven-by-missing → exit 0 lint=justified. NEVER lifts (a).
|
|
35
42
|
#
|
|
36
43
|
# Offline by construction: remote-less mktemp fixture repos (NFR-032).
|
|
37
44
|
#
|
|
38
45
|
# Usage:
|
|
39
46
|
# ./.forge/checks/tests/forge-check-lint.test.sh # all cases
|
|
40
47
|
# ./.forge/checks/tests/forge-check-lint.test.sh cannot-fail # one tag
|
|
41
|
-
# Filter tags: cannot-fail silent-fail existence-only deleted-assertion feature-removal skip-marker justify docs-only invocation
|
|
48
|
+
# Filter tags: cannot-fail silent-fail existence-only deleted-assertion feature-removal skip-marker driven-by waiver-boundary justify docs-only invocation
|
|
42
49
|
#
|
|
43
50
|
# RED-phase friendly: missing helper → every selected case FAILS, suite exits
|
|
44
51
|
# non-zero, no abort. Exits 0 iff every selected case passes. Self-cleans.
|
|
@@ -566,6 +573,121 @@ if running justify; then
|
|
|
566
573
|
check "justify: trailer never saves a PR carrying rule a (exit 1)" "$RC" "1"
|
|
567
574
|
fi
|
|
568
575
|
|
|
576
|
+
# ---------------------------------------------------------------------------
|
|
577
|
+
# driven-by — the config-gated Driven-by trailer rule (hold-waiver patch W2).
|
|
578
|
+
# Default OFF; base-side project.yml attribution.driven_by: true turns it on.
|
|
579
|
+
# ---------------------------------------------------------------------------
|
|
580
|
+
if running driven-by; then
|
|
581
|
+
PROJ_ON='attribution:
|
|
582
|
+
driven_by: true'
|
|
583
|
+
|
|
584
|
+
# OFF (default): no config → a trailer-less commit is clean (zero behavior change)
|
|
585
|
+
R=$(new_repo db-off)
|
|
586
|
+
put "$R" src/app.js 'x'
|
|
587
|
+
pr "$R"
|
|
588
|
+
put "$R" src/app.js 'y'
|
|
589
|
+
run_lint "$R" 'main...pr'
|
|
590
|
+
check "driven-by OFF: no config, trailer-less commit is clean (exit 0)" "$RC" "0"
|
|
591
|
+
check_absent "driven-by OFF: no driven-by finding" "$OUT" "driven-by-missing"
|
|
592
|
+
|
|
593
|
+
# OFF (explicit false): rule stays off
|
|
594
|
+
R=$(new_repo db-false)
|
|
595
|
+
put "$R" .forge/project.yml 'attribution:
|
|
596
|
+
driven_by: false'
|
|
597
|
+
pr "$R"
|
|
598
|
+
put "$R" src/app.js 'y'
|
|
599
|
+
run_lint "$R" 'main...pr'
|
|
600
|
+
check "driven-by OFF: driven_by false stays off (exit 0)" "$RC" "0"
|
|
601
|
+
|
|
602
|
+
# ON: a commit missing the trailer fails, finding names its short SHA
|
|
603
|
+
R=$(new_repo db-on)
|
|
604
|
+
put "$R" .forge/project.yml "$PROJ_ON"
|
|
605
|
+
pr "$R"
|
|
606
|
+
put "$R" src/app.js 'y'
|
|
607
|
+
DB_SHA="$(git -C "$R" rev-parse --short pr)"
|
|
608
|
+
run_lint "$R" 'main...pr'
|
|
609
|
+
check "driven-by ON: missing trailer fails (exit 1)" "$RC" "1"
|
|
610
|
+
check_contains "driven-by ON: verdict" "$OUT" "lint=fail"
|
|
611
|
+
check_contains "driven-by ON: finding names the commit" "$OUT" "finding=driven-by-missing:$DB_SHA"
|
|
612
|
+
|
|
613
|
+
# ON: every commit carrying the trailer is clean (optional session ref allowed)
|
|
614
|
+
R=$(new_repo db-ok)
|
|
615
|
+
put "$R" .forge/project.yml "$PROJ_ON"
|
|
616
|
+
pr "$R"
|
|
617
|
+
put "$R" src/app.js 'y' 'Driven-by: zayne@zayne.co.za (session cse_01X)'
|
|
618
|
+
put "$R" src/app.js 'z' 'Driven-by: zayne@zayne.co.za'
|
|
619
|
+
run_lint "$R" 'main...pr'
|
|
620
|
+
check "driven-by ON: trailered commits are clean (exit 0)" "$RC" "0"
|
|
621
|
+
check "driven-by ON: verdict clean" "$OUT" "lint=clean"
|
|
622
|
+
|
|
623
|
+
# ON: a malformed trailer (no email shape) still fails
|
|
624
|
+
R=$(new_repo db-malformed)
|
|
625
|
+
put "$R" .forge/project.yml "$PROJ_ON"
|
|
626
|
+
pr "$R"
|
|
627
|
+
put "$R" src/app.js 'y' 'Driven-by: somebody'
|
|
628
|
+
run_lint "$R" 'main...pr'
|
|
629
|
+
check "driven-by ON: trailer without an email fails (exit 1)" "$RC" "1"
|
|
630
|
+
check_contains "driven-by ON: malformed finding" "$OUT" "finding=driven-by-missing:"
|
|
631
|
+
|
|
632
|
+
# ON, mixed: one trailered + one bare commit → only the bare one is named
|
|
633
|
+
R=$(new_repo db-mixed)
|
|
634
|
+
put "$R" .forge/project.yml "$PROJ_ON"
|
|
635
|
+
pr "$R"
|
|
636
|
+
put "$R" src/app.js 'y' 'Driven-by: zayne@zayne.co.za'
|
|
637
|
+
put "$R" src/other.js 'w'
|
|
638
|
+
DB_BARE="$(git -C "$R" rev-parse --short pr)"
|
|
639
|
+
DB_GOOD="$(git -C "$R" rev-parse --short pr~1)"
|
|
640
|
+
run_lint "$R" 'main...pr'
|
|
641
|
+
check "driven-by ON mixed: bare commit fails (exit 1)" "$RC" "1"
|
|
642
|
+
check_contains "driven-by ON mixed: bare commit named" "$OUT" "finding=driven-by-missing:$DB_BARE"
|
|
643
|
+
check_absent "driven-by ON mixed: trailered commit not named" "$OUT" "finding=driven-by-missing:$DB_GOOD"
|
|
644
|
+
|
|
645
|
+
# Base-side gate: a PR that deletes the config cannot switch the rule off for itself
|
|
646
|
+
R=$(new_repo db-evade)
|
|
647
|
+
put "$R" .forge/project.yml "$PROJ_ON"
|
|
648
|
+
pr "$R"
|
|
649
|
+
git -C "$R" rm -q .forge/project.yml
|
|
650
|
+
git -C "$R" commit -qm "drop attribution config"
|
|
651
|
+
run_lint "$R" 'main...pr'
|
|
652
|
+
check "driven-by ON: head-side config removal cannot evade (exit 1)" "$RC" "1"
|
|
653
|
+
check_contains "driven-by ON: evasion finding" "$OUT" "finding=driven-by-missing:"
|
|
654
|
+
|
|
655
|
+
# Lint-Justify lifts it (same operator-act channel as the other liftable rules)
|
|
656
|
+
R=$(new_repo db-justify)
|
|
657
|
+
put "$R" .forge/project.yml "$PROJ_ON"
|
|
658
|
+
pr "$R"
|
|
659
|
+
put "$R" src/app.js 'y' 'Lint-Justify: host-authored dependabot commit, attribution n/a'
|
|
660
|
+
run_lint "$R" 'main...pr'
|
|
661
|
+
check "driven-by ON: Lint-Justify lifts (exit 0)" "$RC" "0"
|
|
662
|
+
check_contains "driven-by ON: justified verdict" "$OUT" "lint=justified"
|
|
663
|
+
check_contains "driven-by ON: finding listed as justified" "$OUT" "finding=driven-by-missing:"
|
|
664
|
+
fi
|
|
665
|
+
|
|
666
|
+
# ---------------------------------------------------------------------------
|
|
667
|
+
# waiver-boundary — the hold check's run-waiver binding must have ZERO effect
|
|
668
|
+
# here: the waiver applies to the hold check only, never the lint.
|
|
669
|
+
# ---------------------------------------------------------------------------
|
|
670
|
+
if running waiver-boundary; then
|
|
671
|
+
R=$(new_repo wb)
|
|
672
|
+
put "$R" .forge/hold-config.yml 'version: 1
|
|
673
|
+
operators: []
|
|
674
|
+
irreversible_paths: []
|
|
675
|
+
deploy_paths: []
|
|
676
|
+
gating_tests: []
|
|
677
|
+
waiver:
|
|
678
|
+
workflow: waive-hold.yml
|
|
679
|
+
ref: main'
|
|
680
|
+
put "$R" "$WF" 'jobs: {}'
|
|
681
|
+
pr "$R"
|
|
682
|
+
put "$R" "$WF" 'jobs:
|
|
683
|
+
gate:
|
|
684
|
+
steps:
|
|
685
|
+
- run: exit 0'
|
|
686
|
+
run_lint "$R" 'main...pr'
|
|
687
|
+
check "waiver-boundary: hold waiver binding never lifts a lint finding (exit 1)" "$RC" "1"
|
|
688
|
+
check_contains "waiver-boundary: finding intact" "$OUT" "finding=cannot-fail:$WF"
|
|
689
|
+
fi
|
|
690
|
+
|
|
569
691
|
# ---------------------------------------------------------------------------
|
|
570
692
|
# docs-only — acceptance 6: a status on every PR
|
|
571
693
|
# ---------------------------------------------------------------------------
|
|
@@ -25,6 +25,15 @@
|
|
|
25
25
|
# suites) (+config deploy_paths, gating_tests)
|
|
26
26
|
# Waiver: any --approvals login ∈ base-side config operators: → waived. Absent
|
|
27
27
|
# config / empty operators → NO waiver possible (raise-only safe default).
|
|
28
|
+
# Run-waiver (hold-waiver patch): OPTIONAL base-side `waiver: {workflow, ref}`
|
|
29
|
+
# binding — when a hold fired and no operator approval waives it, a host-queried
|
|
30
|
+
# run of that workflow waives iff ALL of: workflow path matches; run ref ==
|
|
31
|
+
# repo default branch (== configured ref); conclusion == success; display_title
|
|
32
|
+
# carries the PR head SHA. Output: hold=waived waiver_run=<url>. FAIL-CLOSED on
|
|
33
|
+
# every leg (unconfigured / gh missing / API unreachable / no match / wrong ref
|
|
34
|
+
# / config-ref drift / wrong path / non-success / superseded SHA → hold stands);
|
|
35
|
+
# unconfigured behavior is byte-identical to the pre-waiver check (no gh call).
|
|
36
|
+
# gh is env-overridable (FORGE_HOLD_GH) — fixtures stub it and stay offline.
|
|
28
37
|
#
|
|
29
38
|
# Builds throwaway git repos in mktemp dirs with NO remote configured anywhere —
|
|
30
39
|
# offline by construction (NFR-032); any network dependency in the script breaks here.
|
|
@@ -32,7 +41,7 @@
|
|
|
32
41
|
# Usage:
|
|
33
42
|
# ./.forge/checks/tests/forge-hold-check.test.sh # all cases
|
|
34
43
|
# ./.forge/checks/tests/forge-hold-check.test.sh pain6 # one tag
|
|
35
|
-
# Filter tags: irreversible structural pain6 waiver self-protect docs-only invocation multi
|
|
44
|
+
# Filter tags: irreversible structural pain6 waiver run-waiver self-protect docs-only invocation multi
|
|
36
45
|
#
|
|
37
46
|
# RED-phase friendly (TDD): a missing/non-executable helper does NOT abort the
|
|
38
47
|
# suite — every selected case FAILS with "(helper missing ...)" as the got-value
|
|
@@ -382,6 +391,162 @@ if running multi; then
|
|
|
382
391
|
check_contains "multi: protected named" "$OUT" "fact=protected:.github/workflows/deploy.yml"
|
|
383
392
|
fi
|
|
384
393
|
|
|
394
|
+
# ---------------------------------------------------------------------------
|
|
395
|
+
# run-waiver — the Environment-gated waiver run channel: pass-on-match plus a
|
|
396
|
+
# neg-control per fail-closed leg. gh is stubbed offline via FORGE_HOLD_GH.
|
|
397
|
+
# ---------------------------------------------------------------------------
|
|
398
|
+
if running run-waiver; then
|
|
399
|
+
GH_STUB="$ROOT/gh-stub"
|
|
400
|
+
cat > "$GH_STUB" <<'STUB'
|
|
401
|
+
#!/bin/sh
|
|
402
|
+
# Offline gh stand-in for the run-waiver channel. Argv shapes the check uses:
|
|
403
|
+
# gh api repos/{owner}/{repo} --jq .default_branch
|
|
404
|
+
# gh api "repos/{owner}/{repo}/actions/workflows/<wf>/runs?..." --jq <projection>
|
|
405
|
+
# Driven by env: STUB_DEFAULT_BRANCH (empty/unset => API failure, exit 1),
|
|
406
|
+
# STUB_RUNS_FILE (TSV: conclusion, head_branch, path, html_url, display_title;
|
|
407
|
+
# unset/missing => API failure), STUB_LOG (append argv per call when set).
|
|
408
|
+
[ -n "${STUB_LOG:-}" ] && printf '%s\n' "$*" >> "$STUB_LOG"
|
|
409
|
+
case "${2:-}" in
|
|
410
|
+
'repos/{owner}/{repo}')
|
|
411
|
+
[ -n "${STUB_DEFAULT_BRANCH:-}" ] || exit 1
|
|
412
|
+
printf '%s\n' "$STUB_DEFAULT_BRANCH" ;;
|
|
413
|
+
'repos/{owner}/{repo}/actions/workflows/'*)
|
|
414
|
+
[ -n "${STUB_RUNS_FILE:-}" ] && [ -f "$STUB_RUNS_FILE" ] || exit 1
|
|
415
|
+
cat "$STUB_RUNS_FILE" ;;
|
|
416
|
+
*) exit 1 ;;
|
|
417
|
+
esac
|
|
418
|
+
STUB
|
|
419
|
+
chmod +x "$GH_STUB"
|
|
420
|
+
|
|
421
|
+
CONFIG_WAIVER='version: 1
|
|
422
|
+
operators: []
|
|
423
|
+
irreversible_paths: []
|
|
424
|
+
deploy_paths: []
|
|
425
|
+
gating_tests: []
|
|
426
|
+
waiver:
|
|
427
|
+
workflow: waive-hold.yml
|
|
428
|
+
ref: main'
|
|
429
|
+
|
|
430
|
+
WURL='https://host.example/actions/runs/101'
|
|
431
|
+
WPATH='.github/workflows/waive-hold.yml'
|
|
432
|
+
|
|
433
|
+
new_waiver_repo() { # name, config → repo whose pr branch carries a migration (hold fires)
|
|
434
|
+
nw_d=$(new_repo "$1")
|
|
435
|
+
put "$nw_d" .forge/hold-config.yml "$2"
|
|
436
|
+
pr "$nw_d"
|
|
437
|
+
put "$nw_d" app/migrations/010_waiver.py 'alter table'
|
|
438
|
+
printf '%s\n' "$nw_d"
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
# pass-on-match: approved success run on the default branch, title carries head SHA
|
|
442
|
+
R=$(new_waiver_repo rw-pass "$CONFIG_WAIVER")
|
|
443
|
+
SHA=$(git -C "$R" rev-parse pr)
|
|
444
|
+
printf 'success\tmain\t%s\t%s\twaive-hold sha=%s\n' "$WPATH" "$WURL" "$SHA" > "$ROOT/runs.pass"
|
|
445
|
+
export FORGE_HOLD_GH="$GH_STUB" STUB_DEFAULT_BRANCH=main STUB_RUNS_FILE="$ROOT/runs.pass"
|
|
446
|
+
run_hold "$R" 'main...pr'
|
|
447
|
+
check "run-waiver: approved matching run waives (exit 0)" "$RC" "0"
|
|
448
|
+
check_contains "run-waiver: verdict annotates the run URL" "$OUT" "hold=waived waiver_run=$WURL"
|
|
449
|
+
check_contains "run-waiver: lists what it waived" "$OUT" "fact=irreversible:app/migrations/010_waiver.py waived"
|
|
450
|
+
|
|
451
|
+
# neg fail-closed leg: UNCONFIGURED — no waiver block: no gh call, and the
|
|
452
|
+
# blocked output is byte-identical to a run with no stub env at all.
|
|
453
|
+
R=$(new_waiver_repo rw-unconf "$CONFIG_OPS")
|
|
454
|
+
export STUB_LOG="$ROOT/gh.unconf.log"
|
|
455
|
+
: > "$STUB_LOG"
|
|
456
|
+
run_hold "$R" 'main...pr'
|
|
457
|
+
UNCONF_OUT="$OUT"
|
|
458
|
+
check "run-waiver neg (unconfigured): hold stands (exit 1)" "$RC" "1"
|
|
459
|
+
check "run-waiver neg (unconfigured): gh never invoked" "$(cat "$STUB_LOG")" ""
|
|
460
|
+
unset FORGE_HOLD_GH STUB_DEFAULT_BRANCH STUB_RUNS_FILE STUB_LOG
|
|
461
|
+
run_hold "$R" 'main...pr'
|
|
462
|
+
check "run-waiver neg (unconfigured): output byte-identical to pre-waiver check" "$OUT" "$UNCONF_OUT"
|
|
463
|
+
|
|
464
|
+
# neg fail-closed leg: gh MISSING (no host CLI at all)
|
|
465
|
+
R=$(new_waiver_repo rw-nogh "$CONFIG_WAIVER")
|
|
466
|
+
export FORGE_HOLD_GH="$ROOT/definitely-no-such-gh"
|
|
467
|
+
run_hold "$R" 'main...pr'
|
|
468
|
+
check "run-waiver neg (gh missing): hold stands (exit 1)" "$RC" "1"
|
|
469
|
+
check_contains "run-waiver neg (gh missing): blocked verdict" "$OUT" "hold=blocked"
|
|
470
|
+
|
|
471
|
+
# neg fail-closed leg: API UNREACHABLE (default-branch query fails)
|
|
472
|
+
export FORGE_HOLD_GH="$GH_STUB" STUB_DEFAULT_BRANCH="" STUB_RUNS_FILE="$ROOT/runs.pass"
|
|
473
|
+
run_hold "$R" 'main...pr'
|
|
474
|
+
check "run-waiver neg (API unreachable): hold stands (exit 1)" "$RC" "1"
|
|
475
|
+
|
|
476
|
+
# neg fail-closed leg: NO MATCHING RUN (API reachable, zero runs)
|
|
477
|
+
: > "$ROOT/runs.empty"
|
|
478
|
+
export STUB_DEFAULT_BRANCH=main STUB_RUNS_FILE="$ROOT/runs.empty"
|
|
479
|
+
run_hold "$R" 'main...pr'
|
|
480
|
+
check "run-waiver neg (no matching run): hold stands (exit 1)" "$RC" "1"
|
|
481
|
+
|
|
482
|
+
# neg fail-closed leg: WRONG REF — the only success run rode a PR-branch copy
|
|
483
|
+
# of the workflow (gate strippable), not the default branch.
|
|
484
|
+
SHA=$(git -C "$R" rev-parse pr)
|
|
485
|
+
printf 'success\tpr\t%s\t%s\twaive-hold sha=%s\n' "$WPATH" "$WURL" "$SHA" > "$ROOT/runs.wrongref"
|
|
486
|
+
export STUB_RUNS_FILE="$ROOT/runs.wrongref"
|
|
487
|
+
run_hold "$R" 'main...pr'
|
|
488
|
+
check "run-waiver neg (wrong ref): PR-branch run cannot waive (exit 1)" "$RC" "1"
|
|
489
|
+
|
|
490
|
+
# neg fail-closed leg: CONFIG-REF DRIFT — configured ref != repo default branch
|
|
491
|
+
R=$(new_waiver_repo rw-drift 'version: 1
|
|
492
|
+
operators: []
|
|
493
|
+
irreversible_paths: []
|
|
494
|
+
deploy_paths: []
|
|
495
|
+
gating_tests: []
|
|
496
|
+
waiver:
|
|
497
|
+
workflow: waive-hold.yml
|
|
498
|
+
ref: develop')
|
|
499
|
+
SHA=$(git -C "$R" rev-parse pr)
|
|
500
|
+
printf 'success\tmain\t%s\t%s\twaive-hold sha=%s\n' "$WPATH" "$WURL" "$SHA" > "$ROOT/runs.drift"
|
|
501
|
+
export STUB_RUNS_FILE="$ROOT/runs.drift"
|
|
502
|
+
run_hold "$R" 'main...pr'
|
|
503
|
+
check "run-waiver neg (config-ref drift): waiver.ref != default branch (exit 1)" "$RC" "1"
|
|
504
|
+
|
|
505
|
+
# neg fail-closed leg: SUPERSEDED SHA — a new push after the approved run;
|
|
506
|
+
# the run's title carries the OLD head SHA, so it no longer matches.
|
|
507
|
+
R=$(new_waiver_repo rw-stale "$CONFIG_WAIVER")
|
|
508
|
+
OLD_SHA=$(git -C "$R" rev-parse pr)
|
|
509
|
+
printf 'success\tmain\t%s\t%s\twaive-hold sha=%s\n' "$WPATH" "$WURL" "$OLD_SHA" > "$ROOT/runs.stale"
|
|
510
|
+
put "$R" src/followup.js 'pushed after the waiver run'
|
|
511
|
+
export STUB_RUNS_FILE="$ROOT/runs.stale"
|
|
512
|
+
run_hold "$R" 'main...pr'
|
|
513
|
+
check "run-waiver neg (superseded SHA): new push invalidates the waiver (exit 1)" "$RC" "1"
|
|
514
|
+
check_contains "run-waiver neg (superseded SHA): blocked verdict" "$OUT" "hold=blocked"
|
|
515
|
+
|
|
516
|
+
# neg fail-closed leg: WRONG WORKFLOW PATH — a same-shaped run of another workflow
|
|
517
|
+
R=$(new_waiver_repo rw-path "$CONFIG_WAIVER")
|
|
518
|
+
SHA=$(git -C "$R" rev-parse pr)
|
|
519
|
+
printf 'success\tmain\t.github/workflows/other.yml\t%s\twaive-hold sha=%s\n' "$WURL" "$SHA" > "$ROOT/runs.path"
|
|
520
|
+
export STUB_RUNS_FILE="$ROOT/runs.path"
|
|
521
|
+
run_hold "$R" 'main...pr'
|
|
522
|
+
check "run-waiver neg (wrong workflow path): hold stands (exit 1)" "$RC" "1"
|
|
523
|
+
|
|
524
|
+
# neg fail-closed leg: NON-SUCCESS CONCLUSION — pending/rejected run never waives
|
|
525
|
+
printf 'failure\tmain\t%s\t%s\twaive-hold sha=%s\n' "$WPATH" "$WURL" "$SHA" > "$ROOT/runs.fail"
|
|
526
|
+
export STUB_RUNS_FILE="$ROOT/runs.fail"
|
|
527
|
+
run_hold "$R" 'main...pr'
|
|
528
|
+
check "run-waiver neg (non-success run): hold stands (exit 1)" "$RC" "1"
|
|
529
|
+
|
|
530
|
+
# channel precedence: an operator PR approval still waives on its own — the
|
|
531
|
+
# run-waiver channel is additive, not a replacement.
|
|
532
|
+
R=$(new_waiver_repo rw-op 'version: 1
|
|
533
|
+
operators:
|
|
534
|
+
- zayne
|
|
535
|
+
irreversible_paths: []
|
|
536
|
+
deploy_paths: []
|
|
537
|
+
gating_tests: []
|
|
538
|
+
waiver:
|
|
539
|
+
workflow: waive-hold.yml
|
|
540
|
+
ref: main')
|
|
541
|
+
AF=$(approvals rwop zayne)
|
|
542
|
+
export STUB_DEFAULT_BRANCH="" STUB_RUNS_FILE=""
|
|
543
|
+
run_hold "$R" 'main...pr' --approvals "$AF"
|
|
544
|
+
check "run-waiver: operator approval still waives independently (exit 0)" "$RC" "0"
|
|
545
|
+
check_contains "run-waiver: operator verdict unchanged" "$OUT" "hold=waived waived_by=zayne"
|
|
546
|
+
|
|
547
|
+
unset FORGE_HOLD_GH STUB_DEFAULT_BRANCH STUB_RUNS_FILE STUB_LOG
|
|
548
|
+
fi
|
|
549
|
+
|
|
385
550
|
# ---------------------------------------------------------------------------
|
|
386
551
|
# invocation — exit 2 on bad usage, never a false verdict
|
|
387
552
|
# ---------------------------------------------------------------------------
|
|
@@ -339,6 +339,70 @@ else
|
|
|
339
339
|
bad "dry-run rotation" "dry-run had side effects"
|
|
340
340
|
fi
|
|
341
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
|
|
342
406
|
# --- summary -----------------------------------------------------------------
|
|
343
407
|
printf '\n%s: %d passed, %d failed\n' "$(basename "$0")" "$PASSED" "$FAILED"
|
|
344
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 ]
|