@junghanacs/entwurf 0.25.1 → 0.30.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 (86) hide show
  1. package/AGENTS.md +1 -1
  2. package/BASELINE.md +1 -1
  3. package/CHANGELOG.md +190 -0
  4. package/DELIVERY.md +1 -1
  5. package/README.md +183 -674
  6. package/VERIFY.md +21 -17
  7. package/demo/README.md +1 -1
  8. package/docs/acp-backend-rail.md +115 -18
  9. package/docs/external-mcp-host.md +1 -1
  10. package/docs/setup-clean-host.md +346 -10
  11. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +27 -4
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +55 -1
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +1 -1
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +2 -2
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-mailbox.js +2 -2
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +2 -2
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +3 -5
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +3 -3
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +13 -6
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-mailbox-body.js +1 -1
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +3 -3
  22. package/mcp/entwurf-bridge/src/index.ts +30 -3
  23. package/package.json +13 -13
  24. package/pi-extensions/entwurf-control.ts +50 -637
  25. package/pi-extensions/lib/acp/backend-adapter.ts +81 -1
  26. package/pi-extensions/lib/acp/backend.ts +41 -5
  27. package/pi-extensions/lib/entwurf-fact-provider.ts +2 -2
  28. package/pi-extensions/lib/entwurf-mcp-server.ts +74 -0
  29. package/pi-extensions/lib/entwurf-peers-render.ts +1 -1
  30. package/pi-extensions/lib/entwurf-v2-decider.ts +2 -2
  31. package/pi-extensions/lib/entwurf-v2-mailbox.ts +2 -2
  32. package/pi-extensions/lib/entwurf-v2-production.ts +3 -3
  33. package/pi-extensions/lib/entwurf-v2-surface.ts +3 -5
  34. package/pi-extensions/lib/entwurf-v2-visible-resume.ts +3 -3
  35. package/pi-extensions/lib/fresh-call-composition.ts +13 -6
  36. package/pi-extensions/lib/meta-mailbox-body.ts +1 -1
  37. package/pi-extensions/lib/mux-fresh-call.ts +3 -3
  38. package/run.sh +177 -53
  39. package/scripts/check-entwurf-peers-surface.ts +8 -15
  40. package/scripts/check-entwurf-self-address.ts +7 -11
  41. package/scripts/check-entwurf-v2-surface.ts +20 -112
  42. package/scripts/check-fresh-call-dispatch.ts +12 -13
  43. package/scripts/check-gate-qualification.ts +250 -5
  44. package/scripts/check-herdr-plugin-build.ts +159 -57
  45. package/scripts/check-install-surface.ts +3 -2
  46. package/scripts/check-mux-resume-call.ts +24 -35
  47. package/scripts/check-pi-bridge-sender.ts +130 -0
  48. package/scripts/check-pi-mcp-bridge.ts +251 -0
  49. package/scripts/fixtures/herdr-supply.json +8 -8
  50. package/scripts/lib/codex-fresh-live-protocol.ts +17 -5
  51. package/scripts/lib/codex-fresh-source-receipts.ts +21 -2
  52. package/scripts/lib/mutation-qualify.ts +269 -44
  53. package/scripts/lib/pi-mcp-bridge-probe.mjs +170 -0
  54. package/scripts/meta-bridge-doctor.sh +16 -5
  55. package/scripts/mutants/acp-foreground-only.json +17 -0
  56. package/scripts/mutants/acp-prompt-lifecycle.json +2 -2
  57. package/scripts/mutants/acp-vendor-rejection.json +62 -0
  58. package/scripts/mutants/codex-native.json +11 -11
  59. package/scripts/mutants/control-send-receipt.json +6 -4
  60. package/scripts/mutants/copilot-birth.json +2 -1
  61. package/scripts/mutants/fresh-call-dispatch.json +3 -14
  62. package/scripts/mutants/gate-qualification.json +59 -0
  63. package/scripts/mutants/herdr-plugin-build.json +11 -0
  64. package/scripts/mutants/herdr-supply.json +3 -3
  65. package/scripts/mutants/mux-fresh-call.json +43 -45
  66. package/scripts/mutants/mux-resume-call.json +11 -35
  67. package/scripts/mutants/pack-install.json +15 -4
  68. package/scripts/mutants/pi-bridge-sender.json +54 -0
  69. package/scripts/mutants/pi-floor.json +6 -2
  70. package/scripts/mutants/pi-mcp-bridge.json +36 -0
  71. package/scripts/mutants/pi-mcp-register.json +51 -0
  72. package/scripts/mutants/v2-surface.json +6 -6
  73. package/scripts/resident-rpc-drive.ts +5 -1
  74. package/scripts/smoke-acp-bundled-mcp-live.ts +3 -1
  75. package/scripts/smoke-acp-cortex-live.ts +3 -1
  76. package/scripts/smoke-acp-raw-turn-live.ts +1 -1
  77. package/scripts/smoke-acp-session-reuse-live.ts +29 -12
  78. package/scripts/smoke-acp-socket-citizen-live.ts +3 -1
  79. package/scripts/smoke-acp-v2-send-live.ts +3 -1
  80. package/scripts/smoke-codex-fresh-live.ts +20 -1
  81. package/scripts/smoke-entwurf-chain-live.ts +6 -2
  82. package/scripts/smoke-entwurf-v2-matrix-live.ts +3 -1
  83. package/scripts/smoke-herdr-fresh-call-live.ts +21 -7
  84. package/scripts/smoke-herdr-raw-install-live.sh +1 -1
  85. package/scripts/smoke-mux-lifecycle-live.ts +1 -1
  86. package/scripts/smoke-resident-garden-guard.sh +3 -1
