@tekmidian/pai 0.42.1 → 0.42.3
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-Bi_F6lXf.mjs → config-_lQI_W08.mjs} +87 -3
- package/dist/config-_lQI_W08.mjs.map +1 -0
- package/dist/{context-handover-cache--bm6Siim.mjs → context-handover-cache-pNFdBNrU.mjs} +3 -3
- package/dist/{context-handover-cache--bm6Siim.mjs.map → context-handover-cache-pNFdBNrU.mjs.map} +1 -1
- package/dist/daemon/index.mjs +7 -7
- package/dist/{daemon-BOAPyL_b.mjs → daemon-Awt_3UJp.mjs} +7 -7
- package/dist/{daemon-DvXih78C.mjs → daemon-Dq8A1uDF.mjs} +587 -368
- package/dist/daemon-Dq8A1uDF.mjs.map +1 -0
- package/dist/daemon-mcp/index.mjs +5 -5
- package/dist/{fallback-8bzmg6No.mjs → fallback-vQPI1qxt.mjs} +14 -12
- 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 +64 -1
- 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-CqppP_Bw.mjs → main-resolver-BAfOMppW.mjs} +2 -2
- package/dist/{main-resolver-Dip2VLB5.mjs → main-resolver-DnkxPYkr.mjs} +111 -16
- package/dist/main-resolver-DnkxPYkr.mjs.map +1 -0
- package/dist/{planner-ogy-Zy4O.mjs → planner-BfEGcEs1.mjs} +5 -5
- package/dist/{planner-ogy-Zy4O.mjs.map → planner-BfEGcEs1.mjs.map} +1 -1
- package/dist/{program-B7N5OIJR.mjs → program-BDmeZP-v.mjs} +17 -11
- package/dist/{program-B7N5OIJR.mjs.map → program-BDmeZP-v.mjs.map} +1 -1
- package/dist/{chain-DsvSHnO9.mjs → run-CGXbhf2k.mjs} +359 -163
- package/dist/run-CGXbhf2k.mjs.map +1 -0
- package/dist/{server-DvN-IFIV.mjs → server-D4vAieF4.mjs} +25 -6
- 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-19r0cpH4.mjs → work-queue-worker-AEmfVE0F.mjs} +5 -5
- package/dist/{work-queue-worker-DXh_FnT5.mjs → work-queue-worker-DIT-VLSh.mjs} +3 -3
- package/dist/{work-queue-worker-DXh_FnT5.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/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-DsvSHnO9.mjs.map +0 -1
- package/dist/config-Bi_F6lXf.mjs.map +0 -1
- package/dist/daemon-DvXih78C.mjs.map +0 -1
- package/dist/fallback-8bzmg6No.mjs.map +0 -1
- package/dist/main-resolver-Dip2VLB5.mjs.map +0 -1
- package/dist/scope-CfYnjy5R.mjs +0 -360
- package/dist/scope-CfYnjy5R.mjs.map +0 -1
- package/dist/server-DvN-IFIV.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 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-
|
|
3
|
-
import { C as stripPromptValues, S as shortText, _ as loadStatuses, a as resolveSpawnerSession, b as saveStatus, d as alive, g as loadStatus, i as resolveSession, l as UNLABELED, m as describeTool, o as scopeKey, r as itermUuid, v as newWorkerId, x as parseRunnerArgs, y as nowStamp } from "./scope-CfYnjy5R.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
|
/**
|
|
@@ -1742,7 +2094,7 @@ async function runWorker(opts) {
|
|
|
1742
2094
|
const logDir = workersLogDir(config);
|
|
1743
2095
|
mkdirSync(logDir, { recursive: true });
|
|
1744
2096
|
if (opts.className === "plan" && !opts._planner) {
|
|
1745
|
-
const { runPlanner } = await import("./planner-
|
|
2097
|
+
const { runPlanner } = await import("./planner-BfEGcEs1.mjs");
|
|
1746
2098
|
return runPlanner(opts);
|
|
1747
2099
|
}
|
|
1748
2100
|
const parent = launchParent(opts.parent);
|
|
@@ -1904,7 +2256,7 @@ async function executeRun(a) {
|
|
|
1904
2256
|
const restArgs = headless ? stripPromptValues(parsed.rest) : parsed.rest;
|
|
1905
2257
|
const toolArgs = headless ? headlessToolGrants(parsed.allowedTools) : [];
|
|
1906
2258
|
const cmd = ["claude"];
|
|
1907
|
-
if (!parsed.callerModel) cmd.push("--model", model);
|
|
2259
|
+
if (!parsed.callerModel && model) cmd.push("--model", model);
|
|
1908
2260
|
cmd.push(...mcpArgs, ...toolArgs, ...restArgs);
|
|
1909
2261
|
if (headless) {
|
|
1910
2262
|
cmd.push("--output-format", "stream-json", "--verbose", "--input-format", "stream-json");
|
|
@@ -2441,161 +2793,5 @@ function resultFromOutput(out) {
|
|
|
2441
2793
|
}
|
|
2442
2794
|
|
|
2443
2795
|
//#endregion
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
* chain.ts — draft-then-implement chains: `--chain draft,implement[,review]`.
|
|
2447
|
-
*
|
|
2448
|
-
* The chain runs each stage as its own worker (own id, own pane, `parent` set
|
|
2449
|
-
* to the chain id), so `ps` shows the chain as a tree and every stage can be
|
|
2450
|
-
* followed, replayed and said to like any other worker:
|
|
2451
|
-
*
|
|
2452
|
-
* - draft turns the operator's brief into a full spec file under
|
|
2453
|
-
* <logDir>/specs/<chain id>.md (goal, constraints, files likely
|
|
2454
|
-
* touched, acceptance checks, verification commands);
|
|
2455
|
-
* - any other stage (implement, plan, …) runs with that spec as its prompt
|
|
2456
|
-
* and the original brief attached;
|
|
2457
|
-
* - review reads the spec and the working-tree diff and produces the
|
|
2458
|
-
* structured report.
|
|
2459
|
-
*
|
|
2460
|
-
* A stage that fails (or a draft that produces no spec file) stops the chain;
|
|
2461
|
-
* the caller then writes the spec itself and re-runs without the draft stage.
|
|
2462
|
-
*/
|
|
2463
|
-
/** Where a chain's spec file lives: <logDir>/specs/<chain id>.md. */
|
|
2464
|
-
function specPathFor(logDir, chainId) {
|
|
2465
|
-
return join(logDir, "specs", `${chainId}.md`);
|
|
2466
|
-
}
|
|
2467
|
-
/**
|
|
2468
|
-
* Replace the caller's -p value with `prompt`, dropping every existing
|
|
2469
|
-
* -p/--print pair first (two -p flags on one claude command line are an error,
|
|
2470
|
-
* so the chain must never leave the brief in place when swapping prompts).
|
|
2471
|
-
*/
|
|
2472
|
-
function swapPromptArg(claudeArgs, prompt) {
|
|
2473
|
-
const out = [];
|
|
2474
|
-
for (let i = 0; i < claudeArgs.length; i++) {
|
|
2475
|
-
const a = claudeArgs[i];
|
|
2476
|
-
if (a === "-p" || a === "--print") {
|
|
2477
|
-
if (i + 1 < claudeArgs.length && !claudeArgs[i + 1].startsWith("-")) i += 1;
|
|
2478
|
-
continue;
|
|
2479
|
-
}
|
|
2480
|
-
out.push(a);
|
|
2481
|
-
}
|
|
2482
|
-
out.push("-p", prompt);
|
|
2483
|
-
return out;
|
|
2484
|
-
}
|
|
2485
|
-
function draftPrompt(brief, specPath) {
|
|
2486
|
-
return [
|
|
2487
|
-
"You are the DRAFT stage of a worker chain. Turn the operator's brief below into a full implementation spec and write it to",
|
|
2488
|
-
specPath,
|
|
2489
|
-
"with the Write tool.",
|
|
2490
|
-
"",
|
|
2491
|
-
"Use exactly these five sections as Markdown headings:",
|
|
2492
|
-
"# Goal",
|
|
2493
|
-
"# Constraints",
|
|
2494
|
-
"# Files likely touched",
|
|
2495
|
-
"# Acceptance checks",
|
|
2496
|
-
"# Verification commands",
|
|
2497
|
-
"",
|
|
2498
|
-
"Read the repository first (Glob/Grep/Read) so the spec names real files and real commands. Do not implement anything.",
|
|
2499
|
-
"",
|
|
2500
|
-
"## Operator brief",
|
|
2501
|
-
"",
|
|
2502
|
-
brief
|
|
2503
|
-
].join("\n");
|
|
2504
|
-
}
|
|
2505
|
-
function implementPrompt(brief, specPath, spec) {
|
|
2506
|
-
return [
|
|
2507
|
-
...spec ? [
|
|
2508
|
-
"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.",
|
|
2509
|
-
"",
|
|
2510
|
-
"## Spec",
|
|
2511
|
-
"",
|
|
2512
|
-
spec
|
|
2513
|
-
] : ["Implement the operator's brief below."],
|
|
2514
|
-
"",
|
|
2515
|
-
"## Operator brief",
|
|
2516
|
-
"",
|
|
2517
|
-
brief
|
|
2518
|
-
].join("\n");
|
|
2519
|
-
}
|
|
2520
|
-
function reviewPrompt(brief, specPath, spec) {
|
|
2521
|
-
return [
|
|
2522
|
-
"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.",
|
|
2523
|
-
...spec ? [
|
|
2524
|
-
"",
|
|
2525
|
-
"## Spec (also at " + specPath + ")",
|
|
2526
|
-
"",
|
|
2527
|
-
spec
|
|
2528
|
-
] : [],
|
|
2529
|
-
"",
|
|
2530
|
-
"## Operator brief",
|
|
2531
|
-
"",
|
|
2532
|
-
brief
|
|
2533
|
-
].join("\n");
|
|
2534
|
-
}
|
|
2535
|
-
/** Run a chain of stages; returns the exit code of the first failed stage, 0 when all pass. */
|
|
2536
|
-
async function runChain(opts, deps = {}) {
|
|
2537
|
-
const stages = opts.stages.map((s) => s.trim()).filter(Boolean);
|
|
2538
|
-
if (!stages.length) throw new Error("--chain needs at least one class, e.g. --chain draft,implement");
|
|
2539
|
-
const runStage = deps.runStage ?? runWorker;
|
|
2540
|
-
const logDir = deps.logDir ?? workersLogDir(readWorkersSection().workers);
|
|
2541
|
-
const chainId = newWorkerId();
|
|
2542
|
-
const specPath = specPathFor(logDir, chainId);
|
|
2543
|
-
mkdirSync(join(logDir, "specs"), { recursive: true });
|
|
2544
|
-
const baseLabel = opts.label ?? shortText(opts.brief, 40);
|
|
2545
|
-
appendLedger(ledgerPath(logDir), "WORKER-CHAIN", {
|
|
2546
|
-
chain: chainId,
|
|
2547
|
-
stages: stages.join(","),
|
|
2548
|
-
label: baseLabel
|
|
2549
|
-
});
|
|
2550
|
-
opts.onChainStart?.(chainId);
|
|
2551
|
-
let spec = null;
|
|
2552
|
-
for (let i = 0; i < stages.length; i++) {
|
|
2553
|
-
const stage = stages[i];
|
|
2554
|
-
if (stage === "draft") spec = null;
|
|
2555
|
-
else if (spec === null && existsSync(specPath)) spec = readFileSync(specPath, "utf8");
|
|
2556
|
-
const prompt = stage === "draft" ? draftPrompt(opts.brief, specPath) : stage === "review" ? reviewPrompt(opts.brief, spec ? specPath : null, spec) : implementPrompt(opts.brief, spec ? specPath : null, spec);
|
|
2557
|
-
process.stderr.write(`chain ${chainId}: stage ${i + 1}/${stages.length} ${stage} (spec: ${specPath})\n`);
|
|
2558
|
-
const rc = await runStage({
|
|
2559
|
-
className: opts.className ?? stage,
|
|
2560
|
-
providerFlag: opts.providerFlag,
|
|
2561
|
-
modelFlag: opts.modelFlag,
|
|
2562
|
-
label: `${baseLabel} · ${stage}`,
|
|
2563
|
-
noPane: opts.noPane,
|
|
2564
|
-
mcpFlag: opts.mcpFlag,
|
|
2565
|
-
claudeArgs: swapPromptArg(opts.claudeArgs, prompt),
|
|
2566
|
-
cwd: opts.cwd,
|
|
2567
|
-
parent: chainId,
|
|
2568
|
-
stage,
|
|
2569
|
-
quiet: opts.quiet
|
|
2570
|
-
});
|
|
2571
|
-
if (stage === "draft") {
|
|
2572
|
-
if (!existsSync(specPath)) {
|
|
2573
|
-
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`);
|
|
2574
|
-
appendLedger(ledgerPath(logDir), "WORKER-CHAIN-END", {
|
|
2575
|
-
chain: chainId,
|
|
2576
|
-
rc: rc !== 0 ? rc : 1,
|
|
2577
|
-
failed: "draft"
|
|
2578
|
-
});
|
|
2579
|
-
return rc !== 0 ? rc : 1;
|
|
2580
|
-
}
|
|
2581
|
-
spec = readFileSync(specPath, "utf8");
|
|
2582
|
-
}
|
|
2583
|
-
if (rc !== 0) {
|
|
2584
|
-
appendLedger(ledgerPath(logDir), "WORKER-CHAIN-END", {
|
|
2585
|
-
chain: chainId,
|
|
2586
|
-
rc,
|
|
2587
|
-
failed: stage
|
|
2588
|
-
});
|
|
2589
|
-
return rc;
|
|
2590
|
-
}
|
|
2591
|
-
}
|
|
2592
|
-
appendLedger(ledgerPath(logDir), "WORKER-CHAIN-END", {
|
|
2593
|
-
chain: chainId,
|
|
2594
|
-
rc: 0
|
|
2595
|
-
});
|
|
2596
|
-
return 0;
|
|
2597
|
-
}
|
|
2598
|
-
|
|
2599
|
-
//#endregion
|
|
2600
|
-
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 };
|
|
2601
|
-
//# sourceMappingURL=chain-DsvSHnO9.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
|