@junghanacs/entwurf 0.12.8 → 0.12.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/AGENTS.md +4 -4
  2. package/CHANGELOG.md +20 -1
  3. package/DELIVERY.md +1 -1
  4. package/README.md +122 -10
  5. package/demo/README.md +2 -2
  6. package/docs/setup-clean-host.md +14 -3
  7. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +9 -15
  8. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +12 -12
  9. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +1 -1
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +5 -2
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn-production.js +8 -2
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +15 -4
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +483 -34
  14. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook.js +8 -3
  15. package/mcp/entwurf-bridge/dist/scripts/agy-imprint.js +14 -2
  16. package/mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js +155 -29
  17. package/mcp/entwurf-bridge/src/index.ts +10 -12
  18. package/package.json +7 -7
  19. package/pi-extensions/entwurf-control.ts +12 -12
  20. package/pi-extensions/lib/acp/models.ts +12 -12
  21. package/pi-extensions/lib/entwurf-fact-provider.ts +9 -2
  22. package/pi-extensions/lib/entwurf-v2-production.ts +8 -2
  23. package/pi-extensions/lib/entwurf-v2-spawn-production.ts +8 -2
  24. package/pi-extensions/lib/meta-sender-identity.ts +15 -5
  25. package/pi-extensions/lib/meta-session.ts +526 -38
  26. package/pi-extensions/meta-bridge-hook.ts +8 -2
  27. package/run.sh +30 -26
  28. package/scripts/agy-imprint.ts +15 -1
  29. package/scripts/check-acp-session-reuse.ts +1 -1
  30. package/scripts/check-acp-session-store.ts +3 -3
  31. package/scripts/check-agy-sender-identity.ts +83 -0
  32. package/scripts/check-entwurf-control-rpc.ts +2 -2
  33. package/scripts/check-entwurf-fact-provider.ts +9 -1
  34. package/scripts/check-entwurf-facts.ts +1 -1
  35. package/scripts/check-entwurf-mailbox-guard.ts +6 -2
  36. package/scripts/check-entwurf-resume-args.ts +6 -6
  37. package/scripts/check-entwurf-session-identity.ts +7 -6
  38. package/scripts/check-entwurf-v2-production.ts +4 -2
  39. package/scripts/check-entwurf-v2-spawn-production.ts +2 -2
  40. package/scripts/check-fresh-cut-gate.sh +305 -4
  41. package/scripts/check-meta-identity-consumers.ts +501 -1
  42. package/scripts/check-meta-listing.ts +91 -9
  43. package/scripts/check-meta-receiver-marker.ts +54 -0
  44. package/scripts/check-model-lock.ts +1 -1
  45. package/scripts/meta-bridge-fresh-cut.ts +164 -28
  46. package/scripts/pi_settings_io.py +65 -0
  47. package/scripts/register-pi-package.py +183 -37
  48. package/scripts/register-pi-provider.py +68 -10
  49. package/scripts/smoke-acp-socket-citizen-live.ts +2 -2
  50. package/scripts/smoke-meta-install-state.sh +1 -1
  51. package/scripts/smoke-pi-attach.ts +7 -2
  52. package/scripts/smoke-user-scope-citizen.sh +177 -0
@@ -9,7 +9,9 @@
9
9
  # naming the explicit fresh-cut verb in both invocation forms;
10
10
  # 3. fresh-cut REQUIRES quiescence (live OR unprovable both refuse) and archives
11
11
  # the whole generation through one preflighted plan, opening an empty one;
12
- # 4. the archive is forensic only — no runtime reads it, no restore verb.
12
+ # 4. the archive is forensic only — no runtime reads it, no restore verb;
13
+ # 5. exit status reports what already moved: complete / no-move / usage /
14
+ # incomplete cut transition / complete-with-cleanup-residue.
13
15
  #
14
16
  # A–D drive the activation path. E pins the doctor and the writers to ONE
15
17
  # certification verdict on the three defects that PARSE (drift / duplicate /
@@ -20,6 +22,7 @@
20
22
  # from the record through the adapter probe the dispatch itself uses. I holds the
21
23
  # other half of the prescription: a store that cannot be READ is not a store with
22
24
  # defects, and the refusal must not send the operator at a cut that cannot help.
