@junghanacs/entwurf 0.22.0 → 0.23.1

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 (135) hide show
  1. package/AGENTS.md +4 -1
  2. package/BASELINE.md +1 -0
  3. package/CHANGELOG.md +514 -0
  4. package/DELIVERY.md +10 -2
  5. package/README.md +43 -4
  6. package/VERIFY.md +47 -0
  7. package/docs/acp-backend-rail.md +96 -17
  8. package/docs/setup-clean-host.md +11 -1
  9. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +39 -40
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +2 -2
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +2 -1
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-declaration.js +609 -0
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +95 -56
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +40 -0
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +6 -2
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-facts.js +7 -1
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peer-observe.js +67 -2
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +9 -1
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +24 -0
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +2 -2
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +1 -1
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +423 -0
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +98 -0
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +1018 -0
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-placement.js +213 -0
  26. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +46 -273
  27. package/mcp/entwurf-bridge/dist/scripts/peer-facts.js +109 -0
  28. package/mcp/entwurf-bridge/src/index.ts +41 -41
  29. package/mcp/entwurf-bridge/tsconfig.build.json +1 -0
  30. package/package.json +9 -8
  31. package/pi-extensions/entwurf-control.ts +91 -52
  32. package/pi-extensions/lib/acp/acp-client.ts +3 -3
  33. package/pi-extensions/lib/acp/backend-adapter.ts +3 -3
  34. package/pi-extensions/lib/acp/backend.ts +4 -4
  35. package/pi-extensions/lib/acp/event-mapper.ts +5 -5
  36. package/pi-extensions/lib/acp/tool-surface.ts +2 -1
  37. package/pi-extensions/lib/codex-declaration.js +612 -0
  38. package/pi-extensions/lib/codex-fresh-preflight.ts +107 -51
  39. package/pi-extensions/lib/entwurf-control-rpc.ts +46 -0
  40. package/pi-extensions/lib/entwurf-fact-provider.ts +13 -2
  41. package/pi-extensions/lib/entwurf-facts.ts +14 -1
  42. package/pi-extensions/lib/entwurf-peer-observe.ts +72 -2
  43. package/pi-extensions/lib/entwurf-peers-render.ts +9 -1
  44. package/pi-extensions/lib/entwurf-self-address.ts +33 -0
  45. package/pi-extensions/lib/entwurf-v2-contract.ts +2 -2
  46. package/pi-extensions/lib/entwurf-v2-lock.ts +1 -1
  47. package/pi-extensions/lib/entwurf-v2-runner.ts +1 -1
  48. package/pi-extensions/lib/fresh-call-composition.ts +493 -0
  49. package/pi-extensions/lib/fresh-call-dispatch.ts +142 -0
  50. package/pi-extensions/lib/herdr-fresh-call.ts +1378 -0
  51. package/pi-extensions/lib/herdr-placement.ts +276 -0
  52. package/pi-extensions/lib/mux-fresh-call.ts +95 -301
  53. package/run.sh +588 -204
  54. package/scripts/check-acp-usage-accounting.ts +9 -9
  55. package/scripts/check-codex-birth-hook.ts +187 -0
  56. package/scripts/check-entwurf-control-rpc.ts +138 -0
  57. package/scripts/check-entwurf-fact-provider.ts +4 -2
  58. package/scripts/check-entwurf-peers-surface.ts +3 -1
  59. package/scripts/check-entwurf-self-address.ts +68 -0
  60. package/scripts/check-fresh-call-dispatch.ts +301 -0
  61. package/scripts/check-gate-qualification.ts +17 -4
  62. package/scripts/check-herdr-activation.ts +1293 -0
  63. package/scripts/check-herdr-fresh-call.ts +1250 -0
  64. package/scripts/check-herdr-placement.ts +383 -0
  65. package/scripts/check-herdr-plugin-build.ts +842 -0
  66. package/scripts/check-herdr-plugin-profile.ts +371 -0
  67. package/scripts/check-herdr-plugin.ts +523 -0
  68. package/scripts/check-herdr-runtime-bootstrap.ts +1264 -0
  69. package/scripts/check-herdr-sandbox.ts +456 -0
  70. package/scripts/check-herdr-supply.ts +184 -0
  71. package/scripts/check-mux-launcher-fence.ts +138 -0
  72. package/scripts/check-peer-facts.ts +331 -0
  73. package/scripts/check-pi-launch.ts +358 -0
  74. package/scripts/check-release-gate-outcomes.ts +19 -0
  75. package/scripts/check-typing-call-fence.ts +296 -0
  76. package/scripts/codex-birth-doctor.sh +159 -16
  77. package/scripts/codex-birth-install.sh +178 -72
  78. package/scripts/codex-birth-uninstall.sh +162 -7
  79. package/scripts/fixtures/herdr-supply.json +29 -0
  80. package/scripts/herdr-activation.mjs +536 -0
  81. package/scripts/herdr-plugin-activate.mjs +270 -0
  82. package/scripts/herdr-plugin-deactivate.mjs +193 -0
  83. package/scripts/herdr-runtime.mjs +1203 -0
  84. package/scripts/install-herdr-ci.sh +96 -0
  85. package/scripts/inventory-verification-surface.ts +86 -10
  86. package/scripts/lib/codex-fresh-live-protocol.ts +2 -2
  87. package/scripts/lib/tree-digest.ts +97 -0
  88. package/scripts/meta-bridge-install.sh +19 -2
  89. package/scripts/meta-bridge-state.py +167 -11
  90. package/scripts/mutants/codex-caller-seat.json +6 -8
  91. package/scripts/mutants/codex-native.json +195 -29
  92. package/scripts/mutants/control-socket-disconnect.json +57 -0
  93. package/scripts/mutants/fresh-call-dispatch.json +154 -0
  94. package/scripts/mutants/herdr-activation.json +305 -0
  95. package/scripts/mutants/herdr-fresh-call.json +448 -0
  96. package/scripts/mutants/herdr-placement.json +161 -0
  97. package/scripts/mutants/herdr-plugin-build.json +160 -0
  98. package/scripts/mutants/herdr-plugin-profile.json +202 -0
  99. package/scripts/mutants/herdr-plugin.json +133 -0
  100. package/scripts/mutants/herdr-runtime-bootstrap.json +411 -0
  101. package/scripts/mutants/herdr-supply.json +133 -0
  102. package/scripts/mutants/meta-hook-session-switch.json +2 -2
  103. package/scripts/mutants/mux-fresh-call.json +104 -17
  104. package/scripts/mutants/mux-launcher-fence.json +13 -0
  105. package/scripts/mutants/omp-fresh.json +4 -4
  106. package/scripts/mutants/peer-facts.json +98 -0
  107. package/scripts/mutants/pi-launch.json +113 -0
  108. package/scripts/mutants/resume-args.json +6 -6
  109. package/scripts/mutants/self-address.json +33 -0
  110. package/scripts/mutants/typing-call-fence.json +17 -0
  111. package/scripts/mutants/v2-surface.json +2 -2
  112. package/scripts/mutants/v2-visible-resume.json +17 -17
  113. package/scripts/peer-facts.ts +120 -0
  114. package/scripts/register-pi-package.py +8 -0
  115. package/scripts/register-pi-provider.py +88 -12
  116. package/scripts/smoke-acp-raw-turn-live.ts +1 -1
  117. package/scripts/smoke-codex-birth.sh +281 -12
  118. package/scripts/smoke-entwurf-v2-matrix-live.ts +1 -1
  119. package/scripts/smoke-herdr-fresh-call-live.ts +1027 -0
  120. package/scripts/smoke-herdr-plugin-build-live.ts +543 -0
  121. package/scripts/smoke-herdr-raw-install-live.sh +849 -0
  122. package/scripts/tsconfig.json +5 -0
  123. package/scripts/check-entwurf-facts.ts +0 -385
  124. package/scripts/check-entwurf-resume-args.ts +0 -122
  125. package/scripts/check-entwurf-v2-decider.ts +0 -657
  126. package/scripts/check-entwurf-v2-lock.ts +0 -371
  127. package/scripts/check-entwurf-v2-mailbox.ts +0 -261
  128. package/scripts/check-entwurf-v2-matrix.ts +0 -401
  129. package/scripts/check-entwurf-v2-native-push.ts +0 -221
  130. package/scripts/check-entwurf-v2-release.ts +0 -151
  131. package/scripts/check-entwurf-v2-runner.ts +0 -448
  132. package/scripts/check-entwurf-v2-send-fallback.ts +0 -350
  133. package/scripts/check-entwurf-v2-send.ts +0 -458
  134. package/scripts/check-entwurf-v2-visible-resume.ts +0 -445
  135. package/scripts/check-socket-discovery.ts +0 -401
