@tekmidian/pai 0.41.0 → 0.42.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/dist/aibroker-client-BP74gGwD.mjs +215 -0
  2. package/dist/aibroker-client-BP74gGwD.mjs.map +1 -0
  3. package/dist/aibroker-client-C6m71p6O.mjs +3 -0
  4. package/dist/{auto-route-BwzmlW3W.mjs → auto-route-_5zzJz5q.mjs} +2 -2
  5. package/dist/{auto-route-BwzmlW3W.mjs.map → auto-route-_5zzJz5q.mjs.map} +1 -1
  6. package/dist/{chain-zMjCDW-R.mjs → chain-DsvSHnO9.mjs} +133 -361
  7. package/dist/chain-DsvSHnO9.mjs.map +1 -0
  8. package/dist/cli/index.mjs +12 -10
  9. package/dist/cli/index.mjs.map +1 -1
  10. package/dist/cli/program.mjs +12 -10
  11. package/dist/{clusters-DRHhfli9.mjs → clusters-j0ZuNYp9.mjs} +1 -1
  12. package/dist/{clusters-DRHhfli9.mjs.map → clusters-j0ZuNYp9.mjs.map} +1 -1
  13. package/dist/{config-D1G9IFpn.mjs → config-B64vFg14.mjs} +3 -50
  14. package/dist/config-B64vFg14.mjs.map +1 -0
  15. package/dist/{config-Dl8lT4Lu.mjs → config-Bi_F6lXf.mjs} +32 -3
  16. package/dist/config-Bi_F6lXf.mjs.map +1 -0
  17. package/dist/{config-DD52BPB-.mjs → config-C_ErGddD.mjs} +1 -1
  18. package/dist/{context-handover-cache-C6_rs7JT.mjs → context-handover-cache--bm6Siim.mjs} +28 -9
  19. package/dist/context-handover-cache--bm6Siim.mjs.map +1 -0
  20. package/dist/daemon/index.mjs +13 -12
  21. package/dist/daemon/index.mjs.map +1 -1
  22. package/dist/daemon-BOAPyL_b.mjs +23 -0
  23. package/dist/{daemon-CpdVUnl_.mjs → daemon-DvXih78C.mjs} +359 -27
  24. package/dist/daemon-DvXih78C.mjs.map +1 -0
  25. package/dist/daemon-mcp/index.mjs +10 -6
  26. package/dist/daemon-mcp/index.mjs.map +1 -1
  27. package/dist/detector-C-7KRi42.mjs +5 -0
  28. package/dist/{detector-smpCm7ku.mjs → detector-C73Nxm94.mjs} +1 -1
  29. package/dist/{detector-smpCm7ku.mjs.map → detector-C73Nxm94.mjs.map} +1 -1
  30. package/dist/{factory-Bc5EyN54.mjs → factory-DEb9NmHJ.mjs} +7 -7
  31. package/dist/{factory-Bc5EyN54.mjs.map → factory-DEb9NmHJ.mjs.map} +1 -1
  32. package/dist/factory-Dbo8yNPi.mjs +3 -0
  33. package/dist/{fallback-CgQ_x4I7.mjs → fallback-8bzmg6No.mjs} +6 -5
  34. package/dist/fallback-8bzmg6No.mjs.map +1 -0
  35. package/dist/hooks/route-agents-to-worker.mjs.map +2 -2
  36. package/dist/hooks/worker-proxy.mjs.map +2 -2
  37. package/dist/hooks/worker-status-line.mjs +13 -5
  38. package/dist/hooks/worker-status-line.mjs.map +3 -3
  39. package/dist/hooks/worker-supervision.mjs +593 -0
  40. package/dist/hooks/worker-supervision.mjs.map +7 -0
  41. package/dist/{indexer-backend-rohVVKrA.mjs → indexer-backend-BPTzKFZU.mjs} +1 -1
  42. package/dist/{indexer-backend-rohVVKrA.mjs.map → indexer-backend-BPTzKFZU.mjs.map} +1 -1
  43. package/dist/{ipc-client-C7AVpsyv.mjs → ipc-client-BmypMNYk.mjs} +2 -2
  44. package/dist/{ipc-client-C7AVpsyv.mjs.map → ipc-client-BmypMNYk.mjs.map} +1 -1
  45. package/dist/{kg-entity-DbOMPdF9.mjs → kg-entity-r8duqhi9.mjs} +1 -1
  46. package/dist/{kg-entity-DbOMPdF9.mjs.map → kg-entity-r8duqhi9.mjs.map} +1 -1
  47. package/dist/{latent-ideas-iNp5s55R.mjs → latent-ideas-CtSca4qG.mjs} +2 -2
  48. package/dist/{latent-ideas-iNp5s55R.mjs.map → latent-ideas-CtSca4qG.mjs.map} +1 -1
  49. package/dist/{link-boost-NNzADHX_.mjs → link-boost-Bucnz3n8.mjs} +1 -1
  50. package/dist/{link-boost-NNzADHX_.mjs.map → link-boost-Bucnz3n8.mjs.map} +1 -1
  51. package/dist/main-resolver-CqppP_Bw.mjs +7 -0
  52. package/dist/{main-resolver-CM1IHbuu.mjs → main-resolver-Dip2VLB5.mjs} +4 -214
  53. package/dist/main-resolver-Dip2VLB5.mjs.map +1 -0
  54. package/dist/{neighborhood-AGL7oXQ2.mjs → neighborhood-CM7Jgb51.mjs} +1 -1
  55. package/dist/{neighborhood-AGL7oXQ2.mjs.map → neighborhood-CM7Jgb51.mjs.map} +1 -1
  56. package/dist/{note-context-BHwYpSFf.mjs → note-context-DCwE2A-M.mjs} +1 -1
  57. package/dist/{note-context-BHwYpSFf.mjs.map → note-context-DCwE2A-M.mjs.map} +1 -1
  58. package/dist/{planner-DsmMRIZR.mjs → planner-ogy-Zy4O.mjs} +6 -5
  59. package/dist/{planner-DsmMRIZR.mjs.map → planner-ogy-Zy4O.mjs.map} +1 -1
  60. package/dist/{postgres-Dwxe290H.mjs → postgres-CtOzfoQU.mjs} +1 -1
  61. package/dist/{postgres-Dwxe290H.mjs.map → postgres-CtOzfoQU.mjs.map} +1 -1
  62. package/dist/{program-Bed-Pd-8.mjs → program-B7N5OIJR.mjs} +36 -33
  63. package/dist/{program-Bed-Pd-8.mjs.map → program-B7N5OIJR.mjs.map} +1 -1
  64. package/dist/query-feedback-DGoVRaWD.mjs +3 -0
  65. package/dist/{query-feedback-DZfjI34w.mjs → query-feedback-Dqz-HC__.mjs} +1 -1
  66. package/dist/{query-feedback-DZfjI34w.mjs.map → query-feedback-Dqz-HC__.mjs.map} +1 -1
  67. package/dist/router-BTIGZR2_.mjs +3 -0
  68. package/dist/{router-DHracTUC.mjs → router-DD6D4j31.mjs} +1 -1
  69. package/dist/{router-DHracTUC.mjs.map → router-DD6D4j31.mjs.map} +1 -1
  70. package/dist/runtime-paths-rni52zHX.mjs +50 -0
  71. package/dist/runtime-paths-rni52zHX.mjs.map +1 -0
  72. package/dist/scope-CfYnjy5R.mjs +360 -0
  73. package/dist/scope-CfYnjy5R.mjs.map +1 -0
  74. package/dist/{server-BOuAOj9b.mjs → server-DvN-IFIV.mjs} +42 -3
  75. package/dist/server-DvN-IFIV.mjs.map +1 -0
  76. package/dist/skills/Worker/SKILL.md +3 -0
  77. package/dist/{sources-ju6AA7lL.mjs → sources-CS43CpuS.mjs} +1 -1
  78. package/dist/{sources-ju6AA7lL.mjs.map → sources-CS43CpuS.mjs.map} +1 -1
  79. package/dist/{sqlite-kFZeWCJ2.mjs → sqlite-Cq3gJcq9.mjs} +1 -1
  80. package/dist/{sqlite-kFZeWCJ2.mjs.map → sqlite-Cq3gJcq9.mjs.map} +1 -1
  81. package/dist/{state-ahFAbD8b.mjs → state-BTa10SOM.mjs} +1 -1
  82. package/dist/{state-ahFAbD8b.mjs.map → state-BTa10SOM.mjs.map} +1 -1
  83. package/dist/{state-DH_mp5Xe.mjs → state-RXepvbnO.mjs} +1 -1
  84. package/dist/{themes-DqhNUUQk.mjs → themes-B7h9YDiy.mjs} +1 -1
  85. package/dist/{themes-DqhNUUQk.mjs.map → themes-B7h9YDiy.mjs.map} +1 -1
  86. package/dist/{tools-COna9vZR.mjs → tools-BW3NLMPk.mjs} +1 -1
  87. package/dist/{tools-CMlHa9lZ.mjs → tools-LlzcV_eK.mjs} +14 -14
  88. package/dist/{tools-CMlHa9lZ.mjs.map → tools-LlzcV_eK.mjs.map} +1 -1
  89. package/dist/{trace-DTP6k4B_.mjs → trace-u4hT-pLD.mjs} +1 -1
  90. package/dist/{trace-DTP6k4B_.mjs.map → trace-u4hT-pLD.mjs.map} +1 -1
  91. package/dist/{vault-indexer-YMH22mLU.mjs → vault-indexer-mata9S7b.mjs} +1 -1
  92. package/dist/{vault-indexer-YMH22mLU.mjs.map → vault-indexer-mata9S7b.mjs.map} +1 -1
  93. package/dist/work-queue-worker-19r0cpH4.mjs +14 -0
  94. package/dist/{work-queue-worker-B3nl8yyn.mjs → work-queue-worker-DXh_FnT5.mjs} +4 -4
  95. package/dist/{work-queue-worker-B3nl8yyn.mjs.map → work-queue-worker-DXh_FnT5.mjs.map} +1 -1
  96. package/dist/{zettelkasten-DDvKRm2V.mjs → zettelkasten-F0D_XfQ4.mjs} +3 -3
  97. package/dist/{zettelkasten-DDvKRm2V.mjs.map → zettelkasten-F0D_XfQ4.mjs.map} +1 -1
  98. package/docs/commands/worker.md +1 -1
  99. package/docs/merge-demo.md +1 -0
  100. package/docs/worker.md +40 -0
  101. package/package.json +1 -1
  102. package/plugins/productivity/hooks/hooks.json +5 -0
  103. package/src/hooks/ts/user-prompt/worker-supervision.ts +123 -0
  104. package/dist/chain-zMjCDW-R.mjs.map +0 -1
  105. package/dist/config-D1G9IFpn.mjs.map +0 -1
  106. package/dist/config-Dl8lT4Lu.mjs.map +0 -1
  107. package/dist/context-handover-cache-C6_rs7JT.mjs.map +0 -1
  108. package/dist/daemon-BSL9sXxm.mjs +0 -22
  109. package/dist/daemon-CpdVUnl_.mjs.map +0 -1
  110. package/dist/detector-BLVOCiJ_.mjs +0 -5
  111. package/dist/factory-C7ua_3ik.mjs +0 -3
  112. package/dist/fallback-CgQ_x4I7.mjs.map +0 -1
  113. package/dist/main-resolver-CM1IHbuu.mjs.map +0 -1
  114. package/dist/main-resolver-E5WlilhM.mjs +0 -6
  115. package/dist/query-feedback-CGcEiGtY.mjs +0 -3
  116. package/dist/router-CAtyVQud.mjs +0 -3
  117. package/dist/server-BOuAOj9b.mjs.map +0 -1
  118. package/dist/work-queue-worker-BM1GcAAq.mjs +0 -14