25
+ # J executes the #54 exit contract, including the cut-complete cleanup-failure state.
23
26
  #
24
27
  # There is no frozen-fixture apparatus here: fresh-cut never rewrites a byte
25
28
  # (the archive is a rename), so the only byte claim is `archived == seeded`,
@@ -620,6 +623,165 @@ else
620
623
  bad "F8 the cut refused (or kept) a marker whose owner pid is provably absent" "$cut_out"
621
624
  fi
622
625
 
626
+ # THE RESIDUE ROW (#53 A, measured on a second Linux host 2026-07-25). A marker
627
+ # naming pid 1 is the one shape that is neither live, uncertain nor dead: init runs
628
+ # for the whole boot and its start-key does not change while it does, so
629
+ # classifyMarkerOwner answers `live` and THE ACTION THE REFUSAL PRESCRIBES cannot
630
+ # change that — the operator quiesces every session, as told, and the cut refuses
631
+ # again. Yet 0.12.8 refuses install on a pre-v3 store and names THIS cut as the only
632
+ # repair, so that host was stuck until the file was removed by hand. The pure
633
+ # classifier is right; the marker's claim is refuted by construction (no writer in
634
+ # this tree can mint one any more — see the isPlausibleOwnerPid cells in
635
+ # check-meta-receiver-marker / check-agy-sender-identity), so it is clearable
636
+ # residue. The recorded key must be THIS host's real key for pid 1, or the marker
637
+ # would classify `dead` on a key mismatch and the cell would pass while the bug
638
+ # lived.
639
+ init_key="$(node --experimental-strip-types -e '
640
+ import("'"$REPO"'/pi-extensions/lib/meta-session.ts").then((m) => process.stdout.write(m.processStartKey(1)));
641
+ ')"
642
+ if [ -z "$init_key" ]; then
643
+ bad "F19 SETUP MISS: could not read this host's start key for pid 1 — the residue cells cannot run"
644
+ else
645
+ for surface in senders receivers; do
646
+ reset_world prevgen
647
+ case "$surface" in
648
+ senders)
649
+ mkdir -p "$PI_CODING_AGENT_DIR/meta-senders/claude-code"
650
+ ghost="$PI_CODING_AGENT_DIR/meta-senders/claude-code/1.json"
651
+ cat > "$ghost" <<JSON
652
+ {
653
+ "backend": "claude-code",
654
+ "gardenId": "20260305T000000-dddd05",
655
+ "nativeSessionId": "prevgen-native-1",
656
+ "cwd": "/tmp/prevgen",
657
+ "ownerPid": 1,
658
+ "ownerStartKey": "$init_key",
659
+ "updatedAt": "2026-06-10T16:03:10.000Z"
660
+ }
661
+ JSON
662
+ ;;
663
+ receivers)
664
+ mkdir -p "$PI_CODING_AGENT_DIR/meta-receivers"
665
+ ghost="$PI_CODING_AGENT_DIR/meta-receivers/20260305T000000-dddd05.json"
666
+ cat > "$ghost" <<JSON
667
+ {
668
+ "gardenId": "20260305T000000-dddd05",
669
+ "backend": "claude-code",
670
+ "nativeSessionId": "prevgen-native-1",
671
+ "ownerPid": 1,
672
+ "ownerStartKey": "$init_key",
673
+ "ownerKind": "claude-code-cli",
674
+ "armProvenance": "session-start",
675
+ "updatedAt": "2026-06-10T16:03:10.000Z"
676
+ }
677
+ JSON
678
+ ;;
679
+ esac
680
+ set +e
681
+ cut_out="$(node --experimental-strip-types "${FRESH_CUT[@]}" 2>&1)"; rcr=$?
682
+ set -e
683
+ if [ "$rcr" = 0 ]; then
684
+ ok "F19 an init-owned $surface marker does NOT block the cut (refuted, not live)"
685
+ else
686
+ bad "F19 an init-owned $surface marker still deadlocks the cut — the upgrade path is closed" "$cut_out"
687
+ fi
688
+ if [ ! -e "$ghost" ]; then
689
+ ok "F20 the refuted $surface marker was cleared as residue"
690
+ else
691
+ bad "F20 the refuted $surface marker survived the cut"
692
+ fi
693
+ case "$cut_out" in
694
+ *refuted:*) ok "F21 the cut reports it as REFUTED, distinct from a proven-dead owner ($surface)" ;;
695
+ *) bad "F21 the cut disguised a proof of invalidity as a proof of death ($surface)" "$cut_out" ;;
696
+ esac
697
+ done
698
+ # The complement, and the reason F19 is a repair rather than a gate bypass: a marker
699
+ # whose owner is genuinely live still refuses. C1 holds that for a real live pid; this
700
+ # row proves the residue rule did not widen into "any marker the cut dislikes".
701
+ if [ -z "$start_key" ]; then
702
+ bad "F22 SETUP MISS: no start key for this gate's own pid — the live-complement cell cannot run"
703
+ else
704
+ reset_world prevgen
705
+ mkdir -p "$PI_CODING_AGENT_DIR/meta-senders/claude-code"
706
+ cat > "$PI_CODING_AGENT_DIR/meta-senders/claude-code/$$.json" <<JSON
707
+ {
708
+ "backend": "claude-code",
709
+ "gardenId": "20260305T000000-dddd05",
710
+ "ownerPid": $$,
711
+ "ownerStartKey": "$start_key",
712
+ "updatedAt": "2026-03-05T00:00:00.000Z"
713
+ }
714
+ JSON
715
+ store_before="$(store_bytes)"
716
+ set +e
717
+ cut_out="$(node --experimental-strip-types "${FRESH_CUT[@]}" 2>&1)"; rcl=$?
718
+ set -e
719
+ if [ "$rcl" != 0 ] && [ "$store_before" = "$(store_bytes)" ]; then
720
+ ok "F22 a LIVE owner still blocks the cut (the residue rule did not widen)"
721
+ else
722
+ bad "F22 the residue rule widened into a live owner — a running citizen would lose its address" "$cut_out"
723
+ fi
724
+ case "$cut_out" in
725
+ *"LIVE sender marker"*) ok "F22b the live owner is still reported LIVE, never refuted away" ;;
726
+ *) bad "F22b a live owner stopped being reported as live" "$cut_out" ;;
727
+ esac
728
+ fi
729
+ fi
730
+
731
+ # POST-CUT CLEANUP IS REPORTED, NOT LAUNDERED. The sweep runs AFTER the archive has
732
+ # moved, so a failure there must not throw away the story of what DID happen — and it
733
+ # must not be swallowed either, because this command's own output claims the residue
734
+ # was `cleared`. A bare `catch {}` around the unlink read EACCES/EROFS/an immutable
735
+ # attribute as "raced away" (cross-review 2026-07-25, on the very loop #53 A added).
736
+ # ENOENT alone is already-gone; everything else is named and turns the exit nonzero
737
+ # while the success lines stay.
738
+ if [ -z "$init_key" ]; then
739
+ bad "F23 SETUP MISS: no start key for pid 1 — the cleanup-failure cell cannot seed its marker"
740
+ elif [ "$(id -u)" = 0 ]; then
741
+ ok "F23 vacuous under uid 0: permission bits cannot make a directory unwritable for root, so the class this row drives cannot occur here"
742
+ else
743
+ reset_world prevgen
744
+ mkdir -p "$PI_CODING_AGENT_DIR/meta-senders/claude-code"
745
+ stuck_dir="$PI_CODING_AGENT_DIR/meta-senders/claude-code"
746
+ stuck="$stuck_dir/1.json"
747
+ cat > "$stuck" <<JSON
748
+ {
749
+ "backend": "claude-code",
750
+ "gardenId": "20260305T000000-dddd05",
751
+ "ownerPid": 1,
752
+ "ownerStartKey": "$init_key",
753
+ "updatedAt": "2026-06-10T16:03:10.000Z"
754
+ }
755
+ JSON
756
+ # r-x: the walk can still readdir + lstat + read the marker, but the unlink needs
757
+ # WRITE on the parent and fails EACCES. Restored right after, or the sandbox trap
758
+ # (and reset_world) could not clean up.
759
+ chmod a-w "$stuck_dir"
760
+ set +e
761
+ cut_out="$(node --experimental-strip-types "${FRESH_CUT[@]}" 2>&1)"; rcc=$?
762
+ set -e
763
+ chmod u+w "$stuck_dir"
764
+ if [ "$rcc" != 0 ]; then
765
+ ok "F23 an unremovable marker makes the cut exit NONZERO (a partial sweep never reads as a clean one)"
766
+ else
767
+ bad "F23 the cut reported success while a file it claims to have cleared is still there" "$cut_out"
768
+ fi
769
+ case "$cut_out" in
770
+ *"FAIL post-cut cleanup"*) ok "F23b the failure is NAMED as post-cut cleanup, with the file and the errno" ;;
771
+ *) bad "F23b the unlink failure was laundered into 'raced away'" "$cut_out" ;;
772
+ esac
773
+ if [ -n "$(find "$SANDBOX" -maxdepth 1 -type d -name 'store.archive-*')" ]; then
774
+ ok "F23c the archive still happened and is still reported — this is NOT a half-cut"
775
+ else
776
+ bad "F23c a cleanup failure threw away the generation move" "$cut_out"
777
+ fi
778
+ case "$cut_out" in
779
+ *"not a half-cut"*) ok "F23d the refusal tells the operator the store is safe and what to re-run" ;;
780
+ *) bad "F23d the failure left the operator unable to tell a cleanup miss from a half-cut" "$cut_out" ;;
781
+ esac
782
+ rm -f "$stuck"
783
+ fi
784
+
623
785
  # WIRING: the cut must ASK for the verdict, never compute it from a start-key. An
