@junghanacs/entwurf 0.13.0 → 0.13.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 (72) hide show
  1. package/AGENTS.md +5 -3
  2. package/BASELINE.md +57 -203
  3. package/CHANGELOG.md +19 -0
  4. package/CONTRIBUTING.md +1 -1
  5. package/DELIVERY.md +117 -261
  6. package/README.md +68 -408
  7. package/VERIFY.md +58 -87
  8. package/demo/README.md +1 -1
  9. package/docs/acp-backend-rail.md +231 -0
  10. package/docs/external-mcp-host.md +132 -0
  11. package/docs/fresh-cut-policy.md +99 -0
  12. package/docs/setup-clean-host.md +123 -328
  13. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +21 -48
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +12 -0
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +11 -10
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +1 -1
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/engraving.js +42 -1
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +4 -4
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +3 -3
  20. package/mcp/entwurf-bridge/src/index.ts +21 -48
  21. package/package.json +12 -9
  22. package/pi-extensions/entwurf-control.ts +20 -49
  23. package/pi-extensions/lib/acp/acp-client.ts +22 -0
  24. package/pi-extensions/lib/acp/augment.ts +42 -3
  25. package/pi-extensions/lib/acp/backend-adapter.ts +12 -11
  26. package/pi-extensions/lib/acp/backend.ts +305 -36
  27. package/pi-extensions/lib/acp/config.ts +1 -1
  28. package/pi-extensions/lib/acp/engraving.ts +43 -1
  29. package/pi-extensions/lib/acp/event-mapper.ts +16 -13
  30. package/pi-extensions/lib/acp/models.ts +4 -4
  31. package/pi-extensions/lib/acp/overlay.ts +3 -3
  32. package/prompts/engraving.md +13 -5
  33. package/run.sh +244 -108
  34. package/scripts/check-acp-carrier-augment.ts +246 -10
  35. package/scripts/check-acp-cortex.ts +5 -5
  36. package/scripts/check-acp-prompt-lifecycle.ts +565 -0
  37. package/scripts/check-acp-sdk-surface.ts +60 -21
  38. package/scripts/check-acp-stop-reason.ts +342 -0
  39. package/scripts/check-entwurf-v2-surface.ts +41 -0
  40. package/scripts/check-gate-qualification.ts +6 -3
  41. package/scripts/check-probe-ordering.ts +39 -7
  42. package/scripts/check-release-gate-outcomes.ts +332 -0
  43. package/scripts/lib/live-skip.ts +33 -0
  44. package/scripts/lib/probe-acp-turn.ts +33 -12
  45. package/scripts/lib/step-outcome.sh +88 -0
  46. package/scripts/mutants/acp-augment.json +106 -0
  47. package/scripts/mutants/acp-cortex.json +2 -2
  48. package/scripts/mutants/acp-prompt-lifecycle.json +100 -0
  49. package/scripts/mutants/acp-stop-reason.json +80 -0
  50. package/scripts/mutants/probe-ordering.json +27 -5
  51. package/scripts/mutants/release-gate.json +105 -0
  52. package/scripts/mutants/v2-surface.json +26 -15
  53. package/scripts/smoke-acp-bundled-mcp-live.ts +4 -3
  54. package/scripts/smoke-acp-carrier-augment-live.ts +2 -2
  55. package/scripts/smoke-acp-cortex-live.ts +8 -14
  56. package/scripts/smoke-acp-long-turn-live.ts +185 -0
  57. package/scripts/smoke-acp-mcp-live.ts +2 -2
  58. package/scripts/smoke-acp-memory-containment-live.ts +2 -2
  59. package/scripts/smoke-acp-ordering-probe-live.ts +2 -2
  60. package/scripts/smoke-acp-overlay-live.ts +2 -2
  61. package/scripts/smoke-acp-provider-live.ts +2 -2
  62. package/scripts/smoke-acp-raw-turn-live.ts +3 -3
  63. package/scripts/smoke-acp-session-reuse-live.ts +2 -2
  64. package/scripts/smoke-acp-skill-live.ts +2 -2
  65. package/scripts/smoke-acp-socket-citizen-live.ts +4 -3
  66. package/scripts/smoke-acp-v2-send-live.ts +4 -3
  67. package/scripts/smoke-agy-native-push-live.ts +4 -3
  68. package/scripts/smoke-claude-native-resume-live.sh +13 -3
  69. package/scripts/smoke-entwurf-chain-live.ts +352 -0
  70. package/scripts/smoke-entwurf-v2-matrix-live.ts +2 -2
  71. package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +2 -4
  72. package/scripts/smoke-meta-install-state.sh +4 -0