@@ -1,106 +1,13 @@
1
- import { a as WorkersConfigError, h as resolveModelCapability, l as isModelCapability, m as readWorkersSection, o as assertProviderRunnable, p as providerKeyPath, s as classModelCapability } from "./config-Dl8lT4Lu.mjs";
2
- import { _ as workersLogDir, d as setCooldown, f as eventsPath, g as statusPath, h as panesDir, i as buildRunEnv, m as noMcpConfigPath, n as ensureProxyRunning, o as isQuotaFailure, p as ledgerPath, s as nextAutoProvider, t as DEFAULT_PROXY_PORT, u as resolveTarget } from "./server-BOuAOj9b.mjs";
3
- import { appendFileSync, closeSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, renameSync, rmSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
1
+ import { a as WorkersConfigError, g as resolveModelCapability, h as readWorkersSection, m as providerKeyPath, o as assertProviderRunnable, s as classModelCapability, u as isModelCapability } from "./config-Bi_F6lXf.mjs";
2
+ import { _ as workersLogDir, d as setCooldown, f as ensureNoMcpConfig, g as statusPath, h as panesDir, i as buildRunEnv, m as ledgerPath, n as ensureProxyRunning, o as isQuotaFailure, p as eventsPath, s as nextAutoProvider, t as DEFAULT_PROXY_PORT, u as resolveTarget } from "./server-DvN-IFIV.mjs";
3
+ import { C as stripPromptValues, S as shortText, _ as loadStatuses, a as resolveSpawnerSession, b as saveStatus, d as alive, g as loadStatus, i as resolveSession, l as UNLABELED, m as describeTool, o as scopeKey, r as itermUuid, v as newWorkerId, x as parseRunnerArgs, y as nowStamp } from "./scope-CfYnjy5R.mjs";
4
+ import { appendFileSync, closeSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
4
5
  import { homedir, tmpdir } from "node:os";
5
- import { dirname, join, relative } from "node:path";
6
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
6
7
  import { connect, createServer } from "node:net";
7
8
  import { execFileSync, spawn } from "node:child_process";
8
9
  import { createInterface } from "node:readline";
9
10
 
10
- //#region src/workers/args.ts
11
- function parseRunnerArgs(argv) {
12
- let prompt = null;
13
- let outputFormat = "text";
14
- const rest = [];
15
- let headless = false;
16
- let callerModel = false;
17
- let callerMcpConfig = false;
18
- let callerSystemPrompt = false;
19
- const mcp = [];
20
- const allowedTools = [];
21
- let i = 0;
22
- while (i < argv.length) {
23
- const a = argv[i];
24
- if (a === "-p" || a === "--print") {
25
- headless = true;
26
- rest.push(a);
27
- if (i + 1 < argv.length && !argv[i + 1].startsWith("-")) {
28
- prompt = argv[i + 1];
29
- rest.push(prompt);
30
- i += 1;
31
- }
32
- } else if (a === "--output-format") {
33
- const v = argv[i + 1];
34
- if (v === "json" || v === "stream-json") outputFormat = v;
35
- i += 1;
36
- } else if (a.startsWith("--output-format=")) {
37
- const v = a.slice(16);
38
- if (v === "json" || v === "stream-json") outputFormat = v;
39
- } else if (a === "--verbose") {} else if (a === "--mcp") {
40
- const v = argv[i + 1];
41
- if (v !== void 0 && !v.startsWith("-")) {
42
- mcp.push(v);
43
- i += 1;
44
- }
45
- } else if (a.startsWith("--mcp=")) mcp.push(a.slice(6));
46
- else if (a === "--allowedTools") {
47
- rest.push(a);
48
- const v = argv[i + 1];
49
- if (v !== void 0 && !v.startsWith("-")) {
50
- allowedTools.push(v);
51
- rest.push(v);
52
- i += 1;
53
- }
54
- } else if (a.startsWith("--allowedTools=")) {
55
- allowedTools.push(a.slice(15));
56
- rest.push(a);
57
- } else {
58
- if (a === "--model") callerModel = true;
59
- if (a.startsWith("--model=")) callerModel = true;
60
- if (a === "--mcp-config") callerMcpConfig = true;
61
- if (a.startsWith("--mcp-config=")) callerMcpConfig = true;
62
- if (a === "--append-system-prompt") callerSystemPrompt = true;
63
- if (a.startsWith("--append-system-prompt=")) callerSystemPrompt = true;
64
- if (prompt === null && !a.startsWith("-") && rest.length > 0 && (rest[rest.length - 1] === "-p" || rest[rest.length - 1] === "--print")) prompt = a;
65
- rest.push(a);
66
- }
67
- i += 1;
68
- }
69
- return {
70
- prompt,
71
- outputFormat,
72
- rest,
73
- headless,
74
- callerModel,
75
- callerMcpConfig,
76
- callerSystemPrompt,
77
- mcp,
78
- allowedTools
79
- };
80
- }
81
- /**
82
- * Turn `-p "<prompt>"` into bare `-p` (same for --print): for stream-json
83
- * stdin runs the prompt moves to the first user message on stdin, so the
84
- * value must not stay on the command line. Only prompt values directly
85
- * following the flag are touched; everything else passes through.
86
- */
87
- function stripPromptValues(argv) {
88
- const out = [];
89
- for (let i = 0; i < argv.length; i++) {
90
- const a = argv[i];
91
- const isPrint = a === "-p" || a === "--print";
92
- out.push(a);
93
- if (isPrint && i + 1 < argv.length && !argv[i + 1].startsWith("-")) i += 1;
94
- }
95
- return out;
96
- }
97
- /** Collapse whitespace and cut to n chars with an ellipsis (label rendering). */
98
- function shortText(s, n) {
99
- const t = String(s ?? "").split(/\s+/).filter(Boolean).join(" ");
100
- return t.length <= n ? t : t.slice(0, n - 1) + "…";
101
- }
102
-
103
- //#endregion
104
11
  //#region src/workers/ledger.ts
105
12
  /**
106
13
  * ledger.ts — the routing ledger, one append-only log for every worker event.
@@ -170,231 +77,6 @@ function ledgerSummary(path, scope, lastN = 15, now = /* @__PURE__ */ new Date()
170
77
  };
171
78
  }