624
786
  # inline `processStartKey(...) === recorded` is precisely how "" (UNKNOWN) was read
625
787
  # as "dead" before this review — a fail-open path that must not come back.
@@ -825,10 +987,10 @@ done
825
987
  set +e
826
988
  out="$(node --experimental-strip-types "${FRESH_CUT[@]}" 2>&1)"; rcg=$?
827
989
  set -e
828
- if [ "$rcg" != 0 ]; then
829
- ok "G1 an occupied archive destination REFUSES the cut"
990
+ if [ "$rcg" = 1 ]; then
991
+ ok "G1 an occupied archive destination REFUSES the cut with the NO-MOVE status (exit 1)"
830
992
  else
831
- bad "G1 the cut proceeded into an occupied destination" "$out"
993
+ bad "G1 the cut proceeded into an occupied destination, or refused with the wrong status (rc=$rcg, want 1)" "$out"
832
994
  fi
833
995
  if [ -z "$(find "$SANDBOX" -maxdepth 1 -type d -name 'store.archive-*')" ] && [ "$store_before" = "$(store_bytes)" ]; then
834
996
  ok "G2 the collision refusal moved NOTHING — the store never left its place (no half-cut generation)"
@@ -1187,6 +1349,145 @@ case "$out" in
1187
1349
  esac
