@junghanacs/entwurf 0.13.0 → 0.14.0

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 (193) hide show
  1. package/AGENTS.md +52 -17
  2. package/BASELINE.md +60 -206
  3. package/CHANGELOG.md +64 -0
  4. package/CONTRIBUTING.md +14 -10
  5. package/DELIVERY.md +118 -261
  6. package/README.md +91 -431
  7. package/VERIFY.md +74 -95
  8. package/demo/README.md +1 -1
  9. package/demo/demo-baseline.sh +1 -3
  10. package/demo/demo.sh +2 -5
  11. package/docs/acp-backend-rail.md +236 -0
  12. package/docs/external-mcp-host.md +131 -0
  13. package/docs/fresh-cut-policy.md +99 -0
  14. package/docs/setup-clean-host.md +124 -328
  15. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +160 -67
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +12 -0
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +11 -10
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +1 -1
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/engraving.js +42 -1
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +4 -4
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +11 -9
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +7 -5
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-core.js +13 -14
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-resume-args.js +45 -40
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +117 -95
  26. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +23 -57
  27. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +16 -7
  28. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +5 -53
  29. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-release.js +21 -36
  30. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +3 -15
  31. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send-fallback.js +12 -11
  32. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +2 -7
  33. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +30 -67
  34. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +256 -0
  35. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +91 -5
  36. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +258 -0
  37. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-launch.js +202 -0
  38. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-placement.js +289 -0
  39. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +170 -0
  40. package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +136 -0
  41. package/mcp/entwurf-bridge/dist/pi-extensions/lib/session-id.js +8 -5
  42. package/mcp/entwurf-bridge/dist/pi-extensions/lib/socket-discovery.js +3 -3
  43. package/mcp/entwurf-bridge/dist/scripts/meta-facts.js +51 -0
  44. package/mcp/entwurf-bridge/dist/scripts/new-session-id.js +9 -4
  45. package/mcp/entwurf-bridge/src/index.ts +176 -67
  46. package/mcp/entwurf-bridge/start.sh +2 -2
  47. package/mcp/entwurf-bridge/test.sh +23 -9
  48. package/mcp/entwurf-bridge/tsconfig.build.json +11 -2
  49. package/package.json +25 -11
  50. package/pi-extensions/entwurf-control.ts +220 -71
  51. package/pi-extensions/lib/acp/acp-client.ts +22 -0
  52. package/pi-extensions/lib/acp/augment.ts +42 -3
  53. package/pi-extensions/lib/acp/backend-adapter.ts +12 -11
  54. package/pi-extensions/lib/acp/backend.ts +366 -38
  55. package/pi-extensions/lib/acp/config.ts +1 -1
  56. package/pi-extensions/lib/acp/engraving.ts +43 -1
  57. package/pi-extensions/lib/acp/event-mapper.ts +16 -13
  58. package/pi-extensions/lib/acp/models.ts +4 -4
  59. package/pi-extensions/lib/acp/overlay.ts +11 -9
  60. package/pi-extensions/lib/entwurf-control-rpc.ts +7 -5
  61. package/pi-extensions/lib/entwurf-core.ts +15 -15
  62. package/pi-extensions/lib/entwurf-resume-args.ts +41 -52
  63. package/pi-extensions/lib/entwurf-v2-contract-schema.ts +1 -1
  64. package/pi-extensions/lib/entwurf-v2-contract.ts +120 -99
  65. package/pi-extensions/lib/entwurf-v2-decider.ts +30 -91
  66. package/pi-extensions/lib/entwurf-v2-lock.ts +16 -7
  67. package/pi-extensions/lib/entwurf-v2-production.ts +4 -78
  68. package/pi-extensions/lib/entwurf-v2-release.ts +25 -49
  69. package/pi-extensions/lib/entwurf-v2-runner.ts +6 -21
  70. package/pi-extensions/lib/entwurf-v2-send-fallback.ts +12 -11
  71. package/pi-extensions/lib/entwurf-v2-send.ts +2 -7
  72. package/pi-extensions/lib/entwurf-v2-surface.ts +36 -76
  73. package/pi-extensions/lib/entwurf-v2-visible-resume.ts +370 -0
  74. package/pi-extensions/lib/meta-session.ts +93 -5
  75. package/pi-extensions/lib/mux-fresh-call.ts +328 -0
  76. package/pi-extensions/lib/mux-launch.ts +267 -0
  77. package/pi-extensions/lib/mux-placement.ts +387 -0
  78. package/pi-extensions/lib/mux-resume-call.ts +221 -0
  79. package/pi-extensions/lib/resume-launch-identity.ts +162 -0
  80. package/pi-extensions/lib/session-id.js +8 -5
  81. package/pi-extensions/lib/socket-discovery.ts +3 -3
  82. package/prompts/engraving.md +13 -5
  83. package/run.sh +688 -282
  84. package/scripts/agy-bridge-config.py +5 -1
  85. package/scripts/check-acp-backend-preflight.ts +1 -1
  86. package/scripts/check-acp-carrier-augment.ts +246 -10
  87. package/scripts/check-acp-cortex.ts +5 -5
  88. package/scripts/check-acp-overlay.ts +13 -3
  89. package/scripts/check-acp-prompt-lifecycle.ts +565 -0
  90. package/scripts/check-acp-stop-reason.ts +342 -0
  91. package/scripts/check-acp-stream-hooks.ts +504 -0
  92. package/scripts/check-elapsed.sh +25 -0
  93. package/scripts/check-entwurf-bridge-boot.ts +51 -4
  94. package/scripts/check-entwurf-bridge-pi-free.ts +6 -5
  95. package/scripts/check-entwurf-control-rpc.ts +4 -3
  96. package/scripts/check-entwurf-resume-args.ts +72 -70
  97. package/scripts/check-entwurf-session-identity.ts +14 -10
  98. package/scripts/check-entwurf-v2-contract.ts +34 -59
  99. package/scripts/check-entwurf-v2-decider.ts +17 -177
  100. package/scripts/check-entwurf-v2-lock.ts +5 -2
  101. package/scripts/check-entwurf-v2-matrix.ts +3 -53
  102. package/scripts/check-entwurf-v2-production.ts +2 -91
  103. package/scripts/check-entwurf-v2-release.ts +10 -105
  104. package/scripts/check-entwurf-v2-runner.ts +4 -85
  105. package/scripts/check-entwurf-v2-send-fallback.ts +5 -6
  106. package/scripts/check-entwurf-v2-send.ts +0 -28
  107. package/scripts/check-entwurf-v2-surface.ts +198 -128
  108. package/scripts/check-entwurf-v2-visible-resume.ts +445 -0
  109. package/scripts/check-fresh-cut-gate.sh +1 -1
  110. package/scripts/check-gate-qualification.ts +101 -7
  111. package/scripts/check-install-container.sh +10 -2
  112. package/scripts/check-install-surface.ts +1 -1
  113. package/scripts/check-keyset-overlap.py +1 -1
  114. package/scripts/check-meta-facts.ts +249 -0
  115. package/scripts/check-meta-identity-consumers.ts +1 -1
  116. package/scripts/check-meta-session.ts +169 -0
  117. package/scripts/check-mux-launch-tmux.ts +316 -0
  118. package/scripts/check-mux-launch.ts +288 -0
  119. package/scripts/check-mux-launcher-fence.ts +264 -0
  120. package/scripts/check-mux-parent-artifact.ts +195 -0
  121. package/scripts/check-mux-placement-tmux.ts +322 -0
  122. package/scripts/check-mux-placement.ts +323 -0
  123. package/scripts/check-mux-resume-call.ts +282 -0
  124. package/scripts/check-probe-cli-shim.ts +25 -22
  125. package/scripts/check-probe-ordering.ts +121 -81
  126. package/scripts/check-release-gate-outcomes.ts +452 -0
  127. package/scripts/check-resume-launch-identity.ts +244 -0
  128. package/scripts/check-socket-discovery.ts +1 -1
  129. package/scripts/fixtures/mux-parent-transcript.scrubbed.jsonl +3 -0
  130. package/scripts/inventory-verification-surface.ts +349 -0
  131. package/scripts/lib/claude-launcher-fence.ts +322 -0
  132. package/scripts/lib/live-skip.ts +33 -0
  133. package/scripts/lib/mutation-qualify.ts +109 -3
  134. package/scripts/lib/probe-acp-turn.ts +33 -12
  135. package/scripts/lib/step-outcome.sh +88 -0
  136. package/scripts/meta-bridge-doctor.sh +6 -8
  137. package/scripts/meta-facts.ts +60 -0
  138. package/scripts/mutants/acp-augment.json +106 -0
  139. package/scripts/mutants/acp-cortex.json +2 -2
  140. package/scripts/mutants/acp-overlay.json +17 -0
  141. package/scripts/mutants/acp-prompt-lifecycle.json +100 -0
  142. package/scripts/mutants/acp-stop-reason.json +80 -0
  143. package/scripts/mutants/acp-stream-hooks.json +158 -0
  144. package/scripts/mutants/bridge-boot-resume.json +45 -0
  145. package/scripts/mutants/meta-facts.json +50 -0
  146. package/scripts/mutants/meta-identity.json +36 -0
  147. package/scripts/mutants/mux-boundary.json +196 -0
  148. package/scripts/mutants/mux-fresh-call.json +185 -0
  149. package/scripts/mutants/mux-launcher-fence.json +123 -0
  150. package/scripts/mutants/mux-parent-artifact.json +39 -0
  151. package/scripts/mutants/mux-resume-call.json +148 -0
  152. package/scripts/mutants/probe-ordering.json +6 -1021
  153. package/scripts/mutants/release-gate.json +140 -0
  154. package/scripts/mutants/resume-args.json +76 -0
  155. package/scripts/mutants/resume-launch-identity.json +96 -0
  156. package/scripts/mutants/v2-surface.json +74 -23
  157. package/scripts/mutants/v2-visible-resume.json +215 -0
  158. package/scripts/new-session-id.ts +9 -4
  159. package/scripts/smoke-acp-bundled-mcp-live.ts +4 -3
  160. package/scripts/smoke-acp-carrier-augment-live.ts +2 -2
  161. package/scripts/smoke-acp-cortex-live.ts +8 -14
  162. package/scripts/smoke-acp-long-turn-live.ts +185 -0
  163. package/scripts/smoke-acp-mcp-live.ts +2 -2
  164. package/scripts/smoke-acp-memory-containment-live.ts +2 -2
  165. package/scripts/smoke-acp-ordering-probe-live.ts +2 -2
  166. package/scripts/smoke-acp-overlay-live.ts +2 -2
  167. package/scripts/smoke-acp-provider-live.ts +2 -2
  168. package/scripts/smoke-acp-raw-turn-live.ts +3 -3
  169. package/scripts/smoke-acp-session-reuse-live.ts +2 -2
  170. package/scripts/smoke-acp-skill-live.ts +2 -2
  171. package/scripts/smoke-acp-socket-citizen-live.ts +4 -3
  172. package/scripts/smoke-acp-v2-send-live.ts +4 -3
  173. package/scripts/smoke-agy-native-push-live.ts +10 -20
  174. package/scripts/smoke-claude-native-resume-live.sh +13 -3
  175. package/scripts/smoke-entwurf-chain-live.ts +352 -0
  176. package/scripts/smoke-entwurf-v2-matrix-live.ts +3 -3
  177. package/scripts/smoke-meta-honesty.sh +1 -1
  178. package/scripts/smoke-meta-install-state.sh +4 -0
  179. package/scripts/smoke-mux-fresh-call-live.ts +365 -0
  180. package/scripts/smoke-mux-lifecycle-live.ts +1136 -0
  181. package/scripts/smoke-pi-attach.ts +1 -1
  182. package/scripts/smoke-user-scope-citizen.sh +1 -1
  183. package/scripts/tsconfig.json +1 -0
  184. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-preflight.js +0 -160
  185. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn-production.js +0 -273
  186. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn.js +0 -216
  187. package/pi-extensions/lib/entwurf-v2-spawn-production.ts +0 -373
  188. package/pi-extensions/lib/entwurf-v2-spawn.ts +0 -323
  189. package/scripts/check-acp-sdk-surface.ts +0 -236
  190. package/scripts/check-entwurf-v2-spawn-production.ts +0 -551
  191. package/scripts/check-entwurf-v2-spawn.ts +0 -399
  192. package/scripts/smoke-entwurf-v2-spawn-live.ts +0 -188
  193. package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +0 -469
