@junghanacs/entwurf 0.14.0 → 0.14.2

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 (54) hide show
  1. package/AGENTS.md +13 -2
  2. package/CHANGELOG.md +63 -0
  3. package/DELIVERY.md +57 -0
  4. package/README.md +16 -7
  5. package/VERIFY.md +4 -4
  6. package/demo/README.md +3 -1
  7. package/demo/demo-baseline.sh +12 -1
  8. package/demo/demo.sh +9 -1
  9. package/docs/acp-backend-rail.md +103 -4
  10. package/docs/external-mcp-host.md +1 -1
  11. package/docs/setup-clean-host.md +3 -3
  12. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +12 -5
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/classify-tmux-cwd.js +47 -0
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +45 -3
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +18 -47
  16. package/mcp/entwurf-bridge/dist/scripts/doctor-pi-provider.js +139 -47
  17. package/mcp/entwurf-bridge/dist/scripts/probe-bridge-command.js +294 -0
  18. package/mcp/entwurf-bridge/src/index.ts +14 -5
  19. package/mcp/entwurf-bridge/tsconfig.build.json +15 -5
  20. package/package.json +9 -9
  21. package/pi-extensions/entwurf-control.ts +13 -4
  22. package/pi-extensions/lib/acp/backend.ts +229 -9
  23. package/pi-extensions/lib/classify-tmux-cwd.ts +50 -0
  24. package/pi-extensions/lib/mux-fresh-call.ts +57 -4
  25. package/pi-extensions/lib/mux-resume-call.ts +21 -53
  26. package/run.sh +70 -25
  27. package/scripts/agy-bridge-config.py +47 -13
  28. package/scripts/agy-bridge.sh +73 -23
  29. package/scripts/check-acp-prompt-lifecycle.ts +221 -9
  30. package/scripts/check-entwurf-bridge-boot.ts +28 -0
  31. package/scripts/check-gate-qualification.ts +5 -3
  32. package/scripts/check-mux-resume-call.ts +11 -10
  33. package/scripts/check-probe-bridge-command.ts +201 -0
  34. package/scripts/check-release-gate-outcomes.ts +54 -1
  35. package/scripts/doctor-pi-provider.ts +155 -51
  36. package/scripts/meta-bridge-state.py +75 -1
  37. package/scripts/mutants/acp-prompt-lifecycle.json +25 -3
  38. package/scripts/mutants/bridge-command-boot.json +107 -0
  39. package/scripts/mutants/meta-retire.json +47 -0
  40. package/scripts/mutants/mux-fresh-call.json +48 -4
  41. package/scripts/mutants/mux-resume-call.json +3 -3
  42. package/scripts/mutants/release-gate.json +13 -0
  43. package/scripts/probe-bridge-command.ts +330 -0
  44. package/scripts/raw-async-delivery/README.md +158 -1
  45. package/scripts/raw-async-delivery/copilot-ui-server-probe.mjs +337 -0
  46. package/scripts/smoke-acp-raw-turn-live.ts +1 -1
  47. package/scripts/smoke-agy-install-state.sh +76 -2
  48. package/scripts/smoke-entwurf-chain-live.ts +12 -4
  49. package/scripts/smoke-entwurf-v2-matrix-live.ts +2 -2
  50. package/scripts/smoke-meta-install-state.sh +169 -3
  51. package/scripts/smoke-mux-fresh-call-live.ts +1 -1
  52. package/scripts/smoke-mux-lifecycle-live.ts +1 -1
  53. package/scripts/smoke-pi-provider-state.sh +135 -6
  54. package/scripts/smoke-resident-garden-guard.sh +2 -2
