@junghanacs/entwurf 0.25.1 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/BASELINE.md +1 -1
- package/CHANGELOG.md +73 -0
- package/DELIVERY.md +1 -1
- package/README.md +178 -674
- package/VERIFY.md +16 -15
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +6 -2
- 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 +12 -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 +11 -11
- package/pi-extensions/entwurf-control.ts +50 -637
- package/pi-extensions/lib/acp/backend-adapter.ts +13 -1
- 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 +163 -50
- 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 +248 -4
- 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/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/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/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-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-raw-install-live.sh +1 -1
- package/scripts/smoke-mux-lifecycle-live.ts +1 -1
- package/scripts/smoke-resident-garden-guard.sh +3 -1
|
@@ -22,12 +22,15 @@
|
|
|
22
22
|
* commands.
|
|
23
23
|
*
|
|
24
24
|
* Features:
|
|
25
|
-
* - Register the canonical `entwurf_v2` dispatch tool for existing garden citizens.
|
|
26
|
-
* - Expose `entwurf_peers` facts for operator inspection (#50 C4: the socket-scan
|
|
27
|
-
* `/entwurf-sessions` command is gone — the record listing is the only surface).
|
|
28
|
-
* - Maintain the resident control socket used by the v2 live-send path.
|
|
29
25
|
* - Attach this pi session to its meta-record at session_start (#50 C2) and key
|
|
30
26
|
* the control socket on the record's gardenId.
|
|
27
|
+
* - Maintain the resident control socket used by the v2 live-send path (the
|
|
28
|
+
* INBOUND half: a send lands here and enters this session through pi.sendMessage).
|
|
29
|
+
* - Hand the Entwurf verbs (`entwurf_v2`, `entwurf_peers`, `entwurf_fresh_call`,
|
|
30
|
+
* `entwurf_resume_call`, `entwurf_callback`, `entwurf_self`) to Pi's built-in MCP by
|
|
31
|
+
* registering the compiled entwurf-bridge with the born gardenId as its explicit
|
|
32
|
+
* identity carrier (#125). The model calls them as `mcp__entwurf_bridge__<verb>`;
|
|
33
|
+
* there are no native copies (see lib/entwurf-mcp-server.ts).
|
|
31
34
|
*
|
|
32
35
|
* Send-is-throw still applies at the control-socket protocol layer: a `send` RPC
|
|
33
36
|
* ack confirms the receiver enqueued the message (`message_processed` semantics)
|
|
@@ -65,22 +68,10 @@ import { existsSync, promises as fs } from "node:fs";
|
|
|
65
68
|
import * as net from "node:net";
|
|
66
69
|
import * as os from "node:os";
|
|
67
70
|
import * as path from "node:path";
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
// directly mixes typebox 0.34 (Type.*) with typebox 1.x (StringEnum, TSchema
|
|
71
|
-
// inside pi-coding-agent), which silently widens StringEnum-typed parameters
|
|
72
|
-
// to `unknown` and broke renderCall/execute narrowing. Single-source-of-typebox.
|
|
73
|
-
import { StringEnum, type TextContent, Type } from "@earendil-works/pi-ai";
|
|
74
|
-
import type {
|
|
75
|
-
AgentToolResult,
|
|
76
|
-
AgentToolUpdateCallback,
|
|
77
|
-
ExtensionAPI,
|
|
78
|
-
ExtensionContext,
|
|
79
|
-
MessageRenderer,
|
|
80
|
-
} from "@earendil-works/pi-coding-agent";
|
|
71
|
+
import type { TextContent } from "@earendil-works/pi-ai";
|
|
72
|
+
import type { ExtensionAPI, ExtensionContext, MessageRenderer } from "@earendil-works/pi-coding-agent";
|
|
81
73
|
import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
|
|
82
74
|
import { Box, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
83
|
-
import { readCallbackEnv } from "./lib/callback-env.js";
|
|
84
75
|
import {
|
|
85
76
|
type CompactionGuard,
|
|
86
77
|
compactionSendReject,
|
|
@@ -89,15 +80,19 @@ import {
|
|
|
89
80
|
noteCompactionTerminal,
|
|
90
81
|
} from "./lib/compaction-send-guard.js";
|
|
91
82
|
import { sendReceiptBoundary } from "./lib/control-send-receipt.js";
|
|
92
|
-
import { CONTROL_SOCKET_SUFFIX,
|
|
83
|
+
import { CONTROL_SOCKET_SUFFIX, defaultControlSocketDir } from "./lib/control-socket-path.js";
|
|
93
84
|
import {
|
|
94
85
|
attachAcceptedSocketDisconnectPolicy,
|
|
95
86
|
formatSenderInfoBlock,
|
|
96
87
|
type RpcCommand,
|
|
97
88
|
type RpcResponse,
|
|
98
|
-
type SenderEnvelope,
|
|
99
89
|
} from "./lib/entwurf-control-rpc.js";
|
|
100
90
|
import { computeResidentStatusLabel } from "./lib/entwurf-core.js";
|
|
91
|
+
import {
|
|
92
|
+
buildEntwurfMcpServerConfig,
|
|
93
|
+
ENTWURF_MCP_SERVER_NAME,
|
|
94
|
+
entwurfBridgeCompiledEntry,
|
|
95
|
+
} from "./lib/entwurf-mcp-server.js";
|
|
101
96
|
import { probeSocketLiveness, shouldUnlinkOnGc } from "./lib/socket-probe.js";
|
|
102
97
|
|
|
103
98
|
// The `--entwurf-control` socket protocol (wire types + the newline-JSON client) now lives
|
|
@@ -148,14 +143,6 @@ function isErrnoException(error: unknown): error is NodeJS.ErrnoException {
|
|
|
148
143
|
return typeof error === "object" && error !== null && "code" in error;
|
|
149
144
|
}
|
|
150
145
|
|
|
151
|
-
function getSocketPath(sessionId: string): string {
|
|
152
|
-
return controlSocketPathIn(ENTWURF_DIR, sessionId);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function isSafeSessionId(sessionId: string): boolean {
|
|
156
|
-
return !sessionId.includes("/") && !sessionId.includes("\\") && !sessionId.includes("..") && sessionId.length > 0;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
146
|
async function ensureControlDir(): Promise<void> {
|
|
160
147
|
await fs.mkdir(ENTWURF_DIR, { recursive: true });
|
|
161
148
|
}
|
|
@@ -399,38 +386,6 @@ function parseSenderInfo(text: string): SenderInfo | null {
|
|
|
399
386
|
return null;
|
|
400
387
|
}
|
|
401
388
|
|
|
402
|
-
// Build a sender envelope for messages originating from the local pi session.
|
|
403
|
-
// Used by every caller-side send path (the mcp + pi-native entwurf_v2
|
|
404
|
-
// surfaces). Returns undefined when any field
|
|
405
|
-
// cannot be resolved — pi-native callers should fall back to body-less sends
|
|
406
|
-
// rather than synthesize partial envelopes that would render as "(unknown ...)"
|
|
407
|
-
// at the receiver. The MCP-side bridge (mcp/entwurf-bridge entwurf_v2) is
|
|
408
|
-
// strict — it throws when its own env wiring is incomplete — because it
|
|
409
|
-
// represents the public transparency contract.
|
|
410
|
-
//
|
|
411
|
-
// agentId preference order:
|
|
412
|
-
// 1. PI_AGENT_ID env (set by updateSessionEnv as `<ctx.model.provider>/<ctx.model.id>`)
|
|
413
|
-
// 2. `<ctx.model.provider>/<ctx.model.id>` reconstructed from the live pi context
|
|
414
|
-
// 3. undefined → envelope omitted
|
|
415
|
-
function buildLocalSenderEnvelope(ctx: ExtensionContext): SenderEnvelope | undefined {
|
|
416
|
-
// The GARDEN address, not pi's session id (#50 C2): a peer replies to what it
|
|
417
|
-
// reads here, and only the record gardenId is routable.
|
|
418
|
-
const sessionId = residentGardenId;
|
|
419
|
-
if (!sessionId) return undefined;
|
|
420
|
-
const cwd = ctx.cwd;
|
|
421
|
-
if (!cwd) return undefined;
|
|
422
|
-
const envAgent = process.env.PI_AGENT_ID?.trim();
|
|
423
|
-
const ctxAgent = ctx.model?.provider && ctx.model?.id ? `${ctx.model.provider}/${ctx.model.id}` : undefined;
|
|
424
|
-
const agentId = envAgent && envAgent.length > 0 ? envAgent : ctxAgent;
|
|
425
|
-
if (!agentId) return undefined;
|
|
426
|
-
return {
|
|
427
|
-
sessionId,
|
|
428
|
-
agentId,
|
|
429
|
-
cwd,
|
|
430
|
-
timestamp: new Date().toISOString(),
|
|
431
|
-
};
|
|
432
|
-
}
|
|
433
|
-
|
|
434
389
|
// Format a UTC ISO timestamp as `YYYY-MM-DD HH:MM:SS KST`. We avoid pulling
|
|
435
390
|
// Intl into the hot render path — it's heavy and locale-fragile — and instead
|
|
436
391
|
// compute KST manually (UTC+9, no DST). Returns the raw input unchanged when
|
|
@@ -663,7 +618,7 @@ async function handleCommand(
|
|
|
663
618
|
const wantsReply = typeof command.wants_reply === "boolean" ? command.wants_reply : false;
|
|
664
619
|
|
|
665
620
|
// Synthesize <sender_info> JSON at the receiver side. Caller code paths
|
|
666
|
-
// (entwurf-bridge entwurf_v2
|
|
621
|
+
// (entwurf-bridge entwurf_v2 — the one caller-side surface since #125)
|
|
667
622
|
// pass the envelope structurally and never touch the message body — the
|
|
668
623
|
// canonical XML-style payload is the shared formatSenderInfoBlock SSOT
|
|
669
624
|
// (#50 C3: this is the ONE renderer since the visible-first cut removed the
|
|
@@ -880,11 +835,30 @@ function updateSessionEnv(ctx: ExtensionContext | null, enabled: boolean, garden
|
|
|
880
835
|
}
|
|
881
836
|
}
|
|
882
837
|
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
838
|
+
/**
|
|
839
|
+
* Hand this born citizen's verbs to Pi's built-in MCP (#125): the compiled entwurf-bridge, with the
|
|
840
|
+
* record gardenId as its EXPLICIT identity carrier. Pi owns the child from here — it spawns it,
|
|
841
|
+
* and closes it on session_shutdown — so there is deliberately no unregister anywhere in this file.
|
|
842
|
+
* Re-registering on the next session_start (/new, resume, reload) replaces this registration with
|
|
843
|
+
* the NEW record's id. A missing compiled entry is a named failure: there is no TypeScript fallback.
|
|
844
|
+
*/
|
|
845
|
+
function registerEntwurfMcpServer(pi: ExtensionAPI, ctx: ExtensionContext, gardenId: string): void {
|
|
846
|
+
const compiledEntry = entwurfBridgeCompiledEntry(import.meta.dirname);
|
|
847
|
+
if (!existsSync(compiledEntry)) {
|
|
848
|
+
throw new Error(
|
|
849
|
+
`[entwurf-control] compiled entwurf-bridge missing at ${compiledEntry} — build it (\`pnpm run build-bridge\`); ` +
|
|
850
|
+
"the pi session keeps its garden address and control socket but has no Entwurf verbs until then",
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
pi.registerMcpServer(
|
|
854
|
+
ENTWURF_MCP_SERVER_NAME,
|
|
855
|
+
buildEntwurfMcpServerConfig({
|
|
856
|
+
gardenId,
|
|
857
|
+
agentId: ctx.model?.provider && ctx.model?.id ? `${ctx.model.provider}/${ctx.model.id}` : undefined,
|
|
858
|
+
compiledEntry,
|
|
859
|
+
nodePath: process.execPath,
|
|
860
|
+
}),
|
|
861
|
+
);
|
|
888
862
|
}
|
|
889
863
|
|
|
890
864
|
// Read a string-valued CLI flag straight from argv. Handles `--flag value` and
|
|
@@ -962,10 +936,6 @@ async function noticeUncitizenedSession(ctx: ExtensionContext, controlEnabled: b
|
|
|
962
936
|
);
|
|
963
937
|
}
|
|
964
938
|
|
|
965
|
-
function shouldRegisterControlTools(pi: ExtensionAPI): boolean {
|
|
966
|
-
return pi.getFlag(ENTWURF_FLAG) === true || wasBooleanFlagPassed(ENTWURF_FLAG);
|
|
967
|
-
}
|
|
968
|
-
|
|
969
939
|
// ============================================================================
|
|
970
940
|
// Extension Export
|
|
971
941
|
// ============================================================================
|
|
@@ -988,14 +958,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
988
958
|
|
|
989
959
|
pi.registerMessageRenderer(SESSION_MESSAGE_TYPE, renderSessionMessage);
|
|
990
960
|
|
|
991
|
-
if (shouldRegisterControlTools(pi)) {
|
|
992
|
-
registerListSessionsTool(pi);
|
|
993
|
-
registerEntwurfV2Tool(pi);
|
|
994
|
-
registerCallbackTool(pi);
|
|
995
|
-
registerFreshCallTool(pi);
|
|
996
|
-
registerResumeCallTool(pi);
|
|
997
|
-
}
|
|
998
|
-
|
|
999
961
|
// The in-process mint refusals (`/new`, `/fork`, `/clone`, RPC new_session) are
|
|
1000
962
|
// GONE with the id grammar they defended (#50 C2). They existed because pi mints a
|
|
1001
963
|
// uuidv7 for an in-process session and the garden guard hard-exited on a non-garden
|
|
@@ -1056,8 +1018,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
1056
1018
|
// store to warn about; the store is either clean or this session has no
|
|
1057
1019
|
// address.
|
|
1058
1020
|
await startControlServer(pi, state, ctx, birth.socketPath);
|
|
1059
|
-
|
|
1021
|
+
// Identity before UI (#125): the carrier env and the MCP registration that carries the
|
|
1022
|
+
// born gardenId are both made before any status rendering, so a visual failure can never
|
|
1023
|
+
// leave a born citizen without its identity carrier or its verbs.
|
|
1060
1024
|
updateSessionEnv(ctx, true, birth.gardenId);
|
|
1025
|
+
registerEntwurfMcpServer(pi, ctx, birth.gardenId);
|
|
1026
|
+
updateStatus(ctx, true, birth.gardenId);
|
|
1061
1027
|
};
|
|
1062
1028
|
|
|
1063
1029
|
/** Upsert the record for the CURRENT pi session. Reached through the non-literal
|
|
@@ -1143,27 +1109,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1143
1109
|
}
|
|
1144
1110
|
|
|
1145
1111
|
// ============================================================================
|
|
1146
|
-
//
|
|
1112
|
+
// Fence seams
|
|
1147
1113
|
// ============================================================================
|
|
1148
|
-
//
|
|
1149
|
-
//
|
|
1150
|
-
//
|
|
1151
|
-
//
|
|
1152
|
-
// NON-LITERAL dynamic import: root tsc cannot statically resolve a string-const
|
|
1153
|
-
// specifier, and the strip-types runtime loads the `.ts` fence entry fine. The
|
|
1154
|
-
// local interface below is the only contract this file knows about the fence — the
|
|
1155
|
-
// `EntwurfV2RunResult` union stays behind `runAndRenderEntwurfV2FromSurface`, which
|
|
1156
|
-
// hands back just `{ text, isError }`.
|
|
1157
|
-
const ENTWURF_V2_SURFACE_MODULE = "./lib/entwurf-v2-surface.ts";
|
|
1158
|
-
|
|
1159
|
-
// SE-1 (slice 2e-a): a pi-session sender's `replyable` is a FACT (does its canonical
|
|
1160
|
-
// control socket actually exist?), not env presence. entwurf-self-address.ts is a
|
|
1161
|
-
// `.ts`-extension fence lib, so this root-tsc emit surface reaches it the SAME way as the
|
|
1162
|
-
// v2 surface / mailbox guard — a NON-LITERAL dynamic import behind a local interface.
|
|
1114
|
+
// entwurf-self-address.ts is a `.ts`-extension fence lib, so this root-tsc emit surface reaches
|
|
1115
|
+
// it through a NON-LITERAL dynamic import behind a local interface (a static `.ts` import would be
|
|
1116
|
+
// TS5097). Only the not-a-citizen notice decision is consumed here; a pi sender's replyability is
|
|
1117
|
+
// the bridge's to compute now that the verbs live there (#125).
|
|
1163
1118
|
const ENTWURF_SELF_ADDRESS_MODULE = "./lib/entwurf-self-address.ts";
|
|
1164
|
-
const ENTWURF_FACT_PROVIDER_MODULE = "./lib/entwurf-fact-provider.ts";
|
|
1165
|
-
const ENTWURF_PEERS_RENDER_MODULE = "./lib/entwurf-peers-render.ts";
|
|
1166
|
-
const META_SESSION_MODULE = "./lib/meta-session.ts";
|
|
1167
1119
|
// The #50 C2 attach seam. Same fence, same reason: it is a lib→lib VALUE importer
|
|
1168
1120
|
// (upsertMetaSession) carrying an explicit `.ts` extension, which the emit-capable
|
|
1169
1121
|
// root program cannot resolve — so it is reached by a NON-LITERAL dynamic import.
|
|
@@ -1189,545 +1141,6 @@ interface PiCitizenBirthModule {
|
|
|
1189
1141
|
}): PiCitizenBirth;
|
|
1190
1142
|
}
|
|
1191
1143
|
|
|
1192
|
-
type SelfAddressabilityFn = (facts: {
|
|
1193
|
-
origin: "pi-session" | "meta-session" | "external-mcp";
|
|
1194
|
-
socketAlive?: boolean;
|
|
1195
|
-
socketPathComputable?: boolean;
|
|
1196
|
-
recordBacked?: boolean;
|
|
1197
|
-
ownerAlive?: boolean;
|
|
1198
|
-
watchArmed?: boolean;
|
|
1199
|
-
}) => { replyable: boolean; socketState: "alive" | "expected" | "none"; reason: string };
|
|
1200
|
-
|
|
1201
1144
|
interface EntwurfSelfAddressModule {
|
|
1202
|
-
computeSelfAddressability: SelfAddressabilityFn;
|
|
1203
1145
|
decideUncitizenedNotice: (facts: { controlEnabled: boolean; hasUI: boolean; alreadyShown: boolean }) => boolean;
|
|
1204
1146
|
}
|
|
1205
|
-
|
|
1206
|
-
/**
|
|
1207
|
-
* Decorate a local pi sender envelope with its HONEST replyability (SE-1 slice 2e-a).
|
|
1208
|
-
* The old code hardcoded replyability to true from env presence; a pi session running
|
|
1209
|
-
* without --entwurf-control has a session id but no control socket, so a reply silently fails.
|
|
1210
|
-
* Route the v2 senderProvider through the shared computeSelfAddressability truth table:
|
|
1211
|
-
* replyable ⟺ the canonical socket exists (existsSync
|
|
1212
|
-
* — slice-1 level, NOT a listener probe; deeper liveness is a separate hardening slice).
|
|
1213
|
-
*/
|
|
1214
|
-
function decoratePiSenderAddressability(sender: SenderEnvelope, compute: SelfAddressabilityFn): SenderEnvelope {
|
|
1215
|
-
const self = compute({
|
|
1216
|
-
origin: "pi-session",
|
|
1217
|
-
socketAlive: existsSync(getSocketPath(sender.sessionId)),
|
|
1218
|
-
socketPathComputable: true,
|
|
1219
|
-
});
|
|
1220
|
-
return { ...sender, origin: "pi-session", replyable: self.replyable };
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
interface EntwurfV2SurfaceModule {
|
|
1224
|
-
runAndRenderEntwurfV2FromSurface(
|
|
1225
|
-
params: {
|
|
1226
|
-
target: string;
|
|
1227
|
-
intent: "fire-and-forget";
|
|
1228
|
-
mode?: "steer" | "follow_up";
|
|
1229
|
-
wants_reply?: boolean;
|
|
1230
|
-
message: string;
|
|
1231
|
-
},
|
|
1232
|
-
opts: { senderProvider: () => SenderEnvelope | undefined },
|
|
1233
|
-
): Promise<{ text: string; isError: boolean }>;
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
function registerEntwurfV2Tool(pi: ExtensionAPI): void {
|
|
1237
|
-
const entwurfV2Parameters = Type.Object({
|
|
1238
|
-
target: Type.String({ description: "Target garden id (use entwurf_peers to discover)" }),
|
|
1239
|
-
intent: StringEnum(["fire-and-forget"] as const, {
|
|
1240
|
-
description:
|
|
1241
|
-
"fire-and-forget = send/reply/hand-off to a LIVE socket target (currently backend pi) or to any " +
|
|
1242
|
-
"citizen with no socket liveness — the decider picks that citizen's rail, and a rail can also " +
|
|
1243
|
-
"REJECT: self-fetch (e.g. Claude Code) → meta-bridge mailbox while that mailbox is DELIVERABLE, " +
|
|
1244
|
-
"else rejected as mailbox-undeliverable; native-push (e.g. Antigravity) → direct injection into " +
|
|
1245
|
-
"its live conversation, which has NO mailbox, and its probe is three-valued — alive: injected, " +
|
|
1246
|
-
"dead: native-push-target-dead, indeterminate: native-push-probe-indeterminate (two rejects, " +
|
|
1247
|
-
"not one). " +
|
|
1248
|
-
"Set wants_reply for an answer. " +
|
|
1249
|
-
"This is the ONLY intent. The second one, owned-outcome, resumed a dormant citizen by launching " +
|
|
1250
|
-
"a hidden background child and was withdrawn under the visible-first rule; it is not selectable, " +
|
|
1251
|
-
"not deprecated-but-tolerated, and a dormant citizen is currently unreachable by this verb and " +
|
|
1252
|
-
"rejects as dormant-fire-forget-unsupported.",
|
|
1253
|
-
}),
|
|
1254
|
-
message: Type.String({
|
|
1255
|
-
description:
|
|
1256
|
-
"Message / prompt to dispatch. Hard cap 16000 chars; for larger payloads send a file/artifact path plus digest.",
|
|
1257
|
-
maxLength: 16000,
|
|
1258
|
-
}),
|
|
1259
|
-
mode: Type.Optional(
|
|
1260
|
-
StringEnum(["steer", "follow_up"] as const, {
|
|
1261
|
-
description:
|
|
1262
|
-
"Injection style for a CONTROL-SOCKET send only: steer (ask the receiver's steering queue) or follow_up (ask its follow-up queue). NEITHER is an interrupt: a busy receiver drains steering after each turn and follow-ups only when its inner loop ends, so a later steer overtakes every earlier follow_up and there is no order between the two. Both queues are volatile process memory — an abort drops what is in them. The receipt names which queue accepted the message (`queued-steer` / `queued-follow-up`); an idle receiver answers `sent`, meaning the turn was TRIGGERED, not that the model has seen the text. " +
|
|
1263
|
-
"The mailbox and native-push plans carry no mode, so it has no effect on those rails.",
|
|
1264
|
-
}),
|
|
1265
|
-
),
|
|
1266
|
-
wants_reply: Type.Optional(Type.Boolean({ description: "Human-conversation reply hint (default false)" })),
|
|
1267
|
-
});
|
|
1268
|
-
|
|
1269
|
-
type EntwurfV2Params = {
|
|
1270
|
-
target: string;
|
|
1271
|
-
intent: "fire-and-forget";
|
|
1272
|
-
message: string;
|
|
1273
|
-
mode?: "steer" | "follow_up";
|
|
1274
|
-
wants_reply?: boolean;
|
|
1275
|
-
};
|
|
1276
|
-
|
|
1277
|
-
// TS2589 ("type instantiation is excessively deep") workaround: pi's registerTool
|
|
1278
|
-
// generic infers the handler signature from the TypeBox schema, and this schema is
|
|
1279
|
-
// deep enough to blow the instantiation budget. Casting the FUNCTION (not the
|
|
1280
|
-
// argument) keeps the schema itself typed while stopping the inference walk.
|
|
1281
|
-
// Revisit when pi's registerTool takes an explicit params type parameter.
|
|
1282
|
-
const registerTool = pi.registerTool as (def: any) => void;
|
|
1283
|
-
|
|
1284
|
-
registerTool({
|
|
1285
|
-
name: "entwurf_v2",
|
|
1286
|
-
label: "Dispatch (v2)",
|
|
1287
|
-
description: `CANONICAL DELIVERY SURFACE for garden ids: message, reply, or hand off to whoever an id names. The id alone
|
|
1288
|
-
does not say which rail that citizen answers on. Give target + intent; the decider picks transport from
|
|
1289
|
-
liveness (live socket citizen → control-socket send; deliverable self-fetch citizen → meta-bridge mailbox;
|
|
1290
|
-
probe-alive native-push citizen → direct injection into its conversation) and reports ONE outcome, per rail: a control
|
|
1291
|
-
receiver's acceptance boundary ("sent" / "queued-steer" / "queued-follow-up" /
|
|
1292
|
-
"accepted-unknown-boundary" — those four only), a mailbox ENQUEUE receipt, a native-push INJECTION,
|
|
1293
|
-
a rejection, or lock-dirty. EXISTING targets only (see entwurf_peers).
|
|
1294
|
-
A peer shown as liveness=alive → fire-and-forget. A citizen with NO socket liveness
|
|
1295
|
-
(liveness=unsupported) is ALSO fire-and-forget — unsupported means only "no control-socket probe" — and the
|
|
1296
|
-
decider picks its own rail: a self-fetch backend (e.g. Claude Code) gets the mailbox, a native-push backend
|
|
1297
|
-
(e.g. Antigravity) gets direct injection and has NO mailbox at all. THERE IS A THIRD RESULT: the mailbox
|
|
1298
|
-
delivers only to a DELIVERABLE citizen, so a terminated self-fetch session is mailbox-undeliverable
|
|
1299
|
-
rather than queued for an inbox nobody drains. The native-push probe is 3-valued:
|
|
1300
|
-
alive → injected; dead → native-push-target-dead; indeterminate → native-push-probe-indeterminate
|
|
1301
|
-
(unestablished ≠ gone). DORMANT IS UNREACHABLE: a socket-domain citizen that is not running gets
|
|
1302
|
-
dormant-fire-forget-unsupported — same receiver rule as the mailbox, no active drainer means no
|
|
1303
|
-
delivery. The intent that used to answer there, owned-outcome, was withdrawn
|
|
1304
|
-
under the visible-first rule — re-open the session yourself and dispatch again. LOCK: taken for a control-socket-DOMAIN dispatch. The mailbox and native-push
|
|
1305
|
-
rails are lock-free — deliverability and the adapter probe guard them. mode applies to a CONTROL-SOCKET
|
|
1306
|
-
send only; other plans carry no mode. wants_reply rides every rail. message caps at 16000 chars; send an
|
|
1307
|
-
artifact path + digest for more.`,
|
|
1308
|
-
parameters: entwurfV2Parameters,
|
|
1309
|
-
async execute(
|
|
1310
|
-
_toolCallId: string,
|
|
1311
|
-
params: EntwurfV2Params,
|
|
1312
|
-
_signal: AbortSignal | undefined,
|
|
1313
|
-
_onUpdate: unknown,
|
|
1314
|
-
ctx: ExtensionContext,
|
|
1315
|
-
) {
|
|
1316
|
-
const target = params.target?.trim();
|
|
1317
|
-
if (!target) {
|
|
1318
|
-
return { content: [{ type: "text", text: "entwurf_v2: missing target" }], isError: true };
|
|
1319
|
-
}
|
|
1320
|
-
if (!isSafeSessionId(target)) {
|
|
1321
|
-
return { content: [{ type: "text", text: "entwurf_v2: invalid target garden id" }], isError: true };
|
|
1322
|
-
}
|
|
1323
|
-
if (!params.message?.trim()) {
|
|
1324
|
-
return { content: [{ type: "text", text: "entwurf_v2: missing message" }], isError: true };
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
try {
|
|
1328
|
-
// The reply-address envelope for this pi session, decorated with its HONEST
|
|
1329
|
-
// replyability (SE-1 2e-a: socket existsSync, not a hardcoded true). ONE provider
|
|
1330
|
-
// feeds the control-socket RPC sender AND the meta-mailbox body sender (see
|
|
1331
|
-
// entwurf-v2-production). Built per-call so the timestamp is the dispatch moment;
|
|
1332
|
-
// computeSelfAddressability is loaded once here (sync senderProvider can't await).
|
|
1333
|
-
const selfMod = (await import(ENTWURF_SELF_ADDRESS_MODULE)) as unknown as EntwurfSelfAddressModule;
|
|
1334
|
-
const senderProvider = (): SenderEnvelope | undefined => {
|
|
1335
|
-
const s = buildLocalSenderEnvelope(ctx);
|
|
1336
|
-
return s ? decoratePiSenderAddressability(s, selfMod.computeSelfAddressability) : undefined;
|
|
1337
|
-
};
|
|
1338
|
-
const mod = (await import(ENTWURF_V2_SURFACE_MODULE)) as unknown as EntwurfV2SurfaceModule;
|
|
1339
|
-
const rendered = await mod.runAndRenderEntwurfV2FromSurface(
|
|
1340
|
-
{
|
|
1341
|
-
target,
|
|
1342
|
-
intent: params.intent,
|
|
1343
|
-
message: params.message,
|
|
1344
|
-
mode: params.mode,
|
|
1345
|
-
wants_reply: params.wants_reply,
|
|
1346
|
-
},
|
|
1347
|
-
// No trust-preflight inputs are passed, and none exist to pass: the preflight on this
|
|
1348
|
-
// path guarded the resume verdict, so it left with `owned-outcome`. `senderProvider`
|
|
1349
|
-
// is the whole options surface now — do NOT re-add an `ENTWURF_PREFIX_ROOTS` fallback
|
|
1350
|
-
// here without a verdict that reads it (nothing on the dispatch path does).
|
|
1351
|
-
{ senderProvider },
|
|
1352
|
-
);
|
|
1353
|
-
return {
|
|
1354
|
-
content: [{ type: "text", text: rendered.text }],
|
|
1355
|
-
isError: rendered.isError,
|
|
1356
|
-
details: { isError: rendered.isError },
|
|
1357
|
-
};
|
|
1358
|
-
} catch (err) {
|
|
1359
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1360
|
-
return {
|
|
1361
|
-
content: [{ type: "text", text: `entwurf_v2 error: ${msg}` }],
|
|
1362
|
-
isError: true,
|
|
1363
|
-
details: { error: msg },
|
|
1364
|
-
};
|
|
1365
|
-
}
|
|
1366
|
-
},
|
|
1367
|
-
});
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
// ============================================================================
|
|
1371
|
-
// Tool: entwurf_peers
|
|
1372
|
-
// ============================================================================
|
|
1373
|
-
|
|
1374
|
-
interface EntwurfFactProviderModule {
|
|
1375
|
-
listEntwurfFacts(params: {
|
|
1376
|
-
metaEntries: readonly { filename: string; regularFile: boolean }[];
|
|
1377
|
-
readRecord: (filename: string) => string;
|
|
1378
|
-
socket: { dir: string };
|
|
1379
|
-
observationLimit: number;
|
|
1380
|
-
}): Promise<unknown>;
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
interface EntwurfPeersRenderModule {
|
|
1384
|
-
ENTWURF_PEERS_RENDER_LIMIT: number;
|
|
1385
|
-
renderEntwurfPeers(result: unknown): { text: string; payload: unknown };
|
|
1386
|
-
}
|
|
1387
|
-
|
|
1388
|
-
interface MetaSessionModule {
|
|
1389
|
-
defaultMetaSessionsDir(): string;
|
|
1390
|
-
readActiveStoreEntries(dir: string): { filename: string; regularFile: boolean }[];
|
|
1391
|
-
makeStoreRecordReader(dir: string): (filename: string) => string;
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
|
-
async function renderEntwurfPeersForSurface(): Promise<{ text: string; payload: unknown }> {
|
|
1395
|
-
const meta = (await import(META_SESSION_MODULE)) as unknown as MetaSessionModule;
|
|
1396
|
-
const sessionsDir = meta.defaultMetaSessionsDir();
|
|
1397
|
-
// Entries carry their KIND. The name-only readdir this used to do left `readRecord`
|
|
1398
|
-
// free to follow a symlinked `.meta.json` into bytes the store does not own — rule 1
|
|
1399
|
-
// held in the doctor and not on the surface operators actually read (pre-existing;
|
|
1400
|
-
// surfaced by the #52 duplicate pass, which would let such a symlink quarantine the
|
|
1401
|
-
// healthy record it shadowed).
|
|
1402
|
-
const provider = (await import(ENTWURF_FACT_PROVIDER_MODULE)) as unknown as EntwurfFactProviderModule;
|
|
1403
|
-
const render = (await import(ENTWURF_PEERS_RENDER_MODULE)) as unknown as EntwurfPeersRenderModule;
|
|
1404
|
-
const result = await provider.listEntwurfFacts({
|
|
1405
|
-
metaEntries: meta.readActiveStoreEntries(sessionsDir),
|
|
1406
|
-
readRecord: meta.makeStoreRecordReader(sessionsDir),
|
|
1407
|
-
// Same socket axis as the legacy live-session scan, but merged with the
|
|
1408
|
-
// meta-record rail by listEntwurfFacts so meta-mailbox citizens are discoverable too.
|
|
1409
|
-
socket: { dir: ENTWURF_DIR },
|
|
1410
|
-
// #112: observation follows the human render budget. The full machine payload
|
|
1411
|
-
// and every authority/diagnostic pass above remain complete.
|
|
1412
|
-
observationLimit: render.ENTWURF_PEERS_RENDER_LIMIT,
|
|
1413
|
-
});
|
|
1414
|
-
return render.renderEntwurfPeers(result);
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
function registerListSessionsTool(pi: ExtensionAPI): void {
|
|
1418
|
-
// Same TS2589 workaround as registerEntwurfV2Tool — see the comment block there
|
|
1419
|
-
// for the revisit conditions. (It used to point at registerSessionTool, which was
|
|
1420
|
-
// removed in the 0.12 cutover, so the pointer dangled.)
|
|
1421
|
-
const registerTool = pi.registerTool as (def: any) => void;
|
|
1422
|
-
registerTool({
|
|
1423
|
-
name: "entwurf_peers",
|
|
1424
|
-
label: "List Garden Citizens",
|
|
1425
|
-
description:
|
|
1426
|
-
"List the entwurf fact surface: garden citizens from meta-records (including active self-fetch meta receivers such as claude-code) with liveness, plus diagnostics. The record is the sole address axis (#50 C4) — a control socket no record claims surfaces as a record-less-socket diagnostic, never a peer row. Pair with entwurf_v2 to address a peer by garden id; this surface reports facts, never per-row routing verbs. A `dead` row is a REPORTED FACT and nothing more: that citizen is dormant and is currently unreachable by any verb, so listing it grants no action — appearing here is not an invitation to dispatch. It is facts-only and creates nothing: to open a NEW sibling use entwurf_fresh_call.",
|
|
1427
|
-
parameters: Type.Object({}),
|
|
1428
|
-
async execute(
|
|
1429
|
-
_toolCallId: string,
|
|
1430
|
-
_params: Record<string, never>,
|
|
1431
|
-
_signal: AbortSignal | undefined,
|
|
1432
|
-
_onUpdate: AgentToolUpdateCallback<unknown> | undefined,
|
|
1433
|
-
_ctx: ExtensionContext,
|
|
1434
|
-
): Promise<AgentToolResult<unknown>> {
|
|
1435
|
-
try {
|
|
1436
|
-
const { text, payload } = await renderEntwurfPeersForSurface();
|
|
1437
|
-
return {
|
|
1438
|
-
content: [{ type: "text", text }],
|
|
1439
|
-
details: payload,
|
|
1440
|
-
};
|
|
1441
|
-
} catch (err) {
|
|
1442
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1443
|
-
return {
|
|
1444
|
-
content: [{ type: "text", text: `entwurf_peers error: ${msg}` }],
|
|
1445
|
-
details: { error: msg },
|
|
1446
|
-
};
|
|
1447
|
-
}
|
|
1448
|
-
},
|
|
1449
|
-
});
|
|
1450
|
-
}
|
|
1451
|
-
|
|
1452
|
-
// ============================================================================
|
|
1453
|
-
// Tool: entwurf_fresh_call
|
|
1454
|
-
// ============================================================================
|
|
1455
|
-
|
|
1456
|
-
// ONE dynamic import, and it resolves the composition root rather than a rail: which rail opens
|
|
1457
|
-
// the sibling, when the Codex preflight runs, and how the receipt renders are decisions this
|
|
1458
|
-
// surface must not own a second copy of (#116 C3). The import stays non-literal-free and lazy for
|
|
1459
|
-
// the same startup-fence reason it always was.
|
|
1460
|
-
const FRESH_CALL_DISPATCH_MODULE = "./lib/fresh-call-dispatch.ts";
|
|
1461
|
-
|
|
1462
|
-
interface FreshCallDispatchModule {
|
|
1463
|
-
dispatchFreshCall(
|
|
1464
|
-
request: {
|
|
1465
|
-
backend: "pi" | "claude-code" | "copilot" | "omp" | "codex";
|
|
1466
|
-
model: string;
|
|
1467
|
-
task: string;
|
|
1468
|
-
cwd?: string;
|
|
1469
|
-
placement?: { tmuxSession: string };
|
|
1470
|
-
callerGardenId: string | null;
|
|
1471
|
-
},
|
|
1472
|
-
env?: NodeJS.ProcessEnv,
|
|
1473
|
-
): Promise<{ rail: "herdr" | "tmux"; result: { ok: boolean } }>;
|
|
1474
|
-
renderDispatchedFreshCall(dispatched: { rail: "herdr" | "tmux"; result: { ok: boolean } }): {
|
|
1475
|
-
text: string;
|
|
1476
|
-
isError: boolean;
|
|
1477
|
-
};
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
/**
|
|
1481
|
-
* The caller identity for this surface is the RESIDENT's garden address — the one the record
|
|
1482
|
-
* minted at session start and keyed the control socket on. It is read from this extension's own
|
|
1483
|
-
* closure, never from a tool parameter and never from `process.env`.
|
|
1484
|
-
*
|
|
1485
|
-
* A caller that could pass an id could pass a WRONG one, and that value would become the address
|
|
1486
|
-
* the sibling calls back to — docs/mux-launch-rail.md §6-b keeps the measured incident. So there
|
|
1487
|
-
* is no parameter to be wrong with.
|
|
1488
|
-
*
|
|
1489
|
-
* A null resident id refuses LOUDLY rather than falling back: without an address to call back to,
|
|
1490
|
-
* the launch would be a window with no way home.
|
|
1491
|
-
*/
|
|
1492
|
-
function registerFreshCallTool(pi: ExtensionAPI): void {
|
|
1493
|
-
// Same TS2589 workaround as registerEntwurfV2Tool — see the comment block there.
|
|
1494
|
-
const registerTool = pi.registerTool as (def: any) => void;
|
|
1495
|
-
registerTool({
|
|
1496
|
-
name: "entwurf_fresh_call",
|
|
1497
|
-
label: "Open Fresh Sibling",
|
|
1498
|
-
description: `Open ONE fresh visible sibling beside you and hand it a first task. WHERE it opens is decided by
|
|
1499
|
-
where THIS agent runs, never by a parameter: inside herdr (HERDR_ENV=1) it opens a NEW UNFOCUSED TAB in your own
|
|
1500
|
-
herdr workspace and only pi and claude-code may be opened; everywhere else it opens in the operator's tmux with all five backends (pi, claude-code,
|
|
1501
|
-
copilot, omp, codex). There is no fallback — an incomplete herdr context is refused by name rather than opening a tmux
|
|
1502
|
-
window you cannot see from in herdr. The sibling's FIRST action is a callback to you carrying a nonce, whose sender
|
|
1503
|
-
envelope is its garden id — that is how you learn the address of a thing that did not exist a moment ago. This returns
|
|
1504
|
-
a LAUNCH receipt (the owner's coordinates plus that nonce) and nothing else: it does NOT mean the runtime started, the
|
|
1505
|
-
first turn ran, or the task was delivered. Those coordinates are a VIEW, never an address — a herdr tab/pane id can
|
|
1506
|
-
change under a running sibling. Nothing polls; if the callback never arrives the tab is visible. For EXISTING citizens use entwurf_v2 — this tool only creates, and entwurf_peers only reports. Model is REQUIRED and passed to the chosen runtime CLI (provider/model for pi, an id/alias for Claude Code, a name or pattern
|
|
1507
|
-
for the rest). On tmux, copilot/omp/codex are refused BEFORE any window opens when their birth, MCP, receive or
|
|
1508
|
-
visible-identity units are absent, and codex also needs the operator-owned app-server socket entwurf never starts; in
|
|
1509
|
-
herdr those three are refused by name first. Optional
|
|
1510
|
-
placement.tmuxSession is a TMUX-ONLY seat naming ONE EXISTING session and ALWAYS wins; omitted, the seat
|
|
1511
|
-
follows the CALLER, and on this surface that is always your own session — a missing one is
|
|
1512
|
-
tmux-session-missing and NOTHING is created. In herdr the field is refused by name. Do not put secrets in the task — model and task argv are visible to same-user processes.`,
|
|
1513
|
-
parameters: Type.Object({
|
|
1514
|
-
backend: StringEnum(["pi", "claude-code", "copilot", "omp", "codex"], {
|
|
1515
|
-
description:
|
|
1516
|
-
"Which fixed runtime to open. Only these five, and only pi/claude-code when this agent runs inside herdr; there is no arbitrary command.",
|
|
1517
|
-
}),
|
|
1518
|
-
model: Type.String({
|
|
1519
|
-
minLength: 1,
|
|
1520
|
-
maxLength: 200,
|
|
1521
|
-
pattern: "^[A-Za-z0-9][A-Za-z0-9._/:\\[\\]-]*$",
|
|
1522
|
-
description:
|
|
1523
|
-
"Required runtime model: canonical provider/model for pi, a Claude Code model id/alias, or a Copilot/OMP/Codex model name.",
|
|
1524
|
-
}),
|
|
1525
|
-
task: Type.String({
|
|
1526
|
-
minLength: 1,
|
|
1527
|
-
maxLength: 16000,
|
|
1528
|
-
description:
|
|
1529
|
-
"What the sibling should do after it calls you back. Plain instructions; no secrets (see the tool description).",
|
|
1530
|
-
}),
|
|
1531
|
-
cwd: Type.Optional(
|
|
1532
|
-
Type.String({
|
|
1533
|
-
description:
|
|
1534
|
-
"Optional literal ABSOLUTE path of an existing directory to start the sibling in (cross-repo fresh). Omit or pass \"\" to start in this agent's own cwd — on BOTH rails. Taken exactly as given: no trim, no realpath, no project-name resolution. '#' is refused on the tmux rail only, because tmux format-expands a start directory; inside herdr it is an ordinary path character. The receipt echoes what was REQUESTED, never an observation.",
|
|
1535
|
-
}),
|
|
1536
|
-
),
|
|
1537
|
-
placement: Type.Optional(
|
|
1538
|
-
Type.Object(
|
|
1539
|
-
{
|
|
1540
|
-
tmuxSession: Type.String({
|
|
1541
|
-
description:
|
|
1542
|
-
"EXACT name of an EXISTING session on this agent's own tmux server. Nothing is created: an absent session is refused as tmux-session-missing, and a name outside [A-Za-z0-9][A-Za-z0-9_-]* as tmux-session-name-invalid.",
|
|
1543
|
-
}),
|
|
1544
|
-
},
|
|
1545
|
-
{
|
|
1546
|
-
description:
|
|
1547
|
-
"Optional expert seat override, TMUX ONLY: open the sibling in ONE EXISTING tmux session of this agent's own server, and it always wins. When omitted the seat follows the CALLER, never the backend being opened: every caller on THIS surface opens in its own session (a pi session is never a Codex citizen, so the Codex caller-pane rule the MCP bridge carries cannot arise here). Nothing is ever created. Inside herdr this field is refused by name — placement there belongs to herdr, and a tmux session name would silently place the sibling somewhere else. Independent of cwd; neither is inferred from the other. The receipt reports the selected name, its source, and resolved target session id.",
|
|
1548
|
-
},
|
|
1549
|
-
),
|
|
1550
|
-
),
|
|
1551
|
-
}),
|
|
1552
|
-
async execute(
|
|
1553
|
-
_toolCallId: string,
|
|
1554
|
-
params: {
|
|
1555
|
-
backend: "pi" | "claude-code" | "copilot" | "omp" | "codex";
|
|
1556
|
-
model: string;
|
|
1557
|
-
task: string;
|
|
1558
|
-
cwd?: string;
|
|
1559
|
-
placement?: { tmuxSession: string };
|
|
1560
|
-
},
|
|
1561
|
-
_signal: AbortSignal | undefined,
|
|
1562
|
-
_onUpdate: unknown,
|
|
1563
|
-
_ctx: ExtensionContext,
|
|
1564
|
-
) {
|
|
1565
|
-
try {
|
|
1566
|
-
const dispatch = (await import(FRESH_CALL_DISPATCH_MODULE)) as unknown as FreshCallDispatchModule;
|
|
1567
|
-
// ONE input object for ONE dispatch call. Rail selection and the Codex preflight
|
|
1568
|
-
// ordering live behind it: duplicating either here would put the same decision in
|
|
1569
|
-
// two places, which is how a mutant that plants a defect in one of them survives
|
|
1570
|
-
// on the other.
|
|
1571
|
-
const dispatched = await dispatch.dispatchFreshCall({
|
|
1572
|
-
backend: params.backend,
|
|
1573
|
-
model: params.model,
|
|
1574
|
-
task: params.task,
|
|
1575
|
-
cwd: params.cwd,
|
|
1576
|
-
placement: params.placement,
|
|
1577
|
-
callerGardenId: residentGardenId,
|
|
1578
|
-
});
|
|
1579
|
-
const rendered = dispatch.renderDispatchedFreshCall(dispatched);
|
|
1580
|
-
return {
|
|
1581
|
-
content: [{ type: "text", text: rendered.text }],
|
|
1582
|
-
isError: rendered.isError,
|
|
1583
|
-
details: { isError: rendered.isError },
|
|
1584
|
-
};
|
|
1585
|
-
} catch (err) {
|
|
1586
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1587
|
-
return {
|
|
1588
|
-
content: [{ type: "text", text: `entwurf_fresh_call error: ${msg}` }],
|
|
1589
|
-
isError: true,
|
|
1590
|
-
details: { error: msg },
|
|
1591
|
-
};
|
|
1592
|
-
}
|
|
1593
|
-
},
|
|
1594
|
-
});
|
|
1595
|
-
}
|
|
1596
|
-
|
|
1597
|
-
// ============================================================================
|
|
1598
|
-
// Tool: entwurf_resume_call
|
|
1599
|
-
// ============================================================================
|
|
1600
|
-
|
|
1601
|
-
const MUX_RESUME_CALL_MODULE = "./lib/mux-resume-call.ts";
|
|
1602
|
-
const V2_VISIBLE_RESUME_MODULE = "./lib/entwurf-v2-visible-resume.ts";
|
|
1603
|
-
|
|
1604
|
-
interface MuxResumeCallModule {
|
|
1605
|
-
resumeCall(
|
|
1606
|
-
params: { cwd: string; runtimeArgs: readonly string[] },
|
|
1607
|
-
env?: NodeJS.ProcessEnv,
|
|
1608
|
-
): { ok: true; receipt: Record<string, string> } | { ok: false; reason: string };
|
|
1609
|
-
RESUME_CALL_REJECT_HINT: Record<string, string>;
|
|
1610
|
-
}
|
|
1611
|
-
|
|
1612
|
-
interface VisibleResumeModule {
|
|
1613
|
-
visibleResume(target: string, deps: unknown): Promise<{ ok: boolean }>;
|
|
1614
|
-
makeVisibleResumeDeps(launch: unknown): unknown;
|
|
1615
|
-
renderVisibleResume(result: { ok: boolean }): { text: string; isError: boolean };
|
|
1616
|
-
}
|
|
1617
|
-
|
|
1618
|
-
function registerCallbackTool(pi: ExtensionAPI): void {
|
|
1619
|
-
const registerTool = pi.registerTool as (def: any) => void;
|
|
1620
|
-
registerTool({
|
|
1621
|
-
name: "entwurf_callback",
|
|
1622
|
-
label: "Fresh Callback",
|
|
1623
|
-
description: `ZERO-ARGUMENT callback for a fresh sibling this process was launched as. Reads ENTWURF_CALLBACK_TARGET and ENTWURF_CALLBACK_NONCE from this process environment (injected by the launcher next to the identity scrub), validates garden-id and nonce grammar, and dispatches through the existing v2 runner with intent fire-and-forget, message=nonce, wants_reply=false. The target is re-resolved by record/decider. REFUSES BY NAME when the pair is absent, malformed, or this process is a Codex-provenance bridge. No arguments, no fallback to a model-supplied target.`,
|
|
1624
|
-
parameters: Type.Object({}),
|
|
1625
|
-
async execute(
|
|
1626
|
-
_toolCallId: string,
|
|
1627
|
-
_params: Record<string, never>,
|
|
1628
|
-
_signal: AbortSignal | undefined,
|
|
1629
|
-
_onUpdate: unknown,
|
|
1630
|
-
ctx: ExtensionContext,
|
|
1631
|
-
) {
|
|
1632
|
-
const read = readCallbackEnv(process.env);
|
|
1633
|
-
if (!read.ok) {
|
|
1634
|
-
return {
|
|
1635
|
-
content: [{ type: "text", text: `entwurf_callback: ${read.reason}` }],
|
|
1636
|
-
isError: true,
|
|
1637
|
-
};
|
|
1638
|
-
}
|
|
1639
|
-
try {
|
|
1640
|
-
const selfMod = (await import(ENTWURF_SELF_ADDRESS_MODULE)) as unknown as EntwurfSelfAddressModule;
|
|
1641
|
-
const senderProvider = (): SenderEnvelope | undefined => {
|
|
1642
|
-
const s = buildLocalSenderEnvelope(ctx);
|
|
1643
|
-
return s ? decoratePiSenderAddressability(s, selfMod.computeSelfAddressability) : undefined;
|
|
1644
|
-
};
|
|
1645
|
-
const mod = (await import(ENTWURF_V2_SURFACE_MODULE)) as unknown as EntwurfV2SurfaceModule;
|
|
1646
|
-
const rendered = await mod.runAndRenderEntwurfV2FromSurface(
|
|
1647
|
-
{
|
|
1648
|
-
target: read.target,
|
|
1649
|
-
intent: "fire-and-forget",
|
|
1650
|
-
message: read.nonce,
|
|
1651
|
-
wants_reply: false,
|
|
1652
|
-
},
|
|
1653
|
-
{ senderProvider },
|
|
1654
|
-
);
|
|
1655
|
-
return {
|
|
1656
|
-
content: [{ type: "text", text: rendered.text }],
|
|
1657
|
-
isError: rendered.isError,
|
|
1658
|
-
details: { isError: rendered.isError },
|
|
1659
|
-
};
|
|
1660
|
-
} catch (err) {
|
|
1661
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1662
|
-
return {
|
|
1663
|
-
content: [{ type: "text", text: `entwurf_callback error: ${msg}` }],
|
|
1664
|
-
isError: true,
|
|
1665
|
-
details: { error: msg },
|
|
1666
|
-
};
|
|
1667
|
-
}
|
|
1668
|
-
},
|
|
1669
|
-
});
|
|
1670
|
-
}
|
|
1671
|
-
|
|
1672
|
-
/**
|
|
1673
|
-
* Unlike `entwurf_fresh_call`, this tool needs NO caller identity: a resume names an existing
|
|
1674
|
-
* citizen, so the address is the parameter rather than something the sibling has to report back.
|
|
1675
|
-
* What it needs instead is the per-gid lock, which is why the composition lives on the v2 side of
|
|
1676
|
-
* the fence and only the LAUNCH is handed across (docs/mux-launch-rail.md §11).
|
|
1677
|
-
*/
|
|
1678
|
-
function registerResumeCallTool(pi: ExtensionAPI): void {
|
|
1679
|
-
// Same TS2589 workaround as registerEntwurfV2Tool — see the comment block there.
|
|
1680
|
-
const registerTool = pi.registerTool as (def: any) => void;
|
|
1681
|
-
registerTool({
|
|
1682
|
-
name: "entwurf_resume_call",
|
|
1683
|
-
label: "Resume Dormant Citizen",
|
|
1684
|
-
description: `Reopen ONE DORMANT pi citizen under its OWN garden id, in a visible window in the operator's own tmux
|
|
1685
|
-
session. The record supplies everything — which transcript, which model, which provider, which cwd — so the only
|
|
1686
|
-
input is the target id: there is no model override, no task, and no prompt. This runs NO turn: the window comes
|
|
1687
|
-
back with the conversation and waits, and talking to it is still entwurf_v2 fire-and-forget on the socket this
|
|
1688
|
-
call stands up. You get TWO receipts and they mean different things: a LAUNCH receipt (tmux made a window and was
|
|
1689
|
-
asked to start pi) and an OBSERVATION receipt (the control socket answered under the same id, or
|
|
1690
|
-
resume-unobserved). Unobserved is a real outcome, not an error to retry — the window is visible, so read it. A
|
|
1691
|
-
citizen that is already LIVE is refused: address it with entwurf_v2 instead. Only pi citizens have a same-id
|
|
1692
|
-
resume, because only they stand a control socket up.`,
|
|
1693
|
-
parameters: Type.Object({
|
|
1694
|
-
target: Type.String({
|
|
1695
|
-
minLength: 1,
|
|
1696
|
-
pattern: "^\\d{8}T\\d{6}-[0-9a-f]{6}$",
|
|
1697
|
-
description: "Garden id of the DORMANT pi citizen to reopen (discover with entwurf_peers).",
|
|
1698
|
-
}),
|
|
1699
|
-
}),
|
|
1700
|
-
async execute(
|
|
1701
|
-
_toolCallId: string,
|
|
1702
|
-
params: { target: string },
|
|
1703
|
-
_signal: AbortSignal | undefined,
|
|
1704
|
-
_onUpdate: unknown,
|
|
1705
|
-
_ctx: ExtensionContext,
|
|
1706
|
-
) {
|
|
1707
|
-
try {
|
|
1708
|
-
const mux = (await import(MUX_RESUME_CALL_MODULE)) as unknown as MuxResumeCallModule;
|
|
1709
|
-
const v2 = (await import(V2_VISIBLE_RESUME_MODULE)) as unknown as VisibleResumeModule;
|
|
1710
|
-
const launch = (input: { cwd: string; runtimeArgs: readonly string[] }) => {
|
|
1711
|
-
const launched = mux.resumeCall(input);
|
|
1712
|
-
return launched.ok
|
|
1713
|
-
? { ok: true, handle: launched.receipt }
|
|
1714
|
-
: { ok: false, reason: launched.reason, hint: mux.RESUME_CALL_REJECT_HINT[launched.reason] };
|
|
1715
|
-
};
|
|
1716
|
-
const result = await v2.visibleResume(params.target, v2.makeVisibleResumeDeps(launch));
|
|
1717
|
-
const rendered = v2.renderVisibleResume(result);
|
|
1718
|
-
return {
|
|
1719
|
-
content: [{ type: "text", text: rendered.text }],
|
|
1720
|
-
isError: rendered.isError,
|
|
1721
|
-
details: { isError: rendered.isError },
|
|
1722
|
-
};
|
|
1723
|
-
} catch (err) {
|
|
1724
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1725
|
-
return {
|
|
1726
|
-
content: [{ type: "text", text: `entwurf_resume_call error: ${msg}` }],
|
|
1727
|
-
isError: true,
|
|
1728
|
-
details: { error: msg },
|
|
1729
|
-
};
|
|
1730
|
-
}
|
|
1731
|
-
},
|
|
1732
|
-
});
|
|
1733
|
-
}
|