@@ -21,6 +21,9 @@ MARKETPLACE = "meta-bridge-local"
21
21
  PLUGIN_REF = f"{PLUGIN}@{MARKETPLACE}"
22
22
  STATE_VERSION = 1
23
23
  OWNER = "entwurf meta-bridge"
24
+ # The only suffix an assembled marketplace path may end in — the inverse acts on that address, and
25
+ # the doctor already refuses anything else, so the preflight uses the SAME shape (not a looser one).
26
+ ASSEMBLED_SUFFIX = "/entwurf/meta-bridge/.assembled"
24
27
 
25
28
  PERMISSION_ALLOW = [
26
29
  "Bash",
@@ -304,6 +307,44 @@ def is_installed_package(repo: Path) -> bool:
304
307
  return parts[-3:] == ("node_modules", "@junghanacs", "entwurf")
305
308
 
306
309
 
310
+ # #116 M3-b2 — the explicit plugin mode. A Herdr plugin activates on a host where nothing entwurf is
311
+ # on PATH, so the bare bin shim cannot resolve. In that mode the commands are DERIVED from the
312
+ # certified stable active runtime root: the caller passes the root, these helpers append the fixed
313
+ # suffixes, and nothing accepts an arbitrary command. The default installed branch below is
314
+ # untouched — mode is an explicit input, never a path heuristic, because a stable-runtime install
315
+ # lives at `.../active/node_modules/@junghanacs/entwurf` and already satisfies every layout test.
316
+ PLUGIN_RUNTIME: Path | None = None
317
+
318
+
319
+ def stable_active_root() -> Path:
320
+ """The ONE runtime address, derived from this process's XDG/HOME exactly as the runtime owner
321
+ derives it (`scripts/herdr-runtime.mjs` resolveRuntimeLayout). A gate pins the two equal."""
322
+ data_home = os.environ.get("XDG_DATA_HOME") or os.path.join(os.path.expanduser("~"), ".local", "share")
323
+ return Path(data_home) / "entwurf" / "herdr-plugin" / "runtime" / "active"
324
+
325
+
326
+ def set_plugin_runtime(root: Path | None) -> None:
327
+ global PLUGIN_RUNTIME
328
+ if root is None:
329
+ PLUGIN_RUNTIME = None
330
+ return
331
+ if not root.is_absolute():
332
+ die(f"--plugin-runtime must be an ABSOLUTE stable runtime root, got {root}")
333
+ normalised = Path(os.path.normpath(str(root)))
334
+ # Named, not chosen: the caller may say which root, and it is accepted only when it IS this
335
+ # host's stable one. Any absolute directory would let a mode flag point Claude's MCP command at
336
+ # an executable of the caller's choosing.
337
+ expected = stable_active_root()
338
+ if normalised != expected:
339
+ die(f"--plugin-runtime must be this host's stable runtime root {expected}, got {normalised}")
340
+ PLUGIN_RUNTIME = normalised
341
+
342
+
343
+ def plugin_bin(name: str) -> str:
344
+ assert PLUGIN_RUNTIME is not None
345
+ return str(PLUGIN_RUNTIME / "node_modules" / ".bin" / name)
346
+
347
+
307
348
  def desired_mcp(repo: Path) -> dict[str, Any]:
308
349
  env = {
309
350
  # #50 C4: anonymous sends are refused by the BRIDGE DEFAULT now — the
@@ -313,6 +354,8 @@ def desired_mcp(repo: Path) -> dict[str, Any]:
313
354
  # session always has an authoritative garden-id sender.
314
355
  "ENTWURF_BRIDGE_EXTERNAL_AGENT_ID": "external-mcp/claude-code",
315
356
  }
357
+ if PLUGIN_RUNTIME is not None:
358
+ return {"type": "stdio", "command": plugin_bin("entwurf-bridge"), "args": [], "env": env}
316
359
  if is_installed_package(repo):
317
360
  # Installed package: wire the STABLE `entwurf-bridge` bin shim that npm/pnpm
318
361
  # place on PATH. Baking repo/mcp/entwurf-bridge/start.sh would embed the pnpm
@@ -333,6 +376,8 @@ def desired_mcp(repo: Path) -> dict[str, Any]:
333
376
 
334
377
 
335
378
  def desired_statusline(repo: Path) -> dict[str, Any]:
379
+ if PLUGIN_RUNTIME is not None:
380
+ return {"type": "command", "command": plugin_bin("entwurf-statusline")}
336
381
  if is_installed_package(repo):
337
382
  # Installed package: mirror the MCP stable-bin pattern. The statusLine is
338
383
  # executed at render time, so a bare bin shim lets npm/pnpm update the
@@ -342,20 +387,23 @@ def desired_statusline(repo: Path) -> dict[str, Any]:
342
387
  return {"type": "command", "command": str((repo / "scripts" / "meta-bridge-statusline.sh").resolve())}
343
388
 
344
389
 
345
- def prepare(repo: Path, asm: Path) -> None:
346
- existing = load_state(required=False)
347
- state = existing if existing is not None else init_state(repo, asm)
348
- state["updatedAt"] = iso_now()
349
- state["repo"] = str(repo.resolve())
350
- state["assembledMarketplacePath"] = str(asm.resolve())
351
-
390
+ def read_live_documents() -> tuple[dict[str, Any], dict[str, Any]]:
352
391
  settings = read_json(settings_path(), {})
353
392
  root = read_json(claude_root_config_path(), {})
354
393
  if not isinstance(settings, dict):
355
394
  die(f"{settings_path()} root must be a JSON object")
356
395
  if not isinstance(root, dict):
357
396
  die(f"{claude_root_config_path()} root must be a JSON object")
397
+ return settings, root
398
+
358
399
 
400
+ def plan_snapshots(state: dict[str, Any], repo: Path, asm: Path, settings: dict[str, Any], root: dict[str, Any]) -> None:
401
+ """Every snapshot decision `prepare` makes, in one place.
402
+
403
+ #116 M3-b2 amendment. `preflight-install` runs this against DEEP COPIES so the forward promise
404
+ is the real plan and not a JSON-object smoke test: a preflight that only checked the documents
405
+ parse would go green and then let `prepare` die on an array-type mismatch it never looked at.
406
+ """
359
407
  snapshot_value(
360
408
  state,
361
409
  "settings",
@@ -397,6 +445,15 @@ def prepare(repo: Path, asm: Path) -> None:
397
445
  legacy_absent_if_equal=desired_mcp(repo),
398
446
  )
399
447
 
448
+
449
+ def prepare(repo: Path, asm: Path) -> None:
450
+ existing = load_state(required=False)
451
+ state = existing if existing is not None else init_state(repo, asm)
452
+ state["updatedAt"] = iso_now()
453
+ state["repo"] = str(repo.resolve())
454
+ state["assembledMarketplacePath"] = str(asm.resolve())
455
+ settings, root = read_live_documents()
456
+ plan_snapshots(state, repo, asm, settings, root)
400
457
  write_json(state_path(), state, mode=0o600)
401
458
  print(f"[meta-bridge-state] prepared {state_path()}")
402
459
 
@@ -446,10 +503,41 @@ def apply(repo: Path, asm: Path) -> None:
446
503
  print("[meta-bridge-state] applied managed keyset (settings.json + user MCP)")
447
504
 
448
505
 
449
- def restore_entry(obj: dict[str, Any], entry: dict[str, Any]) -> None:
506
+ def certify_entry(obj: dict[str, Any], entry: dict[str, Any], where: str = "state entry") -> str:
507
+ """Prove this entry COULD be restored, and write nothing.
508
+
509
+ #116 M3-b2 amendment. This is the read-only twin of `restore_entry`, and it exists as one
510
+ function rather than two because the first cut of the uninstall preflight re-implemented the
511
+ checks and knew only ONE of the three entry shapes: it demanded `original` on every entry, while
512
+ `snapshot_array_items` writes `{originalExisted, added}`. Every real Claude activation therefore
513
+ failed its own preflight. A preflight that re-derives the restore rules will drift from them
514
+ again the next time a kind is added — so `restore_entry` calls THIS, and so does the preflight.
515
+ """
450
516
  path = entry.get("path")
451
517
  if not isinstance(path, list) or not all(isinstance(p, str) for p in path):
452
- die("bad state entry path")
518
+ die(f"{where}: bad state entry path")
519
+ kind = entry.get("kind")
520
+ if kind in ("map-entry", "scalar"):
521
+ original = entry.get("original")
522
+ if not isinstance(original, dict) or "existed" not in original:
523
+ die(f"{where}: bad scalar/map original in state")
524
+ return kind
525
+ if kind == "array-items":
526
+ if "originalExisted" not in entry:
527
+ die(f"{where}: array-items entry records no originalExisted")
528
+ if not isinstance(entry.get("added", []), list):
529
+ die(f"{where}: bad array added list in state")
530
+ existed, value = get_nested(obj, path)
531
+ if existed and not isinstance(value, list):
532
+ die(f"{where}: {'.'.join(path)} exists but is not an array; restoration is not applicable")
533
+ return kind
534
+ die(f"{where}: unknown state entry kind {kind!r}")
535
+ raise AssertionError("unreachable")
536
+
537
+
538
+ def restore_entry(obj: dict[str, Any], entry: dict[str, Any]) -> None:
539
+ certify_entry(obj, entry)
540
+ path = entry.get("path")
453
541
  kind = entry.get("kind")
454
542
  if kind in ("map-entry", "scalar"):
455
543
  original = entry.get("original")
@@ -525,8 +613,71 @@ def relinquish_retired_scalar(
525
613
 
526
614
 
527
615
  def preflight_uninstall() -> None:
528
- load_state(required=True)
529
- print(f"[meta-bridge-state] uninstall preflight ok ({state_path()})")
616
+ """Prove the ENTIRE restoration plan is applicable, writing nothing.
617
+
618
+ #116 M3-b2. This used to be `load_state(required=True)` and a print — it proved the ledger
619
+ existed and nothing else. An aggregate deactivate that calls it and then claims "every inverse
620
+ was preflighted" would be asserting a check that had not run, and a teardown that starts
621
+ mutating on that promise is how a Claude failure strands a Pi half. Every entry is certified
622
+ through the SAME function `restore_entry` uses, so the two cannot drift.
623
+ """
624
+ state = load_state(required=True)
625
+ assert state is not None
626
+ owner = state.get("owner")
627
+ if owner != OWNER:
628
+ die(f"{state_path()} is not ours: owner={owner!r} (expected {OWNER!r})")
629
+ settings, root = read_live_documents()
630
+ certified = 0
631
+ for path_key, doc in (("settings", settings), ("claudeRoot", root)):
632
+ entries = state.get("files", {}).get(path_key, {}).get("keys", {})
633
+ if not isinstance(entries, dict):
634
+ die(f"{state_path()} has no {path_key} key map")
635
+ for name, entry in entries.items():
636
+ if not isinstance(entry, dict):
637
+ die(f"{state_path()} {path_key} entry {name!r} is not a record")
638
+ certify_entry(doc, entry, f"{path_key} entry {name!r}")
639
+ certified += 1
640
+ certify_assembled_path(state.get("assembledMarketplacePath"))
641
+ print(
642
+ f"[meta-bridge-state] uninstall preflight ok ({state_path()}): owner={owner}, "
643
+ f"settings+claudeRoot parse, {certified} restore entries applicable, "
644
+ f"assembled={state.get('assembledMarketplacePath')}"
645
+ )
646
+
647
+
648
+ def certify_assembled_path(recorded: Any) -> None:
649
+ """The recorded marketplace path is an address the inverse acts on, so its SHAPE is checked."""
650
+ if not isinstance(recorded, str) or not recorded:
651
+ die(f"{state_path()} records no assembledMarketplacePath")
652
+ if not os.path.isabs(recorded):
653
+ die(f"{state_path()} assembledMarketplacePath is not absolute: {recorded}")
654
+ if not recorded.endswith(ASSEMBLED_SUFFIX):
655
+ die(f"{state_path()} assembledMarketplacePath does not end in {ASSEMBLED_SUFFIX}: {recorded}")
656
+
657
+
658
+ def preflight_install(repo: Path, asm: Path) -> None:
659
+ """The forward half of the same promise: run the REAL plan on copies, write nothing.
660
+
661
+ An aggregate activation has to be able to say "nothing will fail once I start" before it starts.
662
+ A preflight that only checked the documents parse would go green and then let `prepare` die on
663
+ an array-type mismatch it never looked at, so this walks `plan_snapshots` — the very function
664
+ `prepare` calls — against deep copies and certifies every entry it produces.
665
+ """
666
+ settings, root = read_live_documents()
667
+ existing = load_state(required=False)
668
+ state = copy.deepcopy(existing) if existing is not None else init_state(repo, asm)
669
+ plan_snapshots(state, repo, asm, copy.deepcopy(settings), copy.deepcopy(root))
670
+ planned = 0
671
+ for path_key, doc in (("settings", settings), ("claudeRoot", root)):
672
+ for name, entry in state["files"][path_key]["keys"].items():
673
+ certify_entry(doc, entry, f"planned {path_key} entry {name!r}")
674
+ planned += 1
675
+ certify_assembled_path(str(asm.resolve()))
676
+ mode = "plugin-runtime" if PLUGIN_RUNTIME is not None else ("installed" if is_installed_package(repo) else "clone")
677
+ print(
678
+ f"[meta-bridge-state] install preflight ok (mode={mode}, {planned} planned entries certified, "
679
+ f"mcp={desired_mcp(repo)['command']}, statusline={desired_statusline(repo)['command']}, asm={asm})"
680
+ )
530
681
 
531
682
 
532
683
  def uninstall() -> None:
@@ -667,6 +818,7 @@ def main() -> int:
667
818
  choices=[
668
819
  "prepare",
669
820
  "apply",
821
+ "preflight-install",
670
822
  "preflight-uninstall",
671
823
  "uninstall",
672
824
  "assembled-path",
@@ -678,6 +830,7 @@ def main() -> int:
678
830
  )
679
831
  parser.add_argument("--repo", default=Path(__file__).resolve().parents[1], type=Path)
680
832
  parser.add_argument("--asm", default=None, type=Path)
833
+ parser.add_argument("--plugin-runtime", default=None, type=Path)
681
834
  args = parser.parse_args()
682
835
  repo = args.repo.resolve()
683
836
  # The live artifact always lives under the XDG data dir — dev clone and
@@ -688,10 +841,13 @@ def main() -> int:
688
841
  default_asm = xdg_data / "entwurf" / "meta-bridge" / ".assembled"
689
842
  asm = (args.asm or default_asm).resolve()
690
843
  try:
844
+ set_plugin_runtime(args.plugin_runtime)
691
845
  if args.command == "prepare":
692
846
  prepare(repo, asm)
693
847
  elif args.command == "apply":
694
848
  apply(repo, asm)
849
+ elif args.command == "preflight-install":
850
+ preflight_install(repo, asm)
695
851
  elif args.command == "preflight-uninstall":
696
852
  preflight_uninstall()
697
853
  elif args.command == "uninstall":
@@ -157,10 +157,8 @@
157
157
  "claim": "CODEX-SEAT-TITLE-BECOMES-DELIVERY-TARGET",
158
158
  "title": "the caller's own garden address is replaced by the thread id the PANE TITLE matched, so a forgeable title decides who the fresh sibling is told to call back to. The sibling's first message would then be delivered against an identity nobody vouched for — the exact Hard Rule 16 crossing this lane is built to make impossible: the title is a placement input and may never become an address",
159
159
  "subject": "pi-extensions/lib/mux-fresh-call.ts",
160
- "find": ["\t\t\tcallerGardenId: params.callerGardenId,\n\t\t\tnonce,\n\t\t}),"],
161
- "replace": [
162
- "\t\t\tcallerGardenId: params.callerNativeSessionId ?? params.callerGardenId,\n\t\t\tnonce,\n\t\t}),"
163
- ],
160
+ "find": ["\t\t\tcallerGardenId,\n\t\t\tnonce,\n\t\t}),"],
161
+ "replace": ["\t\t\tcallerGardenId: params.callerNativeSessionId ?? callerGardenId,\n\t\t\tnonce,\n\t\t}),"],
164
162
  "gate": ["bash", "run.sh", "check-mux-fresh-call"],
165
163
  "timeoutSeconds": 120,
166
164
  "signature": "[QK:CODEX-SEAT-TITLE-BECOMES-DELIVERY-TARGET]",
@@ -203,12 +201,12 @@
203
201
  },
204
202
  {
205
203
  "claim": "CODEX-CALLER-PREFLIGHT-SURFACE-WIRED",
206
- "title": "the bridge stops running the caller-side preflight, so a Codex caller with no `thread-id` in its terminal title gets the generic `unresolved` seat refusal instead of the installer that repairs it a capability gap reported as an operator's missing TUI",
207
- "subject": "mcp/entwurf-bridge/src/index.ts",
204
+ "title": "the shared dispatcher stops running the caller-side preflight, so a Codex caller with no `thread-id` in its terminal title gets the generic `unresolved` seat refusal instead of the capability repair and the MCP surface, its only supplier, cannot tell the two apart",
205
+ "subject": "pi-extensions/lib/fresh-call-dispatch.ts",
208
206
  "find": [
209
- "\t\t\t\ttargetMissing === null && callerNativeSessionId !== undefined && placement === undefined\n\t\t\t\t\t? codexCallerFreshPreflight(process.env)\n\t\t\t\t\t: null;"
207
+ "\t\ttargetMissing === null && request.callerNativeSessionId !== undefined && request.placement === undefined\n\t\t\t? codexCallerFreshPreflight(env)\n\t\t\t: null;"
210
208
  ],
211
- "replace": ["\t\t\t\tnull;"],
209
+ "replace": ["\t\tnull;"],
212
210
  "gate": ["bash", "run.sh", "check-mux-fresh-call"],
213
211
  "timeoutSeconds": 120,
214
212
  "signature": "[QK:CODEX-CALLER-PREFLIGHT-SURFACE-WIRED]",
@@ -46,7 +46,7 @@
46
46
  {
47
47
  "claim": "FRESHCALL-CODEX-ARGV",
48
48
  "title": "Codex fresh launch drops the remote app-server attachment flag and opens an unrelated local session",
49
- "subject": "pi-extensions/lib/mux-fresh-call.ts",
49
+ "subject": "pi-extensions/lib/fresh-call-composition.ts",
50
50
  "find": ["\t\t\t\t\"--remote\","],
51
51
  "replace": ["\t\t\t\t\"--resume\","],
52
52
  "gate": ["bash", "run.sh", "check-mux-fresh-call"],
@@ -156,7 +156,7 @@
156
156
  },
157
157
  {
158
158
  "claim": "FRESHCALL-CODEX-MCP-ENV-ORDER",
159
- "title": "fresh preflight accepts the managed Codex MCP env_vars in any order, as long as the set matches a reordered vendor-visible env manifest passes as exact",
159
+ "title": "fresh preflight accepts the managed Codex MCP env_vars in any order, as long as the set matches \u2014 a reordered vendor-visible env manifest passes as exact",
160
160
  "subject": "pi-extensions/lib/codex-fresh-preflight.ts",
161
161
  "find": ["\t\tenvVars.some((name, index) => name !== CODEX_MCP_ENV_VARS[index])"],
162
162
  "replace": [
@@ -171,8 +171,8 @@
171
171
  "claim": "FRESHCALL-CODEX-HOOKS-SAFE",
172
172
  "title": "fresh preflight stops checking the operator-owned hooks.json for unsafe ownership/permissions/symlinks, so a world-writable or symlinked hooks.json is treated as a safe published birth unit",
173
173
  "subject": "pi-extensions/lib/codex-fresh-preflight.ts",
174
- "find": ["\tif (!isSafeOwnedFile(paths.hooksFile, expectedUid)) return true;"],
175
- "replace": ["\tif (false) return true;"],
174
+ "find": ["\tif (!isSafeOwnedFile(paths.hooksFile, expectedUid)) return null;"],
175
+ "replace": ["\tif (false) return null;"],
176
176
  "gate": ["bash", "run.sh", "check-mux-fresh-call"],
177
177
  "timeoutSeconds": 120,
178
178
  "signature": "[QK:FRESHCALL-CODEX-HOOKS-SAFE]",
@@ -217,14 +217,14 @@
217
217
  },
218
218
  {
219
219
  "claim": "FRESHCALL-CODEX-HOOK-KEYS",
220
- "title": "fresh preflight stops requiring the exact birth handler key set, so an async or extra-key hook one the installer would never publish and one that lets the turn proceed before the record exists reads as an installed birth unit",
221
- "subject": "pi-extensions/lib/codex-fresh-preflight.ts",
222
- "find": ["\t\tif (Object.keys(hook).sort().join(\",\") !== \"command,timeout,type\") return true;"],
223
- "replace": ["\t\tif (false) return true;"],
224
- "gate": ["bash", "run.sh", "check-mux-fresh-call"],
225
- "timeoutSeconds": 120,
220
+ "title": "the declaration leaf stops NAMING the extra handler key, so an async or extra-key hook \u2014 still refused by the normalized digest \u2014 arrives as an anonymous mismatch instead of a diagnosis that quotes the key. This claim owns the diagnosis, never the acceptance",
221
+ "subject": "pi-extensions/lib/codex-declaration.js",
222
+ "find": ["\tif (handlerKeys.join(\",\") !== \"command,timeout,type\") {"],
223
+ "replace": ["\tif (false) {"],
224
+ "gate": ["bash", "run.sh", "check-codex-birth-hook"],
225
+ "timeoutSeconds": 180,
226
226
  "signature": "[QK:FRESHCALL-CODEX-HOOK-KEYS]",
227
- "signatureSource": "test/codex-fresh-preflight.test.ts"
227
+ "signatureSource": "scripts/check-codex-birth-hook.ts"
228
228
  },
229
229
  {
230
230
  "claim": "CODEX-TOML-STATE-BEFORE-CONFIG",
@@ -311,14 +311,14 @@
311
311
  "signatureSource": "scripts/smoke-codex-config-state.sh"
312
312
  },
313
313
  {
314
- "claim": "CODEX-BIRTH-FOREIGN-HOOKS-REFUSED",
315
- "title": "the user-layer birth unit stops refusing a hooks.json no ownership state vouches for, so a declaration somebody else wrote is treated as one of ours",
314
+ "claim": "CODEX-BIRTH-JOINS-FOREIGN-FILE",
315
+ "title": "the birth unit refuses a hooks.json it did not write instead of joining it, so a host carrying a neighbouring integration can never be installed into",
316
316
  "subject": "scripts/codex-birth-install.sh",
317
- "find": [" if [ \"$STATE_PRESENT\" = \"0\" ]; then"],
318
- "replace": [" if false; then # mutant: treat a foreign hooks.json as ours"],
317
+ "find": [" if (sel.code !== \"declaration-absent\") throw new Error(sel.code + \": \" + sel.detail);"],
318
+ "replace": [" throw new Error(\"hooks.json already exists and no ownership state vouches for it\");"],
319
319
  "gate": ["bash", "run.sh", "smoke-codex-birth"],
320
320
  "timeoutSeconds": 120,
321
- "signature": "[QK:CODEX-BIRTH-FOREIGN-HOOKS-REFUSED]",
321
+ "signature": "[QK:CODEX-BIRTH-JOINS-FOREIGN-FILE]",
322
322
  "signatureSource": "scripts/smoke-codex-birth.sh"
323
323
  },
324
324
  {
@@ -380,10 +380,14 @@
380
380
  },
381
381
  {
382
382
  "claim": "FRESHCALL-CODEX-PREMUTATION-MCP",
383
- "title": "the MCP surface stops asking the Codex TARGET capability question before placement, so a host with no birth unit, no vendor trust receipt, no MCP hand or no app-server reaches tmux first — a window opens, and the operator is told about the missing capability only after something visible already exists",
383
+ "title": "the MCP public surface relabels a Codex request as pi at the shared dispatcher seam, so the one central capability preflight is bypassed and placement runs first",
384
384
  "subject": "mcp/entwurf-bridge/src/index.ts",
385
- "find": ["\t\t\tconst targetMissing = backend === \"codex\" ? await codexFreshPreflight(process.env) : null;"],
386
- "replace": ["\t\t\tconst targetMissing = null;"],
385
+ "find": [
386
+ "\t\t\tconst dispatched = await dispatchFreshCall({\n\t\t\t\tbackend,\n\t\t\t\tmodel,\n\t\t\t\ttask,\n\t\t\t\tcwd,\n\t\t\t\tplacement,\n\t\t\t\tcallerGardenId,\n\t\t\t\tcallerNativeSessionId,\n\t\t\t\tcallerCwd,\n\t\t\t});"
387
+ ],
388
+ "replace": [
389
+ "\t\t\tconst dispatched = await dispatchFreshCall({\n\t\t\t\tbackend: \"pi\",\n\t\t\t\tmodel,\n\t\t\t\ttask,\n\t\t\t\tcwd,\n\t\t\t\tplacement,\n\t\t\t\tcallerGardenId,\n\t\t\t\tcallerNativeSessionId,\n\t\t\t\tcallerCwd,\n\t\t\t});"
390
+ ],
387
391
  "gate": ["bash", "run.sh", "check-mux-fresh-call"],
388
392
  "timeoutSeconds": 180,
389
393
  "signature": "[QK:FRESHCALL-CODEX-PREMUTATION-MCP]",
@@ -391,10 +395,10 @@
391
395
  },
392
396
  {
393
397
  "claim": "FRESHCALL-CODEX-PREMUTATION-PI",
394
- "title": "the native pi public surface stops running the Codex capability preflight, so the same window opens from the other public door",
398
+ "title": "the native pi public surface relabels a Codex request as pi at the shared dispatcher seam, bypassing the same central preflight from the other public door",
395
399
  "subject": "pi-extensions/entwurf-control.ts",
396
- "find": ["\t\t\t\t\tparams.backend === \"codex\""],
397
- "replace": ["\t\t\t\t\tfalse"],
400
+ "find": ["\t\t\t\t\tbackend: params.backend,"],
401
+ "replace": ["\t\t\t\t\tbackend: \"pi\","],
398
402
  "gate": ["bash", "run.sh", "check-mux-fresh-call"],
399
403
  "timeoutSeconds": 180,
400
404
  "signature": "[QK:FRESHCALL-CODEX-PREMUTATION-PI]",
@@ -445,9 +449,7 @@
445
449
  "claim": "FRESHCALL-CODEX-TRUST-RECEIPT",
446
450
  "title": "fresh preflight opens a Codex sibling whose birth hook the vendor never agreed to run",
447
451
  "subject": "pi-extensions/lib/codex-fresh-preflight.ts",
448
- "find": [
449
- "\tif (config === null || trustReceiptMissing(config, paths.hooksFile)) return \"codex-birth-trust-missing\";"
450
- ],
452
+ "find": ["\tif (config === null || trustReceiptMissing(config, key)) return \"codex-birth-trust-missing\";"],
451
453
  "replace": ["\tif (config === null) return \"codex-birth-trust-missing\";"],
452
454
  "gate": ["bash", "run.sh", "check-mux-fresh-call"],
453
455
  "timeoutSeconds": 180,
@@ -458,8 +460,10 @@
458
460
  "claim": "FRESHCALL-CODEX-STATE-DIGEST",
459
461
  "title": "fresh preflight accepts a birth closure whose bytes no longer match the digests its own ownership state recorded",
460
462
  "subject": "pi-extensions/lib/codex-fresh-preflight.ts",
461
- "find": ["\treturn closureDriftedFromState(paths, expectedUid);"],
462
- "replace": ["\treturn false; // mutant: existence is enough"],
463
+ "find": [
464
+ "\t\tif (typeof member.sha256 !== \"string\" || digest(path.join(paths.helperDir, rel)) !== member.sha256) return null;"
465
+ ],
466
+ "replace": ["\t\tif (typeof member.sha256 !== \"string\") return null; // mutant: existence is enough"],
463
467
  "gate": ["bash", "run.sh", "check-mux-fresh-call"],
464
468
  "timeoutSeconds": 180,
465
469
  "signature": "[QK:FRESHCALL-CODEX-STATE-DIGEST]",
@@ -518,7 +522,7 @@
518
522
  "title": "fresh preflight compares only the digests a state happens to name, so a short inventory hides an unchecked closure member",
519
523
  "subject": "pi-extensions/lib/codex-fresh-preflight.ts",
520
524
  "find": [
521
- "\tif (named.length !== expected.length) return true;\n\tif (expected.some((name) => !named.includes(name))) return true;\n\tif (named.some((name, index) => named.indexOf(name) !== index)) return true;"
525
+ "\tif (named.length !== expected.length) return null;\n\tif (expected.some((name) => !named.includes(name))) return null;\n\tif (named.some((name, index) => named.indexOf(name) !== index)) return null;"
522
526
  ],
523
527
  "replace": ["\t// mutant: judge only the members a state happens to name"],
524
528
  "gate": ["bash", "run.sh", "check-mux-fresh-call"],
@@ -554,7 +558,7 @@
554
558
  "claim": "FRESHCALL-CODEX-STATE-PARENT-AUTHORITY",
555
559
  "title": "fresh preflight trusts a digest inventory sitting in a directory anyone on the host can rewrite",
556
560
  "subject": "pi-extensions/lib/codex-fresh-preflight.ts",
557
- "find": ["\tif (!isSafeOwnedDir(path.dirname(paths.stateFile), expectedUid)) return true;"],
561
+ "find": ["\tif (!isSafeOwnedDir(path.dirname(paths.stateFile), expectedUid)) return null;"],
558
562
  "replace": ["\t// mutant: the directory holding the receipt is nobody's business"],
559
563
  "gate": ["bash", "run.sh", "check-mux-fresh-call"],
560
564
  "timeoutSeconds": 180,
@@ -833,6 +837,168 @@
833
837
  "timeoutSeconds": 180,
834
838
  "signature": "[QK:CODEX-LIVE-RECOVERY-SOURCE-ONLY]",
835
839
  "signatureSource": "test/codex-fresh-live-protocol.test.ts"
840
+ },
841
+ {
842
+ "claim": "CODEX-DECL-CANONICAL-KEY-ORDER",
843
+ "title": "the declaration digest is taken over unsorted keys, so a neighbour re-serializing the document reads as an edited entwurf declaration",
844
+ "subject": "pi-extensions/lib/codex-declaration.js",
845
+ "find": ["\tconst keys = Object.keys(value).sort();"],
846
+ "replace": ["\tconst keys = Object.keys(value);"],
847
+ "gate": ["bash", "run.sh", "check-codex-birth-hook"],
848
+ "timeoutSeconds": 180,
849
+ "signature": "[QK:CODEX-DECL-CANONICAL-KEY-ORDER]",
850
+ "signatureSource": "scripts/check-codex-birth-hook.ts"
851
+ },
852
+ {
853
+ "claim": "CODEX-DECL-SELECT-BY-COMMAND",
854
+ "title": "entwurf's declaration is selected by POSITION instead of by its launcher command, so a neighbour at index 0 is certified as ours",
855
+ "subject": "pi-extensions/lib/codex-declaration.js",
856
+ "find": [
857
+ "\t\t\tif (handler !== null && typeof handler === \"object\" && !Array.isArray(handler) && handler.command === want) {"
858
+ ],
859
+ "replace": [
860
+ "\t\t\tif (handler !== null && typeof handler === \"object\" && !Array.isArray(handler) && h === 0) {"
861
+ ],
862
+ "gate": ["bash", "run.sh", "check-codex-birth-hook"],
863
+ "timeoutSeconds": 180,
864
+ "signature": "[QK:CODEX-DECL-SELECT-BY-COMMAND]",
865
+ "signatureSource": "scripts/check-codex-birth-hook.ts"
866
+ },
867
+ {
868
+ "claim": "CODEX-DECL-SPLICE-KEEPS-FOREIGN-BYTES",
869
+ "title": "a certified splice returns a RE-SERIALIZED document instead of the spliced text, rewriting every byte a neighbouring integration owns",
870
+ "subject": "pi-extensions/lib/codex-declaration.js",
871
+ "find": ["\treturn spliced;"],
872
+ "replace": ["\treturn JSON.stringify(reparsed, null, 2);"],
873
+ "gate": ["bash", "run.sh", "check-codex-birth-hook"],
874
+ "timeoutSeconds": 180,
875
+ "signature": "[QK:CODEX-DECL-SPLICE-KEEPS-FOREIGN-BYTES]",
876
+ "signatureSource": "scripts/check-codex-birth-hook.ts"
877
+ },
878
+ {
879
+ "claim": "CODEX-DECL-SPLICE-CERTIFIED",
880
+ "title": "the span splice is written out on the span reader's word alone, with no re-parse proving it landed where the caller intended",
881
+ "subject": "pi-extensions/lib/codex-declaration.js",
882
+ "find": ["\tif (canonicalJson(reparsed) !== canonicalJson(expected)) {"],
883
+ "replace": ["\tif (false) {"],
884
+ "gate": ["bash", "run.sh", "check-codex-birth-hook"],
885
+ "timeoutSeconds": 180,
886
+ "signature": "[QK:CODEX-DECL-SPLICE-CERTIFIED]",
887
+ "signatureSource": "scripts/check-codex-birth-hook.ts"
888
+ },
889
+ {
890
+ "claim": "CODEX-DECLARATION-DUPLICATED",
891
+ "title": "a duplicated entwurf declaration is first-match accepted, so the vendor runs the birth hook twice and only one position carries the receipt",
892
+ "subject": "pi-extensions/lib/codex-declaration.js",
893
+ "find": ["\tif (mine.length > 1) {"],
894
+ "replace": ["\tif (mine.length > 2) {"],
895
+ "gate": ["bash", "run.sh", "check-mux-fresh-call"],
896
+ "timeoutSeconds": 180,
897
+ "signature": "[QK:CODEX-DECLARATION-DUPLICATED]",
898
+ "signatureSource": "test/codex-fresh-preflight.test.ts"
899
+ },
900
+ {
901
+ "claim": "CODEX-DECLARATION-TRUST-INDEX",
902
+ "title": "fresh preflight reads the vendor trust receipt at the constant :0:0, so a neighbour's approval at index 0 admits a birth hook the vendor never agreed to run",
903
+ "subject": "pi-extensions/lib/codex-fresh-preflight.ts",
904
+ "find": ["\tconst key = trustReceiptKey(paths.hooksFile, declaration.groupIndex, declaration.handlerIndex);"],
905
+ "replace": ["\tconst key = trustReceiptKey(paths.hooksFile, 0, 0);"],
906
+ "gate": ["bash", "run.sh", "check-mux-fresh-call"],
907
+ "timeoutSeconds": 180,
908
+ "signature": "[QK:CODEX-DECLARATION-TRUST-INDEX]",
909
+ "signatureSource": "test/codex-fresh-preflight.test.ts"
910
+ },
911
+ {
912
+ "claim": "CODEX-BIRTH-TRUST-INDEX-MEASURED",
913
+ "title": "the doctor reads the vendor trust receipt at the constant :0:0, reporting a neighbour's approval as entwurf's own",
914
+ "subject": "scripts/codex-birth-doctor.sh",
915
+ "find": [" TRUST_KEY=\"$HOOKS_FILE:session_start:$DECL_GROUP_INDEX:$DECL_HANDLER_INDEX\""],
916
+ "replace": [" TRUST_KEY=\"$HOOKS_FILE:session_start:0:0\""],
917
+ "gate": ["bash", "run.sh", "smoke-codex-birth"],
918
+ "timeoutSeconds": 180,
919
+ "signature": "[QK:CODEX-BIRTH-TRUST-INDEX-MEASURED]",
920
+ "signatureSource": "scripts/smoke-codex-birth.sh"
921
+ },
922
+ {
923
+ "claim": "CODEX-BIRTH-SHARED-FILE-OWNERSHIP",
924
+ "title": "the shared ownership predicate stops seeing a group/world-writable file, so all four surfaces write into and certify bytes somebody else can rewrite",
925
+ "subject": "pi-extensions/lib/codex-declaration.js",
926
+ "find": ["\tif ((stat.mode & 0o022) !== 0) return \"writable-by-others\";"],
927
+ "replace": [""],
928
+ "gate": ["bash", "run.sh", "smoke-codex-birth"],
929
+ "timeoutSeconds": 300,
930
+ "signature": "[QK:CODEX-BIRTH-SHARED-FILE-OWNERSHIP]",
931
+ "signatureSource": "scripts/smoke-codex-birth.sh"
932
+ },
933
+ {
934
+ "claim": "CODEX-BIRTH-DOCTOR-SHARED-OWNERSHIP",
935
+ "title": "the doctor stops asking who owns the shared file and goes straight to the digest, so it reports green on a host every launch refuses",
936
+ "subject": "scripts/codex-birth-doctor.sh",
937
+ "find": [" if (owned !== \"ok\") {"],
938
+ "replace": [" if (false) {"],
939
+ "gate": ["bash", "run.sh", "smoke-codex-birth"],
940
+ "timeoutSeconds": 300,
941
+ "signature": "[QK:CODEX-BIRTH-DOCTOR-SHARED-OWNERSHIP]",
942
+ "signatureSource": "scripts/smoke-codex-birth.sh"
943
+ },
944
+ {
945
+ "claim": "CODEX-BIRTH-INVERSE-SHARED-OWNERSHIP",
946
+ "title": "the inverse stops asking and rewrites a shared file it cannot bind, instead of leaving it as found and counting the drift",
947
+ "subject": "scripts/codex-birth-uninstall.sh",
948
+ "find": [" if [ \"$owned\" != \"ok\" ]; then"],
949
+ "replace": [" if false; then"],
950
+ "gate": ["bash", "run.sh", "smoke-codex-birth"],
951
+ "timeoutSeconds": 300,
952
+ "signature": "[QK:CODEX-BIRTH-INVERSE-SHARED-OWNERSHIP]",
953
+ "signatureSource": "scripts/smoke-codex-birth.sh"
954
+ },
955
+ {
956
+ "claim": "CODEX-BIRTH-INVERSE-KEEPS-PRE-EXISTING-FILE",
957
+ "title": "the FILE verdict goes back to asking only whether our group was the last declaration, so a hooks.json that existed before this unit is deleted with its owners top-level bytes",
958
+ "subject": "scripts/codex-birth-uninstall.sh",
959
+ "find": [
960
+ " if (createdByUs === \"yes\" && soleDeclaration && topLevel === \"description,hooks\" && ourDescription) {"
961
+ ],
962
+ "replace": [" if (soleDeclaration) {"],
963
+ "gate": ["bash", "run.sh", "smoke-codex-birth"],
964
+ "timeoutSeconds": 300,
965
+ "signature": "[QK:CODEX-BIRTH-INVERSE-KEEPS-PRE-EXISTING-FILE]",
966
+ "signatureSource": "scripts/smoke-codex-birth.sh"
967
+ },
968
+ {
969
+ "claim": "CODEX-BIRTH-DOCTOR-ORPHAN-RED",
970
+ "title": "the doctor stops looking for our own bytes when the receipt is gone, so a half-removed unit reads as a clean host and invites a reinstall over bytes the inverse will refuse",
971
+ "subject": "scripts/codex-birth-doctor.sh",
972
+ "find": [" if [ \"$ORPHAN_DECLARATION\" = \"1\" ] || [ -e \"$HELPER_DIR\" ]; then"],
973
+ "replace": [" if false; then"],
974
+ "gate": ["bash", "run.sh", "smoke-codex-birth"],
975
+ "timeoutSeconds": 300,
976
+ "signature": "[QK:CODEX-BIRTH-DOCTOR-ORPHAN-RED]",
977
+ "signatureSource": "scripts/smoke-codex-birth.sh"
978
+ },
979
+ {
980
+ "claim": "CODEX-BIRTH-INVERSE-KEEPS-FOREIGN",
981
+ "title": "the text splice takes a fixed group index instead of the one the selector measured, so the inverse reaches for a neighbours declaration and the post-condition is the only thing standing between that and a written file",
982
+ "subject": "scripts/codex-birth-uninstall.sh",
983
+ "find": [" let spliced = m.certifySplice(m.removeSessionStartGroup(text, sel.groupIndex), want);"],
984
+ "replace": [" let spliced = m.certifySplice(m.removeSessionStartGroup(text, 0), want);"],
985
+ "gate": ["bash", "run.sh", "smoke-codex-birth"],
986
+ "timeoutSeconds": 300,
987
+ "signature": "[QK:CODEX-BIRTH-INVERSE-KEEPS-FOREIGN]",
988
+ "signatureSource": "scripts/smoke-codex-birth.sh"
989
+ },
990
+ {
991
+ "claim": "CODEX-BIRTH-INVERSE-SOLE-OWNER",
992
+ "title": "the FILE arm drops the target from its own removal, so a hooks.json this unit created and was the sole declaration in survives an uninstall that reports it gone",
993
+ "subject": "scripts/codex-birth-uninstall.sh",
994
+ "find": [
995
+ " FILE) rm -f -- \"$tmp\" \"$target\"; note \"removed $target (entwurf was its only declaration)\" ;;"
996
+ ],
997
+ "replace": [" FILE) rm -f -- \"$tmp\"; note \"removed $target (entwurf was its only declaration)\" ;;"],
998
+ "gate": ["bash", "run.sh", "smoke-codex-birth"],
999
+ "timeoutSeconds": 300,
1000
+ "signature": "[QK:CODEX-BIRTH-INVERSE-SOLE-OWNER]",
1001
+ "signatureSource": "scripts/smoke-codex-birth.sh"
836
1002
  }
837
1003
  ]
838
1004
  }