@@ -0,0 +1,294 @@
1
+ #!/usr/bin/env node
2
+ // probe-bridge-command — does the EFFECTIVE bridge command actually BOOT and serve MCP?
3
+ //
4
+ // WHY THIS EXISTS (#81): every doctor that judged the bridge wiring asked only whether the
5
+ // configured command RESOLVES — `command -v <bare name>` for pi (doctor-pi-provider) and agy
6
+ // (agy-bridge.sh). Resolvability is not bootability, and the gap is not theoretical: on the
7
+ // reference host `~/.local/bin/entwurf-bridge` was a symlink to the canonical pnpm shim, and
8
+ // that shim derives its target from `$0`. Invoked through the relocated link the derived
9
+ // `../global/...` path does not exist, so the launcher exits 127 and the MCP child never boots
10
+ // — while `command -v` kept answering yes and both doctors kept printing ok. The model in a
11
+ // Pi-hosted ACP turn then had no `mcp__entwurf-bridge__*` tool at all, which is a direct
12
+ // violation of #81's minimum core value (explicit Entwurf bridge connectivity).
13
+ //
14
+ // So this leaf asks the only question that carries the claim: run the command, speak MCP to it,
15
+ // and require the entwurf tool surface back. A green here is about the command the harness
16
+ // actually execs — never a recomputed desired one (the same rule meta-bridge-doctor's delivery
17
+ // self-diagnostic already states for the Claude lane).
18
+ //
19
+ // SIDE EFFECTS: none on operator state. Only `initialize` + `tools/list` are sent — never a
20
+ // `tools/call` — so no lock is taken, no record is written, and no message is delivered. The
21
+ // child is killed as soon as the frame arrives or the deadline passes. This is what makes the
22
+ // probe affordable inside a doctor an operator runs repeatedly.
23
+ //
24
+ // USAGE
25
+ // library: const r = await probeBridgeCommand({ command, args }); // doctor-pi-provider.ts
26
+ // CLI: node probe-bridge-command.ts <command> [args...]
27
+ // node probe-bridge-command.ts --invocation-json '{"command":"…","args":[],"env":{}}'
28
+ // exit 0 = booted and served the entwurf tool surface; 1 = did not (reason on stdout).
29
+ import { spawn } from "node:child_process";
30
+ import { basename } from "node:path";
31
+ // The EXACT public verb set of the current bridge. Identity is the whole set, not one member:
32
+ // the #81 symptom was a session whose schema lacked `entwurf_self` specifically, and a probe that
33
+ // only looked for `entwurf_v2` would have called that host healthy. Exactness also catches the
34
+ // other direction — a STALE build still serving a retired verb, or a foreign binary that happens
35
+ // to expose an `entwurf_v2`. check-entwurf-bridge-boot binds this list to the real runtime
36
+ // tools/list, so a verb added or retired without updating it turns that gate red.
37
+ export const EXPECTED_TOOLS = [
38
+ "entwurf_v2",
39
+ "entwurf_self",
40
+ "entwurf_peers",
41
+ "entwurf_inbox_read",
42
+ "entwurf_register_native",
43
+ "entwurf_fresh_call",
44
+ "entwurf_resume_call",
45
+ ];
46
+ const DEFAULT_TIMEOUT_MS = 10_000;
47
+ /** How long a probed child gets to die politely before it is killed outright. */
48
+ const CLEANUP_GRACE_MS = 500;
49
+ /** Collapse captured stderr into one printable line — a doctor verdict must stay one line. */
50
+ function head(text, limit = 200) {
51
+ const flat = text.replace(/\s+/g, " ").trim();
52
+ return flat.length > limit ? `${flat.slice(0, limit)}…` : flat;
53
+ }
54
+ export function probeBridgeCommand(opts) {
55
+ const { command, args = [], env, timeoutMs = DEFAULT_TIMEOUT_MS } = opts;
56
+ return new Promise((resolve) => {
57
+ // No shell: a bare name goes through the normal PATH lookup the harness itself does,
58
+ // and an argument can never be reinterpreted as shell syntax.
59
+ const child = spawn(command, args, { stdio: ["pipe", "pipe", "pipe"], env });
60
+ // `write()` reports a synchronous closed pipe by throwing, but a launcher that dies on exec
61
+ // closes stdin under us and the resulting EPIPE arrives ASYNCHRONOUSLY, as an 'error' event on
62
+ // the stream. Unlistened, Node turns that into an uncaught exception, so the probe dies
63
+ // printing its own stack trace INSTEAD of the launcher's stderr: precisely the diagnosis #81
64
+ // exists to surface, destroyed in the one cell that needs it. The window is real, not
65
+ // theoretical — under CPU contention the child can exec, write its stderr and exit between
66
+ // uv_spawn and our first write, which is how CI saw a doctor verdict with no launcher stderr
67
+ // in it. The `close` handler owns the verdict for that child (with captured stderr), so this
68
+ // listener is installed before anything can write and stays deliberately silent — one reason
69
+ // per failure, never an unhandled probe crash.
70
+ child.stdin.on("error", () => { });
71
+ let stdoutPending = "";
72
+ let stderr = "";
73
+ let settled = false;
74
+ let initialized = false;
75
+ let exited;
76
+ let stderrEnded = false;
77
+ // This probe deliberately runs whatever command the operator configured — including a
78
+ // foreign or broken one — so it owns closing what it opened. Escalate SIGTERM → SIGKILL
79
+ // once, bounded: cleanup for THIS child, not a supervisor (no retries, no watching, no
80
+ // process-tree walking).
81
+ //
82
+ // The reap is AWAITED before the result resolves, and that ordering is load-bearing: the
83
+ // callers are doctors that print a verdict and `process.exit()` immediately, which tears
84
+ // down any still-pending timer. A fire-and-forget SIGTERM plus a deferred SIGKILL would
85
+ // therefore leave a TERM-ignoring launcher running after the doctor is gone — the probe
86
+ // would leak exactly the kind of broken process it exists to detect.
87
+ const reap = () => new Promise((finished) => {
88
+ if (child.exitCode !== null || child.signalCode !== null)
89
+ return finished();
90
+ let reaped = false;
91
+ let hard;
92
+ const finish = () => {
93
+ if (reaped)
94
+ return;
95
+ reaped = true;
96
+ if (hard)
97
+ clearTimeout(hard);
98
+ finished();
99
+ };
100
+ child.once("close", finish);
101
+ try {
102
+ child.kill("SIGTERM");
103
+ }
104
+ catch {
105
+ return finish(); // already gone
106
+ }
107
+ hard = setTimeout(() => {
108
+ try {
109
+ child.kill("SIGKILL");
110
+ }
111
+ catch { }
112
+ // SIGKILL cannot be trapped, so `close` follows. The outer bound exists only so a
113
+ // child already reaped by someone else can never hang this promise forever.
114
+ setTimeout(finish, CLEANUP_GRACE_MS);
115
+ }, CLEANUP_GRACE_MS);
116
+ });
117
+ const done = (reason, detail) => {
118
+ if (settled)
119
+ return;
120
+ settled = true;
121
+ clearTimeout(timer);
122
+ void reap().then(() => resolve({ ok: reason === "ok", reason, detail }));
123
+ };
124
+ const timer = setTimeout(() => {
125
+ const waitingFor = initialized ? "tools/list" : "initialize";
126
+ done("timeout", `'${command}' stayed up but never answered ${waitingFor} within ${timeoutMs}ms` +
127
+ (stderr.trim() ? ` — stderr: ${head(stderr)}` : ""));
128
+ }, timeoutMs);
129
+ child.on("error", (err) => {
130
+ done("spawn-failed", `'${command}' could not be executed: ${String(err)}`);
131
+ });
132
+ // `ChildProcess` close is normally after the stdio pipes close, but under nested shell
133
+ // capture that ordering has raced on this host. Do not mint a no-stderr diagnosis until the
134
+ // stderr reader ended: the launcher's own final diagnostic is part of the operator verdict.
135
+ const reportExited = () => {
136
+ if (!exited || !stderrEnded)
137
+ return;
138
+ // Reaching here un-settled means the process died before answering id:2. This is the
139
+ // cell the relocated pnpm shim lands in (exit 127), and the stderr head is what tells
140
+ // the operator WHICH launcher hop broke.
141
+ done("exited-before-tools-list", `'${command}' exited before answering tools/list (code=${exited.code} signal=${String(exited.signal)})` +
142
+ (stderr.trim() ? ` — stderr: ${head(stderr)}` : " — no stderr"));
143
+ };
144
+ child.on("close", (code, signal) => {
145
+ exited = { code, signal };
146
+ reportExited();
147
+ });
148
+ child.stderr.on("data", (d) => {
149
+ stderr += String(d);
150
+ });
151
+ child.stderr.on("end", () => {
152
+ stderrEnded = true;
153
+ reportExited();
154
+ });
155
+ child.stdout.on("data", (d) => {
156
+ const chunk = String(d);
157
+ stdoutPending += chunk;
158
+ if (settled)
159
+ return;
160
+ // MCP frames are newline-delimited JSON-RPC. Consume each complete frame exactly once:
161
+ // replaying the accumulated id:1 response on a later data event would send a second
162
+ // tools/list before the first reply and stop being a sequential handshake.
163
+ const frames = stdoutPending.split("\n");
164
+ stdoutPending = frames.pop() ?? "";
165
+ for (const line of frames) {
166
+ const trimmed = line.trim();
167
+ if (!trimmed)
168
+ continue;
169
+ let msg;
170
+ try {
171
+ msg = JSON.parse(trimmed);
172
+ }
173
+ catch {
174
+ continue;
175
+ }
176
+ if (msg?.id === 1) {
177
+ const result = msg.result;
178
+ const valid = msg.error === undefined &&
179
+ result?.protocolVersion === "2024-11-05" &&
180
+ typeof result.capabilities === "object" &&
181
+ result.capabilities !== null &&
182
+ typeof result.serverInfo === "object" &&
183
+ result.serverInfo !== null;
184
+ if (!valid) {
185
+ done("initialize-failed", `'${command}' returned an invalid MCP initialize response`);
186
+ return;
187
+ }
188
+ initialized = true;
189
+ sendReadyFrames();
190
+ continue;
191
+ }
192
+ if (msg?.id !== 2)
193
+ continue;
194
+ if (!initialized) {
195
+ done("initialize-failed", `'${command}' answered tools/list before MCP initialize completed`);
196
+ return;
197
+ }
198
+ const tools = msg?.result?.tools;
199
+ if (!Array.isArray(tools)) {
200
+ done("no-tools-array", `'${command}' answered tools/list without result.tools — not an MCP server`);
201
+ return;
202
+ }
203
+ const names = tools
204
+ .map((t) => t?.name)
205
+ .filter((n) => typeof n === "string");
206
+ const served = new Set(names);
207
+ const missing = EXPECTED_TOOLS.filter((t) => !served.has(t));
208
+ const unexpected = names.filter((n) => !EXPECTED_TOOLS.includes(n));
209
+ if (missing.length > 0 || unexpected.length > 0) {
210
+ // Name BOTH directions: a missing verb is the #81 symptom (the model had no
211
+ // entwurf_self), an unexpected one means this is not the bridge we ship.
212
+ const parts = [];
213
+ if (missing.length > 0)
214
+ parts.push(`missing ${missing.join(", ")}`);
215
+ if (unexpected.length > 0)
216
+ parts.push(`unexpected ${unexpected.join(", ")}`);
217
+ done("tool-set-mismatch", `'${command}' is an MCP server but does not serve this bridge's verb set — ${parts.join("; ")} (served: ${names.join(", ") || "none"})`);
218
+ return;
219
+ }
220
+ done("ok", `'${command}' booted and served the exact entwurf verb set (${EXPECTED_TOOLS.length} tools)`);
221
+ return;
222
+ }
223
+ });
224
+ const send = (obj) => {
225
+ try {
226
+ child.stdin.write(`${JSON.stringify(obj)}\n`);
227
+ }
228
+ catch {
229
+ // Synchronous throws (a destroyed stream) land here; async EPIPE lands on the
230
+ // listener above. Both are the same verdict-free silence.
231
+ }
232
+ };
233
+ const sendReadyFrames = () => {
234
+ send({ jsonrpc: "2.0", method: "notifications/initialized" });
235
+ send({ jsonrpc: "2.0", id: 2, method: "tools/list" });
236
+ };
237
+ send({
238
+ jsonrpc: "2.0",
239
+ id: 1,
240
+ method: "initialize",
241
+ params: {
242
+ protocolVersion: "2024-11-05",
243
+ capabilities: {},
244
+ clientInfo: { name: "probe-bridge-command", version: "0" },
245
+ },
246
+ });
247
+ });
248
+ }
249
+ // CLI entrypoint. Kept argv-driven (no env carrier) so `./run.sh probe-bridge-command entwurf-bridge`
250
+ // asks about exactly the name an operator typed, and a doctor can shell out to the same verdict.
251
+ // Match the BASENAME exactly. A suffix test would also fire for `check-probe-bridge-command.ts`,
252
+ // which imports this module — the CLI arm would then hijack that gate's argv and exit 2.
253
+ if (process.argv[1] && /^probe-bridge-command\.(ts|js)$/.test(basename(process.argv[1]))) {
254
+ const argv = process.argv.slice(2);
255
+ let options;
256
+ if (argv[0] === "--invocation-json") {
257
+ let raw;
258
+ try {
259
+ raw = JSON.parse(argv[1] ?? "");
260
+ }
261
+ catch {
262
+ console.error("probe-bridge-command: --invocation-json must be valid JSON");
263
+ process.exit(2);
264
+ }
265
+ const obj = raw;
266
+ if (typeof obj?.command !== "string" ||
267
+ !obj.command ||
268
+ !Array.isArray(obj.args) ||
269
+ !obj.args.every((arg) => typeof arg === "string") ||
270
+ typeof obj.env !== "object" ||
271
+ obj.env === null ||
272
+ Array.isArray(obj.env) ||
273
+ !Object.values(obj.env).every((value) => typeof value === "string")) {
274
+ console.error("probe-bridge-command: invocation JSON requires string command, string[] args, string-map env");
275
+ process.exit(2);
276
+ }
277
+ options = {
278
+ command: obj.command,
279
+ args: obj.args,
280
+ env: { ...process.env, ...obj.env },
281
+ };
282
+ }
283
+ else {
284
+ const [command, ...args] = argv;
285
+ if (!command) {
286
+ console.error("usage: probe-bridge-command <command> [args...]");
287
+ process.exit(2);
288
+ }
289
+ options = { command, args };
290
+ }
291
+ const result = await probeBridgeCommand(options);
292
+ console.log(`[probe-bridge-command] ${result.reason}: ${result.detail}`);
293
+ process.exit(result.ok ? 0 : 1);
294
+ }
@@ -26,7 +26,8 @@
26
26
  * this read is the receipt.
