@tekmidian/pai 1.0.5 → 1.0.7

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 (247) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/dist/{aibroker-client-CQPGqLxL.mjs → aibroker-client-CXsyC7A8.mjs} +2 -2
  3. package/dist/aibroker-client-ChB7gSxV.mjs +3 -0
  4. package/dist/auto-route-CAlZ5p-h.mjs +11 -0
  5. package/dist/{auto-route-DUYr57rs.mjs → auto-route-PDOAYszN.mjs} +2 -2
  6. package/dist/budget-BXmsSBLI.mjs +15 -0
  7. package/dist/{caller-identity-Bim8JT-D.mjs → caller-identity-DPDSrZE2.mjs} +4 -4
  8. package/dist/{capacity-CsZMkVyg.mjs → capacity-CThSydrd.mjs} +82 -10
  9. package/dist/{chain-Dzz6xcKH.mjs → chain-BvdKqRiZ.mjs} +5 -5
  10. package/dist/cli/index.mjs +2175 -284
  11. package/dist/{cockpit-actions-BTA1njMO.mjs → cockpit-actions-BgdKB9G3.mjs} +26 -26
  12. package/dist/{cockpit-actions-BgsiNcUY.mjs → cockpit-actions-DNl0rKfW.mjs} +18 -18
  13. package/dist/cockpit-memory-CqLPjcWe.mjs +20 -0
  14. package/dist/config/defaults/config.yaml +24 -1
  15. package/dist/config/defaults/workers.yaml +2 -0
  16. package/dist/{config-h5qK3fst.mjs → config-C2dd2SXS.mjs} +2 -2
  17. package/dist/{config-CST2uq1v.mjs → config-DGV64YJa.mjs} +11 -3
  18. package/dist/{config-Dv3t7xnT.mjs → config-DHgPx3ER.mjs} +1 -1
  19. package/dist/{config-DYbyEe4-.mjs → config-SlZ-Z52i.mjs} +3 -2
  20. package/dist/{controls-OiVVbe-f.mjs → controls-ByrcupcA.mjs} +59 -66
  21. package/dist/daemon/index.mjs +40 -40
  22. package/dist/{daemon-C1dycZ3W.mjs → daemon-BjgJyJMP.mjs} +79 -31
  23. package/dist/daemon-iymy18Ie.mjs +46 -0
  24. package/dist/daemon-mcp/index.mjs +49 -38
  25. package/dist/detect-C8nFP-kR.mjs +10 -0
  26. package/dist/{detect-CHuxFrLy.mjs → detect-DLjeYwnt.mjs} +2 -2
  27. package/dist/{embed-pass-DtVF2fOV.mjs → embed-pass-DdTMn4DH.mjs} +5 -5
  28. package/dist/{embedding-gate-B98PK_wz.mjs → embedding-gate-BVQvUmox.mjs} +2 -2
  29. package/dist/{embedding-gate-CTAk8STe.mjs → embedding-gate-iq0oQEkF.mjs} +3 -3
  30. package/dist/{endreason-BqyyU7BH.mjs → endreason-BjPom527.mjs} +4 -4
  31. package/dist/{factory-BV0MQA3T.mjs → factory-CYwa0jYj.mjs} +6 -6
  32. package/dist/factory-DFwez99M.mjs +9 -0
  33. package/dist/{fallback-Bj2cyVk_.mjs → fallback-CQ8_CuVu.mjs} +9 -231
  34. package/dist/{health-kufVIf6i.mjs → health-Bb2U4PNK.mjs} +5 -5
  35. package/dist/{helpers-CvbYEKoD.mjs → helpers-Dj2nY56t.mjs} +2 -2
  36. package/dist/{helpers-WV8tdQHM.mjs → helpers-DyHKAwrW.mjs} +6 -6
  37. package/dist/hooks/block-sleep-poll.mjs +9 -0
  38. package/dist/hooks/capture-all-events.mjs +99 -3
  39. package/dist/hooks/capture-tool-output.mjs +99 -3
  40. package/dist/hooks/handoff-link.mjs +159 -0
  41. package/dist/hooks/load-project-context.mjs +10 -1
  42. package/dist/hooks/local-model-hint.mjs +9 -0
  43. package/dist/hooks/route-agents-to-worker.mjs +9 -0
  44. package/dist/hooks/security-validator.mjs +940 -92
  45. package/dist/hooks/supervision-banner.mjs +219 -48
  46. package/dist/hooks/worker-edit-repair.mjs +9 -0
  47. package/dist/hooks/worker-guard.mjs +97 -1
  48. package/dist/hooks/worker-note.mjs +108 -3
  49. package/dist/hooks/worker-pool-host.mjs +1670 -315
  50. package/dist/hooks/worker-proxy.mjs +9 -0
  51. package/dist/hooks/worker-status-line.mjs +251 -70
  52. package/dist/hooks/worker-supervision.mjs +9 -0
  53. package/dist/hooks/worker-tool-name-repair.mjs +9 -0
  54. package/dist/index.mjs +9 -9
  55. package/dist/{ipc-client-CnYEF31R.mjs → ipc-client-CmagIFLh.mjs} +2 -2
  56. package/dist/{ipc-client-DN7EvoVo.mjs → ipc-client-DmbSIBUK.mjs} +1 -1
  57. package/dist/kill-tree-BlPw1vaU.mjs +10 -0
  58. package/dist/{kill-tree-CcpsRLCZ.mjs → kill-tree-DTmiEb3T.mjs} +8 -8
  59. package/dist/{launch-DpQPZGsT.mjs → launch-CV6oPHI2.mjs} +3 -3
  60. package/dist/{launch-UPQBoW2q.mjs → launch-Dy8qQnSD.mjs} +3 -3
  61. package/dist/{indexer-backend-B2vtFl38.mjs → lessons-CdmqZD0G.mjs} +262 -43
  62. package/dist/{local-candidate-Cp_bw4kK.mjs → local-candidate-C2FGhf54.mjs} +3 -3
  63. package/dist/{local-instances-bdCNNSsL.mjs → local-instances-DhF4_sV2.mjs} +2 -2
  64. package/dist/{local-instances-B-c-CxzP.mjs → local-instances-GgXydQ2P.mjs} +4 -4
  65. package/dist/{local-tiers-BtdEEe5n.mjs → local-tiers-CqwGUcEn.mjs} +3 -3
  66. package/dist/{local-tiers-OBqAAfOg.mjs → local-tiers-X_dQfI4Q.mjs} +3 -3
  67. package/dist/{main-config-ops-BYsK3BxM.mjs → main-config-ops-C_fkINDv.mjs} +3 -3
  68. package/dist/{main-resolver-CAC3xfZM.mjs → main-resolver-CN_sfG1R.mjs} +18 -18
  69. package/dist/{migrate-DrA_OwlZ.mjs → migrate-C2z1YmWC.mjs} +2 -2
  70. package/dist/{model-inventory-BuTpi9a6.mjs → model-inventory-BYNrSTaY.mjs} +13 -13
  71. package/dist/{models-add-CJx2YAwh.mjs → models-add-Dc7VXmcu.mjs} +38 -38
  72. package/dist/operator-IrLbk8hy.mjs +7 -0
  73. package/dist/{operator-DrwWItVt.mjs → operator-lRGvYr3D.mjs} +2 -2
  74. package/dist/{pai-marker-DLKJ9Yr5.mjs → pai-marker-kA0iM7rP.mjs} +2 -2
  75. package/dist/{pane-audit-wv9UkyCe.mjs → pane-audit-DUuilP1V.mjs} +13 -23
  76. package/dist/{paths-DS5VQKoR.mjs → paths-BwfwHOvN.mjs} +4 -4
  77. package/dist/{paths-DBd78I7Z.mjs → paths-CKGYM0Lb.mjs} +3 -3
  78. package/dist/{planner-K0mFrCpL.mjs → planner-BDHVUkIM.mjs} +29 -29
  79. package/dist/{pool-BZ94nFuq.mjs → pool-CFeHGX7q.mjs} +2 -2
  80. package/dist/{pool-cli-BjYQDfx6.mjs → pool-cli-Bj2TeA-d.mjs} +6 -6
  81. package/dist/{pool-cli-B1eKySeN.mjs → pool-cli-DOLgPgOM.mjs} +5 -5
  82. package/dist/{postgres-CBHY5PIi.mjs → postgres-B1B80bcj.mjs} +3 -3
  83. package/dist/{postgres-C-T5OWer.mjs → postgres-BL5oBBgY.mjs} +3 -3
  84. package/dist/{project-setup-pIPHZaXf.mjs → project-setup-i5D0xkTn.mjs} +24 -25
  85. package/dist/{providers-Dzx5GAUI.mjs → providers-BC2Myo_O.mjs} +6 -6
  86. package/dist/providers-C-mnlH1b.mjs +16 -0
  87. package/dist/{query-feedback-DN52N9C5.mjs → query-feedback-BWl2HRwN.mjs} +2 -2
  88. package/dist/registrable-Ct0DKNKS.mjs +9 -0
  89. package/dist/{registrable-CXBBpUcz.mjs → registrable-D_4qXCnq.mjs} +4 -4
  90. package/dist/render-CbifCwTb.mjs +2350 -0
  91. package/dist/render-FB4Fv_fi.mjs +13 -0
  92. package/dist/root-80ESg6b7.mjs +36 -0
  93. package/dist/{root-bG48WVpW.mjs → root-B-ZYiEgi.mjs} +10 -10
  94. package/dist/route-table-DcBZCuco.mjs +15 -0
  95. package/dist/{router-CQSc3lk3.mjs → router-D2476pOp.mjs} +2 -2
  96. package/dist/{router-DwHJRgG9.mjs → router-ghujlEFb.mjs} +1 -1
  97. package/dist/{routing-BlD_tKjW.mjs → routing-CF0wlR-_.mjs} +15 -15
  98. package/dist/{routing-D27jxj9x.mjs → routing-CJ8ioPl2.mjs} +13 -13
  99. package/dist/{run-AZVwceiU.mjs → run-CAjfKBQV.mjs} +74 -31
  100. package/dist/{runtime-paths-WsO9sJFc.mjs → runtime-paths-twfV23YL.mjs} +2 -2
  101. package/dist/{scan-C3uotg_f.mjs → scan-ew8M6tTH.mjs} +7 -7
  102. package/dist/{scope-D8HK6vU7.mjs → scope-BRcaPw5D.mjs} +4 -4
  103. package/dist/session-BmQsXF_a.mjs +35 -0
  104. package/dist/{session-BAfypQb9.mjs → session-C1JnDXvP.mjs} +28 -28
  105. package/dist/session-scan-DWCUKhDM.mjs +9 -0
  106. package/dist/{session-scan-BmnjSReu.mjs → session-scan-DihMibI-.mjs} +6 -6
  107. package/dist/{session-usage-oycSdiD3.mjs → session-usage-BRIWFYPS.mjs} +3 -3
  108. package/dist/status-Bj4yVKj0.mjs +6 -0
  109. package/dist/{status-COtzewKV.mjs → status-DTMT5lyn.mjs} +9 -5
  110. package/dist/{tool-errors-DXJ70-sr.mjs → tool-errors-BOW2n882.mjs} +2 -2
  111. package/dist/{tool-errors-B2LnIqR0.mjs → tool-errors-jNJj3Gzm.mjs} +6 -6
  112. package/dist/{viewer-BgGRqXfD.mjs → viewer-4BU8w_gK.mjs} +16 -14
  113. package/dist/viewer-D3VEdqhC.mjs +22 -0
  114. package/dist/{wait-ChpHKJZV.mjs → wait-BindDA5w.mjs} +3 -3
  115. package/dist/{worker-config-DgdT0iTB.mjs → worker-config-CdPmb4_2.mjs} +54 -103
  116. package/dist/worker-config-CfV2dgSb.mjs +42 -0
  117. package/dist/{workers-config-BhZBrR-Q.mjs → workers-config-2VipVOYk.mjs} +2 -2
  118. package/dist/{worktree-BY--Rgmy.mjs → worktree-7ZS3RBf8.mjs} +175 -9
  119. package/dist/{zettelkasten-BHVx5zj-.mjs → zettelkasten-BH8AdNh8.mjs} +4 -4
  120. package/dist/{zettelkasten-LoH9RBiN.mjs → zettelkasten-FFhQFESJ.mjs} +2 -2
  121. package/docs/commands/README.md +32 -2
  122. package/docs/commands/audit.md +34 -3
  123. package/docs/commands/backup.md +1 -1
  124. package/docs/commands/build.md +4 -4
  125. package/docs/commands/cd.md +1 -1
  126. package/docs/commands/clear-names.md +1 -1
  127. package/docs/commands/cockpit.md +1 -1
  128. package/docs/commands/config.md +1 -1
  129. package/docs/commands/daemon.md +1 -1
  130. package/docs/commands/db.md +1 -1
  131. package/docs/commands/doc.md +1 -1
  132. package/docs/commands/end.md +1 -1
  133. package/docs/commands/find.md +1 -1
  134. package/docs/commands/git.md +110 -0
  135. package/docs/commands/handoff.md +70 -0
  136. package/docs/commands/harness.md +74 -0
  137. package/docs/commands/help.md +1 -1
  138. package/docs/commands/home.md +1 -1
  139. package/docs/commands/hooks-db.md +1 -1
  140. package/docs/commands/identity.md +1 -1
  141. package/docs/commands/install.md +1 -1
  142. package/docs/commands/issue.md +1 -1
  143. package/docs/commands/kg.md +1 -1
  144. package/docs/commands/launch.md +1 -1
  145. package/docs/commands/lessons.md +201 -0
  146. package/docs/commands/list.md +1 -1
  147. package/docs/commands/ls.md +1 -1
  148. package/docs/commands/mcp.md +1 -1
  149. package/docs/commands/memory.md +1 -1
  150. package/docs/commands/models.md +1 -1
  151. package/docs/commands/more.md +1 -1
  152. package/docs/commands/new.md +1 -1
  153. package/docs/commands/notes.md +1 -1
  154. package/docs/commands/notify.md +1 -1
  155. package/docs/commands/observation.md +1 -1
  156. package/docs/commands/obsidian.md +1 -1
  157. package/docs/commands/open.md +1 -1
  158. package/docs/commands/pane.md +1 -1
  159. package/docs/commands/pause.md +1 -1
  160. package/docs/commands/plan.md +1 -1
  161. package/docs/commands/project.md +1 -1
  162. package/docs/commands/projects.md +1 -1
  163. package/docs/commands/providers.md +1 -1
  164. package/docs/commands/queue.md +1 -1
  165. package/docs/commands/registry.md +1 -1
  166. package/docs/commands/reload.md +1 -1
  167. package/docs/commands/rename.md +1 -1
  168. package/docs/commands/restore.md +1 -1
  169. package/docs/commands/resume.md +1 -1
  170. package/docs/commands/router.md +1 -1
  171. package/docs/commands/rules.md +70 -0
  172. package/docs/commands/session.md +1 -1
  173. package/docs/commands/sessions.md +1 -1
  174. package/docs/commands/setup.md +1 -1
  175. package/docs/commands/setups.md +1 -1
  176. package/docs/commands/shell-init.md +1 -1
  177. package/docs/commands/skill.md +1 -1
  178. package/docs/commands/task.md +1 -1
  179. package/docs/commands/tasks.md +1 -1
  180. package/docs/commands/topic.md +1 -1
  181. package/docs/commands/update.md +1 -1
  182. package/docs/commands/worker.md +1 -1
  183. package/docs/commands/zettel.md +1 -1
  184. package/docs/concepts/panes.md +3 -2
  185. package/gemini-extension.json +1 -1
  186. package/package.json +2 -2
  187. package/pai-plugin.json +1 -1
  188. package/plugins/context-preservation/plugin.json +1 -1
  189. package/plugins/core/plugin.json +1 -1
  190. package/plugins/creative/plugin.json +1 -1
  191. package/plugins/observability/plugin.json +1 -1
  192. package/plugins/productivity/plugin.json +1 -1
  193. package/plugins/semantic-search/plugin.json +1 -1
  194. package/plugins/ui/plugin.json +1 -1
  195. package/plugins/zettelkasten/plugin.json +1 -1
  196. package/src/hooks/ts/lib/loop-guard.ts +3 -5
  197. package/src/hooks/ts/lib/rules/builtin/base64-decode-to-shell.rule.yaml +15 -0
  198. package/src/hooks/ts/lib/rules/builtin/chmod-world-writable.rule.yaml +13 -0
  199. package/src/hooks/ts/lib/rules/builtin/chmod-zero.rule.yaml +13 -0
  200. package/src/hooks/ts/lib/rules/builtin/credential-file-read.rule.yaml +13 -0
  201. package/src/hooks/ts/lib/rules/builtin/credential-read-then-network.rule.yaml +29 -0
  202. package/src/hooks/ts/lib/rules/builtin/crontab-persistence.rule.yaml +10 -0
  203. package/src/hooks/ts/lib/rules/builtin/curl-piped-to-shell.rule.yaml +22 -0
  204. package/src/hooks/ts/lib/rules/builtin/download-then-execute.rule.yaml +30 -0
  205. package/src/hooks/ts/lib/rules/builtin/find-root-delete.rule.yaml +14 -0
  206. package/src/hooks/ts/lib/rules/builtin/git-push-force.rule.yaml +24 -0
  207. package/src/hooks/ts/lib/rules/builtin/git-push-plus-refspec.rule.yaml +12 -0
  208. package/src/hooks/ts/lib/rules/builtin/git-reset-hard.rule.yaml +12 -0
  209. package/src/hooks/ts/lib/rules/builtin/injection-then-tool.rule.yaml +30 -0
  210. package/src/hooks/ts/lib/rules/builtin/instruction-override-disregard.rule.yaml +14 -0
  211. package/src/hooks/ts/lib/rules/builtin/instruction-override-ignore.rule.yaml +13 -0
  212. package/src/hooks/ts/lib/rules/builtin/launchagents-write-persistence.rule.yaml +12 -0
  213. package/src/hooks/ts/lib/rules/builtin/launchctl-load-persistence.rule.yaml +10 -0
  214. package/src/hooks/ts/lib/rules/builtin/reverse-shell-dev-tcp.rule.yaml +14 -0
  215. package/src/hooks/ts/lib/rules/builtin/reverse-shell-interactive-bash.rule.yaml +12 -0
  216. package/src/hooks/ts/lib/rules/builtin/rm-recursive-parent.rule.yaml +13 -0
  217. package/src/hooks/ts/lib/rules/builtin/rm-recursive-relative.rule.yaml +12 -0
  218. package/src/hooks/ts/lib/rules/builtin/rm-recursive-root.rule.yaml +27 -0
  219. package/src/hooks/ts/lib/rules/builtin/rm-trailing-tilde.rule.yaml +12 -0
  220. package/src/hooks/ts/lib/rules/builtin/sudoers-nopasswd.rule.yaml +15 -0
  221. package/src/hooks/ts/lib/rules/builtin.generated.ts +557 -0
  222. package/src/hooks/ts/lib/rules/eval.ts +76 -0
  223. package/src/hooks/ts/lib/rules/lint.ts +108 -0
  224. package/src/hooks/ts/lib/rules/load.ts +264 -0
  225. package/src/hooks/ts/lib/rules/types.ts +58 -0
  226. package/src/hooks/ts/pre-tool-use/security-validator.ts +35 -96
  227. package/src/hooks/ts/user-prompt/handoff-link.ts +34 -0
  228. package/dist/aibroker-client-BxzeEHPF.mjs +0 -3
  229. package/dist/auto-route-DWYK_0i0.mjs +0 -11
  230. package/dist/budget-DX-VIWGf.mjs +0 -15
  231. package/dist/cockpit-memory-DIrNs4xr.mjs +0 -20
  232. package/dist/daemon-ieo0lnpq.mjs +0 -46
  233. package/dist/detect-C0ZBXqDL.mjs +0 -10
  234. package/dist/factory-DeASMV7Z.mjs +0 -9
  235. package/dist/kill-tree-CXZ0WfVf.mjs +0 -10
  236. package/dist/operator-Bpxq_WB5.mjs +0 -7
  237. package/dist/providers-QJzpaNhX.mjs +0 -16
  238. package/dist/registrable-Drn8wIDO.mjs +0 -9
  239. package/dist/render-C4KR8qQ8.mjs +0 -13
  240. package/dist/render-Cz3BWIBO.mjs +0 -903
  241. package/dist/root-Bfb1zgwE.mjs +0 -36
  242. package/dist/route-table-ZVMNBsy7.mjs +0 -15
  243. package/dist/session-BcnhepLZ.mjs +0 -35
  244. package/dist/session-scan-CqUQdCac.mjs +0 -9
  245. package/dist/status-D_SWmOlH.mjs +0 -6
  246. package/dist/viewer-CbGN2Ri4.mjs +0 -22
  247. package/dist/worker-config-D1KVAMp4.mjs +0 -42
