@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.
- package/AGENTS.md +1 -1
- package/BASELINE.md +1 -1
- package/CHANGELOG.md +190 -0
- package/DELIVERY.md +1 -1
- package/README.md +183 -674
- package/VERIFY.md +21 -17
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +115 -18
- package/docs/external-mcp-host.md +1 -1
- package/docs/setup-clean-host.md +346 -10
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +27 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +55 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +2 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-mailbox.js +2 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +2 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +3 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +3 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +13 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-mailbox-body.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +3 -3
- package/mcp/entwurf-bridge/src/index.ts +30 -3
- package/package.json +13 -13
- package/pi-extensions/entwurf-control.ts +50 -637
- package/pi-extensions/lib/acp/backend-adapter.ts +81 -1
- package/pi-extensions/lib/acp/backend.ts +41 -5
- package/pi-extensions/lib/entwurf-fact-provider.ts +2 -2
- package/pi-extensions/lib/entwurf-mcp-server.ts +74 -0
- package/pi-extensions/lib/entwurf-peers-render.ts +1 -1
- package/pi-extensions/lib/entwurf-v2-decider.ts +2 -2
- package/pi-extensions/lib/entwurf-v2-mailbox.ts +2 -2
- package/pi-extensions/lib/entwurf-v2-production.ts +3 -3
- package/pi-extensions/lib/entwurf-v2-surface.ts +3 -5
- package/pi-extensions/lib/entwurf-v2-visible-resume.ts +3 -3
- package/pi-extensions/lib/fresh-call-composition.ts +13 -6
- package/pi-extensions/lib/meta-mailbox-body.ts +1 -1
- package/pi-extensions/lib/mux-fresh-call.ts +3 -3
- package/run.sh +177 -53
- package/scripts/check-entwurf-peers-surface.ts +8 -15
- package/scripts/check-entwurf-self-address.ts +7 -11
- package/scripts/check-entwurf-v2-surface.ts +20 -112
- package/scripts/check-fresh-call-dispatch.ts +12 -13
- package/scripts/check-gate-qualification.ts +250 -5
- package/scripts/check-herdr-plugin-build.ts +159 -57
- package/scripts/check-install-surface.ts +3 -2
- package/scripts/check-mux-resume-call.ts +24 -35
- package/scripts/check-pi-bridge-sender.ts +130 -0
- package/scripts/check-pi-mcp-bridge.ts +251 -0
- package/scripts/fixtures/herdr-supply.json +8 -8
- package/scripts/lib/codex-fresh-live-protocol.ts +17 -5
- package/scripts/lib/codex-fresh-source-receipts.ts +21 -2
- package/scripts/lib/mutation-qualify.ts +269 -44
- package/scripts/lib/pi-mcp-bridge-probe.mjs +170 -0
- package/scripts/meta-bridge-doctor.sh +16 -5
- package/scripts/mutants/acp-foreground-only.json +17 -0
- package/scripts/mutants/acp-prompt-lifecycle.json +2 -2
- package/scripts/mutants/acp-vendor-rejection.json +62 -0
- package/scripts/mutants/codex-native.json +11 -11
- package/scripts/mutants/control-send-receipt.json +6 -4
- package/scripts/mutants/copilot-birth.json +2 -1
- package/scripts/mutants/fresh-call-dispatch.json +3 -14
- package/scripts/mutants/gate-qualification.json +59 -0
- package/scripts/mutants/herdr-plugin-build.json +11 -0
- package/scripts/mutants/herdr-supply.json +3 -3
- package/scripts/mutants/mux-fresh-call.json +43 -45
- package/scripts/mutants/mux-resume-call.json +11 -35
- package/scripts/mutants/pack-install.json +15 -4
- package/scripts/mutants/pi-bridge-sender.json +54 -0
- package/scripts/mutants/pi-floor.json +6 -2
- package/scripts/mutants/pi-mcp-bridge.json +36 -0
- package/scripts/mutants/pi-mcp-register.json +51 -0
- package/scripts/mutants/v2-surface.json +6 -6
- package/scripts/resident-rpc-drive.ts +5 -1
- package/scripts/smoke-acp-bundled-mcp-live.ts +3 -1
- package/scripts/smoke-acp-cortex-live.ts +3 -1
- package/scripts/smoke-acp-raw-turn-live.ts +1 -1
- package/scripts/smoke-acp-session-reuse-live.ts +29 -12
- package/scripts/smoke-acp-socket-citizen-live.ts +3 -1
- package/scripts/smoke-acp-v2-send-live.ts +3 -1
- package/scripts/smoke-codex-fresh-live.ts +20 -1
- package/scripts/smoke-entwurf-chain-live.ts +6 -2
- package/scripts/smoke-entwurf-v2-matrix-live.ts +3 -1
- package/scripts/smoke-herdr-fresh-call-live.ts +21 -7
- package/scripts/smoke-herdr-raw-install-live.sh +1 -1
- package/scripts/smoke-mux-lifecycle-live.ts +1 -1
- 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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
|
106
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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 —
|
|
81
|
-
*
|
|
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 (
|
|
6
|
-
*
|
|
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
|
|
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 (
|
|
5
|
-
* crosses into the v2 fence
|
|
6
|
-
*
|
|
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
|
|
333
|
-
*
|
|
334
|
-
*
|
|
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:
|
|
37
|
-
* capability
|
|
38
|
-
*
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
-
*
|
|
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
|
|
663
|
-
*
|
|
664
|
-
*
|
|
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
|