27
27
  * - entwurf_register_native — explicit/manual fallback binding an ALREADY-RUNNING native
28
28
  * conversation (antigravity) to a garden id. Never a spawn.
29
- * - entwurf_fresh_call — open ONE fresh visible sibling in the operator's own tmux session;
29
+ * - entwurf_fresh_call — open ONE fresh visible sibling in the operator's own tmux session,
30
+ * optionally at ONE literal requested cwd (cross-repo fresh, #73);
30
31
  * returns a LAUNCH receipt only, and the new address arrives later as the
31
32
  * sender envelope of the sibling's nonce callback.
32
33
  * - entwurf_resume_call — reopen ONE DORMANT pi citizen under its OWN garden id in a visible
@@ -661,8 +662,10 @@ server.tool(
661
662
  "else: it does NOT mean the runtime started, the first turn ran, or the task was delivered. Nothing polls " +
662
663
  "for the callback; if it never arrives the window is visible and can be read directly. For EXISTING " +
663
664
  "citizens use entwurf_v2 — this tool only creates, and entwurf_peers only reports. Model is REQUIRED and " +
664
- "is passed to the chosen runtime CLI (`provider/model` for pi; model id/alias for Claude Code); there are no " +
665
- "arbitrary command/cwd/env knobs. Do not put secrets in the task model and task argv are visible to " +
665
+ "is passed to the chosen runtime CLI (`provider/model` for pi; model id/alias for Claude Code). An optional " +
666
+ "cwd starts the sibling in ONE literal absolute existing directory (cross-repo fresh) never pick resume " +
667
+ "for a dormant record's cwd; resume is continuity-only. Omitted/empty cwd means the caller's own directory. " +
668
+ "There are no arbitrary command/env knobs. Do not put secrets in the task — model and task argv are visible to " +
666
669
  "same-user processes on this host. Requires that this agent itself runs " +
667
670
  "inside tmux: without a pane anchor there is no session to open a sibling beside.",
668
671
  {
@@ -690,8 +693,14 @@ server.tool(
690
693
  .describe(
691
694
  "What the sibling should do after it calls you back. Plain instructions; no secrets (see the tool description).",
692
695
  ),
696
+ cwd: z
697
+ .string()
698
+ .optional()
699
+ .describe(
700
+ "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. Taken exactly as given — no trim, no realpath, no project-name resolution; '#' is refused (tmux format expansion). The receipt echoes what was REQUESTED, never an observation.",
701
+ ),
693
702
  },
694
- async ({ backend, model, task }) => {
703
+ async ({ backend, model, task, cwd }) => {
695
704
  let callerGardenId: string | null = null;
696
705
  try {
697
706
  const self = await buildAuthoritativeSelfEnvelope();
@@ -708,7 +717,7 @@ server.tool(
708
717
  callerGardenId = null;
709
718
  }
710
719
  try {
711
- const rendered = renderFreshCall(freshCall({ backend, model, task, callerGardenId }));
720
+ const rendered = renderFreshCall(freshCall({ backend, model, task, cwd, callerGardenId }));
712
721
  return rendered.isError ? textErr(rendered.text) : textOk(rendered.text);
713
722
  } catch (err) {
714
723
  return textErr(`entwurf_fresh_call error: ${err instanceof Error ? err.message : String(err)}`);
@@ -42,13 +42,16 @@
42
42
  //
43
43
  // It is a TYPE closure, not a boot closure, and the difference is shipped bytes.
44
44
  // The retired claim here ("the pi-lane-only acp/* stays out of the tarball") is
45
- // FALSE: dist/pi-extensions/lib/acp/ carries 8 modules (acp-client, backend-adapter,
46
- // config, context, engraving, models, overlay, tool-surface) that no bridge code
47
- // path executes tsc followed a type reference and emitted them. Dead emit is a
45
+ // FALSE: dist/pi-extensions/lib/acp/ is not a boot closure. config.js IS reached —
46
+ // doctor-pi-provider (#81) value-imports it. The other 7 modules (acp-client,
47
+ // backend-adapter, context, engraving, models, overlay, tool-surface) execute on
48
+ // no bridge/operator path: tsc followed config.ts's `import type { AcpBackendAdapter }`
49
+ // and emitted backend-adapter.js, which value-imports the rest. Dead emit is a
48
50
  // correctness question only for anything that reads its own location (see the
49
- // registry corpse), but it is dead weight in every consumer's tarball. Cleanup is
51
+ // registry corpse), but the 7 are dead weight in every consumer's tarball. Cleanup is
50
52
  // tracked as a follow-up, and the choice is one of two: narrow the emit, or ship
51
- // the assets those modules expect. Do NOT restore the old sentence.
53
+ // the assets those modules expect. Do NOT restore the old sentence, and do NOT
54
+ // put config.js back on the dead list.
52
55
  //
53
56
  // WHY store-doctor rides THIS build: meta-bridge-doctor.sh's full store scan
54
57
  // must run from an INSTALLED package (under node_modules, where strip-types is
@@ -86,6 +89,12 @@
86
89
  // and run_ts refuses them under an installed package rather than emitting 70+
87
90
  // gate leaves into the tarball.
88
91
  //
92
+ // WHY probe-bridge-command rides this build (#81): doctor-pi-provider IMPORTS it, and that
93
+ // doctor is one of the three operator commands above — so on an installed host the import
94
+ // resolves under node_modules where strip-types is refused. Emitting the leaf keeps the
95
+ // installed doctor's new boot cell alive; a missing twin would kill the doctor before its
96
+ // first verdict line. Its only imports are node builtins, so it adds ZERO new deps.
97
+ //
89
98
  // WHY meta-facts rides this build (#65): it is the operator projection external
90
99
  // consumers call INSTEAD of parsing the store, and those consumers live on
91
100
  // INSTALLED hosts — under node_modules, where run_ts needs a compiled twin.
@@ -107,6 +116,7 @@
107
116
  "../../scripts/meta-bridge-store-doctor.ts",
108
117
  "../../scripts/agy-imprint.ts",
109
118
  "../../scripts/doctor-pi-provider.ts",
119
+ "../../scripts/probe-bridge-command.ts",
110
120
  "../../scripts/new-session-id.ts",
111
121
  "../../scripts/meta-bridge-prune.ts",
112
122
  "../../scripts/meta-bridge-fresh-cut.ts",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@junghanacs/entwurf",
3
- "version": "0.14.0",
3
+ "version": "0.14.2",
4
4
  "description": "Garden-citizen dispatch substrate and meta-bridge for Claude Code, Codex, Antigravity, and pi harnesses.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -74,23 +74,23 @@
74
74
  "claudeCodeFloor": ">=2.1.217"
75
75
  },
76
76
  "dependencies": {
77
- "@agentclientprotocol/claude-agent-acp": "0.66.0",
77
+ "@agentclientprotocol/claude-agent-acp": "0.70.0",
78
78
  "@agentclientprotocol/sdk": "1.3.0",
79
79
  "@anthropic-ai/sdk": "0.100.1",
80
80
  "@modelcontextprotocol/sdk": "^1.12.1",
81
81
  "zod": "^3.25.0 || ^4.0.0"
82
82
  },
83
83
  "peerDependencies": {
84
- "@earendil-works/pi-ai": ">=0.84.1 <0.85",
85
- "@earendil-works/pi-coding-agent": ">=0.84.1 <0.85",
86
- "@earendil-works/pi-tui": ">=0.84.1 <0.85",
84
+ "@earendil-works/pi-ai": ">=0.84.2 <0.85",
85
+ "@earendil-works/pi-coding-agent": ">=0.84.2 <0.85",
86
+ "@earendil-works/pi-tui": ">=0.84.2 <0.85",
87
87
  "typebox": "*"
88
88
  },
89
89
  "devDependencies": {
90
90
  "@biomejs/biome": "^2.4.13",
91
- "@earendil-works/pi-ai": "0.84.1",
92
- "@earendil-works/pi-coding-agent": "0.84.1",
93
- "@earendil-works/pi-tui": "0.84.1",
91
+ "@earendil-works/pi-ai": "0.84.2",
92
+ "@earendil-works/pi-coding-agent": "0.84.2",
93
+ "@earendil-works/pi-tui": "0.84.2",
94
94
  "@types/node": "^24.3.0",
95
95
  "husky": "^9.1.7",
96
96
  "rregex": "1.13.1",
@@ -145,7 +145,7 @@
145
145
  "check:toolchain": "pnpm lint && pnpm typecheck",
146
146
  "check:vitest": "./run.sh check-mux-fresh-call && ./run.sh check-acp-sdk-surface",
147
147
  "check:contracts": "./run.sh check-shell-quote && ./run.sh check-entwurf-session-identity && ./run.sh check-meta-session && ./run.sh check-meta-v3-record && ./run.sh check-mailbox-receipt-state && ./run.sh check-entwurf-capabilities && ./run.sh check-capability-bundle-reach && ./run.sh check-meta-mailbox-state-write && ./run.sh check-meta-receiver-marker && ./run.sh check-meta-capability-source && ./run.sh check-model-lock && ./run.sh check-dep-versions && ./run.sh check-node-floor-coherence && ./run.sh check-claude-floor-coherence && ./run.sh check-pi-import-surface && ./run.sh check-env-namespace && ./run.sh check-pi-runtime-version && ./run.sh check-pi-preflight && ./run.sh check-project-trust-handler && ./run.sh check-entwurf-v2-contract && ./run.sh check-entwurf-v2-lock && ./run.sh check-entwurf-v2-decider && ./run.sh check-entwurf-v2-matrix && ./run.sh check-entwurf-v2-release && ./run.sh check-entwurf-v2-send && ./run.sh check-entwurf-v2-send-fallback && ./run.sh check-entwurf-v2-mailbox && ./run.sh check-entwurf-v2-native-push && ./run.sh check-entwurf-v2-runner && ./run.sh check-entwurf-v2-production && ./run.sh check-entwurf-v2-surface && ./run.sh check-entwurf-bridge-pi-free && ./run.sh check-entwurf-resume-args && ./run.sh check-resume-launch-identity && ./run.sh check-mux-placement && ./run.sh check-mux-launch && ./run.sh check-mux-resume-call && ./run.sh check-mux-parent-artifact && ./run.sh check-mux-launcher-fence && ./run.sh check-entwurf-v2-visible-resume && ./run.sh check-entwurf-facts && ./run.sh check-control-socket-path && ./run.sh check-socket-discovery && ./run.sh check-meta-listing && ./run.sh check-entwurf-fact-provider && ./run.sh check-entwurf-peers-surface && ./run.sh check-entwurf-self-address && ./run.sh check-entwurf-deliverability && ./run.sh check-native-push-adapter && ./run.sh check-native-push-register && ./run.sh check-auth-boundary && ./run.sh check-acp-overlay && ./run.sh check-acp-tool-surface && ./run.sh check-acp-event-mapper && ./run.sh check-acp-prompt-builder && ./run.sh check-acp-config && ./run.sh check-acp-session-store && ./run.sh check-acp-carrier-augment",
148
- "check:hermetic": "./run.sh check-bridge-delivery && ./run.sh smoke-pi-attach && ./run.sh check-fresh-cut-gate && ./run.sh check-hook-launch-topology && ./run.sh check-meta-identity-consumers && ./run.sh check-socket-probe && ./run.sh smoke-meta-honesty && ./run.sh check-meta-doctor-oracle && ./run.sh check-agy-permission-matrix && ./run.sh smoke-agy-statusline-state && ./run.sh smoke-agy-hooks-state && ./run.sh smoke-pi-provider-state && ./run.sh smoke-user-scope-citizen && ./run.sh smoke-meta-prune && ./run.sh smoke-meta-keyset-guard && ./run.sh check-meta-manifest-schema && ./run.sh check-entwurf-control-rpc && ./run.sh check-entwurf-bridge-boot && ./run.sh check-meta-facts && ./run.sh check-agy-sender-identity && ./run.sh check-acp-provider-surface && ./run.sh check-acp-stop-reason && ./run.sh check-acp-prompt-lifecycle && ./run.sh check-acp-stream-hooks && ./run.sh check-acp-backend-preflight && ./run.sh check-acp-session-reuse && ./run.sh check-release-gate-outcomes && ./run.sh check-probe-ordering && ./run.sh check-probe-cli-shim && ./run.sh check-acp-cortex",
148
+ "check:hermetic": "./run.sh check-bridge-delivery && ./run.sh smoke-pi-attach && ./run.sh check-fresh-cut-gate && ./run.sh check-hook-launch-topology && ./run.sh check-meta-identity-consumers && ./run.sh check-socket-probe && ./run.sh smoke-meta-honesty && ./run.sh check-meta-doctor-oracle && ./run.sh check-agy-permission-matrix && ./run.sh smoke-agy-statusline-state && ./run.sh smoke-agy-hooks-state && ./run.sh smoke-pi-provider-state && ./run.sh smoke-user-scope-citizen && ./run.sh smoke-meta-prune && ./run.sh smoke-meta-keyset-guard && ./run.sh check-meta-manifest-schema && ./run.sh check-entwurf-control-rpc && ./run.sh check-entwurf-bridge-boot && ./run.sh check-probe-bridge-command && ./run.sh check-meta-facts && ./run.sh check-agy-sender-identity && ./run.sh check-acp-provider-surface && ./run.sh check-acp-stop-reason && ./run.sh check-acp-prompt-lifecycle && ./run.sh check-acp-stream-hooks && ./run.sh check-acp-backend-preflight && ./run.sh check-acp-session-reuse && ./run.sh check-release-gate-outcomes && ./run.sh check-probe-ordering && ./run.sh check-probe-cli-shim && ./run.sh check-acp-cortex",
149
149
  "check:package": "./run.sh check-install-surface && ./run.sh smoke-meta-install-state && ./run.sh smoke-agy-install-state && ./run.sh check-package-source-routing && ./run.sh check-install-preflight && ./run.sh check-pack"
150
150
  },
151
151
  "pi": {
@@ -1489,7 +1489,7 @@ const MUX_FRESH_CALL_MODULE = "./lib/mux-fresh-call.ts";
1489
1489
 
1490
1490
  interface MuxFreshCallModule {
1491
1491
  freshCall(
1492
- params: { backend: "pi" | "claude-code"; model: string; task: string; callerGardenId: string | null },
1492
+ params: { backend: "pi" | "claude-code"; model: string; task: string; cwd?: string; callerGardenId: string | null },
1493
1493
  env?: NodeJS.ProcessEnv,
1494
1494
  ): { ok: boolean };
1495
1495
  renderFreshCall(result: { ok: boolean }): { text: string; isError: boolean };
@@ -1520,8 +1520,10 @@ not exist a moment ago. This returns a LAUNCH receipt (tmux window/pane plus tha
1520
1520
  it does NOT mean the runtime started, the first turn ran, or the task was delivered. Nothing polls for the
1521
1521
  callback; if it never arrives the window is visible and can be read directly. For EXISTING citizens use
1522
1522
  entwurf_v2 — this tool only creates, and entwurf_peers only reports. Model is REQUIRED and passed to the
1523
- chosen runtime CLI (provider/model for pi; model id/alias for Claude Code); there are no arbitrary
1524
- command/cwd/env knobs. Do not put secrets in the taskmodel and task argv are visible to same-user
1523
+ chosen runtime CLI (provider/model for pi; model id/alias for Claude Code). An optional cwd starts the
1524
+ sibling in ONE literal absolute existing directory (cross-repo fresh)never pick resume for a dormant
1525
+ record's cwd; resume is continuity-only. Omitted/empty cwd means the caller's own directory. There are no
1526
+ arbitrary command/env knobs. Do not put secrets in the task — model and task argv are visible to same-user
1525
1527
  processes on this host.`,
1526
1528
  parameters: Type.Object({
1527
1529
  backend: StringEnum(["pi", "claude-code"], {
@@ -1539,10 +1541,16 @@ processes on this host.`,
1539
1541
  description:
1540
1542
  "What the sibling should do after it calls you back. Plain instructions; no secrets (see the tool description).",
1541
1543
  }),
1544
+ cwd: Type.Optional(
1545
+ Type.String({
1546
+ description:
1547
+ "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. Taken exactly as given — no trim, no realpath, no project-name resolution; '#' is refused (tmux format expansion). The receipt echoes what was REQUESTED, never an observation.",
1548
+ }),
1549
+ ),
1542
1550
  }),
1543
1551
  async execute(
1544
1552
  _toolCallId: string,
1545
- params: { backend: "pi" | "claude-code"; model: string; task: string },
1553
+ params: { backend: "pi" | "claude-code"; model: string; task: string; cwd?: string },
1546
1554
  _signal: AbortSignal | undefined,
1547
1555
  _onUpdate: unknown,
1548
1556
  _ctx: ExtensionContext,
@@ -1553,6 +1561,7 @@ processes on this host.`,
1553
1561
  backend: params.backend,
1554
1562
  model: params.model,
1555
1563
  task: params.task,
1564
+ cwd: params.cwd,
1556
1565
  callerGardenId: residentGardenId,
1557
1566
  });
1558
1567
  const rendered = mux.renderFreshCall(result);