@@ -21,6 +21,7 @@
21
21
  import { createRequire } from "node:module";
22
22
  import { homedir } from "node:os";
23
23
  import { fileURLToPath } from "node:url";
24
+ import { RequestError } from "@agentclientprotocol/sdk";
24
25
 
25
26
  import type { AcpConnectionLike } from "./acp-client.js";
26
27
  import { enrichMcpServersWithEnvelope, type ResolvedAcpConfig } from "./config.js";
@@ -235,14 +236,73 @@ export interface AcpBackendAdapter {
235
236
  * entwurf actually reads from a NOTIFICATION cannot desync from a response
236
237
  * shape it never inspects. */
237
238
  sealsTurnAccounting?: true;
239
+
240
+ /** OPTIONAL capability (#127): recognise a typed vendor rejection of `session/prompt`
241
+ * whose raw text must not become pi's `errorMessage`. pi decides a whole-prompt replay
242
+ * from `errorMessage` alone, and a vendor's "Internal error" — or any digit run in the
243
+ * ids it carries — reads as transient, so a turn whose tools already ran would be resent
244
+ * from a cold session. A recognised rejection returns a fixed, authored verdict for
245
+ * `errorMessage`; backend.ts keeps the raw rejection as a pi diagnostic instead.
246
+ *
247
+ * ABSENT means every failure of this backend keeps the verbatim-first `errorMessage`.
248
+ * The vocabulary is the vendor's, so only the adapter that speaks it may declare it. */
249
+ readPromptRejection?(err: unknown): AcpPromptRejection | undefined;
238
250
  }
239
251
 
252
+ /** A vendor rejection an adapter recognised (`readPromptRejection`). */
253
+ export interface AcpPromptRejection {
254
+ /** The vendor's own failure kind, as it named it on the wire. */
255
+ errorKind: string;
256
+ /** Entwurf-authored and fixed — no vendor text, ids or numbers: this is what pi classifies. */
257
+ verdict: string;
258
+ /** The JSON-RPC `data`, re-serialized as evidence (it arrived as parsed JSON). */
259
+ dataJson: string;
260
+ }
261
+
262
+ /** The pi diagnostic type under which backend.ts preserves a recognised rejection verbatim. */
263
+ export const ACP_PROMPT_REJECTION_DIAGNOSTIC = "entwurf_acp_prompt_rejection";
264
+
265
+ /** The verdict for claude-agent-acp's `incomplete_tool_call` (#127; upstream #1212, 0.85.0+). */
266
+ export const INCOMPLETE_TOOL_CALL_VERDICT =
267
+ "ACP turn failed because foreground tool results are incomplete. Some tools may already have run; automatic replay is unsafe. Inspect the preserved vendor rejection before continuing.";
268
+
240
269
  // ---------------------------------------------------------------------------
241
270
  // claude adapter — the first implementation (the rail's reference backend)
242
271
  // ---------------------------------------------------------------------------
243
272
 
244
273
  const SUPPORTED_CLAUDE_IDS: ReadonlySet<string> = new Set(SUPPORTED_ANTHROPIC_MODEL_IDS);
245
274
 