172
79
 
173
- //#endregion
174
- //#region src/workers/status.ts
175
- /**
176
- * status.ts — the live per-worker status file.
177
- *
178
- * One JSON file per run, rewritten atomically on every turn, read by `ps`,
179
- * `follow`, the status line and the pane logic. Same field set the Python
180
- * runner wrote, plus `provider` (routing is multi-provider now) and `session`
181
- * (the AIBroker identity of the launching session, see scope.ts).
182
- */
183
- /** Label a worker gets when launched with neither --label nor a prompt. */
184
- const UNLABELED = "unlabeled";
185
- /** Context-meter percentage 0–100, null when the numbers are missing. */
186
- function contextPercent(s) {
187
- if (!s.contextTokens || !s.contextWindow) return null;
188
- return Math.round(s.contextTokens / s.contextWindow * 100);
189
- }
190
- /** Compact token count: 84k, 200k, 900. */
191
- function fmtContextK(n) {
192
- return n >= 1e3 ? `${Math.round(n / 1e3)}k` : String(n);
193
- }
194
- /**
195
- * The one context-meter label, shared by every renderer so identical
196
- * numbers always render identically: `ctx 84k/200k (42%)` — used-style
197
- * percent. Empty when the numbers are missing (callers drop the part).
198
- */
199
- function contextLabel(s) {
200
- const pct = contextPercent(s);
201
- if (pct === null) return "";
202
- return `ctx ${fmtContextK(s.contextTokens ?? 0)}/${fmtContextK(s.contextWindow ?? 0)} (${pct}%)`;
203
- }
204
- /** Timestamp format shared by status files and the ledger. */
205
- function nowStamp(d = /* @__PURE__ */ new Date()) {
206
- const p = (n) => String(n).padStart(2, "0");
207
- return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
208
- }
209
- let lastId = "";
210
- let idSeq = 0;
211
- function newWorkerId(d = /* @__PURE__ */ new Date(), pid = process.pid) {
212
- const p = (n) => String(n).padStart(2, "0");
213
- const base = `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}-${pid}`;
214
- if (base === lastId) {
215
- idSeq += 1;
216
- return `${base}-${String(idSeq).padStart(2, "0")}`;
217
- }
218
- lastId = base;
219
- idSeq = 0;
220
- return base;
221
- }
222
- /** Write status atomically (temp + rename) and stamp `updated`. */
223
- function saveStatus(logDir, status, d = /* @__PURE__ */ new Date()) {
224
- status.updated = nowStamp(d);
225
- const path = statusPath(logDir, status.id);
226
- const tmp = `${path}.tmp`;
227
- writeFileSync(tmp, JSON.stringify(status), "utf8");
228
- renameSync(tmp, path);
229
- }
230
- /** Load every status file in the logDir, oldest id first, skipping damage. */
231
- function loadStatuses(logDir) {
232
- if (!existsSync(logDir)) return [];
233
- const out = [];
234
- for (const name of readdirSync(logDir).sort()) {
235
- if (!name.endsWith(".status")) continue;
236
- try {
237
- out.push(JSON.parse(readFileSync(join(logDir, name), "utf8")));
238
- } catch {}
239
- }
240
- return out;
241
- }
242
- function loadStatus(logDir, id) {
243
- const path = statusPath(logDir, id);
244
- if (!existsSync(path)) return null;
245
- try {
246
- return JSON.parse(readFileSync(path, "utf8"));
247
- } catch {
248
- return null;
249
- }
250
- }
251
- function alive(pid) {
252
- if (!pid || pid <= 0) return false;
253
- try {
254
- process.kill(pid, 0);
255
- return true;
256
- } catch {
257
- return false;
258
- }
259
- }
260
- /** "42s" under 90s, "7m" above — the coarse age the table and bar show. */
261
- function ageOf(ts, now = /* @__PURE__ */ new Date()) {
262
- const t = Date.parse(ts.replace(" ", "T"));
263
- if (Number.isNaN(t)) return "?";
264
- const s = Math.max(0, Math.floor((now.getTime() - t) / 1e3));
265
- return s < 90 ? `${s}s` : `${Math.floor(s / 60)}m`;
266
- }
267
- /** One-line description of a tool call, e.g. "Bash: npm test". */
268
- function describeTool(name, inp) {
269
- if (typeof inp !== "object" || inp === null) return name;
270
- const i = inp;
271
- const get = (k) => typeof i[k] === "string" ? i[k] : "";
272
- if (name === "Bash") return `Bash: ${shortText(get("command"), 70)}`;
273
- if (name === "Read" || name === "Edit" || name === "Write" || name === "MultiEdit") return `${name}: ${get("file_path").split("/").pop() ?? ""}`;
274
- if (name === "Grep" || name === "Glob") return `${name}: ${shortText(get("pattern"), 50)}`;
275
- if (name === "WebSearch") return `${name}: ${shortText(get("query"), 50)}`;
276
- if (name === "WebFetch") return `${name}: ${shortText(get("url"), 60)}`;
277
- return name;
278
- }
279
-
280
- //#endregion
281
- //#region src/workers/scope.ts
282
- /**
283
- * scope.ts — which workers "belong" to the terminal asking about them.
284
- *
285
- * Two identities, in priority order:
286
- *
287
- * 1. AIBroker's session registry (~/.aibroker/session-names.json): iTerm
288
- * session UUID → session name. The launching session's UUID is stored in
289
- * each worker's status file as `session.id`, so every pane of a named
290
- * session sees its workers — regardless of tab layout.
291
- * 2. The iTerm tab key (`w<window>t<tab>` prefix of ITERM_SESSION_ID): the
292
- * original heuristic, kept as the fallback when no registry entry
293
- * matches (AIBroker absent, unnamed session, non-iTerm terminal).
294
- *
295
- * If neither is available, viewers fall back to "all workers".
296
- *
297
- * A third case: workers spawned from a Claude Code Bash tool (the
298
- * orchestrator pattern) have neither — Claude Code exports no terminal or
299
- * session identity to its Bash children. Those record `spawnerSession`, the
300
- * orchestrator's claude session id bridged through the status line's
301
- * session map (see below), so their tab still claims them.
302
- */
303
- const AIBROKER_REGISTRY = join(homedir(), ".aibroker", "session-names.json");
304
- /**
305
- * `w<window>t<tab>` prefix of an iTerm session id, "" if empty or malformed.
306
- * Panes split from the same tab share this prefix and differ only in `p<n>`.
307
- */
308
- function tabKey(term) {
309
- if (!term) return "";
310
- const head = term.split("p", 1)[0];
311
- const parts = head.slice(1).split("t");
312
- if (head.startsWith("w") && parts.length === 2 && parts.every((p) => /^\d+$/.test(p))) return head;
313
- return "";
314
- }
315
- /** Tab key of the iTerm tab this process runs in, "" if not inside iTerm2. */
316
- function currentTabKey(env = process.env) {
317
- return tabKey(env.ITERM_SESSION_ID ?? "");
318
- }
319
- /** The iTerm UUID part of an ITERM_SESSION_ID (after the last colon). */
320
- function itermUuid(term) {
321
- if (!term) return "";
322
- return term.split(":").pop() ?? "";
323
- }
324
- /**
325
- * Resolve the AIBroker session for an ITERM_SESSION_ID. Reads the persistent
326
- * name registry (the same store `aibroker_rename` writes); returns null when
327
- * AIBroker is absent or the session is not in it — callers then use the tab
328
- * key, which is the pre-AIBroker behaviour.
329
- */
330
- function resolveSession(term, registryPath = AIBROKER_REGISTRY) {
331
- const uuid = itermUuid(term);
332
- if (!uuid) return null;
333
- let names;
334
- try {
335
- if (!existsSync(registryPath)) return null;
336
- names = JSON.parse(readFileSync(registryPath, "utf8"));
337
- } catch {
338
- return null;
339
- }
340
- const name = names[uuid];
341
- if (typeof name !== "string" || !name) return null;
342
- return {
343
- id: uuid,
344
- name
345
- };
346
- }
347
- /**
348
- * True when `worker` was launched from `term` (the terminal asking about it).
349
- * Session-id match first; the tab key only when no registry entry matched —
350
- * a session without a name keeps the tab-scoped behaviour it always had.
351
- */
352
- function workerInScope(worker, term) {
353
- if (!term) return false;
354
- const uuid = itermUuid(term);
355
- if (worker.session?.id && uuid) return worker.session.id === uuid;
356
- return tabKey(worker.term) === tabKey(term) && tabKey(term) !== "";
357
- }
358
- /**
359
- * Registry key for pane files: the session id when AIBroker knows this
360
- * terminal, else the tab key, else the raw iTerm UUID.
361
- */
362
- function scopeKey(term) {
363
- const session = resolveSession(term);
364
- if (session) return session.id;
365
- return currentTabKey() || itermUuid(term);
366
- }
367
- /** `[Name]` when the worker has an AIBroker session name, else "". */
368
- function sessionTag(worker) {
369
- return worker.session?.name ? `[${worker.session.name}]` : "";
370
- }
371
- /** How old a map entry may be for a spawn to adopt it (status lines refresh constantly while a session lives). */
372
- const SPAWNER_SESSION_TTL_MS = 10 * 6e4;
373
- /** Map entries not refreshed within this window are pruned on write. */
374
- const SESSION_MAP_PRUNE_MS = 60 * 6e4;
375
- function sessionMapPath(logDir) {
376
- return join(logDir, "claude-session-map.json");
377
- }
378
- /**
379
- * The claude session a new run was spawned by, when it can be known: a run
380
- * inside another worker inherits its spawner (chain stages keep the
381
- * orchestrator's tab that way); otherwise a fresh map entry for `cwd`.
382
- */
383
- function resolveSpawnerSession(logDir, cwd, env = process.env, now = Date.now()) {
384
- const parentId = env.PAI_WORKER_ID;
385
- if (parentId) {
386
- const inherited = loadStatus(logDir, parentId)?.spawnerSession;
387
- if (inherited) return inherited;
388
- }
389
- const path = sessionMapPath(logDir);
390
- if (!existsSync(path) || !cwd) return null;
391
- try {
392
- const entry = JSON.parse(readFileSync(path, "utf8"))[cwd];
393
- if (entry && entry.session && now - entry.ts < SPAWNER_SESSION_TTL_MS) return entry.session;
394
- } catch {}
395
- return null;
396
- }
397
-
398
80
  //#endregion
