forge-orkes 0.55.0 → 0.58.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.
@@ -0,0 +1,773 @@
1
+ #!/usr/bin/env sh
2
+ # Fixture-repo test suite for .claude/hooks/forge-release-fold.sh (m-30, plan 50-01).
3
+ #
4
+ # Contract under test (plan-01 "Locked interface" — do not re-derive):
5
+ # forge-release-fold.sh <unit-id> [--sha <sha>] [--main <ref>] [--project <path>]
6
+ # stdout: release_state=unmerged|merged|validated [reason=<code>]
7
+ # reason codes: not-on-main | no-approval | no-signoff | bad-signature |
8
+ # not-contained | legacy-complete | signed-off
9
+ # exit: 0 = computed (ANY of the three states — unmerged is an answer, not an
10
+ # error); 2 = bad invocation / unknown unit (never a false verdict).
11
+ # Landed-sha resolution when --sha absent: (1) live branch recorded for the unit
12
+ # (main-tree state/milestone-{id}.yml lifecycle.worktree_branch, else main-tree
13
+ # streams/m-{id}.yml runtime.branch) → branch tip; (2) tracked-only → the LAST
14
+ # COMMIT TOUCHING the state file on main, NEVER main's tip; (3) unknown → exit 2.
15
+ # All YAML field reads go through `git show main:...` — never the working tree.
16
+ #
17
+ # Builds throwaway git repos in mktemp dirs with NO remote configured anywhere —
18
+ # the whole suite is offline by construction (NFR-030). SSH-signed signoff tags
19
+ # reuse the key-gen pattern from verify-signoff.test.sh. Needs ssh-keygen + git
20
+ # with SSH signing support (git >= 2.34).
21
+ #
22
+ # Usage:
23
+ # ./.claude/hooks/tests/forge-release-fold.test.sh # all cases
24
+ # ./.claude/hooks/tests/forge-release-fold.test.sh ordering # ordering controls
25
+ # Filter tags (one arg, selects matching case blocks):
26
+ # unmerged merged grandfather human-verified signoff-tag stored-field
27
+ # exit-2 stable-sha ordering promotion forgery
28
+ # `promotion` + `forgery` (plan 51-01): validation_event=promotion_approval —
29
+ # a CI-signed tag on a deploy sha CONTAINING the unit's landed sha validates;
30
+ # tag deletion reverts; unsigned/wrong-key lookalikes and non-containing tags
31
+ # never validate. Tag pattern FROZEN 2026-07-16: promotion/{sha} (full deploy
32
+ # sha) — Track B must emit it byte-exactly.
33
+ # `ordering` maps to plan truth check 3: the unmerged/merged/validated ladder can
34
+ # never skip (unmerged never renders merged/validated; merged never validated).
35
+ #
36
+ # RED-phase friendly (TDD): a missing/non-executable helper does NOT abort the
37
+ # suite — every selected case FAILS with "(helper missing ...)" as the got-value
38
+ # and the suite exits non-zero. No FATAL short-circuit on the script under test.
39
+ # Exits 0 iff every selected case passes. Self-cleans on exit.
40
+ set -u
41
+
42
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
43
+ HELPER="$SCRIPT_DIR/../forge-release-fold.sh"
44
+
45
+ command -v ssh-keygen >/dev/null 2>&1 || { printf 'FATAL: ssh-keygen not found\n' >&2; exit 1; }
46
+
47
+ [ -x "$HELPER" ] || printf 'NOTE: %s missing or not executable — RED phase, every selected case below should FAIL\n' "$HELPER" >&2
48
+
49
+ ROOT="$(mktemp -d)"
50
+ trap 'rm -rf "$ROOT"' EXIT INT TERM
51
+
52
+ # Isolate from the developer's global/system git config — no $HOME leakage into
53
+ # fixture repos or into the helper's git invocations.
54
+ GIT_CONFIG_GLOBAL=/dev/null
55
+ GIT_CONFIG_SYSTEM=/dev/null
56
+ export GIT_CONFIG_GLOBAL GIT_CONFIG_SYSTEM
57
+
58
+ PASSED=0
59
+ FAILED=0
60
+ SELECTED=0
61
+ FILTER="${1:-}"
62
+
63
+ pass() { PASSED=$((PASSED + 1)); printf 'PASS: %s\n' "$1"; }
64
+ fail() { FAILED=$((FAILED + 1)); printf 'FAIL: %s\n' "$1" >&2; }
65
+ check() { # desc, actual, expected (exact match)
66
+ if [ "$2" = "$3" ]; then pass "$1"; else fail "$1 (got '$2', want '$3')"; fi
67
+ }
68
+ check_prefix() { # desc, actual, expected-prefix
69
+ case "$2" in
70
+ "$3"*) pass "$1" ;;
71
+ *) fail "$1 (got '$2', want prefix '$3')" ;;
72
+ esac
73
+ }
74
+ check_absent() { # desc, actual, forbidden-substring
75
+ case "$2" in
76
+ *"$3"*) fail "$1 (got '$2', must NOT contain '$3')" ;;
77
+ *) pass "$1" ;;
78
+ esac
79
+ }
80
+
81
+ running() { # tag... — 0 if this case block is selected by $FILTER (empty = all)
82
+ if [ -z "$FILTER" ]; then SELECTED=$((SELECTED + 1)); return 0; fi
83
+ for _t in "$@"; do
84
+ if [ "$_t" = "$FILTER" ]; then SELECTED=$((SELECTED + 1)); return 0; fi
85
+ done
86
+ return 1
87
+ }
88
+
89
+ # Run the helper from a given cwd; sets OUT (stdout) and RC (exit code). When the
90
+ # helper is absent (RED phase) each assertion fails with a clear got-value instead
91
+ # of the suite aborting.
92
+ run_fold() { # cwd, helper-args...
93
+ rf_cwd="$1"; shift
94
+ if [ -x "$HELPER" ]; then
95
+ if OUT="$(cd "$rf_cwd" && "$HELPER" "$@" 2>/dev/null)"; then RC=0; else RC=$?; fi
96
+ else
97
+ OUT="(helper missing or not executable: $HELPER)"
98
+ RC=127
99
+ fi
100
+ }
101
+
102
+ # One allowed-signer keypair (the "operator") + one non-allowed keypair (the
103
+ # "agent") shared across cases — the verify-signoff.test.sh pattern.
104
+ gen_key() { # path, comment
105
+ ssh-keygen -t ed25519 -N "" -C "$2" -f "$1" -q
106
+ }
107
+ gen_key "$ROOT/operator_key" "operator@example.com"
108
+ gen_key "$ROOT/agent_key" "agent@example.com"
109
+ gen_key "$ROOT/ci_key" "ci@example.com"
110
+
111
+ allowed_signers="$ROOT/allowed_signers"
112
+ printf 'operator@example.com namespaces="git" %s\n' "$(cat "$ROOT/operator_key.pub")" > "$allowed_signers"
113
+
114
+ # Promotion signers: the CI key ONLY (the host-side, Environment-scoped key).
115
+ # agent_key stays out — it is the rogue key for the forgery neg controls.
116
+ promotion_signers="$ROOT/promotion_signers"
117
+ printf 'ci@example.com namespaces="git" %s\n' "$(cat "$ROOT/ci_key.pub")" > "$promotion_signers"
118
+
119
+ evidence="cafef00d5511c0deba5eba11cafef00d5511c0deba5eba11cafef00d5511c0de"
120
+
121
+ # NO `git remote add` anywhere in this suite: fixtures are remote-less by
122
+ # construction, so any network dependency in the fold breaks here (NFR-030).
123
+ new_repo() { # name → prints repo path (main branch, one root commit)
124
+ d="$ROOT/repo.$1"
125
+ mkdir -p "$d"
126
+ git -C "$d" init -q
127
+ git -C "$d" symbolic-ref HEAD refs/heads/main
128
+ git -C "$d" config user.email t@example.com
129
+ git -C "$d" config user.name tester
130
+ git -C "$d" commit --allow-empty -qm init
131
+ printf '%s\n' "$d"
132
+ }
133
+
134
+ write_milestone() { # repo, N, yaml-body — writes + commits .forge/state/milestone-N.yml on the current branch
135
+ mkdir -p "$1/.forge/state"
136
+ printf '%s\n' "$3" > "$1/.forge/state/milestone-$2.yml"
137
+ git -C "$1" add ".forge/state/milestone-$2.yml"
138
+ git -C "$1" commit -qm "chore(forge): state milestone-$2"
139
+ }
140
+
141
+ write_stream() { # repo, unit-id (m-N), branch — commits .forge/streams/m-N.yml with runtime.branch
142
+ mkdir -p "$1/.forge/streams"
143
+ printf 'version: 1\n\nstream:\n id: %s\n status: active\n milestone: %s\n\nruntime:\n branch: %s\n worktree: ""\n' \
144
+ "$2" "$2" "$3" > "$1/.forge/streams/$2.yml"
145
+ git -C "$1" add ".forge/streams/$2.yml"
146
+ git -C "$1" commit -qm "chore(forge): stream $2"
147
+ }
148
+
149
+ mint_tag() { # repo, unit-id, signing-key, head, evidence — SSH-signed signoff/<unit-id>
150
+ git -C "$1" -c gpg.format=ssh -c "user.signingkey=$3" \
151
+ tag -s "signoff/$2" -m "wu: $2
152
+ head: $4
153
+ evidence: $5"
154
+ }
155
+
156
+ landed_sha() { # repo, N — last commit touching the unit's state file on main (the contract's stable identity)
157
+ git -C "$1" log -1 --format=%H main -- ".forge/state/milestone-$2.yml"
158
+ }
159
+
160
+ # --- promotion_approval fixture helpers (plan 51-01) ---------------------------
161
+
162
+ # write_promotion_binding <repo> <signers-path> — the locked B3 binding shape,
163
+ # working-tree project.yml (the mode file is a working-tree read by design;
164
+ # protected-path-held per the contract). Tag pattern FROZEN: promotion/{sha}.
165
+ write_promotion_binding() {
166
+ mkdir -p "$1/.forge"
167
+ cat > "$1/.forge/project.yml" <<EOF
168
+ forge:
169
+ validation_event: promotion_approval
170
+ promotion:
171
+ tag_pattern: "promotion/{sha}"
172
+ allowed_signers: "$2"
173
+ EOF
174
+ }
175
+
176
+ # mint_promotion_tag <repo> <deploy-sha> <signing-key> — SSH-signed
177
+ # promotion/<deploy-sha> pointing AT the deploy sha. The tag's target commit +
178
+ # containment IS the binding — no extra metadata (no head:/evidence: lines).
179
+ mint_promotion_tag() {
180
+ git -C "$1" -c gpg.format=ssh -c "user.signingkey=$3" \
181
+ tag -s "promotion/$2" -m "promotion: approved deploy $2" "$2"
182
+ }
183
+
184
+ # advance_main <repo> <msg> — one unrelated commit on main; prints the new tip
185
+ # (a deploy point containing everything landed so far).
186
+ advance_main() {
187
+ printf '%s\n' "$2" >> "$1/DEPLOY.txt"
188
+ git -C "$1" add DEPLOY.txt
189
+ git -C "$1" commit -qm "$2"
190
+ git -C "$1" rev-parse main
191
+ }
192
+
193
+ # --- case 1: unmerged — live branch not ancestor of main --------------------
194
+ if running unmerged ordering; then
195
+ printf '\n--- case 1: unmerged (live branch not ancestor of main) ---\n'
196
+ d="$(new_repo unmerged)"
197
+ write_milestone "$d" 91 'milestone:
198
+ id: 91
199
+ name: "fixture: unmerged unit"
200
+
201
+ current:
202
+ tier: standard
203
+ status: executing
204
+ last_updated: "2026-07-16"
205
+ human_verified: null
206
+
207
+ lifecycle:
208
+ worktree_mode: active
209
+ worktree_branch: "forge/m-91"'
210
+ git -C "$d" checkout -qb forge/m-91
211
+ git -C "$d" commit --allow-empty -qm "work on forge/m-91 (never merged)"
212
+ git -C "$d" checkout -q main
213
+ check "fixture repo has no remote (offline by construction)" "$(git -C "$d" remote)" ""
214
+ run_fold "$d" m-91
215
+ check "unmerged: exit 0 (a computed state, not an error)" "$RC" "0"
216
+ check "unmerged: stdout" "$OUT" "release_state=unmerged reason=not-on-main"
217
+
218
+ # Ordering negative control: the main-tree file LOOKS validated (complete +
219
+ # populated human_verified) but the unit's live branch never landed on main →
220
+ # still unmerged. validated REQUIRES merged — checked first, always. The live
221
+ # branch resolves via streams/m-92.yml runtime.branch (the `else` route of
222
+ # resolution rule 1: no lifecycle.worktree_branch in the state file).
223
+ d2="$(new_repo unmerged-control)"
224
+ write_milestone "$d2" 92 'milestone:
225
+ id: 92
226
+ name: "fixture: unmerged unit with validation-looking YAML"
227
+
228
+ current:
229
+ tier: standard
230
+ status: complete
231
+ completed_at: "2026-07-10"
232
+ last_updated: "2026-07-10"
233
+ human_verified:
234
+ at: "2026-07-10"
235
+ method: "smoke test"
236
+ notes: "looks signed off — but the branch never landed"
237
+
238
+ lifecycle:
239
+ worktree_mode: active'
240
+ write_stream "$d2" m-92 forge/m-92
241
+ git -C "$d2" checkout -qb forge/m-92
242
+ git -C "$d2" commit --allow-empty -qm "work on forge/m-92 (never merged)"
243
+ git -C "$d2" checkout -q main
244
+ run_fold "$d2" m-92
245
+ check "ordering control: exit 0" "$RC" "0"
246
+ check "ordering control: unmerged despite complete+human_verified in main tree" "$OUT" "release_state=unmerged reason=not-on-main"
247
+ fi
248
+
249
+ # --- case 2: merged but not validated ----------------------------------------
250
+ if running merged ordering; then
251
+ printf '\n--- case 2: merged-but-not-validated (no approval artifact) ---\n'
252
+ d="$(new_repo merged)"
253
+ write_milestone "$d" 93 'milestone:
254
+ id: 93
255
+ name: "fixture: merged unit, no approval"
256
+
257
+ current:
258
+ tier: standard
259
+ status: executing
260
+ last_updated: "2026-07-16"
261
+ human_verified: null
262
+
263
+ lifecycle:
264
+ worktree_mode: active
265
+ worktree_branch: "forge/m-93"'
266
+ git -C "$d" checkout -qb forge/m-93
267
+ git -C "$d" commit --allow-empty -qm "work on forge/m-93"
268
+ git -C "$d" checkout -q main
269
+ git -C "$d" merge -q --no-ff -m "merge forge/m-93" forge/m-93
270
+ run_fold "$d" m-93
271
+ check "merged: exit 0" "$RC" "0"
272
+ check "merged: stdout (never validated without approval)" "$OUT" "release_state=merged reason=no-signoff"
273
+
274
+ # --project <path>: same answer when invoked from outside the repo.
275
+ run_fold "$ROOT" m-93 --project "$d"
276
+ check "merged via --project from outside: exit 0" "$RC" "0"
277
+ check "merged via --project from outside: stdout" "$OUT" "release_state=merged reason=no-signoff"
278
+ fi
279
+
280
+ # --- case 3: legacy-complete grandfather --------------------------------------
281
+ if running grandfather ordering; then
282
+ printf '\n--- case 3: legacy-complete grandfather (complete, no tag, no human_verified) ---\n'
283
+ d="$(new_repo grandfather)"
284
+ write_milestone "$d" 1 'milestone:
285
+ id: 1
286
+ name: "fixture: pre-gate closed milestone"
287
+
288
+ current:
289
+ tier: standard
290
+ status: complete
291
+ completed_at: "2025-11-01"
292
+ last_updated: "2025-11-01"
293
+
294
+ lifecycle:
295
+ worktree_mode: retired'
296
+ run_fold "$d" m-1
297
+ check "grandfather: exit 0" "$RC" "0"
298
+ check "grandfather: stdout" "$OUT" "release_state=validated reason=legacy-complete"
299
+
300
+ # Explicit `human_verified: null` is NOT "set" — still the grandfather route.
301
+ write_milestone "$d" 2 'milestone:
302
+ id: 2
303
+ name: "fixture: closed milestone with explicit null human_verified"
304
+
305
+ current:
306
+ tier: standard
307
+ status: complete
308
+ completed_at: "2025-12-01"
309
+ last_updated: "2025-12-01"
310
+ human_verified: null
311
+
312
+ lifecycle:
313
+ worktree_mode: retired'
314
+ run_fold "$d" m-2
315
+ check "grandfather (human_verified: null): exit 0" "$RC" "0"
316
+ check "grandfather (human_verified: null): reason stays legacy-complete" "$OUT" "release_state=validated reason=legacy-complete"
317
+ fi
318
+
319
+ # --- case 4: human_verified set, not yet complete ------------------------------
320
+ if running human-verified; then
321
+ printf '\n--- case 4: human_verified populated, status not complete ---\n'
322
+ d="$(new_repo human-verified)"
323
+ write_milestone "$d" 4 'milestone:
324
+ id: 4
325
+ name: "fixture: human-verified unit still in reviewing"
326
+
327
+ current:
328
+ tier: standard
329
+ status: reviewing
330
+ last_updated: "2026-07-16"
331
+ human_verified:
332
+ at: "2026-07-16"
333
+ method: "device smoke test"
334
+ notes: "operator walked the flow"
335
+
336
+ lifecycle:
337
+ worktree_mode: active'
338
+ run_fold "$d" m-4
339
+ check "human_verified: exit 0" "$RC" "0"
340
+ check_prefix "human_verified: validated" "$OUT" "release_state=validated"
341
+ fi
342
+
343
+ # --- case 5: SSH-signed signoff tag, allowed vs non-allowed key ----------------
344
+ if running signoff-tag; then
345
+ printf '\n--- case 5: signoff tag (operator-signed vs wrong key) ---\n'
346
+ # 5a: operator-signed signoff/<id> at the landed sha → validated reason=signed-off
347
+ d="$(new_repo signoff)"
348
+ git -C "$d" config gpg.ssh.allowedSignersFile "$allowed_signers"
349
+ write_milestone "$d" 95 'milestone:
350
+ id: 95
351
+ name: "fixture: signed-off unit"
352
+
353
+ current:
354
+ tier: standard
355
+ status: executing
356
+ last_updated: "2026-07-16"
357
+ human_verified: null
358
+
359
+ lifecycle:
360
+ worktree_mode: active'
361
+ landed="$(landed_sha "$d" 95)"
362
+ mint_tag "$d" m-95 "$ROOT/operator_key" "$landed" "$evidence"
363
+ run_fold "$d" m-95
364
+ check "signed tag: exit 0" "$RC" "0"
365
+ check "signed tag: stdout" "$OUT" "release_state=validated reason=signed-off"
366
+
367
+ # 5b: same shape, tag signed by a NON-allowed key, no YAML fallback → the tag
368
+ # route must not validate; the unit stays merged.
369
+ d2="$(new_repo signoff-badkey)"
370
+ git -C "$d2" config gpg.ssh.allowedSignersFile "$allowed_signers"
371
+ write_milestone "$d2" 96 'milestone:
372
+ id: 96
373
+ name: "fixture: lookalike tag from a non-allowed key"
374
+
375
+ current:
376
+ tier: standard
377
+ status: executing
378
+ last_updated: "2026-07-16"
379
+ human_verified: null
380
+
381
+ lifecycle:
382
+ worktree_mode: active'
383
+ landed2="$(landed_sha "$d2" 96)"
384
+ mint_tag "$d2" m-96 "$ROOT/agent_key" "$landed2" "$evidence"
385
+ run_fold "$d2" m-96
386
+ check "wrong-key tag (no fallback): exit 0" "$RC" "0"
387
+ check_prefix "wrong-key tag (no fallback): merged, never validated" "$OUT" "release_state=merged"
388
+
389
+ # 5c: wrong-key tag but human_verified IS set in the main tree → validated via
390
+ # the YAML route — and never attributed to the tag (reason=signed-off forbidden).
391
+ d3="$(new_repo signoff-badkey-fallback)"
392
+ git -C "$d3" config gpg.ssh.allowedSignersFile "$allowed_signers"
393
+ write_milestone "$d3" 97 'milestone:
394
+ id: 97
395
+ name: "fixture: wrong-key tag with a real human_verified fallback"
396
+
397
+ current:
398
+ tier: standard
399
+ status: reviewing
400
+ last_updated: "2026-07-16"
401
+ human_verified:
402
+ at: "2026-07-16"
403
+ method: "device smoke test"
404
+ notes: "verified in session"
405
+
406
+ lifecycle:
407
+ worktree_mode: active'
408
+ landed3="$(landed_sha "$d3" 97)"
409
+ mint_tag "$d3" m-97 "$ROOT/agent_key" "$landed3" "$evidence"
410
+ run_fold "$d3" m-97
411
+ check "wrong-key tag (YAML fallback): exit 0" "$RC" "0"
412
+ check_prefix "wrong-key tag (YAML fallback): validated via YAML route" "$OUT" "release_state=validated"
413
+ check_absent "wrong-key tag (YAML fallback): never reason=signed-off" "$OUT" "reason=signed-off"
414
+ fi
415
+
416
+ # --- case 6: a stored release_state field cannot stick -------------------------
417
+ if running stored-field; then
418
+ printf '\n--- case 6: stored release_state ignored; main-tree reads only ---\n'
419
+ d="$(new_repo stored-field)"
420
+ write_milestone "$d" 61 'milestone:
421
+ id: 61
422
+ name: "fixture: adversarial stored release_state"
423
+
424
+ release_state: validated
425
+
426
+ current:
427
+ tier: standard
428
+ status: executing
429
+ release_state: validated
430
+ last_updated: "2026-07-16"
431
+ human_verified: null
432
+
433
+ lifecycle:
434
+ worktree_mode: active'
435
+ run_fold "$d" m-61
436
+ check "stored field: exit 0" "$RC" "0"
437
+ check "stored field: recomputed from git truth, stored value ignored" "$OUT" "release_state=merged reason=no-signoff"
438
+
439
+ # 6b: working-tree tamper — an uncommitted edit flips status + human_verified.
440
+ # The fold reads `git show main:...`, so the render must not change (an
441
+ # agent-writable working tree must not flip the result).
442
+ printf 'current:\n status: complete\n human_verified:\n at: "2026-07-16"\n method: "tampered"\n notes: "uncommitted working-tree edit"\n' \
443
+ > "$d/.forge/state/milestone-61.yml"
444
+ run_fold "$d" m-61
445
+ check "working-tree tamper: exit 0" "$RC" "0"
446
+ check "working-tree tamper: main-tree read unchanged" "$OUT" "release_state=merged reason=no-signoff"
447
+ fi
448
+
449
+ # --- case 7: exit 2 — never a false verdict ------------------------------------
450
+ if running exit-2; then
451
+ printf '\n--- case 7: exit 2 on unknown unit / bad invocation ---\n'
452
+ d="$(new_repo exit2)"
453
+ run_fold "$d" m-404
454
+ check "unknown unit: exit 2" "$RC" "2"
455
+ check_absent "unknown unit: no verdict on stdout" "$OUT" "release_state="
456
+
457
+ write_milestone "$d" 70 'milestone:
458
+ id: 70
459
+ name: "fixture: known unit for flag parsing"
460
+
461
+ current:
462
+ tier: standard
463
+ status: executing
464
+ last_updated: "2026-07-16"
465
+ human_verified: null'
466
+ run_fold "$d" m-70 --bogus
467
+ check "bad flag: exit 2" "$RC" "2"
468
+ check_absent "bad flag: no verdict on stdout" "$OUT" "release_state="
469
+
470
+ run_fold "$d"
471
+ check "no unit-id: exit 2" "$RC" "2"
472
+ check_absent "no unit-id: no verdict on stdout" "$OUT" "release_state="
473
+ fi
474
+
475
+ # --- case 8: stable landed sha (anti-flap, operator ruling 2026-07-16) ----------
476
+ if running stable-sha; then
477
+ printf '\n--- case 8: landed sha is stable — advancing main must not flap the state ---\n'
478
+ d="$(new_repo stable-sha)"
479
+ git -C "$d" config gpg.ssh.allowedSignersFile "$allowed_signers"
480
+ write_milestone "$d" 80 'milestone:
481
+ id: 80
482
+ name: "fixture: tracked unit, signed at its landed sha"
483
+
484
+ current:
485
+ tier: standard
486
+ status: executing
487
+ last_updated: "2026-07-16"
488
+ human_verified: null
489
+
490
+ lifecycle:
491
+ worktree_mode: active'
492
+ landed="$(landed_sha "$d" 80)"
493
+ mint_tag "$d" m-80 "$ROOT/operator_key" "$landed" "$evidence"
494
+ run_fold "$d" m-80
495
+ check "stable sha (before advance): exit 0" "$RC" "0"
496
+ check "stable sha (before advance): signed-off at the landed sha" "$OUT" "release_state=validated reason=signed-off"
497
+
498
+ # Advance main with an unrelated commit. The unit's landed sha must remain the
499
+ # last commit touching ITS state file — never main's moving tip. A tip-resolving
500
+ # fold flaps here: the tag's head: no longer matches → tag route dies → merged.
501
+ printf 'unrelated\n' > "$d/UNRELATED.txt"
502
+ git -C "$d" add UNRELATED.txt
503
+ git -C "$d" commit -qm "unrelated: advance main past the landed sha"
504
+ run_fold "$d" m-80
505
+ check "stable sha (after advance): exit 0" "$RC" "0"
506
+ check "stable sha (after advance): still signed-off — identity did not move" "$OUT" "release_state=validated reason=signed-off"
507
+
508
+ # Same anti-flap for a plain merged unit: state + reason unchanged across the advance.
509
+ write_milestone "$d" 81 'milestone:
510
+ id: 81
511
+ name: "fixture: tracked merged unit, no approval"
512
+
513
+ current:
514
+ tier: standard
515
+ status: executing
516
+ last_updated: "2026-07-16"
517
+ human_verified: null'
518
+ run_fold "$d" m-81
519
+ check "stable sha (merged unit, before advance): exit 0" "$RC" "0"
520
+ check "stable sha (merged unit, before advance): stdout" "$OUT" "release_state=merged reason=no-signoff"
521
+ printf 'more\n' >> "$d/UNRELATED.txt"
522
+ git -C "$d" add UNRELATED.txt
523
+ git -C "$d" commit -qm "unrelated: advance main again"
524
+ run_fold "$d" m-81
525
+ check "stable sha (merged unit, after advance): exit 0" "$RC" "0"
526
+ check "stable sha (merged unit, after advance): unchanged" "$OUT" "release_state=merged reason=no-signoff"
527
+ fi
528
+
529
+ # --- case 9: promotion_approval — CI-signed containing tag validates ------------
530
+ if running promotion; then
531
+ printf '\n--- case 9: promotion_approval (containment; delete reverts; isolation; anti-flap) ---\n'
532
+ # 9a: positive — unit landed sha CONTAINED IN a deploy sha carrying a genuine
533
+ # CI-signed promotion tag → validated. Containment, not tip-equality (operator
534
+ # ruling 2026-07-16): the tag approves a deploy; every unit inside it is covered.
535
+ d="$(new_repo promotion)"
536
+ write_milestone "$d" 101 'milestone:
537
+ id: 101
538
+ name: "fixture: promotion-approved unit"
539
+
540
+ current:
541
+ tier: standard
542
+ status: executing
543
+ last_updated: "2026-07-16"
544
+ human_verified: null
545
+
546
+ lifecycle:
547
+ worktree_mode: active'
548
+ landed="$(landed_sha "$d" 101)"
549
+ deploy="$(advance_main "$d" "deploy: ship it")"
550
+ mint_promotion_tag "$d" "$deploy" "$ROOT/ci_key"
551
+ write_promotion_binding "$d" "$promotion_signers"
552
+ check "promotion fixture has no remote (offline by construction)" "$(git -C "$d" remote)" ""
553
+ run_fold "$d" m-101
554
+ check "promotion positive: exit 0" "$RC" "0"
555
+ check_prefix "promotion positive: validated via containing CI-signed tag" "$OUT" "release_state=validated"
556
+
557
+ # 9b: delete-reverts (acceptance 2) — the operator deletes the tag, re-runs,
558
+ # and the same unit reverts to merged. Nothing stored anywhere to un-stick.
559
+ git -C "$d" tag -d "promotion/$deploy" >/dev/null 2>&1
560
+ run_fold "$d" m-101
561
+ check "delete-reverts: exit 0" "$RC" "0"
562
+ check "delete-reverts: merged reason=no-approval" "$OUT" "release_state=merged reason=no-approval"
563
+
564
+ # 9c: not-contained — a genuinely signed tag whose deploy commit does NOT
565
+ # contain the unit's landed sha (sibling history) must not validate it.
566
+ d2="$(new_repo promotion-notcontained)"
567
+ base="$(git -C "$d2" rev-parse main)"
568
+ write_milestone "$d2" 103 'milestone:
569
+ id: 103
570
+ name: "fixture: unit outside the approved deploy"
571
+
572
+ current:
573
+ tier: standard
574
+ status: executing
575
+ last_updated: "2026-07-16"
576
+ human_verified: null
577
+
578
+ lifecycle:
579
+ worktree_mode: active'
580
+ git -C "$d2" checkout -qb sibling "$base"
581
+ git -C "$d2" commit --allow-empty -qm "sibling deploy line (does not contain the unit)"
582
+ sibling_sha="$(git -C "$d2" rev-parse sibling)"
583
+ git -C "$d2" checkout -q main
584
+ mint_promotion_tag "$d2" "$sibling_sha" "$ROOT/ci_key"
585
+ write_promotion_binding "$d2" "$promotion_signers"
586
+ run_fold "$d2" m-103
587
+ check "not-contained: exit 0" "$RC" "0"
588
+ check "not-contained: merged reason=not-contained" "$OUT" "release_state=merged reason=not-contained"
589
+ check_absent "not-contained: never validated" "$OUT" "release_state=validated"
590
+
591
+ # 9d: binding-absent isolation — same repo shape, genuine containing signed
592
+ # tag, but NO promotion binding → in_session_signoff routes apply and the
593
+ # promotion tag alone must NOT validate a default-binding repo.
594
+ d3="$(new_repo promotion-nobinding)"
595
+ write_milestone "$d3" 104 'milestone:
596
+ id: 104
597
+ name: "fixture: promotion tag on a default-binding repo"
598
+
599
+ current:
600
+ tier: standard
601
+ status: executing
602
+ last_updated: "2026-07-16"
603
+ human_verified: null
604
+
605
+ lifecycle:
606
+ worktree_mode: active'
607
+ deploy3="$(advance_main "$d3" "deploy: ship it")"
608
+ mint_promotion_tag "$d3" "$deploy3" "$ROOT/ci_key"
609
+ # no project.yml written — validation_event absent ⇒ in_session_signoff
610
+ run_fold "$d3" m-104
611
+ check "binding-absent: exit 0" "$RC" "0"
612
+ check "binding-absent: promotion tag alone does not validate (no-signoff)" "$OUT" "release_state=merged reason=no-signoff"
613
+ check_absent "binding-absent: never validated" "$OUT" "release_state=validated"
614
+
615
+ # 9e: anti-flap (the ruling's motivating case) — validate via a containing
616
+ # signed tag, then advance main PAST the promotion with unrelated commits.
617
+ # Containment is stable forever; tip-equality would flap validated off here.
618
+ d4="$(new_repo promotion-antiflap)"
619
+ write_milestone "$d4" 105 'milestone:
620
+ id: 105
621
+ name: "fixture: promoted unit, main advances afterwards"
622
+
623
+ current:
624
+ tier: standard
625
+ status: executing
626
+ last_updated: "2026-07-16"
627
+ human_verified: null
628
+
629
+ lifecycle:
630
+ worktree_mode: active'
631
+ deploy4="$(advance_main "$d4" "deploy: ship it")"
632
+ mint_promotion_tag "$d4" "$deploy4" "$ROOT/ci_key"
633
+ write_promotion_binding "$d4" "$promotion_signers"
634
+ run_fold "$d4" m-105
635
+ check "anti-flap (at promotion): exit 0" "$RC" "0"
636
+ check_prefix "anti-flap (at promotion): validated" "$OUT" "release_state=validated"
637
+ first_out="$OUT"
638
+ advance_main "$d4" "unrelated: main moves past the promotion" >/dev/null
639
+ advance_main "$d4" "unrelated: and again" >/dev/null
640
+ run_fold "$d4" m-105
641
+ check "anti-flap (after advance): exit 0" "$RC" "0"
642
+ check_prefix "anti-flap (after advance): STILL validated — containment is stable" "$OUT" "release_state=validated"
643
+ check "anti-flap (after advance): render is identical, no flap" "$OUT" "$first_out"
644
+ fi
645
+
646
+ # --- case 10: forgery neg controls — lookalike tags compute nothing -------------
647
+ if running forgery; then
648
+ printf '\n--- case 10: forgery (unsigned / lightweight / wrong-key lookalikes) ---\n'
649
+ # 10a: annotated-but-UNSIGNED lookalike tag at a genuine deploy point →
650
+ # verification fails → merged reason=bad-signature; validated is impossible.
651
+ d="$(new_repo forgery-unsigned)"
652
+ write_milestone "$d" 111 'milestone:
653
+ id: 111
654
+ name: "fixture: unsigned lookalike promotion tag"
655
+
656
+ current:
657
+ tier: standard
658
+ status: executing
659
+ last_updated: "2026-07-16"
660
+ human_verified: null
661
+
662
+ lifecycle:
663
+ worktree_mode: active'
664
+ deploy="$(advance_main "$d" "deploy: ship it")"
665
+ git -C "$d" tag -a "promotion/$deploy" -m "promotion: approved deploy $deploy" "$deploy"
666
+ write_promotion_binding "$d" "$promotion_signers"
667
+ run_fold "$d" m-111
668
+ check "forgery-unsigned (annotated): exit 0" "$RC" "0"
669
+ check "forgery-unsigned (annotated): merged reason=bad-signature" "$OUT" "release_state=merged reason=bad-signature"
670
+ check_absent "forgery-unsigned (annotated): never validated" "$OUT" "release_state=validated"
671
+
672
+ # 10b: LIGHTWEIGHT lookalike (no tag object at all — nothing to verify).
673
+ d2="$(new_repo forgery-lightweight)"
674
+ write_milestone "$d2" 112 'milestone:
675
+ id: 112
676
+ name: "fixture: lightweight lookalike promotion tag"
677
+
678
+ current:
679
+ tier: standard
680
+ status: executing
681
+ last_updated: "2026-07-16"
682
+ human_verified: null
683
+
684
+ lifecycle:
685
+ worktree_mode: active'
686
+ deploy2="$(advance_main "$d2" "deploy: ship it")"
687
+ git -C "$d2" tag "promotion/$deploy2" "$deploy2"
688
+ write_promotion_binding "$d2" "$promotion_signers"
689
+ run_fold "$d2" m-112
690
+ check "forgery-lightweight: exit 0" "$RC" "0"
691
+ check "forgery-lightweight: merged reason=bad-signature" "$OUT" "release_state=merged reason=bad-signature"
692
+ check_absent "forgery-lightweight: never validated" "$OUT" "release_state=validated"
693
+
694
+ # 10c: tag SSH-signed by a key NOT in the promotion allowed-signers (the
695
+ # agent-minted lookalike, acceptance 5) → merged reason=bad-signature.
696
+ d3="$(new_repo forgery-wrongkey)"
697
+ write_milestone "$d3" 113 'milestone:
698
+ id: 113
699
+ name: "fixture: rogue-key promotion tag"
700
+
701
+ current:
702
+ tier: standard
703
+ status: executing
704
+ last_updated: "2026-07-16"
705
+ human_verified: null
706
+
707
+ lifecycle:
708
+ worktree_mode: active'
709
+ deploy3="$(advance_main "$d3" "deploy: ship it")"
710
+ mint_promotion_tag "$d3" "$deploy3" "$ROOT/agent_key"
711
+ write_promotion_binding "$d3" "$promotion_signers"
712
+ run_fold "$d3" m-113
713
+ check "forgery-wrong-key: exit 0" "$RC" "0"
714
+ check "forgery-wrong-key: merged reason=bad-signature" "$OUT" "release_state=merged reason=bad-signature"
715
+ check_absent "forgery-wrong-key: never validated" "$OUT" "release_state=validated"
716
+
717
+ # 10d: signers-file tamper (root-of-trust hardening) — the allowed-signers
718
+ # file is COMMITTED in-repo and points at the CI key only. An agent appends
719
+ # its own key to the WORKING-TREE copy and mints an own-key-signed tag. The
720
+ # fold reads the signers CONTENT from main-tree (like every other field), so
721
+ # the working-tree key is not trusted → merged reason=bad-signature. Proves
722
+ # an in-repo signers file cannot be forged from the working tree.
723
+ d4="$(new_repo forgery-signers-tamper)"
724
+ # committed signers file (CI key only), referenced by a repo-relative path.
725
+ mkdir -p "$d4/.forge"
726
+ printf 'ci@example.com namespaces="git" %s\n' "$(cat "$ROOT/ci_key.pub")" > "$d4/.forge/promotion_signers"
727
+ git -C "$d4" add ".forge/promotion_signers"
728
+ git -C "$d4" commit -qm "chore: commit CI-only promotion signers"
729
+ cat > "$d4/.forge/project.yml" <<'EOF'
730
+ forge:
731
+ validation_event: promotion_approval
732
+ promotion:
733
+ tag_pattern: "promotion/{sha}"
734
+ allowed_signers: ".forge/promotion_signers"
735
+ EOF
736
+ git -C "$d4" add ".forge/project.yml"
737
+ git -C "$d4" commit -qm "chore: promotion binding (in-repo signers)"
738
+ write_milestone "$d4" 114 'milestone:
739
+ id: 114
740
+ name: "fixture: in-repo signers, working-tree tampered"
741
+
742
+ current:
743
+ tier: standard
744
+ status: executing
745
+ last_updated: "2026-07-17"
746
+ human_verified: null
747
+
748
+ lifecycle:
749
+ worktree_mode: active'
750
+ deploy4="$(advance_main "$d4" "deploy: ship it")"
751
+ # positive control: genuine CI-signed tag validates via the MAIN-TREE signers.
752
+ mint_promotion_tag "$d4" "$deploy4" "$ROOT/ci_key"
753
+ run_fold "$d4" m-114
754
+ check "signers-tamper (control): CI tag validates via main-tree signers" "$OUT" "release_state=validated reason=promotion-approved"
755
+ # attack: append the rogue key to the WORKING-TREE signers (uncommitted),
756
+ # swap the tag for one signed by that rogue key.
757
+ git -C "$d4" tag -d "promotion/$deploy4" >/dev/null
758
+ printf 'agent@example.com namespaces="git" %s\n' "$(cat "$ROOT/agent_key.pub")" >> "$d4/.forge/promotion_signers"
759
+ mint_promotion_tag "$d4" "$deploy4" "$ROOT/agent_key"
760
+ run_fold "$d4" m-114
761
+ check "signers-tamper: exit 0" "$RC" "0"
762
+ check "signers-tamper: working-tree key NOT trusted → bad-signature" "$OUT" "release_state=merged reason=bad-signature"
763
+ check_absent "signers-tamper: never validated from a working-tree signers edit" "$OUT" "release_state=validated"
764
+ fi
765
+
766
+ # --- summary -------------------------------------------------------------------
767
+ if [ -n "$FILTER" ] && [ "$SELECTED" -eq 0 ]; then
768
+ printf 'ERROR: filter "%s" matched no cases (tags: unmerged merged grandfather human-verified signoff-tag stored-field exit-2 stable-sha ordering promotion forgery)\n' "$FILTER" >&2
769
+ exit 2
770
+ fi
771
+
772
+ printf '\n%d passed, %d failed (%d case blocks selected)\n' "$PASSED" "$FAILED" "$SELECTED"
773
+ [ "$FAILED" -eq 0 ]