275
+ /** JSON-RPC internal error — the code claude-agent-acp rejects an unfinished-tool turn with. */
276
+ const JSONRPC_INTERNAL_ERROR = -32603;
277
+ /** claude-agent-acp's own kind for a turn that hit `end_turn` with foreground tools unanswered
278
+ * (read at v0.85.1 `src/acp-agent.ts:4286-4340`, `errorKindData("incomplete_tool_call")`). */
279
+ const CLAUDE_INCOMPLETE_TOOL_CALL = "incomplete_tool_call";
280
+
281
+ /**
282
+ * The claude reader for `readPromptRejection` (#127). Only the root wire SDK's own decode
283
+ * counts: its client rejects a JSON-RPC error response with `new RequestError(code, message,
284
+ * data)`, `data` being the parsed `error.data` (sdk 1.4.0 `dist/jsonrpc.js:842-844`). A
285
+ * look-alike object, another code, or another `errorKind` is not this failure and keeps the
286
+ * verbatim-first path.
287
+ */
288
+ function readClaudePromptRejection(err: unknown): AcpPromptRejection | undefined {
289
+ if (!(err instanceof RequestError) || err.code !== JSONRPC_INTERNAL_ERROR) return undefined;
290
+ const data = err.data;
291
+ if (typeof data !== "object" || data === null || !("errorKind" in data)) return undefined;
292
+ if (data.errorKind !== CLAUDE_INCOMPLETE_TOOL_CALL) return undefined;
293
+ return { errorKind: CLAUDE_INCOMPLETE_TOOL_CALL, verdict: INCOMPLETE_TOOL_CALL_VERDICT, dataJson: jsonText(data) };
294
+ }
295
+
296
+ /** Evidence text for a wire value. A value that came off the wire serializes; one that cannot
297
+ * (a hand-built fake) is named as such, so recording evidence never breaks the turn's seal. */
298
+ function jsonText(value: unknown): string {
299
+ try {
300
+ return JSON.stringify(value) ?? String(value);
301
+ } catch (err) {
302
+ return `[unserializable: ${err instanceof Error ? err.name : typeof err}]`;
303
+ }
304
+ }
305
+
246
306
  /**
247
307
  * Resolve the claude launch — an ENTWURF-OWNED launcher, or the env override for debug.
248
308
  * This is the single source for the claude launch spec; backend.ts holds no private copy.
@@ -267,6 +327,18 @@ function resolveClaudeLaunch(): AcpLaunchSpec {
267
327
  return { command: process.execPath, args: [launcher] };
268
328
  }
269
329
 
330
+ /**
331
+ * The claude child runs with Claude Code's own background-task switch on (#125): the vendor drops
332
+ * `run_in_background` from the Bash/Agent schemas it declares to the model and turns off timeout and
333
+ * turn-abort backgrounding. Without it a vendor-backgrounded command runs in its own session (setsid),
334
+ * outlives an ACP cancel, and is orphaned to init when the child exits — the process-group teardown
335
+ * cannot reach it (`[측정 2026-10-02]` claude-agent-sdk 0.3.274 / claude-agent-acp 0.79.0). A process a
336
+ * command detaches on its own is outside the switch. It rides
337
+ * the claude adapter's launch env, which backend.ts spreads over `process.env` at spawn, so an
338
+ * ambient value cannot turn it off. Cortex declares nothing of the kind.
339
+ */
340
+ export const CLAUDE_FOREGROUND_ONLY_ENV = { CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" } as const;
341
+
270
342
  export const claudeAdapter: AcpBackendAdapter = {
271
343
  backend: "claude",
272
344
 
@@ -292,7 +364,7 @@ export const claudeAdapter: AcpBackendAdapter = {
292
364
  },
293
365
 
294
366
  launchEnvDefaults() {
295
- return claudeLaunchEnvDefaults();
367
+ return { ...claudeLaunchEnvDefaults(), ...CLAUDE_FOREGROUND_ONLY_ENV };
296
368
  },
297
369
 
298
370
  ensureOverlay() {
@@ -347,6 +419,10 @@ export const claudeAdapter: AcpBackendAdapter = {
347
419
  // The token partition is NOT declared here: ACP's only token carrier is a
348
420
  // per-turn round-trip aggregate, which is not what pi's four fields mean.
349
421
  sealsTurnAccounting: true,
422
+
423
+ // #127: claude-agent-acp 0.85.0+ rejects an `end_turn` with unanswered foreground tools as
424
+ // -32603 `incomplete_tool_call` (upstream #1212). Its raw text reads as transient to pi.
425
+ readPromptRejection: readClaudePromptRejection,
350
426
  };
351
427
 
352
428
  // ---------------------------------------------------------------------------
@@ -515,6 +591,10 @@ export const cortexAdapter: AcpBackendAdapter = {
515
591
  // backend.ts seals nothing for a backend without this flag, so cortex's
516
592
  // emitted usage is byte-identical to what it was before #93. Declaring this
517
593
  // with a guess would mint exactly the silent misaccounting #93 exists to end.
594
+
595
+ // NO readPromptRejection — also deliberate. Nobody has read which failures cortex's ACP
596
+ // server rejects a prompt with, so every cortex failure keeps the verbatim-first
597
+ // errorMessage. Borrowing claude's `incomplete_tool_call` reader would be a guess.
518
598
  };
519
599
 
520
600
  const ADAPTERS: readonly AcpBackendAdapter[] = [claudeAdapter, cortexAdapter];
@@ -45,7 +45,12 @@ import { readFileSync } from "node:fs";
45
45
  import { Readable, Writable } from "node:stream";
46
46
  import { ndJsonStream, PROTOCOL_VERSION } from "@agentclientprotocol/sdk";
47
47
  import type { Api, AssistantMessage, Model, SimpleStreamOptions, TranscriptContext } from "@earendil-works/pi-ai";
48
- import { createAssistantMessageEventStream, getCurrentTools } from "@earendil-works/pi-ai";
48
+ import {
49
+ appendAssistantMessageDiagnostic,
50
+ createAssistantMessageDiagnostic,
51
+ createAssistantMessageEventStream,
52
+ getCurrentTools,
53
+ } from "@earendil-works/pi-ai";
49
54
  import {
50
55
  type AcpClientHandlers,
51
56
  type AcpConnectionLike,
@@ -53,7 +58,11 @@ import {
53
58
  connectAcpClient,
54
59
  } from "./acp-client.js";
55
60
  import { prependNewPromptAugment } from "./augment.js";
56
- import { type AcpBackendAdapter, resolveAcpBackendAdapter } from "./backend-adapter.js";
61
+ import {
62
+ ACP_PROMPT_REJECTION_DIAGNOSTIC,
63
+ type AcpBackendAdapter,
64
+ resolveAcpBackendAdapter,
65
+ } from "./backend-adapter.js";
57
66
  import {
58
67
  enrichMcpServersWithEnvelope,
59
68
  mcpServerNames,
@@ -1435,13 +1444,18 @@ export function streamAcpTurn(
1435
1444
  stderrTail?: string[],
1436
1445
  lifecycle?: string,
1437
1446
  launchObservation?: AcpLaunchObservation,
1447
+ /** Passed only from the two prompt-phase catches, where the turn's adapter is resolved. */
1448
+ adapter?: AcpBackendAdapter,
1438
1449
  ): void {
1439
1450
  finalizeAcpStreamState(state);
1440
1451
  state.output.stopReason = aborted ? "aborted" : "error";
1441
1452
  const base = err instanceof Error ? err.message : String(err);
1442
1453
  // The FIRST failure stays first and verbatim (it is what the backend
1443
1454
  // actually said); the lifecycle line is added, never substituted, so a
1444
- // reader can still match the transport's own text.
1455
+ // reader can still match the transport's own text. One typed exception
1456
+ // below: a vendor rejection the adapter recognises keeps that verbatim text
1457
+ // as a pi diagnostic instead, because pi reads errorMessage to decide a
1458
+ // whole-prompt replay (#127).
1445
1459
  const withLifecycle = lifecycle ? `${base}\n${lifecycle}` : base;
1446
1460
  // Its OWN line, above the vendor tail: an entwurf-owned observation must not
1447
1461
  // have to be recovered by reading vendor prose.
@@ -1453,7 +1467,28 @@ export function streamAcpTurn(
1453
1467
  // overflow gets an actionable hint appended, so "API Error" stops hiding
1454
1468
  // the turn-scoped full-transcript-replay cause.
1455
1469
  const hint = aborted ? undefined : actionableAcpBackendHint(full);
1456
- state.output.errorMessage = hint ? `${full}\n\n${hint}` : full;
1470
+ // #127: pi's retry and overflow classifiers read `errorMessage` ALONE, and a
1471
+ // vendor's "Internal error" or any digit run in its ids/stderr reads as transient
1472
+ // — a replay of a turn whose tools already ran. A rejection the adapter names gets
1473
+ // its fixed verdict there; every raw part rides one diagnostic, appended after any
1474
+ // existing ones. An abort keeps the abort path.
1475
+ const rejection = aborted ? undefined : adapter?.readPromptRejection?.(err);
1476
+ if (rejection) {
1477
+ appendAssistantMessageDiagnostic(
1478
+ state.output,
1479
+ createAssistantMessageDiagnostic(ACP_PROMPT_REJECTION_DIAGNOSTIC, err, {
1480
+ errorKind: rejection.errorKind,
1481
+ dataJson: rejection.dataJson,
1482
+ ...(lifecycle ? { lifecycle } : {}),
1483
+ ...(observed ? { launchSignal: observed } : {}),
1484
+ ...(tail ? { stderrTail: tail } : {}),
1485
+ ...(hint ? { hint } : {}),
1486
+ }),
1487
+ );
1488
+ state.output.errorMessage = rejection.verdict;
1489
+ } else {
1490
+ state.output.errorMessage = hint ? `${full}\n\n${hint}` : full;
1491
+ }
1457
1492
  stream.push({ type: "error", reason: aborted ? "aborted" : "error", error: state.output });
1458
1493
  stream.end();
1459
1494
  }
@@ -1915,6 +1950,7 @@ export function streamAcpTurn(
1915
1950
  session?.stderrTail ?? stderrTail,
1916
1951
  lifecycle,
1917
1952
  session?.launchObservation ?? launchObservation,
1953
+ adapter,
1918
1954
  );
1919
1955
  // error/abort → drop the (uncertain) session and close its child; an
1920
1956
  // uncertain connection must never be reused (GPT ④).
@@ -1982,7 +2018,7 @@ export function streamAcpTurn(
1982
2018
  // Without the session-scoped tail a mid-turn child death on a resident
1983
2019
  // session surfaced as a bare "ACP connection closed" with nothing to read
1984
2020
  // it by.
1985
- finishError(err, aborted, session.stderrTail, lifecycle, session.launchObservation);
2021
+ finishError(err, aborted, session.stderrTail, lifecycle, session.launchObservation, adapter);
1986
2022
  // error/abort on a reused session → drop it and close the child (GPT ④).
1987
2023
  session.retiring = true;
1988
2024
  retainedChildren.delete(session.child);
@@ -102,8 +102,8 @@ export interface EntwurfFactsDeps {
102
102
  /** Socket axis: injected into scanSocketProbes (controlDir/readdir/probe). */
103
103
  socket?: Partial<SocketScanDeps>;
104
104
  /** Observation axis (#101): per-citizen receiver + transcript facts. Defaults to the
105
- * REAL measurement — the same seam shape `makeProductionEntwurfV2Deps` uses, so the two
106
- * wiring sites (MCP + pi-native) cannot drift by each passing their own observer, and a
105
+ * REAL measurement — the same seam shape `makeProductionEntwurfV2Deps` uses, so a wiring
106
+ * site cannot drift by passing its own observer (one site, the bridge, since #125), and a
107
107
  * gate still drives the assembly with a fake and no filesystem. */
108
108
  observe?: PeerObserver;
109
109
  /** Placement axis (#116 S1): the ONE placement-owner read this listing is allowed.
@@ -0,0 +1,74 @@
1
+ /**
2
+ * entwurf-mcp-server — how a pi citizen hands its Entwurf verbs to Pi's built-in MCP (#125).
3
+ *
4
+ * WHY THE VERBS ARE NOT NATIVE TOOLS ANY MORE. Pi 0.99 ships MCP as a built-in extension
5
+ * (`builtin:mcp`): it spawns stdio servers, owns their lifecycle, and closes them on
6
+ * `session_shutdown`. The same compiled `entwurf-bridge` every other harness already reaches is
7
+ * therefore the one surface a pi session needs too — the five hand-written `registerTool` copies
8
+ * that duplicated it are gone, and so is the drift they invited. Pi, not Entwurf, supervises the
9
+ * child: nothing here starts, restarts or stops a process.
10
+ *
11
+ * WHAT THIS MODULE DECIDES, AND NOTHING ELSE:
12
+ * - the server NAME. `entwurf-bridge` is the key every other host already uses. Pi sanitizes it
13
+ * into the call name, so a pi model calls `mcp__entwurf_bridge__<verb>` (`[측정 2026-10-01,
14
+ * pi 0.99.2]` `extensions/mcp/tools.ts:82-90`: everything outside `[A-Za-z0-9_]` becomes `_`).
15
+ * The key itself is unchanged. It is a call-name prefix, never an address: the record is the
16
+ * only address axis.
17
+ * - the ENTRY. The COMPILED bridge (`scripts/build-bridge.sh` output) that a packaged consumer
18
+ * receives next to `pi-extensions/`. There is no TypeScript fallback: a missing build is a
19
+ * named failure, not a silent switch to a different artifact.
20
+ * - the IDENTITY carrier. `PI_SESSION_ID` / `PI_AGENT_ID` are passed EXPLICITLY in the server
21
+ * config from the record this session was just born with. Pi merges a stdio child's env as
22
+ * `{...process.env, ...config.env}`, so the explicit value wins over whatever the process
23
+ * happens to carry — the carrier is the record-established identity by construction, not by
24
+ * the timing of an ambient export.
25
+ * - the EXPOSURE. `direct`: the verbs are declared to the model like built-in tools. Two bridge
26
+ * tools have no use on a pi host and are hidden: `entwurf_inbox_read` drains a caller-supplied
27
+ * mailbox (a pi citizen has no mailbox), and `entwurf_register_native` binds a NATIVE harness
28
+ * session (a pi citizen is born by this extension).
29
+ *
30
+ * A same-namespace entry in the operator's or a trusted project's `mcp.json` — `entwurf-bridge` or
31
+ * `entwurf_bridge`, which Pi folds to one namespace — replaces this registration by Pi's own
32
+ * precedence, and `/mcp` says so. That is the operator's choice and
33
+ * Pi's contract; this module does not scan config, fight it, or add a second owner.
34
+ */
35
+
36
+ import * as path from "node:path";
37
+ import type { McpServerConfig } from "@earendil-works/pi-coding-agent";
38
+
39
+ /** The built-in MCP server key. Pi sanitizes it into the call name (`mcp__entwurf_bridge__<verb>`); never an address. */
40
+ export const ENTWURF_MCP_SERVER_NAME = "entwurf-bridge";
41
+
42
+ /** Bridge tools with no use on a pi host (see the module header). */
43
+ export const ENTWURF_MCP_HIDDEN_TOOLS = ["entwurf_inbox_read", "entwurf_register_native"] as const;
44
+
45
+ /**
46
+ * The compiled bridge entry, resolved from the directory of the extension file that loads it.
47
+ * Checkout and packaged install share this layout: `pi-extensions/` beside `mcp/entwurf-bridge/dist/`.
48
+ */
49
+ export function entwurfBridgeCompiledEntry(extensionDir: string): string {
50
+ return path.resolve(extensionDir, "..", "mcp", "entwurf-bridge", "dist", "mcp", "entwurf-bridge", "src", "index.js");
51
+ }
52
+
53
+ export interface EntwurfMcpServerInput {
54
+ /** The record gardenId this session was just born with. */
55
+ gardenId: string;
56
+ /** `<provider>/<model>` of the live session, when known. */
57
+ agentId: string | undefined;
58
+ /** Absolute path of the compiled bridge entry. */
59
+ compiledEntry: string;
60
+ /** The node binary running pi. */
61
+ nodePath: string;
62
+ }
63
+
64
+ /** The one server config a born pi citizen registers. Pure: no IO, no env reads. */
65
+ export function buildEntwurfMcpServerConfig(input: EntwurfMcpServerInput): McpServerConfig {
66
+ if (!input.gardenId) throw new Error("entwurf-mcp-server: a server config needs the born record's gardenId");
67
+ return {
68
+ command: input.nodePath,
69
+ args: [input.compiledEntry],
70
+ env: { PI_SESSION_ID: input.gardenId, ...(input.agentId ? { PI_AGENT_ID: input.agentId } : {}) },
71
+ exposure: "direct",
72
+ toolExposure: Object.fromEntries(ENTWURF_MCP_HIDDEN_TOOLS.map((tool) => [tool, "hidden"])),
73
+ };
74
+ }
@@ -3,7 +3,7 @@
3
3
  * surface (0.11 Stage 0 step 4, slice 4c). The MCP handler stays thin: it does IO
4
4
  * (readdir the meta-store + probe sockets via `listEntwurfFacts`) and then calls
5
5
  * THIS to shape the text + JSON. No IO here, so the gate drives it without a
6
- * filesystem and the SAME facts can feed pi-native / doctor / v2 dispatch later
6
+ * filesystem and the SAME facts can feed the bridge / doctor / v2 dispatch
7
7
  * (a handler that did its own brain work would deny them that reuse).
8
8
  *
9
9
  * Two hard rules carried from the frozen contract (동결결정 10 + #50 C4):
@@ -22,7 +22,7 @@
22
22
  *
23
23
  * The frozen 7-step order (NEXT.md "통합 decider 순서"):
24
24
  * 1. requireGardenId — runtime guard BEFORE any path is built (F2-P1; closes the
25
- * MCP-schema bypass for pi-native/internal callers).
25
+ * MCP-schema bypass for internal callers).
26
26
  * 2. resolveTarget — no citizen → bad-target; a record-less control socket →
27
27
  * record-less-socket (#50 C4: the record is the sole address authority);
28
28
  * quarantined (out-of-socket-domain record sharing a socket/symlink) → target-address-conflict.
@@ -227,7 +227,7 @@ export interface DispatchDeciderDeps {
227
227
 
228
228
  /**
229
229
  * F2-P1 defense in depth: never build a lock/socket path from an unvalidated gid.
230
- * The MCP TypeBox pattern guards that one surface; a pi-native / internal caller
230
+ * The MCP schema pattern guards that one surface; an internal caller
231
231
  * bypasses the schema, so the decider re-validates as its very first step.
232
232
  */
233
233
  function requireGardenId(target: string): string {
@@ -77,8 +77,8 @@ export function executeMetaMailboxSend(
77
77
  * Build the production `sendViaMailbox(plan, lock)` adapter the 5c-2a send hand consumes.
78
78
  * It IGNORES `lock` entirely (release is the hand's job; a mailbox plan is lock-free) and
79
79
  * wraps the sync body in the async dep signature. `senderProvider` is supplied by the
80
- * wiring site — it calls the private `buildLocalSenderEnvelope(ctx)` and decorates
81
- * origin/replyable — so this module never imports ExtensionContext. `enqueue` defaults to
80
+ * wiring site — the bridge's authoritative sender resolution (the one caller-side
81
+ * surface since #125) — so this module never imports ExtensionContext. `enqueue` defaults to
82
82
  * the real `enqueueMetaMessage`; the gate injects a fake.
83
83
  */
84
84
  export function makeProductionSendViaMailbox(opts: {
@@ -2,8 +2,8 @@
2
2
  * entwurf-v2-production — 5d-2b: the ctx-free PRODUCTION assembly of `runEntwurfV2`'s deps.
3
3
  * `makeProductionEntwurfV2Deps(opts)` wires the real 5b decider IO seams + the three 5c
4
4
  * transport hands into one `EntwurfV2RunDeps` (`{decide, executor}`). It touches NO
5
- * `ExtensionContext`/`ExtensionAPI` — the wiring SITE (5d-3, entwurf-control.ts) builds the
6
- * `senderProvider` from `buildLocalSenderEnvelope(ctx)` and passes it in, so this module
5
+ * `ExtensionContext`/`ExtensionAPI` — the wiring SITE (the entwurf-bridge `entwurf_v2`
6
+ * handler; the pi-native one left with #125) builds the `senderProvider` and passes it in, so this module
7
7
  * stays in `lib/` with the rest of the gate-/smoke-testable v2 core.
8
8
  *
9
9
  * Three invariants this factory exists to guarantee (each gate-proven):
@@ -136,7 +136,7 @@ export interface ProductionEntwurfV2Seams {
136
136
  }
137
137
 
138
138
  export interface ProductionEntwurfV2Opts {
139
- /** Built at the wiring site from `buildLocalSenderEnvelope(ctx)`, decorated with its
139
+ /** Built at the wiring site (the bridge's authoritative sender resolution), carrying its
140
140
  * HONEST pi-session replyability (SE-1 2e-a: `replyable` reflects whether the canonical
141
141
  * control socket actually exists, not a hardcoded true). ONE provider feeds ALL THREE
142
142
  * rails — the control-socket RPC sender, the meta-mailbox body sender, and the
@@ -1,11 +1,9 @@
1
1
  /**
2
2
  * entwurf-v2-surface — 5d-3a: the ctx-free SURFACE adapter that joins a surface's raw params
3
3
  * to `runEntwurfV2` and renders the outcome-rich result back to a human/tool string. It is the
4
- * ONE place a surface (pi-native `entwurf_v2` tool now; the MCP `entwurf_v2` verb in 5d-3b)
5
- * crosses into the v2 fence — so `entwurf-control.ts` (a root-tsc, emit-capable surface that
6
- * CANNOT statically import the `.ts`-extension fence without TS5097) reaches this via a
7
- * NON-LITERAL dynamic import, and the MCP bridge (already a `.ts`-import consumer) imports it
8
- * directly. Either way the ctx binding stays OUT of the fence: the caller builds
4
+ * ONE place a surface (the MCP `entwurf_v2` verb — the only one since #125 retired the
5
+ * pi-native copy) crosses into the v2 fence; the MCP bridge (a `.ts`-import consumer) imports
6
+ * it directly. Either way the ctx binding stays OUT of the fence: the caller builds
9
7
  * `senderProvider` from its own envelope source and passes it in `opts`.
10
8
  *
11
9
  * Three exports:
@@ -329,9 +329,9 @@ const REJECT_HINT: Record<VisibleResumeRejectReason, string> = {
329
329
  };
330
330
 
331
331
  /**
332
- * ONE renderer for both surfaces — the same reason `mux-fresh-call` has one: the two
333
- * registrations are separate literals, and a shared renderer is what keeps the operator-visible
334
- * answer from drifting apart between native pi and the MCP bridge.
332
+ * ONE renderer — the same reason `mux-fresh-call` has one: it keeps the operator-visible answer
333
+ * out of the registration literal. (Since #125 the MCP bridge is the only registration; the
334
+ * native pi copy it once had to agree with is retired.)
335
335
  *
336
336
  * The success text keeps the two receipts visibly apart, including in the good case. A reader
337
337
  * who sees "window opened" and "socket alive" as one sentence has been told the resume worked;
@@ -33,9 +33,16 @@ export const FRESH_CALL_BACKENDS = ["pi", "claude-code", "copilot", "omp", "code
33
33
  export type FreshCallBackend = (typeof FRESH_CALL_BACKENDS)[number];
34
34
 
35
35
  /**
36
- * The callback tool NAME differs per backend and that is not cosmetic: native pi exposes the
37
- * capability directly (`entwurf_v2`), while an MCP-hosted session reaches it under whatever
38
- * name that harness composes. Naming the wrong one costs the whole first turn.
36
+ * The callback tool NAME differs per backend and that is not cosmetic: every session reaches the
37
+ * capability under whatever name its harness composes for the `entwurf-bridge` MCP server, and
38
+ * naming the wrong one costs the whole first turn.
39
+ *
40
+ * `[측정 2026-10-01, pi 0.99.2]` pi reaches the verbs through its BUILT-IN MCP (#125 — the native
41
+ * `registerTool` copies are gone), and Pi composes `mcp__<server>__<tool>` replacing everything
42
+ * outside `[A-Za-z0-9_]` with `_` (`extensions/mcp/tools.ts:82-90`, tag v0.99.2): the server key
43
+ * stays `entwurf-bridge`, but the model-facing names are `mcp__entwurf_bridge__entwurf_*` — the
44
+ * same spelling Codex composes, and NOT Claude Code's. 0.99.1 kept the hyphen; the floor moved to
45
+ * 0.99.2 as a hard cut — one literal pi dialect, chosen over an alias or a version router.
39
46
  *
40
47
  * `[측정]` Copilot CLI 1.0.80 composes `<mcpServerName>-<mcpToolName>` — NOT Claude Code's
41
48
  * `mcp__<server>__<tool>`. Read from two independent sessions' own event logs
@@ -54,7 +61,7 @@ export type FreshCallBackend = (typeof FRESH_CALL_BACKENDS)[number];
54
61
  * permission dialect: omp's approval layer consults the same minted string (`source-audit.md`).
55
62
  */
56
63
  export const FRESH_CALL_CALLBACK_TOOL: Record<FreshCallBackend, string> = {
57
- pi: "entwurf_callback",
64
+ pi: "mcp__entwurf_bridge__entwurf_callback",
58
65
  "claude-code": "mcp__entwurf-bridge__entwurf_callback",
59
66
  copilot: "entwurf-bridge-entwurf_callback",
60
67
  omp: "mcp__entwurf_bridge_entwurf_callback",
@@ -65,7 +72,7 @@ export const FRESH_CALL_CALLBACK_TOOL: Record<FreshCallBackend, string> = {
65
72
 
66
73
  /** Where a sibling SENDS the task result. Always the delivery verb, never the birth callback. */
67
74
  export const FRESH_CALL_DELIVERY_TOOL: Record<FreshCallBackend, string> = {
68
- pi: "entwurf_v2",
75
+ pi: "mcp__entwurf_bridge__entwurf_v2",
69
76
  "claude-code": "mcp__entwurf-bridge__entwurf_v2",
70
77
  copilot: "entwurf-bridge-entwurf_v2",
71
78
  omp: "mcp__entwurf_bridge_entwurf_v",
@@ -87,7 +94,7 @@ export const FRESH_CALL_DELIVERY_TOOL: Record<FreshCallBackend, string> = {
87
94
  * SAME rule produces the whole word here.
88
95
  */
89
96
  export const FRESH_CALL_PEERS_TOOL: Record<FreshCallBackend, string> = {
90
- pi: "entwurf_peers",
97
+ pi: "mcp__entwurf_bridge__entwurf_peers",
91
98
  "claude-code": "mcp__entwurf-bridge__entwurf_peers",
92
99
  copilot: "entwurf-bridge-entwurf_peers",
93
100
  omp: "mcp__entwurf_bridge_entwurf_peers",
@@ -3,7 +3,7 @@
3
3
  * meta-bridge mailbox body. Every transport that can deliver to a garden citizen
4
4
  * with no live control socket uses this:
5
5
  * - the MCP bridge entwurf_v2 (mcp/entwurf-bridge) — external/Claude-host sends
6
- * - the pi-native entwurf_v2 (pi-extensions/entwurf-control.ts) — pi-session sends
6
+ * (pi-session sends included since #125 — a pi session reaches that same verb)
7
7
  * - the native-push rail (pi-extensions/lib/entwurf-v2-native-push.ts) — direct
8
8
  * injection into an Antigravity conversation or a loaded Codex thread. Its
9
9
  * THIRD consumer, added after the #95 A LIVE run measured what an envelope-less
@@ -659,9 +659,9 @@ const REJECT_HINT: Record<FreshCallRejectReason, string> = {
659
659
  };
660
660
 
661
661
  /**
662
- * ONE renderer for both surfaces. Not a convenience: the two registrations are separate literals
663
- * (that is this repo's shape), so a shared renderer is what keeps the operator-visible answer
664
- * from drifting apart between native pi and the MCP bridge.
662
+ * ONE renderer, outside the registration. Not a convenience: it keeps the operator-visible answer
663
+ * out of a registration literal. (Since #125 the MCP bridge is the only registration; the native
664
+ * pi copy it once had to agree with is retired.)
665
665
  *
666
666
  * The success text states the boundary out loud. A caller that reads "launched" and assumes
667
667
  * "delivered" is the exact confusion the two-receipt split exists to prevent, so the text refuses