399
81
  //#region src/workers/pane.ts
400
82
  /**
@@ -405,10 +87,14 @@ function resolveSpawnerSession(logDir, cwd, env = process.env, now = Date.now())
405
87
  * one splits the lowest live worker pane horizontally, so panes stack top to
406
88
  * bottom. The split never sizes the new session — that would grow the whole
407
89
  * window; instead the window's bounds are read before the split and restored
408
- * right after, so the panes share the space the window already had. A split
409
- * would also make the new session the tab's active one the scripts re-select
410
- * the launching session right after, so a pane opening never steals the
411
- * keystrokes the operator is typing. Each
90
+ * right after, so the panes share the space the window already had. The follow
91
+ * command is part of the split itself iTerm creates the new session already
92
+ * running it so no text is ever typed into any session afterwards; a separate
93
+ * typing step raced the operator's keystrokes (focus briefly sits on the new
94
+ * pane after a split, and in one observed failure the command landed in the
95
+ * operator's own shell). A split still makes the new session the tab's active
96
+ * one — the scripts re-select the launching session right after, so a pane
97
+ * opening never steals the keystrokes the operator is typing. Each
412
98
  * pane runs `pai worker follow <id> --auto-exit <n>` under the `pai-worker`
413
99
  * dynamic profile (Close Sessions On End), so panes disappear by themselves.
414
100
  *
@@ -463,34 +149,35 @@ const WORKER_SPLIT_SCRIPT = `on run(argv)
463
149
  end if
464
150
  end if
465
151
  end repeat
152
+ -- the follow command is part of the split itself: the
153
+ -- pane is born already running it, so no text is ever
154
+ -- typed into any session — a typing step raced the
155
+ -- operator's keystrokes and could even land in the
156
+ -- operator's own session
466
157
  if profileName is "" then
467
158
  if splitS is missing value then
468
159
  tell s
469
- set newS to split vertically with default profile
160
+ set newS to split vertically with default profile command followCmd
470
161
  end tell
471
162
  else
472
163
  tell splitS
473
- set newS to split horizontally with default profile
164
+ set newS to split horizontally with default profile command followCmd
474
165
  end tell
475
166
  end if
476
167
  else
477
168
  if splitS is missing value then
478
169
  tell s
479
- set newS to split vertically with profile profileName
170
+ set newS to split vertically with profile profileName command followCmd
480
171
  end tell
481
172
  else
482
173
  tell splitS
483
- set newS to split horizontally with profile profileName
174
+ set newS to split horizontally with profile profileName command followCmd
484
175
  end tell
485
176
  end if
486
177
  end if
487
- tell newS
488
- write text followCmd
489
- end tell
490
178
  -- a split makes the new session the tab's active one:
491
- -- re-select the launching session so keystrokes keep
492
- -- going where the operator was typing, not into the
493
- -- pane's chat prompt
179
+ -- re-select the launching session so focus returns to
180
+ -- where the operator was typing, never the new pane
494
181
  try
495
182
  select s
496
183
  end try
@@ -571,11 +258,11 @@ const SPLIT_SCRIPT = `on run(argv)
571
258
  repeat with t in tabs of w
572
259
  repeat with s in sessions of t
573
260
  if id of s is targetID then
261
+ -- the follow command is part of the split itself (see
262
+ -- WORKER_SPLIT_SCRIPT): nothing is ever typed into a
263
+ -- session afterwards
574
264
  tell s
575
- set newS to split vertically with default profile
576
- end tell
577
- tell newS
578
- write text followCmd
265
+ set newS to split vertically with default profile command followCmd
579
266
  end tell
580
267
  -- keep the tab's active session where it was (see
581
268
  -- WORKER_SPLIT_SCRIPT)
@@ -808,6 +495,23 @@ function saveRegistry(path, reg) {
808
495
  writeFileSync(tmp, JSON.stringify(reg, null, 1), "utf8");
809
496
  renameSync(tmp, path);
810
497
  }
498
+ /**
499
+ * The command a worker pane runs: one exec-able line with no shell syntax —
500
+ * iTerm's split command parameter execs without a shell (proven live: panes
501
+ * survive `sleep 30` and a single-quoted `/bin/sh -c`, die instantly on
502
+ * `export …; …` or a redirect), so the command must be one exec-able line.
503
+ * Absolute node on the CLI entry file also sidesteps the panes' bare launchd
504
+ * PATH (no pai, no node) and the `#!/usr/bin/env node` shebang.
505
+ */
506
+ function followCommand(wid, autoExitSecs) {
507
+ return `${process.execPath} ${absoluteCliPath()} worker follow ${wid} --auto-exit ${autoExitSecs}`;
508
+ }
509
+ /** Absolute path of the running pai CLI; falls back to PATH lookup at spawn. */
510
+ function absoluteCliPath() {
511
+ const argv1 = process.argv[1] ?? "";
512
+ if (argv1.endsWith("pai")) return argv1;
513
+ return "pai";
514
+ }
811
515
  /** Open (or only report on) the stacked follow pane of one worker. */