@@ -39,6 +39,18 @@ export function connectAcpClient(stream, handlers) {
39
39
  newSession: (params) => agent.request(AGENT_METHODS.session_new, params),
40
40
  prompt: (params) => agent.request(AGENT_METHODS.session_prompt, params),
41
41
  setSessionConfigOption: (params) => agent.request(AGENT_METHODS.session_set_config_option, params),
42
+ cancel: (params) => {
43
+ // Notification, not a request: nothing resolves it, and the connection
44
+ // may already be closing when the operator aborts. Swallow both the sync
45
+ // throw and the rejected send — the caller's abort path continues either
46
+ // way (it escalates to teardown after a bounded grace).
47
+ try {
48
+ void Promise.resolve(agent.notify(AGENT_METHODS.session_cancel, params)).catch(() => { });
49
+ }
50
+ catch {
51
+ // connection already closed — the abort path escalates on its own.
52
+ }
53
+ },
42
54
  close: (error) => {
43
55
  // Best-effort by contract (see AcpConnectionLike.close): a teardown-path
44
56
  // close that threw would mask the turn's real error and skip the child
@@ -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`.
@@ -54,7 +55,7 @@ function resolveClaudeLaunch() {
54
55
  }
55
56
  export const claudeAdapter = {
56
57
  backend: "claude",
57
- // Claude owns its UNPREFIXED curated ids only (GPT-agreed §9-1). The native id
58
+ // Claude owns its UNPREFIXED curated ids only (rail “Adapter contract”). The native id
58
59
  // equals the curated id — claude carries no curation prefix to strip.
59
60
  routeModel(modelId) {
60
61
  return SUPPORTED_CLAUDE_IDS.has(modelId) ? { nativeModelId: modelId } : undefined;
@@ -121,7 +122,7 @@ export const claudeAdapter = {
121
122
  * adapters claim one id. */
122
123
  // ---------------------------------------------------------------------------
123
124
  // cortex adapter — Snowflake Cortex Code, the first non-claude backend on the
124
- // rail (docs/acp-backend-rail.md §4/§6). It adds ZERO to the common layer:
125
+ // rail (docs/acp-backend-rail.md, “Shipped adapters”). It adds ZERO to the common layer:
125
126
  // everything cortex-specific lives here + models.ts + overlay.ts + the gates.
126
127
  // ---------------------------------------------------------------------------
127
128
  const SUPPORTED_CORTEX_IDS = new Set(SUPPORTED_CORTEX_MODEL_IDS);
@@ -131,7 +132,7 @@ const SUPPORTED_CORTEX_IDS = new Set(SUPPORTED_CORTEX_MODEL_IDS);
131
132
  export const CORTEX_CONNECTION_ENV = "ENTWURF_ACP_CORTEX_CONNECTION";
132
133
  export const cortexAdapter = {
133
134
  backend: "cortex",
134
- // Cortex owns the reserved `cortex-` prefix (§9-1). routeModel strips it to the
135
+ // Cortex owns the reserved `cortex-` prefix (rail “Adapter contract”). routeModel strips it to the
135
136
  // native id: `cortex-auto` → "auto", `cortex-claude-sonnet-5` → "claude-sonnet-5".
136
137
  routeModel(modelId) {
137
138
  if (!SUPPORTED_CORTEX_IDS.has(modelId))
@@ -213,7 +214,7 @@ export const cortexAdapter = {
213
214
  });
214
215
  return { envOverrides: { HOME: overlay.home, SNOWFLAKE_HOME: overlay.snowflakeHome } };
215
216
  },
216
- // System-prompt-carrier-less (§9-4/§11-8): Cortex ACP exposes no
217
+ // System-prompt-carrier-less (ACP rail “Cortex Code audit”): Cortex ACP exposes no
217
218
  // `_meta.systemPrompt` and has no developer_instructions / GEMINI_SYSTEM_MD
218
219
  // equivalent. (It does READ `_meta` — a caller-session-id seam, measured but
219
220
  // unexplored and deliberately not part of this contract.) loadCarrier returns
@@ -242,7 +243,7 @@ export const cortexAdapter = {
242
243
  }
243
244
  await setConfig.call(connection, { sessionId: acpSessionId, configId: "model", value: nativeModelId });
244
245
  },
245
- // A connection change must invalidate a reused session (§4/§7). Flat,
246
+ // A connection change must invalidate a reused session (rail: Adapter contract). Flat,
246
247
  // sorted-stable primitive map; reads ONLY the opaque adapterSettings. `backend`
247
248
  // + `nativeModelId` are added by backend.ts.
248
249
  configSignatureFields(adapterSettings) {
@@ -254,7 +255,7 @@ const ADAPTERS = [claudeAdapter, cortexAdapter];
254
255
  /**
255
256
  * Resolve the backend adapter that owns `modelId`.
256
257
  *
257
- * GPT-agreed §9-1 fail-fast contract:
258
+ * Routing fail-fast contract (rail “Adapter contract”):
258
259
  * - 0 matches → throw (unknown model — no silent default).
259
260
  * - 2+ matches → throw (prefix collision — a startup-visible registry bug).
260
261
  *
@@ -278,7 +279,7 @@ export function resolveAcpBackendAdapter(modelId) {
278
279
  return matches[0];
279
280
  }
280
281
  /** Every curated model row across all registered adapters — for provider registration.
281
- * GPT-agreed §9-6 registration-time fail-fast: every curated id must route to EXACTLY
282
+ * Registration-time fail-fast (rail “Adapter contract”): every curated id must route to EXACTLY
282
283
  * one adapter and no id may be duplicated across adapters. Catching it here means a
283
284
  * prefix-collision / duplicate surfaces at provider registration, not mid-turn. */
284
285
  export function allCuratedModels() {
@@ -23,7 +23,7 @@
23
23
  // the backend-invariant exclude-tools preflight and folds every one of them into
24
24
  // `bridgeConfigSignature` (so changing one still invalidates a reused cortex
25
25
  // session). Backend-OWNED settings never land here: they ride the opaque
26
- // `adapterSettings` seam (§10 B), which is what keeps a backend-named key like
26
+ // `adapterSettings` seam (rail “Adapter contract”), which is what keeps a backend-named key like
27
27
  // `cortexConnection` off this common shape.
28
28
  //
29
29
  // PURITY / SIGNATURE contract (NEXT oracle C / 핀1 / GPT `…2f9325` boost):
@@ -35,11 +35,30 @@
35
35
  // incompatible and the next turn opens a fresh ACP session with the new carrier.
36
36
  // That per-turn rebuild is the accepted cost of the A/B opt-in surface, never the
37
37
  // shipped default (which stays cached precisely so a resident never rebuilds).
38
+ //
39
+ // A-JOIN (measured LIVE 2026-07-31, 0.64.0 adapter, fresh Claude ACP): the model's
40
+ // system prompt arrived as
41
+ // `You are a Claude agent, built on Anthropic's Claude Agent SDK.# Engraving Here`
42
+ // A string-form `_meta.systemPrompt` replaces the `claude_code` preset, but the
43
+ // SDK still PREFIXES its own fixed identity sentence and joins the two with
44
+ // NOTHING — so the operator's heading was swallowed into the tail of the SDK's
45
+ // sentence. The boundary therefore belongs to the CARRIER, and it cannot be
46
+ // delegated to engraving.md: the render is trimmed (below), so a leading blank
47
+ // line in the markdown is eaten before it ever reaches the wire.
38
48
  import { readFileSync } from "node:fs";
39
49
  import { dirname, join, resolve } from "node:path";
40
50
  import { fileURLToPath } from "node:url";
41
51
  const HERE = dirname(fileURLToPath(import.meta.url));
42
52
  const DEFAULT_ENGRAVING_PATH = join(HERE, "prompts", "engraving.md");
53
+ /**
54
+ * The carrier's LEADING boundary — the one the Claude Agent SDK does not supply
55
+ * (see A-JOIN above). A constant, never derived from the template, so the render
56
+ * stays a pure function of (template, backend, mcpServerNames) and the operator's
57
+ * file whitespace can never drift `bridgeConfigSignature`. One blank line is the
58
+ * whole lever: it puts the carrier's first line at the start of its own block
59
+ * instead of at the end of the SDK's fixed sentence.
60
+ */
61
+ export const CARRIER_LEAD_SEPARATOR = "\n\n";
43
62
  let cached = null;
44
63
  /** Point the loader at an alternate engraving file (A/B); bypasses the cache. */
45
64
  function resolveEngravingPath() {
@@ -65,6 +84,22 @@ function interpolate(template, params) {
65
84
  const mcpList = names.length > 0 ? names.join(", ") : "(none registered)";
66
85
  return template.replace(/\{\{backend\}\}/g, params.backend).replace(/\{\{mcp_servers\}\}/g, mcpList);
67
86
  }
87
+ /**
88
+ * Render one template into a wire-ready carrier, or `""` when the template has no
89
+ * body (the caller turns that into the opt-out / fail-loud branches).
90
+ *
91
+ * The ORDER here is the A-join fix. Trim first: the operator template's own
92
+ * leading/trailing whitespace must not reach the wire (it would drift
93
+ * bridgeConfigSignature) and the emptiness test must see the BODY — a
94
+ * separator-only string is an opt-out, not a carrier. Then attach OUR boundary,
95
+ * which is what the SDK's fixed sentence has nothing of.
96
+ */
97
+ function renderCarrier(source, params) {
98
+ const body = interpolate(source, params).trim();
99
+ if (body.length === 0)
100
+ return "";
101
+ return `${CARRIER_LEAD_SEPARATOR}${body}`;
102
+ }
68
103
  /**
69
104
  * The rendered engraving carrier, or null when an ENV-OVERRIDE engraving file
70
105
  * (`ENTWURF_ACP_ENGRAVING_PATH`) is empty, whitespace-only, missing, or
@@ -77,6 +112,12 @@ function interpolate(template, params) {
77
112
  * Callers MUST treat null as "no carrier configured" and omit `_meta.systemPrompt`
78
113
  * entirely (passing "" as the `appendSystemPrompt` signature input) so
79
114
  * subscription billing is never reclassified.
115
+ *
116
+ * A non-null carrier always LEADS with `CARRIER_LEAD_SEPARATOR`, and callers must
117
+ * pass it on BYTE-FOR-BYTE: the same string feeds `bridgeConfigSignature`
118
+ * (`appendSystemPrompt`) and the wire (`_meta.systemPrompt`), so normalizing it at
119
+ * either hop both re-opens the A-join and makes reuse key on a string that was
120
+ * never sent.
80
121
  */
81
122
  export function loadEngraving(params) {
82
123
  const filePath = resolveEngravingPath();
@@ -92,7 +133,7 @@ export function loadEngraving(params) {
92
133
  }
93
134
  return null;
94
135
  }
95
- const rendered = interpolate(source, params).trim();
136
+ const rendered = renderCarrier(source, params);
96
137
  if (rendered.length === 0) {
97
138
  if (isShippedDefault) {
98
139
  throw new Error(`entwurf: shipped engraving carrier at ${filePath} is empty — it is the auto-memory ` +
@@ -9,8 +9,8 @@
9
9
  // Two curated sets live here, one per adapter on the rail
10
10
  // (backend-adapter.ts): the unprefixed Claude ids (the reference backend) and
11
11
  // the `cortex-` prefixed Snowflake Cortex Code ids (the second backend, landed
12
- // 0.13.0 — docs/acp-backend-rail.md §6/§11-8). Codex is already a native garden
13
- // citizen and Gemini/major tools use native, so neither is an ACP backend here.
12
+ // 0.13.0 — docs/acp-backend-rail.md “Cortex Code audit”). Codex has native
13
+ // delivery-probe evidence but no managed citizen lane; neither it nor Gemini is an ACP backend here.
14
14
  // A third governed backend EXTENDS this file the same way — it does not change
15
15
  // the pattern.
16
16
  // pi 0.80 migration: the standalone root `getModels()` moved to the deprecated
@@ -77,9 +77,9 @@ function claudeContextWindow(model) {
77
77
  // ── Cortex (Snowflake Cortex Code) curated surface ──────────────────────────
78
78
  //
79
79
  // entwurf's ACP rail is backend-extensible: Cortex is the first NON-claude
80
- // backend to land through it (docs/acp-backend-rail.md §6). pi-ai carries no
80
+ // backend to land through it (docs/acp-backend-rail.md, “Shipped adapters”). pi-ai carries no
81
81
  // snowflake/cortex model source, so this surface is HAND-CURATED. Every id
82
- // carries the reserved `cortex-` prefix (§9-1): the prefix is the SINGLE routing
82
+ // carries the reserved `cortex-` prefix: the prefix is the SINGLE routing
83
83
  // authority (backend-adapter.ts `routeModel`) and keeps the ids off the Claude
84
84
  // native ids Cortex routes to (`cortex-claude-sonnet-5` vs the unprefixed
85
85
  // `claude-sonnet-5` the claude adapter owns). `routeModel` strips the prefix to
@@ -15,7 +15,7 @@
15
15
  // materializer, and cortex did: `ensureCortexDualHomeOverlay` (+ projectCortexMcpJson /
16
16
  // sweepDeadCortexOverlays / cortexOverlayScopeId) lives further down this file with its
17
17
  // own D-number rationale — an isolated HOME rather than a redirect knob, because cortex
18
- // has no CLAUDE_CONFIG_DIR equivalent (§11-8 D2). The two share the module, never the
18
+ // has no CLAUDE_CONFIG_DIR equivalent (ACP rail Cortex audit D2). The two share the module, never the
19
19
  // strategy. Codex/Gemini overlays (CODEX_HOME / admin.toml) remain behavior-oracle
20
20
  // territory and are out of scope here.
21
21
  //
@@ -201,11 +201,11 @@ export function ensureClaudeConfigOverlay(realDir = CLAUDE_REAL_CONFIG_DIR, over
201
201
  }
202
202
  // ============================================================================
203
203
  // Cortex dual-HOME overlay — the as-measured containment for `cortex acp serve`
204
- // (CP0 audit 2026-07-29, Cortex Code v1.1.52; docs/acp-backend-rail.md §4/§11-8).
204
+ // (CP0 audit 2026-07-29, Cortex Code v1.1.52; docs/acp-backend-rail.md “Cortex Code audit”).
205
205
  // ============================================================================
206
206
  //
207
207
  // Why this is NOT the claude-shaped `SNOWFLAKE_HOME`-only overlay PR #40 shipped
208
- // (each point is a measured defect, not a preference — §11-8 D-numbers):
208
+ // (each point is a measured defect, not a preference — Cortex audit D-numbers):
209
209
  //
210
210
  // - D2: cortex reads `CONFIG_DIRS = [".claude", ".cortex"]` at `homedir()` and
211
211
  // `~/.claude/skills` — a SNOWFLAKE_HOME redirect cannot move that axis. An
@@ -344,54 +344,27 @@ function abbreviateHomeMcp(cwd: string): string {
344
344
  // sender replyability).
345
345
  server.tool(
346
346
  "entwurf_v2",
347
- "CANONICAL DELIVERY SURFACE for garden ids. When you have a garden id and want to reach " +
348
- "whoever it names message / reply / hand-off use THIS verb. A garden id alone does " +
349
- "not tell you which rail that citizen answers on a live socket citizen, a dormant one, a " +
350
- "mailbox-backed self-fetch session, or a native-push session and entwurf_v2 is the one " +
351
- 'surface that reads that for you and routes correctly (so "when unsure which transport, use ' +
352
- 'entwurf_v2"). You give the target ' +
353
- "garden id + your intent; the decider picks the transport from the target's liveness " +
354
- "(live socket citizen → control-socket send; dormant socket citizen → spawn-bg resume; active " +
355
- "deliverable self-fetch citizen meta-bridge mailbox; probe-alive native-push citizen direct " +
356
- "injection into its live conversation) under the v2 lock policy, and reports ONE outcome " +
357
- "(delivered / rejected / lock-retained / delivered-but-lock-dirty). LOCK POLICY (do not " +
358
- "over-generalize it): the per-target lock is taken for a control-socket-DOMAIN dispatch, which is " +
359
- "both the live send AND the dormant cell's spawn-bg resume spawn-bg is a separate relaunch " +
360
- "transport yet it still runs under that domain's lock. The mailbox and native-push rails are " +
361
- "lock-free: the mailbox is guarded instead by active-receiver deliverability, and native-push by " +
362
- "its adapter probe. The decider not the " +
363
- "caller chooses the transport. Note: entwurf_v2 dispatches to EXISTING targets; " +
364
- "brand-new sibling creation is deferred to a later v2 lane. " +
365
- "CHOOSING INTENT (read thispicking wrong is rejected, never auto-fixed): to message / " +
366
- "reply / hand off a peer that entwurf_peers shows as liveness=alive (a live socket citizen, " +
367
- "currently backend pi) use intent: fire-and-forget it routes to the live control-socket; set " +
368
- "wants_reply:true if you need an answer (wants_reply is NOT owned-outcome). Replies to a citizen " +
369
- "with NO socket liveness (liveness=unsupported) are ALSO fire-and-forget, and the decider picks " +
370
- "that citizen's own rail: a self-fetch backend (e.g. Claude Code) gets the meta-bridge mailbox, " +
371
- "while a native-push backend (e.g. Antigravity) gets direct injection into its live conversation " +
372
- "and has NO mailbox at all — do not assume mailbox semantics for every unsupported citizen. A " +
373
- "native-push target IS measured by its own adapter probe, and that probe is THREE-valued, so " +
374
- "the send is never silently queued: alive → injected; dead → rejected as " +
375
- "native-push-target-dead; indeterminate → rejected as native-push-probe-indeterminate. " +
376
- "Those two rejects stay distinct because 'we could not establish it' is not 'it is gone' — " +
377
- "collapsing them reports a guess as a fact. THERE IS A " +
378
- "THIRD RESULT, so do not read liveness=unsupported as 'reachable by some rail': the mailbox path " +
379
- "delivers only to a DELIVERABLE citizen (a self-fetch backend whose receiver is live and armed), " +
380
- "so a terminated Claude Code session — and any record whose backend has no adapter on this lane, " +
381
- "e.g. codex — is rejected as mailbox-undeliverable rather than queued into an inbox nobody drains. " +
382
- "unsupported means only 'this backend has no control-socket probe'. " +
383
- "owned-outcome is ONLY for waking a DORMANT socket-domain citizen (spawn-bg resume, currently " +
384
- "backend pi); on a live target it is rejected as owned-live-no-autosend. Neither self-fetch nor " +
385
- "native-push has resume authority, but they reject under DIFFERENT reasons — self-fetch as " +
386
- "backend-liveness-unsupported, native-push as native-push-no-resume-authority. " +
387
- "It is NEVER auto-converted — so pick the right intent up front. " +
388
- "mode applies to a CONTROL-SOCKET send only — it is the injection style for a live pi turn, and " +
389
- "the mailbox, native-push, and spawn-bg plans carry no mode at all, so setting it for those " +
390
- "targets changes nothing (a native-push send IS live and still ignores it). wants_reply rides " +
391
- "every rail. Use entwurf_peers to discover targets. " +
392
- "Payload guidance: message hard cap 16000 chars. For larger reviews/logs, write an " +
393
- "artifact and dispatch its path plus a short digest; avoid multi-part sends because " +
394
- "mailbox doorbells may coalesce.",
347
+ "CANONICAL DELIVERY SURFACE for garden ids: message, reply, or hand off to whoever an id names. The id " +
348
+ "alone does not say which rail that citizen answers on. Give target + intent; the decider picks transport " +
349
+ "from liveness (live socket citizen control-socket send; dormant socket citizen spawn-bg resume; " +
350
+ "deliverable self-fetch citizen meta-bridge mailbox; probe-alive native-push citizen direct injection " +
351
+ "into its conversation) and reports ONE outcome (delivered / rejected / lock-retained / " +
352
+ "delivered-but-lock-dirty). EXISTING targets only; discover with entwurf_peers. INTENT — picking wrong is " +
353
+ "rejected, never auto-converted. A peer entwurf_peers shows as liveness=alive fire-and-forget. A " +
354
+ "citizen with NO socket liveness (liveness=unsupported) is ALSO fire-and-forget unsupported means only " +
355
+ '"no control-socket probe" and the decider picks its own rail: a self-fetch backend (e.g. Claude Code) ' +
356
+ "gets the mailbox, a native-push backend (e.g. Antigravity) gets direct injection and has NO mailbox at " +
357
+ "all. THERE IS A THIRD RESULT: the mailbox delivers only to a DELIVERABLE citizen, so a terminated " +
358
+ "session, or a backend with no adapter here (e.g. codex), is mailbox-undeliverable, not queued for an " +
359
+ "inbox nobody drains. The native-push probe is 3-valued: alive injected; dead " +
360
+ "native-push-target-dead; indeterminate native-push-probe-indeterminate (unestablished gone). " +
361
+ "owned-outcome wakes a DORMANT socket-domain citizen by spawn-bg resume ONLY live target → " +
362
+ "owned-live-no-autosend, self-fetch backend-liveness-unsupported, native-push " +
363
+ "native-push-no-resume-authority. LOCK: taken for a control-socket-DOMAIN dispatch the live send AND " +
364
+ "the dormant cell's spawn-bg resume, a separate transport that still runs under that domain's lock. The " +
365
+ "mailbox and native-push rails are lock-free deliverability and the adapter probe guard them. mode " +
366
+ "applies to a CONTROL-SOCKET send only; other plans carry no mode. wants_reply rides every rail. message " +
367
+ "caps at 16000 chars; send an artifact path + digest for more.",
395
368
  {
396
369
  target: z.string().min(1).describe("Target garden id (use entwurf_peers to discover)"),
397
370
  intent: z
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@junghanacs/entwurf",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "Garden-citizen dispatch substrate and meta-bridge for Claude Code, Codex, Antigravity, and pi harnesses.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -42,6 +42,9 @@
42
42
  "demo/demo.sh",
43
43
  "demo/demo-baseline.sh",
44
44
  "docs/setup-clean-host.md",
45
+ "docs/external-mcp-host.md",
46
+ "docs/fresh-cut-policy.md",
47
+ "docs/acp-backend-rail.md",
45
48
  "docs/assets/entwurf-hero.jpg",
46
49
  "docs/assets/entwurf-demo.gif",
47
50
  "docs/assets/entwurf-doomemacs.gif",
@@ -71,23 +74,23 @@
71
74
  "claudeCodeFloor": ">=2.1.217"
72
75
  },
73
76
  "dependencies": {
74
- "@agentclientprotocol/claude-agent-acp": "0.62.0",
77
+ "@agentclientprotocol/claude-agent-acp": "0.64.0",
75
78
  "@agentclientprotocol/sdk": "1.3.0",
76
79
  "@anthropic-ai/sdk": "0.100.1",
77
80
  "@modelcontextprotocol/sdk": "^1.12.1",
78
81
  "zod": "^3.25.0 || ^4.0.0"
79
82
  },
80
83
  "peerDependencies": {
81
- "@earendil-works/pi-ai": ">=0.82.1 <0.83",
82
- "@earendil-works/pi-coding-agent": ">=0.82.1 <0.83",
83
- "@earendil-works/pi-tui": ">=0.82.1 <0.83",
84
+ "@earendil-works/pi-ai": ">=0.83.0 <0.84",
85
+ "@earendil-works/pi-coding-agent": ">=0.83.0 <0.84",
86
+ "@earendil-works/pi-tui": ">=0.83.0 <0.84",
84
87
  "typebox": "*"
85
88
  },
86
89
  "devDependencies": {
87
90
  "@biomejs/biome": "^2.4.13",
88
- "@earendil-works/pi-ai": "0.82.1",
89
- "@earendil-works/pi-coding-agent": "0.82.1",
90
- "@earendil-works/pi-tui": "0.82.1",
91
+ "@earendil-works/pi-ai": "0.83.0",
92
+ "@earendil-works/pi-coding-agent": "0.83.0",
93
+ "@earendil-works/pi-tui": "0.83.0",
91
94
  "@types/node": "^24.3.0",
92
95
  "husky": "^9.1.7",
93
96
  "typescript": "^6.0.2"
@@ -132,7 +135,7 @@
132
135
  "format": "biome check --write .",
133
136
  "check-meta-manifest-schema": "./run.sh check-meta-manifest-schema",
134
137
  "check-package-source-routing": "./run.sh check-package-source-routing",
135
- "check": "pnpm lint && pnpm typecheck && ./run.sh check-shell-quote && ./run.sh check-install-surface && ./run.sh check-entwurf-session-identity && ./run.sh check-meta-session && ./run.sh check-meta-v3-record && ./run.sh check-mailbox-receipt-state && ./run.sh check-entwurf-capabilities && ./run.sh check-capability-bundle-reach && ./run.sh check-bridge-delivery && ./run.sh smoke-pi-attach && ./run.sh check-fresh-cut-gate && ./run.sh check-meta-mailbox-state-write && ./run.sh check-meta-receiver-marker && ./run.sh check-hook-launch-topology && ./run.sh check-meta-identity-consumers && ./run.sh check-meta-capability-source && ./run.sh check-socket-probe && ./run.sh smoke-meta-honesty && ./run.sh smoke-meta-install-state && ./run.sh check-meta-doctor-oracle && ./run.sh smoke-agy-install-state && ./run.sh check-agy-permission-matrix && ./run.sh smoke-agy-statusline-state && ./run.sh smoke-agy-hooks-state && ./run.sh smoke-pi-provider-state && ./run.sh smoke-user-scope-citizen && ./run.sh smoke-meta-prune && ./run.sh smoke-meta-keyset-guard && ./run.sh check-meta-manifest-schema && ./run.sh check-package-source-routing && ./run.sh check-model-lock && ./run.sh check-dep-versions && ./run.sh check-node-floor-coherence && ./run.sh check-claude-floor-coherence && ./run.sh check-install-preflight && ./run.sh check-pi-import-surface && ./run.sh check-env-namespace && ./run.sh check-pi-runtime-version && ./run.sh check-pi-preflight && ./run.sh check-project-trust-handler && ./run.sh check-entwurf-v2-contract && ./run.sh check-entwurf-v2-lock && ./run.sh check-entwurf-v2-decider && ./run.sh check-entwurf-v2-matrix && ./run.sh check-entwurf-v2-release && ./run.sh check-entwurf-v2-send && ./run.sh check-entwurf-v2-send-fallback && ./run.sh check-entwurf-v2-mailbox && ./run.sh check-entwurf-v2-native-push && ./run.sh check-entwurf-v2-runner && ./run.sh check-entwurf-control-rpc && ./run.sh check-entwurf-v2-production && ./run.sh check-entwurf-v2-surface && ./run.sh check-entwurf-bridge-boot && ./run.sh check-entwurf-bridge-pi-free && ./run.sh check-entwurf-v2-spawn && ./run.sh check-entwurf-resume-args && ./run.sh check-entwurf-v2-spawn-production && ./run.sh check-entwurf-facts && ./run.sh check-control-socket-path && ./run.sh check-socket-discovery && ./run.sh check-meta-listing && ./run.sh check-entwurf-fact-provider && ./run.sh check-entwurf-peers-surface && ./run.sh check-entwurf-self-address && ./run.sh check-entwurf-deliverability && ./run.sh check-native-push-adapter && ./run.sh check-native-push-register && ./run.sh check-agy-sender-identity && ./run.sh check-auth-boundary && ./run.sh check-acp-provider-surface && ./run.sh check-acp-sdk-surface && ./run.sh check-acp-overlay && ./run.sh check-acp-tool-surface && ./run.sh check-acp-event-mapper && ./run.sh check-acp-prompt-builder && ./run.sh check-acp-config && ./run.sh check-acp-session-store && ./run.sh check-acp-backend-preflight && ./run.sh check-acp-session-reuse && ./run.sh check-acp-carrier-augment && ./run.sh check-probe-ordering && ./run.sh check-probe-cli-shim && ./run.sh check-acp-cortex && ./run.sh check-gate-qualification && ./run.sh check-pack"
138
+ "check": "pnpm lint && pnpm typecheck && ./run.sh check-shell-quote && ./run.sh check-install-surface && ./run.sh check-entwurf-session-identity && ./run.sh check-meta-session && ./run.sh check-meta-v3-record && ./run.sh check-mailbox-receipt-state && ./run.sh check-entwurf-capabilities && ./run.sh check-capability-bundle-reach && ./run.sh check-bridge-delivery && ./run.sh smoke-pi-attach && ./run.sh check-fresh-cut-gate && ./run.sh check-meta-mailbox-state-write && ./run.sh check-meta-receiver-marker && ./run.sh check-hook-launch-topology && ./run.sh check-meta-identity-consumers && ./run.sh check-meta-capability-source && ./run.sh check-socket-probe && ./run.sh smoke-meta-honesty && ./run.sh smoke-meta-install-state && ./run.sh check-meta-doctor-oracle && ./run.sh smoke-agy-install-state && ./run.sh check-agy-permission-matrix && ./run.sh smoke-agy-statusline-state && ./run.sh smoke-agy-hooks-state && ./run.sh smoke-pi-provider-state && ./run.sh smoke-user-scope-citizen && ./run.sh smoke-meta-prune && ./run.sh smoke-meta-keyset-guard && ./run.sh check-meta-manifest-schema && ./run.sh check-package-source-routing && ./run.sh check-model-lock && ./run.sh check-dep-versions && ./run.sh check-node-floor-coherence && ./run.sh check-claude-floor-coherence && ./run.sh check-install-preflight && ./run.sh check-pi-import-surface && ./run.sh check-env-namespace && ./run.sh check-pi-runtime-version && ./run.sh check-pi-preflight && ./run.sh check-project-trust-handler && ./run.sh check-entwurf-v2-contract && ./run.sh check-entwurf-v2-lock && ./run.sh check-entwurf-v2-decider && ./run.sh check-entwurf-v2-matrix && ./run.sh check-entwurf-v2-release && ./run.sh check-entwurf-v2-send && ./run.sh check-entwurf-v2-send-fallback && ./run.sh check-entwurf-v2-mailbox && ./run.sh check-entwurf-v2-native-push && ./run.sh check-entwurf-v2-runner && ./run.sh check-entwurf-control-rpc && ./run.sh check-entwurf-v2-production && ./run.sh check-entwurf-v2-surface && ./run.sh check-entwurf-bridge-boot && ./run.sh check-entwurf-bridge-pi-free && ./run.sh check-entwurf-v2-spawn && ./run.sh check-entwurf-resume-args && ./run.sh check-entwurf-v2-spawn-production && ./run.sh check-entwurf-facts && ./run.sh check-control-socket-path && ./run.sh check-socket-discovery && ./run.sh check-meta-listing && ./run.sh check-entwurf-fact-provider && ./run.sh check-entwurf-peers-surface && ./run.sh check-entwurf-self-address && ./run.sh check-entwurf-deliverability && ./run.sh check-native-push-adapter && ./run.sh check-native-push-register && ./run.sh check-agy-sender-identity && ./run.sh check-auth-boundary && ./run.sh check-acp-provider-surface && ./run.sh check-acp-sdk-surface && ./run.sh check-acp-overlay && ./run.sh check-acp-tool-surface && ./run.sh check-acp-event-mapper && ./run.sh check-acp-stop-reason && ./run.sh check-acp-prompt-lifecycle && ./run.sh check-acp-prompt-builder && ./run.sh check-acp-config && ./run.sh check-acp-session-store && ./run.sh check-acp-backend-preflight && ./run.sh check-acp-session-reuse && ./run.sh check-acp-carrier-augment && ./run.sh check-release-gate-outcomes && ./run.sh check-probe-ordering && ./run.sh check-probe-cli-shim && ./run.sh check-acp-cortex && ./run.sh check-gate-qualification && ./run.sh check-pack"
136
139
  },
137
140
  "pi": {
138
141
  "extensions": [
@@ -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; dormant socket citizen spawn-bg resume; deliverable
1328
+ self-fetch citizen meta-bridge mailbox; probe-alive native-push citizen → direct injection into its
1329
+ conversation) and reports ONE outcome (delivered / rejected / lock-retained / delivered-but-lock-dirty).
1330
+ EXISTING targets only; discover with entwurf_peers. INTENT picking wrong is rejected, never
1331
+ auto-converted. A peer entwurf_peers shows as liveness=alive fire-and-forget. A citizen with NO socket
1332
+ liveness (liveness=unsupported) is ALSO fire-and-forget unsupported means only "no control-socket probe"
1333
+ and the decider picks its own rail: a self-fetch backend (e.g. Claude Code) gets the mailbox, a native-push
1334
+ backend (e.g. Antigravity) gets direct injection and has NO mailbox at all. THERE IS A THIRD RESULT: the
1335
+ mailbox delivers only to a DELIVERABLE citizen, so a terminated session, or a backend with no adapter here
1336
+ (e.g. codex), is mailbox-undeliverable, not queued for an inbox nobody drains. The native-push probe is
1337
+ 3-valued: alive injected; dead native-push-target-dead; indeterminate native-push-probe-indeterminate
1338
+ (unestablished gone). owned-outcome wakes a DORMANT socket-domain citizen by spawn-bg resume ONLY live
1339
+ target owned-live-no-autosend, self-fetch backend-liveness-unsupported, native-push
1340
+ native-push-no-resume-authority. LOCK: taken for a control-socket-DOMAIN dispatch — the live send AND the
1341
+ dormant cell's spawn-bg resume, a separate transport that still runs under that domain's lock. The mailbox
1342
+ and native-push rails are lock-free deliverability and the adapter probe guard them. mode applies to a
1343
+ CONTROL-SOCKET send only; other plans carry no mode. wants_reply rides every rail. message caps at 16000
1344
+ chars; send an artifact path + digest for more.`,
1374
1345
  parameters: entwurfV2Parameters,
1375
1346
  async execute(
1376
1347
  _toolCallId: string,
@@ -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.