@@ -1,903 +0,0 @@
1
- import { M as readWorkersSection, p as getProviderOrNative, t as ANTHROPIC_NATIVE, v as nativeAnthropicModels } from "./config-CST2uq1v.mjs";
2
- import { _ as ownedTree } from "./worktree-BY--Rgmy.mjs";
3
- import { a as UNLABELED, c as contextFooter, f as elapsedOf, g as isLive, h as isChatPane, k as saveStatus, l as contextLabel, nt as shortText, o as ageOf, u as contextPercent, v as loadStatus, y as loadStatuses } from "./status-COtzewKV.mjs";
4
- import { p as currentWorkerId, u as sessionTag } from "./scope-D8HK6vU7.mjs";
5
- import { A as waitOnCallLine, E as detachedLine, M as waitingLine, d as parseWorkerReport, j as waitOnResultLine, k as realWaitDeps, n as bashExitCode, p as renderReport, t as WAIT_EXIT_STILL_RUNNING } from "./wait-ChpHKJZV.mjs";
6
- import { c as queuedText, f as sessionOf } from "./capacity-CsZMkVyg.mjs";
7
- import { a as endLabel } from "./endreason-BqyyU7BH.mjs";
8
- import { mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
9
- import { basename, join, relative } from "node:path";
10
-
11
- //#region src/workers/tree.ts
12
- /** The worker this process runs inside, when it runs inside one. */
13
- function parentFromEnv(env = process.env) {
14
- const id = currentWorkerId(env);
15
- return typeof id === "string" && id.trim() ? id.trim() : null;
16
- }
17
- /**
18
- * Depth of `id` in the worker forest: 0 for a top-level worker, 1 + the
19
- * parent's depth for a sub-worker. Parents without a status file (chain ids,
20
- * unknown ids) count as roots; a cycle reads as its own depth and is cut off
21
- * after the statuses it walked. `chatIsRoot` stops one level short of the
22
- * chat-pane tracker (origin "chat"): the status line treats the pane as the
23
- * bar itself, so the workers it spawned are its top level.
24
- */
25
- function workerDepth(statuses, id, opts) {
26
- const byId = new Map(statuses.map((s) => [s.id, s]));
27
- let depth = 0;
28
- let cur = byId.get(id);
29
- const seen = new Set([id]);
30
- while (cur?.parent && !seen.has(cur.parent)) {
31
- seen.add(cur.parent);
32
- const next = byId.get(cur.parent);
33
- if (!next) break;
34
- if (!(opts?.chatIsRoot && next.origin === "chat")) depth += 1;
35
- cur = next;
36
- }
37
- return depth;
38
- }
39
- /** Children of `parent` that are still running, oldest first. */
40
- function runningChildren(statuses, parent) {
41
- return statuses.filter((s) => s.parent === parent && isLive(s));
42
- }
43
- /**
44
- * Would starting a child of `parent` stay within the caps? Throws with a
45
- * clear message when it would not; returns silently when `parent` has no
46
- * status file (a chain id or an unknown id — not a worker, not capped).
47
- */
48
- function assertChildAllowed(logDir, parent, caps, statuses = loadStatuses(logDir)) {
49
- if (!statuses.some((s) => s.id === parent)) return;
50
- const depth = workerDepth(statuses, parent);
51
- 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":"…"}'`);
52
- const kids = runningChildren(statuses, parent);
53
- 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`);
54
- }
55
- /**
56
- * The parent a launch should record: an explicit one (chain stage) wins, else
57
- * the worker this process runs inside. Returns null for top-level runs.
58
- */
59
- function launchParent(explicit, env = process.env) {
60
- return explicit ?? parentFromEnv(env);
61
- }
62
- /** Does a status file exist for `id` (i.e. is it a worker rather than a chain)? */
63
- function isWorkerId(logDir, id) {
64
- return loadStatus(logDir, id) !== null;
65
- }
66
- /**
67
- * A worker owns its children's results (the worker contract says so): if
68
- * `parent` exits leaving sub-workers unaccounted for, its own run must not
69
- * report success. Two shapes count, both read off `runningChildren` and the
70
- * raw statuses, never a second live-child lookup:
71
- *
72
- * - still live: left running untouched (killing another process's work
73
- * here would be its own bug) but named in the reason.
74
- * - marked "running" with a dead pid (the child never got to write its own
75
- * terminal state before its parent vanished from under it): finalised
76
- * here as failed, so `ps` stops showing a corpse as live.
77
- *
78
- * Returns the reason string for the parent's own report, or null when every
79
- * child it started is accounted for.
80
- */
81
- function checkOrphanedChildren(logDir, parent) {
82
- const statuses = loadStatuses(logDir);
83
- const live = runningChildren(statuses, parent);
84
- const abandoned = statuses.filter((s) => s.parent === parent && (s.state === "running" || s.state === "queued") && !isLive(s));
85
- for (const child of abandoned) saveStatus(logDir, {
86
- ...child,
87
- state: "failed",
88
- last: `parent ${parent} exited`
89
- });
90
- const ids = [...live, ...abandoned].map((s) => s.id);
91
- if (!ids.length) return null;
92
- return `exited with ${ids.length} sub-worker${ids.length === 1 ? "" : "s"} still running (${ids.join(", ")}); they were not waited for — run children in the foreground or wait for them before finishing`;
93
- }
94
-
95
- //#endregion
96
- //#region src/workers/active-time.ts
97
- /**
98
- * active-time.ts — a worker's deadline counts active time only. Waiting for a
99
- * local slot (capacity gate) and blocked in a build-gate wait (`pai build
100
- * run|lock`, a door with --wait) are not work: the first is added to the
101
- * deadline when the slot is granted, the second is recorded here by the build
102
- * gate (it knows the ticket's worker via PAI_WORKER_ID) in a sidecar file
103
- * <logDir>/waits/<worker>.json = { ms, since }: `ms` closed waits, `since`
104
- * the start of the wait in progress. A model request waiting in the router
105
- * queue counts too: the router mirrors per-worker accounting to
106
- * <logDir>/router-sched.json (routerWaitMs); blockedMsOf sums both.
107
- */
108
- const waitPath = (logDir, id) => join(logDir, "waits", `${id.replace(/[^\w.-]/g, "_")}.json`);
109
- function read(logDir, id) {
110
- try {
111
- const w = JSON.parse(readFileSync(waitPath(logDir, id), "utf8"));
112
- return {
113
- ms: Number(w.ms) || 0,
114
- since: typeof w.since === "number" ? w.since : null
115
- };
116
- } catch {
117
- return {
118
- ms: 0,
119
- since: null
120
- };
121
- }
122
- }
123
- function write(logDir, id, w) {
124
- try {
125
- mkdirSync(join(logDir, "waits"), { recursive: true });
126
- writeFileSync(waitPath(logDir, id), JSON.stringify(w));
127
- } catch {}
128
- }
129
- /** The worker starts waiting on a build gate. */
130
- function beginBuildWait(logDir, id, now = Date.now()) {
131
- const w = read(logDir, id);
132
- if (w.since === null) write(logDir, id, {
133
- ...w,
134
- since: now
135
- });
136
- }
137
- /** The wait ended (slot granted, aborted): fold it into the closed total. */
138
- function endBuildWait(logDir, id, now = Date.now()) {
139
- const w = read(logDir, id);
140
- if (w.since !== null) write(logDir, id, {
141
- ms: w.ms + Math.max(0, now - w.since),
142
- since: null
143
- });
144
- }
145
- /** Total ms the worker spent blocked in build-gate waits, the one in progress included. */
146
- function buildWaitMs(logDir, id, now = Date.now()) {
147
- const w = read(logDir, id);
148
- return w.ms + (w.since === null ? 0 : Math.max(0, now - w.since));
149
- }
150
- /** "active 12 of 60 min, waited 31" for a limited run (waited = slot queue + build-gate + router-queue waits); null without a limit. */
151
- function activeText(s, now) {
152
- if (!s.maxMinutes) return null;
153
- const blocked = s.blockedMs ?? 0;
154
- const startedAt = Date.parse(s.started.replace(" ", "T"));
155
- const active = Number.isFinite(startedAt) ? Math.max(0, now - startedAt - blocked) : 0;
156
- const m = (ms) => Math.floor(ms / 6e4);
157
- return `active ${m(active)} of ${s.maxMinutes} min, waited ${m(blocked + (s.queueWaitedMs ?? 0))}`;
158
- }
159
- const schedCache = /* @__PURE__ */ new Map();
160
- /** The router's per-worker accounting from <logDir>/router-sched.json, re-parsed only when the file changed. */
161
- function routerAcct(logDir, id) {
162
- const p = join(logDir, "router-sched.json");
163
- try {
164
- const mtimeMs = statSync(p).mtimeMs;
165
- let c = schedCache.get(p);
166
- if (!c || c.mtimeMs !== mtimeMs) {
167
- c = {
168
- mtimeMs,
169
- workers: JSON.parse(readFileSync(p, "utf8")).workers ?? {}
170
- };
171
- schedCache.set(p, c);
172
- }
173
- return c.workers[id];
174
- } catch {
175
- return;
176
- }
177
- }
178
- /** Total ms the worker's model requests waited in the router queue, the wait in progress included. */
179
- function routerWaitMs(logDir, id, now = Date.now()) {
180
- const a = routerAcct(logDir, id);
181
- if (!a) return 0;
182
- const open = a.waitingSince ? Math.max(0, now - a.waitingSince) : 0;
183
- return (Number(a.waitMsTotal) || 0) + open;
184
- }
185
- /** Everything that does not count against the deadline: build-gate waits plus router-queue waits. */
186
- function blockedMsOf(logDir, id, now = Date.now()) {
187
- return buildWaitMs(logDir, id, now) + routerWaitMs(logDir, id, now);
188
- }
189
-
190
- //#endregion
191
- //#region src/workers/deadline.ts
192
- /** Slide a minutes-based limit by time spent waiting (queue); an absolute one stays. */
193
- function extendLimit(limit, waitedMs) {
194
- return limit.absolute || waitedMs <= 0 ? limit : {
195
- ...limit,
196
- deadlineAt: limit.deadlineAt + waitedMs
197
- };
198
- }
199
- /** Local HH:MM → epoch ms: today, or tomorrow when already past. */
200
- function parseDeadline(hhmm, now = /* @__PURE__ */ new Date()) {
201
- const m = /^(\d{1,2}):(\d{2})$/.exec(hhmm.trim());
202
- if (!m || Number(m[1]) > 23 || Number(m[2]) > 59) throw new Error(`--deadline: expected HH:MM (local time, 00:00-23:59), got "${hhmm}"`);
203
- const at = new Date(now);
204
- at.setHours(Number(m[1]), Number(m[2]), 0, 0);
205
- if (at.getTime() <= now.getTime()) at.setDate(at.getDate() + 1);
206
- return at.getTime();
207
- }
208
- /** The run's limit, or null when disabled (--max-minutes 0 / default 0). */
209
- function resolveLimit(flags, defaultMinutes, now = /* @__PURE__ */ new Date()) {
210
- if (flags.maxMinutes !== void 0 && flags.deadline !== void 0) throw new Error("--max-minutes and --deadline are mutually exclusive");
211
- if (flags.deadline !== void 0) {
212
- const deadlineAt = parseDeadline(flags.deadline, now);
213
- return {
214
- deadlineAt,
215
- minutes: Math.max(1, Math.round((deadlineAt - now.getTime()) / 6e4)),
216
- absolute: true
217
- };
218
- }
219
- const minutes = flags.maxMinutes ?? defaultMinutes;
220
- if (!Number.isFinite(minutes) || minutes < 0) throw new Error(`--max-minutes: expected a non-negative number, got ${minutes}`);
221
- return minutes === 0 ? null : {
222
- deadlineAt: now.getTime() + minutes * 6e4,
223
- minutes
224
- };
225
- }
226
- const pad = (n) => String(n).padStart(2, "0");
227
- /** The one line injected into the worker's system prompt. */
228
- function deadlinePromptLine(limit) {
229
- const d = new Date(limit.deadlineAt);
230
- return `Hard deadline ${pad(d.getHours())}:${pad(d.getMinutes())} (${limit.minutes} min). Finish and report before it; at the deadline the run is stopped and whatever you have is salvaged.`;
231
- }
232
- /** ps column: "36s left", "12m left", "1h05m left" for running; "OVERDUE ..." for overdue; "timed out" for stopped; null otherwise. */
233
- function deadlineLabel(s, now = Date.now()) {
234
- if (s.timedOut) return "timed out";
235
- if (s.state !== "running" || !s.deadlineAt) return null;
236
- const secs = Math.abs(s.deadlineAt - now) / 1e3;
237
- let label;
238
- if (secs < 60) label = `${Math.floor(secs)}s`;
239
- else if (secs < 3600) label = `${Math.floor(secs / 60)}m`;
240
- else {
241
- const hours = Math.floor(secs / 3600);
242
- const mins = Math.floor(secs % 3600 / 60);
243
- label = `${hours}h${String(mins).padStart(2, "0")}m`;
244
- }
245
- return s.deadlineAt >= now ? `${label} left` : `OVERDUE ${label}`;
246
- }
247
- /** Grace between SIGTERM and SIGKILL. */
248
- const DEADLINE_GRACE_MS = 1e4;
249
- /**
250
- * Stop `proc` at the deadline: `onFire`, SIGTERM, SIGKILL after `graceMs`
251
- * when it is still alive. Returns the disarm function.
252
- */
253
- function armDeadline(proc, limit, onFire, graceMs = DEADLINE_GRACE_MS, blocked = () => 0, now = Date.now) {
254
- let kill = null;
255
- let t;
256
- const left = () => limit.deadlineAt + (limit.absolute ? 0 : blocked()) - now();
257
- const arm = () => {
258
- t = setTimeout(fire, Math.max(0, left()));
259
- };
260
- const fire = () => {
261
- if (left() > 0) return arm();
262
- onFire();
263
- try {
264
- proc.kill("SIGTERM");
265
- } catch {}
266
- kill = setTimeout(() => {
267
- if (proc.exitCode === null && proc.signalCode === null) try {
268
- proc.kill("SIGKILL");
269
- } catch {}
270
- }, graceMs);
271
- kill.unref();
272
- };
273
- arm();
274
- return () => {
275
- clearTimeout(t);
276
- if (kill) clearTimeout(kill);
277
- };
278
- }
279
-
280
- //#endregion
281
- //#region src/workers/denials.ts
282
- /** Entries kept per run; `denialCount` on the status keeps the true total. */
283
- const MAX_DENIALS = 50;
284
- const MIN_SPEC_OVERLAP = 20;
285
- /**
286
- * Claude Code's denial texts: `PreToolUse:<Tool> hook error: [<hook>]: <reason>`
287
- * (also "hook blocking error from command"), the `<prog>: command denied by
288
- * permission rules` shape, and the plain permission-denied sentences.
289
- */
290
- function parseDenialText(text, fallbackTool = "?") {
291
- const pre = text.match(/^PreToolUse:(\S+) hook (?:blocking )?error(?: from command)?:\s*(?:\[([^\]]*)\]:?\s*)?([\s\S]*)/);
292
- if (pre) return {
293
- tool: pre[1],
294
- hook: pre[2] ?? "hook",
295
- reason: pre[3].trim()
296
- };
297
- const rules = text.match(/(?:^|\n)(\S+): command denied by permission rules/);
298
- if (rules) return {
299
- tool: fallbackTool,
300
- hook: rules[1],
301
- reason: text.trim()
302
- };
303
- if (/Permission to use \S+ has been denied|requested permissions to use \S+, but you haven't granted/.test(text)) return {
304
- tool: fallbackTool,
305
- hook: "permission",
306
- reason: text.trim()
307
- };
308
- return null;
309
- }
310
- /** The denied input as one short line: command / file_path / path / pattern / url, else JSON. */
311
- function denialInput(input) {
312
- if (typeof input !== "object" || input === null) return "";
313
- const o = input;
314
- const key = [
315
- "command",
316
- "file_path",
317
- "path",
318
- "pattern",
319
- "url"
320
- ].find((k) => typeof o[k] === "string");
321
- return (key ? String(o[key]) : JSON.stringify(o)).replace(/\s+/g, " ").slice(0, 120);
322
- }
323
- const normalizeReason = (r) => r.toLowerCase().replace(/\d+/g, "#").replace(/\s+/g, " ").trim();
324
- /**
325
- * specConflict: the denial is likely the orchestrator's spec contradicting the
326
- * worker contract, not a worker mistake. True when
327
- * (a) the same (hook, tool, normalized reason) was denied 2+ times in the run, or
328
- * (b) the denied input shares a >= 20 char verbatim substring with the spec/prompt.
329
- * Sets the flag on every entry it matches, so the first occurrence of a
330
- * repeated denial is flagged too. Cheap enough: O(input × spec) on ≤120 chars.
331
- */
332
- function markSpecConflicts(denials, spec) {
333
- const key = (d) => `${d.hook}\0${d.tool}\0${normalizeReason(d.reason)}`;
334
- const seen = /* @__PURE__ */ new Map();
335
- for (const d of denials) seen.set(key(d), (seen.get(key(d)) ?? 0) + 1);
336
- for (const d of denials) d.specConflict = (seen.get(key(d)) ?? 0) >= 2 || sharesSubstring(d.input, spec ?? "");
337
- }
338
- function sharesSubstring(input, spec) {
339
- for (let i = 0; i + MIN_SPEC_OVERLAP <= input.length; i++) if (spec.includes(input.slice(i, i + MIN_SPEC_OVERLAP))) return true;
340
- return false;
341
- }
342
- /**
343
- * Record one tool_result block when it is a denial. `uses` maps tool_use ids to
344
- * the tool name and input. Returns whether the status changed.
345
- */
346
- function recordDenial(status, block, text, uses, spec, at) {
347
- if (block.is_error !== true) return false;
348
- const use = block.tool_use_id ? uses.get(block.tool_use_id) : void 0;
349
- const p = parseDenialText(text, use?.name ?? "?");
350
- if (!p) return false;
351
- const list = status.denials ??= [];
352
- status.denialCount = (status.denialCount ?? 0) + 1;
353
- if (list.length < MAX_DENIALS) list.push({
354
- at,
355
- tool: use?.name ?? p.tool,
356
- hook: p.hook,
357
- input: denialInput(use?.input),
358
- reason: p.reason.slice(0, 200),
359
- specConflict: false
360
- });
361
- markSpecConflicts(list, spec);
362
- return true;
363
- }
364
- /** `denied N` plus `spec?` when any entry looks like a spec conflict; "" when none. */
365
- function denialMark(s) {
366
- const n = s.denialCount ?? s.denials?.length ?? 0;
367
- if (n <= 0) return "";
368
- return `denied ${n}${s.denials?.some((d) => d.specConflict) ? " spec?" : ""}`;
369
- }
370
- /** Replay lines: one per recorded denial, `!` marking a likely spec conflict. */
371
- function denialLines(s) {
372
- const list = s.denials ?? [];
373
- if (!list.length) return [];
374
- const more = (s.denialCount ?? list.length) - list.length;
375
- return [
376
- ` ${denialMark(s)}:`,
377
- ...list.map((d) => ` ${d.specConflict ? "!" : " "} ${d.at.slice(11, 19)} ${d.tool} [${d.hook}] ${d.input} — ${d.reason}`),
378
- ...more > 0 ? [` … ${more} more`] : []
379
- ];
380
- }
381
-
382
- //#endregion
383
- //#region src/workers/render.ts
384
- /**
385
- * render.ts — turn worker events into the lines a human reads.
386
- *
387
- * Ports the glm-ps transcript rendering: the gutter rules (dim `>` for reads
388
- * and searches, `$` for shell, magenta `~` + red/green diff for edits, green
389
- * `+` for writes), Read-result trimming (first 3 lines then a count), and the
390
- * result footer. Colors are applied only when the output is a TTY, so MCP
391
- * tool output stays plain text.
392
- */
393
- const CODES = {
394
- dim: "2",
395
- bold: "1",
396
- red: "31",
397
- green: "32",
398
- yellow: "33",
399
- blue: "34",
400
- mag: "35",
401
- cyan: "36"
402
- };
403
- function makeColor(enabled) {
404
- return (name, s) => enabled ? `\x1b[${CODES[name]}m${s}\x1b[0m` : s;
405
- }
406
- /** Path relative to cwd when it lies inside it, otherwise unchanged. */
407
- function relPath(path, cwd) {
408
- if (!path || !cwd) return path;
409
- let r;
410
- try {
411
- r = relative(cwd, path);
412
- } catch {
413
- return path;
414
- }
415
- return r.startsWith("..") ? path : r;
416
- }
417
- /** Minimal line diff for Edit previews: common prefix/suffix, one hunk. */
418
- function unifiedDiffLines(oldStr, newStr) {
419
- const oldL = oldStr.split("\n");
420
- const newL = newStr.split("\n");
421
- let start = 0;
422
- while (start < oldL.length && start < newL.length && oldL[start] === newL[start]) start++;
423
- let endOld = oldL.length;
424
- let endNew = newL.length;
425
- while (endOld > start && endNew > start && oldL[endOld - 1] === newL[endNew - 1]) {
426
- endOld--;
427
- endNew--;
428
- }
429
- const out = [];
430
- for (let i = start; i < endOld; i++) out.push("-" + oldL[i]);
431
- for (let i = start; i < endNew; i++) out.push("+" + newL[i]);
432
- return out;
433
- }
434
- /**
435
- * `HH:MM:SS` at an offset east of UTC in minutes (Date.getTimezoneOffset()
436
- * negated), from any ISO stamp the runner wrote — `Z` or a local `+HH:MM`.
437
- * null when the stamp cannot be parsed. Offsets make this testable without
438
- * depending on the machine's zone; the default is this machine's.
439
- */
440
- function clockOf(ts, offMin = -(/* @__PURE__ */ new Date()).getTimezoneOffset()) {
441
- const t = Date.parse(ts);
442
- if (Number.isNaN(t)) return null;
443
- return new Date(t + offMin * 6e4).toISOString().slice(11, 19);
444
- }
445
- /** `YYYY-MM-DD` at the same offset — the local day a date separator shows. */
446
- function dayOf(ts, offMin = -(/* @__PURE__ */ new Date()).getTimezoneOffset()) {
447
- const t = Date.parse(ts);
448
- if (Number.isNaN(t)) return null;
449
- return new Date(t + offMin * 6e4).toISOString().slice(0, 10);
450
- }
451
- /**
452
- * The transcript gutter (2g): `HH:MM:SS │ ` from the event's `_ts`, dim, with
453
- * the worker tag in front when several run at once. Continuation lines get
454
- * blanks of the same width so wrapped text stays aligned; when the viewer
455
- * wraps lines itself, continuation rows carry the `│` bar instead (barCont)
456
- * so the bar runs unbroken down the pane. null when the event carries no
457
- * stamp (logs from before 2g render with the plain prefix). The time is the
458
- * stamp's wall clock at `offMin` — the default renders local time, whatever
459
- * zone stamped the log (old logs were stamped in UTC).
460
- */
461
- function gutterFor(c, e, tag, offMin) {
462
- if (!e._ts) return null;
463
- const time = clockOf(e._ts, offMin) ?? (e._ts.length >= 19 ? e._ts.slice(11, 19) : e._ts);
464
- const head = tag ? `${tag} ${time}` : time;
465
- const width = head.length + 3;
466
- return {
467
- first: c("dim", `${head} │ `),
468
- cont: " ".repeat(width),
469
- barCont: c("dim", `${" ".repeat(head.length)} │ `),
470
- width
471
- };
472
- }
473
- /**
474
- * The ticker's tool part: `$ <command>` for Bash (first 60 chars), the file
475
- * basename for the file tools, the bare name for everything else.
476
- */
477
- function tickerTool(name, inp) {
478
- const i = typeof inp === "object" && inp !== null ? inp : {};
479
- const get = (k) => typeof i[k] === "string" ? i[k] : "";
480
- if (name === "Bash") return `$ ${shortText(get("command").replace(/\s+/g, " ").trim(), 60)}`;
481
- if (name === "Read" || name === "Edit" || name === "Write" || name === "MultiEdit") return (get("file_path").split("/").pop() ?? "") || name;
482
- return name;
483
- }
484
- /**
485
- * The liveness line: `⋯ 12s · run tests before the fix · $ bun run test` —
486
- * seconds since the last *rendered* event, the worker's last stated intent
487
- * (its last assistant text, ≤60 chars) and the tool it is currently running.
488
- * Parts that are empty drop out.
489
- */
490
- function tickerText(secs, intent, tool, meter) {
491
- const head = `⋯ ${secs}s`;
492
- const tail = [intent, tool].map((p) => p.trim()).filter(Boolean).join(" · ");
493
- const line = tail ? `${head} · ${tail}` : head;
494
- return meter ? `${line} · ${meter}` : line;
495
- }
496
- /** The queue wording of a waiting run: the full line when the gate recorded it, the older total/cap form otherwise. */
497
- function capacityText(cap) {
498
- return cap.queued !== void 0 && cap.sinceMs !== void 0 ? queuedText({
499
- provider: cap.provider,
500
- position: cap.position,
501
- queued: cap.queued,
502
- behind: cap.behind,
503
- sinceMs: cap.sinceMs
504
- }) : `queued: ${cap.total} on ${cap.provider}, cap ${cap.cap} (position ${cap.position})`;
505
- }
506
- /**
507
- * One blank line between turns, none inside one: a blank goes before an
508
- * assistant message that follows a tool result or an operator message (the
509
- * worker starting to speak again after its tools were answered / it was told
510
- * something), not between the text, tool calls and results of one turn.
511
- */
512
- function blankBetween(prev, e) {
513
- if (!prev) return false;
514
- if (e.type !== "assistant") return false;
515
- return prev.type === "user" || prev.type === "operator" || prev.type === "handoff";
516
- }
517
- /** The worker's last stated intent: the first line of its last text, ≤60. */
518
- function intentOf(text) {
519
- return shortText((text.trim().split("\n").find((l) => l.trim()) ?? "").trim().replace(/\s+/g, " "), 60);
520
- }
521
- /**
522
- * The context meter `ctx 84k/200k (42%)` (contextLabel, shared with the
523
- * chat status row), yellow from 70 %, red from 85 %. null when the numbers
524
- * are missing or below `minPct` (the table only shows it past 60; the pane
525
- * liveness line always shows it).
526
- */
527
- function contextMeter(c, s, minPct = 0) {
528
- const pct = contextPercent(s);
529
- if (pct === null || pct <= minPct) return null;
530
- const label = contextLabel(s);
531
- if (pct > 85) return c("red", label);
532
- if (pct > 70) return c("yellow", label);
533
- return label;
534
- }
535
- /** Lines of a multi-line text, trimmed of blank edges; never capped (the pane wraps rows itself). */
536
- function fullLines(text) {
537
- const lines = text.replace(/\s+$/, "").split("\n").map((l) => l.replace(/\s+$/, ""));
538
- while (lines.length && lines[0] === "") lines.shift();
539
- return lines;
540
- }
541
- function renderToolUse(c, prefix, name, inp, cwd) {
542
- const i = typeof inp === "object" && inp !== null ? inp : {};
543
- const get = (k) => typeof i[k] === "string" ? i[k] : "";
544
- if (name === "Read") return [`${prefix}${c("dim", ">")} Reading ${relPath(get("file_path"), cwd)}`];
545
- if (name === "Grep" || name === "Glob") return [`${prefix}${c("dim", ">")} Searching ${c("cyan", get("pattern"))} in ${relPath(get("path") || ".", cwd)}`];
546
- if (name === "Bash") {
547
- const plain = waitOnCallLine(get("command")) ?? detachedLine(get("command"));
548
- if (plain) return [`${prefix}${c("yellow", plain)}`];
549
- return fullLines(get("command")).map((ln, k) => k === 0 ? `${prefix}${c("dim", "$")} ${ln}` : `${prefix} ${ln}`);
550
- }
551
- if (name === "Edit") {
552
- const out = [`${prefix}${c("mag", "~")} Editing ${relPath(get("file_path"), cwd)}`];
553
- for (const line of unifiedDiffLines(get("old_string"), get("new_string"))) if (line.startsWith("-")) out.push(`${prefix} ${c("red", line)}`);
554
- else if (line.startsWith("+")) out.push(`${prefix} ${c("green", line)}`);
555
- else out.push(`${prefix} ${c("dim", line)}`);
556
- return out;
557
- }
558
- if (name === "Write") {
559
- const n = get("content").split("\n").length;
560
- return [`${prefix}${c("green", "+")} Writing ${relPath(get("file_path"), cwd)} (${n} lines)`];
561
- }
562
- if (name === "WebSearch" || name === "WebFetch") return [`${prefix}${c("dim", ">")} ${name} ${shortText(get("query") || get("url"), 100)}`];
563
- return [`${prefix}${c("dim", ">")} ${name} ${shortText(JSON.stringify(i), 120)}`];
564
- }
565
- /** Render one stream-json event; `tools` maps tool_use_id → tool name. */
566
- function renderEvent(c, prefix, e, cwd, tools, gutter) {
567
- const out = [];
568
- if (e.type === "system" && e.subtype === "init") out.push(`${prefix}${c("dim", `worker started · model ${e.model ?? "?"} · cwd ${basename(e.cwd || cwd || "")}`)}`);
569
- else if (e.type === "operator") {
570
- if (e.handoff) return out;
571
- for (const ln of String(e.text ?? "").split("\n")) out.push(`${prefix}${c("cyan", "» " + ln)}`);
572
- } else if (e.type === "worker-stopped") out.push(`${prefix}${c("red", `■ stopped by ${e.killed_by ?? "?"}${e.forced ? " (forced)" : ""}: ${e.reason ?? "no reason given"}`)}`);
573
- else if (e.type === "handoff") for (const ln of String(e.text ?? "").split("\n")) out.push(`${prefix}${c("mag", `◆ from ${e.from ?? "?"} · ${e.kind ?? "?"}: ${ln}`)}`);
574
- else if (e.type === "assistant") {
575
- for (const b of e.message?.content ?? []) if (b.type === "text" && (b.text ?? "").trim()) for (const ln of (b.text ?? "").trim().split("\n")) out.push(`${prefix}${ln}`);
576
- else if (b.type === "tool_use") out.push(...renderToolUse(c, prefix, b.name ?? "?", b.input, cwd));
577
- } else if (e.type === "user") for (const b of e.message?.content ?? []) {
578
- if (b.type !== "tool_result") continue;
579
- let content = b.content ?? "";
580
- if (Array.isArray(content)) content = content.map((x) => typeof x === "object" && x !== null && "text" in x ? String(x.text ?? "") : "").join("\n");
581
- const text = String(content);
582
- const isError = b.is_error === true;
583
- const waited = !isError && tools[b.tool_use_id ?? ""] === "Bash:wait-on" ? waitOnResultLine(text) : null;
584
- if (waited) out.push(`${prefix}${c("yellow", waited)}`);
585
- else if (!isError && tools[b.tool_use_id ?? ""] === "Read") {
586
- const lines = text.split("\n");
587
- for (const ln of lines.slice(0, 3)) out.push(`${prefix}${c("dim", ln)}`);
588
- if (lines.length > 3) out.push(`${prefix}${c("dim", ` … ${lines.length} lines`)}`);
589
- } else if (isError && tools[b.tool_use_id ?? ""] === "Bash:wait" && bashExitCode(text) === WAIT_EXIT_STILL_RUNNING) out.push(`${prefix}${c("yellow", "… still running, waiting again")}`);
590
- else if (isError) {
591
- const denied = parseDenialText(text) !== null || / blocked:/.test(text);
592
- fullLines(text).forEach((ln, k) => out.push(`${prefix} ${c(denied ? "yellow" : "red", k === 0 ? "! " + ln : " " + ln)}`));
593
- } else if (text.trim()) for (const ln of fullLines(text)) out.push(`${prefix} ${c("dim", ln)}`);
594
- }
595
- else if (e.type === "result") {
596
- const mark = !e.is_error ? c("green", "✓ done") : c("red", "✗ failed");
597
- out.push(`${prefix}${mark} · ${e.num_turns ?? "?"} turns · ${Math.floor((e.duration_ms ?? 0) / 1e3)}s`);
598
- const report = parseWorkerReport(String(e.result ?? ""));
599
- if (report) out.push(...renderReport(c, prefix, report, cwd));
600
- else for (const ln of String(e.result ?? "").trim().split("\n")) out.push(`${prefix} ${ln}`);
601
- }
602
- if (!gutter) return out;
603
- return out.map((ln, i) => (i === 0 ? gutter.first : gutter.cont) + ln);
604
- }
605
- /** Transcript header: id, provider, label, session name, project dir. */
606
- function headerLine(c, s) {
607
- const bits = [s.provider ? `[${s.provider}]` : "", sessionTag(s)].filter(Boolean).join(" ");
608
- const sep = bits ? ` ${bits}` : "";
609
- const specBit = s.spec ? ` spec: ${s.spec}` : "";
610
- const reportBit = s.reportValid === false ? " " + c("red", s.reportFormat === "ag2" && (s.reportErrors ?? []).length ? "R!" : "R?") : "";
611
- const why = endLabel(s);
612
- const endBit = why ? ` ended: ${why}` : "";
613
- return c("bold", `━━ ${s.id}${sep} ${s.label} (${basename(s.cwd)})${specBit}${reportBit}${endBit}`);
614
- }
615
- /** The chain label behind a stage label: strip the trailing " · <stage>". */
616
- function chainLabelOf(stages) {
617
- const first = stages[0];
618
- if (!first) return "";
619
- const suffix = first.stage ? ` · ${first.stage}` : "";
620
- return first.label.endsWith(suffix) && suffix ? first.label.slice(0, first.label.length - suffix.length) : first.label;
621
- }
622
- /**
623
- * The ps table (RUNNING + FINISHED last 8) as a forest: a worker whose parent
624
- * is another worker renders indented under it (`├`/`└` connectors); a parent
625
- * that is not a worker is a chain id and gets its old `chain <id>` header.
626
- * `⎇` marks an unmerged worktree branch, `◆N` an inbox with N handoffs.
627
- */
628
- /** Delivery state of the latest note: pending until the worker's next tool call, then the delivery time. */
629
- function noteMark(s) {
630
- if (s.notePending?.length) return " [note pending]";
631
- if (s.noteDeliveredAt) return ` [note delivered ${s.noteDeliveredAt.slice(11, 16)}]`;
632
- return "";
633
- }
634
- function renderTable(c, statuses, scopeLabel, now = /* @__PURE__ */ new Date(), inbox = {}, workers = safeWorkers()) {
635
- const provTag = (s) => providerTag(s, workers);
636
- const queued = [];
637
- const running = [];
638
- const done = [];
639
- for (const s of statuses) if (s.state === "queued" && isLive(s)) queued.push(s);
640
- else if (isLive(s)) running.push(s);
641
- else {
642
- if (s.state === "running") s.state = "lost";
643
- done.push(s);
644
- }
645
- const qKey = (s) => [s.capacity?.provider ?? s.provider, s.capacity?.position ?? 1e9];
646
- queued.sort((a, b) => {
647
- const [pa, na] = qKey(a);
648
- const [pb, nb] = qKey(b);
649
- if (pa !== pb) return pa < pb ? -1 : 1;
650
- if (na !== nb) return na - nb;
651
- return a.id < b.id ? -1 : 1;
652
- });
653
- const isWorker = (id) => statuses.some((s) => s.id === id);
654
- const chainOf = (s) => s.parent && !isWorker(s.parent) ? s.parent : null;
655
- const treeLine = (line, chain, last) => {
656
- if (chain === null) return line;
657
- const mark = last ? "└" : "├";
658
- const bar = last ? " " : "│";
659
- return line.startsWith(" ") ? ` ${bar} ${line.slice(6)}` : ` ${mark} ${line.slice(2)}`;
660
- };
661
- const rowPrefix = (depth, last) => depth <= 0 ? " " : " " + "│ ".repeat(depth - 1) + (last ? "└ " : "├ ");
662
- const rowCont = (depth, last) => depth <= 0 ? " " : " " + "│ ".repeat(depth - 1) + (last ? " " : "│ ");
663
- const branchMark = (s) => s.branch && !s.merged ? " " + c("yellow", "⎇" + (s.commits ? String(s.commits) : "")) : "";
664
- const treeMark = (s) => {
665
- const t = ownedTree(s);
666
- const parts = [
667
- ...t ? [`tree ${basename(t.dir)}`] : [],
668
- ...s.pool?.slot ? [`slot ${basename(s.pool.slot)} #${s.pool.task}${s.pool.warm ? ` ${s.pool.warm}` : ""}`] : [],
669
- ...s.resumedFrom ? [`resumed from ${s.resumedFrom}`] : [],
670
- ...s.resumedBy ? [`resumed by ${s.resumedBy}`] : [],
671
- ...s.route ? [`route ${s.route.reason}`] : [],
672
- ...s.switchReason ? [s.switchReason] : [],
673
- ...s.fallThroughOf ? [`fall-through of ${s.fallThroughOf}`] : [],
674
- ...s.fallThroughTo ? [`fell through to ${s.fallThroughTo}`] : [],
675
- ...s.escalated ? [s.escalated] : [],
676
- ...s.escalatedTo ? [`escalated -> ${s.escalatedTo}`] : []
677
- ];
678
- return parts.length ? " " + c("dim", parts.join(" ")) : "";
679
- };
680
- const inboxMark = (s) => inbox[s.id] ? " " + c("mag", `◆${inbox[s.id]}`) : "";
681
- const reportMark = (s) => s.reportValid === false ? " " + c("red", s.reportFormat === "ag2" && (s.reportErrors ?? []).length ? "R!" : "R?") : "";
682
- const deniedMark = (s) => {
683
- const m = denialMark(s);
684
- return m ? " " + c("red", m) : "";
685
- };
686
- const timeMark = (s) => {
687
- const l = deadlineLabel(s, now.getTime());
688
- const a = s.state === "running" ? activeText(s, now.getTime()) : null;
689
- return l ? " " + c(l === "timed out" || l.startsWith("OVERDUE") ? "red" : "dim", l + (a ? ` (${a})` : "")) : "";
690
- };
691
- const lines = [c("bold", `Workers ${`${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}:${String(now.getSeconds()).padStart(2, "0")}`}`), ""];
692
- if (queued.length) {
693
- lines.push(c("bold", `QUEUED (${queued.length})`));
694
- for (const s of queued) {
695
- const cap = s.capacity;
696
- const pos = cap ? `position ${cap.position} of ${cap.total}, cap ${cap.cap}` : `waiting for a ${s.provider} slot`;
697
- lines.push(` ${c("cyan", s.id)} ${provTag(s)} ${c("yellow", pos)} waited ${elapsedOf(s.started, now)}`);
698
- lines.push(` task: ${s.label} ${basename(s.cwd)}`);
699
- }
700
- const provs = new Set(queued.map((s) => s.provider));
701
- const per = /* @__PURE__ */ new Map();
702
- for (const s of statuses) {
703
- if (!provs.has(s.provider) || s.state !== "running" && s.state !== "queued") continue;
704
- const k = sessionOf(s);
705
- const e = per.get(k) ?? {
706
- name: s.sessionKey || s.session?.name || k.slice(0, 8),
707
- run: 0,
708
- wait: 0
709
- };
710
- if (s.state === "queued") e.wait++;
711
- else e.run++;
712
- per.set(k, e);
713
- }
714
- for (const e of per.values()) lines.push(c("dim", ` session ${e.name}: ${e.run} running, ${e.wait} waiting`));
715
- lines.push("");
716
- }
717
- lines.push(c("bold", `RUNNING (${running.length})`));
718
- if (!running.length) lines.push(" none");
719
- for (let i = 0; i < running.length; i++) {
720
- const s = running[i];
721
- const chain = chainOf(s);
722
- if (chain) {
723
- const prev = running[i - 1];
724
- if (!prev || chainOf(prev) !== chain) {
725
- const stages = running.filter((x) => chainOf(x) === chain);
726
- lines.push(` ${c("bold", `chain ${chain}`)} ${chainLabelOf(stages)}`);
727
- }
728
- }
729
- const last = !!chain && (i + 1 >= running.length || chainOf(running[i + 1]) !== chain);
730
- const depth = workerDepth(statuses, s.id);
731
- const subLast = !chain && (i + 1 >= running.length || running[i + 1].parent !== s.parent);
732
- const meter = contextMeter(c, s, 60);
733
- const p = chain ? " " : rowPrefix(depth, subLast);
734
- const q = chain ? " " : rowCont(depth, subLast);
735
- const tag = sessionTag(s);
736
- lines.push(treeLine(`${p}${c("cyan", s.id)}${inboxMark(s)} ${provTag(s)}${tag ? " " + tag : ""} ${ageOf(s.started, now).padStart(4)} old turns ${String(s.turns).padStart(2)} tools ${String(s.tools).padStart(2)} ${basename(s.cwd)}${branchMark(s)}${treeMark(s)}${timeMark(s)}${deniedMark(s)}${meter ? " " + meter : ""}`, chain, last));
737
- lines.push(treeLine(`${q}task: ${s.label}`, chain, last));
738
- if (s.note) lines.push(treeLine(`${q}${c("dim", `note: ${s.note.replace(/\s+/g, " ").slice(0, 100)}${noteMark(s)}`)}`, chain, last));
739
- lines.push(treeLine(s.waitingOn ? `${q}${c("yellow", waitingLine(s.waitingOn, now.getTime()))}` : `${q}now: ${c("yellow", s.last)} (${ageOf(s.updated, now)} ago)`, chain, last));
740
- }
741
- lines.push("");
742
- lines.push(c("bold", "FINISHED (last 8)"));
743
- const doneSlice = done.slice(-8);
744
- for (let i = 0; i < doneSlice.length; i++) {
745
- const s = doneSlice[i];
746
- const chain = chainOf(s);
747
- if (chain) {
748
- const prev = doneSlice[i - 1];
749
- if (!prev || chainOf(prev) !== chain) {
750
- const stages = doneSlice.filter((x) => chainOf(x) === chain);
751
- lines.push(` ${c("bold", `chain ${chain}`)} ${chainLabelOf(stages)}`);
752
- }
753
- }
754
- const last = !!chain && (i + 1 >= doneSlice.length || chainOf(doneSlice[i + 1]) !== chain);
755
- const depth = workerDepth(statuses, s.id);
756
- const subLast = !chain && (i + 1 >= doneSlice.length || doneSlice[i + 1].parent !== s.parent);
757
- const col = s.state === "done" ? "green" : s.state === "lost" ? "yellow" : "red";
758
- const tag = sessionTag(s);
759
- lines.push(treeLine(`${chain ? " " : rowPrefix(depth, subLast)}${s.id}${inboxMark(s)} ${provTag(s)}${tag ? " " + tag : ""} ${c(col, (s.state === "lost" ? "? lost" : 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)}${branchMark(s)}${treeMark(s)}${timeMark(s)}${deniedMark(s)}${reportMark(s)}${endLabel(s) ? " " + c("dim", `[${endLabel(s)}]`) : ""} ${s.label}`, chain, last));
760
- }
761
- lines.push("");
762
- lines.push(c("dim", "worker follow live transcript of running workers"));
763
- lines.push(c("dim", "worker <id> replay one worker"));
764
- lines.push(c("dim", scopeLabel));
765
- return lines.join("\n");
766
- }
767
- /**
768
- * The one place a model id is compacted for display, so every renderer
769
- * shortens identically: the vendor prefix goes (the provider column already
770
- * says who serves it), a release-date suffix goes, version segments read as
771
- * dots, and a `[1m]` window marker is kept because it changes what the run
772
- * costs. `claude-opus-5[1m]` → `opus-5[1m]`, `claude-haiku-4-5-20251001` →
773
- * `haiku-4.5`, `glm-5.3[1m]` and `k3[1m]` unchanged. Empty in, empty out.
774
- */
775
- function safeWorkers() {
776
- try {
777
- return readWorkersSection().workers;
778
- } catch {
779
- return {
780
- providers: {},
781
- nativeModels: nativeAnthropicModels()
782
- };
783
- }
784
- }
785
- /** `[provider]`, or `[provider/model]` when the served model is not the
786
- * provider's default (e.g. the router picked another). */
787
- function providerTag(s, workers) {
788
- const model = shortModel(s.model);
789
- const dflt = getProviderOrNative(workers, s.provider)?.models.default;
790
- if (!model || dflt && shortModel(dflt) === model) return `[${s.provider}]`;
791
- return `[${s.provider}/${model}]`;
792
- }
793
- function shortModel(model) {
794
- const raw = String(model ?? "").trim();
795
- if (!raw) return "";
796
- const wide = /\[1m\]$/i.test(raw);
797
- let id = wide ? raw.slice(0, -4) : raw;
798
- id = id.replace(/^(?:us\.|eu\.|apac\.)?(?:anthropic|claude|openai|google|models)[./-]/i, "");
799
- id = id.replace(/-\d{8}$/, "");
800
- const parts = id.split("-");
801
- const nums = [];
802
- while (parts.length > 1 && /^\d+(?:\.\d+)*$/.test(parts[parts.length - 1])) nums.unshift(parts.pop());
803
- if (nums.length) id = `${parts.join("-")}-${nums.join(".")}`;
804
- return wide ? `${id}[1m]` : id;
805
- }
806
- /** `waiting (warm-up) 7s/12s` for a run held in the stagger gate, else null. */
807
- function warmupText(w) {
808
- return w ? `waiting (warm-up) ${Math.max(0, Math.floor(w.elapsedSec))}s/${Math.floor(w.limitSec)}s` : null;
809
- }
810
- /**
811
- * The goal of a statusline row: what the worker is FOR, never what it is doing
812
- * this second. The operator's `--label` is the goal and is preferred over
813
- * anything the model wrote; an unlabelled run's label already holds its prompt
814
- * (see run.ts), so the same path shortens that to its first sentence, cut on a
815
- * word boundary. A run with neither label nor prompt reads "unlabeled". Width
816
- * fitting goes through shortText, the repo's one truncation helper.
817
- */
818
- function goalOf(s, max = 40) {
819
- const raw = String(s.label ?? "").trim();
820
- if (!raw || raw === "(no prompt)") return UNLABELED;
821
- const first = raw.split(/(?<=[.!?])\s+/)[0] ?? raw;
822
- if (first.length <= max) return shortText(first, max);
823
- return shortText(`${first.slice(0, max - 1).replace(/\s+\S*$/, "") || first.slice(0, max - 1)}…`, max);
824
- }
825
- /**
826
- * A worker pane's bottom line: `<goal> · <model> · started HH:MM · <age>` —
827
- * what the pane is FOR and how long it has run, never the tool call it
828
- * happens to be executing this second (that is `pai worker ps` / `follow`
829
- * territory). `columns`, when known, shrinks the goal so the fixed
830
- * `model · started · age` tail always survives — trimming that tail instead
831
- * would defeat the one thing an operator scanning several panes needs at a
832
- * glance. A legacy status with no recorded model omits that field rather
833
- * than rendering a bare gap.
834
- */
835
- function paneStatusRow(s, now = /* @__PURE__ */ new Date(), columns = null) {
836
- const model = [shortModel(s.model), contextFooter(s)].filter(Boolean).join(" · ");
837
- const started = String(s.started ?? "").slice(11, 16);
838
- const elapsed = elapsedOf(s.started, now);
839
- const tail = [
840
- model,
841
- started ? `started ${started}` : "",
842
- elapsed,
843
- denialMark(s)
844
- ].filter(Boolean).join(" · ");
845
- const budget = columns !== null ? Math.max(1, columns - tail.length - 3) : 40;
846
- const cap = s.capacity;
847
- return `${(cap ? capacityText(cap) : null) ?? warmupText(s.warmup) ?? (s.waitingOn ? waitingLine(s.waitingOn, now.getTime(), realWaitDeps, Math.max(10, budget - 30)) : goalOf(s, budget))} · ${tail}`;
848
- }
849
- /**
850
- * The statusline bar: `▶N (M nested) · <providers> · oldest <age>` and today's
851
- * ✓/✗ tail. `pai worker ps` has the full table and each pane its own bottom
852
- * line, so the bar only answers how many workers are running, on which
853
- * providers, and how long the oldest has been going. `▶N` counts every live
854
- * worker (the whole tree); `(M nested)` says how many of them were spawned by
855
- * another worker rather than by a session. `<providers>` lists the provider
856
- * names `pai worker providers` shows, most-populous first (ties by name), each
857
- * suffixed `×N` past one; native Anthropic runs show their short model name
858
- * (`sonnet-5.5`) since "anthropic" alone says nothing. No active-provider
859
- * head: it read as "workers run on glm" when none did, and the bar already
860
- * shows the session's provider elsewhere. `columns`, when known, shrinks only
861
- * the providers segment.
862
- */
863
- function renderStatusLine(mine, now = /* @__PURE__ */ new Date(), columns = null) {
864
- const running = mine.filter((s) => !isChatPane(s) && isLive(s));
865
- const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
866
- const doneToday = mine.filter((s) => s.state !== "running" && s.state !== "queued" && s.started.startsWith(today));
867
- const ok = doneToday.filter((s) => s.state === "done").length;
868
- const bad = doneToday.length - ok;
869
- const tail = doneToday.length ? ` ✓${ok} ✗${bad} today` : "";
870
- if (!running.length) return tail.trim();
871
- const ids = new Set(mine.map((s) => s.id));
872
- const nested = running.filter((s) => s.parent && ids.has(s.parent)).length;
873
- const head = `▶${running.length}${nested ? ` (${nested} nested)` : ""}`;
874
- const counts = /* @__PURE__ */ new Map();
875
- for (const s of running) {
876
- const name = s.provider === ANTHROPIC_NATIVE ? shortModel(s.model) || s.provider : s.provider || "?";
877
- counts.set(name, (counts.get(name) ?? 0) + 1);
878
- }
879
- const providersFull = [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([m, n]) => n > 1 ? `${m} ×${n}` : m).join(" · ");
880
- const oldestPart = `oldest ${ageOf(running.reduce((a, b) => a.started < b.started ? a : b).started, now)}`;
881
- let providers = providersFull;
882
- const fixedLen = head.length + 3 + oldestPart.length + tail.length;
883
- if (columns !== null && fixedLen + 3 + providersFull.length > columns) {
884
- const budget = columns - fixedLen - 3;
885
- providers = budget > 0 ? shortText(providersFull, budget) : "";
886
- }
887
- return `${head} · ${[providers, oldestPart].filter(Boolean).join(" · ")}${tail}`;
888
- }
889
- /** The bottom line of a `pai pane run` pane: label · started HH:MM · elapsed. */
890
- function paneRunStatusRow(label, startedMs, now = /* @__PURE__ */ new Date()) {
891
- const d = new Date(startedMs);
892
- const hhmm = `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
893
- const ts = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}T${String(d.getHours()).padStart(2, "0")}:${hhmm.slice(3)}:${String(d.getSeconds()).padStart(2, "0")}`;
894
- return [
895
- goalOf({ label }),
896
- `started ${hhmm}`,
897
- elapsedOf(ts, now)
898
- ].join(" · ");
899
- }
900
-
901
- //#endregion
902
- export { resolveLimit as A, denialLines as C, deadlineLabel as D, armDeadline as E, checkOrphanedChildren as F, isWorkerId as I, launchParent as L, blockedMsOf as M, endBuildWait as N, deadlinePromptLine as O, assertChildAllowed as P, parentFromEnv as R, warmupText as S, recordDenial as T, renderTable as _, fullLines as a, tickerTool as b, headerLine as c, paneRunStatusRow as d, paneStatusRow as f, renderStatusLine as g, renderEvent as h, dayOf as i, beginBuildWait as j, extendLimit as k, intentOf as l, relPath as m, clockOf as n, goalOf as o, providerTag as p, contextMeter as r, gutterFor as s, blankBetween as t, makeColor as u, shortModel as v, parseDenialText as w, unifiedDiffLines as x, tickerText as y };
903
- //# sourceMappingURL=render-Cz3BWIBO.mjs.map