1188
1350
  rm -rf "$SANDBOX/nm"
1189
1351
 
1352
+ # ── J. the EXIT CONTRACT (#54): the status says WHAT ALREADY MOVED ───────────
1353
+ # Three world-states used to share one `1`, so the documented `fresh-cut && setup`
1354
+ # chain stopped identically on a refusal that changed nothing and on a cut that had
1355
+ # already unblocked the install. These cells pin each reachable code to the state it
1356
+ # names — the point is not that failures are nonzero, it is that a caller can BRANCH.
1357
+ echo "[check-fresh-cut-gate] J. exit contract"
1358
+
1359
+ # J1/J2 — the two states that need no host at all.
1360
+ set +e
1361
+ help_out="$(node --experimental-strip-types "${FRESH_CUT[@]}" --help 2>&1)"; rc=$?
1362
+ set -e
1363
+ if [ "$rc" = 0 ]; then ok "J1 --help exits 0"; else bad "J1 --help exited $rc" "$help_out"; fi
1364
+ case "$help_out" in
1365
+ *"EXIT CONTRACT"*) ok "J1b --help PRINTS the contract (an operator can read it where they hit it)" ;;
1366
+ *) bad "J1b --help never printed the exit contract" "$help_out" ;;
1367
+ esac
1368
+ set +e
1369
+ bad_out="$(node --experimental-strip-types "${FRESH_CUT[@]}" --wat 2>&1)"; rc=$?
1370
+ set -e
1371
+ if [ "$rc" = 2 ]; then ok "J2 an unknown argument is USAGE (exit 2), never a store verdict"; else bad "J2 unknown arg exited $rc, want 2" "$bad_out"; fi
1372
+
1373
+ # J3 — the success state, distinct from every failure below.
1374
+ reset_world prevgen
1375
+ set +e
1376
+ out="$(node --experimental-strip-types "${FRESH_CUT[@]}" 2>&1)"; rc=$?
1377
+ set -e
1378
+ if [ "$rc" = 0 ]; then ok "J3 a complete cut exits 0"; else bad "J3 a complete cut exited $rc" "$out"; fi
1379
+
1380
+ # J4 — quiesce refusal is NO-MOVE (1), the same code as the collision refusal in G1,
1381
+ # because they are the same world-state: the host is exactly what it was.
1382
+ reset_world prevgen
1383
+ start_key="$(node --experimental-strip-types -e '
1384
+ import("'"$REPO"'/pi-extensions/lib/meta-session.ts").then((m) => process.stdout.write(m.processStartKey(Number(process.argv[1]))));
1385
+ ' "$$")"
1386
+ if [ -z "$start_key" ]; then
1387
+ bad "J4 could not compute a start key for the gate's own pid"
1388
+ else
1389
+ mkdir -p "$PI_CODING_AGENT_DIR/meta-senders/claude-code"
1390
+ cat > "$PI_CODING_AGENT_DIR/meta-senders/claude-code/$$.json" <<JSON
1391
+ {
1392
+ "backend": "claude-code",
1393
+ "gardenId": "20260305T000000-dddd05",
1394
+ "nativeSessionId": "prevgen-native-1",
1395
+ "cwd": "/tmp/prevgen",
1396
+ "ownerPid": $$,
1397
+ "ownerStartKey": "$start_key",
1398
+ "updatedAt": "2026-03-05T00:00:00.000Z"
1399
+ }
1400
+ JSON
1401
+ set +e
1402
+ out="$(node --experimental-strip-types "${FRESH_CUT[@]}" 2>&1)"; rc=$?
1403
+ set -e
1404
+ if [ "$rc" = 1 ]; then ok "J4 a quiesce refusal is NO-MOVE (exit 1) — same code as any other untouched host"; else bad "J4 quiesce refusal exited $rc, want 1" "$out"; fi
1405
+ rm -f "$PI_CODING_AGENT_DIR/meta-senders/claude-code/$$.json"
1406
+ fi
1407
+
1408
+ # J5 — THE state #54 was opened for: the cut IS done and the cleanup is not. Staged by
1409
+ # taking write permission off the socket surface AFTER a dead socket is parked in it:
1410
+ # readdir/lstat/probe all still work (r-x), the unlink does not. Root bypasses DAC, so
1411
+ # it cannot prove anything here and says so instead of passing vacuously.
1412
+ if [ "$(id -u)" = 0 ]; then
1413
+ echo " skip J5 post-cut cleanup failure needs a non-root uid (DAC is what stages it)"
1414
+ else
1415
+ reset_world prevgen
1416
+ mkdir -p "$ENTWURF_DIR"
1417
+ node -e '
1418
+ const net = require("node:net");
1419
+ net.createServer().listen(process.argv[1], () => process.exit(0));
1420
+ ' "$ENTWURF_DIR/20260305T000000-dddd05.sock"
1421
+ if [ ! -S "$ENTWURF_DIR/20260305T000000-dddd05.sock" ]; then
1422
+ bad "J5 could not stage a dead socket — the cleanup-failure cells cannot run"
1423
+ else
1424
+ chmod 0500 "$ENTWURF_DIR"
1425
+ set +e
1426
+ out="$(node --experimental-strip-types "${FRESH_CUT[@]}" 2>&1)"; rc=$?
1427
+ set -e
1428
+ chmod 0700 "$ENTWURF_DIR"
1429
+ if [ "$rc" = 4 ]; then
1430
+ ok "J5 a cut whose post-cut cleanup failed exits 4 — NOT the 1 that means nothing moved"
1431
+ else
1432
+ bad "J5 post-cut cleanup failure exited $rc, want 4" "$out"
1433
+ fi
1434
+ if [ -n "$(find "$SANDBOX" -maxdepth 1 -type d -name 'store.archive-*')" ]; then
1435
+ ok "J5b …and the generation really WAS archived (the 4 is a claim about a cut that happened)"
1436
+ else
1437
+ bad "J5b exit 4 was reported although nothing was archived" "$out"
1438
+ fi
1439
+ if [ -d "$ENTWURF_META_SESSIONS_DIR" ] && [ -z "$(find "$ENTWURF_META_SESSIONS_DIR" -name '*.meta.json' 2>/dev/null)" ]; then
1440
+ ok "J5c …and the fresh generation is open and empty"
1441
+ else
1442
+ bad "J5c the fresh generation was not opened" "$out"
1443
+ fi
1444
+ case "$out" in
1445
+ *"FAIL post-cut cleanup"*) ok "J5d the report still names every file it could not remove" ;;
1446
+ *) bad "J5d the cleanup failure was silent" "$out" ;;
1447
+ esac
1448
+ # The whole point of distinguishing this state: the operator may go on.
1449
+ set +e
1450
+ out2="$("$REPO/run.sh" install "$PROJ" 2>&1)"; rc2=$?
1451
+ set -e
1452
+ if [ "$rc2" = 0 ]; then
1453
+ ok "J5e install PASSES after an exit-4 cut — the store no longer blocks it, which is what 4 means"
1454
+ else
1455
+ bad "J5e install still refused after a completed cut (then 4 is describing the wrong state)" "$out2"
1456
+ fi
1457
+ rm -f "$ENTWURF_DIR/20260305T000000-dddd05.sock"
1458
+ fi
1459
+ fi
1460
+
1461
+ # J6 — the INCOMPLETE-TRANSITION verdict, EXECUTED. A real post-move failure needs a
1462
+ # rename/open step to fail after another move succeeded, and every unprivileged way to stage that is closed by the plan
1463
+ # preflight (collision, unreadable surface, non-writable parent) — the remaining causes
1464
+ # (immutable attr, read-only mount, LSM) all need privileges this gate must not take. So
1465
+ # the branch calls one exported decision function and the gate drives THAT, rather than
1466
+ # asserting the state by grep and calling it proven.
1467
+ half="$(node --experimental-strip-types -e '
1468
+ import("'"$REPO"'/pi-extensions/lib/meta-session.ts").then((m) => {
1469
+ process.stdout.write([m.midCutExit(0), m.midCutExit(1), m.midCutExit(2), m.FRESH_CUT_EXIT.HALF_CUT].join(","));
1470
+ });
1471
+ ')"
1472
+ if [ "$half" = "1,3,3,3" ]; then
1473
+ ok "J6 midCutExit: a first-rename failure is NO-MOVE (1), any post-move failure is CUT-TRANSITION-INCOMPLETE (3)"
1474
+ else
1475
+ bad "J6 midCutExit verdicts drifted (got '$half', want '1,3,3,3')"
1476
+ fi
1477
+ # …and the branch really routes through it, so the executed verdict is the shipped one.
1478
+ if grep -q 'return midCutExit(archived.length);' "$REPO/scripts/meta-bridge-fresh-cut.ts"; then
1479
+ ok "J6b the mid-cut branch returns midCutExit(...), not a hand-written literal"
1480
+ else
1481
+ bad "J6b the mid-cut branch no longer routes through midCutExit — J6 would be vacuous"
1482
+ fi
1483
+ # The crash path is the one place a wrong answer is invisible: it reports a status for a
1484
+ # state nobody inspected, so it must ask the same function rather than assume a no-op.
1485
+ if grep -q 'process.exit(midCutExit(archivedSoFar));' "$REPO/scripts/meta-bridge-fresh-cut.ts"; then
1486
+ ok "J6c the top-level crash handler answers with midCutExit too (never a flat 'nothing moved')"
1487
+ else
1488
+ bad "J6c the crash handler exits a fixed code — a post-rename crash would claim nothing moved"
1489
+ fi
1490
+
1190
1491
  echo
1191
1492
  echo "[check-fresh-cut-gate] passed=$PASSED failed=$FAILED"
1192
1493
  [ "$FAILED" = 0 ] || exit 1