812
516
  async function openPaneForWorker(logDir, config, wid, term) {
813
517
  if (workerPaneOpen(wid)) return `pane for ${wid} already open`;
@@ -815,7 +519,7 @@ async function openPaneForWorker(logDir, config, wid, term) {
815
519
  const regPath = join(panesDir(logDir), `${scopeKey(term)}.json`);
816
520
  const reg = loadRegistry(regPath);
817
521
  const profile = await followProfile(config.pane.fontSize);
818
- const cmd = ` exec pai worker follow ${JSON.stringify(wid)} --auto-exit ${config.pane.autoExitSecs}`;
522
+ const cmd = followCommand(wid, config.pane.autoExitSecs);
819
523
  const p = await osascript(WORKER_SPLIT_SCRIPT, [
820
524
  uid,
821
525
  [...reg].reverse().map((e) => e.session).join(","),
@@ -889,7 +593,7 @@ async function openFollowPane(logDir, _config, term, checkOnly) {
889
593
  if (out === "notrunning") throw new Error("pai worker pane: iTerm2 is not running");
890
594
  if ([...new Set(out.split(",").map((t) => t.trim()).filter(Boolean))].filter((t) => followTtys().has(t)).length > 0) return "follow pane already open";
891
595
  if (checkOnly) return "no follow pane";
892
- const s = await osascript(SPLIT_SCRIPT, [uid, " exec pai worker follow"]);
596
+ const s = await osascript(SPLIT_SCRIPT, [uid, `${process.execPath} ${absoluteCliPath()} worker follow`]);
893
597
  const sout = s.stdout.trim();
894
598
  if (sout === "opened") return "follow pane opened";
895
599
  if (sout === "notfound") throw new Error("pai worker pane: no open iTerm2 session matches ITERM_SESSION_ID");
@@ -1232,6 +936,11 @@ function worktreeWanted(flag, opts) {
1232
936
  * degrade to an in-place run.
1233
937
  */
1234
938
  function addWorktree(logDir, id, cwd) {
939
+ const swept = sweepOrphanWorktrees(logDir);
940
+ if (swept.length) appendLedger(ledgerPath(logDir), "WORKER-NOTE", {
941
+ id,
942
+ note: `swept orphan worktree(s): ${swept.join(", ")}`
943
+ });
1235
944
  const dir = worktreePath(logDir, id);
1236
945
  const branch = worktreeBranch(id);
1237
946
  const base = git(cwd, ["rev-parse", "HEAD"]);
@@ -1248,6 +957,46 @@ function addWorktree(logDir, id, cwd) {
1248
957
  base
1249
958
  };
1250
959
  }
960
+ /**
961
+ * Remove worktrees (and their `worker/<id>` branches) whose worker no longer
962
+ * exists — no status file left in the log dir. Killed and failed runs clean
963
+ * up after themselves, but a `kill -9` or a crash strands a directory and a
964
+ * branch; every worktree creation sweeps first so they cannot accumulate.
965
+ * Directories younger than `minAgeMin` minutes are left alone: a run that is
966
+ * just starting owns its worktree a moment before its status file exists.
967
+ */
968
+ function sweepOrphanWorktrees(logDir, minAgeMin = 10) {
969
+ const root = worktreesDir(logDir);
970
+ if (!existsSync(root)) return [];
971
+ const swept = [];
972
+ for (const ent of readdirSync(root, { withFileTypes: true })) {
973
+ if (!ent.isDirectory()) continue;
974
+ const id = ent.name;
975
+ const dir = join(root, id);
976
+ if (existsSync(statusPath(logDir, id))) continue;
977
+ try {
978
+ if ((Date.now() - statSync(dir).mtimeMs) / 6e4 < minAgeMin) continue;
979
+ } catch {}
980
+ let gitDir = null;
981
+ try {
982
+ const raw = git(dir, ["rev-parse", "--git-common-dir"]);
983
+ gitDir = isAbsolute(raw) ? raw : resolve(dir, raw);
984
+ } catch {
985
+ gitDir = null;
986
+ }
987
+ removeWorktree(gitDir ?? dir, dir, true);
988
+ if (gitDir) try {
989
+ git(gitDir, ["worktree", "prune"]);
990
+ git(gitDir, [
991
+ "branch",
992
+ "-D",
993
+ worktreeBranch(id)
994
+ ]);
995
+ } catch {}
996
+ swept.push(id);
997
+ }
998
+ return swept;
999
+ }
1251
1000
  /** Commits the branch collected on top of its base. */
1252
1001
  function commitsSince(dir, base) {
1253
1002
  try {
@@ -1533,6 +1282,9 @@ const WORKER_CONTRACT_PROMPT = [
1533
1282
  "notes: one line, the headline a reviewer reads first.",
1534
1283
  "Never write shell heredocs — write the script to a file with the Write tool, then run it",
1535
1284
  "(heredocs fail silently or get swallowed).",
1285
+ "Never inline code one-liners (python3 -c, node -e, ruby -e and friends) — the permission layer",
1286
+ "denies them (\"ztk: command denied by permission rules\"); write the script to a file with the",
1287
+ "Write tool and run it (python3 script.py), or use jq for JSON inspection.",
1536
1288
  "",
1537
1289
  `Operator messages: a user turn starting with ${OPERATOR_MARK} was typed by the operator while you`,
1538
1290
  "run (the prompt of a resumed run is the operator's too). Answer it FIRST, in one or two plain lines —",
@@ -1890,12 +1642,19 @@ function codexInstalled() {
1890
1642
  * - env: ANTHROPIC_BASE_URL/AUTH_TOKEN from the provider (token from its
1891
1643
  * key file, never from the environment), the three DEFAULT_*_MODEL vars,
1892
1644
  * the provider's extra env, nonessential traffic off, and ANTHROPIC_API_KEY
1893
- * stripped so nothing can fall back to Anthropic billing. OpenAI-protocol
1894
- * providers point at the PAI proxy instead (started on demand, the
1895
- * provider name in the URL path); codex-engine providers run the Codex CLI.
1645
+ * stripped so nothing can fall back to Anthropic billing. Headless runs
1646
+ * also drop the spawner's session identity (messaging socket, session id,
1647
+ * nesting markers) inherited, a worktree child starts tool-blind.
1648
+ * OpenAI-protocol providers point at the PAI proxy instead (started on
1649
+ * demand, the provider name in the URL path); codex-engine providers run
1650
+ * the Codex CLI.
1896
1651
  * - headless (-p): strict empty MCP config unless the caller brings one or
1897
- * names servers via --mcp / a role (then a filtered <id>.mcp.json),
1898
- * PAI_WORKER=1 so PAI's per-session hooks leave it alone, the worker
1652
+ * names servers via --mcp / a role (then a filtered <id>.mcp.json), the
1653
+ * core tool grants the caller did not bring (a headless run cannot
1654
+ * approve a permission-gated tool mid-flight — with no grant at all,
1655
+ * claude drops the file/shell tools entirely and the worker is
1656
+ * tool-blind), PAI_WORKER=1 so PAI's per-session hooks leave it alone,
1657
+ * the worker
1899
1658
  * contract appended to the system prompt, `--input-format stream-json`
1900
1659
  * with the prompt as the first stdin user message (the operator socket
1901
1660
  * can add more mid-run), stream-json mirroring (every line stamped `_ts`)
@@ -1908,15 +1667,6 @@ function codexInstalled() {
1908
1667
  * Auto-routed runs that die of a quota error before the first tool call are
1909
1668
  * restarted on the next provider in routing order (WORKER-REROUTE ledger line).
1910
1669
  */
1911
- /** The strict empty MCP config for headless workers, written on demand. */
1912
- function ensureNoMcpConfig(logDir) {
1913
- const path = noMcpConfigPath(logDir);
1914
- if (!existsSync(path)) {
1915
- mkdirSync(logDir, { recursive: true });
1916
- writeFileSync(path, "{ \"mcpServers\": {} }\n", "utf8");
1917
- }
1918
- return path;
1919
- }
1920
1670
  /** A stream-json user message for the child's stdin. */
1921
1671
  function stdinUserMessage(text) {
1922
1672
  return JSON.stringify({
@@ -1992,7 +1742,7 @@ async function runWorker(opts) {
1992
1742
  const logDir = workersLogDir(config);
1993
1743
  mkdirSync(logDir, { recursive: true });
1994
1744
  if (opts.className === "plan" && !opts._planner) {
1995
- const { runPlanner } = await import("./planner-DsmMRIZR.mjs");
1745
+ const { runPlanner } = await import("./planner-ogy-Zy4O.mjs");
1996
1746
  return runPlanner(opts);
1997
1747
  }
1998
1748
  const parent = launchParent(opts.parent);
@@ -2051,6 +1801,19 @@ async function runWorker(opts) {
2051
1801
  throw e;
2052
1802
  }
2053
1803
  }
1804
+ /**
1805
+ * The core tools a headless run grants when the caller brings none of its
1806
+ * own. Permission-gated tools (Bash, Read, Write, …) are never offered to a
1807
+ * headless session that has no allow rule for them — the child starts with
1808
+ * only the tools that never ask (tool search, web, cron, tasks) and cannot
1809
+ * touch a file or shell (2026-09-18). A caller's own --allowedTools is a
1810
+ * deliberate restriction and passes through untouched.
1811
+ */
1812
+ const DEFAULT_WORKER_TOOLS = "Read,Edit,Write,Bash,Grep,Glob";
1813
+ /** The --allowedTools args a headless run needs; [] when the caller granted. */
1814
+ function headlessToolGrants(allowedTools) {
1815
+ return allowedTools.length ? [] : ["--allowedTools", DEFAULT_WORKER_TOOLS];
1816
+ }
2054
1817
  async function executeRun(a) {
2055
1818
  const { config, logDir, target, model, label, parsed, noPane } = a;
2056
1819
  const headless = parsed.headless;
@@ -2139,9 +1902,10 @@ async function executeRun(a) {
2139
1902
  ];
2140
1903
  }
2141
1904
  const restArgs = headless ? stripPromptValues(parsed.rest) : parsed.rest;
1905
+ const toolArgs = headless ? headlessToolGrants(parsed.allowedTools) : [];
2142
1906
  const cmd = ["claude"];
2143
1907
  if (!parsed.callerModel) cmd.push("--model", model);
2144
- cmd.push(...mcpArgs, ...restArgs);
1908
+ cmd.push(...mcpArgs, ...toolArgs, ...restArgs);
2145
1909
  if (headless) {
2146
1910
  cmd.push("--output-format", "stream-json", "--verbose", "--input-format", "stream-json");
2147
1911
  if (!parsed.callerSystemPrompt) cmd.push("--append-system-prompt", WORKER_CONTRACT_PROMPT);
@@ -2214,6 +1978,9 @@ async function executeRun(a) {
2214
1978
  killed: 1,
2215
1979
  label
2216
1980
  });
1981
+ if (worktree) try {
1982
+ recordWorktree(logDir, status, worktree, false);
1983
+ } catch {}
2217
1984
  cleanup();
2218
1985
  try {
2219
1986
  proc.kill();
@@ -2222,6 +1989,7 @@ async function executeRun(a) {
2222
1989
  };
2223
1990
  process.once("SIGTERM", () => onSignal("SIGTERM"));
2224
1991
  process.once("SIGINT", () => onSignal("SIGINT"));
1992
+ process.once("SIGHUP", () => onSignal("SIGHUP"));
2225
1993
  const ctx = {
2226
1994
  resultEvent: null,
2227
1995
  resultReport: null
@@ -2457,6 +2225,7 @@ async function executeCodexRun(a) {
2457
2225
  let killed = false;
2458
2226
  process.once("SIGTERM", onCodexSignal("SIGTERM"));
2459
2227
  process.once("SIGINT", onCodexSignal("SIGINT"));
2228
+ process.once("SIGHUP", onCodexSignal("SIGHUP"));
2460
2229
  function onCodexSignal(sig) {
2461
2230
  return () => {
2462
2231
  killed = true;
@@ -2465,6 +2234,9 @@ async function executeCodexRun(a) {
2465
2234
  status.secs = Math.floor((Date.now() - t0) / 1e3);
2466
2235
  status.last = `killed by signal ${sig}`;
2467
2236
  saveStatus(logDir, status);
2237
+ if (worktree) try {
2238
+ recordWorktree(logDir, status, worktree, false);
2239
+ } catch {}
2468
2240
  try {
2469
2241
  proc.kill();
2470
2242
  } catch {}
@@ -2825,5 +2597,5 @@ async function runChain(opts, deps = {}) {
2825
2597
  }
2826
2598
 
2827
2599
  //#endregion
2828
- export { appendLedger as A, ageOf as C, loadStatus as D, contextPercent as E, parseRunnerArgs as M, shortText as N, loadStatuses as O, UNLABELED as S, contextLabel as T, openPaneForWorker as _, testProvider as a, sessionTag as b, renderReport as c, handoffFromInside as d, readInbox as f, openFollowPane as g, checkPaneForWorker as h, runWorker as i, ledgerSummary as j, newWorkerId as k, discardWorker as l, workerDepth as m, swapPromptArg as n, describeMcp as o, sayToWorker as p, printResult as r, parseWorkerReport as s, runChain as t, mergeWorker as u, currentTabKey as v, alive as w, workerInScope as x, resolveSession as y };
2829
- //# sourceMappingURL=chain-zMjCDW-R.mjs.map
2600
+ export { openPaneForWorker as _, testProvider as a, renderReport as c, handoffFromInside as d, readInbox as f, openFollowPane as g, checkPaneForWorker as h, runWorker as i, discardWorker as l, workerDepth as m, swapPromptArg as n, describeMcp as o, sayToWorker as p, printResult as r, parseWorkerReport as s, runChain as t, mergeWorker as u, appendLedger as v, ledgerSummary as y };
2601
+ //# sourceMappingURL=chain-DsvSHnO9.mjs.map