@@ -1,20 +1,23 @@
1
1
  /**
2
- * check-entwurf-resume-args — deterministic gate for the 5c-3b resume-argv SSOT
3
- * (`buildResumePiArgs`). It pins the RESIDENT citizen launch shape so it cannot drift.
4
- * This gate once carried a second `legacy` one-shot variant as a contrast; that launcher
5
- * and its enum member were removed 2026-07-27, and the half of A1 that still ships is
6
- * asserted directly instead — a resumed citizen stands its control socket up and never
7
- * emits the one-shot `--no-extensions`:
2
+ * check-entwurf-resume-args — deterministic gate for the resume-argv SSOT (`buildResumePiArgs`).
8
3
  *
9
- * 2. v2-control carries `--entwurf-control` and NO `--no-extensions`.
10
- * 3. The headless prefix `--mode json -p` stays and the prompt is the final positional
11
- * (the prompt-as-turn authority is unchanged in v2 `-p` is NOT dropped).
12
- * 4. `explicitExtensionArgs` is preserved verbatim, exactly once
13
- * (load-bearing for a recorded `provider=entwurf` resume; #29 footgun).
14
- * 5. `plan.launchArgs` (`--approve` / empty) ride as flags BEFORE the prompt.
15
- * 6. a null/undefined provider emits NO `--provider` flag; `--model <m>` and `<prompt>`
16
- * are the last three tokens.
17
- * 7. the resident posture is exact: `--entwurf-control` present, `--no-extensions` never.
4
+ * The contract this gate holds changed with S1, and the change is the point: the builder used to
5
+ * emit a HEADLESS child (`--mode json -p <prompt>`) because its only caller was the detached
6
+ * `spawn-bg` watcher. That transport was withdrawn, and the argv was MEASURED against the runtime
7
+ * before the new consumer was written (2026-08-06, private tmux + fixture): dropped into a tmux
8
+ * window, `-p` is pi's own "non-interactive mode" and the operator gets a JSON stream instead of a
9
+ * session they can type into. The shape that actually reopens a visible citizen carries neither.
10
+ *
11
+ * So this gate pins the ONE shipped posture, and pins the absences as hard as the presences —
12
+ * a re-introduced `-p` would still open a window, still stand a socket up, and still look green
13
+ * to anything that only asserted `--entwurf-control`:
14
+ *
15
+ * RESUMEARGS-CONTROL-FIRST `--entwurf-control` leads, so the resumed session is addressable
16
+ * RESUMEARGS-NO-HEADLESS no `--mode`, no `-p` — the window is interactive
17
+ * RESUMEARGS-NO-PROMPT-TAIL no positional prompt — a resume runs no turn
18
+ * RESUMEARGS-EXT-ONCE-BEFORE-SESSION bridge args exactly once, between control and --session
19
+ * RESUMEARGS-PROVIDER-OPTIONAL a recorded provider is emitted, an absent one emits no flag
20
+ * RESUMEARGS-SESSION-IS-FILE `--session <abs path>`, never `--session-id` (which MINTS)
18
21
  *
19
22
  * Pure string assembly — no IO, no spawn.
20
23
  */
