@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.
- package/AGENTS.md +13 -2
- package/CHANGELOG.md +63 -0
- package/DELIVERY.md +57 -0
- package/README.md +16 -7
- package/VERIFY.md +4 -4
- package/demo/README.md +3 -1
- package/demo/demo-baseline.sh +12 -1
- package/demo/demo.sh +9 -1
- package/docs/acp-backend-rail.md +103 -4
- package/docs/external-mcp-host.md +1 -1
- package/docs/setup-clean-host.md +3 -3
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +12 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/classify-tmux-cwd.js +47 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +45 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +18 -47
- package/mcp/entwurf-bridge/dist/scripts/doctor-pi-provider.js +139 -47
- package/mcp/entwurf-bridge/dist/scripts/probe-bridge-command.js +294 -0
- package/mcp/entwurf-bridge/src/index.ts +14 -5
- package/mcp/entwurf-bridge/tsconfig.build.json +15 -5
- package/package.json +9 -9
- package/pi-extensions/entwurf-control.ts +13 -4
- package/pi-extensions/lib/acp/backend.ts +229 -9
- package/pi-extensions/lib/classify-tmux-cwd.ts +50 -0
- package/pi-extensions/lib/mux-fresh-call.ts +57 -4
- package/pi-extensions/lib/mux-resume-call.ts +21 -53
- package/run.sh +70 -25
- package/scripts/agy-bridge-config.py +47 -13
- package/scripts/agy-bridge.sh +73 -23
- package/scripts/check-acp-prompt-lifecycle.ts +221 -9
- package/scripts/check-entwurf-bridge-boot.ts +28 -0
- package/scripts/check-gate-qualification.ts +5 -3
- package/scripts/check-mux-resume-call.ts +11 -10
- package/scripts/check-probe-bridge-command.ts +201 -0
- package/scripts/check-release-gate-outcomes.ts +54 -1
- package/scripts/doctor-pi-provider.ts +155 -51
- package/scripts/meta-bridge-state.py +75 -1
- package/scripts/mutants/acp-prompt-lifecycle.json +25 -3
- package/scripts/mutants/bridge-command-boot.json +107 -0
- package/scripts/mutants/meta-retire.json +47 -0
- package/scripts/mutants/mux-fresh-call.json +48 -4
- package/scripts/mutants/mux-resume-call.json +3 -3
- package/scripts/mutants/release-gate.json +13 -0
- package/scripts/probe-bridge-command.ts +330 -0
- package/scripts/raw-async-delivery/README.md +158 -1
- package/scripts/raw-async-delivery/copilot-ui-server-probe.mjs +337 -0
- package/scripts/smoke-acp-raw-turn-live.ts +1 -1
- package/scripts/smoke-agy-install-state.sh +76 -2
- package/scripts/smoke-entwurf-chain-live.ts +12 -4
- package/scripts/smoke-entwurf-v2-matrix-live.ts +2 -2
- package/scripts/smoke-meta-install-state.sh +169 -3
- package/scripts/smoke-mux-fresh-call-live.ts +1 -1
- package/scripts/smoke-mux-lifecycle-live.ts +1 -1
- package/scripts/smoke-pi-provider-state.sh +135 -6
- 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)
|
|
665
|
-
"
|
|
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/
|
|
46
|
-
//
|
|
47
|
-
//
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
85
|
-
"@earendil-works/pi-coding-agent": ">=0.84.
|
|
86
|
-
"@earendil-works/pi-tui": ">=0.84.
|
|
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.
|
|
92
|
-
"@earendil-works/pi-coding-agent": "0.84.
|
|
93
|
-
"@earendil-works/pi-tui": "0.84.
|
|
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)
|
|
1524
|
-
|
|
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);
|