@tekmidian/pai 0.42.0 → 0.42.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/README.md +1 -1
- package/dist/chain-B-DbR82m.mjs +164 -0
- package/dist/chain-B-DbR82m.mjs.map +1 -0
- package/dist/cli/index.mjs +8 -8
- package/dist/cli/program.mjs +8 -8
- package/dist/{config-DgzR5sfb.mjs → config-_lQI_W08.mjs} +116 -3
- package/dist/config-_lQI_W08.mjs.map +1 -0
- package/dist/{context-handover-cache-dUnSZwaA.mjs → context-handover-cache-pNFdBNrU.mjs} +27 -8
- package/dist/context-handover-cache-pNFdBNrU.mjs.map +1 -0
- package/dist/daemon/index.mjs +7 -7
- package/dist/{daemon-CKBkhOsZ.mjs → daemon-Awt_3UJp.mjs} +7 -7
- package/dist/{daemon-DsvP8KMy.mjs → daemon-Dq8A1uDF.mjs} +591 -370
- package/dist/daemon-Dq8A1uDF.mjs.map +1 -0
- package/dist/daemon-mcp/index.mjs +5 -5
- package/dist/{fallback-5UJO-RyX.mjs → fallback-vQPI1qxt.mjs} +15 -13
- package/dist/fallback-vQPI1qxt.mjs.map +1 -0
- package/dist/hooks/mcp-deferred-gate.mjs +286 -0
- package/dist/hooks/mcp-deferred-gate.mjs.map +7 -0
- package/dist/hooks/mcp-deferred-reminder.mjs +54 -0
- package/dist/hooks/mcp-deferred-reminder.mjs.map +7 -0
- package/dist/hooks/route-agents-to-worker.mjs +65 -2
- package/dist/hooks/route-agents-to-worker.mjs.map +2 -2
- package/dist/hooks/worker-proxy.mjs +64 -1
- package/dist/hooks/worker-proxy.mjs.map +2 -2
- package/dist/hooks/worker-status-line.mjs +68 -2
- package/dist/hooks/worker-status-line.mjs.map +2 -2
- package/dist/hooks/worker-supervision.mjs +64 -1
- package/dist/hooks/worker-supervision.mjs.map +2 -2
- package/dist/{main-resolver-DXGuygFG.mjs → main-resolver-BAfOMppW.mjs} +2 -2
- package/dist/{main-resolver-BgIrC-D9.mjs → main-resolver-DnkxPYkr.mjs} +111 -16
- package/dist/main-resolver-DnkxPYkr.mjs.map +1 -0
- package/dist/{planner-BLDJt3ZS.mjs → planner-BfEGcEs1.mjs} +5 -5
- package/dist/{planner-BLDJt3ZS.mjs.map → planner-BfEGcEs1.mjs.map} +1 -1
- package/dist/{program-CruRnLtc.mjs → program-BDmeZP-v.mjs} +17 -11
- package/dist/{program-CruRnLtc.mjs.map → program-BDmeZP-v.mjs.map} +1 -1
- package/dist/{chain-DGXNoTwc.mjs → run-CGXbhf2k.mjs} +422 -195
- package/dist/run-CGXbhf2k.mjs.map +1 -0
- package/dist/{server-Cp7tNzNC.mjs → server-D4vAieF4.mjs} +49 -8
- package/dist/server-D4vAieF4.mjs.map +1 -0
- package/dist/{state-BTa10SOM.mjs → state-BRdB-Bf1.mjs} +7 -2
- package/dist/state-BRdB-Bf1.mjs.map +1 -0
- package/dist/state-CY6I-Hnc.mjs +3 -0
- package/dist/{work-queue-worker-D1p2V-ae.mjs → work-queue-worker-AEmfVE0F.mjs} +5 -5
- package/dist/{work-queue-worker-7Vkli4Yh.mjs → work-queue-worker-DIT-VLSh.mjs} +3 -3
- package/dist/{work-queue-worker-7Vkli4Yh.mjs.map → work-queue-worker-DIT-VLSh.mjs.map} +1 -1
- package/docs/cache-keepalive.md +82 -0
- package/docs/commands/worker.md +1 -0
- package/docs/merge-demo.md +1 -0
- package/docs/provider-abstraction.md +52 -2
- package/package.json +1 -1
- package/plugins/core/hooks/hooks.json +12 -0
- package/src/hooks/ts/lib/mcp-deferred-gate.test.ts +258 -0
- package/src/hooks/ts/lib/mcp-deferred-gate.ts +261 -0
- package/src/hooks/ts/lib/mcp-deferred-reminder.test.ts +90 -0
- package/src/hooks/ts/lib/mcp-deferred-reminder.ts +66 -0
- package/src/hooks/ts/pre-tool-use/mcp-deferred-gate.ts +126 -0
- package/src/hooks/ts/pre-tool-use/route-agents-to-worker.ts +6 -2
- package/src/hooks/ts/session-start/mcp-deferred-reminder.ts +52 -0
- package/statusline-command.sh +149 -72
- package/dist/chain-DGXNoTwc.mjs.map +0 -1
- package/dist/config-DgzR5sfb.mjs.map +0 -1
- package/dist/context-handover-cache-dUnSZwaA.mjs.map +0 -1
- package/dist/daemon-DsvP8KMy.mjs.map +0 -1
- package/dist/fallback-5UJO-RyX.mjs.map +0 -1
- package/dist/main-resolver-BgIrC-D9.mjs.map +0 -1
- package/dist/scope-NIwCGxRI.mjs +0 -351
- package/dist/scope-NIwCGxRI.mjs.map +0 -1
- package/dist/server-Cp7tNzNC.mjs.map +0 -1
- package/dist/state-BTa10SOM.mjs.map +0 -1
- package/dist/state-RXepvbnO.mjs +0 -3
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { _ as workersLogDir, d as setCooldown, f as
|
|
3
|
-
import { S as stripPromptValues, _ as newWorkerId, a as resolveSpawnerSession, b as parseRunnerArgs, d as alive, g as loadStatuses, h as loadStatus, i as resolveSession, l as UNLABELED, m as describeTool, o as scopeKey, r as itermUuid, v as nowStamp, x as shortText, y as saveStatus } from "./scope-NIwCGxRI.mjs";
|
|
1
|
+
import { _ as readWorkersSection, c as classModelCapability, f as isModelCapability, g as providerKeyPath, o as WorkersConfigError, s as assertProviderRunnable, v as resolveModelCapability } from "./config-_lQI_W08.mjs";
|
|
2
|
+
import { _ as workersLogDir, d as setCooldown, f as ensureNoMcpConfig, g as statusPath, h as panesDir, i as buildRunEnv, m as ledgerPath, n as ensureProxyRunning, o as isQuotaFailure, p as eventsPath, s as nextAutoProvider, t as DEFAULT_PROXY_PORT, u as resolveTarget } from "./server-D4vAieF4.mjs";
|
|
4
3
|
import { appendFileSync, closeSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
|
|
5
4
|
import { homedir, tmpdir } from "node:os";
|
|
6
5
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
@@ -8,6 +7,100 @@ import { connect, createServer } from "node:net";
|
|
|
8
7
|
import { execFileSync, spawn } from "node:child_process";
|
|
9
8
|
import { createInterface } from "node:readline";
|
|
10
9
|
|
|
10
|
+
//#region src/workers/args.ts
|
|
11
|
+
function parseRunnerArgs(argv) {
|
|
12
|
+
let prompt = null;
|
|
13
|
+
let outputFormat = "text";
|
|
14
|
+
const rest = [];
|
|
15
|
+
let headless = false;
|
|
16
|
+
let callerModel = false;
|
|
17
|
+
let callerMcpConfig = false;
|
|
18
|
+
let callerSystemPrompt = false;
|
|
19
|
+
const mcp = [];
|
|
20
|
+
const allowedTools = [];
|
|
21
|
+
let i = 0;
|
|
22
|
+
while (i < argv.length) {
|
|
23
|
+
const a = argv[i];
|
|
24
|
+
if (a === "-p" || a === "--print") {
|
|
25
|
+
headless = true;
|
|
26
|
+
rest.push(a);
|
|
27
|
+
if (i + 1 < argv.length && !argv[i + 1].startsWith("-")) {
|
|
28
|
+
prompt = argv[i + 1];
|
|
29
|
+
rest.push(prompt);
|
|
30
|
+
i += 1;
|
|
31
|
+
}
|
|
32
|
+
} else if (a === "--output-format") {
|
|
33
|
+
const v = argv[i + 1];
|
|
34
|
+
if (v === "json" || v === "stream-json") outputFormat = v;
|
|
35
|
+
i += 1;
|
|
36
|
+
} else if (a.startsWith("--output-format=")) {
|
|
37
|
+
const v = a.slice(16);
|
|
38
|
+
if (v === "json" || v === "stream-json") outputFormat = v;
|
|
39
|
+
} else if (a === "--verbose") {} else if (a === "--mcp") {
|
|
40
|
+
const v = argv[i + 1];
|
|
41
|
+
if (v !== void 0 && !v.startsWith("-")) {
|
|
42
|
+
mcp.push(v);
|
|
43
|
+
i += 1;
|
|
44
|
+
}
|
|
45
|
+
} else if (a.startsWith("--mcp=")) mcp.push(a.slice(6));
|
|
46
|
+
else if (a === "--allowedTools") {
|
|
47
|
+
rest.push(a);
|
|
48
|
+
const v = argv[i + 1];
|
|
49
|
+
if (v !== void 0 && !v.startsWith("-")) {
|
|
50
|
+
allowedTools.push(v);
|
|
51
|
+
rest.push(v);
|
|
52
|
+
i += 1;
|
|
53
|
+
}
|
|
54
|
+
} else if (a.startsWith("--allowedTools=")) {
|
|
55
|
+
allowedTools.push(a.slice(15));
|
|
56
|
+
rest.push(a);
|
|
57
|
+
} else {
|
|
58
|
+
if (a === "--model") callerModel = true;
|
|
59
|
+
if (a.startsWith("--model=")) callerModel = true;
|
|
60
|
+
if (a === "--mcp-config") callerMcpConfig = true;
|
|
61
|
+
if (a.startsWith("--mcp-config=")) callerMcpConfig = true;
|
|
62
|
+
if (a === "--append-system-prompt") callerSystemPrompt = true;
|
|
63
|
+
if (a.startsWith("--append-system-prompt=")) callerSystemPrompt = true;
|
|
64
|
+
if (prompt === null && !a.startsWith("-") && rest.length > 0 && (rest[rest.length - 1] === "-p" || rest[rest.length - 1] === "--print")) prompt = a;
|
|
65
|
+
rest.push(a);
|
|
66
|
+
}
|
|
67
|
+
i += 1;
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
prompt,
|
|
71
|
+
outputFormat,
|
|
72
|
+
rest,
|
|
73
|
+
headless,
|
|
74
|
+
callerModel,
|
|
75
|
+
callerMcpConfig,
|
|
76
|
+
callerSystemPrompt,
|
|
77
|
+
mcp,
|
|
78
|
+
allowedTools
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Turn `-p "<prompt>"` into bare `-p` (same for --print): for stream-json
|
|
83
|
+
* stdin runs the prompt moves to the first user message on stdin, so the
|
|
84
|
+
* value must not stay on the command line. Only prompt values directly
|
|
85
|
+
* following the flag are touched; everything else passes through.
|
|
86
|
+
*/
|
|
87
|
+
function stripPromptValues(argv) {
|
|
88
|
+
const out = [];
|
|
89
|
+
for (let i = 0; i < argv.length; i++) {
|
|
90
|
+
const a = argv[i];
|
|
91
|
+
const isPrint = a === "-p" || a === "--print";
|
|
92
|
+
out.push(a);
|
|
93
|
+
if (isPrint && i + 1 < argv.length && !argv[i + 1].startsWith("-")) i += 1;
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
/** Collapse whitespace and cut to n chars with an ellipsis (label rendering). */
|
|
98
|
+
function shortText(s, n) {
|
|
99
|
+
const t = String(s ?? "").split(/\s+/).filter(Boolean).join(" ");
|
|
100
|
+
return t.length <= n ? t : t.slice(0, n - 1) + "…";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
//#endregion
|
|
11
104
|
//#region src/workers/ledger.ts
|
|
12
105
|
/**
|
|
13
106
|
* ledger.ts — the routing ledger, one append-only log for every worker event.
|
|
@@ -77,6 +170,265 @@ function ledgerSummary(path, scope, lastN = 15, now = /* @__PURE__ */ new Date()
|
|
|
77
170
|
};
|
|
78
171
|
}
|
|
79
172
|
|
|
173
|
+
//#endregion
|
|
174
|
+
//#region src/workers/status.ts
|
|
175
|
+
/**
|
|
176
|
+
* status.ts — the live per-worker status file.
|
|
177
|
+
*
|
|
178
|
+
* One JSON file per run, rewritten atomically on every turn, read by `ps`,
|
|
179
|
+
* `follow`, the status line and the pane logic. Same field set the Python
|
|
180
|
+
* runner wrote, plus `provider` (routing is multi-provider now) and `session`
|
|
181
|
+
* (the AIBroker identity of the launching session, see scope.ts).
|
|
182
|
+
*/
|
|
183
|
+
/** Label a worker gets when launched with neither --label nor a prompt. */
|
|
184
|
+
const UNLABELED = "unlabeled";
|
|
185
|
+
/**
|
|
186
|
+
* Whether a status is the terminal's interactive chat pane, not a spawned
|
|
187
|
+
* task worker. `origin` is the real discriminator; the fallback catches
|
|
188
|
+
* running entries from before the flag existed (unlabeled, no turns yet).
|
|
189
|
+
* Removable once every pane runs code that writes `origin`.
|
|
190
|
+
*/
|
|
191
|
+
function isChatPane(s) {
|
|
192
|
+
return s.origin === "chat" || !s.origin && (s.label === UNLABELED || s.label === "(no prompt)") && s.turns === 0;
|
|
193
|
+
}
|
|
194
|
+
/** Context-meter percentage 0–100, null when the numbers are missing. */
|
|
195
|
+
function contextPercent(s) {
|
|
196
|
+
if (!s.contextTokens || !s.contextWindow) return null;
|
|
197
|
+
return Math.round(s.contextTokens / s.contextWindow * 100);
|
|
198
|
+
}
|
|
199
|
+
/** Compact token count: 84k, 200k, 900. */
|
|
200
|
+
function fmtContextK(n) {
|
|
201
|
+
return n >= 1e3 ? `${Math.round(n / 1e3)}k` : String(n);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* The one context-meter label, shared by every renderer so identical
|
|
205
|
+
* numbers always render identically: `ctx 84k/200k (42%)` — used-style
|
|
206
|
+
* percent. Empty when the numbers are missing (callers drop the part).
|
|
207
|
+
*/
|
|
208
|
+
function contextLabel(s) {
|
|
209
|
+
const pct = contextPercent(s);
|
|
210
|
+
if (pct === null) return "";
|
|
211
|
+
return `ctx ${fmtContextK(s.contextTokens ?? 0)}/${fmtContextK(s.contextWindow ?? 0)} (${pct}%)`;
|
|
212
|
+
}
|
|
213
|
+
/** Timestamp format shared by status files and the ledger. */
|
|
214
|
+
function nowStamp(d = /* @__PURE__ */ new Date()) {
|
|
215
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
216
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
|
|
217
|
+
}
|
|
218
|
+
let lastId = "";
|
|
219
|
+
let idSeq = 0;
|
|
220
|
+
function newWorkerId(d = /* @__PURE__ */ new Date(), pid = process.pid) {
|
|
221
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
222
|
+
const base = `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}-${pid}`;
|
|
223
|
+
if (base === lastId) {
|
|
224
|
+
idSeq += 1;
|
|
225
|
+
return `${base}-${String(idSeq).padStart(2, "0")}`;
|
|
226
|
+
}
|
|
227
|
+
lastId = base;
|
|
228
|
+
idSeq = 0;
|
|
229
|
+
return base;
|
|
230
|
+
}
|
|
231
|
+
/** Write status atomically (temp + rename) and stamp `updated`. */
|
|
232
|
+
function saveStatus(logDir, status, d = /* @__PURE__ */ new Date()) {
|
|
233
|
+
status.updated = nowStamp(d);
|
|
234
|
+
const path = statusPath(logDir, status.id);
|
|
235
|
+
const tmp = `${path}.tmp`;
|
|
236
|
+
writeFileSync(tmp, JSON.stringify(status), "utf8");
|
|
237
|
+
renameSync(tmp, path);
|
|
238
|
+
}
|
|
239
|
+
/** Load every status file in the logDir, oldest id first, skipping damage. */
|
|
240
|
+
function loadStatuses(logDir) {
|
|
241
|
+
if (!existsSync(logDir)) return [];
|
|
242
|
+
const out = [];
|
|
243
|
+
for (const name of readdirSync(logDir).sort()) {
|
|
244
|
+
if (!name.endsWith(".status")) continue;
|
|
245
|
+
try {
|
|
246
|
+
out.push(JSON.parse(readFileSync(join(logDir, name), "utf8")));
|
|
247
|
+
} catch {}
|
|
248
|
+
}
|
|
249
|
+
return out;
|
|
250
|
+
}
|
|
251
|
+
function loadStatus(logDir, id) {
|
|
252
|
+
const path = statusPath(logDir, id);
|
|
253
|
+
if (!existsSync(path)) return null;
|
|
254
|
+
try {
|
|
255
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
256
|
+
} catch {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function alive(pid) {
|
|
261
|
+
if (!pid || pid <= 0) return false;
|
|
262
|
+
try {
|
|
263
|
+
process.kill(pid, 0);
|
|
264
|
+
return true;
|
|
265
|
+
} catch {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/** "42s" under 90s, "7m" above — the coarse age the table and bar show. */
|
|
270
|
+
function ageOf(ts, now = /* @__PURE__ */ new Date()) {
|
|
271
|
+
const t = Date.parse(ts.replace(" ", "T"));
|
|
272
|
+
if (Number.isNaN(t)) return "?";
|
|
273
|
+
const s = Math.max(0, Math.floor((now.getTime() - t) / 1e3));
|
|
274
|
+
return s < 90 ? `${s}s` : `${Math.floor(s / 60)}m`;
|
|
275
|
+
}
|
|
276
|
+
/** One-line description of a tool call, e.g. "Bash: npm test". */
|
|
277
|
+
function describeTool(name, inp) {
|
|
278
|
+
if (typeof inp !== "object" || inp === null) return name;
|
|
279
|
+
const i = inp;
|
|
280
|
+
const get = (k) => typeof i[k] === "string" ? i[k] : "";
|
|
281
|
+
if (name === "Bash") return `Bash: ${shortText(get("command"), 70)}`;
|
|
282
|
+
if (name === "Read" || name === "Edit" || name === "Write" || name === "MultiEdit") return `${name}: ${get("file_path").split("/").pop() ?? ""}`;
|
|
283
|
+
if (name === "Grep" || name === "Glob") return `${name}: ${shortText(get("pattern"), 50)}`;
|
|
284
|
+
if (name === "WebSearch") return `${name}: ${shortText(get("query"), 50)}`;
|
|
285
|
+
if (name === "WebFetch") return `${name}: ${shortText(get("url"), 60)}`;
|
|
286
|
+
return name;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
//#endregion
|
|
290
|
+
//#region src/workers/scope.ts
|
|
291
|
+
/**
|
|
292
|
+
* scope.ts — which workers "belong" to the terminal asking about them.
|
|
293
|
+
*
|
|
294
|
+
* Two identities, in priority order:
|
|
295
|
+
*
|
|
296
|
+
* 1. AIBroker's session registry (~/.aibroker/session-names.json): iTerm
|
|
297
|
+
* session UUID → session name. The launching session's UUID is stored in
|
|
298
|
+
* each worker's status file as `session.id`, so every pane of a named
|
|
299
|
+
* session sees its workers — regardless of tab layout.
|
|
300
|
+
* 2. The iTerm tab key (`w<window>t<tab>` prefix of ITERM_SESSION_ID): the
|
|
301
|
+
* original heuristic, kept as the fallback when no registry entry
|
|
302
|
+
* matches (AIBroker absent, unnamed session, non-iTerm terminal).
|
|
303
|
+
*
|
|
304
|
+
* If neither is available, viewers fall back to "all workers".
|
|
305
|
+
*
|
|
306
|
+
* A third case: workers spawned from a Claude Code Bash tool (the
|
|
307
|
+
* orchestrator pattern) have neither — Claude Code exports no terminal or
|
|
308
|
+
* session identity to its Bash children. Those record `spawnerSession`, the
|
|
309
|
+
* orchestrator's claude session id bridged through the status line's
|
|
310
|
+
* session map (see below), so their tab still claims them.
|
|
311
|
+
*/
|
|
312
|
+
const AIBROKER_REGISTRY = join(homedir(), ".aibroker", "session-names.json");
|
|
313
|
+
/**
|
|
314
|
+
* `w<window>t<tab>` prefix of an iTerm session id, "" if empty or malformed.
|
|
315
|
+
* Panes split from the same tab share this prefix and differ only in `p<n>`.
|
|
316
|
+
*/
|
|
317
|
+
function tabKey(term) {
|
|
318
|
+
if (!term) return "";
|
|
319
|
+
const head = term.split("p", 1)[0];
|
|
320
|
+
const parts = head.slice(1).split("t");
|
|
321
|
+
if (head.startsWith("w") && parts.length === 2 && parts.every((p) => /^\d+$/.test(p))) return head;
|
|
322
|
+
return "";
|
|
323
|
+
}
|
|
324
|
+
/** Tab key of the iTerm tab this process runs in, "" if not inside iTerm2. */
|
|
325
|
+
function currentTabKey(env = process.env) {
|
|
326
|
+
return tabKey(env.ITERM_SESSION_ID ?? "");
|
|
327
|
+
}
|
|
328
|
+
/** The iTerm UUID part of an ITERM_SESSION_ID (after the last colon). */
|
|
329
|
+
function itermUuid(term) {
|
|
330
|
+
if (!term) return "";
|
|
331
|
+
return term.split(":").pop() ?? "";
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Resolve the AIBroker session for an ITERM_SESSION_ID. Reads the persistent
|
|
335
|
+
* name registry (the same store `aibroker_rename` writes); returns null when
|
|
336
|
+
* AIBroker is absent or the session is not in it — callers then use the tab
|
|
337
|
+
* key, which is the pre-AIBroker behaviour.
|
|
338
|
+
*/
|
|
339
|
+
function resolveSession(term, registryPath = AIBROKER_REGISTRY) {
|
|
340
|
+
const uuid = itermUuid(term);
|
|
341
|
+
if (!uuid) return null;
|
|
342
|
+
let names;
|
|
343
|
+
try {
|
|
344
|
+
if (!existsSync(registryPath)) return null;
|
|
345
|
+
names = JSON.parse(readFileSync(registryPath, "utf8"));
|
|
346
|
+
} catch {
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
const name = names[uuid];
|
|
350
|
+
if (typeof name !== "string" || !name) return null;
|
|
351
|
+
return {
|
|
352
|
+
id: uuid,
|
|
353
|
+
name
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* True when `worker` was launched from `term` (the terminal asking about it).
|
|
358
|
+
* Session-id match first; the tab key only when no registry entry matched —
|
|
359
|
+
* a session without a name keeps the tab-scoped behaviour it always had.
|
|
360
|
+
*/
|
|
361
|
+
function workerInScope(worker, term) {
|
|
362
|
+
if (!term) return false;
|
|
363
|
+
const uuid = itermUuid(term);
|
|
364
|
+
if (worker.session?.id && uuid) return worker.session.id === uuid;
|
|
365
|
+
return tabKey(worker.term) === tabKey(term) && tabKey(term) !== "";
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Registry key for pane files: the session id when AIBroker knows this
|
|
369
|
+
* terminal, else the tab key, else the raw iTerm UUID.
|
|
370
|
+
*/
|
|
371
|
+
function scopeKey(term) {
|
|
372
|
+
const session = resolveSession(term);
|
|
373
|
+
if (session) return session.id;
|
|
374
|
+
return currentTabKey() || itermUuid(term);
|
|
375
|
+
}
|
|
376
|
+
/** `[Name]` when the worker has an AIBroker session name, else "". */
|
|
377
|
+
function sessionTag(worker) {
|
|
378
|
+
return worker.session?.name ? `[${worker.session.name}]` : "";
|
|
379
|
+
}
|
|
380
|
+
/** How old a map entry may be for a spawn to adopt it (status lines refresh constantly while a session lives). */
|
|
381
|
+
const SPAWNER_SESSION_TTL_MS = 10 * 6e4;
|
|
382
|
+
/** Map entries not refreshed within this window are pruned on write. */
|
|
383
|
+
const SESSION_MAP_PRUNE_MS = 60 * 6e4;
|
|
384
|
+
function sessionMapPath(logDir) {
|
|
385
|
+
return join(logDir, "claude-session-map.json");
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* The claude session a new run was spawned by, when it can be known: a run
|
|
389
|
+
* inside another worker inherits its spawner (chain stages keep the
|
|
390
|
+
* orchestrator's tab that way); otherwise a fresh map entry for `cwd`.
|
|
391
|
+
*/
|
|
392
|
+
function resolveSpawnerSession(logDir, cwd, env = process.env, now = Date.now()) {
|
|
393
|
+
const parentId = env.PAI_WORKER_ID;
|
|
394
|
+
if (parentId) {
|
|
395
|
+
const inherited = loadStatus(logDir, parentId)?.spawnerSession;
|
|
396
|
+
if (inherited) return inherited;
|
|
397
|
+
}
|
|
398
|
+
const path = sessionMapPath(logDir);
|
|
399
|
+
if (!existsSync(path) || !cwd) return null;
|
|
400
|
+
try {
|
|
401
|
+
const entry = JSON.parse(readFileSync(path, "utf8"))[cwd];
|
|
402
|
+
if (entry && entry.session && now - entry.ts < SPAWNER_SESSION_TTL_MS) return entry.session;
|
|
403
|
+
} catch {}
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* The iTerm UUID a claude session last rendered its status line in, when the
|
|
408
|
+
* map knows one: the freshest entry naming that session. This is the reverse
|
|
409
|
+
* of resolveSpawnerSession — worker → orchestrator there, orchestrator →
|
|
410
|
+
* terminal here — and it exists so daemon-side pushes (supervision) can reach
|
|
411
|
+
* a session that has no iTerm identity of its own in any worker status.
|
|
412
|
+
*/
|
|
413
|
+
function itermForClaudeSession(logDir, claudeSession, now = Date.now()) {
|
|
414
|
+
const path = sessionMapPath(logDir);
|
|
415
|
+
if (!existsSync(path) || !claudeSession) return null;
|
|
416
|
+
let map;
|
|
417
|
+
try {
|
|
418
|
+
map = JSON.parse(readFileSync(path, "utf8"));
|
|
419
|
+
} catch {
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
let best = null;
|
|
423
|
+
for (const e of Object.values(map)) {
|
|
424
|
+
if (e.session !== claudeSession || !e.term) continue;
|
|
425
|
+
if (now - e.ts >= SESSION_MAP_PRUNE_MS) continue;
|
|
426
|
+
if (!best || e.ts > best.ts) best = e;
|
|
427
|
+
}
|
|
428
|
+
const term = best?.term;
|
|
429
|
+
return term ? itermUuid(term) : null;
|
|
430
|
+
}
|
|
431
|
+
|
|
80
432
|
//#endregion
|
|
81
433
|
//#region src/workers/pane.ts
|
|
82
434
|
/**
|
|
@@ -87,10 +439,14 @@ function ledgerSummary(path, scope, lastN = 15, now = /* @__PURE__ */ new Date()
|
|
|
87
439
|
* one splits the lowest live worker pane horizontally, so panes stack top to
|
|
88
440
|
* bottom. The split never sizes the new session — that would grow the whole
|
|
89
441
|
* window; instead the window's bounds are read before the split and restored
|
|
90
|
-
* right after, so the panes share the space the window already had.
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
442
|
+
* right after, so the panes share the space the window already had. The follow
|
|
443
|
+
* command is part of the split itself — iTerm creates the new session already
|
|
444
|
+
* running it — so no text is ever typed into any session afterwards; a separate
|
|
445
|
+
* typing step raced the operator's keystrokes (focus briefly sits on the new
|
|
446
|
+
* pane after a split, and in one observed failure the command landed in the
|
|
447
|
+
* operator's own shell). A split still makes the new session the tab's active
|
|
448
|
+
* one — the scripts re-select the launching session right after, so a pane
|
|
449
|
+
* opening never steals the keystrokes the operator is typing. Each
|
|
94
450
|
* pane runs `pai worker follow <id> --auto-exit <n>` under the `pai-worker`
|
|
95
451
|
* dynamic profile (Close Sessions On End), so panes disappear by themselves.
|
|
96
452
|
*
|
|
@@ -145,34 +501,35 @@ const WORKER_SPLIT_SCRIPT = `on run(argv)
|
|
|
145
501
|
end if
|
|
146
502
|
end if
|
|
147
503
|
end repeat
|
|
504
|
+
-- the follow command is part of the split itself: the
|
|
505
|
+
-- pane is born already running it, so no text is ever
|
|
506
|
+
-- typed into any session — a typing step raced the
|
|
507
|
+
-- operator's keystrokes and could even land in the
|
|
508
|
+
-- operator's own session
|
|
148
509
|
if profileName is "" then
|
|
149
510
|
if splitS is missing value then
|
|
150
511
|
tell s
|
|
151
|
-
set newS to split vertically with default profile
|
|
512
|
+
set newS to split vertically with default profile command followCmd
|
|
152
513
|
end tell
|
|
153
514
|
else
|
|
154
515
|
tell splitS
|
|
155
|
-
set newS to split horizontally with default profile
|
|
516
|
+
set newS to split horizontally with default profile command followCmd
|
|
156
517
|
end tell
|
|
157
518
|
end if
|
|
158
519
|
else
|
|
159
520
|
if splitS is missing value then
|
|
160
521
|
tell s
|
|
161
|
-
set newS to split vertically with profile profileName
|
|
522
|
+
set newS to split vertically with profile profileName command followCmd
|
|
162
523
|
end tell
|
|
163
524
|
else
|
|
164
525
|
tell splitS
|
|
165
|
-
set newS to split horizontally with profile profileName
|
|
526
|
+
set newS to split horizontally with profile profileName command followCmd
|
|
166
527
|
end tell
|
|
167
528
|
end if
|
|
168
529
|
end if
|
|
169
|
-
tell newS
|
|
170
|
-
write text followCmd
|
|
171
|
-
end tell
|
|
172
530
|
-- a split makes the new session the tab's active one:
|
|
173
|
-
-- re-select the launching session so
|
|
174
|
-
--
|
|
175
|
-
-- pane's chat prompt
|
|
531
|
+
-- re-select the launching session so focus returns to
|
|
532
|
+
-- where the operator was typing, never the new pane
|
|
176
533
|
try
|
|
177
534
|
select s
|
|
178
535
|
end try
|
|
@@ -253,11 +610,11 @@ const SPLIT_SCRIPT = `on run(argv)
|
|
|
253
610
|
repeat with t in tabs of w
|
|
254
611
|
repeat with s in sessions of t
|
|
255
612
|
if id of s is targetID then
|
|
613
|
+
-- the follow command is part of the split itself (see
|
|
614
|
+
-- WORKER_SPLIT_SCRIPT): nothing is ever typed into a
|
|
615
|
+
-- session afterwards
|
|
256
616
|
tell s
|
|
257
|
-
set newS to split vertically with default profile
|
|
258
|
-
end tell
|
|
259
|
-
tell newS
|
|
260
|
-
write text followCmd
|
|
617
|
+
set newS to split vertically with default profile command followCmd
|
|
261
618
|
end tell
|
|
262
619
|
-- keep the tab's active session where it was (see
|
|
263
620
|
-- WORKER_SPLIT_SCRIPT)
|
|
@@ -490,6 +847,23 @@ function saveRegistry(path, reg) {
|
|
|
490
847
|
writeFileSync(tmp, JSON.stringify(reg, null, 1), "utf8");
|
|
491
848
|
renameSync(tmp, path);
|
|
492
849
|
}
|
|
850
|
+
/**
|
|
851
|
+
* The command a worker pane runs: one exec-able line with no shell syntax —
|
|
852
|
+
* iTerm's split command parameter execs without a shell (proven live: panes
|
|
853
|
+
* survive `sleep 30` and a single-quoted `/bin/sh -c`, die instantly on
|
|
854
|
+
* `export …; …` or a redirect), so the command must be one exec-able line.
|
|
855
|
+
* Absolute node on the CLI entry file also sidesteps the panes' bare launchd
|
|
856
|
+
* PATH (no pai, no node) and the `#!/usr/bin/env node` shebang.
|
|
857
|
+
*/
|
|
858
|
+
function followCommand(wid, autoExitSecs) {
|
|
859
|
+
return `${process.execPath} ${absoluteCliPath()} worker follow ${wid} --auto-exit ${autoExitSecs}`;
|
|
860
|
+
}
|
|
861
|
+
/** Absolute path of the running pai CLI; falls back to PATH lookup at spawn. */
|
|
862
|
+
function absoluteCliPath() {
|
|
863
|
+
const argv1 = process.argv[1] ?? "";
|
|
864
|
+
if (argv1.endsWith("pai")) return argv1;
|
|
865
|
+
return "pai";
|
|
866
|
+
}
|
|
493
867
|
/** Open (or only report on) the stacked follow pane of one worker. */
|
|
494
868
|
async function openPaneForWorker(logDir, config, wid, term) {
|
|
495
869
|
if (workerPaneOpen(wid)) return `pane for ${wid} already open`;
|
|
@@ -497,7 +871,7 @@ async function openPaneForWorker(logDir, config, wid, term) {
|
|
|
497
871
|
const regPath = join(panesDir(logDir), `${scopeKey(term)}.json`);
|
|
498
872
|
const reg = loadRegistry(regPath);
|
|
499
873
|
const profile = await followProfile(config.pane.fontSize);
|
|
500
|
-
const cmd =
|
|
874
|
+
const cmd = followCommand(wid, config.pane.autoExitSecs);
|
|
501
875
|
const p = await osascript(WORKER_SPLIT_SCRIPT, [
|
|
502
876
|
uid,
|
|
503
877
|
[...reg].reverse().map((e) => e.session).join(","),
|
|
@@ -571,7 +945,7 @@ async function openFollowPane(logDir, _config, term, checkOnly) {
|
|
|
571
945
|
if (out === "notrunning") throw new Error("pai worker pane: iTerm2 is not running");
|
|
572
946
|
if ([...new Set(out.split(",").map((t) => t.trim()).filter(Boolean))].filter((t) => followTtys().has(t)).length > 0) return "follow pane already open";
|
|
573
947
|
if (checkOnly) return "no follow pane";
|
|
574
|
-
const s = await osascript(SPLIT_SCRIPT, [uid,
|
|
948
|
+
const s = await osascript(SPLIT_SCRIPT, [uid, `${process.execPath} ${absoluteCliPath()} worker follow`]);
|
|
575
949
|
const sout = s.stdout.trim();
|
|
576
950
|
if (sout === "opened") return "follow pane opened";
|
|
577
951
|
if (sout === "notfound") throw new Error("pai worker pane: no open iTerm2 session matches ITERM_SESSION_ID");
|
|
@@ -1627,8 +2001,12 @@ function codexInstalled() {
|
|
|
1627
2001
|
* demand, the provider name in the URL path); codex-engine providers run
|
|
1628
2002
|
* the Codex CLI.
|
|
1629
2003
|
* - headless (-p): strict empty MCP config unless the caller brings one or
|
|
1630
|
-
* names servers via --mcp / a role (then a filtered <id>.mcp.json),
|
|
1631
|
-
*
|
|
2004
|
+
* names servers via --mcp / a role (then a filtered <id>.mcp.json), the
|
|
2005
|
+
* core tool grants the caller did not bring (a headless run cannot
|
|
2006
|
+
* approve a permission-gated tool mid-flight — with no grant at all,
|
|
2007
|
+
* claude drops the file/shell tools entirely and the worker is
|
|
2008
|
+
* tool-blind), PAI_WORKER=1 so PAI's per-session hooks leave it alone,
|
|
2009
|
+
* the worker
|
|
1632
2010
|
* contract appended to the system prompt, `--input-format stream-json`
|
|
1633
2011
|
* with the prompt as the first stdin user message (the operator socket
|
|
1634
2012
|
* can add more mid-run), stream-json mirroring (every line stamped `_ts`)
|
|
@@ -1641,15 +2019,6 @@ function codexInstalled() {
|
|
|
1641
2019
|
* Auto-routed runs that die of a quota error before the first tool call are
|
|
1642
2020
|
* restarted on the next provider in routing order (WORKER-REROUTE ledger line).
|
|
1643
2021
|
*/
|
|
1644
|
-
/** The strict empty MCP config for headless workers, written on demand. */
|
|
1645
|
-
function ensureNoMcpConfig(logDir) {
|
|
1646
|
-
const path = noMcpConfigPath(logDir);
|
|
1647
|
-
if (!existsSync(path)) {
|
|
1648
|
-
mkdirSync(logDir, { recursive: true });
|
|
1649
|
-
writeFileSync(path, "{ \"mcpServers\": {} }\n", "utf8");
|
|
1650
|
-
}
|
|
1651
|
-
return path;
|
|
1652
|
-
}
|
|
1653
2022
|
/** A stream-json user message for the child's stdin. */
|
|
1654
2023
|
function stdinUserMessage(text) {
|
|
1655
2024
|
return JSON.stringify({
|
|
@@ -1725,7 +2094,7 @@ async function runWorker(opts) {
|
|
|
1725
2094
|
const logDir = workersLogDir(config);
|
|
1726
2095
|
mkdirSync(logDir, { recursive: true });
|
|
1727
2096
|
if (opts.className === "plan" && !opts._planner) {
|
|
1728
|
-
const { runPlanner } = await import("./planner-
|
|
2097
|
+
const { runPlanner } = await import("./planner-BfEGcEs1.mjs");
|
|
1729
2098
|
return runPlanner(opts);
|
|
1730
2099
|
}
|
|
1731
2100
|
const parent = launchParent(opts.parent);
|
|
@@ -1784,6 +2153,19 @@ async function runWorker(opts) {
|
|
|
1784
2153
|
throw e;
|
|
1785
2154
|
}
|
|
1786
2155
|
}
|
|
2156
|
+
/**
|
|
2157
|
+
* The core tools a headless run grants when the caller brings none of its
|
|
2158
|
+
* own. Permission-gated tools (Bash, Read, Write, …) are never offered to a
|
|
2159
|
+
* headless session that has no allow rule for them — the child starts with
|
|
2160
|
+
* only the tools that never ask (tool search, web, cron, tasks) and cannot
|
|
2161
|
+
* touch a file or shell (2026-09-18). A caller's own --allowedTools is a
|
|
2162
|
+
* deliberate restriction and passes through untouched.
|
|
2163
|
+
*/
|
|
2164
|
+
const DEFAULT_WORKER_TOOLS = "Read,Edit,Write,Bash,Grep,Glob";
|
|
2165
|
+
/** The --allowedTools args a headless run needs; [] when the caller granted. */
|
|
2166
|
+
function headlessToolGrants(allowedTools) {
|
|
2167
|
+
return allowedTools.length ? [] : ["--allowedTools", DEFAULT_WORKER_TOOLS];
|
|
2168
|
+
}
|
|
1787
2169
|
async function executeRun(a) {
|
|
1788
2170
|
const { config, logDir, target, model, label, parsed, noPane } = a;
|
|
1789
2171
|
const headless = parsed.headless;
|
|
@@ -1872,9 +2254,10 @@ async function executeRun(a) {
|
|
|
1872
2254
|
];
|
|
1873
2255
|
}
|
|
1874
2256
|
const restArgs = headless ? stripPromptValues(parsed.rest) : parsed.rest;
|
|
2257
|
+
const toolArgs = headless ? headlessToolGrants(parsed.allowedTools) : [];
|
|
1875
2258
|
const cmd = ["claude"];
|
|
1876
|
-
if (!parsed.callerModel) cmd.push("--model", model);
|
|
1877
|
-
cmd.push(...mcpArgs, ...restArgs);
|
|
2259
|
+
if (!parsed.callerModel && model) cmd.push("--model", model);
|
|
2260
|
+
cmd.push(...mcpArgs, ...toolArgs, ...restArgs);
|
|
1878
2261
|
if (headless) {
|
|
1879
2262
|
cmd.push("--output-format", "stream-json", "--verbose", "--input-format", "stream-json");
|
|
1880
2263
|
if (!parsed.callerSystemPrompt) cmd.push("--append-system-prompt", WORKER_CONTRACT_PROMPT);
|
|
@@ -2410,161 +2793,5 @@ function resultFromOutput(out) {
|
|
|
2410
2793
|
}
|
|
2411
2794
|
|
|
2412
2795
|
//#endregion
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
* chain.ts — draft-then-implement chains: `--chain draft,implement[,review]`.
|
|
2416
|
-
*
|
|
2417
|
-
* The chain runs each stage as its own worker (own id, own pane, `parent` set
|
|
2418
|
-
* to the chain id), so `ps` shows the chain as a tree and every stage can be
|
|
2419
|
-
* followed, replayed and said to like any other worker:
|
|
2420
|
-
*
|
|
2421
|
-
* - draft turns the operator's brief into a full spec file under
|
|
2422
|
-
* <logDir>/specs/<chain id>.md (goal, constraints, files likely
|
|
2423
|
-
* touched, acceptance checks, verification commands);
|
|
2424
|
-
* - any other stage (implement, plan, …) runs with that spec as its prompt
|
|
2425
|
-
* and the original brief attached;
|
|
2426
|
-
* - review reads the spec and the working-tree diff and produces the
|
|
2427
|
-
* structured report.
|
|
2428
|
-
*
|
|
2429
|
-
* A stage that fails (or a draft that produces no spec file) stops the chain;
|
|
2430
|
-
* the caller then writes the spec itself and re-runs without the draft stage.
|
|
2431
|
-
*/
|
|
2432
|
-
/** Where a chain's spec file lives: <logDir>/specs/<chain id>.md. */
|
|
2433
|
-
function specPathFor(logDir, chainId) {
|
|
2434
|
-
return join(logDir, "specs", `${chainId}.md`);
|
|
2435
|
-
}
|
|
2436
|
-
/**
|
|
2437
|
-
* Replace the caller's -p value with `prompt`, dropping every existing
|
|
2438
|
-
* -p/--print pair first (two -p flags on one claude command line are an error,
|
|
2439
|
-
* so the chain must never leave the brief in place when swapping prompts).
|
|
2440
|
-
*/
|
|
2441
|
-
function swapPromptArg(claudeArgs, prompt) {
|
|
2442
|
-
const out = [];
|
|
2443
|
-
for (let i = 0; i < claudeArgs.length; i++) {
|
|
2444
|
-
const a = claudeArgs[i];
|
|
2445
|
-
if (a === "-p" || a === "--print") {
|
|
2446
|
-
if (i + 1 < claudeArgs.length && !claudeArgs[i + 1].startsWith("-")) i += 1;
|
|
2447
|
-
continue;
|
|
2448
|
-
}
|
|
2449
|
-
out.push(a);
|
|
2450
|
-
}
|
|
2451
|
-
out.push("-p", prompt);
|
|
2452
|
-
return out;
|
|
2453
|
-
}
|
|
2454
|
-
function draftPrompt(brief, specPath) {
|
|
2455
|
-
return [
|
|
2456
|
-
"You are the DRAFT stage of a worker chain. Turn the operator's brief below into a full implementation spec and write it to",
|
|
2457
|
-
specPath,
|
|
2458
|
-
"with the Write tool.",
|
|
2459
|
-
"",
|
|
2460
|
-
"Use exactly these five sections as Markdown headings:",
|
|
2461
|
-
"# Goal",
|
|
2462
|
-
"# Constraints",
|
|
2463
|
-
"# Files likely touched",
|
|
2464
|
-
"# Acceptance checks",
|
|
2465
|
-
"# Verification commands",
|
|
2466
|
-
"",
|
|
2467
|
-
"Read the repository first (Glob/Grep/Read) so the spec names real files and real commands. Do not implement anything.",
|
|
2468
|
-
"",
|
|
2469
|
-
"## Operator brief",
|
|
2470
|
-
"",
|
|
2471
|
-
brief
|
|
2472
|
-
].join("\n");
|
|
2473
|
-
}
|
|
2474
|
-
function implementPrompt(brief, specPath, spec) {
|
|
2475
|
-
return [
|
|
2476
|
-
...spec ? [
|
|
2477
|
-
"You are the IMPLEMENT stage of a worker chain. The draft stage wrote the spec below (also at " + specPath + "). Implement it exactly, then run the spec's verification commands before finishing.",
|
|
2478
|
-
"",
|
|
2479
|
-
"## Spec",
|
|
2480
|
-
"",
|
|
2481
|
-
spec
|
|
2482
|
-
] : ["Implement the operator's brief below."],
|
|
2483
|
-
"",
|
|
2484
|
-
"## Operator brief",
|
|
2485
|
-
"",
|
|
2486
|
-
brief
|
|
2487
|
-
].join("\n");
|
|
2488
|
-
}
|
|
2489
|
-
function reviewPrompt(brief, specPath, spec) {
|
|
2490
|
-
return [
|
|
2491
|
-
"You are the REVIEW stage of a worker chain. The implement stage just ran. Read the repository's diff (run `git diff` and `git status`; use `git diff --stat` for the overview) and check it against the spec's acceptance checks and verification commands — run the checks when they are cheap. Do not fix anything you find; report it.",
|
|
2492
|
-
...spec ? [
|
|
2493
|
-
"",
|
|
2494
|
-
"## Spec (also at " + specPath + ")",
|
|
2495
|
-
"",
|
|
2496
|
-
spec
|
|
2497
|
-
] : [],
|
|
2498
|
-
"",
|
|
2499
|
-
"## Operator brief",
|
|
2500
|
-
"",
|
|
2501
|
-
brief
|
|
2502
|
-
].join("\n");
|
|
2503
|
-
}
|
|
2504
|
-
/** Run a chain of stages; returns the exit code of the first failed stage, 0 when all pass. */
|
|
2505
|
-
async function runChain(opts, deps = {}) {
|
|
2506
|
-
const stages = opts.stages.map((s) => s.trim()).filter(Boolean);
|
|
2507
|
-
if (!stages.length) throw new Error("--chain needs at least one class, e.g. --chain draft,implement");
|
|
2508
|
-
const runStage = deps.runStage ?? runWorker;
|
|
2509
|
-
const logDir = deps.logDir ?? workersLogDir(readWorkersSection().workers);
|
|
2510
|
-
const chainId = newWorkerId();
|
|
2511
|
-
const specPath = specPathFor(logDir, chainId);
|
|
2512
|
-
mkdirSync(join(logDir, "specs"), { recursive: true });
|
|
2513
|
-
const baseLabel = opts.label ?? shortText(opts.brief, 40);
|
|
2514
|
-
appendLedger(ledgerPath(logDir), "WORKER-CHAIN", {
|
|
2515
|
-
chain: chainId,
|
|
2516
|
-
stages: stages.join(","),
|
|
2517
|
-
label: baseLabel
|
|
2518
|
-
});
|
|
2519
|
-
opts.onChainStart?.(chainId);
|
|
2520
|
-
let spec = null;
|
|
2521
|
-
for (let i = 0; i < stages.length; i++) {
|
|
2522
|
-
const stage = stages[i];
|
|
2523
|
-
if (stage === "draft") spec = null;
|
|
2524
|
-
else if (spec === null && existsSync(specPath)) spec = readFileSync(specPath, "utf8");
|
|
2525
|
-
const prompt = stage === "draft" ? draftPrompt(opts.brief, specPath) : stage === "review" ? reviewPrompt(opts.brief, spec ? specPath : null, spec) : implementPrompt(opts.brief, spec ? specPath : null, spec);
|
|
2526
|
-
process.stderr.write(`chain ${chainId}: stage ${i + 1}/${stages.length} ${stage} (spec: ${specPath})\n`);
|
|
2527
|
-
const rc = await runStage({
|
|
2528
|
-
className: opts.className ?? stage,
|
|
2529
|
-
providerFlag: opts.providerFlag,
|
|
2530
|
-
modelFlag: opts.modelFlag,
|
|
2531
|
-
label: `${baseLabel} · ${stage}`,
|
|
2532
|
-
noPane: opts.noPane,
|
|
2533
|
-
mcpFlag: opts.mcpFlag,
|
|
2534
|
-
claudeArgs: swapPromptArg(opts.claudeArgs, prompt),
|
|
2535
|
-
cwd: opts.cwd,
|
|
2536
|
-
parent: chainId,
|
|
2537
|
-
stage,
|
|
2538
|
-
quiet: opts.quiet
|
|
2539
|
-
});
|
|
2540
|
-
if (stage === "draft") {
|
|
2541
|
-
if (!existsSync(specPath)) {
|
|
2542
|
-
process.stderr.write(`chain ${chainId}: draft stage produced no spec at ${specPath} — stopping. Write the spec yourself and re-run without the draft stage.\n`);
|
|
2543
|
-
appendLedger(ledgerPath(logDir), "WORKER-CHAIN-END", {
|
|
2544
|
-
chain: chainId,
|
|
2545
|
-
rc: rc !== 0 ? rc : 1,
|
|
2546
|
-
failed: "draft"
|
|
2547
|
-
});
|
|
2548
|
-
return rc !== 0 ? rc : 1;
|
|
2549
|
-
}
|
|
2550
|
-
spec = readFileSync(specPath, "utf8");
|
|
2551
|
-
}
|
|
2552
|
-
if (rc !== 0) {
|
|
2553
|
-
appendLedger(ledgerPath(logDir), "WORKER-CHAIN-END", {
|
|
2554
|
-
chain: chainId,
|
|
2555
|
-
rc,
|
|
2556
|
-
failed: stage
|
|
2557
|
-
});
|
|
2558
|
-
return rc;
|
|
2559
|
-
}
|
|
2560
|
-
}
|
|
2561
|
-
appendLedger(ledgerPath(logDir), "WORKER-CHAIN-END", {
|
|
2562
|
-
chain: chainId,
|
|
2563
|
-
rc: 0
|
|
2564
|
-
});
|
|
2565
|
-
return 0;
|
|
2566
|
-
}
|
|
2567
|
-
|
|
2568
|
-
//#endregion
|
|
2569
|
-
export { openPaneForWorker as _, testProvider as a, renderReport as c, handoffFromInside as d, readInbox as f, openFollowPane as g, checkPaneForWorker as h, runWorker as i, discardWorker as l, workerDepth as m, swapPromptArg as n, describeMcp as o, sayToWorker as p, printResult as r, parseWorkerReport as s, runChain as t, mergeWorker as u, appendLedger as v, ledgerSummary as y };
|
|
2570
|
-
//# sourceMappingURL=chain-DGXNoTwc.mjs.map
|
|
2796
|
+
export { nowStamp as A, alive as C, loadStatus as D, isChatPane as E, ledgerSummary as M, parseRunnerArgs as N, loadStatuses as O, shortText as P, ageOf as S, contextPercent as T, itermForClaudeSession as _, parseWorkerReport as a, workerInScope as b, mergeWorker as c, sayToWorker as d, workerDepth as f, currentTabKey as g, openPaneForWorker as h, describeMcp as i, appendLedger as j, newWorkerId as k, handoffFromInside as l, openFollowPane as m, runWorker as n, renderReport as o, checkPaneForWorker as p, testProvider as r, discardWorker as s, printResult as t, readInbox as u, resolveSession as v, contextLabel as w, UNLABELED as x, sessionTag as y };
|
|
2797
|
+
//# sourceMappingURL=run-CGXbhf2k.mjs.map
|