@@ -29,89 +32,88 @@ function ok(label: string, cond: boolean): void {
29
32
  passed++;
30
33
  }
31
34
 
32
- // The resume target is a session FILE now (#50 C2), not a garden id: the record owns
33
- // the address, so argv only has to name WHICH transcript to reopen. An absolute path
34
- // is the production shape (record.transcriptPath).
35
+ // The resume target is a session FILE (#50 C2), not a garden id: the record owns the address, so
36
+ // argv only has to name WHICH transcript to reopen.
35
37
  const SESSION_FILE =
36
38
  "/home/op/.pi/agent/sessions/-home-op-repo/2026-06-13T09-10-00-000Z_019e8faa-04ea-7b73-bf2c-1465d525c2e8.jsonl";
37
- // Real production shape: getEntwurfExplicitExtensions emits `-e <path>` (entwurf-core.ts),
38
- // NOT `--extension`. The builder spreads verbatim, but the fixture mirrors production so the
39
- // "preserved exactly once" assertion exercises the token the launcher actually passes.
39
+ // Production shape, measured: getEntwurfExplicitExtensions emits `-e <path>`, not `--extension`.
40
40
  const EXT = ["-e", "/path/to/entwurf/index.ts"] as const;
41
41
 
42
- // index of token `flag`'s VALUE (the token right after it), or -1 if the flag is absent.
43
42
  function valueAfter(args: readonly string[], flag: string): string | undefined {
44
43
  const i = args.indexOf(flag);
45
44
  return i === -1 ? undefined : args[i + 1];
46
45
  }
