@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
@@ -25,14 +25,14 @@
25
25
  * - Register the canonical `entwurf_v2` dispatch tool for existing garden citizens.
26
26
  * - Expose `entwurf_peers` facts for operator inspection (#50 C4: the socket-scan
27
27
  * `/entwurf-sessions` command is gone — the record listing is the only surface).
28
- * - Maintain the resident control socket used by v2 live-send / spawn-bg paths.
28
+ * - Maintain the resident control socket used by the v2 live-send path.
29
29
  * - Attach this pi session to its meta-record at session_start (#50 C2) and key
30
30
  * the control socket on the record's gardenId.
31
31
  *
32
32
  * Send-is-throw still applies at the control-socket protocol layer: a `send` RPC
33
33
  * ack confirms the receiver enqueued the message (`message_processed` semantics)
34
34
  * and does not wait for a peer turn result. Public v1 send surfaces were removed;
35
- * callers use `entwurf_v2`, whose decider chooses send / spawn-bg / mailbox.
35
+ * callers use `entwurf_v2`, whose decider chooses control-socket send / mailbox / native-push.
36
36
  *
37
37
  * Usage:
38
38
  * pi --entwurf-control (no id injection: pi owns its id, the record owns
@@ -758,8 +758,9 @@ async function handleCommand(
758
758
  // (entwurf-bridge entwurf_v2, the pi-native entwurf_v2 senderProvider via buildLocalSenderEnvelope)
759
759
  // pass the envelope structurally and never touch the message body — the
760
760
  // canonical XML-style payload is the shared formatSenderInfoBlock SSOT
761
- // (#50 C3: the dormant spawn-resume rail appends the same block to its
762
- // prompt, so both rails render one shape).
761
+ // (#50 C3: this is the ONE renderer since the visible-first cut removed the
762
+ // dormant spawn-resume rail that used to append the same block to a resume
763
+ // prompt — a future VISIBLE resume must render through it, not beside it).
763
764
  const senderInfoBlock = sender ? formatSenderInfoBlock(sender, wantsReply) : "";
764
765
 
765
766
  const mode = command.mode ?? "steer";
@@ -938,7 +939,8 @@ function updateStatus(ctx: ExtensionContext | null, enabled: boolean, gardenId:
938
939
  // is GONE with the id it mirrored. A name was a second place the address lived; the
939
940
  // record is the only one now, so there is nothing left to keep in sync and nothing
940
941
  // to crash over. (The dormant-resume authorization that leaned on the `entwurf` tag
941
- // moved to record existence see entwurf-v2-spawn-production.)
942
+ // moved to record existence, and that resume rail has since been withdrawn entirely;
943
+ // the record-authoritative remainder lives in resume-launch-identity.ts.)
942
944
 
943
945
  function updateSessionEnv(ctx: ExtensionContext | null, enabled: boolean, gardenId: string | null): void {
944
946
  if (!enabled || !gardenId) {
@@ -985,7 +987,7 @@ function getStringFlagFromArgv(flagName: string): string | undefined {
985
987
  }
986
988
 
987
989
  // `--emacs-agent-socket <name>` exports PI_EMACS_AGENT_SOCKET so this session's
988
- // own Bash/emacsclient calls (and any spawn-bg child that inherits this env)
990
+ // own Bash/emacsclient calls (and any child process that inherits this env)
989
991
  // target the right Emacs server socket — e.g. `emacsclient -s "$PI_EMACS_AGENT_SOCKET"`.
990
992
  // v2-only revival: the original ACP path injected this into the ACP child's spawn
991
993
  // env (acp-bridge.ts); with no ACP child on this branch, the consumer IS this pi
@@ -1042,6 +1044,8 @@ export default function (pi: ExtensionAPI) {
1042
1044
  if (shouldRegisterControlTools(pi)) {
1043
1045
  registerListSessionsTool(pi);
1044
1046
  registerEntwurfV2Tool(pi);
1047
+ registerFreshCallTool(pi);
1048
+ registerResumeCallTool(pi);
1045
1049
  }
1046
1050
 
1047
1051
  // The in-process mint refusals (`/new`, `/fork`, `/clone`, RPC new_session) are
@@ -1258,23 +1262,19 @@ interface EntwurfV2SurfaceModule {
1258
1262
  runAndRenderEntwurfV2FromSurface(
1259
1263
  params: {
1260
1264
  target: string;
1261
- intent: "fire-and-forget" | "owned-outcome";
1265
+ intent: "fire-and-forget";
1262
1266
  mode?: "steer" | "follow_up";
1263
1267
  wants_reply?: boolean;
1264
1268
  message: string;
1265
1269
  },
1266
- opts: {
1267
- senderProvider: () => SenderEnvelope | undefined;
1268
- agentDir?: string;
1269
- prefixRoots?: readonly string[];
1270
- },
1270
+ opts: { senderProvider: () => SenderEnvelope | undefined },
1271
1271
  ): Promise<{ text: string; isError: boolean }>;
1272
1272
  }
1273
1273
 
1274
1274
  function registerEntwurfV2Tool(pi: ExtensionAPI): void {
1275
1275
  const entwurfV2Parameters = Type.Object({
1276
1276
  target: Type.String({ description: "Target garden id (use entwurf_peers to discover)" }),
1277
- intent: StringEnum(["fire-and-forget", "owned-outcome"] as const, {
1277
+ intent: StringEnum(["fire-and-forget"] as const, {
1278
1278
  description:
1279
1279
  "fire-and-forget = send/reply/hand-off to a LIVE socket target (currently backend pi) or to any " +
1280
1280
  "citizen with no socket liveness — the decider picks that citizen's rail, and a rail can also " +
@@ -1284,10 +1284,10 @@ function registerEntwurfV2Tool(pi: ExtensionAPI): void {
1284
1284
  "dead: native-push-target-dead, indeterminate: native-push-probe-indeterminate (two rejects, " +
1285
1285
  "not one). " +
1286
1286
  "Set wants_reply for an answer. " +
1287
- "owned-outcome = wake a DORMANT socket-domain citizen via spawn-bg resume ONLY on a live target " +
1288
- "rejected as owned-live-no-autosend, on self-fetch as backend-liveness-unsupported, on " +
1289
- "native-push as native-push-no-resume-authority (both lack resume authority, but the reasons " +
1290
- "differ because native-push IS probe-measured), and never auto-converted",
1287
+ "This is the ONLY intent. The second one, owned-outcome, resumed a dormant citizen by launching " +
1288
+ "a hidden background child and was withdrawn under the visible-first rule; it is not selectable, " +
1289
+ "not deprecated-but-tolerated, and a dormant citizen is currently unreachable by this verb and " +
1290
+ "rejects as dormant-fire-forget-unsupported.",
1291
1291
  }),
1292
1292
  message: Type.String({
1293
1293
  description:
@@ -1298,7 +1298,7 @@ function registerEntwurfV2Tool(pi: ExtensionAPI): void {
1298
1298
  StringEnum(["steer", "follow_up"] as const, {
1299
1299
  description:
1300
1300
  "Injection style for a CONTROL-SOCKET send only: steer (immediate) or follow_up (after task). " +
1301
- "The mailbox, native-push, and spawn-bg plans carry no mode, so it has no effect on those rails.",
1301
+ "The mailbox and native-push plans carry no mode, so it has no effect on those rails.",
1302
1302
  }),
1303
1303
  ),
1304
1304
  wants_reply: Type.Optional(Type.Boolean({ description: "Human-conversation reply hint (default false)" })),
@@ -1306,7 +1306,7 @@ function registerEntwurfV2Tool(pi: ExtensionAPI): void {
1306
1306
 
1307
1307
  type EntwurfV2Params = {
1308
1308
  target: string;
1309
- intent: "fire-and-forget" | "owned-outcome";
1309
+ intent: "fire-and-forget";
1310
1310
  message: string;
1311
1311
  mode?: "steer" | "follow_up";
1312
1312
  wants_reply?: boolean;
@@ -1322,55 +1322,26 @@ function registerEntwurfV2Tool(pi: ExtensionAPI): void {
1322
1322
  registerTool({
1323
1323
  name: "entwurf_v2",
1324
1324
  label: "Dispatch (v2)",
1325
- description: `CANONICAL delivery surface for a garden id. When you have a garden id and want to
1326
- reach whoever it names message / reply / hand-off use THIS verb. A garden id alone does not
1327
- reveal which rail that citizen answers on a live socket session, a dormant one, a mailbox-backed
1328
- self-fetch session, or a native-push session
1329
- and entwurf_v2 is the one surface that reads that and routes correctly (so "when unsure which
1330
- transport, use entwurf_v2"). It dispatches to EXISTING targets; brand-new sibling creation is deferred
1331
- to a later v2 lane. Dispatch to a garden citizen through the unified entwurf_v2 verb: the 5b decider
1332
- picks the transport (live control-socket send / spawn-bg resume / meta-mailbox enqueue / native-push
1333
- direct injection) from the target's liveness + your intent, runs it under the v2 lock policy, and reports
1334
- one outcome (delivered / rejected / lock-retained / delivered-but-lock-dirty).
1335
-
1336
- LOCK POLICY (do not over-generalize it): the per-target lock is taken for a control-socket-DOMAIN
1337
- dispatch, which is BOTH the live send AND the dormant cell's spawn-bg resume spawn-bg is a separate
1338
- relaunch transport yet it still runs under that domain's lock. The mailbox and native-push rails are
1339
- lock-free: the mailbox is guarded instead by active-receiver deliverability, and native-push by its
1340
- adapter probe.
1341
-
1342
- - target: the garden id of the citizen to reach (required).
1343
- - intent: fire-and-forget (a send with no owned result) or owned-outcome (you own the result).
1344
- - message: the message/prompt to dispatch (required).
1345
- - mode: steer or follow_up for a CONTROL-SOCKET send (optional). The mailbox, native-push, and
1346
- spawn-bg plans carry no mode, so it has no effect on those rails — a native-push send IS live and
1347
- still ignores it.
1348
- - wants_reply: reply hint; it rides every rail (optional, default false).
1349
-
1350
- CHOOSING INTENT (picking wrong is rejected, never auto-fixed): to message / reply / hand off a peer
1351
- that entwurf_peers shows as liveness=alive (a live socket citizen, currently backend pi), use intent:
1352
- fire-and-forget — it routes to the live control-socket; set wants_reply:true if you need an answer
1353
- (wants_reply is NOT owned-outcome). Replies to a citizen with NO socket liveness
1354
- (liveness=unsupported) are ALSO fire-and-forget, and the decider picks that citizen's own rail: a
1355
- self-fetch backend (e.g. Claude Code) gets the meta-bridge mailbox, while a native-push backend
1356
- (e.g. Antigravity) gets direct injection into its live conversation and has NO mailbox at all — do
1357
- not assume mailbox semantics for every unsupported citizen. A native-push target IS measured by its
1358
- own adapter probe, and that probe is THREE-valued, so the send is never silently queued: alive →
1359
- injected; dead → rejected as native-push-target-dead; indeterminate → rejected as
1360
- native-push-probe-indeterminate. Those last two stay separate on purpose — "we could not establish
1361
- it" is not "it is gone". THERE IS A THIRD RESULT, so do not read
1362
- liveness=unsupported as "reachable by some rail": the mailbox path delivers only to a DELIVERABLE
1363
- citizen (a self-fetch backend whose receiver is live and armed), so a terminated Claude Code session —
1364
- and any record whose backend has no adapter on this lane, e.g. codex — is rejected as
1365
- mailbox-undeliverable rather than queued into an inbox nobody drains. unsupported means only "this
1366
- backend has no control-socket probe".
1367
- owned-outcome is ONLY for waking a DORMANT socket-domain citizen (spawn-bg resume, currently backend
1368
- pi); on a live target it is rejected as owned-live-no-autosend. Neither self-fetch nor native-push
1369
- has resume authority, but they reject under DIFFERENT reasons — self-fetch as
1370
- backend-liveness-unsupported, native-push as native-push-no-resume-authority.
1371
- It is NEVER auto-converted — so pick the right intent up front.
1372
-
1373
- The decider — not this surface — chooses the transport.`,
1325
+ description: `CANONICAL DELIVERY SURFACE for garden ids: message, reply, or hand off to whoever an id names. The id alone
1326
+ does not say which rail that citizen answers on. Give target + intent; the decider picks transport from
1327
+ liveness (live socket citizen control-socket send; deliverable self-fetch citizen meta-bridge mailbox;
1328
+ probe-alive native-push citizen → direct injection into its conversation) and reports ONE outcome
1329
+ (delivered / rejected / delivered-but-lock-dirty). EXISTING targets only; discover with entwurf_peers.
1330
+ A peer entwurf_peers shows as liveness=alive fire-and-forget. A citizen with NO socket liveness
1331
+ (liveness=unsupported) is ALSO fire-and-forget unsupported means only "no control-socket probe" and the
1332
+ decider picks its own rail: a self-fetch backend (e.g. Claude Code) gets the mailbox, a native-push backend
1333
+ (e.g. Antigravity) gets direct injection and has NO mailbox at all. THERE IS A THIRD RESULT: the mailbox
1334
+ delivers only to a DELIVERABLE citizen, so a terminated session, or a backend with no adapter here (e.g.
1335
+ codex), is mailbox-undeliverable, not queued for an inbox nobody drains. The native-push probe is 3-valued:
1336
+ alive injected; dead native-push-target-dead; indeterminate native-push-probe-indeterminate
1337
+ (unestablished gone). DORMANT IS UNREACHABLE: a socket-domain citizen that is not running gets
1338
+ dormant-fire-forget-unsupported same receiver rule as the mailbox, no active drainer means no
1339
+ delivery. The intent that used to answer there, owned-outcome, resumed it by
1340
+ launching a hidden background child; it was withdrawn under the visible-first rule, so re-open the session
1341
+ yourself and dispatch again. LOCK: taken for a control-socket-DOMAIN dispatch. The mailbox and native-push
1342
+ rails are lock-free deliverability and the adapter probe guard them. mode applies to a CONTROL-SOCKET
1343
+ send only; other plans carry no mode. wants_reply rides every rail. message caps at 16000 chars; send an
1344
+ artifact path + digest for more.`,
1374
1345
  parameters: entwurfV2Parameters,
1375
1346
  async execute(
1376
1347
  _toolCallId: string,
@@ -1410,8 +1381,10 @@ The decider — not this surface — chooses the transport.`,
1410
1381
  mode: params.mode,
1411
1382
  wants_reply: params.wants_reply,
1412
1383
  },
1413
- // agentDir / prefixRoots intentionally omitted here: the surface adapter falls back
1414
- // to the ENTWURF_PREFIX_ROOTS env SSOT for prefixRoots (5d-4); agentDir stays undefined.
1384
+ // No trust-preflight inputs are passed, and none exist to pass: the preflight on this
1385
+ // path guarded the resume verdict, so it left with `owned-outcome`. `senderProvider`
1386
+ // is the whole options surface now — do NOT re-add an `ENTWURF_PREFIX_ROOTS` fallback
1387
+ // here without a verdict that reads it (nothing on the dispatch path does).
1415
1388
  { senderProvider },
1416
1389
  );
1417
1390
  return {
@@ -1482,7 +1455,7 @@ function registerListSessionsTool(pi: ExtensionAPI): void {
1482
1455
  name: "entwurf_peers",
1483
1456
  label: "List Garden Citizens",
1484
1457
  description:
1485
- "List the entwurf fact surface: garden citizens from meta-records (including active self-fetch meta receivers such as claude-code) with liveness, plus diagnostics. The record is the sole address axis (#50 C4) — a control socket no record claims surfaces as a record-less-socket diagnostic, never a peer row. Pair with entwurf_v2 to address a peer by garden id; this surface reports facts, never per-row routing verbs.",
1458
+ "List the entwurf fact surface: garden citizens from meta-records (including active self-fetch meta receivers such as claude-code) with liveness, plus diagnostics. The record is the sole address axis (#50 C4) — a control socket no record claims surfaces as a record-less-socket diagnostic, never a peer row. Pair with entwurf_v2 to address a peer by garden id; this surface reports facts, never per-row routing verbs. A `dead` row is a REPORTED FACT and nothing more: that citizen is dormant and is currently unreachable by any verb, so listing it grants no action — appearing here is not an invitation to dispatch. It is facts-only and creates nothing: to open a NEW sibling use entwurf_fresh_call.",
1486
1459
  parameters: Type.Object({}),
1487
1460
  async execute(
1488
1461
  _toolCallId: string,
@@ -1507,3 +1480,179 @@ function registerListSessionsTool(pi: ExtensionAPI): void {
1507
1480
  },
1508
1481
  });
1509
1482
  }
1483
+
1484
+ // ============================================================================
1485
+ // Tool: entwurf_fresh_call
1486
+ // ============================================================================
1487
+
1488
+ const MUX_FRESH_CALL_MODULE = "./lib/mux-fresh-call.ts";
1489
+
1490
+ interface MuxFreshCallModule {
1491
+ freshCall(
1492
+ params: { backend: "pi" | "claude-code"; model: string; task: string; callerGardenId: string | null },
1493
+ env?: NodeJS.ProcessEnv,
1494
+ ): { ok: boolean };
1495
+ renderFreshCall(result: { ok: boolean }): { text: string; isError: boolean };
1496
+ }
1497
+
1498
+ /**
1499
+ * The caller identity for this surface is the RESIDENT's garden address — the one the record
1500
+ * minted at session start and keyed the control socket on. It is read from this extension's own
1501
+ * closure, never from a tool parameter and never from `process.env`.
1502
+ *
1503
+ * A caller that could pass an id could pass a WRONG one, and that value would become the address
1504
+ * the sibling calls back to — docs/mux-launch-rail.md §6-b keeps the measured incident. So there
1505
+ * is no parameter to be wrong with.
1506
+ *
1507
+ * A null resident id refuses LOUDLY rather than falling back: without an address to call back to,
1508
+ * the launch would be a window with no way home.
1509
+ */
1510
+ function registerFreshCallTool(pi: ExtensionAPI): void {
1511
+ // Same TS2589 workaround as registerEntwurfV2Tool — see the comment block there.
1512
+ const registerTool = pi.registerTool as (def: any) => void;
1513
+ registerTool({
1514
+ name: "entwurf_fresh_call",
1515
+ label: "Open Fresh Sibling",
1516
+ description: `Open ONE fresh visible sibling in the operator's own tmux session and hand it a first task. Two fixed
1517
+ backends only: pi, claude-code. The sibling's FIRST action is a callback to you carrying a nonce, and the
1518
+ sender envelope of that callback is its garden id — that is how you learn the address of something that did
1519
+ not exist a moment ago. This returns a LAUNCH receipt (tmux window/pane plus that nonce) and nothing else:
1520
+ it does NOT mean the runtime started, the first turn ran, or the task was delivered. Nothing polls for the
1521
+ callback; if it never arrives the window is visible and can be read directly. For EXISTING citizens use
1522
+ entwurf_v2 — this tool only creates, and entwurf_peers only reports. Model is REQUIRED and passed to the
1523
+ chosen runtime CLI (provider/model for pi; model id/alias for Claude Code); there are no arbitrary
1524
+ command/cwd/env knobs. Do not put secrets in the task — model and task argv are visible to same-user
1525
+ processes on this host.`,
1526
+ parameters: Type.Object({
1527
+ backend: StringEnum(["pi", "claude-code"], {
1528
+ description: "Which fixed runtime to open. Only these two; there is no arbitrary command.",
1529
+ }),
1530
+ model: Type.String({
1531
+ minLength: 1,
1532
+ maxLength: 200,
1533
+ pattern: "^[A-Za-z0-9][A-Za-z0-9._/:\\[\\]-]*$",
1534
+ description: "Required runtime model: canonical provider/model for pi, or a Claude Code model id/alias.",
1535
+ }),
1536
+ task: Type.String({
1537
+ minLength: 1,
1538
+ maxLength: 16000,
1539
+ description:
1540
+ "What the sibling should do after it calls you back. Plain instructions; no secrets (see the tool description).",
1541
+ }),
1542
+ }),
1543
+ async execute(
1544
+ _toolCallId: string,
1545
+ params: { backend: "pi" | "claude-code"; model: string; task: string },
1546
+ _signal: AbortSignal | undefined,
1547
+ _onUpdate: unknown,
1548
+ _ctx: ExtensionContext,
1549
+ ) {
1550
+ try {
1551
+ const mux = (await import(MUX_FRESH_CALL_MODULE)) as unknown as MuxFreshCallModule;
1552
+ const result = mux.freshCall({
1553
+ backend: params.backend,
1554
+ model: params.model,
1555
+ task: params.task,
1556
+ callerGardenId: residentGardenId,
1557
+ });
1558
+ const rendered = mux.renderFreshCall(result);
1559
+ return {
1560
+ content: [{ type: "text", text: rendered.text }],
1561
+ isError: rendered.isError,
1562
+ details: { isError: rendered.isError },
1563
+ };
1564
+ } catch (err) {
1565
+ const msg = err instanceof Error ? err.message : String(err);
1566
+ return {
1567
+ content: [{ type: "text", text: `entwurf_fresh_call error: ${msg}` }],
1568
+ isError: true,
1569
+ details: { error: msg },
1570
+ };
1571
+ }
1572
+ },
1573
+ });
1574
+ }
1575
+
1576
+ // ============================================================================
1577
+ // Tool: entwurf_resume_call
1578
+ // ============================================================================
1579
+
1580
+ const MUX_RESUME_CALL_MODULE = "./lib/mux-resume-call.ts";
1581
+ const V2_VISIBLE_RESUME_MODULE = "./lib/entwurf-v2-visible-resume.ts";
1582
+
1583
+ interface MuxResumeCallModule {
1584
+ resumeCall(
1585
+ params: { cwd: string; runtimeArgs: readonly string[] },
1586
+ env?: NodeJS.ProcessEnv,
1587
+ ): { ok: true; receipt: Record<string, string> } | { ok: false; reason: string };
1588
+ RESUME_CALL_REJECT_HINT: Record<string, string>;
1589
+ }
1590
+
1591
+ interface VisibleResumeModule {
1592
+ visibleResume(target: string, deps: unknown): Promise<{ ok: boolean }>;
1593
+ makeVisibleResumeDeps(launch: unknown): unknown;
1594
+ renderVisibleResume(result: { ok: boolean }): { text: string; isError: boolean };
1595
+ }
1596
+
1597
+ /**
1598
+ * Unlike `entwurf_fresh_call`, this tool needs NO caller identity: a resume names an existing
1599
+ * citizen, so the address is the parameter rather than something the sibling has to report back.
1600
+ * What it needs instead is the per-gid lock, which is why the composition lives on the v2 side of
1601
+ * the fence and only the LAUNCH is handed across (docs/mux-launch-rail.md §11).
1602
+ */
1603
+ function registerResumeCallTool(pi: ExtensionAPI): void {
1604
+ // Same TS2589 workaround as registerEntwurfV2Tool — see the comment block there.
1605
+ const registerTool = pi.registerTool as (def: any) => void;
1606
+ registerTool({
1607
+ name: "entwurf_resume_call",
1608
+ label: "Resume Dormant Citizen",
1609
+ description: `Reopen ONE DORMANT pi citizen under its OWN garden id, in a visible window in the operator's own tmux
1610
+ session. The record supplies everything — which transcript, which model, which provider, which cwd — so the only
1611
+ input is the target id: there is no model override, no task, and no prompt. This runs NO turn: the window comes
1612
+ back with the conversation and waits, and talking to it is still entwurf_v2 fire-and-forget on the socket this
1613
+ call stands up. You get TWO receipts and they mean different things: a LAUNCH receipt (tmux made a window and was
1614
+ asked to start pi) and an OBSERVATION receipt (the control socket answered under the same id, or
1615
+ resume-unobserved). Unobserved is a real outcome, not an error to retry — the window is visible, so read it. A
1616
+ citizen that is already LIVE is refused: address it with entwurf_v2 instead. Only pi citizens have a same-id
1617
+ resume, because only they stand a control socket up.`,
1618
+ parameters: Type.Object({
1619
+ target: Type.String({
1620
+ minLength: 1,
1621
+ pattern: "^\\d{8}T\\d{6}-[0-9a-f]{6}$",
1622
+ description: "Garden id of the DORMANT pi citizen to reopen (discover with entwurf_peers).",
1623
+ }),
1624
+ }),
1625
+ async execute(
1626
+ _toolCallId: string,
1627
+ params: { target: string },
1628
+ _signal: AbortSignal | undefined,
1629
+ _onUpdate: unknown,
1630
+ _ctx: ExtensionContext,
1631
+ ) {
1632
+ try {
1633
+ const mux = (await import(MUX_RESUME_CALL_MODULE)) as unknown as MuxResumeCallModule;
1634
+ const v2 = (await import(V2_VISIBLE_RESUME_MODULE)) as unknown as VisibleResumeModule;
1635
+ const launch = (input: { cwd: string; runtimeArgs: readonly string[] }) => {
1636
+ const launched = mux.resumeCall(input);
1637
+ return launched.ok
1638
+ ? { ok: true, handle: launched.receipt }
1639
+ : { ok: false, reason: launched.reason, hint: mux.RESUME_CALL_REJECT_HINT[launched.reason] };
1640
+ };
1641
+ const result = await v2.visibleResume(params.target, v2.makeVisibleResumeDeps(launch));
1642
+ const rendered = v2.renderVisibleResume(result);
1643
+ return {
1644
+ content: [{ type: "text", text: rendered.text }],
1645
+ isError: rendered.isError,
1646
+ details: { isError: rendered.isError },
1647
+ };
1648
+ } catch (err) {
1649
+ const msg = err instanceof Error ? err.message : String(err);
1650
+ return {
1651
+ content: [{ type: "text", text: `entwurf_resume_call error: ${msg}` }],
1652
+ isError: true,
1653
+ details: { error: msg },
1654
+ };
1655
+ }
1656
+ },
1657
+ });
1658
+ }
@@ -26,6 +26,17 @@ export interface AcpConnectionLike {
26
26
  newSession(params: unknown): Promise<{ sessionId?: string }>;
27
27
  prompt(params: { sessionId: string; prompt: AcpTextBlock[] }): Promise<{ stopReason?: string }>;
28
28
  setSessionConfigOption?(params: unknown): Promise<unknown>;
29
+ /**
30
+ * ACP `session/cancel` — the PROTOCOL way to end an in-flight prompt turn.
31
+ *
32
+ * The spec requires the agent to answer the pending `session/prompt` with
33
+ * `stopReason: "cancelled"` after this notification, so a user abort ends the
34
+ * turn as a protocol event (backend.ts maps cancelled → aborted) instead of a
35
+ * signal race. Fire-and-forget by contract: it is a JSON-RPC notification, so
36
+ * there is nothing to await and a send failure on an already-closed connection
37
+ * must not mask the abort the caller is executing.
38
+ */
39
+ cancel?(params: { sessionId: string }): void;
29
40
  /**
30
41
  * Closes the underlying SDK connection before child process teardown. With
31
42
  * the fluent SDK connection this is load-bearing: otherwise a successful
@@ -76,6 +87,17 @@ export function connectAcpClient(stream: Stream, handlers: AcpClientHandlers): A
76
87
  prompt: (params) =>
77
88
  agent.request(AGENT_METHODS.session_prompt, params as never) as Promise<{ stopReason?: string }>,
78
89
  setSessionConfigOption: (params) => agent.request(AGENT_METHODS.session_set_config_option, params as never),
90
+ cancel: (params) => {
91
+ // Notification, not a request: nothing resolves it, and the connection
92
+ // may already be closing when the operator aborts. Swallow both the sync
93
+ // throw and the rejected send — the caller's abort path continues either
94
+ // way (it escalates to teardown after a bounded grace).
95
+ try {
96
+ void Promise.resolve(agent.notify(AGENT_METHODS.session_cancel, params as never)).catch(() => {});
97
+ } catch {
98
+ // connection already closed — the abort path escalates on its own.
99
+ }
100
+ },
79
101
  close: (error) => {
80
102
  // Best-effort by contract (see AcpConnectionLike.close): a teardown-path
81
103
  // close that threw would mask the turn's real error and skip the child
@@ -36,10 +36,10 @@ import type { AcpTextBlock } from "./context.js";
36
36
 
37
37
  const MAX_AUGMENT_BYTES = 50 * 1024;
38
38
 
39
- // Carrier-less backends expose no `_meta.systemPrompt` (docs/acp-backend-rail.md
40
- // §9-4). claude folds the operator engraving into that carrier; a carrier-less
39
+ // Carrier-less backends expose no `_meta.systemPrompt` (docs/acp-backend-rail.md,
40
+ // “Cortex Code audit”). claude folds the operator engraving into that carrier; a carrier-less
41
41
  // backend (cortex, the first — precisely: SYSTEM-PROMPT-carrier-less, since
42
- // cortex does read `_meta` for an unpromoted caller-session-id seam, §11-8)
42
+ // cortex does read `_meta` for an unpromoted caller-session-id seam)
43
43
  // has nowhere to put it — so its operator engraving
44
44
  // rides HERE, prepended as the LEADING section of the first-user augment. It
45
45
  // travels on the WIRE only (new-only, never the config signature), so an
@@ -48,6 +48,13 @@ const MAX_AUGMENT_BYTES = 50 * 1024;
48
48
  // carrier feeds bridgeConfigSignature.
49
49
  const CARRIER_LESS_BACKENDS: ReadonlySet<string> = new Set(["cortex"]);
50
50
 
51
+ // The one sentence both rails owe the model: WHAT this block is and what it is
52
+ // NOT. Shared so neither rail's frame can quietly lose it (and so the gate has a
53
+ // single anchor to mutate). Everything after it is rail-specific, because the
54
+ // rails really do differ — see the frame in buildPiContextAugment.
55
+ const PROVENANCE_LEAD =
56
+ "This block is prepended to the FIRST USER MESSAGE of this session. It is not your system prompt.";
57
+
51
58
  // The env var an operator points at a personal engraving file. Shared surface
52
59
  // with the claude carrier (engraving.ts) BY NAME, but read INLINE here (not
53
60
  // imported): augment.ts is loaded raw by the strip-types carrier-augment gate,
@@ -116,6 +123,38 @@ export function buildPiContextAugment(params: PiContextAugmentParams): string {
116
123
  if (engraving) sections.push(engraving);
117
124
  }
118
125
 
126
+ // PROVENANCE FRAME — leads the bridge narrative, and it is not decoration.
127
+ //
128
+ // Measured 2026-07-30: asked where its instructions came from, the Claude ACP
129
+ // model asserted that the two paragraphs below were its system prompt. They
130
+ // are not — they are text prepended to its FIRST USER MESSAGE. Nothing in the
131
+ // block said otherwise, and a model has no other way to tell: on the wire a
132
+ // long first user message and a system prompt read the same. So the block now
133
+ // states its own provenance, per rail, because the rails genuinely differ
134
+ // (backend-adapter.ts: claude's buildSessionMeta carries `_meta.systemPrompt`,
135
+ // cortex's returns undefined so no `_meta` key is sent at all).
136
+ //
137
+ // Honest about scope: this makes the boundary STATEABLE, not enforced. It
138
+ // tells the model what it cannot otherwise observe; it does not stop a model
139
+ // from guessing anyway.
140
+ sections.push(
141
+ CARRIER_LESS_BACKENDS.has(params.backend)
142
+ ? [
143
+ "# entwurf: where this text comes from",
144
+ "",
145
+ PROVENANCE_LEAD,
146
+ `The ${params.backend} rail carries no system-prompt carrier at all — entwurf sends no \`_meta.systemPrompt\`, so everything entwurf tells you, including any operator engraving above, arrives here as user-message text.`,
147
+ "If you are asked what your system prompt says, do not quote this block as one.",
148
+ ].join("\n")
149
+ : [
150
+ "# entwurf: where this text comes from",
151
+ "",
152
+ PROVENANCE_LEAD,
153
+ `The ${params.backend} rail does have a system-prompt carrier (\`_meta.systemPrompt\`), and entwurf keeps it deliberately tiny: it carries the operator engraving only — never this narrative, never AGENTS.md.`,
154
+ "So: system prompt = the short engraving; everything below = first-user-message text. If you are asked what your system prompt says, do not quote this block as one.",
155
+ ].join("\n"),
156
+ );
157
+
119
158
  // Bridge identity + caller-side sibling stance. The closing line is a
120
159
  // caller-perspective statement: when THIS session throws an entwurf, the peer
121
160
  // on the other end is a sibling, not a worker.
@@ -1,9 +1,10 @@
1
1
  // ACP backend adapter rail — the PRODUCT seam by which a curated model id selects
2
- // which ACP backend (claude / future backend / …) drives a turn. See docs/acp-backend-rail.md §9.
2
+ // which ACP backend (claude / future backend / …) drives a turn. See the adapter contract in
3
+ // docs/acp-backend-rail.md.
3
4
  //
4
5
  // This seam is DISTINCT from `AcpTurnDeps` (backend.ts), which is the test/runtime
5
- // seam (fake spawn/connection/clock for the gates). The two are kept apart on
6
- // purpose (GPT-agreed §9-2): merging them would make a fake-deps fixture look like
6
+ // seam (fake spawn/connection/clock for the gates). Merging them would make a
7
+ // fake-deps fixture look like
7
8
  // a fake backend and force the adapter to carry clock/sessionDir/createConnection.
8
9
  // The wiring is `defaultDeps(adapter)` — the turn loop in backend.ts stays
9
10
  // backend-invariant; only these per-backend functions change with `adapter`.
@@ -217,7 +218,7 @@ function resolveClaudeLaunch(): AcpLaunchSpec {
217
218
  export const claudeAdapter: AcpBackendAdapter = {
218
219
  backend: "claude",
219
220
 
220
- // Claude owns its UNPREFIXED curated ids only (GPT-agreed §9-1). The native id
221
+ // Claude owns its UNPREFIXED curated ids only (rail “Adapter contract”). The native id
221
222
  // equals the curated id — claude carries no curation prefix to strip.
222
223
  routeModel(modelId) {
223
224
  return SUPPORTED_CLAUDE_IDS.has(modelId) ? { nativeModelId: modelId } : undefined;
@@ -298,13 +299,13 @@ export const claudeAdapter: AcpBackendAdapter = {
298
299
  * adapters claim one id. */
299
300
  // ---------------------------------------------------------------------------
300
301
  // cortex adapter — Snowflake Cortex Code, the first non-claude backend on the
301
- // rail (docs/acp-backend-rail.md §4/§6). It adds ZERO to the common layer:
302
+ // rail (docs/acp-backend-rail.md, “Shipped adapters”). It adds ZERO to the common layer:
302
303
  // everything cortex-specific lives here + models.ts + overlay.ts + the gates.
303
304
  // ---------------------------------------------------------------------------
304
305
 
305
306
  const SUPPORTED_CORTEX_IDS: ReadonlySet<string> = new Set(SUPPORTED_CORTEX_MODEL_IDS);
306
307
 
307
- /** Cortex's OWN settings (§10 B): a Snowflake connection name, or null. Opaque to
308
+ /** Cortex's OWN settings: a Snowflake connection name, or null. Opaque to
308
309
  * config.ts / backend.ts — only cortexAdapter reads it (casting back). */
309
310
  export interface CortexAdapterSettings {
310
311
  cortexConnection: string | null;
@@ -318,7 +319,7 @@ export const CORTEX_CONNECTION_ENV = "ENTWURF_ACP_CORTEX_CONNECTION";
318
319
  export const cortexAdapter: AcpBackendAdapter = {
319
320
  backend: "cortex",
320
321
 
321
- // Cortex owns the reserved `cortex-` prefix (§9-1). routeModel strips it to the
322
+ // Cortex owns the reserved `cortex-` prefix (rail “Adapter contract”). routeModel strips it to the
322
323
  // native id: `cortex-auto` → "auto", `cortex-claude-sonnet-5` → "claude-sonnet-5".
323
324
  routeModel(modelId) {
324
325
  if (!SUPPORTED_CORTEX_IDS.has(modelId)) return undefined;
@@ -406,7 +407,7 @@ export const cortexAdapter: AcpBackendAdapter = {
406
407
  return { envOverrides: { HOME: overlay.home, SNOWFLAKE_HOME: overlay.snowflakeHome } };
407
408
  },
408
409
 
409
- // System-prompt-carrier-less (§9-4/§11-8): Cortex ACP exposes no
410
+ // System-prompt-carrier-less (ACP rail “Cortex Code audit”): Cortex ACP exposes no
410
411
  // `_meta.systemPrompt` and has no developer_instructions / GEMINI_SYSTEM_MD
411
412
  // equivalent. (It does READ `_meta` — a caller-session-id seam, measured but
412
413
  // unexplored and deliberately not part of this contract.) loadCarrier returns
@@ -438,7 +439,7 @@ export const cortexAdapter: AcpBackendAdapter = {
438
439
  await setConfig.call(connection, { sessionId: acpSessionId, configId: "model", value: nativeModelId });
439
440
  },
440
441
 
441
- // A connection change must invalidate a reused session (§4/§7). Flat,
442
+ // A connection change must invalidate a reused session (rail: Adapter contract). Flat,
442
443
  // sorted-stable primitive map; reads ONLY the opaque adapterSettings. `backend`
443
444
  // + `nativeModelId` are added by backend.ts.
444
445
  configSignatureFields(adapterSettings) {
@@ -452,7 +453,7 @@ const ADAPTERS: readonly AcpBackendAdapter[] = [claudeAdapter, cortexAdapter];
452
453
  /**
453
454
  * Resolve the backend adapter that owns `modelId`.
454
455
  *
455
- * GPT-agreed §9-1 fail-fast contract:
456
+ * Routing fail-fast contract (rail “Adapter contract”):
456
457
  * - 0 matches → throw (unknown model — no silent default).
457
458
  * - 2+ matches → throw (prefix collision — a startup-visible registry bug).
458
459
  *
@@ -476,7 +477,7 @@ export function resolveAcpBackendAdapter(modelId: string): { adapter: AcpBackend
476
477
  }
477
478
 
478
479
  /** Every curated model row across all registered adapters — for provider registration.
479
- * GPT-agreed §9-6 registration-time fail-fast: every curated id must route to EXACTLY
480
+ * Registration-time fail-fast (rail “Adapter contract”): every curated id must route to EXACTLY
480
481
  * one adapter and no id may be duplicated across adapters. Catching it here means a
481
482
  * prefix-collision / duplicate surfaces at provider registration, not mid-turn. */
482
483
  export function allCuratedModels(): AcpModelRow[] {