@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
@@ -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
- // Use pi-ai's re-exports of typebox so the schema universe matches what
69
- // pi-coding-agent.registerTool consumes. Importing Type from @sinclair/typebox
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, controlSocketPathIn, defaultControlSocketDir } from "./lib/control-socket-path.js";
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, the pi-native entwurf_v2 senderProvider via buildLocalSenderEnvelope)
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
- // Extension factories run before extension flag values are hydrated into runtime.flagValues,
884
- // so we inspect argv directly when deciding whether to register tools at load time.
885
- function wasBooleanFlagPassed(flagName: string): boolean {
886
- const flag = `--${flagName}`;
887
- return process.argv.slice(2).includes(flag);
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
- updateStatus(ctx, true, birth.gardenId);
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
- // Tool: entwurf_v2 (5d-3a) — the unified v2 dispatch verb
1112
+ // Fence seams
1147
1113
  // ============================================================================
1148
- //
1149
- // The v2 runner + production deps live in the `.ts`-extension fence (excluded from
1150
- // this emit-capable root program). A STATIC import would pull the fence's literal
1151
- // `.ts` imports into root tsc → TS5097. So we reach the surface adapter via a
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
- }