@tekmidian/pai 0.37.0 → 0.39.0
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/dist/{auto-route-DM7GhJ8y.mjs → auto-route-BlWOWT4z.mjs} +2 -2
- package/dist/{auto-route-DM7GhJ8y.mjs.map → auto-route-BlWOWT4z.mjs.map} +1 -1
- package/dist/{providers-sXcK5bDZ.mjs → chain-CTtHligI.mjs} +1040 -782
- package/dist/chain-CTtHligI.mjs.map +1 -0
- package/dist/cli/index.mjs +4 -3
- package/dist/cli/index.mjs.map +1 -1
- package/dist/cli/program.mjs +4 -3
- package/dist/{clusters-Do4tEGyc.mjs → clusters-wlK0w41E.mjs} +1 -1
- package/dist/{clusters-Do4tEGyc.mjs.map → clusters-wlK0w41E.mjs.map} +1 -1
- package/dist/daemon/index.mjs +7 -7
- package/dist/{daemon-CGg1VCbA.mjs → daemon-CTkN_Vn8.mjs} +25 -25
- package/dist/{daemon-CGg1VCbA.mjs.map → daemon-CTkN_Vn8.mjs.map} +1 -1
- package/dist/{daemon-DsGGiIJM.mjs → daemon-CmHHmKpg.mjs} +7 -7
- package/dist/daemon-mcp/index.mjs +207 -34
- package/dist/daemon-mcp/index.mjs.map +1 -1
- package/dist/{detector-CMap-9vw.mjs → detector-Bwk_4Pk4.mjs} +1 -1
- package/dist/{detector-CMap-9vw.mjs.map → detector-Bwk_4Pk4.mjs.map} +1 -1
- package/dist/detector-DO730Zq0.mjs +5 -0
- package/dist/{factory-DD2T33C9.mjs → factory-A9x-T2Kg.mjs} +5 -5
- package/dist/{factory-DD2T33C9.mjs.map → factory-A9x-T2Kg.mjs.map} +1 -1
- package/dist/factory-BD-np0Vd.mjs +3 -0
- package/dist/hooks/route-agents-to-worker.mjs +112 -20
- package/dist/hooks/route-agents-to-worker.mjs.map +2 -2
- package/dist/hooks/worker-proxy.mjs +110 -18
- package/dist/hooks/worker-proxy.mjs.map +2 -2
- package/dist/hooks/worker-status-line.mjs +176 -23
- package/dist/hooks/worker-status-line.mjs.map +4 -4
- package/dist/{indexer-backend-Cox9BCo-.mjs → indexer-backend-TG64CCQC.mjs} +1 -1
- package/dist/{indexer-backend-Cox9BCo-.mjs.map → indexer-backend-TG64CCQC.mjs.map} +1 -1
- package/dist/{latent-ideas-BC1oINZ-.mjs → latent-ideas-B7wq75Pt.mjs} +2 -2
- package/dist/{latent-ideas-BC1oINZ-.mjs.map → latent-ideas-B7wq75Pt.mjs.map} +1 -1
- package/dist/{link-boost-QFLrJwD6.mjs → link-boost-HkG7JWZR.mjs} +1 -1
- package/dist/{link-boost-QFLrJwD6.mjs.map → link-boost-HkG7JWZR.mjs.map} +1 -1
- package/dist/{neighborhood-D9MJ1c8f.mjs → neighborhood-lThN-MaQ.mjs} +1 -1
- package/dist/{neighborhood-D9MJ1c8f.mjs.map → neighborhood-lThN-MaQ.mjs.map} +1 -1
- package/dist/{note-context-d1wT_-GA.mjs → note-context-b6k0mAKi.mjs} +1 -1
- package/dist/{note-context-d1wT_-GA.mjs.map → note-context-b6k0mAKi.mjs.map} +1 -1
- package/dist/planner-BDI7bE9B.mjs +243 -0
- package/dist/planner-BDI7bE9B.mjs.map +1 -0
- package/dist/{postgres--BjPtLa0.mjs → postgres-mW1n7Vi1.mjs} +1 -1
- package/dist/{postgres--BjPtLa0.mjs.map → postgres-mW1n7Vi1.mjs.map} +1 -1
- package/dist/{program-CEIHn_Ma.mjs → program-DXBwJV7h.mjs} +274 -65
- package/dist/program-DXBwJV7h.mjs.map +1 -0
- package/dist/providers-FYkZjn_C.mjs +1405 -0
- package/dist/providers-FYkZjn_C.mjs.map +1 -0
- package/dist/{query-feedback-BUJxgw5B.mjs → query-feedback-BV4CcxqS.mjs} +1 -1
- package/dist/{query-feedback-BUJxgw5B.mjs.map → query-feedback-BV4CcxqS.mjs.map} +1 -1
- package/dist/query-feedback-DSVyHtrG.mjs +3 -0
- package/dist/router-Bk77E7hj.mjs +3 -0
- package/dist/{router-DK_sLsUL.mjs → router-DcHKnEPa.mjs} +1 -1
- package/dist/{router-DK_sLsUL.mjs.map → router-DcHKnEPa.mjs.map} +1 -1
- package/dist/skills/Worker/SKILL.md +34 -12
- package/dist/{sources-Bi7--33T.mjs → sources-kLnQsNrW.mjs} +1 -1
- package/dist/{sources-Bi7--33T.mjs.map → sources-kLnQsNrW.mjs.map} +1 -1
- package/dist/{sqlite-DtaL1glm.mjs → sqlite-BenGr3UP.mjs} +1 -1
- package/dist/{sqlite-DtaL1glm.mjs.map → sqlite-BenGr3UP.mjs.map} +1 -1
- package/dist/{state-BY2L6-vX.mjs → state-CAeyOdfq.mjs} +1 -1
- package/dist/{state-BY2L6-vX.mjs.map → state-CAeyOdfq.mjs.map} +1 -1
- package/dist/{state-8Hm9E4tW.mjs → state-Ca9F_sZc.mjs} +1 -1
- package/dist/{themes-BN0a2duq.mjs → themes-BI4GMRP9.mjs} +1 -1
- package/dist/{themes-BN0a2duq.mjs.map → themes-BI4GMRP9.mjs.map} +1 -1
- package/dist/{tools-CGPqpU3A.mjs → tools-Bp7hj6OW.mjs} +1 -1
- package/dist/{tools-y2bJpKom.mjs → tools-DGcB3o_S.mjs} +13 -13
- package/dist/{tools-y2bJpKom.mjs.map → tools-DGcB3o_S.mjs.map} +1 -1
- package/dist/{trace-h23JCcFD.mjs → trace-bobARFEX.mjs} +1 -1
- package/dist/{trace-h23JCcFD.mjs.map → trace-bobARFEX.mjs.map} +1 -1
- package/dist/{vault-indexer-DgsPjMgs.mjs → vault-indexer-C3OfXTNF.mjs} +1 -1
- package/dist/{vault-indexer-DgsPjMgs.mjs.map → vault-indexer-C3OfXTNF.mjs.map} +1 -1
- package/dist/{work-queue-worker-gsKd2LJa.mjs → work-queue-worker-DW8lz-Oo.mjs} +3 -3
- package/dist/{work-queue-worker-Dva_v_pI.mjs → work-queue-worker-R7UGIag_.mjs} +3 -3
- package/dist/{work-queue-worker-Dva_v_pI.mjs.map → work-queue-worker-R7UGIag_.mjs.map} +1 -1
- package/dist/{zettelkasten-vo7psPdT.mjs → zettelkasten-m5QPtb-L.mjs} +3 -3
- package/dist/{zettelkasten-vo7psPdT.mjs.map → zettelkasten-m5QPtb-L.mjs.map} +1 -1
- package/docs/commands/README.md +12 -7
- package/docs/commands/worker.md +110 -17
- package/docs/worker.md +244 -25
- package/package.json +1 -1
- package/src/hooks/ts/pre-tool-use/route-agents-to-worker.ts +2 -2
- package/dist/detector-DtLExmHN.mjs +0 -5
- package/dist/factory-BXzqRYVZ.mjs +0 -3
- package/dist/program-CEIHn_Ma.mjs.map +0 -1
- package/dist/providers-sXcK5bDZ.mjs.map +0 -1
- package/dist/query-feedback-DhyLOe5S.mjs +0 -3
- package/dist/router-1zi8jiNF.mjs +0 -3
- /package/dist/{main-resolver-IhZo4pI0.mjs → main-resolver-D6IImXvF.mjs} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { n as CONFIG_FILE } from "./config-B64vFg14.mjs";
|
|
2
|
-
import { appendFileSync,
|
|
2
|
+
import { appendFileSync, closeSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, renameSync, rmSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
|
|
3
3
|
import { homedir, tmpdir } from "node:os";
|
|
4
|
-
import {
|
|
4
|
+
import { dirname, join, relative } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { connect, createServer } from "node:net";
|
|
7
7
|
import { execFileSync, spawn } from "node:child_process";
|
|
@@ -104,6 +104,28 @@ function writeJsonAtomic(path, data, opts = {}) {
|
|
|
104
104
|
* `engine: "codex"` providers run through the Codex CLI instead of Claude
|
|
105
105
|
* Code.
|
|
106
106
|
*/
|
|
107
|
+
const DEFAULT_COST_TIER = 3;
|
|
108
|
+
/** Tags a provider may carry; classes filter auto-routing on them. */
|
|
109
|
+
const PROVIDER_TAGS = [
|
|
110
|
+
"code",
|
|
111
|
+
"vision",
|
|
112
|
+
"image-gen",
|
|
113
|
+
"long-context",
|
|
114
|
+
"fast",
|
|
115
|
+
"reasoning"
|
|
116
|
+
];
|
|
117
|
+
/** The standard task classes; `workers.classes` maps each to a target. */
|
|
118
|
+
const WORKER_CLASSES = [
|
|
119
|
+
"draft",
|
|
120
|
+
"plan",
|
|
121
|
+
"implement",
|
|
122
|
+
"review",
|
|
123
|
+
"research",
|
|
124
|
+
"spotcheck",
|
|
125
|
+
"simple",
|
|
126
|
+
"complex",
|
|
127
|
+
"image"
|
|
128
|
+
];
|
|
107
129
|
const DEFAULT_LOG_DIR = "~/.claude/logs/workers";
|
|
108
130
|
const DEFAULT_PANE = {
|
|
109
131
|
enabled: true,
|
|
@@ -115,19 +137,32 @@ const DEFAULT_ROUTING = {
|
|
|
115
137
|
cooldownMinutes: 30,
|
|
116
138
|
retryOnQuota: true
|
|
117
139
|
};
|
|
140
|
+
const DEFAULT_TREE = {
|
|
141
|
+
maxDepth: 2,
|
|
142
|
+
maxChildren: 4
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* The default MCP sets every config starts from. `desktop` names the clickr
|
|
146
|
+
* server so `--mcp desktop` hands a worker the machine controls (read-only
|
|
147
|
+
* tools always; actuating ones after the operator hands the controls over,
|
|
148
|
+
* see `pai worker controls`). A user's mcpSets section is merged over this,
|
|
149
|
+
* so `desktop: []` removes the set deliberately.
|
|
150
|
+
*/
|
|
151
|
+
const DEFAULT_MCP_SETS = { desktop: ["clickr"] };
|
|
118
152
|
function defaultWorkersConfig() {
|
|
119
153
|
return {
|
|
120
154
|
enabled: false,
|
|
121
155
|
active: null,
|
|
122
156
|
providers: {},
|
|
123
|
-
|
|
124
|
-
mcpSets: {},
|
|
157
|
+
classes: {},
|
|
158
|
+
mcpSets: { ...DEFAULT_MCP_SETS },
|
|
125
159
|
pane: { ...DEFAULT_PANE },
|
|
126
160
|
logDir: DEFAULT_LOG_DIR,
|
|
127
161
|
routing: {
|
|
128
162
|
...DEFAULT_ROUTING,
|
|
129
163
|
order: []
|
|
130
|
-
}
|
|
164
|
+
},
|
|
165
|
+
tree: { ...DEFAULT_TREE }
|
|
131
166
|
};
|
|
132
167
|
}
|
|
133
168
|
var WorkersConfigError = class extends Error {};
|
|
@@ -171,6 +206,16 @@ function parseProvider(name, raw) {
|
|
|
171
206
|
}
|
|
172
207
|
const upstreamUrl = str(p.upstreamUrl);
|
|
173
208
|
if (protocol === "openai" && !upstreamUrl) bad(`.providers.${name}.upstreamUrl`, `is required for protocol "openai" (the Chat Completions base, e.g. "https://api.openai.com/v1")`);
|
|
209
|
+
const costTier = p.costTier === void 0 ? void 0 : p.costTier;
|
|
210
|
+
if (costTier !== void 0) {
|
|
211
|
+
if (typeof costTier !== "number" || !Number.isInteger(costTier) || costTier < 1 || costTier > 5) bad(`.providers.${name}.costTier`, "must be an integer 1 (cheapest) … 5 (most expensive)");
|
|
212
|
+
}
|
|
213
|
+
let tags;
|
|
214
|
+
if (p.tags !== void 0) {
|
|
215
|
+
if (!Array.isArray(p.tags) || p.tags.some((x) => typeof x !== "string")) bad(`.providers.${name}.tags`, `must be an array of tags from: ${PROVIDER_TAGS.join(", ")}`);
|
|
216
|
+
for (const t of p.tags) if (!PROVIDER_TAGS.includes(t)) bad(`.providers.${name}.tags`, `"${t}" is not a tag (from: ${PROVIDER_TAGS.join(", ")})`);
|
|
217
|
+
tags = p.tags;
|
|
218
|
+
}
|
|
174
219
|
return {
|
|
175
220
|
enabled: p.enabled === void 0 ? true : p.enabled === true,
|
|
176
221
|
protocol,
|
|
@@ -186,7 +231,9 @@ function parseProvider(name, raw) {
|
|
|
186
231
|
...engine !== "claude" ? { engine } : {},
|
|
187
232
|
...str(p.quotaProbe) ? { quotaProbe: str(p.quotaProbe) } : {},
|
|
188
233
|
...quotaSkipAt !== void 0 ? { quotaSkipAt } : {},
|
|
189
|
-
...contextWindow !== void 0 ? { contextWindow } : {}
|
|
234
|
+
...contextWindow !== void 0 ? { contextWindow } : {},
|
|
235
|
+
...costTier !== void 0 ? { costTier } : {},
|
|
236
|
+
...tags ? { tags } : {}
|
|
190
237
|
};
|
|
191
238
|
}
|
|
192
239
|
/**
|
|
@@ -203,29 +250,50 @@ function parseWorkersConfig(raw) {
|
|
|
203
250
|
if (typeof w.providers !== "object" || w.providers === null || Array.isArray(w.providers)) bad(".providers", "must be an object keyed by provider name");
|
|
204
251
|
for (const [name, p] of Object.entries(w.providers)) providers[name] = parseProvider(name, p);
|
|
205
252
|
}
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
253
|
+
const classes = {};
|
|
254
|
+
const classesRaw = w.classes !== void 0 ? w.classes : w.roles;
|
|
255
|
+
if (classesRaw !== void 0) {
|
|
256
|
+
if (typeof classesRaw !== "object" || classesRaw === null || Array.isArray(classesRaw)) bad(w.classes !== void 0 ? ".classes" : ".roles", "must be an object of class → provider[/alias] or {provider, mcp, maxCostTier, requireTags, order}");
|
|
257
|
+
for (const [cls, target] of Object.entries(classesRaw)) if (typeof target === "object" && target !== null && !Array.isArray(target)) {
|
|
210
258
|
const o = target;
|
|
211
259
|
const provider = str(o.provider);
|
|
212
|
-
if (!provider || provider.includes(" ")) bad(`.
|
|
260
|
+
if (o.provider !== void 0 && (!provider || provider.includes(" "))) bad(`.classes.${cls}.provider`, `invalid provider "${provider}"`);
|
|
213
261
|
let mcp;
|
|
214
262
|
if (o.mcp !== void 0) {
|
|
215
|
-
if (!Array.isArray(o.mcp) || o.mcp.some((x) => typeof x !== "string")) bad(`.
|
|
263
|
+
if (!Array.isArray(o.mcp) || o.mcp.some((x) => typeof x !== "string")) bad(`.classes.${cls}.mcp`, "must be an array of MCP server or set names");
|
|
216
264
|
mcp = o.mcp;
|
|
217
265
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
266
|
+
let maxCostTier;
|
|
267
|
+
if (o.maxCostTier !== void 0) {
|
|
268
|
+
if (typeof o.maxCostTier !== "number" || !Number.isInteger(o.maxCostTier) || o.maxCostTier < 1 || o.maxCostTier > 5) bad(`.classes.${cls}.maxCostTier`, "must be an integer 1 … 5");
|
|
269
|
+
maxCostTier = o.maxCostTier;
|
|
270
|
+
}
|
|
271
|
+
let requireTags;
|
|
272
|
+
if (o.requireTags !== void 0) {
|
|
273
|
+
if (!Array.isArray(o.requireTags) || o.requireTags.some((x) => typeof x !== "string")) bad(`.classes.${cls}.requireTags`, `must be an array of tags from: ${PROVIDER_TAGS.join(", ")}`);
|
|
274
|
+
for (const t of o.requireTags) if (!PROVIDER_TAGS.includes(t)) bad(`.classes.${cls}.requireTags`, `"${t}" is not a tag (from: ${PROVIDER_TAGS.join(", ")})`);
|
|
275
|
+
requireTags = o.requireTags;
|
|
276
|
+
}
|
|
277
|
+
let order;
|
|
278
|
+
if (o.order !== void 0) {
|
|
279
|
+
if (!Array.isArray(o.order) || o.order.some((x) => typeof x !== "string")) bad(`.classes.${cls}.order`, "must be an array of provider names");
|
|
280
|
+
order = o.order;
|
|
281
|
+
}
|
|
282
|
+
const obj = {
|
|
283
|
+
...provider ? { provider } : {},
|
|
284
|
+
...mcp ? { mcp } : {},
|
|
285
|
+
...maxCostTier !== void 0 ? { maxCostTier } : {},
|
|
286
|
+
...requireTags ? { requireTags } : {},
|
|
287
|
+
...order ? { order } : {}
|
|
288
|
+
};
|
|
289
|
+
classes[cls] = Object.keys(obj).length ? obj : {};
|
|
222
290
|
} else {
|
|
223
291
|
const t = str(target);
|
|
224
|
-
if (!t || t.includes(" ")) bad(`.
|
|
225
|
-
|
|
292
|
+
if (!t || t.includes(" ")) bad(`.classes.${cls}`, `invalid target "${t}"`);
|
|
293
|
+
classes[cls] = t;
|
|
226
294
|
}
|
|
227
295
|
}
|
|
228
|
-
const mcpSets = {};
|
|
296
|
+
const mcpSets = { ...DEFAULT_MCP_SETS };
|
|
229
297
|
if (w.mcpSets !== void 0) {
|
|
230
298
|
if (typeof w.mcpSets !== "object" || w.mcpSets === null || Array.isArray(w.mcpSets)) bad(".mcpSets", "must be an object of set name → [server names]");
|
|
231
299
|
for (const [setName, servers] of Object.entries(w.mcpSets)) {
|
|
@@ -265,17 +333,33 @@ function parseWorkersConfig(raw) {
|
|
|
265
333
|
retryOnQuota: r.retryOnQuota === void 0 ? DEFAULT_ROUTING.retryOnQuota : r.retryOnQuota === true
|
|
266
334
|
};
|
|
267
335
|
}
|
|
336
|
+
let tree = { ...DEFAULT_TREE };
|
|
337
|
+
if (w.tree !== void 0) {
|
|
338
|
+
if (typeof w.tree !== "object" || w.tree === null || Array.isArray(w.tree)) bad(".tree", "must be an object");
|
|
339
|
+
const t = w.tree;
|
|
340
|
+
if (t.maxDepth !== void 0) {
|
|
341
|
+
if (typeof t.maxDepth !== "number" || !Number.isInteger(t.maxDepth) || t.maxDepth < 0) bad(".tree.maxDepth", "must be a non-negative integer");
|
|
342
|
+
}
|
|
343
|
+
if (t.maxChildren !== void 0) {
|
|
344
|
+
if (typeof t.maxChildren !== "number" || !Number.isInteger(t.maxChildren) || t.maxChildren < 1) bad(".tree.maxChildren", "must be a positive integer");
|
|
345
|
+
}
|
|
346
|
+
tree = {
|
|
347
|
+
maxDepth: t.maxDepth === void 0 ? DEFAULT_TREE.maxDepth : t.maxDepth,
|
|
348
|
+
maxChildren: t.maxChildren === void 0 ? DEFAULT_TREE.maxChildren : t.maxChildren
|
|
349
|
+
};
|
|
350
|
+
}
|
|
268
351
|
const active = w.active === void 0 || w.active === null ? null : str(w.active);
|
|
269
352
|
if (active !== null && active !== "auto" && !(active in providers)) {}
|
|
270
353
|
return {
|
|
271
354
|
enabled: w.enabled === void 0 ? d.enabled : w.enabled === true,
|
|
272
355
|
active,
|
|
273
356
|
providers,
|
|
274
|
-
|
|
357
|
+
classes,
|
|
275
358
|
mcpSets,
|
|
276
359
|
pane,
|
|
277
360
|
logDir: str(w.logDir) || d.logDir,
|
|
278
|
-
routing
|
|
361
|
+
routing,
|
|
362
|
+
tree
|
|
279
363
|
};
|
|
280
364
|
}
|
|
281
365
|
/**
|
|
@@ -308,6 +392,10 @@ function providerKeyPath(p) {
|
|
|
308
392
|
return p.keyFile ? expandHome(p.keyFile) : null;
|
|
309
393
|
}
|
|
310
394
|
const DEFAULT_CONTEXT_WINDOW = 2e5;
|
|
395
|
+
/** Cost tier of a provider for class filtering (unset = 3, the middle). */
|
|
396
|
+
function providerCostTier(p) {
|
|
397
|
+
return p.costTier ?? DEFAULT_COST_TIER;
|
|
398
|
+
}
|
|
311
399
|
/** Context window used by the meter when the init event carries none. */
|
|
312
400
|
function providerContextWindow(p) {
|
|
313
401
|
return p.contextWindow ?? DEFAULT_CONTEXT_WINDOW;
|
|
@@ -525,9 +613,18 @@ function nowStamp(d = /* @__PURE__ */ new Date()) {
|
|
|
525
613
|
const p = (n) => String(n).padStart(2, "0");
|
|
526
614
|
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
|
|
527
615
|
}
|
|
616
|
+
let lastId = "";
|
|
617
|
+
let idSeq = 0;
|
|
528
618
|
function newWorkerId(d = /* @__PURE__ */ new Date(), pid = process.pid) {
|
|
529
619
|
const p = (n) => String(n).padStart(2, "0");
|
|
530
|
-
|
|
620
|
+
const base = `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}-${pid}`;
|
|
621
|
+
if (base === lastId) {
|
|
622
|
+
idSeq += 1;
|
|
623
|
+
return `${base}-${String(idSeq).padStart(2, "0")}`;
|
|
624
|
+
}
|
|
625
|
+
lastId = base;
|
|
626
|
+
idSeq = 0;
|
|
627
|
+
return base;
|
|
531
628
|
}
|
|
532
629
|
/** Write status atomically (temp + rename) and stamp `updated`. */
|
|
533
630
|
function saveStatus(logDir, status, d = /* @__PURE__ */ new Date()) {
|
|
@@ -676,16 +773,19 @@ function sessionTag(worker) {
|
|
|
676
773
|
//#endregion
|
|
677
774
|
//#region src/workers/routing.ts
|
|
678
775
|
/**
|
|
679
|
-
* routing.ts — provider selection: flag >
|
|
776
|
+
* routing.ts — provider selection: flag > class > active (possibly "auto").
|
|
680
777
|
*
|
|
681
|
-
* Auto-routing walks `workers.routing.order`
|
|
682
|
-
* that is enabled, out of cooldown,
|
|
683
|
-
*
|
|
684
|
-
*
|
|
685
|
-
*
|
|
686
|
-
*
|
|
778
|
+
* Auto-routing walks `workers.routing.order` — or the class's own `order` —
|
|
779
|
+
* and takes the first provider that is enabled, out of cooldown, (when it
|
|
780
|
+
* defines a quotaProbe) under its quotaSkipAt threshold, and — when the class
|
|
781
|
+
* constrains it — within `maxCostTier` and carrying all `requireTags`. A run
|
|
782
|
+
* that dies of a quota/rate error puts its provider in cooldown for
|
|
783
|
+
* cooldownMinutes; when that happens before the first tool call and
|
|
784
|
+
* retryOnQuota is set, the runner restarts the same task on the next provider
|
|
785
|
+
* (ledger: WORKER-REROUTE).
|
|
687
786
|
*
|
|
688
|
-
* An explicit --provider or
|
|
787
|
+
* An explicit --provider or a class mapping that pins a provider always
|
|
788
|
+
* bypasses all of this.
|
|
689
789
|
*/
|
|
690
790
|
const QUOTA_SKIP_DEFAULT = 95;
|
|
691
791
|
const PROBE_TIMEOUT_MS = 1e4;
|
|
@@ -764,34 +864,61 @@ function mustBeRunnable(name, p) {
|
|
|
764
864
|
if (!p.enabled) throw new NoProviderError(`provider "${name}" is disabled. Enable it with: pai worker providers enable ${name}`);
|
|
765
865
|
return p;
|
|
766
866
|
}
|
|
867
|
+
/** Why one provider of a routing order did not qualify (message fragment). */
|
|
868
|
+
function exclusionReason(config, state, name, cls) {
|
|
869
|
+
const p = config.providers[name];
|
|
870
|
+
if (!p) return "not configured";
|
|
871
|
+
if (!p.enabled) return "disabled";
|
|
872
|
+
if (cooldownRemaining(state, name) > 0) return "cooldown";
|
|
873
|
+
if (quotaExceeded(p)) return "quota";
|
|
874
|
+
const tier = providerCostTier(p);
|
|
875
|
+
if (cls?.maxCostTier !== void 0 && tier > cls.maxCostTier) return `cost tier ${tier} > max ${cls.maxCostTier}`;
|
|
876
|
+
if (cls?.requireTags?.length) {
|
|
877
|
+
const have = p.tags ?? [];
|
|
878
|
+
const missing = cls.requireTags.filter((t) => !have.includes(t));
|
|
879
|
+
if (missing.length) return `missing tags: ${missing.join(", ")}`;
|
|
880
|
+
}
|
|
881
|
+
return null;
|
|
882
|
+
}
|
|
767
883
|
/**
|
|
768
884
|
* Resolve which provider (and model alias) a run uses.
|
|
769
885
|
*
|
|
770
886
|
* @param flagProvider --provider value, highest precedence
|
|
771
|
-
* @param
|
|
887
|
+
* @param className --class value (the old --role), looked up in workers.classes
|
|
772
888
|
*/
|
|
773
889
|
function resolveTarget(config, logDir, opts = {}) {
|
|
774
890
|
if (opts.flagProvider) return {
|
|
775
891
|
providerName: opts.flagProvider,
|
|
776
892
|
provider: mustBeRunnable(opts.flagProvider, mustExist(config, opts.flagProvider)),
|
|
777
893
|
modelAlias: null,
|
|
778
|
-
|
|
894
|
+
classMcp: null,
|
|
779
895
|
via: "flag"
|
|
780
896
|
};
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
const name =
|
|
785
|
-
const alias = typeof target === "string" ? target.split("/")[1] : void 0;
|
|
786
|
-
const roleMcp = typeof target === "string" ? null : target.mcp ?? null;
|
|
897
|
+
const clsTarget = opts.className ? config.classes[opts.className] : void 0;
|
|
898
|
+
const cls = typeof clsTarget === "object" && clsTarget !== null ? clsTarget : void 0;
|
|
899
|
+
if (opts.className && clsTarget !== void 0 && typeof clsTarget !== "object") {
|
|
900
|
+
const [name, alias] = clsTarget.split("/");
|
|
787
901
|
return {
|
|
788
902
|
providerName: name,
|
|
789
903
|
provider: mustBeRunnable(name, mustExist(config, name)),
|
|
790
904
|
modelAlias: alias ?? null,
|
|
791
|
-
|
|
792
|
-
via: "
|
|
905
|
+
classMcp: null,
|
|
906
|
+
via: "class"
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
if (opts.className && cls?.provider) {
|
|
910
|
+
const provider = mustBeRunnable(cls.provider, mustExist(config, cls.provider));
|
|
911
|
+
return {
|
|
912
|
+
providerName: cls.provider,
|
|
913
|
+
provider,
|
|
914
|
+
modelAlias: null,
|
|
915
|
+
classMcp: cls.mcp ?? null,
|
|
916
|
+
via: "class"
|
|
793
917
|
};
|
|
794
918
|
}
|
|
919
|
+
if (opts.className && clsTarget === void 0) {
|
|
920
|
+
if (!WORKER_CLASSES.includes(opts.className)) throw new NoProviderError(`no class named "${opts.className}". Standard classes: ${WORKER_CLASSES.join(", ")}.Defined: ${Object.keys(config.classes).join(", ") || "(none)"}.\nSet one with: pai worker classes set ${opts.className}=<provider[/alias]>`);
|
|
921
|
+
}
|
|
795
922
|
if (config.active !== "auto") {
|
|
796
923
|
const name = config.active;
|
|
797
924
|
if (!name) throw new NoProviderError("no active worker provider. Add one with: pai worker providers add <name> --base-url <url> --key-file <path> --model <model>\n(or point one that exists at it: pai worker providers use <name>)");
|
|
@@ -799,32 +926,29 @@ function resolveTarget(config, logDir, opts = {}) {
|
|
|
799
926
|
providerName: name,
|
|
800
927
|
provider: mustBeRunnable(name, mustExist(config, name)),
|
|
801
928
|
modelAlias: null,
|
|
802
|
-
|
|
929
|
+
classMcp: null,
|
|
803
930
|
via: "active"
|
|
804
931
|
};
|
|
805
932
|
}
|
|
806
933
|
const state = readRoutingState(logDir);
|
|
807
|
-
const
|
|
808
|
-
|
|
934
|
+
const order = cls?.order ?? config.routing.order;
|
|
935
|
+
const excluded = [];
|
|
936
|
+
for (const name of order) {
|
|
809
937
|
const p = config.providers[name];
|
|
810
|
-
|
|
811
|
-
if (
|
|
812
|
-
|
|
813
|
-
continue;
|
|
814
|
-
}
|
|
815
|
-
if (quotaExceeded(p)) {
|
|
816
|
-
skipped.push(`${name}(quota)`);
|
|
938
|
+
const why = exclusionReason(config, state, name, cls);
|
|
939
|
+
if (why) {
|
|
940
|
+
excluded.push(`${name}: ${why}`);
|
|
817
941
|
continue;
|
|
818
942
|
}
|
|
819
943
|
return {
|
|
820
944
|
providerName: name,
|
|
821
945
|
provider: p,
|
|
822
946
|
modelAlias: null,
|
|
823
|
-
|
|
947
|
+
classMcp: cls?.mcp ?? null,
|
|
824
948
|
via: "auto"
|
|
825
949
|
};
|
|
826
950
|
}
|
|
827
|
-
throw new NoProviderError(`auto-routing found no usable provider (order: [${
|
|
951
|
+
throw new NoProviderError(`auto-routing found no usable provider${opts.className ? ` for class "${opts.className}"` : ""} (order: [${order.join(", ")}]).${excluded.length ? `\nExcluded: ${excluded.join("; ")}.` : ""}\nClear a cooldown with: pai worker providers enable <name>; widen the class with: pai worker classes set ${opts.className ?? "<class>"}=<target>`);
|
|
828
952
|
}
|
|
829
953
|
/**
|
|
830
954
|
* Next provider after `from` in auto order, applying the same filters.
|
|
@@ -859,11 +983,13 @@ function isQuotaFailure(resultText) {
|
|
|
859
983
|
* pane.ts — the per-worker follow pane in iTerm2.
|
|
860
984
|
*
|
|
861
985
|
* One small-font pane per worker, stacked in a right-hand column: the first
|
|
862
|
-
* worker of a scope splits the launching session vertically
|
|
863
|
-
*
|
|
864
|
-
*
|
|
865
|
-
*
|
|
866
|
-
*
|
|
986
|
+
* worker of a scope splits the launching session vertically, every further
|
|
987
|
+
* one splits the lowest live worker pane horizontally, so panes stack top to
|
|
988
|
+
* bottom. The split never sizes the new session — that would grow the whole
|
|
989
|
+
* window; instead the window's bounds are read before the split and restored
|
|
990
|
+
* right after, so the panes share the space the window already had. Each
|
|
991
|
+
* pane runs `pai worker follow <id> --auto-exit <n>` under the `pai-worker`
|
|
992
|
+
* dynamic profile (Close Sessions On End), so panes disappear by themselves.
|
|
867
993
|
*
|
|
868
994
|
* Panes are tracked per scope (AIBroker session id, else tab key) in
|
|
869
995
|
* <logDir>/panes/<key>.json, keyed by iTerm session unique id, newest first —
|
|
@@ -893,10 +1019,14 @@ const WORKER_SPLIT_SCRIPT = `on run(argv)
|
|
|
893
1019
|
repeat with t in tabs of w
|
|
894
1020
|
repeat with s in sessions of t
|
|
895
1021
|
if id of s is targetID then
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
1022
|
+
-- sizing the new session would grow the whole window:
|
|
1023
|
+
-- pin the window's bounds now and restore them after
|
|
1024
|
+
-- the split, so the panes share the existing space.
|
|
1025
|
+
-- copy, never set: set stores the property
|
|
1026
|
+
-- reference lazily, so restoring it would re-read the
|
|
1027
|
+
-- post-split bounds instead of these — observed as the
|
|
1028
|
+
-- window jumping to the main display
|
|
1029
|
+
copy bounds of w to winBounds
|
|
900
1030
|
set sessIDs to {}
|
|
901
1031
|
repeat with other in sessions of t
|
|
902
1032
|
set end of sessIDs to (id of other as text)
|
|
@@ -936,13 +1066,9 @@ const WORKER_SPLIT_SCRIPT = `on run(argv)
|
|
|
936
1066
|
tell newS
|
|
937
1067
|
write text followCmd
|
|
938
1068
|
end tell
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
set columns to (round (parentCols * 0.4))
|
|
943
|
-
end tell
|
|
944
|
-
end try
|
|
945
|
-
end if
|
|
1069
|
+
try
|
|
1070
|
+
set bounds of w to winBounds
|
|
1071
|
+
end try
|
|
946
1072
|
set out to ""
|
|
947
1073
|
repeat with lid in lived
|
|
948
1074
|
set out to out & lid & ","
|
|
@@ -987,6 +1113,27 @@ const TAB_TTYS_SCRIPT = `on run(argv)
|
|
|
987
1113
|
end tell
|
|
988
1114
|
return "notfound"
|
|
989
1115
|
end run`;
|
|
1116
|
+
const WINDOW_BOUNDS_SCRIPT = `on run(argv)
|
|
1117
|
+
set targetID to item 1 of argv
|
|
1118
|
+
tell application id "com.googlecode.iterm2"
|
|
1119
|
+
if not running then return "notrunning"
|
|
1120
|
+
repeat with w in windows
|
|
1121
|
+
repeat with t in tabs of w
|
|
1122
|
+
repeat with s in sessions of t
|
|
1123
|
+
if id of s is targetID then
|
|
1124
|
+
copy bounds of w to winBounds
|
|
1125
|
+
set prevDels to AppleScript's text item delimiters
|
|
1126
|
+
set AppleScript's text item delimiters to ", "
|
|
1127
|
+
set out to winBounds as text
|
|
1128
|
+
set AppleScript's text item delimiters to prevDels
|
|
1129
|
+
return out
|
|
1130
|
+
end if
|
|
1131
|
+
end repeat
|
|
1132
|
+
end repeat
|
|
1133
|
+
end repeat
|
|
1134
|
+
end tell
|
|
1135
|
+
return "notfound"
|
|
1136
|
+
end run`;
|
|
990
1137
|
const SPLIT_SCRIPT = `on run(argv)
|
|
991
1138
|
set targetID to item 1 of argv
|
|
992
1139
|
set followCmd to item 2 of argv
|
|
@@ -1031,7 +1178,7 @@ function osascript(script, args) {
|
|
|
1031
1178
|
proc.stdin.end();
|
|
1032
1179
|
});
|
|
1033
1180
|
}
|
|
1034
|
-
function psOutput
|
|
1181
|
+
function psOutput(format) {
|
|
1035
1182
|
try {
|
|
1036
1183
|
return execFileSync("ps", ["-axo", format], { encoding: "utf8" });
|
|
1037
1184
|
} catch {
|
|
@@ -1041,7 +1188,7 @@ function psOutput$1(format) {
|
|
|
1041
1188
|
/** TTys of running `worker follow` processes, normalised to /dev/ttysNNN. */
|
|
1042
1189
|
function followTtys() {
|
|
1043
1190
|
const ttys = /* @__PURE__ */ new Set();
|
|
1044
|
-
for (const line of psOutput
|
|
1191
|
+
for (const line of psOutput("tty=,command=").split("\n")) {
|
|
1045
1192
|
const parts = line.trim().split(/\s+/, 2);
|
|
1046
1193
|
if (parts.length < 2 || parts[0] === "ps") continue;
|
|
1047
1194
|
if (!/(^|\/)(worker-follow|pai worker follow|glm-ps follow)\b/.test(parts[1])) continue;
|
|
@@ -1052,7 +1199,7 @@ function followTtys() {
|
|
|
1052
1199
|
/** True while a `follow <wid>` process runs (its pane shows that worker). */
|
|
1053
1200
|
function workerPaneOpen(wid) {
|
|
1054
1201
|
const pat = new RegExp(`(?:worker follow|worker-follow|glm-ps follow) ${wid.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`);
|
|
1055
|
-
return psOutput
|
|
1202
|
+
return psOutput("command=").split("\n").some((ln) => pat.test(ln));
|
|
1056
1203
|
}
|
|
1057
1204
|
/** plutil's message for an ExecFileSync failure, else the exception text. */
|
|
1058
1205
|
function toolErr(e) {
|
|
@@ -1257,12 +1404,31 @@ async function openPaneForWorker(logDir, config, wid, term) {
|
|
|
1257
1404
|
return `pane opened for ${wid}`;
|
|
1258
1405
|
}
|
|
1259
1406
|
/**
|
|
1407
|
+
* One `--check` line with the bounds of the window hosting `term`'s iTerm
|
|
1408
|
+
* session — the before/after pair that shows whether a split moved it.
|
|
1409
|
+
* Read-only; never touches the window.
|
|
1410
|
+
*/
|
|
1411
|
+
async function windowBoundsLine(term) {
|
|
1412
|
+
const uid = itermUuid(term);
|
|
1413
|
+
if (!uid) return "window bounds: (not in iTerm2)";
|
|
1414
|
+
try {
|
|
1415
|
+
const p = await osascript(WINDOW_BOUNDS_SCRIPT, [uid]);
|
|
1416
|
+
const out = p.stdout.trim();
|
|
1417
|
+
if (out && out !== "notfound" && out !== "notrunning") return `window bounds: ${out}`;
|
|
1418
|
+
return `window bounds: (${out === "notfound" ? "iTerm2 session not found" : out === "notrunning" ? "iTerm2 not running" : (p.stderr.trim() || "no output").slice(0, 120)})`;
|
|
1419
|
+
} catch (e) {
|
|
1420
|
+
return `window bounds: (osascript: ${String(e.message ?? e).slice(0, 120)})`;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
/**
|
|
1260
1424
|
* Report-only variant used by `pai worker pane <id> --check`: whether a pane
|
|
1261
|
-
* runs for the worker,
|
|
1262
|
-
* the
|
|
1425
|
+
* runs for the worker, the bounds of the window hosting the asking session,
|
|
1426
|
+
* plus the dynamic profile's path, its existence, and the font it contains
|
|
1427
|
+
* (or, when missing, would write).
|
|
1263
1428
|
*/
|
|
1264
|
-
async function checkPaneForWorker(wid, fontSize) {
|
|
1429
|
+
async function checkPaneForWorker(wid, fontSize, term) {
|
|
1265
1430
|
const lines = [workerPaneOpen(wid) ? `pane for ${wid} open` : `no pane for ${wid}`];
|
|
1431
|
+
lines.push(await windowBoundsLine(term));
|
|
1266
1432
|
const path = dynamicProfilePath();
|
|
1267
1433
|
if (existsSync(path)) {
|
|
1268
1434
|
let font = "(unreadable)";
|
|
@@ -1297,6 +1463,483 @@ async function openFollowPane(logDir, _config, term, checkOnly) {
|
|
|
1297
1463
|
throw new Error(`pai worker pane: osascript failed: ${(s.stderr || sout).slice(0, 200)}`);
|
|
1298
1464
|
}
|
|
1299
1465
|
|
|
1466
|
+
//#endregion
|
|
1467
|
+
//#region src/workers/tree.ts
|
|
1468
|
+
/** The env var the runner sets in every worker's environment. */
|
|
1469
|
+
const WORKER_ID_ENV = "PAI_WORKER_ID";
|
|
1470
|
+
/** The worker this process runs inside, when it runs inside one. */
|
|
1471
|
+
function parentFromEnv(env = process.env) {
|
|
1472
|
+
const id = env[WORKER_ID_ENV];
|
|
1473
|
+
return typeof id === "string" && id.trim() ? id.trim() : null;
|
|
1474
|
+
}
|
|
1475
|
+
/**
|
|
1476
|
+
* Depth of `id` in the worker forest: 0 for a top-level worker, 1 + the
|
|
1477
|
+
* parent's depth for a sub-worker. Parents without a status file (chain ids,
|
|
1478
|
+
* unknown ids) count as roots; a cycle reads as its own depth and is cut off
|
|
1479
|
+
* after the statuses it walked.
|
|
1480
|
+
*/
|
|
1481
|
+
function workerDepth(statuses, id) {
|
|
1482
|
+
const byId = new Map(statuses.map((s) => [s.id, s]));
|
|
1483
|
+
let depth = 0;
|
|
1484
|
+
let cur = byId.get(id);
|
|
1485
|
+
const seen = new Set([id]);
|
|
1486
|
+
while (cur?.parent && !seen.has(cur.parent)) {
|
|
1487
|
+
seen.add(cur.parent);
|
|
1488
|
+
const next = byId.get(cur.parent);
|
|
1489
|
+
if (!next) break;
|
|
1490
|
+
depth += 1;
|
|
1491
|
+
cur = next;
|
|
1492
|
+
}
|
|
1493
|
+
return depth;
|
|
1494
|
+
}
|
|
1495
|
+
/** Children of `parent` that are still running, oldest first. */
|
|
1496
|
+
function runningChildren(statuses, parent) {
|
|
1497
|
+
return statuses.filter((s) => s.parent === parent && s.state === "running" && alive(s.pid));
|
|
1498
|
+
}
|
|
1499
|
+
/**
|
|
1500
|
+
* Would starting a child of `parent` stay within the caps? Throws with a
|
|
1501
|
+
* clear message when it would not; returns silently when `parent` has no
|
|
1502
|
+
* status file (a chain id or an unknown id — not a worker, not capped).
|
|
1503
|
+
*/
|
|
1504
|
+
function assertChildAllowed(logDir, parent, caps, statuses = loadStatuses(logDir)) {
|
|
1505
|
+
if (!statuses.some((s) => s.id === parent)) return;
|
|
1506
|
+
const depth = workerDepth(statuses, parent);
|
|
1507
|
+
if (depth + 1 > caps.maxDepth) throw new Error(`worker tree: ${parent} sits at depth ${depth} and workers.tree.maxDepth is ${caps.maxDepth} — it cannot start another level of sub-workers. Hand the task up instead: pai worker handoff '{"kind":"proposal","text":"…"}'`);
|
|
1508
|
+
const kids = runningChildren(statuses, parent);
|
|
1509
|
+
if (kids.length >= caps.maxChildren) throw new Error(`worker tree: ${parent} already has ${kids.length} running sub-workers (${kids.map((k) => k.id).join(", ")}) and workers.tree.maxChildren is ${caps.maxChildren} — wait for one to finish, or raise the cap in the workers config`);
|
|
1510
|
+
}
|
|
1511
|
+
/**
|
|
1512
|
+
* The parent a launch should record: an explicit one (chain stage) wins, else
|
|
1513
|
+
* the worker this process runs inside. Returns null for top-level runs.
|
|
1514
|
+
*/
|
|
1515
|
+
function launchParent(explicit, env = process.env) {
|
|
1516
|
+
return explicit ?? parentFromEnv(env);
|
|
1517
|
+
}
|
|
1518
|
+
/** Does a status file exist for `id` (i.e. is it a worker rather than a chain)? */
|
|
1519
|
+
function isWorkerId(logDir, id) {
|
|
1520
|
+
return loadStatus(logDir, id) !== null;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
//#endregion
|
|
1524
|
+
//#region src/workers/operator.ts
|
|
1525
|
+
/**
|
|
1526
|
+
* operator.ts — talking to a running worker.
|
|
1527
|
+
*
|
|
1528
|
+
* Headless workers run with `--input-format stream-json` and their stdin held
|
|
1529
|
+
* open, so a run is a conversation: every line sent to the per-worker Unix
|
|
1530
|
+
* socket `<logDir>/<id>.sock` is forwarded to the child as a user message and
|
|
1531
|
+
* mirrored into the transcript as an `operator` event (rendered with a »
|
|
1532
|
+
* marker). Once the worker has finished its turn, stdin closes 2 s later
|
|
1533
|
+
* unless a new message arrives — after that, `say` refuses and `resume`
|
|
1534
|
+
* continues the same Claude session with the worker's context intact.
|
|
1535
|
+
*/
|
|
1536
|
+
function operatorSocketPath(logDir, id) {
|
|
1537
|
+
return join(logDir, `${id}.sock`);
|
|
1538
|
+
}
|
|
1539
|
+
/**
|
|
1540
|
+
* The runner's side: listen on the worker socket, hand every received line to
|
|
1541
|
+
* `onLine`. Returns the server (close it when the run ends; the socket file is
|
|
1542
|
+
* unlinked on close, best effort).
|
|
1543
|
+
*/
|
|
1544
|
+
function createOperatorServer(logDir, id, onLine) {
|
|
1545
|
+
const path = operatorSocketPath(logDir, id);
|
|
1546
|
+
try {
|
|
1547
|
+
if (existsSync(path)) unlinkSync(path);
|
|
1548
|
+
} catch {}
|
|
1549
|
+
const server = createServer((socket) => {
|
|
1550
|
+
let buf = "";
|
|
1551
|
+
socket.on("data", (chunk) => {
|
|
1552
|
+
buf += chunk.toString("utf8");
|
|
1553
|
+
let nl;
|
|
1554
|
+
while ((nl = buf.indexOf("\n")) >= 0) {
|
|
1555
|
+
const line = buf.slice(0, nl).replace(/\r$/, "");
|
|
1556
|
+
buf = buf.slice(nl + 1);
|
|
1557
|
+
if (line.trim()) onLine(line);
|
|
1558
|
+
socket.write("ok\n");
|
|
1559
|
+
}
|
|
1560
|
+
});
|
|
1561
|
+
});
|
|
1562
|
+
server.listen(path);
|
|
1563
|
+
server.on("close", () => {
|
|
1564
|
+
try {
|
|
1565
|
+
if (existsSync(path)) unlinkSync(path);
|
|
1566
|
+
} catch {}
|
|
1567
|
+
});
|
|
1568
|
+
return server;
|
|
1569
|
+
}
|
|
1570
|
+
/**
|
|
1571
|
+
* `pai worker say <id> "<text>"`: forward one line to a running worker.
|
|
1572
|
+
* Resolves "ok", rejects with a clear message when the worker is not running.
|
|
1573
|
+
*/
|
|
1574
|
+
function sayToWorker(logDir, id, text, timeoutMs = 4e3) {
|
|
1575
|
+
const status = loadStatus(logDir, id);
|
|
1576
|
+
if (!status) return Promise.reject(/* @__PURE__ */ new Error(`no worker named "${id}"`));
|
|
1577
|
+
if (status.state !== "running" || !alive(status.pid)) return Promise.reject(/* @__PURE__ */ new Error(`worker ${id} is not running (state: ${status.state}) — continue it instead with: pai worker resume ${id} "<text>"`));
|
|
1578
|
+
const path = operatorSocketPath(logDir, id);
|
|
1579
|
+
if (!existsSync(path)) return Promise.reject(/* @__PURE__ */ new Error(`worker ${id} has no operator socket (${path}) — it may predate this PAI version`));
|
|
1580
|
+
return new Promise((resolve, reject) => {
|
|
1581
|
+
const sock = connect(path);
|
|
1582
|
+
const fail = (e) => {
|
|
1583
|
+
sock.destroy();
|
|
1584
|
+
reject(/* @__PURE__ */ new Error(`cannot talk to worker ${id}: ${e.message}`));
|
|
1585
|
+
};
|
|
1586
|
+
sock.setTimeout(timeoutMs, () => fail(/* @__PURE__ */ new Error("timeout")));
|
|
1587
|
+
sock.once("error", (e) => fail(e));
|
|
1588
|
+
sock.once("connect", () => {
|
|
1589
|
+
sock.write(text.replace(/\n/g, " ") + "\n");
|
|
1590
|
+
});
|
|
1591
|
+
sock.once("data", () => {
|
|
1592
|
+
sock.end();
|
|
1593
|
+
resolve("ok");
|
|
1594
|
+
});
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
//#endregion
|
|
1599
|
+
//#region src/workers/handoff.ts
|
|
1600
|
+
/**
|
|
1601
|
+
* handoff.ts — upward messages between workers.
|
|
1602
|
+
*
|
|
1603
|
+
* Coordination in the worker tree is upward only: a child appends a handoff
|
|
1604
|
+
* to `<logDir>/<parent>.inbox.jsonl` and (when the parent is a running
|
|
1605
|
+
* worker) the same text is said to it, so it lands in the parent's
|
|
1606
|
+
* conversation as `[handoff from <child>] …`. The parent's pane renders the
|
|
1607
|
+
* inbox as `◆` lines, `ps`/`worker_status` show an inbox count, and a child
|
|
1608
|
+
* that finishes delivers its structured report automatically as a
|
|
1609
|
+
* `kind: "result"` handoff. There is no downward or sideways path — telling
|
|
1610
|
+
* a worker something is the operator's job (`pai worker say`).
|
|
1611
|
+
*/
|
|
1612
|
+
const HANDOFF_KINDS = [
|
|
1613
|
+
"proposal",
|
|
1614
|
+
"result",
|
|
1615
|
+
"question",
|
|
1616
|
+
"blocker"
|
|
1617
|
+
];
|
|
1618
|
+
/** Where a worker's handoffs land: <logDir>/<id>.inbox.jsonl. */
|
|
1619
|
+
function inboxPath(logDir, id) {
|
|
1620
|
+
return join(logDir, `${id}.inbox.jsonl`);
|
|
1621
|
+
}
|
|
1622
|
+
function isHandoffKind(v) {
|
|
1623
|
+
return typeof v === "string" && HANDOFF_KINDS.includes(v);
|
|
1624
|
+
}
|
|
1625
|
+
/**
|
|
1626
|
+
* Normalize a raw parsed value into a Handoff, or explain what is missing.
|
|
1627
|
+
* `from`/`to` may be preset by the caller (the CLI fills them from the
|
|
1628
|
+
* environment); everything else must be present and well-formed.
|
|
1629
|
+
*/
|
|
1630
|
+
function parseHandoff(v, preset = {}) {
|
|
1631
|
+
if (typeof v !== "object" || v === null || Array.isArray(v)) throw new Error("handoff payload must be a JSON object");
|
|
1632
|
+
const o = v;
|
|
1633
|
+
const from = typeof o.from === "string" && o.from ? o.from : preset.from;
|
|
1634
|
+
const to = typeof o.to === "string" && o.to ? o.to : preset.to;
|
|
1635
|
+
if (!from) throw new Error(`handoff needs "from" (the sending worker id)`);
|
|
1636
|
+
if (!to) throw new Error(`handoff needs "to" (the parent worker id)`);
|
|
1637
|
+
if (!isHandoffKind(o.kind)) throw new Error(`handoff "kind" must be one of: ${HANDOFF_KINDS.join(", ")}`);
|
|
1638
|
+
const text = typeof o.text === "string" ? o.text : "";
|
|
1639
|
+
if (!text.trim()) throw new Error(`handoff needs a non-empty "text"`);
|
|
1640
|
+
const data = typeof o.data === "object" && o.data !== null && !Array.isArray(o.data) ? o.data : void 0;
|
|
1641
|
+
return {
|
|
1642
|
+
from,
|
|
1643
|
+
to,
|
|
1644
|
+
kind: o.kind,
|
|
1645
|
+
text,
|
|
1646
|
+
...data ? { data } : {}
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
/** Append one handoff to its recipient's inbox, stamped now. */
|
|
1650
|
+
function appendHandoff(logDir, h, now = /* @__PURE__ */ new Date()) {
|
|
1651
|
+
const path = inboxPath(logDir, h.to);
|
|
1652
|
+
const dir = dirname(path);
|
|
1653
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
1654
|
+
appendFileSync(path, JSON.stringify({
|
|
1655
|
+
...h,
|
|
1656
|
+
_ts: now.toISOString()
|
|
1657
|
+
}) + "\n", "utf8");
|
|
1658
|
+
}
|
|
1659
|
+
/** Every handoff in a worker's inbox, oldest first; damaged lines skip. */
|
|
1660
|
+
function readInbox(logDir, id) {
|
|
1661
|
+
const path = inboxPath(logDir, id);
|
|
1662
|
+
if (!existsSync(path)) return [];
|
|
1663
|
+
const out = [];
|
|
1664
|
+
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
1665
|
+
if (!line.trim()) continue;
|
|
1666
|
+
try {
|
|
1667
|
+
out.push(JSON.parse(line));
|
|
1668
|
+
} catch {}
|
|
1669
|
+
}
|
|
1670
|
+
return out;
|
|
1671
|
+
}
|
|
1672
|
+
/** How the say path prefixes a handoff so the parent model knows its source. */
|
|
1673
|
+
function handoffMessage(h) {
|
|
1674
|
+
return `[handoff from ${h.from}] (${h.kind}) ${h.text.replace(/\s+/g, " ").trim()}`;
|
|
1675
|
+
}
|
|
1676
|
+
/** True for the exact shape of handoffMessage() — the runner marks the mirror. */
|
|
1677
|
+
function isHandoffMessage(text) {
|
|
1678
|
+
return new RegExp(`^\\[handoff from \\S+\\] \\((${HANDOFF_KINDS.join("|")})\\) `).test(text);
|
|
1679
|
+
}
|
|
1680
|
+
/**
|
|
1681
|
+
* Deliver one handoff: append it to the parent's inbox, then — when the
|
|
1682
|
+
* parent is a running worker with a live operator socket — say it so it
|
|
1683
|
+
* enters the parent's conversation. The say is best effort: a parent that is
|
|
1684
|
+
* busy, finished or gone still has the inbox line, and nothing here may make
|
|
1685
|
+
* a finishing child's exit path fail.
|
|
1686
|
+
*/
|
|
1687
|
+
async function deliverHandoff(logDir, h, deps = {}, now = /* @__PURE__ */ new Date()) {
|
|
1688
|
+
appendHandoff(logDir, h, now);
|
|
1689
|
+
const say = deps.say ?? ((id, text) => sayToWorker(logDir, id, text));
|
|
1690
|
+
const parent = loadStatus(logDir, h.to);
|
|
1691
|
+
if (parent && parent.state === "running" && alive(parent.pid)) try {
|
|
1692
|
+
await say(h.to, handoffMessage(h));
|
|
1693
|
+
} catch {}
|
|
1694
|
+
return h;
|
|
1695
|
+
}
|
|
1696
|
+
/**
|
|
1697
|
+
* Send a handoff from inside a worker (the `pai worker handoff` CLI and the
|
|
1698
|
+
* MCP tool both land here): the sender comes from PAI_WORKER_ID, the
|
|
1699
|
+
* recipient from its status file's `parent`. Rejects with a clear message
|
|
1700
|
+
* outside a worker or under a parentless worker.
|
|
1701
|
+
*/
|
|
1702
|
+
async function handoffFromInside(logDir, env, payload, deps = {}) {
|
|
1703
|
+
const mine = env.PAI_WORKER_ID;
|
|
1704
|
+
if (!mine) throw new Error("not inside a worker — handoffs are sent by workers (PAI_WORKER_ID unset). Talk to a running worker with: pai worker say <id> \"<text>\"");
|
|
1705
|
+
const st = loadStatus(logDir, mine);
|
|
1706
|
+
if (!st) throw new Error(`no status for this worker (${mine}) — cannot find its parent`);
|
|
1707
|
+
if (!st.parent || !loadStatus(logDir, st.parent)) throw new Error(`worker ${mine} has no worker parent to hand off to (parent: ${st.parent ?? "(none)"}) — handoffs go up the worker tree only`);
|
|
1708
|
+
return deliverHandoff(logDir, parseHandoff(payload, {
|
|
1709
|
+
from: mine,
|
|
1710
|
+
to: st.parent
|
|
1711
|
+
}), deps);
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
//#endregion
|
|
1715
|
+
//#region src/workers/worktree.ts
|
|
1716
|
+
/**
|
|
1717
|
+
* worktree.ts — one git worktree per writing worker.
|
|
1718
|
+
*
|
|
1719
|
+
* A run whose class edits files (implement, complex, plan) and whose cwd is a
|
|
1720
|
+
* git repository gets its own worktree by default: `git worktree add
|
|
1721
|
+
* <logDir>/worktrees/<id> -b worker/<id>` from the current HEAD. The worker
|
|
1722
|
+
* commits its own work on that branch — the no-commit rule applies to the
|
|
1723
|
+
* main branch only — and the parent (or the operator) merges the result back:
|
|
1724
|
+
*
|
|
1725
|
+
* pai worker merge <id> git merge --no-ff worker/<id> + remove worktree
|
|
1726
|
+
* pai worker discard <id> remove worktree and branch, keep nothing
|
|
1727
|
+
*
|
|
1728
|
+
* `ps` marks a worker with an unmerged branch `⎇`. Draft and review run in
|
|
1729
|
+
* place; `--no-worktree` opts out, `--worktree` forces one on.
|
|
1730
|
+
*/
|
|
1731
|
+
function worktreesDir(logDir) {
|
|
1732
|
+
return join(logDir, "worktrees");
|
|
1733
|
+
}
|
|
1734
|
+
function worktreeBranch(id) {
|
|
1735
|
+
return `worker/${id}`;
|
|
1736
|
+
}
|
|
1737
|
+
function worktreePath(logDir, id) {
|
|
1738
|
+
return join(worktreesDir(logDir), id);
|
|
1739
|
+
}
|
|
1740
|
+
/** Run git in `cwd`, returning trimmed stdout; throws with stderr on failure. */
|
|
1741
|
+
function git(cwd, args) {
|
|
1742
|
+
try {
|
|
1743
|
+
return execFileSync("git", [
|
|
1744
|
+
"-C",
|
|
1745
|
+
cwd,
|
|
1746
|
+
...args
|
|
1747
|
+
], {
|
|
1748
|
+
encoding: "utf8",
|
|
1749
|
+
timeout: 3e4,
|
|
1750
|
+
stdio: [
|
|
1751
|
+
"ignore",
|
|
1752
|
+
"pipe",
|
|
1753
|
+
"pipe"
|
|
1754
|
+
]
|
|
1755
|
+
}).trim();
|
|
1756
|
+
} catch (e) {
|
|
1757
|
+
const err = e;
|
|
1758
|
+
const why = (typeof err.stderr === "string" ? err.stderr : err.stderr?.toString("utf8")) || err.message || String(e);
|
|
1759
|
+
throw new Error(`git ${args.join(" ")} in ${cwd}: ${why.trim()}`);
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
/** Is `cwd` inside a git repository (a .git dir — worktrees: a .git file)? */
|
|
1763
|
+
function isGitRepo(cwd) {
|
|
1764
|
+
try {
|
|
1765
|
+
return git(cwd, ["rev-parse", "--git-dir"]) !== "";
|
|
1766
|
+
} catch {
|
|
1767
|
+
return false;
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
/**
|
|
1771
|
+
* Does the prompt read as a read-only task? Writing classes default to a
|
|
1772
|
+
* worktree; a prompt that only asks to look at things should not pay for one.
|
|
1773
|
+
* First-word verbs plus the explicit markers people actually write.
|
|
1774
|
+
*/
|
|
1775
|
+
function promptLooksReadonly(prompt) {
|
|
1776
|
+
const p = prompt.trim();
|
|
1777
|
+
if (!p) return true;
|
|
1778
|
+
if (/\b(read[- ]only|do not (modify|change|edit|write)|don'?t (modify|change|edit|write)|no changes)\b/i.test(p)) return true;
|
|
1779
|
+
return /^(review|read|analy[sz]e|research|summar[iy]|inspect|investigate|spotcheck|report|find|list|check|verify|describe|explain|show)\b/i.test(p);
|
|
1780
|
+
}
|
|
1781
|
+
/** The classes whose runs write files and therefore default to a worktree. */
|
|
1782
|
+
const WORKTREE_CLASSES = [
|
|
1783
|
+
"implement",
|
|
1784
|
+
"complex",
|
|
1785
|
+
"plan"
|
|
1786
|
+
];
|
|
1787
|
+
/** Should this run get a worktree? Explicit flag first, then the default rule. */
|
|
1788
|
+
function worktreeWanted(flag, opts) {
|
|
1789
|
+
if (flag !== void 0) return flag;
|
|
1790
|
+
if (!opts.className || !WORKTREE_CLASSES.includes(opts.className)) return false;
|
|
1791
|
+
if (!isGitRepo(opts.cwd)) return false;
|
|
1792
|
+
return !promptLooksReadonly(opts.prompt ?? "");
|
|
1793
|
+
}
|
|
1794
|
+
/**
|
|
1795
|
+
* Create the worktree and branch for `id` from `cwd`'s HEAD. Throws when git
|
|
1796
|
+
* refuses (no commits yet, branch exists, …) — the caller decides whether to
|
|
1797
|
+
* degrade to an in-place run.
|
|
1798
|
+
*/
|
|
1799
|
+
function addWorktree(logDir, id, cwd) {
|
|
1800
|
+
const dir = worktreePath(logDir, id);
|
|
1801
|
+
const branch = worktreeBranch(id);
|
|
1802
|
+
const base = git(cwd, ["rev-parse", "HEAD"]);
|
|
1803
|
+
git(cwd, [
|
|
1804
|
+
"worktree",
|
|
1805
|
+
"add",
|
|
1806
|
+
dir,
|
|
1807
|
+
"-b",
|
|
1808
|
+
branch
|
|
1809
|
+
]);
|
|
1810
|
+
return {
|
|
1811
|
+
dir,
|
|
1812
|
+
branch,
|
|
1813
|
+
base
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1816
|
+
/** Commits the branch collected on top of its base. */
|
|
1817
|
+
function commitsSince(dir, base) {
|
|
1818
|
+
try {
|
|
1819
|
+
return parseInt(git(dir, [
|
|
1820
|
+
"rev-list",
|
|
1821
|
+
"--count",
|
|
1822
|
+
`${base}..HEAD`
|
|
1823
|
+
]), 10) || 0;
|
|
1824
|
+
} catch {
|
|
1825
|
+
return 0;
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
/**
|
|
1829
|
+
* Record the worktree result in the status file: branch and commit count on
|
|
1830
|
+
* success, the worktree cleaned up on failure. Returns the updated status.
|
|
1831
|
+
*/
|
|
1832
|
+
function recordWorktree(logDir, status, info, ok) {
|
|
1833
|
+
const s = { ...status };
|
|
1834
|
+
if (ok) {
|
|
1835
|
+
s.branch = info.branch;
|
|
1836
|
+
s.commits = commitsSince(info.dir, info.base);
|
|
1837
|
+
s.worktreeDir = info.dir;
|
|
1838
|
+
s.worktreeBase = info.base;
|
|
1839
|
+
} else {
|
|
1840
|
+
removeWorktree(s.cwd, info.dir, true);
|
|
1841
|
+
try {
|
|
1842
|
+
git(s.cwd, [
|
|
1843
|
+
"branch",
|
|
1844
|
+
"-D",
|
|
1845
|
+
info.branch
|
|
1846
|
+
]);
|
|
1847
|
+
} catch {}
|
|
1848
|
+
s.branch = null;
|
|
1849
|
+
s.worktreeDir = null;
|
|
1850
|
+
s.worktreeBase = null;
|
|
1851
|
+
s.commits = null;
|
|
1852
|
+
}
|
|
1853
|
+
saveStatus(logDir, s);
|
|
1854
|
+
return s;
|
|
1855
|
+
}
|
|
1856
|
+
/** Remove a worktree directory from git's books and the filesystem. */
|
|
1857
|
+
function removeWorktree(cwd, dir, force) {
|
|
1858
|
+
try {
|
|
1859
|
+
git(cwd, [
|
|
1860
|
+
"worktree",
|
|
1861
|
+
"remove",
|
|
1862
|
+
...force ? ["--force"] : [],
|
|
1863
|
+
dir
|
|
1864
|
+
]);
|
|
1865
|
+
return;
|
|
1866
|
+
} catch {}
|
|
1867
|
+
if (existsSync(dir)) try {
|
|
1868
|
+
rmSync(dir, {
|
|
1869
|
+
recursive: true,
|
|
1870
|
+
force: true
|
|
1871
|
+
});
|
|
1872
|
+
git(cwd, ["worktree", "prune"]);
|
|
1873
|
+
} catch {}
|
|
1874
|
+
}
|
|
1875
|
+
/**
|
|
1876
|
+
* `pai worker merge <id>`: merge the worker's branch into the original
|
|
1877
|
+
* checkout with --no-ff (the merge commit names the worker), then remove the
|
|
1878
|
+
* worktree. The branch stays for the record; the status gains `merged: true`.
|
|
1879
|
+
*/
|
|
1880
|
+
function mergeWorker(logDir, id) {
|
|
1881
|
+
const st = mustHaveBranch(logDir, id);
|
|
1882
|
+
if (st.merged) return `worker ${id}: branch ${st.branch} already merged`;
|
|
1883
|
+
git(st.cwd, [
|
|
1884
|
+
"merge",
|
|
1885
|
+
"--no-ff",
|
|
1886
|
+
st.branch,
|
|
1887
|
+
"-m",
|
|
1888
|
+
`merge worker ${id} (${st.label})`
|
|
1889
|
+
]);
|
|
1890
|
+
removeWorktree(st.cwd, st.worktreeDir, false);
|
|
1891
|
+
saveStatus(logDir, {
|
|
1892
|
+
...st,
|
|
1893
|
+
merged: true
|
|
1894
|
+
});
|
|
1895
|
+
return `merged ${st.branch} into ${st.cwd} (worktree removed)`;
|
|
1896
|
+
}
|
|
1897
|
+
/** `pai worker discard <id>`: drop worktree and branch, keep nothing. */
|
|
1898
|
+
function discardWorker(logDir, id) {
|
|
1899
|
+
const st = mustHaveBranch(logDir, id);
|
|
1900
|
+
removeWorktree(st.cwd, st.worktreeDir, true);
|
|
1901
|
+
let branchGone = false;
|
|
1902
|
+
try {
|
|
1903
|
+
git(st.cwd, [
|
|
1904
|
+
"branch",
|
|
1905
|
+
"-D",
|
|
1906
|
+
st.branch
|
|
1907
|
+
]);
|
|
1908
|
+
branchGone = true;
|
|
1909
|
+
} catch {
|
|
1910
|
+
branchGone = false;
|
|
1911
|
+
}
|
|
1912
|
+
saveStatus(logDir, {
|
|
1913
|
+
...st,
|
|
1914
|
+
branch: null,
|
|
1915
|
+
worktreeDir: null,
|
|
1916
|
+
worktreeBase: null,
|
|
1917
|
+
commits: null,
|
|
1918
|
+
merged: false
|
|
1919
|
+
});
|
|
1920
|
+
return `discarded worker ${id}: worktree removed${branchGone ? `, branch ${st.branch} deleted` : ""}`;
|
|
1921
|
+
}
|
|
1922
|
+
function mustHaveBranch(logDir, id) {
|
|
1923
|
+
const st = loadStatus(logDir, id);
|
|
1924
|
+
if (!st) throw new Error(`no worker named "${id}"`);
|
|
1925
|
+
if (!st.branch || !st.worktreeDir) throw new Error(`worker ${id} has no worktree branch to ${st.branch ? "clean up" : "merge"} — it ran in place or its branch was already handled`);
|
|
1926
|
+
return st;
|
|
1927
|
+
}
|
|
1928
|
+
/**
|
|
1929
|
+
* The paragraph a worktree run's system prompt gains: it may commit on its
|
|
1930
|
+
* own branch (the no-commit rule holds for the main branch only), it must not
|
|
1931
|
+
* merge or push itself, and the parent or operator merges.
|
|
1932
|
+
*/
|
|
1933
|
+
function worktreeSystemPrompt(id, branch, dir) {
|
|
1934
|
+
return [
|
|
1935
|
+
"You are running in your own git worktree:",
|
|
1936
|
+
` ${dir} on branch ${branch} (worker id ${id}).`,
|
|
1937
|
+
"Commit your work on that branch as you go (git add / git commit) — committing here is expected;",
|
|
1938
|
+
"the no-commit rule applies to the main branch only, and this is not it.",
|
|
1939
|
+
"Do not merge, rebase or push; the operator merges your branch back with `pai worker merge`."
|
|
1940
|
+
].join("\n");
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1300
1943
|
//#endregion
|
|
1301
1944
|
//#region src/workers/report.ts
|
|
1302
1945
|
/**
|
|
@@ -1307,6 +1950,8 @@ async function openFollowPane(logDir, _config, term, checkOnly) {
|
|
|
1307
1950
|
* and nothing else. The runner parses it out of the result; the viewer renders
|
|
1308
1951
|
* it as a compact block instead of a wall of text.
|
|
1309
1952
|
*/
|
|
1953
|
+
/** The marker the runner puts on operator messages the worker must answer. */
|
|
1954
|
+
const OPERATOR_MARK = "[operator]";
|
|
1310
1955
|
/** Appended to the caller's system prompt on every headless run (2h). */
|
|
1311
1956
|
const WORKER_CONTRACT_PROMPT = [
|
|
1312
1957
|
"You are a headless implementation worker, run non-interactively by an orchestrating session.",
|
|
@@ -1315,7 +1960,20 @@ const WORKER_CONTRACT_PROMPT = [
|
|
|
1315
1960
|
"{\"changed\":[{\"path\":\"…\",\"summary\":\"…\"}],\"commands\":[\"…\"],\"checks\":[{\"name\":\"…\",\"ok\":true,\"detail\":\"…\"}],\"open\":[\"…\"],\"notes\":\"one line\"}",
|
|
1316
1961
|
"changed: files you touched (path + one-line summary). commands: the commands that verify the work.",
|
|
1317
1962
|
"checks: each with ok true/false and the evidence in detail. open: what you could not finish, if anything.",
|
|
1318
|
-
"notes: one line, the headline a reviewer reads first."
|
|
1963
|
+
"notes: one line, the headline a reviewer reads first.",
|
|
1964
|
+
"",
|
|
1965
|
+
`Operator messages: a user turn starting with ${OPERATOR_MARK} was typed by the operator while you`,
|
|
1966
|
+
"run (the prompt of a resumed run is the operator's too). Answer it FIRST, in one or two plain lines —",
|
|
1967
|
+
"a question gets its answer, an instruction gets one line naming what will change — then continue the",
|
|
1968
|
+
"task you were on.",
|
|
1969
|
+
"",
|
|
1970
|
+
"Sub-workers: you may start your own workers with `pai worker run --class <class> -p '<prompt>'`",
|
|
1971
|
+
"(Bash tool). Your children run on their own provider and report back to you automatically when",
|
|
1972
|
+
"they finish. Send a handoff UP instead of doing work yourself when a cheaper provider would",
|
|
1973
|
+
"suffice, the task is out of your scope, or a decision is needed:",
|
|
1974
|
+
"`pai worker handoff '{\"kind\":\"proposal\",\"text\":\"…\",\"data\":{…}}'` (kinds: proposal,",
|
|
1975
|
+
"question, blocker; results are sent for you when you finish). Sibling workers are not a",
|
|
1976
|
+
"coordination path — there is no sideways channel; everything goes up to your parent."
|
|
1319
1977
|
].join("\n");
|
|
1320
1978
|
/**
|
|
1321
1979
|
* Extract the JSON report from a worker's final message. Accepts a bare
|
|
@@ -1465,81 +2123,6 @@ function describeMcp(config, claudeJson = CLAUDE_JSON) {
|
|
|
1465
2123
|
return lines;
|
|
1466
2124
|
}
|
|
1467
2125
|
|
|
1468
|
-
//#endregion
|
|
1469
|
-
//#region src/workers/operator.ts
|
|
1470
|
-
/**
|
|
1471
|
-
* operator.ts — talking to a running worker.
|
|
1472
|
-
*
|
|
1473
|
-
* Headless workers run with `--input-format stream-json` and their stdin held
|
|
1474
|
-
* open, so a run is a conversation: every line sent to the per-worker Unix
|
|
1475
|
-
* socket `<logDir>/<id>.sock` is forwarded to the child as a user message and
|
|
1476
|
-
* mirrored into the transcript as an `operator` event (rendered with a »
|
|
1477
|
-
* marker). Once the worker has finished its turn, stdin closes 2 s later
|
|
1478
|
-
* unless a new message arrives — after that, `say` refuses and `resume`
|
|
1479
|
-
* continues the same Claude session with the worker's context intact.
|
|
1480
|
-
*/
|
|
1481
|
-
function operatorSocketPath(logDir, id) {
|
|
1482
|
-
return join(logDir, `${id}.sock`);
|
|
1483
|
-
}
|
|
1484
|
-
/**
|
|
1485
|
-
* The runner's side: listen on the worker socket, hand every received line to
|
|
1486
|
-
* `onLine`. Returns the server (close it when the run ends; the socket file is
|
|
1487
|
-
* unlinked on close, best effort).
|
|
1488
|
-
*/
|
|
1489
|
-
function createOperatorServer(logDir, id, onLine) {
|
|
1490
|
-
const path = operatorSocketPath(logDir, id);
|
|
1491
|
-
try {
|
|
1492
|
-
if (existsSync(path)) unlinkSync(path);
|
|
1493
|
-
} catch {}
|
|
1494
|
-
const server = createServer((socket) => {
|
|
1495
|
-
let buf = "";
|
|
1496
|
-
socket.on("data", (chunk) => {
|
|
1497
|
-
buf += chunk.toString("utf8");
|
|
1498
|
-
let nl;
|
|
1499
|
-
while ((nl = buf.indexOf("\n")) >= 0) {
|
|
1500
|
-
const line = buf.slice(0, nl).replace(/\r$/, "");
|
|
1501
|
-
buf = buf.slice(nl + 1);
|
|
1502
|
-
if (line.trim()) onLine(line);
|
|
1503
|
-
socket.write("ok\n");
|
|
1504
|
-
}
|
|
1505
|
-
});
|
|
1506
|
-
});
|
|
1507
|
-
server.listen(path);
|
|
1508
|
-
server.on("close", () => {
|
|
1509
|
-
try {
|
|
1510
|
-
if (existsSync(path)) unlinkSync(path);
|
|
1511
|
-
} catch {}
|
|
1512
|
-
});
|
|
1513
|
-
return server;
|
|
1514
|
-
}
|
|
1515
|
-
/**
|
|
1516
|
-
* `pai worker say <id> "<text>"`: forward one line to a running worker.
|
|
1517
|
-
* Resolves "ok", rejects with a clear message when the worker is not running.
|
|
1518
|
-
*/
|
|
1519
|
-
function sayToWorker(logDir, id, text, timeoutMs = 4e3) {
|
|
1520
|
-
const status = loadStatus(logDir, id);
|
|
1521
|
-
if (!status) return Promise.reject(/* @__PURE__ */ new Error(`no worker named "${id}"`));
|
|
1522
|
-
if (status.state !== "running" || !alive(status.pid)) return Promise.reject(/* @__PURE__ */ new Error(`worker ${id} is not running (state: ${status.state}) — continue it instead with: pai worker resume ${id} "<text>"`));
|
|
1523
|
-
const path = operatorSocketPath(logDir, id);
|
|
1524
|
-
if (!existsSync(path)) return Promise.reject(/* @__PURE__ */ new Error(`worker ${id} has no operator socket (${path}) — it may predate this PAI version`));
|
|
1525
|
-
return new Promise((resolve, reject) => {
|
|
1526
|
-
const sock = connect(path);
|
|
1527
|
-
const fail = (e) => {
|
|
1528
|
-
sock.destroy();
|
|
1529
|
-
reject(/* @__PURE__ */ new Error(`cannot talk to worker ${id}: ${e.message}`));
|
|
1530
|
-
};
|
|
1531
|
-
sock.setTimeout(timeoutMs, () => fail(/* @__PURE__ */ new Error("timeout")));
|
|
1532
|
-
sock.once("error", (e) => fail(e));
|
|
1533
|
-
sock.once("connect", () => {
|
|
1534
|
-
sock.write(text.replace(/\n/g, " ") + "\n");
|
|
1535
|
-
});
|
|
1536
|
-
sock.once("data", () => {
|
|
1537
|
-
sock.end();
|
|
1538
|
-
resolve("ok");
|
|
1539
|
-
});
|
|
1540
|
-
});
|
|
1541
|
-
}
|
|
1542
|
-
|
|
1543
2126
|
//#endregion
|
|
1544
2127
|
//#region src/workers/proxy/server.ts
|
|
1545
2128
|
/**
|
|
@@ -1877,9 +2460,23 @@ function stdinUserMessage(text) {
|
|
|
1877
2460
|
}
|
|
1878
2461
|
});
|
|
1879
2462
|
}
|
|
1880
|
-
/**
|
|
1881
|
-
function
|
|
1882
|
-
return
|
|
2463
|
+
/** An operator line as the worker sees it: carrying the contract's marker. */
|
|
2464
|
+
function operatorUserText(text) {
|
|
2465
|
+
return `${OPERATOR_MARK} ${text}`;
|
|
2466
|
+
}
|
|
2467
|
+
/**
|
|
2468
|
+
* ISO stamp with seconds, attached to every mirrored event (2g): local time
|
|
2469
|
+
* with its offset (`2026-09-17T14:19:23+02:00`), so the viewer can render the
|
|
2470
|
+
* wall clock the operator lives in. `offMin` is east-positive minutes —
|
|
2471
|
+
* injectable so tests do not depend on the machine's zone.
|
|
2472
|
+
*/
|
|
2473
|
+
function isoStamp(d = /* @__PURE__ */ new Date(), offMin = -d.getTimezoneOffset()) {
|
|
2474
|
+
const t = new Date(d.getTime() + offMin * 6e4);
|
|
2475
|
+
const sign = offMin < 0 ? "-" : "+";
|
|
2476
|
+
const abs = Math.abs(offMin);
|
|
2477
|
+
const hh = String(Math.floor(abs / 60)).padStart(2, "0");
|
|
2478
|
+
const mm = String(abs % 60).padStart(2, "0");
|
|
2479
|
+
return `${t.toISOString().slice(0, 19)}${sign}${hh}:${mm}`;
|
|
1883
2480
|
}
|
|
1884
2481
|
/** Context tokens of an assistant/result usage block (input+cache+output). */
|
|
1885
2482
|
function usageContextTokens(u) {
|
|
@@ -1902,9 +2499,15 @@ async function runWorker(opts) {
|
|
|
1902
2499
|
if (!config.enabled) throw new Error("workers are off. Turn them on with: pai worker on\n(then the Agent-tool hook stops denying Anthropic subagents only when you do)");
|
|
1903
2500
|
const logDir = workersLogDir(config);
|
|
1904
2501
|
mkdirSync(logDir, { recursive: true });
|
|
2502
|
+
if (opts.className === "plan" && !opts._planner) {
|
|
2503
|
+
const { runPlanner } = await import("./planner-BDI7bE9B.mjs");
|
|
2504
|
+
return runPlanner(opts);
|
|
2505
|
+
}
|
|
2506
|
+
const parent = launchParent(opts.parent);
|
|
2507
|
+
if (parent) assertChildAllowed(logDir, parent, config.tree);
|
|
1905
2508
|
const target = resolveTarget(config, logDir, {
|
|
1906
2509
|
flagProvider: opts.providerFlag,
|
|
1907
|
-
|
|
2510
|
+
className: opts.className
|
|
1908
2511
|
});
|
|
1909
2512
|
assertProviderRunnable(target.providerName, target.provider);
|
|
1910
2513
|
const parsed = parseRunnerArgs(opts.claudeArgs);
|
|
@@ -1920,7 +2523,14 @@ async function runWorker(opts) {
|
|
|
1920
2523
|
parsed,
|
|
1921
2524
|
claudeArgs: opts.claudeArgs,
|
|
1922
2525
|
noPane: opts.noPane ?? false,
|
|
1923
|
-
|
|
2526
|
+
cwd: opts.cwd,
|
|
2527
|
+
parent: parent ?? void 0,
|
|
2528
|
+
stage: opts.stage,
|
|
2529
|
+
quiet: opts.quiet,
|
|
2530
|
+
onWorkerStart: opts.onWorkerStart,
|
|
2531
|
+
id: opts.id,
|
|
2532
|
+
worktreeFlag: opts.worktreeFlag,
|
|
2533
|
+
className: opts.className
|
|
1924
2534
|
});
|
|
1925
2535
|
return await executeRun({
|
|
1926
2536
|
config,
|
|
@@ -1932,7 +2542,14 @@ async function runWorker(opts) {
|
|
|
1932
2542
|
claudeArgs: opts.claudeArgs,
|
|
1933
2543
|
noPane: opts.noPane ?? false,
|
|
1934
2544
|
mcpFlag: opts.mcpFlag,
|
|
2545
|
+
cwd: opts.cwd,
|
|
2546
|
+
parent: parent ?? void 0,
|
|
2547
|
+
stage: opts.stage,
|
|
2548
|
+
quiet: opts.quiet,
|
|
1935
2549
|
onWorkerStart: opts.onWorkerStart,
|
|
2550
|
+
id: opts.id,
|
|
2551
|
+
worktreeFlag: opts.worktreeFlag,
|
|
2552
|
+
className: opts.className,
|
|
1936
2553
|
reroutes: opts._reroutes ?? 0
|
|
1937
2554
|
});
|
|
1938
2555
|
} catch (e) {
|
|
@@ -1946,10 +2563,26 @@ async function executeRun(a) {
|
|
|
1946
2563
|
let proxyUrl;
|
|
1947
2564
|
if (target.provider.protocol === "openai") proxyUrl = `${await ensureProxyRunning(DEFAULT_PROXY_PORT, logDir)}/${target.providerName}`;
|
|
1948
2565
|
const env = buildRunEnv(target.provider, headless, proxyUrl);
|
|
1949
|
-
const wid = newWorkerId();
|
|
1950
|
-
const cwd = process.cwd();
|
|
2566
|
+
const wid = a.id ?? newWorkerId();
|
|
2567
|
+
const cwd = a.cwd ?? process.cwd();
|
|
1951
2568
|
const term = process.env.ITERM_SESSION_ID ?? "";
|
|
1952
2569
|
const session = resolveSession(term);
|
|
2570
|
+
let worktree = null;
|
|
2571
|
+
if (headless && worktreeWanted(a.worktreeFlag, {
|
|
2572
|
+
cwd,
|
|
2573
|
+
className: a.className,
|
|
2574
|
+
prompt: parsed.prompt
|
|
2575
|
+
})) try {
|
|
2576
|
+
worktree = addWorktree(logDir, wid, cwd);
|
|
2577
|
+
} catch (e) {
|
|
2578
|
+
const why = e.message;
|
|
2579
|
+
process.stderr.write(`pai worker: no worktree (${why}) — running in place\n`);
|
|
2580
|
+
appendLedger(ledgerPath(logDir), "WORKER-NOTE", {
|
|
2581
|
+
id: wid,
|
|
2582
|
+
note: `no worktree: ${why}`
|
|
2583
|
+
});
|
|
2584
|
+
}
|
|
2585
|
+
env.PAI_WORKER_ID = wid;
|
|
1953
2586
|
const status = {
|
|
1954
2587
|
id: wid,
|
|
1955
2588
|
pid: process.pid,
|
|
@@ -1967,7 +2600,16 @@ async function executeRun(a) {
|
|
|
1967
2600
|
rc: null,
|
|
1968
2601
|
secs: null,
|
|
1969
2602
|
...session ? { session } : {},
|
|
1970
|
-
contextWindow: providerContextWindow(target.provider)
|
|
2603
|
+
contextWindow: providerContextWindow(target.provider),
|
|
2604
|
+
...a.parent ? {
|
|
2605
|
+
parent: a.parent,
|
|
2606
|
+
stage: a.stage
|
|
2607
|
+
} : {},
|
|
2608
|
+
...worktree ? {
|
|
2609
|
+
worktreeDir: worktree.dir,
|
|
2610
|
+
branch: worktree.branch,
|
|
2611
|
+
worktreeBase: worktree.base
|
|
2612
|
+
} : {}
|
|
1971
2613
|
};
|
|
1972
2614
|
saveStatus(logDir, status);
|
|
1973
2615
|
a.onWorkerStart?.(wid);
|
|
@@ -1986,7 +2628,7 @@ async function executeRun(a) {
|
|
|
1986
2628
|
const wanted = [
|
|
1987
2629
|
...a.mcpFlag ? [a.mcpFlag] : [],
|
|
1988
2630
|
...parsed.mcp,
|
|
1989
|
-
...target.
|
|
2631
|
+
...target.classMcp ?? []
|
|
1990
2632
|
];
|
|
1991
2633
|
if (wanted.length) mcpArgs = [
|
|
1992
2634
|
"--strict-mcp-config",
|
|
@@ -2006,11 +2648,12 @@ async function executeRun(a) {
|
|
|
2006
2648
|
if (headless) {
|
|
2007
2649
|
cmd.push("--output-format", "stream-json", "--verbose", "--input-format", "stream-json");
|
|
2008
2650
|
if (!parsed.callerSystemPrompt) cmd.push("--append-system-prompt", WORKER_CONTRACT_PROMPT);
|
|
2651
|
+
if (worktree) cmd.push("--append-system-prompt", worktreeSystemPrompt(wid, worktree.branch, worktree.dir));
|
|
2009
2652
|
}
|
|
2010
2653
|
const t0 = Date.now();
|
|
2011
2654
|
const proc = spawn(cmd[0], cmd.slice(1), {
|
|
2012
2655
|
env,
|
|
2013
|
-
cwd,
|
|
2656
|
+
cwd: worktree?.dir ?? cwd,
|
|
2014
2657
|
stdio: headless ? [
|
|
2015
2658
|
"pipe",
|
|
2016
2659
|
"pipe",
|
|
@@ -2045,10 +2688,11 @@ async function executeRun(a) {
|
|
|
2045
2688
|
}
|
|
2046
2689
|
writeEvent({
|
|
2047
2690
|
type: "operator",
|
|
2048
|
-
text
|
|
2691
|
+
text,
|
|
2692
|
+
handoff: isHandoffMessage(text)
|
|
2049
2693
|
});
|
|
2050
2694
|
try {
|
|
2051
|
-
proc.stdin?.write(stdinUserMessage(text) + "\n");
|
|
2695
|
+
proc.stdin?.write(stdinUserMessage(operatorUserText(text)) + "\n");
|
|
2052
2696
|
} catch {}
|
|
2053
2697
|
}) : null;
|
|
2054
2698
|
let killed = false;
|
|
@@ -2154,7 +2798,8 @@ async function executeRun(a) {
|
|
|
2154
2798
|
tools: status.tools,
|
|
2155
2799
|
label
|
|
2156
2800
|
});
|
|
2157
|
-
if (
|
|
2801
|
+
if (worktree) recordWorktree(logDir, status, worktree, ok);
|
|
2802
|
+
if (headless && !a.quiet) printResult(parsed.outputFormat, resultEvent, rc, logDir, wid, ctx.resultReport, worktreeExtras(status));
|
|
2158
2803
|
const resultText = resultEvent?.result ?? "";
|
|
2159
2804
|
if (!ok && headless && a.target.via === "auto" && config.routing.retryOnQuota && status.turns <= 1 && status.tools === 0 && isQuotaFailure(resultText)) {
|
|
2160
2805
|
setCooldown(logDir, target.providerName, config.routing.cooldownMinutes);
|
|
@@ -2170,22 +2815,69 @@ async function executeRun(a) {
|
|
|
2170
2815
|
label,
|
|
2171
2816
|
noPane: a.noPane,
|
|
2172
2817
|
mcpFlag: a.mcpFlag,
|
|
2818
|
+
cwd: a.cwd,
|
|
2819
|
+
parent: a.parent,
|
|
2820
|
+
stage: a.stage,
|
|
2173
2821
|
claudeArgs: a.claudeArgs,
|
|
2174
2822
|
onWorkerStart: a.onWorkerStart,
|
|
2823
|
+
worktreeFlag: a.worktreeFlag,
|
|
2824
|
+
className: a.className,
|
|
2175
2825
|
_reroutes: a.reroutes + 1
|
|
2176
2826
|
});
|
|
2177
2827
|
}
|
|
2178
2828
|
}
|
|
2179
|
-
|
|
2829
|
+
const finalRc = rc !== 0 ? rc : ok ? 0 : 1;
|
|
2830
|
+
if (status.parent && isWorkerId(logDir, status.parent)) try {
|
|
2831
|
+
await deliverHandoff(logDir, {
|
|
2832
|
+
from: wid,
|
|
2833
|
+
to: status.parent,
|
|
2834
|
+
kind: "result",
|
|
2835
|
+
text: shortText(ctx.resultReport?.notes ?? resultEvent?.result ?? (ok ? "done" : "failed"), 400),
|
|
2836
|
+
data: {
|
|
2837
|
+
rc: finalRc,
|
|
2838
|
+
ok,
|
|
2839
|
+
...status.branch ? {
|
|
2840
|
+
branch: status.branch,
|
|
2841
|
+
commits: status.commits ?? 0
|
|
2842
|
+
} : {},
|
|
2843
|
+
...ctx.resultReport ? { report: ctx.resultReport } : {}
|
|
2844
|
+
}
|
|
2845
|
+
});
|
|
2846
|
+
} catch {}
|
|
2847
|
+
return finalRc;
|
|
2848
|
+
}
|
|
2849
|
+
/** The worktree fields printResult adds to a json payload, when there is one. */
|
|
2850
|
+
function worktreeExtras(s) {
|
|
2851
|
+
return s.branch ? {
|
|
2852
|
+
branch: s.branch,
|
|
2853
|
+
commits: s.commits ?? 0
|
|
2854
|
+
} : void 0;
|
|
2180
2855
|
}
|
|
2181
2856
|
async function executeCodexRun(a) {
|
|
2182
2857
|
const { config, logDir, target, model, label, parsed, noPane } = a;
|
|
2183
2858
|
if (!parsed.headless || parsed.prompt === null) throw new Error(`provider "${target.providerName}" (engine codex) supports headless runs only: pass the task with -p '<prompt>'`);
|
|
2184
2859
|
const env = buildCodexEnv(target.provider);
|
|
2185
|
-
const wid = newWorkerId();
|
|
2186
|
-
const cwd = process.cwd();
|
|
2860
|
+
const wid = a.id ?? newWorkerId();
|
|
2861
|
+
const cwd = a.cwd ?? process.cwd();
|
|
2187
2862
|
const term = process.env.ITERM_SESSION_ID ?? "";
|
|
2188
2863
|
const session = resolveSession(term);
|
|
2864
|
+
let worktree = null;
|
|
2865
|
+
if (worktreeWanted(a.worktreeFlag, {
|
|
2866
|
+
cwd,
|
|
2867
|
+
className: a.className,
|
|
2868
|
+
prompt: parsed.prompt
|
|
2869
|
+
})) try {
|
|
2870
|
+
worktree = addWorktree(logDir, wid, cwd);
|
|
2871
|
+
} catch (e) {
|
|
2872
|
+
const why = e.message;
|
|
2873
|
+
process.stderr.write(`pai worker: no worktree (${why}) — running in place\n`);
|
|
2874
|
+
appendLedger(ledgerPath(logDir), "WORKER-NOTE", {
|
|
2875
|
+
id: wid,
|
|
2876
|
+
note: `no worktree: ${why}`
|
|
2877
|
+
});
|
|
2878
|
+
}
|
|
2879
|
+
env.PAI_WORKER_ID = wid;
|
|
2880
|
+
const prompt = (worktree ? worktreeSystemPrompt(wid, worktree.branch, worktree.dir) + "\n\n" : "") + parsed.prompt;
|
|
2189
2881
|
const status = {
|
|
2190
2882
|
id: wid,
|
|
2191
2883
|
pid: process.pid,
|
|
@@ -2203,7 +2895,16 @@ async function executeCodexRun(a) {
|
|
|
2203
2895
|
rc: null,
|
|
2204
2896
|
secs: null,
|
|
2205
2897
|
...session ? { session } : {},
|
|
2206
|
-
contextWindow: providerContextWindow(target.provider)
|
|
2898
|
+
contextWindow: providerContextWindow(target.provider),
|
|
2899
|
+
...a.parent ? {
|
|
2900
|
+
parent: a.parent,
|
|
2901
|
+
stage: a.stage
|
|
2902
|
+
} : {},
|
|
2903
|
+
...worktree ? {
|
|
2904
|
+
worktreeDir: worktree.dir,
|
|
2905
|
+
branch: worktree.branch,
|
|
2906
|
+
worktreeBase: worktree.base
|
|
2907
|
+
} : {}
|
|
2207
2908
|
};
|
|
2208
2909
|
saveStatus(logDir, status);
|
|
2209
2910
|
a.onWorkerStart?.(wid);
|
|
@@ -2224,9 +2925,9 @@ async function executeCodexRun(a) {
|
|
|
2224
2925
|
});
|
|
2225
2926
|
if (!noPane && config.pane.enabled && term && process.env.PAI_WORKER_AUTOPANE !== "0") openPaneForWorker(logDir, config, wid, term).catch(() => {});
|
|
2226
2927
|
const t0 = Date.now();
|
|
2227
|
-
const proc = spawn("codex", buildCodexArgs(
|
|
2928
|
+
const proc = spawn("codex", buildCodexArgs(prompt, parsed.callerModel ? void 0 : model), {
|
|
2228
2929
|
env,
|
|
2229
|
-
cwd,
|
|
2930
|
+
cwd: worktree?.dir ?? cwd,
|
|
2230
2931
|
stdio: [
|
|
2231
2932
|
"ignore",
|
|
2232
2933
|
"pipe",
|
|
@@ -2313,10 +3014,29 @@ async function executeCodexRun(a) {
|
|
|
2313
3014
|
tools: status.tools,
|
|
2314
3015
|
label
|
|
2315
3016
|
});
|
|
2316
|
-
|
|
2317
|
-
|
|
3017
|
+
if (worktree) recordWorktree(logDir, status, worktree, ok);
|
|
3018
|
+
if (!a.quiet) printResult(parsed.outputFormat, resultEvent, rc, logDir, wid, report, worktreeExtras(status));
|
|
3019
|
+
const finalRc = rc !== 0 ? rc : ok ? 0 : 1;
|
|
3020
|
+
if (status.parent && isWorkerId(logDir, status.parent)) try {
|
|
3021
|
+
await deliverHandoff(logDir, {
|
|
3022
|
+
from: wid,
|
|
3023
|
+
to: status.parent,
|
|
3024
|
+
kind: "result",
|
|
3025
|
+
text: shortText(report?.notes ?? finalText ?? (ok ? "done" : "failed"), 400),
|
|
3026
|
+
data: {
|
|
3027
|
+
rc: finalRc,
|
|
3028
|
+
ok,
|
|
3029
|
+
...status.branch ? {
|
|
3030
|
+
branch: status.branch,
|
|
3031
|
+
commits: status.commits ?? 0
|
|
3032
|
+
} : {},
|
|
3033
|
+
...report ? { report } : {}
|
|
3034
|
+
}
|
|
3035
|
+
});
|
|
3036
|
+
} catch {}
|
|
3037
|
+
return finalRc;
|
|
2318
3038
|
}
|
|
2319
|
-
function printResult(fmt, resultEvent, rc, logDir, wid, report) {
|
|
3039
|
+
function printResult(fmt, resultEvent, rc, logDir, wid, report, extras) {
|
|
2320
3040
|
if (fmt === "stream-json") return;
|
|
2321
3041
|
if (fmt === "json") {
|
|
2322
3042
|
const payload = report ? {
|
|
@@ -2325,11 +3045,15 @@ function printResult(fmt, resultEvent, rc, logDir, wid, report) {
|
|
|
2325
3045
|
result: "no result event",
|
|
2326
3046
|
rc
|
|
2327
3047
|
},
|
|
2328
|
-
report
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
3048
|
+
report,
|
|
3049
|
+
...extras
|
|
3050
|
+
} : {
|
|
3051
|
+
...resultEvent ?? {
|
|
3052
|
+
is_error: true,
|
|
3053
|
+
result: "no result event",
|
|
3054
|
+
rc
|
|
3055
|
+
},
|
|
3056
|
+
...extras
|
|
2333
3057
|
};
|
|
2334
3058
|
console.log(JSON.stringify(payload));
|
|
2335
3059
|
return;
|
|
@@ -2441,627 +3165,161 @@ function resultFromOutput(out) {
|
|
|
2441
3165
|
}
|
|
2442
3166
|
|
|
2443
3167
|
//#endregion
|
|
2444
|
-
//#region src/workers/
|
|
3168
|
+
//#region src/workers/chain.ts
|
|
2445
3169
|
/**
|
|
2446
|
-
*
|
|
3170
|
+
* chain.ts — draft-then-implement chains: `--chain draft,implement[,review]`.
|
|
2447
3171
|
*
|
|
2448
|
-
*
|
|
2449
|
-
*
|
|
2450
|
-
*
|
|
2451
|
-
*
|
|
2452
|
-
*
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
cyan: "36"
|
|
2463
|
-
};
|
|
2464
|
-
function makeColor(enabled) {
|
|
2465
|
-
return (name, s) => enabled ? `\x1b[${CODES[name]}m${s}\x1b[0m` : s;
|
|
2466
|
-
}
|
|
2467
|
-
/** Path relative to cwd when it lies inside it, otherwise unchanged. */
|
|
2468
|
-
function relPath(path, cwd) {
|
|
2469
|
-
if (!path || !cwd) return path;
|
|
2470
|
-
let r;
|
|
2471
|
-
try {
|
|
2472
|
-
r = relative(cwd, path);
|
|
2473
|
-
} catch {
|
|
2474
|
-
return path;
|
|
2475
|
-
}
|
|
2476
|
-
return r.startsWith("..") ? path : r;
|
|
2477
|
-
}
|
|
2478
|
-
/** Minimal line diff for Edit previews: common prefix/suffix, one hunk. */
|
|
2479
|
-
function unifiedDiffLines(oldStr, newStr) {
|
|
2480
|
-
const oldL = oldStr.split("\n");
|
|
2481
|
-
const newL = newStr.split("\n");
|
|
2482
|
-
let start = 0;
|
|
2483
|
-
while (start < oldL.length && start < newL.length && oldL[start] === newL[start]) start++;
|
|
2484
|
-
let endOld = oldL.length;
|
|
2485
|
-
let endNew = newL.length;
|
|
2486
|
-
while (endOld > start && endNew > start && oldL[endOld - 1] === newL[endNew - 1]) {
|
|
2487
|
-
endOld--;
|
|
2488
|
-
endNew--;
|
|
2489
|
-
}
|
|
2490
|
-
const out = [];
|
|
2491
|
-
for (let i = start; i < endOld; i++) out.push("-" + oldL[i]);
|
|
2492
|
-
for (let i = start; i < endNew; i++) out.push("+" + newL[i]);
|
|
2493
|
-
return out;
|
|
2494
|
-
}
|
|
2495
|
-
/**
|
|
2496
|
-
* The transcript gutter (2g): `HH:MM:SS │ ` from the event's `_ts`, dim, with
|
|
2497
|
-
* the worker tag in front when several run at once. Continuation lines get
|
|
2498
|
-
* blanks of the same width so wrapped text stays aligned. null when the event
|
|
2499
|
-
* carries no stamp (logs from before 2g render with the plain prefix).
|
|
3172
|
+
* The chain runs each stage as its own worker (own id, own pane, `parent` set
|
|
3173
|
+
* to the chain id), so `ps` shows the chain as a tree and every stage can be
|
|
3174
|
+
* followed, replayed and said to like any other worker:
|
|
3175
|
+
*
|
|
3176
|
+
* - draft turns the operator's brief into a full spec file under
|
|
3177
|
+
* <logDir>/specs/<chain id>.md (goal, constraints, files likely
|
|
3178
|
+
* touched, acceptance checks, verification commands);
|
|
3179
|
+
* - any other stage (implement, plan, …) runs with that spec as its prompt
|
|
3180
|
+
* and the original brief attached;
|
|
3181
|
+
* - review reads the spec and the working-tree diff and produces the
|
|
3182
|
+
* structured report.
|
|
3183
|
+
*
|
|
3184
|
+
* A stage that fails (or a draft that produces no spec file) stops the chain;
|
|
3185
|
+
* the caller then writes the spec itself and re-runs without the draft stage.
|
|
2500
3186
|
*/
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
const head = tag ? `${tag} ${time}` : time;
|
|
2505
|
-
const width = head.length + 3;
|
|
2506
|
-
return {
|
|
2507
|
-
first: c("dim", `${head} │ `),
|
|
2508
|
-
cont: " ".repeat(width)
|
|
2509
|
-
};
|
|
2510
|
-
}
|
|
2511
|
-
/** Compact token count: 84k, 200k, 900. */
|
|
2512
|
-
function fmtK(n) {
|
|
2513
|
-
return n >= 1e3 ? `${Math.round(n / 1e3)}k` : String(n);
|
|
3187
|
+
/** Where a chain's spec file lives: <logDir>/specs/<chain id>.md. */
|
|
3188
|
+
function specPathFor(logDir, chainId) {
|
|
3189
|
+
return join(logDir, "specs", `${chainId}.md`);
|
|
2514
3190
|
}
|
|
2515
3191
|
/**
|
|
2516
|
-
*
|
|
2517
|
-
*
|
|
2518
|
-
*
|
|
3192
|
+
* Replace the caller's -p value with `prompt`, dropping every existing
|
|
3193
|
+
* -p/--print pair first (two -p flags on one claude command line are an error,
|
|
3194
|
+
* so the chain must never leave the brief in place when swapping prompts).
|
|
2519
3195
|
*/
|
|
2520
|
-
function
|
|
2521
|
-
const pct = contextPercent(s);
|
|
2522
|
-
if (pct === null || pct <= minPct) return null;
|
|
2523
|
-
const label = `ctx ${fmtK(s.contextTokens ?? 0)}/${fmtK(s.contextWindow ?? 0)} (${pct}%)`;
|
|
2524
|
-
if (pct > 85) return c("red", label);
|
|
2525
|
-
if (pct > 70) return c("yellow", label);
|
|
2526
|
-
return label;
|
|
2527
|
-
}
|
|
2528
|
-
function renderToolUse(c, prefix, name, inp, cwd) {
|
|
2529
|
-
const i = typeof inp === "object" && inp !== null ? inp : {};
|
|
2530
|
-
const get = (k) => typeof i[k] === "string" ? i[k] : "";
|
|
2531
|
-
if (name === "Read") return [`${prefix}${c("dim", ">")} Reading ${relPath(get("file_path"), cwd)}`];
|
|
2532
|
-
if (name === "Grep" || name === "Glob") return [`${prefix}${c("dim", ">")} Searching ${c("cyan", get("pattern"))} in ${relPath(get("path") || ".", cwd)}`];
|
|
2533
|
-
if (name === "Bash") return [`${prefix}${c("dim", "$")} ${shortText(get("command"), 160)}`];
|
|
2534
|
-
if (name === "Edit") {
|
|
2535
|
-
const out = [`${prefix}${c("mag", "~")} Editing ${relPath(get("file_path"), cwd)}`];
|
|
2536
|
-
for (const line of unifiedDiffLines(get("old_string"), get("new_string"))) if (line.startsWith("-")) out.push(`${prefix} ${c("red", line)}`);
|
|
2537
|
-
else if (line.startsWith("+")) out.push(`${prefix} ${c("green", line)}`);
|
|
2538
|
-
else out.push(`${prefix} ${c("dim", line)}`);
|
|
2539
|
-
return out;
|
|
2540
|
-
}
|
|
2541
|
-
if (name === "Write") {
|
|
2542
|
-
const n = get("content").split("\n").length;
|
|
2543
|
-
return [`${prefix}${c("green", "+")} Writing ${relPath(get("file_path"), cwd)} (${n} lines)`];
|
|
2544
|
-
}
|
|
2545
|
-
if (name === "WebSearch" || name === "WebFetch") return [`${prefix}${c("dim", ">")} ${name} ${shortText(get("query") || get("url"), 100)}`];
|
|
2546
|
-
return [`${prefix}${c("dim", ">")} ${name} ${shortText(JSON.stringify(i), 120)}`];
|
|
2547
|
-
}
|
|
2548
|
-
/** Render one stream-json event; `tools` maps tool_use_id → tool name. */
|
|
2549
|
-
function renderEvent(c, prefix, e, cwd, tools, gutter) {
|
|
3196
|
+
function swapPromptArg(claudeArgs, prompt) {
|
|
2550
3197
|
const out = [];
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
else if (b.type === "tool_use") out.push(...renderToolUse(c, prefix, b.name ?? "?", b.input, cwd));
|
|
2556
|
-
} else if (e.type === "user") for (const b of e.message?.content ?? []) {
|
|
2557
|
-
if (b.type !== "tool_result") continue;
|
|
2558
|
-
let content = b.content ?? "";
|
|
2559
|
-
if (Array.isArray(content)) content = content.map((x) => typeof x === "object" && x !== null && "text" in x ? String(x.text ?? "") : "").join("\n");
|
|
2560
|
-
const text = String(content);
|
|
2561
|
-
const isError = b.is_error === true;
|
|
2562
|
-
if (!isError && tools[b.tool_use_id ?? ""] === "Read") {
|
|
2563
|
-
const lines = text.split("\n");
|
|
2564
|
-
for (const ln of lines.slice(0, 3)) out.push(`${prefix}${c("dim", ln)}`);
|
|
2565
|
-
if (lines.length > 3) out.push(`${prefix}${c("dim", ` … ${lines.length} lines`)}`);
|
|
2566
|
-
} else if (isError) out.push(`${prefix} ${c("red", "! " + shortText(text, 200))}`);
|
|
2567
|
-
else if (text.trim()) out.push(`${prefix} ${c("dim", shortText(text, 120))}`);
|
|
2568
|
-
}
|
|
2569
|
-
else if (e.type === "result") {
|
|
2570
|
-
const mark = !e.is_error ? c("green", "✓ done") : c("red", "✗ failed");
|
|
2571
|
-
out.push(`${prefix}${mark} · ${e.num_turns ?? "?"} turns · ${Math.floor((e.duration_ms ?? 0) / 1e3)}s`);
|
|
2572
|
-
const report = parseWorkerReport(String(e.result ?? ""));
|
|
2573
|
-
if (report) out.push(...renderReport(c, prefix, report, cwd));
|
|
2574
|
-
else for (const ln of String(e.result ?? "").trim().split("\n")) out.push(`${prefix} ${ln}`);
|
|
2575
|
-
}
|
|
2576
|
-
if (!gutter) return out;
|
|
2577
|
-
return out.map((ln, i) => (i === 0 ? gutter.first : gutter.cont) + ln);
|
|
2578
|
-
}
|
|
2579
|
-
/** Transcript header: id, provider, label, session name, project dir. */
|
|
2580
|
-
function headerLine(c, s) {
|
|
2581
|
-
const bits = [s.provider ? `[${s.provider}]` : "", sessionTag(s)].filter(Boolean).join(" ");
|
|
2582
|
-
const sep = bits ? ` ${bits}` : "";
|
|
2583
|
-
return c("bold", `━━ ${s.id}${sep} ${s.label} (${basename(s.cwd)})`);
|
|
2584
|
-
}
|
|
2585
|
-
/** The ps table (RUNNING + FINISHED last 8). */
|
|
2586
|
-
function renderTable(c, statuses, scopeLabel, now = /* @__PURE__ */ new Date()) {
|
|
2587
|
-
const running = [];
|
|
2588
|
-
const done = [];
|
|
2589
|
-
for (const s of statuses) if (s.state === "running" && alive(s.pid)) running.push(s);
|
|
2590
|
-
else {
|
|
2591
|
-
if (s.state === "running") s.state = "lost";
|
|
2592
|
-
done.push(s);
|
|
2593
|
-
}
|
|
2594
|
-
const lines = [c("bold", `Workers ${`${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}:${String(now.getSeconds()).padStart(2, "0")}`}`), ""];
|
|
2595
|
-
lines.push(c("bold", `RUNNING (${running.length})`));
|
|
2596
|
-
if (!running.length) lines.push(" none");
|
|
2597
|
-
for (const s of running) {
|
|
2598
|
-
const meter = contextMeter(c, s, 60);
|
|
2599
|
-
lines.push(` ${c("cyan", s.id)} [${s.provider}] ${ageOf(s.started, now).padStart(4)} old turns ${String(s.turns).padStart(2)} tools ${String(s.tools).padStart(2)} ${basename(s.cwd)}${meter ? " " + meter : ""}`);
|
|
2600
|
-
lines.push(` task: ${s.label}`);
|
|
2601
|
-
lines.push(` now: ${c("yellow", s.last)} (${ageOf(s.updated, now)} ago)`);
|
|
2602
|
-
}
|
|
2603
|
-
lines.push("");
|
|
2604
|
-
lines.push(c("bold", "FINISHED (last 8)"));
|
|
2605
|
-
for (const s of done.slice(-8)) {
|
|
2606
|
-
const col = s.state === "done" ? "green" : "red";
|
|
2607
|
-
const tag = sessionTag(s);
|
|
2608
|
-
lines.push(` ${s.id} [${s.provider}]${tag ? " " + tag : ""} ${c(col, s.state.padEnd(6))} rc=${s.rc} ${String(s.secs ?? "?").padStart(4)}s turns ${String(s.turns).padStart(2)} tools ${String(s.tools).padStart(2)} ${basename(s.cwd)} ${s.label}`);
|
|
2609
|
-
}
|
|
2610
|
-
lines.push("");
|
|
2611
|
-
lines.push(c("dim", "worker follow live transcript of running workers"));
|
|
2612
|
-
lines.push(c("dim", "worker <id> replay one worker"));
|
|
2613
|
-
lines.push(c("dim", scopeLabel));
|
|
2614
|
-
return lines.join("\n");
|
|
2615
|
-
}
|
|
2616
|
-
/** One-line status-bar summary (same shape glm-ps --status printed). */
|
|
2617
|
-
function renderStatusLine(mine, now = /* @__PURE__ */ new Date(), c = makeColor(true)) {
|
|
2618
|
-
if (!mine.length) return "";
|
|
2619
|
-
const running = mine.filter((s) => s.state === "running" && alive(s.pid));
|
|
2620
|
-
const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
|
2621
|
-
const doneToday = mine.filter((s) => s.state !== "running" && s.started.startsWith(today));
|
|
2622
|
-
const ok = doneToday.filter((s) => s.state === "done").length;
|
|
2623
|
-
const bad = doneToday.length - ok;
|
|
2624
|
-
const providers = new Set(running.map((s) => s.provider));
|
|
2625
|
-
let head = `${providers.size === 1 ? [...providers][0] : "workers"} ▶${running.length}`;
|
|
2626
|
-
const parts = running.slice(0, 3).map((s) => {
|
|
2627
|
-
const meter = contextMeter(c, s, 60);
|
|
2628
|
-
return `${s.id.slice(-4)} ${s.label.slice(0, 26)} ${ageOf(s.started, now)} · ${s.last.slice(0, 30)}` + (meter ? ` ${meter}` : "");
|
|
2629
|
-
});
|
|
2630
|
-
if (parts.length) head += " " + parts.join(" | ");
|
|
2631
|
-
if (doneToday.length) head += ` ✓${ok} ✗${bad} today`;
|
|
2632
|
-
return head;
|
|
2633
|
-
}
|
|
2634
|
-
|
|
2635
|
-
//#endregion
|
|
2636
|
-
//#region src/workers/viewer.ts
|
|
2637
|
-
/**
|
|
2638
|
-
* viewer.ts — ps / follow / replay / status line over the worker logDir.
|
|
2639
|
-
*
|
|
2640
|
-
* Scoping: workers launched from this terminal's AIBroker session (or, as the
|
|
2641
|
-
* fallback, its iTerm tab) unless --all or an explicit worker id is given.
|
|
2642
|
-
* Outside iTerm, everything degrades to "all workers" — the Python behaviour.
|
|
2643
|
-
*
|
|
2644
|
-
* Transcripts render with a `HH:MM:SS │ ` gutter (2g): dim, taken from the
|
|
2645
|
-
* `_ts` stamp on every mirrored event, the worker tag in front when several
|
|
2646
|
-
* run at once, a date separator when the day changes, and — on a TTY — a
|
|
2647
|
-
* liveness line (`⋯ 12s since last event · Bash: npm test · ctx 84k/200k (42%)`)
|
|
2648
|
-
* that is rewritten in place between events. Following one worker also wires
|
|
2649
|
-
* this pane's stdin: every typed line is said to the worker while it runs and
|
|
2650
|
-
* resumes it (same Claude session) once it has finished.
|
|
2651
|
-
*/
|
|
2652
|
-
function psOutput(logDir, showAll, env = process.env, color = process.stdout.isTTY === true) {
|
|
2653
|
-
const c = makeColor(color);
|
|
2654
|
-
const term = env.ITERM_SESSION_ID ?? "";
|
|
2655
|
-
const statuses = loadStatuses(logDir);
|
|
2656
|
-
return renderTable(c, showAll || !term ? statuses : statuses.filter((s) => workerInScope(s, term)), showAll || !term ? "scope: all workers" : resolveSession(term) ? `scope: session ${resolveSession(term).name}` : `scope: tab ${currentTabKey(env)} (this iTerm tab)`);
|
|
2657
|
-
}
|
|
2658
|
-
/** The rendered transcript of one worker, from the start, as one string. */
|
|
2659
|
-
function replayOutput(logDir, wid, color = process.stdout.isTTY === true, tailLines) {
|
|
2660
|
-
const path = eventsPath(logDir, wid);
|
|
2661
|
-
if (!existsSync(path)) throw new Error(`no event log for ${wid}`);
|
|
2662
|
-
const c = makeColor(color);
|
|
2663
|
-
const st = loadStatuses(logDir).find((s) => s.id === wid) ?? {
|
|
2664
|
-
id: wid,
|
|
2665
|
-
label: "",
|
|
2666
|
-
cwd: "",
|
|
2667
|
-
provider: ""
|
|
2668
|
-
};
|
|
2669
|
-
const tools = {};
|
|
2670
|
-
const out = [headerLine(c, st)];
|
|
2671
|
-
const lines = readFileSync(path, "utf8").split("\n");
|
|
2672
|
-
let lastDay = "";
|
|
2673
|
-
for (const line of tailLines !== void 0 ? lines.slice(-tailLines) : lines) {
|
|
2674
|
-
if (!line.trim()) continue;
|
|
2675
|
-
let e;
|
|
2676
|
-
try {
|
|
2677
|
-
e = JSON.parse(line);
|
|
2678
|
-
} catch {
|
|
3198
|
+
for (let i = 0; i < claudeArgs.length; i++) {
|
|
3199
|
+
const a = claudeArgs[i];
|
|
3200
|
+
if (a === "-p" || a === "--print") {
|
|
3201
|
+
if (i + 1 < claudeArgs.length && !claudeArgs[i + 1].startsWith("-")) i += 1;
|
|
2679
3202
|
continue;
|
|
2680
3203
|
}
|
|
2681
|
-
|
|
2682
|
-
lastDay = e._ts.slice(0, 10);
|
|
2683
|
-
out.push(c("dim", `── ${lastDay} ──`));
|
|
2684
|
-
}
|
|
2685
|
-
const g = gutterFor(c, e);
|
|
2686
|
-
collectTools(e, tools);
|
|
2687
|
-
out.push(...renderEvent(c, g ? "" : " ", e, st.cwd ?? "", tools, g));
|
|
3204
|
+
out.push(a);
|
|
2688
3205
|
}
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
function collectTools(e, tools) {
|
|
2692
|
-
if (e.type !== "assistant") return;
|
|
2693
|
-
for (const b of e.message?.content ?? []) if (b.type === "tool_use" && b.id) tools[b.id] = b.name ?? "?";
|
|
2694
|
-
}
|
|
2695
|
-
/**
|
|
2696
|
-
* The follow exit decision for one worker: it is over once its result event
|
|
2697
|
-
* was rendered, or once its status left "running" while its pid is gone — a
|
|
2698
|
-
* worker killed without writing a result still ends. A status never read
|
|
2699
|
-
* (undefined) means "not over": follow keeps waiting for the first event.
|
|
2700
|
-
*/
|
|
2701
|
-
function workerEnded(resultRendered, state, pidAlive) {
|
|
2702
|
-
return resultRendered || state !== void 0 && state !== "running" && !pidAlive;
|
|
3206
|
+
out.push("-p", prompt);
|
|
3207
|
+
return out;
|
|
2703
3208
|
}
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
else noteLine(c("red", `» ${e.message}`));
|
|
3209
|
+
function draftPrompt(brief, specPath) {
|
|
3210
|
+
return [
|
|
3211
|
+
"You are the DRAFT stage of a worker chain. Turn the operator's brief below into a full implementation spec and write it to",
|
|
3212
|
+
specPath,
|
|
3213
|
+
"with the Write tool.",
|
|
3214
|
+
"",
|
|
3215
|
+
"Use exactly these five sections as Markdown headings:",
|
|
3216
|
+
"# Goal",
|
|
3217
|
+
"# Constraints",
|
|
3218
|
+
"# Files likely touched",
|
|
3219
|
+
"# Acceptance checks",
|
|
3220
|
+
"# Verification commands",
|
|
3221
|
+
"",
|
|
3222
|
+
"Read the repository first (Glob/Grep/Read) so the spec names real files and real commands. Do not implement anything.",
|
|
3223
|
+
"",
|
|
3224
|
+
"## Operator brief",
|
|
3225
|
+
"",
|
|
3226
|
+
brief
|
|
3227
|
+
].join("\n");
|
|
3228
|
+
}
|
|
3229
|
+
function implementPrompt(brief, specPath, spec) {
|
|
3230
|
+
return [
|
|
3231
|
+
...spec ? [
|
|
3232
|
+
"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.",
|
|
3233
|
+
"",
|
|
3234
|
+
"## Spec",
|
|
3235
|
+
"",
|
|
3236
|
+
spec
|
|
3237
|
+
] : ["Implement the operator's brief below."],
|
|
3238
|
+
"",
|
|
3239
|
+
"## Operator brief",
|
|
3240
|
+
"",
|
|
3241
|
+
brief
|
|
3242
|
+
].join("\n");
|
|
3243
|
+
}
|
|
3244
|
+
function reviewPrompt(brief, specPath, spec) {
|
|
3245
|
+
return [
|
|
3246
|
+
"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.",
|
|
3247
|
+
...spec ? [
|
|
3248
|
+
"",
|
|
3249
|
+
"## Spec (also at " + specPath + ")",
|
|
3250
|
+
"",
|
|
3251
|
+
spec
|
|
3252
|
+
] : [],
|
|
3253
|
+
"",
|
|
3254
|
+
"## Operator brief",
|
|
3255
|
+
"",
|
|
3256
|
+
brief
|
|
3257
|
+
].join("\n");
|
|
3258
|
+
}
|
|
3259
|
+
/** Run a chain of stages; returns the exit code of the first failed stage, 0 when all pass. */
|
|
3260
|
+
async function runChain(opts, deps = {}) {
|
|
3261
|
+
const stages = opts.stages.map((s) => s.trim()).filter(Boolean);
|
|
3262
|
+
if (!stages.length) throw new Error("--chain needs at least one class, e.g. --chain draft,implement");
|
|
3263
|
+
const runStage = deps.runStage ?? runWorker;
|
|
3264
|
+
const logDir = deps.logDir ?? workersLogDir(readWorkersSection().workers);
|
|
3265
|
+
const chainId = newWorkerId();
|
|
3266
|
+
const specPath = specPathFor(logDir, chainId);
|
|
3267
|
+
mkdirSync(join(logDir, "specs"), { recursive: true });
|
|
3268
|
+
const baseLabel = opts.label ?? shortText(opts.brief, 40);
|
|
3269
|
+
appendLedger(ledgerPath(logDir), "WORKER-CHAIN", {
|
|
3270
|
+
chain: chainId,
|
|
3271
|
+
stages: stages.join(","),
|
|
3272
|
+
label: baseLabel
|
|
3273
|
+
});
|
|
3274
|
+
opts.onChainStart?.(chainId);
|
|
3275
|
+
let spec = null;
|
|
3276
|
+
for (let i = 0; i < stages.length; i++) {
|
|
3277
|
+
const stage = stages[i];
|
|
3278
|
+
if (stage === "draft") spec = null;
|
|
3279
|
+
else if (spec === null && existsSync(specPath)) spec = readFileSync(specPath, "utf8");
|
|
3280
|
+
const prompt = stage === "draft" ? draftPrompt(opts.brief, specPath) : stage === "review" ? reviewPrompt(opts.brief, spec ? specPath : null, spec) : implementPrompt(opts.brief, spec ? specPath : null, spec);
|
|
3281
|
+
process.stderr.write(`chain ${chainId}: stage ${i + 1}/${stages.length} ${stage} (spec: ${specPath})\n`);
|
|
3282
|
+
const rc = await runStage({
|
|
3283
|
+
className: opts.className ?? stage,
|
|
3284
|
+
providerFlag: opts.providerFlag,
|
|
3285
|
+
modelFlag: opts.modelFlag,
|
|
3286
|
+
label: `${baseLabel} · ${stage}`,
|
|
3287
|
+
noPane: opts.noPane,
|
|
3288
|
+
mcpFlag: opts.mcpFlag,
|
|
3289
|
+
claudeArgs: swapPromptArg(opts.claudeArgs, prompt),
|
|
3290
|
+
cwd: opts.cwd,
|
|
3291
|
+
parent: chainId,
|
|
3292
|
+
stage,
|
|
3293
|
+
quiet: opts.quiet
|
|
2790
3294
|
});
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
for (;;) {
|
|
2799
|
-
if (aborted) return;
|
|
2800
|
-
for (const wid of target ? [target] : runningIds()) {
|
|
2801
|
-
if (handles.has(wid) || finished.has(wid)) continue;
|
|
2802
|
-
const path = eventsPath(logDir, wid);
|
|
2803
|
-
if (existsSync(path)) handles.set(wid, {
|
|
2804
|
-
fd: openSync(path, "r"),
|
|
2805
|
-
buf: ""
|
|
3295
|
+
if (stage === "draft") {
|
|
3296
|
+
if (!existsSync(specPath)) {
|
|
3297
|
+
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`);
|
|
3298
|
+
appendLedger(ledgerPath(logDir), "WORKER-CHAIN-END", {
|
|
3299
|
+
chain: chainId,
|
|
3300
|
+
rc: rc !== 0 ? rc : 1,
|
|
3301
|
+
failed: "draft"
|
|
2806
3302
|
});
|
|
3303
|
+
return rc !== 0 ? rc : 1;
|
|
2807
3304
|
}
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
};
|
|
2818
|
-
const prefix = multi ? c("cyan", wid.slice(-4)) + c("dim", " ┃ ") : " ";
|
|
2819
|
-
const buffer = Buffer.alloc(65536);
|
|
2820
|
-
for (;;) {
|
|
2821
|
-
let n;
|
|
2822
|
-
try {
|
|
2823
|
-
n = readSync(h.fd, buffer, 0, buffer.length, null);
|
|
2824
|
-
} catch {
|
|
2825
|
-
n = 0;
|
|
2826
|
-
}
|
|
2827
|
-
if (n <= 0) break;
|
|
2828
|
-
h.buf += buffer.toString("utf8", 0, n);
|
|
2829
|
-
}
|
|
2830
|
-
const lines = h.buf.split("\n");
|
|
2831
|
-
h.buf = lines.pop() ?? "";
|
|
2832
|
-
for (const line of lines) {
|
|
2833
|
-
progressed = true;
|
|
2834
|
-
if (!seenHeader.has(wid)) {
|
|
2835
|
-
eraseLiveness();
|
|
2836
|
-
process.stdout.write(headerLine(c, st) + "\n");
|
|
2837
|
-
seenHeader.add(wid);
|
|
2838
|
-
}
|
|
2839
|
-
if (!line.trim()) continue;
|
|
2840
|
-
let e;
|
|
2841
|
-
try {
|
|
2842
|
-
e = JSON.parse(line);
|
|
2843
|
-
} catch {
|
|
2844
|
-
continue;
|
|
2845
|
-
}
|
|
2846
|
-
const day = typeof e._ts === "string" ? e._ts.slice(0, 10) : "";
|
|
2847
|
-
if (day && day !== lastDayBy.get(wid)) {
|
|
2848
|
-
eraseLiveness();
|
|
2849
|
-
process.stdout.write(`${multi ? prefix : ""}${c("dim", `── ${day} ──`)}\n`);
|
|
2850
|
-
lastDayBy.set(wid, day);
|
|
2851
|
-
}
|
|
2852
|
-
collectTools(e, tools);
|
|
2853
|
-
const g = gutterFor(c, e, multi ? c("cyan", wid.slice(-4)) : void 0);
|
|
2854
|
-
eraseLiveness();
|
|
2855
|
-
for (const ln of renderEvent(c, g ? "" : prefix, e, st.cwd ?? "", tools, g)) process.stdout.write(ln + "\n");
|
|
2856
|
-
lastEventAt = Date.now();
|
|
2857
|
-
if (e.type === "operator") lastAction = "operator message";
|
|
2858
|
-
else if (e.type === "assistant") {
|
|
2859
|
-
const tool = (e.message?.content ?? []).find((b) => b.type === "tool_use");
|
|
2860
|
-
lastAction = tool ? `${tool.name ?? "?"} running` : "thinking";
|
|
2861
|
-
} else if (e.type === "result") lastAction = "finished";
|
|
2862
|
-
meterStatus = st;
|
|
2863
|
-
if (e.type === "result") finished.add(wid);
|
|
2864
|
-
}
|
|
2865
|
-
if (workerEnded(finished.has(wid), st.state, alive(st.pid))) {
|
|
2866
|
-
if (!finished.has(wid)) {
|
|
2867
|
-
eraseLiveness();
|
|
2868
|
-
process.stdout.write(`${prefix}${c("red", "✗ " + (st.state || "ended"))} · ${st.last ?? ""}\n`);
|
|
2869
|
-
finished.add(wid);
|
|
2870
|
-
}
|
|
2871
|
-
closeSync(h.fd);
|
|
2872
|
-
handles.delete(wid);
|
|
2873
|
-
}
|
|
2874
|
-
}
|
|
2875
|
-
const lingerOn = target;
|
|
2876
|
-
if (lingerOn !== null && finished.has(lingerOn)) {
|
|
2877
|
-
if (autoExit) {
|
|
2878
|
-
const until = Date.now() + autoExit * 1e3;
|
|
2879
|
-
while (Date.now() < until && !aborted && target === lingerOn) {
|
|
2880
|
-
writeLiveness();
|
|
2881
|
-
await sleep(250);
|
|
2882
|
-
}
|
|
2883
|
-
if (aborted || target !== lingerOn) continue;
|
|
2884
|
-
eraseLiveness();
|
|
2885
|
-
process.stdout.write(c("dim", "closing") + "\n");
|
|
2886
|
-
return;
|
|
2887
|
-
}
|
|
2888
|
-
if (rlIn === null) return;
|
|
2889
|
-
}
|
|
2890
|
-
if (autoExit && !target) {
|
|
2891
|
-
if (runningIds().length) idleSince = null;
|
|
2892
|
-
else if (idleSince === null) idleSince = Date.now();
|
|
2893
|
-
else if (Date.now() - started >= 3e4 && Date.now() - idleSince >= autoExit * 1e3) {
|
|
2894
|
-
eraseLiveness();
|
|
2895
|
-
process.stdout.write(c("dim", "closing") + "\n");
|
|
2896
|
-
return;
|
|
2897
|
-
}
|
|
2898
|
-
}
|
|
2899
|
-
if (!progressed) {
|
|
2900
|
-
writeLiveness();
|
|
2901
|
-
await sleep(500);
|
|
2902
|
-
}
|
|
3305
|
+
spec = readFileSync(specPath, "utf8");
|
|
3306
|
+
}
|
|
3307
|
+
if (rc !== 0) {
|
|
3308
|
+
appendLedger(ledgerPath(logDir), "WORKER-CHAIN-END", {
|
|
3309
|
+
chain: chainId,
|
|
3310
|
+
rc,
|
|
3311
|
+
failed: stage
|
|
3312
|
+
});
|
|
3313
|
+
return rc;
|
|
2903
3314
|
}
|
|
2904
|
-
} finally {
|
|
2905
|
-
rlIn?.close();
|
|
2906
|
-
process.removeListener("SIGINT", onInt);
|
|
2907
|
-
for (const h of handles.values()) try {
|
|
2908
|
-
closeSync(h.fd);
|
|
2909
|
-
} catch {}
|
|
2910
|
-
}
|
|
2911
|
-
}
|
|
2912
|
-
function sleep(ms) {
|
|
2913
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
2914
|
-
}
|
|
2915
|
-
/** Workers of this terminal for the status bar; "" when there are none. */
|
|
2916
|
-
function statusLineOutput(logDir, term, cwd, now = /* @__PURE__ */ new Date()) {
|
|
2917
|
-
return renderStatusLine(loadStatuses(logDir).filter((s) => {
|
|
2918
|
-
const sameScope = term && workerInScope(s, term);
|
|
2919
|
-
const sameDir = cwd && s.cwd.startsWith(cwd);
|
|
2920
|
-
return sameScope || !term && sameDir;
|
|
2921
|
-
}), now);
|
|
2922
|
-
}
|
|
2923
|
-
|
|
2924
|
-
//#endregion
|
|
2925
|
-
//#region src/workers/providers.ts
|
|
2926
|
-
/**
|
|
2927
|
-
* providers.ts — provider, role and switch management over the workers config.
|
|
2928
|
-
*
|
|
2929
|
-
* One layer under both `pai worker providers …` and the MCP worker_providers
|
|
2930
|
-
* tool. Every mutation re-reads the config file, changes only the workers
|
|
2931
|
-
* section, and writes it back atomically — the file is shared with everything
|
|
2932
|
-
* else PAI runs, so a torn write is not an option.
|
|
2933
|
-
*
|
|
2934
|
-
* Keys are never stored in the config and never accepted inline from the CLI:
|
|
2935
|
-
* only file paths. (The MCP `add` tool additionally accepts a raw `key`, which
|
|
2936
|
-
* it immediately parks in ~/.config/pai/keys/<name>, mode 0600, storing only
|
|
2937
|
-
* the path — a chat is not a place to leave a credential lying around.)
|
|
2938
|
-
*/
|
|
2939
|
-
function addProvider(input) {
|
|
2940
|
-
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(input.name)) throw new WorkersConfigError(`provider name "${input.name}" may only contain letters, digits, - and _`);
|
|
2941
|
-
if (input.protocol === "openai" && !input.upstreamUrl) throw new WorkersConfigError("protocol \"openai\" needs the upstreamUrl (the Chat Completions base, e.g. \"https://api.openai.com/v1\") — the PAI proxy translates to it.");
|
|
2942
|
-
if (!input.baseUrl && input.protocol !== "openai") throw new WorkersConfigError("baseUrl is required");
|
|
2943
|
-
let keyFile = input.keyFile ?? null;
|
|
2944
|
-
if (input.key !== void 0) {
|
|
2945
|
-
const dir = keysDir();
|
|
2946
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
2947
|
-
const path = `${dir}/${input.name}`;
|
|
2948
|
-
writeFileSync(path, input.key.trim() + "\n", {
|
|
2949
|
-
encoding: "utf8",
|
|
2950
|
-
mode: 384
|
|
2951
|
-
});
|
|
2952
|
-
try {
|
|
2953
|
-
chmodSync(path, 384);
|
|
2954
|
-
} catch {}
|
|
2955
|
-
keyFile = path;
|
|
2956
|
-
}
|
|
2957
|
-
const { raw, workers } = readWorkersSection();
|
|
2958
|
-
const provider = {
|
|
2959
|
-
enabled: true,
|
|
2960
|
-
protocol: input.protocol ?? "anthropic",
|
|
2961
|
-
baseUrl: input.baseUrl,
|
|
2962
|
-
keyFile,
|
|
2963
|
-
models: input.fastModel ? {
|
|
2964
|
-
default: input.model,
|
|
2965
|
-
fast: input.fastModel
|
|
2966
|
-
} : { default: input.model },
|
|
2967
|
-
env: input.env ?? {},
|
|
2968
|
-
...input.note ? { note: input.note } : {},
|
|
2969
|
-
...input.upstreamUrl ? { upstreamUrl: input.upstreamUrl } : {},
|
|
2970
|
-
...input.engine && input.engine !== "claude" ? { engine: input.engine } : {},
|
|
2971
|
-
...input.quotaProbe ? { quotaProbe: input.quotaProbe } : {},
|
|
2972
|
-
...input.contextWindow ? { contextWindow: input.contextWindow } : {}
|
|
2973
|
-
};
|
|
2974
|
-
workers.providers[input.name] = provider;
|
|
2975
|
-
if (Object.keys(workers.providers).length === 1) {
|
|
2976
|
-
workers.enabled = true;
|
|
2977
|
-
workers.active = input.name;
|
|
2978
|
-
workers.logDir = workers.logDir || DEFAULT_LOG_DIR;
|
|
2979
|
-
workers.roles = {
|
|
2980
|
-
implement: input.name,
|
|
2981
|
-
research: input.name,
|
|
2982
|
-
spotcheck: input.fastModel ? `${input.name}/fast` : input.name
|
|
2983
|
-
};
|
|
2984
3315
|
}
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
if (!workers.providers[name]) throw new WorkersConfigError(`no provider named "${name}"`);
|
|
2991
|
-
delete workers.providers[name];
|
|
2992
|
-
for (const [role, target] of Object.entries(workers.roles)) if ((typeof target === "string" ? target.split("/")[0] : target.provider) === name) delete workers.roles[role];
|
|
2993
|
-
if (workers.active === name) workers.active = null;
|
|
2994
|
-
writeWorkersSection(raw, workers);
|
|
2995
|
-
return workers;
|
|
2996
|
-
}
|
|
2997
|
-
function useProvider(name) {
|
|
2998
|
-
const { raw, workers } = readWorkersSection();
|
|
2999
|
-
if (!workers.providers[name]) throw new WorkersConfigError(`no provider named "${name}". Configured: ${Object.keys(workers.providers).join(", ") || "(none)"}`);
|
|
3000
|
-
workers.active = name;
|
|
3001
|
-
writeWorkersSection(raw, workers);
|
|
3002
|
-
return workers;
|
|
3003
|
-
}
|
|
3004
|
-
function setProviderEnabled(name, enabled) {
|
|
3005
|
-
const { raw, workers } = readWorkersSection();
|
|
3006
|
-
const p = workers.providers[name];
|
|
3007
|
-
if (!p) throw new WorkersConfigError(`no provider named "${name}"`);
|
|
3008
|
-
p.enabled = enabled;
|
|
3009
|
-
writeWorkersSection(raw, workers);
|
|
3010
|
-
if (enabled) clearCooldown(workersLogDir(parseWorkersConfig(raw.workers)), name);
|
|
3011
|
-
return workers;
|
|
3012
|
-
}
|
|
3013
|
-
function setRole(role, target) {
|
|
3014
|
-
const { raw, workers } = readWorkersSection();
|
|
3015
|
-
const [name, alias] = target.split("/");
|
|
3016
|
-
const p = workers.providers[name];
|
|
3017
|
-
if (!p) throw new WorkersConfigError(`no provider named "${name}" in "${target}". Configured: ${Object.keys(workers.providers).join(", ") || "(none)"}`);
|
|
3018
|
-
if (alias && alias !== "default" && alias !== "fast") throw new WorkersConfigError(`unknown model alias "${alias}" — providers expose "default" and "fast"`);
|
|
3019
|
-
if (alias === "fast" && !p.models.fast) throw new WorkersConfigError(`provider "${name}" has no fast model configured`);
|
|
3020
|
-
workers.roles[role] = target;
|
|
3021
|
-
writeWorkersSection(raw, workers);
|
|
3022
|
-
return workers;
|
|
3023
|
-
}
|
|
3024
|
-
function unsetRole(role) {
|
|
3025
|
-
const { raw, workers } = readWorkersSection();
|
|
3026
|
-
if (!(role in workers.roles)) throw new WorkersConfigError(`no role named "${role}"`);
|
|
3027
|
-
delete workers.roles[role];
|
|
3028
|
-
writeWorkersSection(raw, workers);
|
|
3029
|
-
return workers;
|
|
3030
|
-
}
|
|
3031
|
-
function setWorkersEnabled(enabled) {
|
|
3032
|
-
const { raw, workers } = readWorkersSection();
|
|
3033
|
-
workers.enabled = enabled;
|
|
3034
|
-
writeWorkersSection(raw, workers);
|
|
3035
|
-
return workers;
|
|
3036
|
-
}
|
|
3037
|
-
/** Human-readable provider listing (quota probe included when configured). */
|
|
3038
|
-
function describeProviders(workers) {
|
|
3039
|
-
const lines = [];
|
|
3040
|
-
const names = Object.keys(workers.providers);
|
|
3041
|
-
if (!names.length) {
|
|
3042
|
-
lines.push(`no providers configured. Add one with:`);
|
|
3043
|
-
lines.push(` pai worker providers add <name> --base-url <url> --key-file <path> --model <model>`);
|
|
3044
|
-
return lines;
|
|
3045
|
-
}
|
|
3046
|
-
for (const name of names) {
|
|
3047
|
-
const p = workers.providers[name];
|
|
3048
|
-
const flags = [p.enabled ? "enabled" : "disabled", workers.active === name ? "active" : null].filter(Boolean);
|
|
3049
|
-
const quota = p.quotaProbe ? probeQuota(p) : null;
|
|
3050
|
-
const quotaNote = quota === null ? "" : ` quota ${quota}% (skip at ${quotaSkipThreshold(p)})`;
|
|
3051
|
-
lines.push(`${name} [${flags.join(", ")}] ${p.baseUrl}`);
|
|
3052
|
-
lines.push(` model ${p.models.default}${p.models.fast ? ` (fast: ${p.models.fast})` : ""}${quotaNote}`);
|
|
3053
|
-
if (p.keyFile) lines.push(` key file ${expandHome(p.keyFile)}`);
|
|
3054
|
-
else lines.push(` no key file (token "local")`);
|
|
3055
|
-
if (p.note) lines.push(` ${p.note}`);
|
|
3056
|
-
if (p.protocol === "openai") lines.push(` via PAI proxy ← ${p.upstreamUrl ?? "(upstreamUrl missing)"}`);
|
|
3057
|
-
if (p.engine === "codex") lines.push(` engine codex (runs through the Codex CLI)`);
|
|
3058
|
-
}
|
|
3059
|
-
const setNames = Object.keys(workers.mcpSets);
|
|
3060
|
-
if (setNames.length) lines.push(`mcp sets: ${setNames.map((s) => `${s}=[${workers.mcpSets[s].join(",")}]`).join(" ")}`);
|
|
3061
|
-
if (workers.active === "auto") lines.push(`routing: auto — order [${workers.routing.order.join(", ")}], cooldown ${workers.routing.cooldownMinutes}m`);
|
|
3062
|
-
return lines;
|
|
3316
|
+
appendLedger(ledgerPath(logDir), "WORKER-CHAIN-END", {
|
|
3317
|
+
chain: chainId,
|
|
3318
|
+
rc: 0
|
|
3319
|
+
});
|
|
3320
|
+
return 0;
|
|
3063
3321
|
}
|
|
3064
3322
|
|
|
3065
3323
|
//#endregion
|
|
3066
|
-
export {
|
|
3067
|
-
//# sourceMappingURL=
|
|
3324
|
+
export { contextPercent as A, workersLogDir as B, quotaSkipThreshold as C, workerInScope as D, sessionTag as E, ledgerSummary as F, keysDir as G, PROVIDER_TAGS as H, parseRunnerArgs as I, readWorkersSection as J, parseWorkersConfig as K, shortText as L, loadStatuses as M, newWorkerId as N, ageOf as O, appendLedger as P, eventsPath as R, probeQuota as S, resolveSession as T, WorkersConfigError as U, DEFAULT_LOG_DIR as V, expandHome as W, readJsonStrict as X, writeWorkersSection as Y, writeJsonAtomic as Z, workerDepth as _, testProvider as a, openPaneForWorker as b, stopProxy as c, renderReport as d, discardWorker as f, sayToWorker as g, readInbox as h, runWorker as i, loadStatus as j, alive as k, describeMcp as l, handoffFromInside as m, swapPromptArg as n, DEFAULT_PROXY_PORT as o, mergeWorker as p, providerCostTier as q, printResult as r, ensureProxyRunning as s, runChain as t, parseWorkerReport as u, checkPaneForWorker as v, currentTabKey as w, clearCooldown as x, openFollowPane as y, ledgerPath as z };
|
|
3325
|
+
//# sourceMappingURL=chain-CTtHligI.mjs.map
|