47
46
 
48
47
  function main(): void {
49
- // ── v2-control variant ──────────────────────────────────────────────────────────
48
+ // ── The shipped posture, with a recorded ACP provider (the only shape whose ext args are
49
+ // non-empty — measured as `["-e", "<bridge>"]` on a real recorded provider=entwurf record).
50
50
  {
51
51
  const args = buildResumePiArgs({
52
- variant: "v2-control",
53
52
  sessionFile: SESSION_FILE,
54
53
  explicitExtensionArgs: EXT,
55
54
  provider: "entwurf",
56
- model: "claude-opus-5",
57
- prompt: "resume now",
58
- launchArgs: ["--approve"],
55
+ model: "claude-sonnet-5",
59
56
  });
60
- ok("2 v2 has --entwurf-control", args.includes("--entwurf-control"));
61
- ok("2 v2 has NO --no-extensions", !args.includes("--no-extensions"));
62
- ok("3 v2 keeps -p (prompt-as-turn NOT dropped)", args.includes("-p"));
63
- ok("3 v2 headless prefix --mode json -p", args[0] === "--mode" && args[1] === "json" && args[2] === "-p");
64
- ok("3 v2 prompt is the final positional", args[args.length - 1] === "resume now");
65
- ok("4 v2 keeps ext args exactly once", args.filter((a) => a === "-e").length === 1);
66
- ok("5 v2 includes launchArgs --approve", args.includes("--approve"));
67
- ok("5 v2 --approve is before the prompt", args.indexOf("--approve") < args.length - 1);
68
- ok("6 v2 provider laid out", valueAfter(args, "--provider") === "entwurf");
69
- ok("6 v2 model laid out", valueAfter(args, "--model") === "claude-opus-5");
70
- ok("6 v2 resumes by exact FILE (--session <abs path>)", valueAfter(args, "--session") === SESSION_FILE);
57
+
58
+ // Assertion order is a contract, not a reading preference: each claim must be the FIRST
59
+ // one a defect against it reaches, or a red would be reported under someone else's name.
60
+ // The absences come first because a re-introduced headless prefix also displaces the
61
+ // leading flag, and "the argv is headless again" is the truer diagnosis of that.
62
+ ok(
63
+ "[QK:RESUMEARGS-NO-HEADLESS] no --mode and no -p — `-p` is pi's non-interactive mode, so the headless prefix would open a window streaming JSON instead of a session the operator can use",
64
+ !args.includes("--mode") && !args.includes("-p") && !args.includes("--print"),
65
+ );
66
+ ok(
67
+ "[QK:RESUMEARGS-CONTROL-FIRST] --entwurf-control is the FIRST token — the resumed session stands its control socket up, which is the whole difference between a restored transcript and an addressable citizen",
68
+ args[0] === "--entwurf-control",
69
+ );
71
70
  ok(
72
- "6 v2 carries NO --session-id (a garden id would MINT a session, not resume one)",
73
- !args.includes("--session-id"),
71
+ "[QK:RESUMEARGS-NO-PROMPT-TAIL] the argv ends at --model <m> with NO positional prompt a resume reopens a conversation and runs no turn (measured: the transcript stayed byte-identical)",
72
+ args[args.length - 2] === "--model" && args[args.length - 1] === "claude-sonnet-5",
74
73
  );
75
74
  ok(
76
- "6 v2 --model <m> <prompt> tail",
77
- args[args.length - 3] === "--model" &&
78
- args[args.length - 2] === "claude-opus-5" &&
79
- args[args.length - 1] === "resume now",
75
+ "no --no-extensions either: the one-shot posture that let `pi -p` exit is exactly what a resident resume must not emit",
76
+ !args.includes("--no-extensions"),
77
+ );
78
+
79
+ // SESSION-IS-FILE precedes the ext-args claim because the ext claim is stated RELATIVE to
80
+ // `--session`: if that flag were renamed, the positional assertion below would go red about
81
+ // the extensions while the actual defect is the flag that MINTS a session.
82
+ ok(
83
+ "[QK:RESUMEARGS-SESSION-IS-FILE] the builder targets an exact FILE via --session and never --session-id, which would CREATE the id when missing and silently mint an empty session instead of resuming one",
84
+ valueAfter(args, "--session") === SESSION_FILE && !args.includes("--session-id"),
85
+ );
86
+ ok(
87
+ "[QK:RESUMEARGS-EXT-ONCE-BEFORE-SESSION] the bridge args appear EXACTLY once and sit between --entwurf-control and --session — dropping them re-opens the #29 'Unknown provider' footgun, and doubling them is a different broken argv",
88
+ args.filter((a) => a === "-e").length === 1 &&
89
+ args.indexOf("--entwurf-control") < args.indexOf("-e") &&
90
+ args.indexOf("-e") < args.indexOf("--session"),
91
+ );
92
+ ok("a recorded provider is laid out as two tokens", valueAfter(args, "--provider") === "entwurf");
93
+ ok("model is laid out as two tokens", valueAfter(args, "--model") === "claude-sonnet-5");
94
+ ok(
95
+ "the full measured argv is reproduced exactly",
96
+ args.join(" ") ===
97
+ `--entwurf-control -e /path/to/entwurf/index.ts --session ${SESSION_FILE} --provider entwurf --model claude-sonnet-5`,
80
98
  );
81
99
  }
82
100
 
83
- // ── 6. null/undefined provider emits NO --provider flag ───────────────────────────
101
+ // ── The native shape: no recorded provider, no bridge. Measured as explicitExtensionArgs=[].
84
102
  for (const provider of [null, undefined] as const) {
85
103
  const args = buildResumePiArgs({
86
- variant: "v2-control",
87
104
  sessionFile: SESSION_FILE,
88
105
  explicitExtensionArgs: [],
89
106
  provider,
90
- model: "m",
91
- prompt: "p",
92
- launchArgs: [],
93
- });
94
- ok(`6 provider=${provider}: no --provider flag`, !args.includes("--provider"));
95
- ok(`6 provider=${provider}: --model <m> <prompt> still tail`, args.slice(-3).join(" ") === "--model m p");
96
- }
97
-
98
- // ── 7. the resident posture is exact: the one-shot flag never reappears ───────────
99
- // The `legacy` variant was removed 2026-07-27, so the old cross-contamination pair is
100
- // gone. What must NOT rot is the A1 half that still ships: a resumed citizen stands its
101
- // control socket up, which means `--no-extensions` (the one-shot flag that let `pi -p`
102
- // exit) can never be emitted here again.
103
- {
104
- const v2 = buildResumePiArgs({
105
- sessionFile: SESSION_FILE,
106
- explicitExtensionArgs: EXT,
107
- provider: "entwurf",
108
- model: "m",
109
- prompt: "p",
110
- launchArgs: ["--approve"],
111
- variant: "v2-control",
107
+ model: "gpt-5.6-terra",
112
108
  });
113
- ok("7 resident posture never emits the one-shot --no-extensions", !v2.includes("--no-extensions"));
114
- ok("7 resident posture stands the control socket up", v2.includes("--entwurf-control"));
109
+ ok(
110
+ `[QK:RESUMEARGS-PROVIDER-OPTIONAL] provider=${provider}: NO --provider flag is emitted — an empty flag value would be a different citizen's launch, not a missing one`,
111
+ !args.includes("--provider"),
112
+ );
113
+ ok(
114
+ `provider=${provider}: the argv is exactly the four tokens the native shape needs`,
115
+ args.join(" ") === `--entwurf-control --session ${SESSION_FILE} --model gpt-5.6-terra`,
116
+ );
115
117
  }
116
118
 
117
119
  console.log(`\ncheck-entwurf-resume-args: ${passed} checks passed`);
@@ -3,8 +3,8 @@
3
3
  * identity contract. No backend, no API, no spawn. Pure-string + synthetic-fs.
4
4
  *
5
5
  * - T-grammar the garden-id grammar (session-id.js SSOT): validator +
6
- * generator uniqueness + timestamp format. This is the RECORD's
7
- * gardenId shape now, not pi's session id.
6
+ * generator shape + timestamp format, all on fixed inputs. This
7
+ * is the RECORD's gardenId shape now, not pi's session id.
8
8
  * - T-identity readSessionIdentity: first model_change is the model authority;
9
9
  * drift fail-fast; the session NAME is pi's and is never read
10
10
  * (#50 C3 — the old name-mirror/`requireEntwurf` refusals are
@@ -15,7 +15,8 @@
15
15
  * build-parse round-trip / T-no-logic / T-model-immut — the name is pi's and no
16
16
  * code assembles or parses one), the header-scan lookup (T-collision), the
17
17
  * transcript analyzer, and the remote scope-lock. The meta-record resolves a
18
- * garden id to its transcript now (see check-entwurf-v2-spawn-production §9).
18
+ * garden id to its transcript now (see check-resume-launch-identity, which inherited
19
+ * those cells from the deleted check-entwurf-v2-spawn-production §9).
19
20
  *
20
21
  * The sessions base is isolated to a temp dir BEFORE the module computes its
21
22
  * load-time paths, so the real ~/.pi/agent is never touched.
@@ -73,13 +74,16 @@ try {
73
74
  ok(!isValidSessionId("20260603T191245-a3f09c "), "trailing space rejected");
74
75
  ok(!isValidSessionId(123 as unknown), "non-string rejected");
75
76
 
76
- // generateSessionId: format + uniqueness (same-second parallel spawn safety)
77
- const ids = Array.from({ length: 256 }, () => generateSessionId());
78
- ok(
79
- ids.every((id) => isValidSessionId(id)),
80
- "all generated ids valid",
81
- );
82
- eq(new Set(ids).size, 256, "256 generated ids unique (6-hex suffix)");
77
+ // generateSessionId on a FIXED Date: deterministic shape proof the id is the
78
+ // formatted timestamp prefix + `-` + 6 lowercase hex, and it satisfies the
79
+ // validator. The suffix is 24 random bits, so batch uniqueness is PROBABILISTIC
80
+ // (256 ids collide ~0.19% of runs) and is deliberately NOT asserted here — a
81
+ // deterministic gate must not carry a birthday-bound flake. This gate proves
82
+ // generator grammar/validator wiring ONLY; the fail-closed production repair
83
+ // for a minted-gid collision at the record store is tracked in #66, not here.
84
+ const genId = generateSessionId(new Date(2026, 0, 9, 5, 6, 7));
85
+ ok(/^20260109T050607-[0-9a-f]{6}$/.test(genId), "generated id = fixed timestamp prefix + 6-hex suffix");
86
+ ok(isValidSessionId(genId), "generated id passes the validator");
83
87
  eq(formatSessionTimestamp(new Date(2026, 5, 3, 19, 12, 45)), "20260603T191245", "timestamp value (local)");
84
88
 
85
89
  // ---- T-identity: resume identity authority = FIRST model_change ----
@@ -11,19 +11,19 @@
11
11
  * - R1 domain guard: the control-socket domain currently contains pi; claude-code / codex /
12
12
  * antigravity are `unsupported` — NEVER folded into dead/indeterminate.
13
13
  * - the 6-cell table is exhaustive and every cell is a SINGLE verdict (Q2):
14
- * exactly two allow cells (ff+live=send, owned+dormant=resume), four reject.
14
+ * ONE allow cell (ff+live=send) plus two rejects — the visible-first cut removed the
15
+ * `owned-outcome` row, and with it the only resume/spawn verdict the table had.
15
16
  * - N1/F3: an indeterminate target never spawns (both intents reject it).
16
- * - Q2/F1: owned-outcome + live = reject (owned-live-no-autosend), not auto-send.
17
17
  * - F-mailbox: an `unsupported` citizen routes through a SEPARATE intent-keyed
18
18
  * mailbox mini-table (NOT the 6-cell table): ff+deliverable → meta-mailbox/
19
19
  * ack-only, ff+undeliverable → mailbox-undeliverable (fail-closed),
20
- * owned → backend-liveness-unsupported. Deliverability is a 2nd fact ignored
20
+ * Deliverability is a 2nd fact ignored
21
21
  * in-domain (pi). The N2 asymmetry (dormant-pi reject vs unsupported mailbox)
22
22
  * is pinned side-by-side.
23
23
  * - R3: resolveDispatch round-trip — receipt.observedLiveness === input AND
24
24
  * action/transport/ownership/reason === the table cell, every (intent × liveness).
25
25
  * - R5: taxonomy covers the table reasons + pre-claims bad-target /
26
- * untrusted-fail-fast / target-locked (so bucket B F2 won't reopen it), and
26
+ * target-locked (so bucket B F2 won't reopen it), and
27
27
  * is PRE-DISPATCH only (no send-fail-fallback member).
28
28
  * - schema ↔ types drift guard: the TypeBox input/receipt property keysets and
29
29
  * the StringEnum members match the hand-written contract constants.
@@ -137,9 +137,9 @@ for (const intent of ENTWURF_INTENTS) {
137
137
  }
138
138
  }
139
139
  }
140
- eq("table has 6 cells (2 intents × 3 liveness)", allowCells + rejectCells, 6);
141
- eq("v2-initial: exactly 2 allow cells", allowCells, 2);
142
- eq("v2-initial: exactly 4 reject cells", rejectCells, 4);
140
+ eq("table has 3 cells (1 intent × 3 liveness)", allowCells + rejectCells, 3);
141
+ eq("post-cut: exactly 1 allow cell (fire-and-forget × live)", allowCells, 1);
142
+ eq("post-cut: exactly 2 reject cells (dormant, indeterminate)", rejectCells, 2);
143
143
 
144
144
  // the two allow cells are exactly the intended ones
145
145
  eq("allow: fire-and-forget+live = send/control-socket/ack-only", DISPATCH_TABLE["fire-and-forget"].live, {
@@ -147,13 +147,8 @@ eq("allow: fire-and-forget+live = send/control-socket/ack-only", DISPATCH_TABLE[
147
147
  transport: "control-socket",
148
148
  ownership: "ack-only",
149
149
  });
150
- eq("allow: owned-outcome+dormant = resume/spawn-bg/owned", DISPATCH_TABLE["owned-outcome"].dormant, {
151
- action: "resume",
152
- transport: "spawn-bg",
153
- ownership: "owned",
154
- });
155
150
 
156
- // ── N1/F3: indeterminate never spawns (both intents reject) ────────────────
151
+ // ── N1/F3: an indeterminate target is never dispatched into ────────────────
157
152
  for (const intent of ENTWURF_INTENTS) {
158
153
  const cell = DISPATCH_TABLE[intent].indeterminate;
159
154
  eq(`N1: ${intent}+indeterminate = reject(indeterminate-no-spawn)`, cell, {
@@ -168,12 +163,6 @@ eq("N2: fire-and-forget+dormant = reject(dormant-fire-forget-unsupported)", DISP
168
163
  reason: "dormant-fire-forget-unsupported",
169
164
  });
170
165
 
171
- // ── Q2/F1: owned-outcome+live = reject, never auto-send ────────────────────
172
- eq("Q2: owned-outcome+live = reject(owned-live-no-autosend)", DISPATCH_TABLE["owned-outcome"].live, {
173
- action: "reject",
174
- reason: "owned-live-no-autosend",
175
- });
176
-
177
166
  // ── R3: resolveDispatch round-trip — table cell ↔ receipt, every cell ──────
178
167
  // In-domain (pi) liveness is authoritative; the F-mailbox deliverability fact is
179
168
  // IGNORED here, so the verdict is identical for deliverable true/false.
@@ -207,7 +196,7 @@ for (const intent of ENTWURF_INTENTS) {
207
196
  // ── F-mailbox: unsupported mailbox mini-table — SEPARATE from the 6-cell table ──
208
197
  // The unsupported domain-guard routes through UNSUPPORTED_DISPATCH_TABLE (intent-
209
198
  // keyed), NOT the liveness table. Two cells, each a single verdict (Q2).
210
- eq("mini-table: 2 cells (one per intent)", Object.keys(UNSUPPORTED_DISPATCH_TABLE).length, 2);
199
+ eq("mini-table: 1 cell (one per intent)", Object.keys(UNSUPPORTED_DISPATCH_TABLE).length, 1);
211
200
  eq(
212
201
  "mini-table[fire-and-forget] = send/meta-mailbox/ack-only (deliverable path)",
213
202
  UNSUPPORTED_DISPATCH_TABLE["fire-and-forget"],
@@ -217,10 +206,6 @@ eq(
217
206
  ownership: "ack-only",
218
207
  },
219
208
  );
220
- eq("mini-table[owned-outcome] = reject(backend-liveness-unsupported)", UNSUPPORTED_DISPATCH_TABLE["owned-outcome"], {
221
- action: "reject",
222
- reason: "backend-liveness-unsupported",
223
- });
224
209
 
225
210
  // ── F-mailbox: resolveDispatch over the deliverability axis ────────────────
226
211
  // fire-and-forget + deliverable → meta-mailbox send (NOT reject); ack-only +
@@ -248,19 +233,6 @@ eq(
248
233
  observedLiveness: "unsupported",
249
234
  },
250
235
  );
251
- // owned-outcome+unsupported still rejects regardless of deliverability — a
252
- // self-fetch backend has no real liveness for the caller to own (point 3).
253
- for (const deliverable of [true, false]) {
254
- eq(
255
- `F-mailbox: owned+unsupported (deliverable=${deliverable}) → reject(backend-liveness-unsupported)`,
256
- resolveDispatch("owned-outcome", "unsupported" as FactLiveness, deliverable),
257
- {
258
- ok: false,
259
- reason: "backend-liveness-unsupported",
260
- observedLiveness: "unsupported",
261
- },
262
- );
263
- }
264
236
 
265
237
  // ── N2 asymmetry — the two tables are NOT contradictory ────────────────────
266
238
  // fire-and-forget+dormant-PI = reject (confirmed not-running → enqueue is silent
@@ -273,6 +245,13 @@ ok(
273
245
  resolveDispatch("fire-and-forget", "unsupported" as FactLiveness, true).ok === true,
274
246
  );
275
247
 
248
+ // ── Delivery transports: exact set; process launch is not a receipt rail ───
249
+ eq(
250
+ "[QK:V2-DELIVERY-EXCLUDES-MUX] transport enum is the exact four delivery outcomes; mux launch handles stay out",
251
+ [...ENTWURF_V2_TRANSPORTS],
252
+ ["control-socket", "meta-mailbox", "native-push"],
253
+ );
254
+
276
255
  // ── F-mailbox: transport + taxonomy members present ────────────────────────
277
256
  ok(
278
257
  "transport: meta-mailbox present (F-mailbox liveness-free delivery)",
@@ -350,9 +329,9 @@ for (const intent of ENTWURF_INTENTS) {
350
329
  }
351
330
  }
352
331
  }
353
- eq("native-push table has 6 cells (2 intents × 3 liveness)", allow + reject, 6);
332
+ eq("native-push table has 3 cells (1 intent × 3 liveness)", allow + reject, 3);
354
333
  eq("native-push: exactly 1 allow cell (fire-and-forget × alive)", allow, 1);
355
- eq("native-push: exactly 5 reject cells", reject, 5);
334
+ eq("native-push: exactly 2 reject cells (dead, indeterminate)", reject, 2);
356
335
  }
357
336
  // the one allow cell + the intended reject reasons.
358
337
  eq("native-push allow: ff+alive = send/native-push/ack-only", NATIVE_PUSH_DISPATCH_TABLE["fire-and-forget"].alive, {
@@ -369,21 +348,13 @@ eq(
369
348
  NATIVE_PUSH_DISPATCH_TABLE["fire-and-forget"].indeterminate,
370
349
  { action: "reject", reason: "native-push-probe-indeterminate" },
371
350
  );
372
- // owned-outcome × * = single, state-independent no-resume-authority reject.
373
- for (const lv of NATIVE_PUSH_LIVENESSES) {
374
- eq(
375
- `native-push: owned+${lv} = reject(native-push-no-resume-authority)`,
376
- NATIVE_PUSH_DISPATCH_TABLE["owned-outcome"][lv],
377
- {
378
- action: "reject",
379
- reason: "native-push-no-resume-authority",
380
- },
381
- );
382
- }
383
- // `backend-liveness-unsupported` is NOT reused for a native-push backend (봉인 1).
351
+ // The removed `owned-outcome` row rejected native-push-no-resume-authority on every
352
+ // liveness. Its reason left the taxonomy with the intent, so assert it is gone rather
353
+ // than leaving a cell that proves retired behavior.
384
354
  ok(
385
- "native-push: reject reasons never reuse backend-liveness-unsupported (agy IS measured)",
386
- !(NATIVE_PUSH_REJECT_REASONS as readonly string[]).includes("backend-liveness-unsupported"),
355
+ "native-push: the no-resume-authority reason left the taxonomy with the owned-outcome intent",
356
+ !(NATIVE_PUSH_REJECT_REASONS as readonly string[]).includes("native-push-no-resume-authority") &&
357
+ !(ENTWURF_V2_REJECT_REASONS as readonly string[]).includes("native-push-no-resume-authority"),
387
358
  );
388
359
  // resolveNativePushDispatch round-trip: observedLiveness === probed value, verdict === cell.
389
360
  for (const intent of ENTWURF_INTENTS) {
@@ -424,7 +395,7 @@ for (const r of NATIVE_PUSH_REJECT_REASONS) {
424
395
  for (const r of RESOLVER_REJECT_REASONS) {
425
396
  ok(`taxonomy: resolver reason '${r}' is in the enum`, (ENTWURF_V2_REJECT_REASONS as readonly string[]).includes(r));
426
397
  }
427
- for (const pre of ["bad-target", "untrusted-fail-fast", "target-locked"]) {
398
+ for (const pre of ["bad-target", "target-locked"]) {
428
399
  ok(
429
400
  `taxonomy: pre-claim '${pre}' present (R5 — bucket B won't reopen)`,
430
401
  (ENTWURF_V2_REJECT_REASONS as readonly string[]).includes(pre),
@@ -472,9 +443,13 @@ for (const r of ENTWURF_V2_REJECT_REASONS) {
472
443
  for (const r of RESOLVER_REJECT_REASONS) {
473
444
  ok(`?6: resolver reason '${r}' is post-probe (non-null observedLiveness required)`, !isPreProbeReject(r));
474
445
  }
475
- // untrusted-fail-fast is post-probe too (1B: it now runs AFTER lock+probe, only on
476
- // a resume verdict, so observedLiveness is the honest measured dormant).
477
- ok("?6: untrusted-fail-fast is post-probe (1B runs after lock+probe)", !isPreProbeReject("untrusted-fail-fast"));
446
+ // untrusted-fail-fast is GONE from the taxonomy: preflight ran only behind the resume
447
+ // verdict, so the reject it produced left with that verdict. Asserting its absence keeps
448
+ // the enum honesta reason nothing can emit is not a reason.
449
+ ok(
450
+ "taxonomy: untrusted-fail-fast is gone (its only producer was the removed resume verdict)",
451
+ !(ENTWURF_V2_REJECT_REASONS as readonly string[]).includes("untrusted-fail-fast"),
452
+ );
478
453
 
479
454
  // #50 C4: record-less-socket — a gid-shaped non-symlink control socket with NO
480
455
  // meta-record. The record is the sole address authority, so this is refused for
@@ -506,7 +481,7 @@ for (const r of PRE_PROBE_REJECT_REASONS) {
506
481
  );
507
482
  }
508
483
  }
509
- for (const r of [...RESOLVER_REJECT_REASONS, ...NATIVE_PUSH_REJECT_REASONS, "untrusted-fail-fast" as const]) {
484
+ for (const r of [...RESOLVER_REJECT_REASONS, ...NATIVE_PUSH_REJECT_REASONS]) {
510
485
  ok(
511
486
  `?6: well-formed('${r}', null) = false (post-probe must carry a value)`,
512
487
  !rejectObservedLivenessWellFormed(r, null),
@@ -567,7 +542,7 @@ for (const r of PRE_PROBE_REJECT_REASONS) {
567
542
  passed++;
568
543
  }
569
544
  }
570
- for (const r of [...RESOLVER_REJECT_REASONS, ...NATIVE_PUSH_REJECT_REASONS, "untrusted-fail-fast" as const]) {
545
+ for (const r of [...RESOLVER_REJECT_REASONS, ...NATIVE_PUSH_REJECT_REASONS]) {
571
546
  assert.throws(
572
547
  () => makeRejectReceipt(r, null),
573
548
  /ill-formed reject/,