@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,69 +1,69 @@
1
1
  #!/usr/bin/env node
2
2
  import { _ as warn$1, a as fmtDate, c as ok$1, d as scaffoldProjectDirs, f as shortenPath, g as stripAnsi, h as smartDecodeDir, i as err, l as renderTable$1, m as slugify, n as dim$1, o as header, r as encodeDir, s as now, t as bold$1, u as resolvePath } from "../utils-J2b9y8PC.mjs";
3
- import { _ as resolvePaiFile, a as paiCacheDir, c as PaiFileMigrationError, d as defaultPaiHomeDir, f as legacyConfigDir, g as paiHomePath, h as paiHomeDir, l as claudeHomeDir, m as migratePaiFile, o as paiSocketPath, p as migratePaiDir, r as daemonLogPath, s as schedulerLogPath, u as claudeProjectsDir } from "../runtime-paths-WsO9sJFc.mjs";
4
- import { a as slugify$1, i as parseSessionFilename } from "../migrate-DrA_OwlZ.mjs";
5
- import { Et as appendLedger, M as readWorkersSection, R as LOOPBACK_HOST, _t as DEFAULT_CONTEXT_WINDOW, bt as stripModelVariant, c as WorkersConfigError, et as relocateWorkersYaml, f as escalationCloud, m as isLocalProvider, p as getProviderOrNative, rt as workersYamlPath, t as ANTHROPIC_NATIVE, v as nativeAnthropicModels, yt as contextWindowFromModelId } from "../config-CST2uq1v.mjs";
6
- import { A as DEFAULT_PG_CONNECTION_TIMEOUT_MS, C as migrateMainConfigToYaml, F as quoteIdent, M as isContainerTarget, N as postgresConnectionString, O as readJsonStrict, P as postgresFullTarget, S as MainConfigError, T as resolvedMainConfigPath, c as loadConfig, d as paiConfigYamlFilePath, f as readMainConfigRaw, j as assertPgName, k as writeJsonAtomic, l as migrateConfigFile, n as CONFIG_FILE$2, o as expandHome, p as writeMainConfigRaw, t as CONFIG_DIR, u as paiConfigFilePath } from "../config-DYbyEe4-.mjs";
7
- import { i as ledgerPath, n as ensureNoMcpConfig, u as workersLogDir } from "../paths-DS5VQKoR.mjs";
8
- import "../worktree-BY--Rgmy.mjs";
9
- import { B as loadStatusConfig, G as writeSectionToForge, L as formatStale, U as upsertSection, b as loadStatusesCached, g as isLive, h as isChatPane, y as loadStatuses } from "../status-COtzewKV.mjs";
10
- import { A as readClaudeJson, N as writeClaudeJson, S as inputSideTokens, h as poolSettings, i as bundleBuildStamp, j as registerMcpScript, k as mcpEntryRunsScript, v as readPoolState, w as expandMcpNames } from "../pool-BZ94nFuq.mjs";
11
- import { a as unregistrableReason, s as worktreeReason } from "../registrable-CXBBpUcz.mjs";
12
- import "../pai-marker-DLKJ9Yr5.mjs";
3
+ import { _ as paiHomePath, a as paiCacheDir, c as PaiFileMigrationError, d as defaultPaiHomeDir, f as legacyConfigDir, g as paiHomeDir, h as migratePaiFile, l as claudeHomeDir, m as migratePaiDir, o as paiSocketPath, r as daemonLogPath, s as schedulerLogPath, u as claudeProjectsDir, v as resolvePaiFile } from "../runtime-paths-twfV23YL.mjs";
4
+ import { a as slugify$1, i as parseSessionFilename } from "../migrate-C2z1YmWC.mjs";
5
+ import { Et as appendLedger, M as readWorkersSection, R as LOOPBACK_HOST, _t as DEFAULT_CONTEXT_WINDOW, bt as stripModelVariant, c as WorkersConfigError, et as relocateWorkersYaml, f as escalationCloud, m as isLocalProvider, p as getProviderOrNative, rt as workersYamlPath, t as ANTHROPIC_NATIVE, v as nativeAnthropicModels, yt as contextWindowFromModelId } from "../config-DGV64YJa.mjs";
6
+ import { A as DEFAULT_PG_CONNECTION_TIMEOUT_MS, C as migrateMainConfigToYaml, F as quoteIdent, M as isContainerTarget, N as postgresConnectionString, O as readJsonStrict, P as postgresFullTarget, S as MainConfigError, T as resolvedMainConfigPath, c as loadConfig, d as paiConfigYamlFilePath, f as readMainConfigRaw, h as effectiveMainConfig, j as assertPgName, k as writeJsonAtomic, l as migrateConfigFile, n as CONFIG_FILE$2, o as expandHome, p as writeMainConfigRaw, t as CONFIG_DIR, u as paiConfigFilePath } from "../config-SlZ-Z52i.mjs";
7
+ import { i as ledgerPath, n as ensureNoMcpConfig, r as eventsPath, u as workersLogDir } from "../paths-BwfwHOvN.mjs";
8
+ import "../worktree-7ZS3RBf8.mjs";
9
+ import { K as writeSectionToForge, R as formatStale, V as loadStatusConfig, W as upsertSection, b as loadStatusesCached, g as isLive, h as isChatPane, v as loadStatus, y as loadStatuses } from "../status-DTMT5lyn.mjs";
10
+ import { A as readClaudeJson, N as writeClaudeJson, S as inputSideTokens, h as poolSettings, i as bundleBuildStamp, j as registerMcpScript, k as mcpEntryRunsScript, v as readPoolState, w as expandMcpNames } from "../pool-CFeHGX7q.mjs";
11
+ import { a as unregistrableReason, s as worktreeReason } from "../registrable-D_4qXCnq.mjs";
12
+ import "../pai-marker-kA0iM7rP.mjs";
13
13
  import "../helpers-CRuBUas3.mjs";
14
14
  import { t as applyRecencyBoost } from "../search-RSyeQqj1.mjs";
15
15
  import { t as dropItermBundleId } from "../env-BE8mrsxr.mjs";
16
- import { a as ownerSessionName, d as terminalHasName } from "../scope-D8HK6vU7.mjs";
17
- import { At as resolveBindAddress, Bn as setParkConfigSource, Dn as allSetups, Dt as routerPort, E as budgetCache, Et as routerHandler, F as LimitsCache, Gn as appendUsage, Hn as recordDuration, In as setupsPath, Kn as formatSessionStats, Nn as saveSetup, On as downLocalProviders, Ot as defaultV1Guard, Sn as providerHostPort, U as computeInventory, Un as aggregate, Vn as sharedParker, W as currentInventory, Wn as aggregateBySession, Xn as parseSince, Y as inventoryText, Zn as readUsage, at as readInventoryInput, jn as loadSaved, kt as displayHost, m as readRoutingState, pn as readProfiles, pt as isLoopbackProvider, qn as formatStats, qt as sessionStartTarget, un as SESSION_NAME_ENV, vn as localProviderForDelegation, x as SESSION_WORKER_PROVIDER_ENV, xt as routeVerdictLine, zn as parkConfigFrom } from "../routing-BlD_tKjW.mjs";
16
+ import { a as ownerSessionName, d as terminalHasName } from "../scope-BRcaPw5D.mjs";
17
+ import { At as resolveBindAddress, Bn as setParkConfigSource, Dn as allSetups, Dt as routerPort, E as budgetCache, Et as routerHandler, F as LimitsCache, Gn as appendUsage, Hn as recordDuration, In as setupsPath, Kn as formatSessionStats, Nn as saveSetup, On as downLocalProviders, Ot as defaultV1Guard, Sn as providerHostPort, U as computeInventory, Un as aggregate, Vn as sharedParker, W as currentInventory, Wn as aggregateBySession, Xn as parseSince, Y as inventoryText, Zn as readUsage, at as readInventoryInput, jn as loadSaved, kt as displayHost, m as readRoutingState, pn as readProfiles, pt as isLoopbackProvider, qn as formatStats, qt as sessionStartTarget, un as SESSION_NAME_ENV, vn as localProviderForDelegation, x as SESSION_WORKER_PROVIDER_ENV, xt as routeVerdictLine, zn as parkConfigFrom } from "../routing-CF0wlR-_.mjs";
18
18
  import { n as minOf } from "../min-max-DrSRUaWQ.mjs";
19
- import "../tool-errors-DXJ70-sr.mjs";
20
- import "../wait-ChpHKJZV.mjs";
21
- import "../local-tiers-BtdEEe5n.mjs";
22
- import { a as buildsDir, c as listBuilds, f as removeTicket, h as tryStart, i as buildNext, o as enqueue, p as renderBuildQueue, r as buildLogPath, s as getTicket, u as patchTicket, v as poolSlots, y as slotsFor } from "../caller-identity-Bim8JT-D.mjs";
19
+ import "../tool-errors-BOW2n882.mjs";
20
+ import "../wait-BindDA5w.mjs";
21
+ import "../local-tiers-CqwGUcEn.mjs";
22
+ import { a as buildsDir, c as listBuilds, f as removeTicket, h as tryStart, i as buildNext, o as enqueue, p as renderBuildQueue, r as buildLogPath, s as getTicket, u as patchTicket, v as poolSlots, y as slotsFor } from "../caller-identity-DPDSrZE2.mjs";
23
23
  import { d as setSchedConfigSource, f as sharedScheduler, i as mirrorTo, s as schedConfigFrom } from "../scheduler-CtUyWJ3O.mjs";
24
- import { i as effectiveCap, o as providerCap, r as capacityOf } from "../capacity-CsZMkVyg.mjs";
25
- import "../local-instances-bdCNNSsL.mjs";
26
- import { n as tierProviderNames, t as isLocalCandidate } from "../local-candidate-Cp_bw4kK.mjs";
24
+ import { i as effectiveCap, o as providerCap, r as capacityOf } from "../capacity-CThSydrd.mjs";
25
+ import "../local-instances-DhF4_sV2.mjs";
26
+ import { n as tierProviderNames, t as isLocalCandidate } from "../local-candidate-C2FGhf54.mjs";
27
27
  import { n as OPT, t as CliError } from "../define-CjJsmq-6.mjs";
28
- import { a as getStorageBackend, c as MergeError, f as humanDuration, i as getRegistryBackend, l as federationDbPath, o as oldRegistryPath, r as createStorageBackend, s as registryDbPath, u as oldFederationPath } from "../factory-BV0MQA3T.mjs";
29
- import { c as resolveFromModule, i as retryTransient, n as embedChunksWithBackend, r as indexProjectWithBackend, s as packageRoot } from "../postgres-C-T5OWer.mjs";
30
- import { d as OLLAMA_ID, f as ollamaDefaultUrl, i as measureThroughput, l as provisionOllama, o as createBackend, s as detectBackends, u as OLLAMA_DEFAULT_MODEL } from "../embedding-gate-CTAk8STe.mjs";
28
+ import { a as getStorageBackend, c as MergeError, f as humanDuration, i as getRegistryBackend, l as federationDbPath, o as oldRegistryPath, r as createStorageBackend, s as registryDbPath, u as oldFederationPath } from "../factory-CYwa0jYj.mjs";
29
+ import { c as resolveFromModule, i as retryTransient, n as embedChunksWithBackend, r as indexProjectWithBackend, s as packageRoot } from "../postgres-BL5oBBgY.mjs";
30
+ import { d as OLLAMA_ID, f as ollamaDefaultUrl, i as measureThroughput, l as provisionOllama, o as createBackend, s as detectBackends, u as OLLAMA_DEFAULT_MODEL } from "../embedding-gate-iq0oQEkF.mjs";
31
31
  import "../embeddings-BVq3esBF.mjs";
32
- import "../router-CQSc3lk3.mjs";
33
- import { t as detectProject } from "../detect-CHuxFrLy.mjs";
34
- import { n as getProject$1, o as requireProject, r as getProjectAliases, s as resolveIdentifier } from "../helpers-CvbYEKoD.mjs";
35
- import { a as resolveHome, c as upsertSession, d as scanTranscriptFolders, f as transcriptFiles, l as claudeProjectsDir$1, n as loadScanConfig, o as saveScanConfig, r as migrateScanConfig, s as upsertProject, t as cmdScan, u as findMovedProjects } from "../scan-C3uotg_f.mjs";
36
- import { A as cmdAdd, B as cmdRebind, F as cmdEdit, H as cmdUnarchive, I as cmdGo, L as cmdInfo$1, M as cmdArchive, N as cmdConsolidate, P as cmdDetect, R as cmdList$1, S as legacy, U as findMovedPath, V as cmdTag$1, _ as cmdList, a as newTop, b as cmdSlug, f as sessionCommand, g as cmdInfo, h as cmdAutoRoute, i as lsCommand, j as cmdAlias, l as renameTop, m as cmdActive, r as collectView, s as openTop, v as cmdRename, x as cmdTag, y as cmdRoute, z as cmdMove } from "../session-BAfypQb9.mjs";
37
- import { a as detectCurrentProviderName, i as detectCurrentModel, n as buildLaunchPlan, o as launchableProviderNames, r as buildLaunchRows, s as maskLaunchEnv } from "../launch-DpQPZGsT.mjs";
38
- import { a as colorEnabled, c as renderCommandHelp, d as renderOverview, f as renderTable$2, l as renderMarkdown, m as usageLine, o as makeStyle, p as suggest, s as mdCommand, u as renderMore } from "../health-kufVIf6i.mjs";
39
- import { a as providerResumePlan, n as launchInDir, o as restoreTopLevel, r as launchPrompt, s as resumeArgvText, t as hasConversation } from "../launch-UPQBoW2q.mjs";
40
- import { _ as setupsTable, a as lastSetup, m as rootCommand } from "../root-bG48WVpW.mjs";
41
- import { n as fetchLiveSessions, r as sendToSession, t as callAiBroker } from "../aibroker-client-CQPGqLxL.mjs";
42
- import { a as resolveSessionByNameOrId, o as scanCacheFilePath, r as migrateSessionScanCache, s as scanSessions, t as fmtAge } from "../session-scan-BmnjSReu.mjs";
43
- import { A as routerPidFile, B as usageContextTokens, D as routerHandoverFile, E as probeRouter, F as startHandover, I as paneWaiver, L as launchPane, M as routerSwapArgs, P as spawnRouter, R as noTerminalReasons, S as usageJson, T as projectLaunchConfig, b as renderUsage, c as reportOriginWithHost, f as dirtyPaths, g as realRunnerDeps, j as routerRunningPid, k as routerLogFile, l as runMessenger, s as finishDetached, u as startDetached, w as pinNotices, x as transcriptUsage, y as priceTableText } from "../run-AZVwceiU.mjs";
44
- import { C as readRunLog, D as resumeState, E as requestPlanStop, F as runLogPath, I as PlanTracker, M as dirtyConflicts, O as runExitCode, P as loadPlan, S as planStopPath, T as renderStatus, Y as readRunLock, ht as resolveHostSession, j as PlanError, jt as runPaneExec, k as runPlan, l as PlanReporter, nt as closePaneForWorker, ot as inVisiblePane, q as planLockPath, rt as closeSelfPane, u as QueueReporter, vt as writePaneTitle, w as renderSchedule, x as planStatus, y as blockedMessage, yt as psTable } from "../controls-OiVVbe-f.mjs";
45
- import "../endreason-BqyyU7BH.mjs";
46
- import { N as endBuildWait, j as beginBuildWait } from "../render-Cz3BWIBO.mjs";
47
- import "../operator-DrwWItVt.mjs";
48
- import "../kill-tree-CcpsRLCZ.mjs";
49
- import { s as poolReloadLive } from "../pool-cli-B1eKySeN.mjs";
50
- import "../chain-Dzz6xcKH.mjs";
51
- import { _ as issueClaims, a as branchDiff, b as workerCommand, c as runQueue, d as loadSettings, f as move, g as issueClaim, h as renderReview, i as projectChecks, l as QueueStore, m as renderList, n as initProject, o as realQueueDeps, p as queueStatePath, r as isGitProject, s as acceptItems, t as formatCheck, u as again, v as issueStale } from "../project-setup-pIPHZaXf.mjs";
52
- import { m as runInPane, r as closeFinishedPanes } from "../pane-audit-wv9UkyCe.mjs";
53
- import "../viewer-BgGRqXfD.mjs";
54
- import "../fallback-Bj2cyVk_.mjs";
55
- import { g as setWorkersEnabled, s as describeProviders } from "../providers-Dzx5GAUI.mjs";
56
- import { $ as exportDefaultLayout, B as mergeSettings, G as cachedRows, J as handleCockpit, K as cachedSessionMap, Q as warmCockpit, W as SystemMonitor, X as setEndpointHealth, Y as setActionHooks, Z as setRuntimeSampler, at as withWorkerAttribution, et as loadBuildsConfig, it as sessionDisplayName, lt as providersCommand, n as WORKER_CONFIG_COMMANDS, nt as loadSessionMap, q as cachedWorkerWindows, rt as loadWorkerWindows, t as PROVIDER_EDIT_COMMANDS, tt as createEndpointHealth, z as installWorkers } from "../worker-config-DgdT0iTB.mjs";
57
- import { a as listConfigOp, i as getConfigValueOp, n as formatConfigGetOutput, o as setConfigValueOp, r as formatConfigSource, s as unsetConfigValueOp, t as MainConfigOpsError } from "../main-config-ops-BYsK3BxM.mjs";
58
- import { H as acquireRun, U as readRun, V as cockpitProbeSettings, k as readRuntimeCommands } from "../cockpit-actions-BTA1njMO.mjs";
59
- import { n as PaiClient } from "../ipc-client-CnYEF31R.mjs";
60
- import { a as EMBED_PASS_BATCH_SIZE, c as liveYieldCheck, n as DEFAULT_EMBED_MAX_LOAD, o as formatEmbedStatus, r as DEFAULT_EMBED_MAX_MINUTES, s as lastRecordedEmbedPass } from "../embed-pass-DtVF2fOV.mjs";
61
- import { a as liveWorkersConfigShadows, i as totalUsageTokens, r as parseSessionUsage } from "../session-usage-oycSdiD3.mjs";
62
- import { Bt as findLatestNote, C as migrateSummaryCooldowns, Dt as nextOccurrence, Et as describeQuiet, Ht as readBodyFile, It as readContextHandoverCache, Mt as LAUNCHD_LABEL, Ot as readQuiet, Pt as resolveDaemonPid, Rt as appendCheckpointToNote, St as loadSessionKeepaliveState, Ut as restartRouter, Vt as findNotesDir$1, bt as reapJob, ht as obsidianEnabled, i as reexecBackground, jt as writeQuiet, mt as OBSIDIAN_DISABLED_MESSAGE, o as backfillKgFromNotes, s as migrateKgBackfillState, v as migrateWorkQueue, vt as migrateIdentityFile, wt as sessionKeepaliveLedgerSummary, yt as jobPaths, zt as applyContinue } from "../indexer-backend-B2vtFl38.mjs";
63
- import { n as queriesDirPath, t as migrateQueriesDir } from "../query-feedback-DN52N9C5.mjs";
32
+ import "../router-D2476pOp.mjs";
33
+ import { t as detectProject } from "../detect-DLjeYwnt.mjs";
34
+ import { n as getProject$1, o as requireProject, r as getProjectAliases, s as resolveIdentifier } from "../helpers-Dj2nY56t.mjs";
35
+ import { a as resolveHome, c as upsertSession, d as scanTranscriptFolders, f as transcriptFiles, l as claudeProjectsDir$1, n as loadScanConfig, o as saveScanConfig, r as migrateScanConfig, s as upsertProject, t as cmdScan, u as findMovedProjects } from "../scan-ew8M6tTH.mjs";
36
+ import { A as cmdAdd, B as cmdRebind, F as cmdEdit, H as cmdUnarchive, I as cmdGo, L as cmdInfo$1, M as cmdArchive, N as cmdConsolidate, P as cmdDetect, R as cmdList$1, S as legacy, U as findMovedPath, V as cmdTag$1, _ as cmdList, a as newTop, b as cmdSlug, f as sessionCommand, g as cmdInfo, h as cmdAutoRoute, i as lsCommand, j as cmdAlias, l as renameTop, m as cmdActive, r as collectView, s as openTop, v as cmdRename, x as cmdTag, y as cmdRoute, z as cmdMove } from "../session-C1JnDXvP.mjs";
37
+ import { a as detectCurrentProviderName, i as detectCurrentModel, n as buildLaunchPlan, o as launchableProviderNames, r as buildLaunchRows, s as maskLaunchEnv } from "../launch-CV6oPHI2.mjs";
38
+ import { a as colorEnabled, c as renderCommandHelp, d as renderOverview, f as renderTable$2, l as renderMarkdown, m as usageLine, o as makeStyle, p as suggest, s as mdCommand, u as renderMore } from "../health-Bb2U4PNK.mjs";
39
+ import { a as providerResumePlan, n as launchInDir, o as restoreTopLevel, r as launchPrompt, s as resumeArgvText, t as hasConversation } from "../launch-Dy8qQnSD.mjs";
40
+ import { _ as setupsTable, a as lastSetup, m as rootCommand } from "../root-B-ZYiEgi.mjs";
41
+ import { n as fetchLiveSessions, r as sendToSession, t as callAiBroker } from "../aibroker-client-CXsyC7A8.mjs";
42
+ import { a as resolveSessionByNameOrId, o as scanCacheFilePath, r as migrateSessionScanCache, s as scanSessions, t as fmtAge } from "../session-scan-DihMibI-.mjs";
43
+ import { A as routerPidFile, B as usageContextTokens, D as routerHandoverFile, E as probeRouter, F as startHandover, I as paneWaiver, L as launchPane, M as routerSwapArgs, P as spawnRouter, R as noTerminalReasons, S as usageJson, T as projectLaunchConfig, b as renderUsage, c as reportOriginWithHost, f as dirtyPaths, g as realRunnerDeps, j as routerRunningPid, k as routerLogFile, l as runMessenger, s as finishDetached, u as startDetached, w as pinNotices, x as transcriptUsage, y as priceTableText } from "../run-CAjfKBQV.mjs";
44
+ import { C as readRunLog, D as resumeState, E as requestPlanStop, F as runLogPath, I as PlanTracker, M as dirtyConflicts, O as runExitCode, Ot as runPaneExec, P as loadPlan, S as planStopPath, T as renderStatus, Y as readRunLock, ht as resolveHostSession, j as PlanError, k as runPlan, l as PlanReporter, nt as closePaneForWorker, ot as inVisiblePane, q as planLockPath, rt as closeSelfPane, u as QueueReporter, vt as writePaneTitle, w as renderSchedule, x as planStatus, y as blockedMessage } from "../controls-ByrcupcA.mjs";
45
+ import "../endreason-BjPom527.mjs";
46
+ import { B as userRuleDirs, H as maskSecrets, J as psTable, L as listRules, N as endBuildWait, P as claudeSessionTranscript, R as loadRules, U as loadBuildsConfig, V as scanTranscript, j as beginBuildWait, z as projectRuleDirs } from "../render-CbifCwTb.mjs";
47
+ import "../operator-lRGvYr3D.mjs";
48
+ import "../kill-tree-DTmiEb3T.mjs";
49
+ import { s as poolReloadLive } from "../pool-cli-DOLgPgOM.mjs";
50
+ import "../chain-BvdKqRiZ.mjs";
51
+ import { _ as issueClaims, a as branchDiff, b as workerCommand, c as runQueue, d as loadSettings, f as move, g as issueClaim, h as renderReview, i as projectChecks, l as QueueStore, m as renderList, n as initProject, o as realQueueDeps, p as queueStatePath, r as isGitProject, s as acceptItems, t as formatCheck, u as again, v as issueStale } from "../project-setup-i5D0xkTn.mjs";
52
+ import { m as runInPane, r as closeFinishedPanes } from "../pane-audit-DUuilP1V.mjs";
53
+ import "../viewer-4BU8w_gK.mjs";
54
+ import { g as setWorkersEnabled, s as describeProviders } from "../providers-BC2Myo_O.mjs";
55
+ import { $ as exportDefaultLayout, B as mergeSettings, G as cachedRows, J as handleCockpit, K as cachedSessionMap, Q as warmCockpit, W as SystemMonitor, X as setEndpointHealth, Y as setActionHooks, Z as setRuntimeSampler, ct as providersCommand, et as createEndpointHealth, it as withWorkerAttribution, n as WORKER_CONFIG_COMMANDS, nt as loadWorkerWindows, q as cachedWorkerWindows, rt as sessionDisplayName, t as PROVIDER_EDIT_COMMANDS, tt as loadSessionMap, z as installWorkers } from "../worker-config-CdPmb4_2.mjs";
56
+ import { a as listConfigOp, i as getConfigValueOp, n as formatConfigGetOutput, o as setConfigValueOp, r as formatConfigSource, s as unsetConfigValueOp, t as MainConfigOpsError } from "../main-config-ops-C_fkINDv.mjs";
57
+ import { H as acquireRun, U as readRun, V as cockpitProbeSettings, k as readRuntimeCommands } from "../cockpit-actions-BgdKB9G3.mjs";
58
+ import { n as PaiClient } from "../ipc-client-CmagIFLh.mjs";
59
+ import { a as EMBED_PASS_BATCH_SIZE, c as liveYieldCheck, n as DEFAULT_EMBED_MAX_LOAD, o as formatEmbedStatus, r as DEFAULT_EMBED_MAX_MINUTES, s as lastRecordedEmbedPass } from "../embed-pass-DdTMn4DH.mjs";
60
+ import { a as liveWorkersConfigShadows, i as totalUsageTokens, r as parseSessionUsage } from "../session-usage-BRIWFYPS.mjs";
61
+ import "../fallback-CQ8_CuVu.mjs";
62
+ import { $t as readBodyFile, At as jobPaths, Ct as historyDir, D as migrateWorkQueue, Et as obsidianEnabled, Ft as sessionKeepaliveLedgerSummary, Gt as resolveDaemonPid, Ht as writeQuiet, Lt as describeQuiet, M as migrateSummaryCooldowns, Nt as loadSessionKeepaliveState, Qt as findNotesDir$1, Rt as nextOccurrence, Ut as LAUNCHD_LABEL, Xt as applyContinue, Yt as appendCheckpointToNote, Zt as findLatestNote, c as proposeFromTranscript, en as restartRouter, g as migrateKgBackfillState, h as backfillKgFromNotes, i as getLesson, jt as reapJob, kt as migrateIdentityFile, l as setState, n as LESSON_STATES, o as listLessons, p as reexecBackground, qt as readContextHandoverCache, r as addLesson, s as promoteLesson, t as LESSON_KINDS, wt as OBSIDIAN_DISABLED_MESSAGE, zt as readQuiet } from "../lessons-CdmqZD0G.mjs";
63
+ import { n as queriesDirPath, t as migrateQueriesDir } from "../query-feedback-BWl2HRwN.mjs";
64
64
  import { appendFileSync, chmodSync, closeSync, copyFileSync, cpSync, createReadStream, createWriteStream, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
65
65
  import { createHash, randomUUID } from "node:crypto";
66
- import { basename, dirname, join, relative, resolve, sep } from "node:path";
66
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
67
67
  import chalk from "chalk";
68
68
  import { cpus, homedir, platform, userInfo } from "node:os";
69
69
  import { execFile, execFileSync, execSync, spawn, spawnSync } from "node:child_process";
@@ -74,8 +74,8 @@ import pg from "pg";
74
74
  import BetterSqlite3 from "better-sqlite3";
75
75
  import { createInterface } from "node:readline";
76
76
  import net$1, { createConnection as createConnection$1 } from "net";
77
- import { promisify } from "node:util";
78
77
  import { getEncoding } from "js-tiktoken";
78
+ import { promisify } from "node:util";
79
79
 
80
80
  //#region src/cli/lib/exit.ts
81
81
  /**
@@ -152,7 +152,7 @@ const GLOBAL_FLAGS = [{
152
152
  type: "boolean",
153
153
  summary: ""
154
154
  }];
155
- function parseArgs(cmd, argv) {
155
+ function parseArgs$1(cmd, argv) {
156
156
  const defs = [...cmd.options ?? [], ...GLOBAL_FLAGS.filter((g) => !(cmd.options ?? []).some((o) => o.name === g.name))];
157
157
  const byLong = new Map(defs.map((o) => [o.name, o]));
158
158
  const byShort = new Map(defs.filter((o) => o.short).map((o) => [o.short, o]));
@@ -326,7 +326,7 @@ async function runCli(groups, argv, io = defaultIo(), root) {
326
326
  rest: argv
327
327
  } : resolve$1(groups, argv);
328
328
  const { cmd, path, rest } = resolved;
329
- const parsed = parseArgs(cmd, rest);
329
+ const parsed = parseArgs$1(cmd, rest);
330
330
  const noArgs = rest.length === 0;
331
331
  if (parsed.help || noArgs && !cmd.run && cmd.subcommands?.length) {
332
332
  if (target) {
@@ -830,7 +830,7 @@ function routerSamplerDeps(sched, engineStatusTimeoutMs = () => DEFAULT_RUNTIME_
830
830
  });
831
831
  return {
832
832
  inventory: async () => {
833
- const { currentInventory } = await import("../model-inventory-BuTpi9a6.mjs");
833
+ const { currentInventory } = await import("../model-inventory-BYNrSTaY.mjs");
834
834
  return currentInventory();
835
835
  },
836
836
  sched,
@@ -845,7 +845,7 @@ function routerSamplerDeps(sched, engineStatusTimeoutMs = () => DEFAULT_RUNTIME_
845
845
  }
846
846
  },
847
847
  async rssMb(port) {
848
- const { processTreeRssMb } = await import("../model-inventory-BuTpi9a6.mjs");
848
+ const { processTreeRssMb } = await import("../model-inventory-BYNrSTaY.mjs");
849
849
  const pid = (await exec("lsof", [
850
850
  "-ti",
851
851
  `tcp:${port}`,
@@ -1214,9 +1214,9 @@ const runCommand = {
1214
1214
  sampler.start();
1215
1215
  setRuntimeSampler(sampler);
1216
1216
  setActionHooks({ runLocalJob: async (kind, tier) => {
1217
- const { startTier, stopAll } = await import("../worker-config-D1KVAMp4.mjs");
1218
- const { readLocalConfig } = await import("../local-tiers-OBqAAfOg.mjs");
1219
- const { workersYamlPath } = await import("../workers-config-BhZBrR-Q.mjs");
1217
+ const { startTier, stopAll } = await import("../worker-config-CfV2dgSb.mjs");
1218
+ const { readLocalConfig } = await import("../local-tiers-X_dQfI4Q.mjs");
1219
+ const { workersYamlPath } = await import("../workers-config-2VipVOYk.mjs");
1220
1220
  const cfg = readLocalConfig(workersYamlPath());
1221
1221
  if (kind === "off") return void await stopAll(cfg);
1222
1222
  const t = cfg.tiers[tier];
@@ -1593,7 +1593,7 @@ const modelsCommand = {
1593
1593
  if (words[0] === "add") {
1594
1594
  const source = words[1];
1595
1595
  if (!source) throw new Error("usage: pai models add <hf-id|path> [--name n] [--download] [--try]");
1596
- const { addModel } = await import("../models-add-CJx2YAwh.mjs");
1596
+ const { addModel } = await import("../models-add-Dc7VXmcu.mjs");
1597
1597
  const r = await addModel({
1598
1598
  source,
1599
1599
  name: typeof ctx.opts.name === "string" ? ctx.opts.name : void 0,
@@ -1608,7 +1608,7 @@ const modelsCommand = {
1608
1608
  if (op) {
1609
1609
  const runtime = words[1];
1610
1610
  if (!runtime) throw new Error(`usage: pai models ${op} <runtime> (configured: ${Object.keys(readRuntimeCommands()).join(", ") || "none"})`);
1611
- const { defaultDeps, runtimeStartStop } = await import("../cockpit-actions-BgsiNcUY.mjs");
1611
+ const { defaultDeps, runtimeStartStop } = await import("../cockpit-actions-DNl0rKfW.mjs");
1612
1612
  const r = await runtimeStartStop(runtime, op, defaultDeps(), { model: typeof ctx.opts.model === "string" ? ctx.opts.model : void 0 });
1613
1613
  process.stdout.write(r.message + "\n");
1614
1614
  if (!r.ok) process.exitCode = 1;
@@ -1812,7 +1812,7 @@ const MAX_PROMPT_CHARS = 220;
1812
1812
  /** Newest prompts kept per transcript — headroom over MAX_PROMPTS for the duplicate collapse. */
1813
1813
  const KEEP_PROMPTS = 64;
1814
1814
  /** Cap on listed working-tree entries, so a big refactor cannot flood TODO.md. */
1815
- const MAX_FILES = 20;
1815
+ const MAX_FILES$1 = 20;
1816
1816
  /**
1817
1817
  * Text of a JSONL message, or null when the entry carries none.
1818
1818
  * Mirrors the shape handled by slug-generator: content is either a plain
@@ -1998,8 +1998,8 @@ function readWorkingTree(cwd) {
1998
1998
  "-1",
1999
1999
  "--pretty=%h %s"
2000
2000
  ]),
2001
- changes: all.slice(0, MAX_FILES),
2002
- overflow: Math.max(0, all.length - MAX_FILES)
2001
+ changes: all.slice(0, MAX_FILES$1),
2002
+ overflow: Math.max(0, all.length - MAX_FILES$1)
2003
2003
  };
2004
2004
  }
2005
2005
  /**
@@ -2923,7 +2923,7 @@ async function cmdPause(opts) {
2923
2923
  process.exitCode = 1;
2924
2924
  return;
2925
2925
  }
2926
- const { getRegistryBackend } = await import("../factory-DeASMV7Z.mjs");
2926
+ const { getRegistryBackend } = await import("../factory-DFwez99M.mjs");
2927
2927
  const registryBackend = await getRegistryBackend();
2928
2928
  const cwd = process.cwd();
2929
2929
  const project = await resolveProjectByCwd(registryBackend, cwd);
@@ -3023,7 +3023,7 @@ function touchSentinel(rootPath) {
3023
3023
  async function cmdAutosave(opts) {
3024
3024
  const minGap = parseInt(opts.minGap ?? String(DEFAULT_MIN_GAP), 10);
3025
3025
  const cwd = process.cwd();
3026
- const { getRegistryBackend } = await import("../factory-DeASMV7Z.mjs");
3026
+ const { getRegistryBackend } = await import("../factory-DFwez99M.mjs");
3027
3027
  const registryBackend = await getRegistryBackend();
3028
3028
  const project = await resolveProjectByCwd(registryBackend, cwd);
3029
3029
  if (!project) return;
@@ -3249,7 +3249,7 @@ function finalizeNote(notePath) {
3249
3249
  }
3250
3250
  async function cmdEnd(opts) {
3251
3251
  await cmdPause(opts);
3252
- const { getRegistryBackend } = await import("../factory-DeASMV7Z.mjs");
3252
+ const { getRegistryBackend } = await import("../factory-DFwez99M.mjs");
3253
3253
  const registryBackend = await getRegistryBackend();
3254
3254
  const cwd = process.cwd();
3255
3255
  const project = await registryBackend.findProjectByCwdPrefix(cwd);
@@ -3430,7 +3430,7 @@ function buildStopBody(project, sessionId) {
3430
3430
  }
3431
3431
  }
3432
3432
  async function cmdHandover(projectSlug, numberOrLatest, sessionId) {
3433
- const { getRegistryBackend } = await import("../factory-DeASMV7Z.mjs");
3433
+ const { getRegistryBackend } = await import("../factory-DFwez99M.mjs");
3434
3434
  const registryBackend = await getRegistryBackend();
3435
3435
  let project;
3436
3436
  if (projectSlug) {
@@ -4376,8 +4376,8 @@ async function analyzeProject(registryBackend, project) {
4376
4376
  async function countVectorDbPaths(oldPaths) {
4377
4377
  if (oldPaths.length === 0) return 0;
4378
4378
  try {
4379
- const { loadConfig } = await import("../config-Dv3t7xnT.mjs");
4380
- const { createStorageBackend } = await import("../factory-DeASMV7Z.mjs");
4379
+ const { loadConfig } = await import("../config-DHgPx3ER.mjs");
4380
+ const { createStorageBackend } = await import("../factory-DFwez99M.mjs");
4381
4381
  const config = loadConfig();
4382
4382
  if (config.storageBackend !== "postgres") return 0;
4383
4383
  const backend = await createStorageBackend(config);
@@ -4394,8 +4394,8 @@ async function countVectorDbPaths(oldPaths) {
4394
4394
  async function updateVectorDbPaths(moves) {
4395
4395
  if (moves.length === 0) return 0;
4396
4396
  try {
4397
- const { loadConfig } = await import("../config-Dv3t7xnT.mjs");
4398
- const { createStorageBackend } = await import("../factory-DeASMV7Z.mjs");
4397
+ const { loadConfig } = await import("../config-DHgPx3ER.mjs");
4398
+ const { createStorageBackend } = await import("../factory-DFwez99M.mjs");
4399
4399
  const config = loadConfig();
4400
4400
  if (config.storageBackend !== "postgres") return 0;
4401
4401
  const backend = await createStorageBackend(config);
@@ -4482,7 +4482,7 @@ async function displayDryRun(plans) {
4482
4482
  console.log();
4483
4483
  }
4484
4484
  async function executeCleanup(plans, skipReindex) {
4485
- const { getRegistryBackend } = await import("../factory-DeASMV7Z.mjs");
4485
+ const { getRegistryBackend } = await import("../factory-DFwez99M.mjs");
4486
4486
  const registryBackend = await getRegistryBackend();
4487
4487
  let deleted = 0;
4488
4488
  let renamed = 0;
@@ -4682,7 +4682,7 @@ async function executeCleanup(plans, skipReindex) {
4682
4682
  * them into YYYY/MM/ folders and renumber. Dry run unless `execute` is set.
4683
4683
  */
4684
4684
  async function runCleanup(projectSlug, opts) {
4685
- const { getRegistryBackend } = await import("../factory-DeASMV7Z.mjs");
4685
+ const { getRegistryBackend } = await import("../factory-DFwez99M.mjs");
4686
4686
  const registryBackend = await getRegistryBackend();
4687
4687
  const dryRun = !opts.execute;
4688
4688
  const skipReindex = opts.reindex === false;
@@ -6664,7 +6664,7 @@ async function runRestore(backupPath, opts) {
6664
6664
  * backup is written first. Any other content, or an unknown previous rendering,
6665
6665
  * counts as user-edited: the new version goes beside it as CLAUDE.md.pai-new.
6666
6666
  */
6667
- function stamp(d) {
6667
+ function stamp$1(d) {
6668
6668
  return d.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
6669
6669
  }
6670
6670
  function writeAtomic(path, content) {
@@ -6683,7 +6683,7 @@ function refreshClaudeMd(claudeMd, newRendered, previousRendered, now = /* @__PU
6683
6683
  newPath
6684
6684
  };
6685
6685
  }
6686
- const backup = `${claudeMd}.bak-${stamp(now)}`;
6686
+ const backup = `${claudeMd}.bak-${stamp$1(now)}`;
6687
6687
  copyFileSync(claudeMd, backup);
6688
6688
  writeAtomic(claudeMd, newRendered);
6689
6689
  return {
@@ -8258,6 +8258,10 @@ const SETTINGS_HOOKS = [
8258
8258
  hookType: "UserPromptSubmit",
8259
8259
  command: "${ADAPTER_DIR}/Hooks/local-model-hint.mjs"
8260
8260
  },
8261
+ {
8262
+ hookType: "UserPromptSubmit",
8263
+ command: "${ADAPTER_DIR}/Hooks/handoff-link.mjs"
8264
+ },
8261
8265
  {
8262
8266
  hookType: "UserPromptSubmit",
8263
8267
  command: "${ADAPTER_DIR}/Hooks/capture-all-events.mjs --event-type UserPromptSubmit"
@@ -8965,8 +8969,8 @@ function cmdLogs(opts) {
8965
8969
  return 0;
8966
8970
  }
8967
8971
  async function daemonServeCmd() {
8968
- const { serve } = await import("../daemon-ieo0lnpq.mjs");
8969
- const { loadConfig: lc, ensureConfigDir } = await import("../config-Dv3t7xnT.mjs");
8972
+ const { serve } = await import("../daemon-iymy18Ie.mjs");
8973
+ const { loadConfig: lc, ensureConfigDir } = await import("../config-DHgPx3ER.mjs");
8970
8974
  ensureConfigDir();
8971
8975
  await serve(lc());
8972
8976
  return 0;
@@ -11137,6 +11141,232 @@ async function watchTicket(dir, id, deps = {}, intervalMs = 1e3, simctl, reap) {
11137
11141
  }
11138
11142
  }
11139
11143
 
11144
+ //#endregion
11145
+ //#region src/audit/tokens.ts
11146
+ /**
11147
+ * tokens.ts — the one tokenizer every audit section counts through.
11148
+ *
11149
+ * cl100k_base via js-tiktoken (pure JS, no native build). "all" + [] for the
11150
+ * allowed/disallowed-special-token arguments matches Python tiktoken's
11151
+ * `disallowed_special=()`: audited text (hook stdout, session transcripts)
11152
+ * routinely contains literal "<|...|>"-shaped substrings that are not actual
11153
+ * special tokens, and the strict default throws on sight of them.
11154
+ */
11155
+ const TOKEN_ENCODING = "cl100k_base";
11156
+ let cached = null;
11157
+ function encoder() {
11158
+ if (!cached) cached = getEncoding(TOKEN_ENCODING);
11159
+ return cached;
11160
+ }
11161
+ function countTokens(text) {
11162
+ if (!text) return 0;
11163
+ return encoder().encode(text, "all", []).length;
11164
+ }
11165
+
11166
+ //#endregion
11167
+ //#region src/audit/hooks.ts
11168
+ /**
11169
+ * `pai audit tokens hooks` — token cost of the hooks that fire before the
11170
+ * model ever sees a token: SessionStart and UserPromptSubmit run on every
11171
+ * new session / every prompt respectively, and whatever they print to
11172
+ * stdout is injected into context.
11173
+ */
11174
+ const HOOK_TIMEOUT_MS = 2e4;
11175
+ function settingsPaths(cwd) {
11176
+ return [join(claudeHomeDir(), "settings.json"), join(cwd, ".claude", "settings.json")].filter(existsSync);
11177
+ }
11178
+ function readSettings(path) {
11179
+ try {
11180
+ return JSON.parse(readFileSync(path, "utf8"));
11181
+ } catch {
11182
+ return {};
11183
+ }
11184
+ }
11185
+ /** Expand ${PAI_DIR} and $HOME/${HOME} the way settings.json's own hooks do. */
11186
+ function expandCommand(command, env) {
11187
+ const paiDir = env.PAI_DIR ?? process.env.PAI_DIR ?? claudeHomeDir();
11188
+ return command.replace(/\$\{PAI_DIR\}/g, paiDir).replace(/\$\{HOME\}/g, homedir()).replace(/\$HOME\b/g, homedir());
11189
+ }
11190
+ function syntheticStdin(cwd, eventName) {
11191
+ const sessionId = randomUUID();
11192
+ return JSON.stringify({
11193
+ session_id: sessionId,
11194
+ cwd,
11195
+ hook_event_name: eventName,
11196
+ prompt: "audit",
11197
+ transcript_path: join(claudeProjectsDir(), "audit", `${sessionId}.jsonl`)
11198
+ });
11199
+ }
11200
+ function runHook(command, cwd, eventName) {
11201
+ const result = spawnSync(command, {
11202
+ shell: true,
11203
+ cwd,
11204
+ input: syntheticStdin(cwd, eventName),
11205
+ encoding: "utf8",
11206
+ timeout: HOOK_TIMEOUT_MS
11207
+ });
11208
+ if (result.error) return {
11209
+ stdout: "",
11210
+ error: result.error.message
11211
+ };
11212
+ if (result.signal) return {
11213
+ stdout: result.stdout ?? "",
11214
+ error: `killed by ${result.signal} (timeout?)`
11215
+ };
11216
+ return { stdout: result.stdout ?? "" };
11217
+ }
11218
+ const NOT_RUN = "not run (pass --run to execute hooks)";
11219
+ const AUDITED_EVENTS = ["SessionStart", "UserPromptSubmit"];
11220
+ /** Hook events the capture hook records, so a configured hook can be checked
11221
+ * against what actually fired. */
11222
+ const OBSERVABLE_EVENTS = [
11223
+ "PreToolUse",
11224
+ "PostToolUse",
11225
+ "UserPromptSubmit",
11226
+ "Stop",
11227
+ "SubagentStop",
11228
+ "SessionStart",
11229
+ "PreCompact",
11230
+ "Notification"
11231
+ ];
11232
+ /** Default observation window for `--since`. */
11233
+ const DEFAULT_SINCE_HOURS = 24;
11234
+ /** Where capture-all-events writes its JSONL: PAI_HOME/History/raw-outputs. */
11235
+ function defaultCaptureDir() {
11236
+ return join(paiHomePath("History"), "raw-outputs");
11237
+ }
11238
+ /**
11239
+ * Counts hook events in the raw-capture JSONL files modified since `sinceMs`
11240
+ * (capture-all-events lines carry `hook_event_type` and `timestamp`). Files
11241
+ * are read line by line; malformed lines are skipped.
11242
+ */
11243
+ async function observedHookEvents(captureDir, sinceMs) {
11244
+ const observed = /* @__PURE__ */ new Map();
11245
+ if (!existsSync(captureDir)) return observed;
11246
+ const monthDirs = readdirSync(captureDir, { withFileTypes: true }).filter((d) => d.isDirectory() && /^\d{4}-\d{2}$/.test(d.name)).map((d) => join(captureDir, d.name));
11247
+ for (const monthDir of monthDirs) for (const name of readdirSync(monthDir)) {
11248
+ if (!name.endsWith(".jsonl")) continue;
11249
+ const file = join(monthDir, name);
11250
+ try {
11251
+ if (statSync(file).mtimeMs < sinceMs) continue;
11252
+ } catch {
11253
+ continue;
11254
+ }
11255
+ const rl = createInterface({
11256
+ input: createReadStream(file, "utf8"),
11257
+ crlfDelay: Infinity
11258
+ });
11259
+ for await (const raw of rl) foldObservedLine(raw, observed);
11260
+ }
11261
+ return observed;
11262
+ }
11263
+ /** True when the command references a file path that does not exist (a hook
11264
+ * script that was moved or deleted — the hook silently never runs). */
11265
+ function commandHasMissingFile(command) {
11266
+ for (const token of command.split(/\s+/)) {
11267
+ if (!token.includes("/")) continue;
11268
+ if (token.includes("$") || token.includes("*")) continue;
11269
+ if (!existsSync(token)) return true;
11270
+ }
11271
+ return false;
11272
+ }
11273
+ function auditHooks(cwd, run, maybeOpts) {
11274
+ const opts = typeof run === "function" ? maybeOpts ?? {} : run ?? {};
11275
+ const runner = typeof run === "function" ? run : void 0;
11276
+ const sinceHours = opts.sinceHours ?? DEFAULT_SINCE_HOURS;
11277
+ const now = opts.now ?? Date.now();
11278
+ const observed = observedHookEventsSync(opts.captureDir ?? defaultCaptureDir(), now - sinceHours * 36e5);
11279
+ const settingsFiles = settingsPaths(cwd);
11280
+ const readings = [];
11281
+ const totalsByEvent = {};
11282
+ const preToolUseHooks = [];
11283
+ let preToolUseRewritesBash = false;
11284
+ for (const settingsPath of settingsFiles) {
11285
+ const settings = readSettings(settingsPath);
11286
+ const env = settings.env ?? {};
11287
+ for (const eventName of AUDITED_EVENTS) {
11288
+ const groups = settings.hooks?.[eventName] ?? [];
11289
+ totalsByEvent[eventName] = totalsByEvent[eventName] ?? 0;
11290
+ for (const group of groups) for (const entry of group.hooks ?? []) {
11291
+ if (!entry.command) continue;
11292
+ const command = expandCommand(entry.command, env);
11293
+ const { stdout, error } = runner ? runner(command, cwd, eventName) : {
11294
+ stdout: "",
11295
+ error: NOT_RUN
11296
+ };
11297
+ const tokens = countTokens(stdout);
11298
+ totalsByEvent[eventName] += tokens;
11299
+ const seen = observed.get(eventName);
11300
+ readings.push({
11301
+ event: eventName,
11302
+ matcher: group.matcher,
11303
+ command,
11304
+ tokens,
11305
+ error,
11306
+ state: commandHasMissingFile(command) ? "missing-script" : seen ? "observed" : "configured",
11307
+ lastSeen: seen?.last
11308
+ });
11309
+ }
11310
+ }
11311
+ for (const group of settings.hooks?.PreToolUse ?? []) for (const entry of group.hooks ?? []) {
11312
+ if (!entry.command) continue;
11313
+ preToolUseHooks.push({
11314
+ matcher: group.matcher,
11315
+ command: expandCommand(entry.command, env)
11316
+ });
11317
+ if (group.matcher === "Bash") preToolUseRewritesBash = true;
11318
+ }
11319
+ }
11320
+ return {
11321
+ encoding: TOKEN_ENCODING,
11322
+ settingsFiles,
11323
+ readings,
11324
+ totalsByEvent,
11325
+ preToolUseHooks,
11326
+ preToolUseRewritesBash
11327
+ };
11328
+ }
11329
+ /** Sync variant used by auditHooks; same folding as observedHookEvents. */
11330
+ function observedHookEventsSync(captureDir, sinceMs) {
11331
+ const observed = /* @__PURE__ */ new Map();
11332
+ if (!existsSync(captureDir)) return observed;
11333
+ const monthDirs = readdirSync(captureDir, { withFileTypes: true }).filter((d) => d.isDirectory() && /^\d{4}-\d{2}$/.test(d.name)).map((d) => join(captureDir, d.name));
11334
+ for (const monthDir of monthDirs) for (const name of readdirSync(monthDir)) {
11335
+ if (!name.endsWith(".jsonl")) continue;
11336
+ const file = join(monthDir, name);
11337
+ let text;
11338
+ try {
11339
+ if (statSync(file).mtimeMs < sinceMs) continue;
11340
+ text = readFileSync(file, "utf8");
11341
+ } catch {
11342
+ continue;
11343
+ }
11344
+ for (const raw of text.split("\n")) foldObservedLine(raw, observed);
11345
+ }
11346
+ return observed;
11347
+ }
11348
+ function foldObservedLine(raw, observed) {
11349
+ if (!raw.includes("hook_event_type")) return;
11350
+ let parsed;
11351
+ try {
11352
+ parsed = JSON.parse(raw);
11353
+ } catch {
11354
+ return;
11355
+ }
11356
+ const event = parsed.hook_event_type;
11357
+ if (!event) return;
11358
+ const ts = typeof parsed.timestamp === "number" ? parsed.timestamp : Date.now();
11359
+ const last = new Date(ts).toISOString();
11360
+ const entry = observed.get(event);
11361
+ if (entry) {
11362
+ entry.count += 1;
11363
+ if (last > entry.last) entry.last = last;
11364
+ } else observed.set(event, {
11365
+ count: 1,
11366
+ last
11367
+ });
11368
+ }
11369
+
11140
11370
  //#endregion
11141
11371
  //#region src/builds/doctor.ts
11142
11372
  /**
@@ -11173,6 +11403,33 @@ function doctor(cwd) {
11173
11403
  findings
11174
11404
  };
11175
11405
  }
11406
+ /**
11407
+ * One line per hook event: whether it is configured in settings.json and
11408
+ * whether a capture proves it actually ran in the last `sinceHours`. A hook
11409
+ * that is configured but never observed is the classic silent-failure case.
11410
+ */
11411
+ async function hookEventFindings(cwd, opts = {}) {
11412
+ const sinceHours = opts.sinceHours ?? DEFAULT_SINCE_HOURS;
11413
+ const now = opts.now ?? Date.now();
11414
+ const report = auditHooks(cwd, {
11415
+ sinceHours,
11416
+ captureDir: opts.captureDir,
11417
+ now
11418
+ });
11419
+ const observed = await observedHookEvents(opts.captureDir ?? defaultCaptureDir(), now - sinceHours * 36e5);
11420
+ const configuredEvents = new Set(report.readings.map((r) => r.event));
11421
+ const findings = [];
11422
+ for (const event of OBSERVABLE_EVENTS) {
11423
+ const seen = observed.get(event);
11424
+ findings.push({
11425
+ event,
11426
+ configured: configuredEvents.has(event),
11427
+ observed: !!seen,
11428
+ lastSeen: seen?.last
11429
+ });
11430
+ }
11431
+ return findings;
11432
+ }
11176
11433
 
11177
11434
  //#endregion
11178
11435
  //#region src/cli/commands/build.ts
@@ -11313,14 +11570,19 @@ const nextCmd = {
11313
11570
  };
11314
11571
  const doctorCmd = {
11315
11572
  name: "doctor",
11316
- summary: "Check the project's registered build doors",
11317
- description: "Reads .pai/builds.yaml above the current directory and flags every door that is missing or never calls `pai build lock`, which would bypass the queue.",
11573
+ summary: "Check the project's registered build doors and hook events",
11574
+ description: "Reads .pai/builds.yaml above the current directory and flags every door that is missing or never calls `pai build lock`, which would bypass the queue. Also prints one line per hook event: observed (a capture proves it ran) or configured-but-never-observed (warning — the hook may silently not be running).",
11318
11575
  examples: ["pai build doctor"],
11319
- run: (ctx) => {
11576
+ run: async (ctx) => {
11320
11577
  const r = doctor(process.cwd());
11321
- if (!r.root) return out$2("no .pai/builds.yaml here: no doors registered"), 0;
11322
- for (const f of r.findings) out$2(`${f.ok ? "ok " : "FAIL"} ${f.door}${f.problem ? ` — ${f.problem}` : ""}`);
11323
- if (!r.findings.length) out$2("no doors registered");
11578
+ if (!r.root) out$2("no .pai/builds.yaml here: no doors registered");
11579
+ else {
11580
+ for (const f of r.findings) out$2(`${f.ok ? "ok " : "FAIL"} ${f.door}${f.problem ? ` — ${f.problem}` : ""}`);
11581
+ if (!r.findings.length) out$2("no doors registered");
11582
+ }
11583
+ const sinceHours = 24;
11584
+ for (const h of await hookEventFindings(process.cwd(), { sinceHours })) if (h.observed) out$2(`ok ${h.event}: observed ${h.lastSeen ?? ""}`.trimEnd());
11585
+ else if (h.configured) out$2(`WARN ${h.event}: configured, never observed in ${sinceHours}h`);
11324
11586
  return r.findings.every((f) => f.ok) ? 0 : 1;
11325
11587
  }
11326
11588
  };
@@ -11997,8 +12259,8 @@ async function escalate(task, headline, detail, state, now, opts) {
11997
12259
  state.alarmedAt[task.id] = now;
11998
12260
  let delivered = false;
11999
12261
  try {
12000
- const { routeNotification } = await import("../router-DwHJRgG9.mjs");
12001
- const { loadConfig } = await import("../config-Dv3t7xnT.mjs");
12262
+ const { routeNotification } = await import("../router-ghujlEFb.mjs");
12263
+ const { loadConfig } = await import("../config-DHgPx3ER.mjs");
12002
12264
  delivered = (await routeNotification({
12003
12265
  event: "error",
12004
12266
  title: `PAI: ${headline}`,
@@ -14940,7 +15202,7 @@ async function migrateToPostgres(opts = {}) {
14940
15202
  const dump = pgDump(container, pgConfig, dumpPath);
14941
15203
  if (!dump.ok) return refused(dump.reason, dryRun);
14942
15204
  }
14943
- const { PostgresBackend } = await import("../postgres-CBHY5PIi.mjs");
15205
+ const { PostgresBackend } = await import("../postgres-B1B80bcj.mjs");
14944
15206
  await PostgresBackend.ensureDatabase(pgConfig);
14945
15207
  const sqlite = new BetterSqlite3(sqliteFederationPath, { readonly: true });
14946
15208
  const registrySqlite = sqliteRegistryPath === sqliteFederationPath ? sqlite : new BetterSqlite3(sqliteRegistryPath, { readonly: true });
@@ -16327,7 +16589,7 @@ const out = (s) => process.stdout.write(s + "\n");
16327
16589
  async function repoOf(project) {
16328
16590
  if (typeof project === "string" && project) {
16329
16591
  if (existsSync(project)) return resolve(project);
16330
- const { getProject } = await import("../helpers-WV8tdQHM.mjs");
16592
+ const { getProject } = await import("../helpers-DyHKAwrW.mjs");
16331
16593
  const row = await getProject(project);
16332
16594
  if (!row) throw new CliError(`no project ${project}`, "pai projects list");
16333
16595
  return row.root_path;
@@ -16556,7 +16818,7 @@ const reviewCmd = {
16556
16818
  return 0;
16557
16819
  }
16558
16820
  };
16559
- const listCmd = {
16821
+ const listCmd$2 = {
16560
16822
  name: "list",
16561
16823
  summary: "Every item of the project with its state",
16562
16824
  description: "States: pending, running, ready, too-big, needs-decision, cloud, accepted, rejected.",
@@ -16650,7 +16912,7 @@ const queueCommand = {
16650
16912
  addCmd,
16651
16913
  runCmd,
16652
16914
  reviewCmd,
16653
- listCmd,
16915
+ listCmd$2,
16654
16916
  acceptCmd,
16655
16917
  rejectCmd,
16656
16918
  againCmd
@@ -17150,7 +17412,7 @@ async function cmdPromote(opts) {
17150
17412
  console.error(dim$1(` Pick a durable location for the new project.`));
17151
17413
  process.exit(1);
17152
17414
  }
17153
- const { getRegistryBackend } = await import("../factory-DeASMV7Z.mjs");
17415
+ const { getRegistryBackend } = await import("../factory-DFwez99M.mjs");
17154
17416
  const registryBackend = await getRegistryBackend();
17155
17417
  const encodedDir = encodeDir(targetPath);
17156
17418
  if (await registryBackend.getProjectBySlug(slug) ?? await registryBackend.getProjectByRootPath(targetPath) ?? await registryBackend.getProjectByEncodedDir(encodedDir)) {
@@ -18394,7 +18656,7 @@ async function cmdUnregister(slug, opts = {}) {
18394
18656
  * deprecated alias that forwards here with a stderr notice.
18395
18657
  */
18396
18658
  async function projectListCmd(opts) {
18397
- const { cmdMain } = await import("../main-resolver-CAC3xfZM.mjs");
18659
+ const { cmdMain } = await import("../main-resolver-CN_sfG1R.mjs");
18398
18660
  await cmdMain(void 0, void 0, opts);
18399
18661
  return 0;
18400
18662
  }
@@ -19817,7 +20079,7 @@ async function registryScanCmd(opts) {
19817
20079
  return 0;
19818
20080
  }
19819
20081
  if (opts.filterReport) {
19820
- const { reportUnregistrable } = await import("../registrable-Drn8wIDO.mjs");
20082
+ const { reportUnregistrable } = await import("../registrable-Ct0DKNKS.mjs");
19821
20083
  const r = await reportUnregistrable(await getRegistryBackend());
19822
20084
  console.log(`Registry rows: ${r.total}`);
19823
20085
  console.log(` matching the registration filter: ${r.matching} (${r.matchingMissing} with a missing path)`);
@@ -21660,7 +21922,7 @@ async function runEmbed(backend, projectId, projectSlug, batchSize = 50, pageSiz
21660
21922
  const label = projectSlug ? `project ${projectSlug}` : "all projects";
21661
21923
  console.log(dim$1(`Generating embeddings for ${label} (this may take a while on first run)...`));
21662
21924
  const { serializeEmbedding } = await import("../embeddings-CIkJBKd-.mjs");
21663
- const { gateBackend } = await import("../embedding-gate-B98PK_wz.mjs");
21925
+ const { gateBackend } = await import("../embedding-gate-BVQvUmox.mjs");
21664
21926
  const gate = await gateBackend(backend, { record: true });
21665
21927
  if (!gate.ok) {
21666
21928
  console.error(err(gate.reason));
@@ -21989,7 +22251,7 @@ async function memorySearchCmd(query, opts) {
21989
22251
  let results;
21990
22252
  if (mode === "keyword") results = await backend.searchKeyword(query, searchOpts);
21991
22253
  else {
21992
- const { embedQuery } = await import("../embedding-gate-B98PK_wz.mjs");
22254
+ const { embedQuery } = await import("../embedding-gate-BVQvUmox.mjs");
21993
22255
  console.log(dim$1("Generating query embedding..."));
21994
22256
  const q = await embedQuery(backend, query);
21995
22257
  const queryEmbedding = q.vec;
@@ -22069,8 +22331,8 @@ async function memorySearchCmd(query, opts) {
22069
22331
  //#region src/cli/commands/memory/sources-cmd.ts
22070
22332
  /** `pai memory sources` — what the indexer has taken in. */
22071
22333
  async function memorySourcesCmd(opts) {
22072
- const { createStorageBackend } = await import("../factory-DeASMV7Z.mjs");
22073
- const { loadConfig } = await import("../config-Dv3t7xnT.mjs");
22334
+ const { createStorageBackend } = await import("../factory-DFwez99M.mjs");
22335
+ const { loadConfig } = await import("../config-DHgPx3ER.mjs");
22074
22336
  const { cmdMemorySources } = await import("../sources-CiE3rZek.mjs");
22075
22337
  let backend;
22076
22338
  try {
@@ -22830,7 +23092,7 @@ async function zettelExploreCmd(note, opts) {
22830
23092
  const depth = opts.depth ?? 3;
22831
23093
  const direction = opts.direction ?? "both";
22832
23094
  const mode = opts.mode ?? "all";
22833
- const { zettelExplore } = await import("../zettelkasten-BHVx5zj-.mjs");
23095
+ const { zettelExplore } = await import("../zettelkasten-BH8AdNh8.mjs");
22834
23096
  const result = await zettelExplore(await getFedBackend(), {
22835
23097
  startNote: note,
22836
23098
  depth,
@@ -22886,7 +23148,7 @@ async function zettelHealthCmd(opts) {
22886
23148
  const projectPath = opts.project;
22887
23149
  const recentDays = opts.days ?? 30;
22888
23150
  const includeTypes = opts.include ? opts.include.split(",").map((s) => s.trim()) : void 0;
22889
- const { zettelHealth } = await import("../zettelkasten-BHVx5zj-.mjs");
23151
+ const { zettelHealth } = await import("../zettelkasten-BH8AdNh8.mjs");
22890
23152
  const result = await zettelHealth(await getFedBackend(), {
22891
23153
  scope,
22892
23154
  projectPath,
@@ -22944,7 +23206,7 @@ async function zettelSurpriseCmd(note, opts) {
22944
23206
  const limit = opts.limit ?? 10;
22945
23207
  const minSimilarity = opts.minSimilarity ?? .3;
22946
23208
  const minGraphDistance = opts.minDistance ?? 3;
22947
- const { zettelSurprise } = await import("../zettelkasten-BHVx5zj-.mjs");
23209
+ const { zettelSurprise } = await import("../zettelkasten-BH8AdNh8.mjs");
22948
23210
  const backend = await getFedBackend();
22949
23211
  console.log();
22950
23212
  console.log(header(" PAI Zettel Surprise"));
@@ -22986,7 +23248,7 @@ async function zettelConverseCmd(question, opts) {
22986
23248
  const vaultProjectId = Number(opts.vaultProjectId);
22987
23249
  const depth = opts.depth ?? 2;
22988
23250
  const limit = opts.limit ?? 15;
22989
- const { zettelConverse } = await import("../zettelkasten-BHVx5zj-.mjs");
23251
+ const { zettelConverse } = await import("../zettelkasten-BH8AdNh8.mjs");
22990
23252
  const backend = await getFedBackend();
22991
23253
  console.log();
22992
23254
  console.log(header(" PAI Zettel Converse"));
@@ -23036,7 +23298,7 @@ async function zettelThemesCmd(opts) {
23036
23298
  const minClusterSize = opts.minSize ?? 3;
23037
23299
  const maxThemes = opts.maxThemes ?? 10;
23038
23300
  const similarityThreshold = opts.threshold ?? .65;
23039
- const { zettelThemes } = await import("../zettelkasten-BHVx5zj-.mjs");
23301
+ const { zettelThemes } = await import("../zettelkasten-BH8AdNh8.mjs");
23040
23302
  const backend = await getFedBackend();
23041
23303
  console.log();
23042
23304
  console.log(header(" PAI Zettel Themes"));
@@ -23084,7 +23346,7 @@ async function zettelSuggestCmd(note, opts) {
23084
23346
  const vaultProjectId = Number(opts.vaultProjectId);
23085
23347
  const limit = opts.limit ?? 5;
23086
23348
  const excludeLinked = opts.excludeLinked !== true;
23087
- const { zettelSuggest } = await import("../zettelkasten-BHVx5zj-.mjs");
23349
+ const { zettelSuggest } = await import("../zettelkasten-BH8AdNh8.mjs");
23088
23350
  const backend = await getFedBackend();
23089
23351
  console.log();
23090
23352
  console.log(header(" PAI Zettel Suggest"));
@@ -24083,28 +24345,6 @@ const reloadCommand = {
24083
24345
  }
24084
24346
  };
24085
24347
 
24086
- //#endregion
24087
- //#region src/audit/tokens.ts
24088
- /**
24089
- * tokens.ts — the one tokenizer every audit section counts through.
24090
- *
24091
- * cl100k_base via js-tiktoken (pure JS, no native build). "all" + [] for the
24092
- * allowed/disallowed-special-token arguments matches Python tiktoken's
24093
- * `disallowed_special=()`: audited text (hook stdout, session transcripts)
24094
- * routinely contains literal "<|...|>"-shaped substrings that are not actual
24095
- * special tokens, and the strict default throws on sight of them.
24096
- */
24097
- const TOKEN_ENCODING = "cl100k_base";
24098
- let cached = null;
24099
- function encoder() {
24100
- if (!cached) cached = getEncoding(TOKEN_ENCODING);
24101
- return cached;
24102
- }
24103
- function countTokens(text) {
24104
- if (!text) return 0;
24105
- return encoder().encode(text, "all", []).length;
24106
- }
24107
-
24108
24348
  //#endregion
24109
24349
  //#region src/audit/files.ts
24110
24350
  /**
@@ -24198,149 +24438,41 @@ function auditFiles(paths) {
24198
24438
  }
24199
24439
 
24200
24440
  //#endregion
24201
- //#region src/audit/hooks.ts
24441
+ //#region src/audit/first-turn.ts
24202
24442
  /**
24203
- * `pai audit tokens hooks` — token cost of the hooks that fire before the
24204
- * model ever sees a token: SessionStart and UserPromptSubmit run on every
24205
- * new session / every prompt respectively, and whatever they print to
24206
- * stdout is injected into context.
24443
+ * first-turn.ts — where a turn's context actually comes from, attributed
24444
+ * line by line from the transcript.
24445
+ *
24446
+ * The API reports one number per assistant turn (cache_read + cache_creation
24447
+ * + input). The transcript holds the pieces Claude Code injected since the
24448
+ * previous turn: SessionStart / UserPromptSubmit hook output, the skill
24449
+ * listing, the deferred-tool name list, MCP server instructions, the human
24450
+ * prompt, any isMeta expansion (a skill body, a pasted slash-command result),
24451
+ * tool results, and the previous turn's own visible output (re-sent as
24452
+ * context on every later turn). Each of those is counted in cl100k here;
24453
+ * what is left over is the system prompt, the built-in tool schemas, agents
24454
+ * and memory files, which never appear in the transcript.
24455
+ *
24456
+ * Only the live lineage counts. Lines chain by parentUuid, and a prompt the
24457
+ * user abandoned before a reply (Escape, re-submit) leaves a dead branch
24458
+ * whose lines were never sent — run 9 measured a session whose first
24459
+ * /Name was abandoned and read 24k where the true baseline is 30k, because
24460
+ * the 3.5k-token skill listing sat on the dead branch.
24461
+ *
24462
+ * `type:"system"` lines with subtype local_command hold a slash command's
24463
+ * raw stdout, and the model receives it verbatim in addition to the isMeta
24464
+ * markdown copy — run 11 measured both present on the same turn, so both are
24465
+ * counted.
24207
24466
  */
24208
- const HOOK_TIMEOUT_MS = 2e4;
24209
- function settingsPaths(cwd) {
24210
- return [join(claudeHomeDir(), "settings.json"), join(cwd, ".claude", "settings.json")].filter(existsSync);
24211
- }
24212
- function readSettings(path) {
24213
- try {
24214
- return JSON.parse(readFileSync(path, "utf8"));
24215
- } catch {
24216
- return {};
24467
+ function parseLines$1(path) {
24468
+ const lines = [];
24469
+ for (const raw of readFileSync(path, "utf8").split("\n")) {
24470
+ if (!raw.trim()) continue;
24471
+ try {
24472
+ lines.push(JSON.parse(raw));
24473
+ } catch {}
24217
24474
  }
24218
- }
24219
- /** Expand ${PAI_DIR} and $HOME/${HOME} the way settings.json's own hooks do. */
24220
- function expandCommand(command, env) {
24221
- const paiDir = env.PAI_DIR ?? process.env.PAI_DIR ?? claudeHomeDir();
24222
- return command.replace(/\$\{PAI_DIR\}/g, paiDir).replace(/\$\{HOME\}/g, homedir()).replace(/\$HOME\b/g, homedir());
24223
- }
24224
- function syntheticStdin(cwd, eventName) {
24225
- const sessionId = randomUUID();
24226
- return JSON.stringify({
24227
- session_id: sessionId,
24228
- cwd,
24229
- hook_event_name: eventName,
24230
- prompt: "audit",
24231
- transcript_path: join(claudeProjectsDir(), "audit", `${sessionId}.jsonl`)
24232
- });
24233
- }
24234
- function runHook(command, cwd, eventName) {
24235
- const result = spawnSync(command, {
24236
- shell: true,
24237
- cwd,
24238
- input: syntheticStdin(cwd, eventName),
24239
- encoding: "utf8",
24240
- timeout: HOOK_TIMEOUT_MS
24241
- });
24242
- if (result.error) return {
24243
- stdout: "",
24244
- error: result.error.message
24245
- };
24246
- if (result.signal) return {
24247
- stdout: result.stdout ?? "",
24248
- error: `killed by ${result.signal} (timeout?)`
24249
- };
24250
- return { stdout: result.stdout ?? "" };
24251
- }
24252
- const NOT_RUN = "not run (pass --run to execute hooks)";
24253
- const AUDITED_EVENTS = ["SessionStart", "UserPromptSubmit"];
24254
- /**
24255
- * Lists every SessionStart / UserPromptSubmit hook. Without `run` the hooks are only
24256
- * listed (tokens 0, marked not run): a project's settings.json is untrusted input and
24257
- * its hooks execute with shell:true. Pass `runHook` (CLI `--run`) to measure them.
24258
- */
24259
- function auditHooks(cwd, run) {
24260
- const settingsFiles = settingsPaths(cwd);
24261
- const readings = [];
24262
- const totalsByEvent = {};
24263
- const preToolUseHooks = [];
24264
- let preToolUseRewritesBash = false;
24265
- for (const settingsPath of settingsFiles) {
24266
- const settings = readSettings(settingsPath);
24267
- const env = settings.env ?? {};
24268
- for (const eventName of AUDITED_EVENTS) {
24269
- const groups = settings.hooks?.[eventName] ?? [];
24270
- totalsByEvent[eventName] = totalsByEvent[eventName] ?? 0;
24271
- for (const group of groups) for (const entry of group.hooks ?? []) {
24272
- if (!entry.command) continue;
24273
- const command = expandCommand(entry.command, env);
24274
- const { stdout, error } = run ? run(command, cwd, eventName) : {
24275
- stdout: "",
24276
- error: NOT_RUN
24277
- };
24278
- const tokens = countTokens(stdout);
24279
- totalsByEvent[eventName] += tokens;
24280
- readings.push({
24281
- event: eventName,
24282
- matcher: group.matcher,
24283
- command,
24284
- tokens,
24285
- error
24286
- });
24287
- }
24288
- }
24289
- for (const group of settings.hooks?.PreToolUse ?? []) for (const entry of group.hooks ?? []) {
24290
- if (!entry.command) continue;
24291
- preToolUseHooks.push({
24292
- matcher: group.matcher,
24293
- command: expandCommand(entry.command, env)
24294
- });
24295
- if (group.matcher === "Bash") preToolUseRewritesBash = true;
24296
- }
24297
- }
24298
- return {
24299
- encoding: TOKEN_ENCODING,
24300
- settingsFiles,
24301
- readings,
24302
- totalsByEvent,
24303
- preToolUseHooks,
24304
- preToolUseRewritesBash
24305
- };
24306
- }
24307
-
24308
- //#endregion
24309
- //#region src/audit/first-turn.ts
24310
- /**
24311
- * first-turn.ts — where a turn's context actually comes from, attributed
24312
- * line by line from the transcript.
24313
- *
24314
- * The API reports one number per assistant turn (cache_read + cache_creation
24315
- * + input). The transcript holds the pieces Claude Code injected since the
24316
- * previous turn: SessionStart / UserPromptSubmit hook output, the skill
24317
- * listing, the deferred-tool name list, MCP server instructions, the human
24318
- * prompt, any isMeta expansion (a skill body, a pasted slash-command result),
24319
- * tool results, and the previous turn's own visible output (re-sent as
24320
- * context on every later turn). Each of those is counted in cl100k here;
24321
- * what is left over is the system prompt, the built-in tool schemas, agents
24322
- * and memory files, which never appear in the transcript.
24323
- *
24324
- * Only the live lineage counts. Lines chain by parentUuid, and a prompt the
24325
- * user abandoned before a reply (Escape, re-submit) leaves a dead branch
24326
- * whose lines were never sent — run 9 measured a session whose first
24327
- * /Name was abandoned and read 24k where the true baseline is 30k, because
24328
- * the 3.5k-token skill listing sat on the dead branch.
24329
- *
24330
- * `type:"system"` lines with subtype local_command hold a slash command's
24331
- * raw stdout, and the model receives it verbatim in addition to the isMeta
24332
- * markdown copy — run 11 measured both present on the same turn, so both are
24333
- * counted.
24334
- */
24335
- function parseLines(path) {
24336
- const lines = [];
24337
- for (const raw of readFileSync(path, "utf8").split("\n")) {
24338
- if (!raw.trim()) continue;
24339
- try {
24340
- lines.push(JSON.parse(raw));
24341
- } catch {}
24342
- }
24343
- return lines;
24475
+ return lines;
24344
24476
  }
24345
24477
  function classify(line) {
24346
24478
  if (line.type === "attachment") {
@@ -24406,7 +24538,7 @@ function apiContextOf(usage) {
24406
24538
  */
24407
24539
  function turnBreakdown(path, turn) {
24408
24540
  if (!existsSync(path)) return null;
24409
- const lines = parseLines(path);
24541
+ const lines = parseLines$1(path);
24410
24542
  const byUuid = /* @__PURE__ */ new Map();
24411
24543
  for (const l of lines) if (l.uuid) byUuid.set(l.uuid, l);
24412
24544
  const seen = /* @__PURE__ */ new Set();
@@ -24512,7 +24644,7 @@ function firstTurnBreakdown(path) {
24512
24644
  if (!existsSync(path)) return empty;
24513
24645
  const tb = turnBreakdown(path, 1);
24514
24646
  if (!tb) return empty;
24515
- const lines = parseLines(path);
24647
+ const lines = parseLines$1(path);
24516
24648
  const byUuid = /* @__PURE__ */ new Map();
24517
24649
  let firstIndex = -1;
24518
24650
  for (let i = 0; i < lines.length; i++) {
@@ -24759,8 +24891,8 @@ function deriveSessionTrigger(transcriptPath, env = process.env, now = Date.now(
24759
24891
  * and the cache-TTL split (ephemeral 5m vs 1h) that says which cache the
24760
24892
  * session is riding.
24761
24893
  */
24762
- /** Newest *.jsonl under ~/.claude/projects, excluding subagents/. */
24763
- function newestSessionLog() {
24894
+ /** Newest *.jsonl under ~/.claude/projects, excluding subagents/. With a session id, that session's file instead. */
24895
+ function newestSessionLog(sessionId) {
24764
24896
  const projectsDir = claudeProjectsDir();
24765
24897
  let best = null;
24766
24898
  let projectDirs;
@@ -24779,6 +24911,7 @@ function newestSessionLog() {
24779
24911
  }
24780
24912
  for (const entry of entries) {
24781
24913
  if (!entry.endsWith(".jsonl")) continue;
24914
+ if (sessionId && entry !== `${sessionId}.jsonl`) continue;
24782
24915
  const filePath = join(dirPath, entry);
24783
24916
  let mtime;
24784
24917
  try {
@@ -26360,12 +26493,14 @@ function hookRunner(run) {
26360
26493
  console.error(warn$1("--run executes every SessionStart/UserPromptSubmit hook in ~/.claude/settings.json and ./.claude/settings.json with a shell. Do not use it on a repository you do not trust."));
26361
26494
  return runHook;
26362
26495
  }
26363
- function printHooks(report) {
26496
+ function printHooks(report, sinceHours) {
26364
26497
  console.log(header(`Hook output (encoding: ${report.encoding})`));
26365
26498
  console.log(dim$1(`settings.json: ${report.settingsFiles.join(", ") || "none found"}`));
26366
26499
  const rows = report.readings.map((r) => [
26367
26500
  r.event,
26368
26501
  r.matcher ?? "",
26502
+ r.state,
26503
+ r.lastSeen ?? "",
26369
26504
  String(r.tokens),
26370
26505
  r.error ? err(r.error) : "",
26371
26506
  r.command
@@ -26373,10 +26508,14 @@ function printHooks(report) {
26373
26508
  console.log(renderTable$1([
26374
26509
  "event",
26375
26510
  "matcher",
26511
+ "state",
26512
+ "last seen",
26376
26513
  "tokens",
26377
26514
  "error",
26378
26515
  "command"
26379
26516
  ], rows));
26517
+ for (const r of report.readings) if (r.state === "missing-script") console.log(err(` RED: ${r.event} hook script does not exist: ${r.command}`));
26518
+ else if (r.state === "configured") console.log(warn$1(` AMBER: ${r.event} hook configured, never observed in ${sinceHours}h — it may not be running`));
26380
26519
  for (const [event, total] of Object.entries(report.totalsByEvent)) console.log(bold$1(`${event} total: ${total} tokens`));
26381
26520
  console.log(dim$1(`PreToolUse hooks: ${report.preToolUseHooks.length}` + (report.preToolUseRewritesBash ? ", including a Bash-matcher rewrite hook" : "")));
26382
26521
  }
@@ -26702,7 +26841,7 @@ const DEFAULT_CTX_THRESHOLD = 2e5;
26702
26841
  async function gatherCombined() {
26703
26842
  const cwd = process.cwd();
26704
26843
  const files = auditFiles(defaultFileSet(cwd));
26705
- const hooks = auditHooks(cwd);
26844
+ const hooks = auditHooks(cwd, { sinceHours: DEFAULT_SINCE_HOURS });
26706
26845
  const session = await auditSession(newestSessionLog() ?? void 0).catch(() => null);
26707
26846
  const daemon = auditDaemon();
26708
26847
  const env = auditEnv();
@@ -26761,7 +26900,7 @@ function combinedSectionOutputs(data) {
26761
26900
  });
26762
26901
  sections.push({
26763
26902
  name: "hooks",
26764
- text: captureOutput(() => printHooks(data.hooks))
26903
+ text: captureOutput(() => printHooks(data.hooks, DEFAULT_SINCE_HOURS))
26765
26904
  });
26766
26905
  if (data.session) sections.push({
26767
26906
  name: "session",
@@ -26844,6 +26983,71 @@ function recordCombinedReport(dir, data) {
26844
26983
  for (const f of data.findings) counts[f.severity]++;
26845
26984
  appendFileSync(runsPath, `- ${date} run${runNumber}: ${counts.RED} RED, ${counts.AMBER} AMBER, ${counts.GREEN} GREEN — ${baseName}.md\n`, "utf8");
26846
26985
  }
26986
+ const SCAN_SEVERITIES = [
26987
+ "block",
26988
+ "confirm",
26989
+ "warn"
26990
+ ];
26991
+ /**
26992
+ * `pai audit scan`: run the rule set over a transcript after the fact —
26993
+ * single-step rules per tool call, sequence rules correlated across the run.
26994
+ * Exit 0 when nothing hit, 1 when any block-severity finding did.
26995
+ */
26996
+ async function auditScanCmd(opts) {
26997
+ let target = null;
26998
+ if (opts.worker) {
26999
+ const { workers } = readWorkersSection();
27000
+ const logDir = workersLogDir(workers);
27001
+ const st = loadStatus(logDir, opts.worker);
27002
+ if (!st) {
27003
+ console.error(err(` No worker named ${opts.worker}.`));
27004
+ return 1;
27005
+ }
27006
+ target = st.claudeSession ? claudeSessionTranscript(st.claudeSession, st.cwd) : eventsPath(logDir, opts.worker);
27007
+ if (target && !existsSync(target)) target = null;
27008
+ } else if (opts.session) target = newestSessionLog(opts.session);
27009
+ else if (opts.path) target = existsSync(opts.path) ? opts.path : null;
27010
+ else target = newestSessionLog();
27011
+ if (!target) {
27012
+ console.error(err(" No transcript found."));
27013
+ return 1;
27014
+ }
27015
+ const { rules, errors } = loadRules(userRuleDirs(process.cwd()), { projectDirs: projectRuleDirs(process.cwd()) });
27016
+ for (const e of errors) console.error(dim$1(`rule: ${e}`));
27017
+ const findings = await scanTranscript(target, rules);
27018
+ if (opts.json) {
27019
+ printJson({
27020
+ path: target,
27021
+ findings
27022
+ });
27023
+ return findings.some((f) => f.severity === "block") ? 1 : 0;
27024
+ }
27025
+ console.log(header(`Security scan: ${target}`));
27026
+ console.log(dim$1(`${rules.length} rules, ${findings.length} findings`));
27027
+ for (const sev of SCAN_SEVERITIES) {
27028
+ const hits = findings.filter((f) => f.severity === sev);
27029
+ if (!hits.length) continue;
27030
+ console.log(header(sev));
27031
+ const rows = hits.map((f) => [
27032
+ severityColor(sev === "block" ? "RED" : "AMBER", sev),
27033
+ f.ruleId,
27034
+ f.tool,
27035
+ String(f.line),
27036
+ f.ts ? f.ts.slice(11, 19) : "",
27037
+ f.evidence.replace(/\s+/g, " ").slice(0, 100)
27038
+ ]);
27039
+ console.log(renderTable$1([
27040
+ "severity",
27041
+ "rule",
27042
+ "tool",
27043
+ "line",
27044
+ "time",
27045
+ "evidence"
27046
+ ], rows));
27047
+ }
27048
+ if (!findings.length) console.log(ok$1("no findings"));
27049
+ return findings.some((f) => f.severity === "block") ? 1 : 0;
27050
+ }
26847
27051
  /**
26848
27052
  * `--json` is declared once, on `tokensCmd` itself, and read back from there
26849
27053
  * in every subcommand action below — not redeclared per subcommand. The parser
@@ -26864,12 +27068,16 @@ async function auditFilesCmd(paths, opts) {
26864
27068
  }
26865
27069
  /** Exported function for audit tokens hooks command */
26866
27070
  function auditHooksCmd(opts) {
26867
- const report = auditHooks(process.cwd(), hookRunner(opts.run));
27071
+ const sinceHours = opts.since ? parseInt(opts.since, 10) : DEFAULT_SINCE_HOURS;
27072
+ const report = auditHooks(process.cwd(), {
27073
+ run: hookRunner(opts.run),
27074
+ sinceHours
27075
+ });
26868
27076
  if (opts.json) {
26869
27077
  printJson(report);
26870
27078
  return 0;
26871
27079
  }
26872
- printHooks(report);
27080
+ printHooks(report, sinceHours);
26873
27081
  return 0;
26874
27082
  }
26875
27083
  /** Exported function for audit tokens session command */
@@ -27057,7 +27265,8 @@ const t = (name, summary, description, extra = {}) => a({
27057
27265
  if (name === "files") return auditFilesCmd(ctx.args.PATHS ?? [], { json: ctx.opts.json === true });
27058
27266
  if (name === "hooks") return auditHooksCmd({
27059
27267
  json: ctx.opts.json === true,
27060
- run: ctx.opts.run === true
27268
+ run: ctx.opts.run === true,
27269
+ since: ctx.opts.since
27061
27270
  });
27062
27271
  if (name === "session") return auditSessionCmd(ctx.args.PATH, {
27063
27272
  json: ctx.opts.json === true,
@@ -27130,13 +27339,22 @@ const auditCommand = {
27130
27339
  }],
27131
27340
  examples: ["pai audit tokens files", "pai audit tokens files ./CLAUDE.md --json"]
27132
27341
  }),
27133
- t("hooks", "Token cost of every start-up and prompt hook", "Lists each SessionStart and UserPromptSubmit hook. With --run it executes them and measures what each adds to the context; without it nothing is executed.", {
27342
+ t("hooks", "Token cost and observed state of every start-up and prompt hook", "Lists each SessionStart and UserPromptSubmit hook with its state: observed (a capture for that event exists in the window), configured (never observed — the hook may silently not be running), or missing-script. With --run it executes them and measures what each adds to the context; without it nothing is executed.", {
27134
27343
  options: [{
27135
27344
  name: "run",
27136
27345
  type: "boolean",
27137
27346
  summary: "Execute the hooks to measure their output (runs the repository's own hook commands; trusted repositories only)"
27347
+ }, {
27348
+ name: "since",
27349
+ type: "number",
27350
+ value: "HOURS",
27351
+ summary: "Observation window for the state column (default 24h)"
27138
27352
  }],
27139
- examples: ["pai audit tokens hooks", "pai audit tokens hooks --run"]
27353
+ examples: [
27354
+ "pai audit tokens hooks",
27355
+ "pai audit tokens hooks --run",
27356
+ "pai audit tokens hooks --since 48"
27357
+ ]
27140
27358
  }),
27141
27359
  t("session", "Token split for one session transcript", "Shows cache, input and output tokens of a session (default: the newest).", {
27142
27360
  args: [{
@@ -27243,6 +27461,1677 @@ const auditCommand = {
27243
27461
  examples: ["pai audit tokens ladder", "pai audit tokens ladder --live --model haiku"]
27244
27462
  })
27245
27463
  ]
27464
+ }, a({
27465
+ name: "scan",
27466
+ summary: "After-the-fact security scan of a session or worker transcript",
27467
+ description: "Runs the rule set (the same rules the PreToolUse hook enforces, plus two-step sequence rules that only make sense after the fact) over a transcript: single-step rules against every tool call, sequence rules correlated across the run — credential read followed by an outbound call, download followed by execute, injected text followed by a shell call. Findings are grouped by severity; evidence is masked. Exit 1 when a block-severity finding hit.",
27468
+ args: [{
27469
+ name: "PATH",
27470
+ summary: "Transcript .jsonl (default: newest session under ~/.claude/projects)"
27471
+ }],
27472
+ options: [{
27473
+ name: "session",
27474
+ type: "string",
27475
+ value: "ID",
27476
+ summary: "Scan this Claude Code session's transcript"
27477
+ }, {
27478
+ name: "worker",
27479
+ type: "string",
27480
+ value: "ID",
27481
+ summary: "Scan a worker's transcript (its Claude session, else its event log)"
27482
+ }],
27483
+ examples: [
27484
+ "pai audit scan",
27485
+ "pai audit scan --worker w-20260101-abc",
27486
+ "pai audit scan ./session.jsonl --json"
27487
+ ],
27488
+ seeAlso: ["pai rules list", "pai audit tokens session"]
27489
+ }, (ctx) => auditScanCmd({
27490
+ json: ctx.opts.json === true,
27491
+ session: ctx.opts.session,
27492
+ worker: ctx.opts.worker,
27493
+ path: ctx.args.PATH
27494
+ }))]
27495
+ };
27496
+
27497
+ //#endregion
27498
+ //#region src/session/git-notes.ts
27499
+ /** Link commits to the session/worker that wrote them via `git notes --ref=pai`. */
27500
+ const EDIT_TOOLS = new Set([
27501
+ "Edit",
27502
+ "Write",
27503
+ "MultiEdit",
27504
+ "NotebookEdit"
27505
+ ]);
27506
+ const HOOK_MARKER = "# PAI-GIT-NOTES: managed by `pai git setup`";
27507
+ const NOTES_REF = "refs/notes/pai";
27508
+ const LOOKBACK_MS = 6 * 3600 * 1e3;
27509
+ const MAX_CAPTURE_BYTES = 8 * 1024 * 1024;
27510
+ const HOOK_BUDGET_MS = 500;
27511
+ /** A refusal `pai git setup` reports to the user as-is. */
27512
+ var GitNotesError = class extends Error {};
27513
+ /** Call `onLine` for each line of `file`, reading in chunks; stops early when it returns false. */
27514
+ function eachLine(file, onLine) {
27515
+ const fd = openSync(file, "r");
27516
+ try {
27517
+ const buf = Buffer.alloc(64 * 1024);
27518
+ let rest = "";
27519
+ for (let n = readSync(fd, buf, 0, buf.length, null); n > 0; n = readSync(fd, buf, 0, buf.length, null)) {
27520
+ const parts = (rest + buf.toString("utf8", 0, n)).split("\n");
27521
+ rest = parts.pop() ?? "";
27522
+ for (const l of parts) if (!onLine(l)) return;
27523
+ }
27524
+ if (rest) onLine(rest);
27525
+ } finally {
27526
+ closeSync(fd);
27527
+ }
27528
+ }
27529
+ const real = (p) => {
27530
+ try {
27531
+ return realpathSync(p);
27532
+ } catch {
27533
+ return resolve(p);
27534
+ }
27535
+ };
27536
+ function jsonlFiles(dir, sinceMs, out = []) {
27537
+ let names;
27538
+ try {
27539
+ names = readdirSync(dir);
27540
+ } catch {
27541
+ return out;
27542
+ }
27543
+ for (const n of names) {
27544
+ const p = join(dir, n);
27545
+ try {
27546
+ const st = statSync(p);
27547
+ if (st.isDirectory()) jsonlFiles(p, sinceMs, out);
27548
+ else if (/\.jsonl(\.1)?$/.test(n) && st.mtimeMs >= sinceMs) out.push(p);
27549
+ } catch {}
27550
+ }
27551
+ return out;
27552
+ }
27553
+ /** Sessions whose Edit/Write-style tool events (after `sinceMs`) touched any of `paths`. */
27554
+ function sessionsTouching(paths, sinceMs, captureDir, deadline = Infinity) {
27555
+ const wanted = new Map(paths.map((p) => [real(p), p]));
27556
+ const bySession = /* @__PURE__ */ new Map();
27557
+ for (const file of jsonlFiles(captureDir, sinceMs)) {
27558
+ if (Date.now() > deadline) break;
27559
+ try {
27560
+ if (statSync(file).size > MAX_CAPTURE_BYTES) continue;
27561
+ } catch {
27562
+ continue;
27563
+ }
27564
+ try {
27565
+ eachLine(file, (line) => {
27566
+ if (Date.now() > deadline) return false;
27567
+ if (!line.includes("file_path") && !line.includes("notebook_path")) return true;
27568
+ let e;
27569
+ try {
27570
+ e = JSON.parse(line);
27571
+ } catch {
27572
+ return true;
27573
+ }
27574
+ const tool = e.tool ?? e.payload?.tool_name;
27575
+ if (!EDIT_TOOLS.has(tool)) return true;
27576
+ const ts = typeof e.timestamp === "number" ? e.timestamp : Date.parse(e.timestamp);
27577
+ if (Number.isFinite(ts) && ts < sinceMs) return true;
27578
+ const input = e.input ?? e.payload?.tool_input ?? {};
27579
+ const fp = input.file_path ?? input.notebook_path;
27580
+ const sessionId = e.session ?? e.session_id;
27581
+ if (typeof fp !== "string" || typeof sessionId !== "string") return true;
27582
+ const orig = wanted.get(real(fp));
27583
+ if (!orig) return true;
27584
+ const workerId = e.workerId ?? e.worker_id ?? e.env?.PAI_WORKER_ID ?? e.payload?.env?.PAI_WORKER_ID;
27585
+ const cur = bySession.get(sessionId) ?? {
27586
+ sessionId,
27587
+ files: []
27588
+ };
27589
+ if (typeof workerId === "string" && !cur.workerId) cur.workerId = workerId;
27590
+ if (!cur.files.includes(orig)) cur.files.push(orig);
27591
+ bySession.set(sessionId, cur);
27592
+ return true;
27593
+ });
27594
+ } catch {}
27595
+ }
27596
+ return [...bySession.values()];
27597
+ }
27598
+ /** Note body: one `pai-session` line per session, plus `pai-worker` where known. */
27599
+ function noteText(entries) {
27600
+ const lines = [];
27601
+ for (const e of entries) {
27602
+ lines.push(`pai-session: ${e.sessionId}`);
27603
+ if (e.workerId) lines.push(`pai-worker: ${e.workerId}`);
27604
+ }
27605
+ return lines.join("\n");
27606
+ }
27607
+ /** Attach `text` to `commit` under refs/notes/pai; false on any error. */
27608
+ function writeNote(repo, commit, text) {
27609
+ try {
27610
+ execFileSync("git", [
27611
+ "notes",
27612
+ "--ref=pai",
27613
+ "add",
27614
+ "-f",
27615
+ "-m",
27616
+ text,
27617
+ commit
27618
+ ], {
27619
+ cwd: repo,
27620
+ timeout: 3e3,
27621
+ stdio: "ignore"
27622
+ });
27623
+ return true;
27624
+ } catch {
27625
+ return false;
27626
+ }
27627
+ }
27628
+ const git = (repo, args, timeout = 1e3) => execFileSync("git", args, {
27629
+ cwd: repo,
27630
+ timeout,
27631
+ encoding: "utf8",
27632
+ stdio: [
27633
+ "ignore",
27634
+ "pipe",
27635
+ "ignore"
27636
+ ]
27637
+ }).trim();
27638
+ const gitOk = (repo, args) => {
27639
+ try {
27640
+ git(repo, args);
27641
+ return true;
27642
+ } catch {
27643
+ return false;
27644
+ }
27645
+ };
27646
+ /** The post-commit work: note the sessions that edited the files of HEAD. Never throws. */
27647
+ function postCommitHook(repo, captureDir, env = process.env, now = Date.now()) {
27648
+ const deadline = now + HOOK_BUDGET_MS;
27649
+ try {
27650
+ if (/rebase/.test(env.GIT_REFLOG_ACTION ?? "")) return false;
27651
+ if (gitOk(repo, [
27652
+ "rev-parse",
27653
+ "-q",
27654
+ "--verify",
27655
+ "REBASE_HEAD"
27656
+ ]) || gitOk(repo, [
27657
+ "rev-parse",
27658
+ "-q",
27659
+ "--verify",
27660
+ "CHERRY_PICK_HEAD"
27661
+ ])) return false;
27662
+ const top = git(repo, ["rev-parse", "--show-toplevel"]);
27663
+ const text = noteText(sessionsTouching(git(repo, [
27664
+ "diff-tree",
27665
+ "--root",
27666
+ "--no-commit-id",
27667
+ "--name-only",
27668
+ "-r",
27669
+ "HEAD"
27670
+ ]).split("\n").filter(Boolean).map((f) => join(top, f)), now - LOOKBACK_MS, captureDir, deadline));
27671
+ if (Date.now() > deadline) return false;
27672
+ return text ? writeNote(repo, "HEAD", text) : false;
27673
+ } catch {
27674
+ return false;
27675
+ }
27676
+ }
27677
+ function hookPath(repo) {
27678
+ const p = git(repo, [
27679
+ "rev-parse",
27680
+ "--git-path",
27681
+ "hooks/post-commit"
27682
+ ]);
27683
+ return isAbsolute(p) ? p : resolve(repo, p);
27684
+ }
27685
+ /** Install the post-commit hook, chaining an existing foreign hook as post-commit.pre-pai. */
27686
+ function setupGitNotes(repo) {
27687
+ const hook = hookPath(repo);
27688
+ const gitDirs = ["--git-dir", "--git-common-dir"].map((a) => real(resolve(repo, git(repo, ["rev-parse", a]))));
27689
+ const hookDir = real(dirname(hook));
27690
+ if (!gitDirs.some((d) => hookDir === d || hookDir.startsWith(d + sep))) throw new GitNotesError(`core.hooksPath points outside .git (${hook}); refusing to write a hook there. Unset it or install the hook yourself`);
27691
+ const old = `${hook}.pre-pai`;
27692
+ let chained = existsSync(old);
27693
+ if (existsSync(hook) && !readFileSync(hook, "utf8").includes(HOOK_MARKER)) {
27694
+ if (existsSync(old)) throw new GitNotesError(`${old} already exists; refusing to overwrite it. Move it away and re-run`);
27695
+ renameSync(hook, old);
27696
+ chained = true;
27697
+ }
27698
+ writeFileSync(hook, `#!/bin/sh\n${HOOK_MARKER}\n${chained ? "d=$(dirname \"$0\")\n[ -x \"$d/post-commit.pre-pai\" ] && \"$d/post-commit.pre-pai\" \"$@\"\n" : ""}( pai git hook post-commit >/dev/null 2>&1 & )\n`);
27699
+ chmodSync(hook, 493);
27700
+ git(repo, [
27701
+ "config",
27702
+ "notes.rewriteRef",
27703
+ NOTES_REF
27704
+ ]);
27705
+ return {
27706
+ hook,
27707
+ chained
27708
+ };
27709
+ }
27710
+ /** Remove the PAI hook and restore the chained one; false when PAI had no hook here. */
27711
+ function removeGitNotes(repo) {
27712
+ const hook = hookPath(repo);
27713
+ const old = `${hook}.pre-pai`;
27714
+ if (!existsSync(hook) || !readFileSync(hook, "utf8").includes(HOOK_MARKER)) return false;
27715
+ unlinkSync(hook);
27716
+ if (existsSync(old)) renameSync(old, hook);
27717
+ try {
27718
+ if (git(repo, ["config", "notes.rewriteRef"]) === NOTES_REF) git(repo, [
27719
+ "config",
27720
+ "--unset",
27721
+ "notes.rewriteRef"
27722
+ ]);
27723
+ } catch {}
27724
+ return true;
27725
+ }
27726
+ /** The PAI note of `rev`, or undefined. */
27727
+ function showNote(repo, rev = "HEAD") {
27728
+ try {
27729
+ return git(repo, [
27730
+ "notes",
27731
+ "--ref=pai",
27732
+ "show",
27733
+ rev
27734
+ ]) || void 0;
27735
+ } catch {
27736
+ return;
27737
+ }
27738
+ }
27739
+
27740
+ //#endregion
27741
+ //#region src/cli/commands/git.ts
27742
+ /** `pai git ...`: link commits to the session/worker that wrote them (git notes, ref pai). */
27743
+ const repoOrFail = () => process.cwd();
27744
+ const gitCommand = {
27745
+ name: "git",
27746
+ summary: "Link commits to the session that wrote them",
27747
+ description: "Installs a post-commit hook that records the session and worker that edited a commit's files as a git note (ref pai).",
27748
+ examples: [
27749
+ "pai git setup",
27750
+ "pai git notes",
27751
+ "pai git remove"
27752
+ ],
27753
+ seeAlso: ["pai worker ps"],
27754
+ subcommands: [
27755
+ {
27756
+ name: "setup",
27757
+ summary: "Install the post-commit hook in the current repository",
27758
+ description: "Writes .git/hooks/post-commit, keeping an existing hook as post-commit.pre-pai and calling it first, and sets notes.rewriteRef so notes follow amends and rebases.",
27759
+ examples: ["pai git setup"],
27760
+ run: () => {
27761
+ try {
27762
+ const r = setupGitNotes(repoOrFail());
27763
+ process.stdout.write(`Installed ${r.hook}${r.chained ? " (existing hook chained)" : ""}\n`);
27764
+ return 0;
27765
+ } catch (e) {
27766
+ if (e instanceof GitNotesError) throw new CliError(e.message, "Fix it and re-run pai git setup");
27767
+ throw new CliError("Not a git repository", "Run it inside a repository");
27768
+ }
27769
+ }
27770
+ },
27771
+ {
27772
+ name: "remove",
27773
+ summary: "Remove the hook and restore the previous one",
27774
+ description: "Deletes the PAI post-commit hook, puts a chained hook back and unsets notes.rewriteRef.",
27775
+ examples: ["pai git remove"],
27776
+ run: () => {
27777
+ process.stdout.write(removeGitNotes(repoOrFail()) ? "Removed the PAI post-commit hook\n" : "No PAI hook installed\n");
27778
+ return 0;
27779
+ }
27780
+ },
27781
+ {
27782
+ name: "notes",
27783
+ summary: "Show the PAI note of a commit",
27784
+ description: "Prints the session and worker recorded for a commit (default HEAD), or 'no PAI note'.",
27785
+ args: [{
27786
+ name: "REV",
27787
+ summary: "Commit to show (default HEAD)"
27788
+ }],
27789
+ examples: ["pai git notes", "pai git notes HEAD~1"],
27790
+ run: (ctx) => {
27791
+ process.stdout.write((showNote(repoOrFail(), ctx.args.REV) ?? "no PAI note") + "\n");
27792
+ return 0;
27793
+ }
27794
+ },
27795
+ {
27796
+ name: "hook",
27797
+ summary: "Git hook entry points (called by git)",
27798
+ description: "Internal: `pai git hook post-commit` is what the installed hook runs. Always exits 0.",
27799
+ examples: ["pai git hook post-commit"],
27800
+ hidden: true,
27801
+ subcommands: [{
27802
+ name: "post-commit",
27803
+ summary: "Record the sessions that edited the last commit's files",
27804
+ description: "Skips during rebase and cherry-pick; looks back 6 hours in the raw captures. Always exits 0.",
27805
+ examples: ["pai git hook post-commit"],
27806
+ run: () => {
27807
+ postCommitHook(process.cwd(), join(historyDir(), "raw-outputs"));
27808
+ return 0;
27809
+ }
27810
+ }]
27811
+ }
27812
+ ]
27813
+ };
27814
+
27815
+ //#endregion
27816
+ //#region src/cli/commands/lessons.ts
27817
+ /** `pai lessons ...`: review-gated lessons (candidate, approved, rejected, superseded). */
27818
+ const ID = {
27819
+ name: "ID",
27820
+ required: true,
27821
+ summary: "Lesson id (its file name without .md)"
27822
+ };
27823
+ function state(v) {
27824
+ if (v === void 0) return void 0;
27825
+ if (!LESSON_STATES.includes(v)) throw new CliError(`unknown state "${v}" (${LESSON_STATES.join("|")})`);
27826
+ return v;
27827
+ }
27828
+ function row(l) {
27829
+ return `${l.id} [${l.state}] ${l.kind} ${l.project} ${l.title}`;
27830
+ }
27831
+ function wrap(f) {
27832
+ try {
27833
+ return f();
27834
+ } catch (e) {
27835
+ throw new CliError(e instanceof Error ? e.message : String(e));
27836
+ }
27837
+ }
27838
+ /** Newest transcript of the current directory under ~/.claude/projects. */
27839
+ function newestTranscriptOfCwd() {
27840
+ const dir = join(claudeProjectsDir(), process.cwd().replace(/[^a-zA-Z0-9]/g, "-"));
27841
+ if (!existsSync(dir)) return null;
27842
+ const files = readdirSync(dir).filter((f) => f.endsWith(".jsonl")).map((f) => ({
27843
+ f: join(dir, f),
27844
+ m: statSync(join(dir, f)).mtimeMs
27845
+ }));
27846
+ files.sort((a, b) => b.m - a.m);
27847
+ return files[0]?.f ?? null;
27848
+ }
27849
+ const transition = (name, to, summary) => ({
27850
+ name,
27851
+ summary,
27852
+ description: `${summary}. Only candidates can be ${name}d.`,
27853
+ args: [ID],
27854
+ examples: [`pai lessons ${name} my-lesson`],
27855
+ run: (ctx) => {
27856
+ const l = wrap(() => setState(ctx.args.ID, to));
27857
+ console.log(`${l.id}: ${l.state}`);
27858
+ return 0;
27859
+ }
27860
+ });
27861
+ const lessonsCommand = {
27862
+ name: "lessons",
27863
+ summary: "Review-gated lessons for agents",
27864
+ description: "A short curated memory layer, one Markdown file per lesson in the PAI home. Candidates only reach agents once approved.",
27865
+ examples: [
27866
+ "pai lessons list --state candidate",
27867
+ "pai lessons propose",
27868
+ "pai lessons approve my-lesson"
27869
+ ],
27870
+ seeAlso: ["pai memory"],
27871
+ subcommands: [
27872
+ {
27873
+ name: "list",
27874
+ summary: "List lessons",
27875
+ description: "Lists lessons, optionally filtered by state or project.",
27876
+ options: [
27877
+ {
27878
+ name: "state",
27879
+ type: "string",
27880
+ value: "S",
27881
+ summary: "candidate, approved, rejected or superseded"
27882
+ },
27883
+ {
27884
+ name: "project",
27885
+ type: "string",
27886
+ value: "P",
27887
+ summary: "Project slug or global"
27888
+ },
27889
+ {
27890
+ ...OPT.json,
27891
+ summary: "Print JSON"
27892
+ }
27893
+ ],
27894
+ examples: ["pai lessons list", "pai lessons list --state approved --project global"],
27895
+ run: (ctx) => {
27896
+ const ls = wrap(() => listLessons({
27897
+ state: state(ctx.opts.state),
27898
+ project: ctx.opts.project
27899
+ }));
27900
+ if (ctx.opts.json === true) console.log(JSON.stringify(ls, null, 2));
27901
+ else if (ls.length === 0) console.log("No lessons.");
27902
+ else for (const l of ls) console.log(row(l));
27903
+ return 0;
27904
+ }
27905
+ },
27906
+ {
27907
+ name: "show",
27908
+ summary: "Show one lesson",
27909
+ description: "Prints a lesson with its frontmatter fields and body.",
27910
+ args: [ID],
27911
+ examples: ["pai lessons show my-lesson"],
27912
+ run: (ctx) => {
27913
+ const l = getLesson(ctx.args.ID);
27914
+ if (!l) throw new CliError(`lesson not found: ${ctx.args.ID}`);
27915
+ console.log(row(l));
27916
+ console.log(`applies when: ${l.applies_when || "(not set)"}`);
27917
+ console.log(`evidence: ${l.evidence.join(", ") || "(none)"}`);
27918
+ if (l.superseded_by) console.log(`superseded by: ${l.superseded_by}`);
27919
+ console.log(`\n${l.body}`);
27920
+ return 0;
27921
+ }
27922
+ },
27923
+ {
27924
+ name: "add",
27925
+ summary: "Add a candidate lesson",
27926
+ description: "Creates a lesson in the candidate state; approve it after review.",
27927
+ options: [
27928
+ {
27929
+ name: "title",
27930
+ type: "string",
27931
+ value: "T",
27932
+ summary: "Short title"
27933
+ },
27934
+ {
27935
+ name: "kind",
27936
+ type: "string",
27937
+ value: "K",
27938
+ summary: LESSON_KINDS.join(", ")
27939
+ },
27940
+ {
27941
+ name: "applies-when",
27942
+ type: "string",
27943
+ value: "W",
27944
+ summary: "One sentence: when the lesson applies"
27945
+ },
27946
+ {
27947
+ name: "project",
27948
+ type: "string",
27949
+ value: "P",
27950
+ summary: "Project slug (default: global)"
27951
+ },
27952
+ {
27953
+ name: "body",
27954
+ type: "string",
27955
+ value: "B",
27956
+ summary: "The lesson in 1-5 sentences"
27957
+ }
27958
+ ],
27959
+ examples: ["pai lessons add --title \"Run tsc first\" --kind workflow --applies-when \"before committing\""],
27960
+ run: (ctx) => {
27961
+ const { title, kind } = ctx.opts;
27962
+ const when = ctx.opts["applies-when"];
27963
+ if (!title || !kind || !when) throw new CliError("--title, --kind and --applies-when are required");
27964
+ const l = wrap(() => addLesson({
27965
+ title: String(title),
27966
+ kind,
27967
+ applies_when: String(when),
27968
+ project: ctx.opts.project,
27969
+ body: ctx.opts.body
27970
+ }));
27971
+ console.log(`added ${l.id} [candidate]`);
27972
+ return 0;
27973
+ }
27974
+ },
27975
+ transition("approve", "approved", "Approve a candidate"),
27976
+ transition("reject", "rejected", "Reject a candidate"),
27977
+ {
27978
+ name: "promote",
27979
+ summary: "Promote an approved lesson to a project skill",
27980
+ description: "Writes the lesson as a SKILL.md under <project>/.claude/skills/<slug>/ (and with --agents also <project>/.agents/skills/<slug>/). Refuses to overwrite an existing skill unless --force.",
27981
+ args: [ID],
27982
+ options: [
27983
+ {
27984
+ name: "project",
27985
+ type: "string",
27986
+ value: "DIR",
27987
+ summary: "Project directory (default: this directory)"
27988
+ },
27989
+ {
27990
+ name: "force",
27991
+ type: "boolean",
27992
+ summary: "Overwrite an existing SKILL.md"
27993
+ },
27994
+ {
27995
+ name: "agents",
27996
+ type: "boolean",
27997
+ summary: "Also write under .agents/skills"
27998
+ }
27999
+ ],
28000
+ examples: ["pai lessons promote my-lesson --project ~/code/app --agents"],
28001
+ run: (ctx) => {
28002
+ const paths = wrap(() => promoteLesson(ctx.args.ID, {
28003
+ projectDir: ctx.opts.project ?? process.cwd(),
28004
+ force: ctx.opts.force === true,
28005
+ agentsDir: ctx.opts.agents === true
28006
+ }));
28007
+ for (const p of paths) console.log(`promoted ${p}`);
28008
+ return 0;
28009
+ }
28010
+ },
28011
+ {
28012
+ name: "supersede",
28013
+ summary: "Replace an approved lesson by another approved one",
28014
+ description: "Marks ID superseded; the replacement given with --by must exist and be approved.",
28015
+ args: [ID],
28016
+ options: [{
28017
+ name: "by",
28018
+ type: "string",
28019
+ value: "ID2",
28020
+ summary: "The approved lesson that replaces it"
28021
+ }],
28022
+ examples: ["pai lessons supersede old-lesson --by new-lesson"],
28023
+ run: (ctx) => {
28024
+ const by = ctx.opts.by;
28025
+ if (!by) throw new CliError("--by is required");
28026
+ const l = wrap(() => setState(ctx.args.ID, "superseded", { supersededBy: by }));
28027
+ console.log(`${l.id}: superseded by ${by}`);
28028
+ return 0;
28029
+ }
28030
+ },
28031
+ {
28032
+ name: "propose",
28033
+ summary: "Add candidates from a transcript",
28034
+ description: "Finds user messages that correct the assistant (no model call) and adds each as a candidate to review.",
28035
+ args: [{
28036
+ name: "TRANSCRIPT",
28037
+ summary: "Transcript file (default: newest of this directory)"
28038
+ }],
28039
+ examples: ["pai lessons propose", "pai lessons propose ./session.jsonl"],
28040
+ run: (ctx) => {
28041
+ const path = ctx.args.TRANSCRIPT ?? newestTranscriptOfCwd();
28042
+ if (!path) throw new CliError("no transcript found for this directory");
28043
+ const added = wrap(() => proposeFromTranscript(path).map(addLesson));
28044
+ if (added.length === 0) console.log("No corrections found.");
28045
+ for (const l of added) console.log(`added ${l.id} [candidate] ${l.title}`);
28046
+ return 0;
28047
+ }
28048
+ }
28049
+ ]
28050
+ };
28051
+
28052
+ //#endregion
28053
+ //#region src/harness/claude-code.ts
28054
+ /**
28055
+ * claude-code.ts — adapter over Claude Code's JSONL transcripts:
28056
+ * `<claude home>/projects/<encoded cwd>/<session id>.jsonl`. The encoded-cwd
28057
+ * spelling is the one PAI already uses everywhere (encodeDir, src/cli/lib/utils.ts).
28058
+ */
28059
+ /** Text of a tool_result's content: a string or its text blocks joined. */
28060
+ function resultText(c) {
28061
+ if (typeof c === "string") return c;
28062
+ if (Array.isArray(c)) return c.map((b) => b && typeof b === "object" && typeof b.text === "string" ? b.text : "").join("\n");
28063
+ return "";
28064
+ }
28065
+ /** Exit code a Bash result shows ("Exit code N" header or an exitCode field); else undefined. */
28066
+ function exitCodeOf(content) {
28067
+ if (typeof content === "string") {
28068
+ const m = /^Exit code (\d+)/m.exec(content);
28069
+ return m ? Number(m[1]) : void 0;
28070
+ }
28071
+ if (content && typeof content === "object" && !Array.isArray(content)) {
28072
+ const v = content.exitCode;
28073
+ if (typeof v === "number") return v;
28074
+ }
28075
+ }
28076
+ /** Events of one parsed transcript line; nothing for lines that carry no event. */
28077
+ function eventsOfLine(harness, sessionId, obj) {
28078
+ const ts = obj.timestamp ?? "";
28079
+ const content = obj.message?.content;
28080
+ const out = [];
28081
+ const blocks = Array.isArray(content) ? content : [];
28082
+ if (obj.type === "user") {
28083
+ for (const b of blocks) if (b.type === "tool_result") out.push({
28084
+ harness,
28085
+ sessionId,
28086
+ ts,
28087
+ kind: "tool_result",
28088
+ text: resultText(b.content),
28089
+ exitCode: exitCodeOf(b.content),
28090
+ source: "transcript",
28091
+ fidelity: "observed"
28092
+ });
28093
+ if (!obj.isMeta) {
28094
+ const text = typeof content === "string" ? content : blocks.filter((b) => b.type === "text").map((b) => b.text ?? "").join("\n");
28095
+ if (text) out.push({
28096
+ harness,
28097
+ sessionId,
28098
+ ts,
28099
+ kind: "user",
28100
+ text,
28101
+ source: "transcript",
28102
+ fidelity: "observed"
28103
+ });
28104
+ }
28105
+ return out;
28106
+ }
28107
+ if (obj.type === "assistant" && Array.isArray(content)) {
28108
+ for (const b of content) if (b.type === "text" && b.text) out.push({
28109
+ harness,
28110
+ sessionId,
28111
+ ts,
28112
+ kind: "assistant",
28113
+ text: b.text,
28114
+ source: "transcript",
28115
+ fidelity: "observed"
28116
+ });
28117
+ else if (b.type === "tool_use" && b.name) out.push({
28118
+ harness,
28119
+ sessionId,
28120
+ ts,
28121
+ kind: "tool_call",
28122
+ tool: b.name,
28123
+ input: b.input,
28124
+ source: "transcript",
28125
+ fidelity: "observed"
28126
+ });
28127
+ }
28128
+ return out;
28129
+ }
28130
+ /** Session files under a project dir: top-level `<id>.jsonl` only — the
28131
+ * `sessions/` archive holds hardlinks of the same files, and subagent logs
28132
+ * live in their own subdirs. */
28133
+ function sessionFiles(dir) {
28134
+ try {
28135
+ return readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
28136
+ } catch {
28137
+ return [];
28138
+ }
28139
+ }
28140
+ const CHUNK = 64 * 1024;
28141
+ /** Parsed lines of `text`; the first and last may be cut by the chunk edge, so `partialFirst`/`partialLast` drop them. */
28142
+ function parseLines(text, partialFirst, partialLast) {
28143
+ const lines = text.split("\n");
28144
+ if (partialFirst) lines.shift();
28145
+ if (partialLast) lines.pop();
28146
+ const out = [];
28147
+ for (const raw of lines) {
28148
+ if (!raw.trim()) continue;
28149
+ try {
28150
+ out.push(JSON.parse(raw));
28151
+ } catch {}
28152
+ }
28153
+ return out;
28154
+ }
28155
+ /** The session of one transcript, reading only its first and last 64 KB; null when the file vanished. */
28156
+ function toSession$1(harness, file, dir) {
28157
+ const id = file.slice(0, -6);
28158
+ const path = join(dir, file);
28159
+ let startedAt;
28160
+ let updatedAt;
28161
+ let cwd;
28162
+ let mtime;
28163
+ try {
28164
+ const size = statSync(path).size;
28165
+ mtime = new Date(statSync(path).mtimeMs).toISOString();
28166
+ try {
28167
+ const fd = openSync(path, "r");
28168
+ try {
28169
+ const read = (pos, len) => {
28170
+ const buf = Buffer.alloc(len);
28171
+ return buf.toString("utf8", 0, readSync(fd, buf, 0, len, pos));
28172
+ };
28173
+ const head = parseLines(read(0, Math.min(size, CHUNK)), false, size > CHUNK);
28174
+ for (const obj of head) {
28175
+ if (typeof obj.cwd === "string" && obj.cwd) cwd ??= obj.cwd;
28176
+ if (typeof obj.timestamp === "string" && obj.timestamp) startedAt ??= obj.timestamp;
28177
+ if (cwd && startedAt) break;
28178
+ }
28179
+ const from = Math.max(0, size - CHUNK);
28180
+ const tail = parseLines(read(from, size - from), from > 0, false);
28181
+ for (const obj of tail) if (typeof obj.timestamp === "string" && obj.timestamp) updatedAt = obj.timestamp;
28182
+ } finally {
28183
+ closeSync(fd);
28184
+ }
28185
+ } catch {}
28186
+ } catch {
28187
+ return null;
28188
+ }
28189
+ return {
28190
+ harness,
28191
+ id,
28192
+ cwd,
28193
+ path,
28194
+ startedAt,
28195
+ updatedAt: updatedAt ?? mtime
28196
+ };
28197
+ }
28198
+ const claudeCodeAdapter = {
28199
+ name: "claude-code",
28200
+ detect() {
28201
+ return existsSync(claudeProjectsDir());
28202
+ },
28203
+ listSessions(opts = {}) {
28204
+ const projectsDir = claudeProjectsDir();
28205
+ if (!existsSync(projectsDir)) return [];
28206
+ const dirs = opts.cwd ? [encodeDir(opts.cwd)] : readdirSafe(projectsDir);
28207
+ const out = [];
28208
+ for (const d of dirs) {
28209
+ const dir = join(projectsDir, d);
28210
+ if (!existsSync(dir)) continue;
28211
+ for (const f of sessionFiles(dir)) {
28212
+ const sess = toSession$1(this.name, f, dir);
28213
+ if (sess) out.push(sess);
28214
+ }
28215
+ }
28216
+ const since = opts.sinceMs !== void 0 ? Date.now() - opts.sinceMs : void 0;
28217
+ return out.filter((s) => since === void 0 || Date.parse(s.updatedAt) >= since).sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt));
28218
+ },
28219
+ *readEvents(session) {
28220
+ let text;
28221
+ try {
28222
+ text = readFileSync(session.path, "utf8");
28223
+ } catch {
28224
+ return;
28225
+ }
28226
+ for (const raw of text.split("\n")) {
28227
+ if (!raw.trim()) continue;
28228
+ let obj;
28229
+ try {
28230
+ obj = JSON.parse(raw);
28231
+ } catch {
28232
+ continue;
28233
+ }
28234
+ yield* eventsOfLine(this.name, session.id, obj);
28235
+ }
28236
+ }
28237
+ };
28238
+ function readdirSafe(dir) {
28239
+ try {
28240
+ return readdirSync(dir);
28241
+ } catch {
28242
+ return [];
28243
+ }
28244
+ }
28245
+
28246
+ //#endregion
28247
+ //#region src/harness/codex.ts
28248
+ /**
28249
+ * codex.ts — adapter over Codex CLI session logs:
28250
+ * `<codex home>/sessions/**\/rollout-*.jsonl`. Implemented against the
28251
+ * documented rollout shape (JSON lines; `type:"message"` with role
28252
+ * user/assistant, `function_call` / `function_call_output`); unknown entry
28253
+ * types and malformed lines are skipped rather than failing the session.
28254
+ */
28255
+ /** Codex CLI's config dir: CODEX_HOME if set, else ~/.codex. */
28256
+ function codexHomeDir() {
28257
+ return process.env.CODEX_HOME || join(homedir(), ".codex");
28258
+ }
28259
+ /** Plain text of a message payload's content: a string, or text parts joined. */
28260
+ function contentText(c) {
28261
+ if (typeof c === "string") return c;
28262
+ if (Array.isArray(c)) return c.map((p) => {
28263
+ if (typeof p === "string") return p;
28264
+ if (p && typeof p === "object") {
28265
+ const o = p;
28266
+ if (typeof o.text === "string") return o.text;
28267
+ }
28268
+ return "";
28269
+ }).filter(Boolean).join("\n");
28270
+ return "";
28271
+ }
28272
+ /** function_call arguments: parsed as JSON when they are a JSON string. */
28273
+ function parseArgs(a) {
28274
+ if (typeof a !== "string") return a;
28275
+ try {
28276
+ return JSON.parse(a);
28277
+ } catch {
28278
+ return a;
28279
+ }
28280
+ }
28281
+ /** Events of one parsed rollout entry; nothing for entries that carry none. */
28282
+ function eventsOfEntry(sessionId, e) {
28283
+ const ts = e.timestamp ?? "";
28284
+ const p = e.payload;
28285
+ if (!p || !p.type) return [];
28286
+ if (p.type === "message" && (p.role === "user" || p.role === "assistant")) {
28287
+ const text = contentText(p.content);
28288
+ if (!text) return [];
28289
+ return [{
28290
+ harness: NAME,
28291
+ sessionId,
28292
+ ts,
28293
+ kind: p.role,
28294
+ text,
28295
+ source: "transcript",
28296
+ fidelity: "observed"
28297
+ }];
28298
+ }
28299
+ if (p.type === "function_call" && p.name) return [{
28300
+ harness: NAME,
28301
+ sessionId,
28302
+ ts,
28303
+ kind: "tool_call",
28304
+ tool: p.name,
28305
+ input: parseArgs(p.arguments),
28306
+ source: "transcript",
28307
+ fidelity: "observed"
28308
+ }];
28309
+ if (p.type === "function_call_output") return [{
28310
+ harness: NAME,
28311
+ sessionId,
28312
+ ts,
28313
+ kind: "tool_result",
28314
+ text: contentText(p.output),
28315
+ source: "transcript",
28316
+ fidelity: "observed"
28317
+ }];
28318
+ return [];
28319
+ }
28320
+ const NAME = "codex";
28321
+ /** rollout-*.jsonl under `dir`, recursing into subdirectories. */
28322
+ function rolloutFiles(dir) {
28323
+ const out = [];
28324
+ let entries;
28325
+ try {
28326
+ entries = readdirSync(dir, { withFileTypes: true });
28327
+ } catch {
28328
+ return out;
28329
+ }
28330
+ for (const ent of entries) {
28331
+ const p = join(dir, ent.name);
28332
+ if (ent.isDirectory()) out.push(...rolloutFiles(p));
28333
+ else if (ent.name.startsWith("rollout-") && ent.name.endsWith(".jsonl")) out.push(p);
28334
+ }
28335
+ return out;
28336
+ }
28337
+ function toSession(file) {
28338
+ const id = file.slice(0, -6).split("/").pop() ?? file;
28339
+ let startedAt;
28340
+ let updatedAt;
28341
+ try {
28342
+ for (const raw of readFileSync(file, "utf8").split("\n")) {
28343
+ if (!raw.trim()) continue;
28344
+ let e;
28345
+ try {
28346
+ e = JSON.parse(raw);
28347
+ } catch {
28348
+ continue;
28349
+ }
28350
+ if (typeof e.timestamp === "string" && e.timestamp) {
28351
+ startedAt ??= e.timestamp;
28352
+ updatedAt = e.timestamp;
28353
+ }
28354
+ }
28355
+ } catch {}
28356
+ const mtime = new Date(statSync(file).mtimeMs).toISOString();
28357
+ return {
28358
+ harness: NAME,
28359
+ id,
28360
+ path: file,
28361
+ startedAt,
28362
+ updatedAt: updatedAt ?? mtime
28363
+ };
28364
+ }
28365
+ const codexAdapter = {
28366
+ name: NAME,
28367
+ detect() {
28368
+ return existsSync(join(codexHomeDir(), "sessions"));
28369
+ },
28370
+ listSessions(opts = {}) {
28371
+ const root = join(codexHomeDir(), "sessions");
28372
+ if (!existsSync(root)) return [];
28373
+ const out = rolloutFiles(root).map(toSession);
28374
+ const since = opts.sinceMs !== void 0 ? Date.now() - opts.sinceMs : void 0;
28375
+ return out.filter((s) => since === void 0 || Date.parse(s.updatedAt) >= since).sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt));
28376
+ },
28377
+ *readEvents(session) {
28378
+ let text;
28379
+ try {
28380
+ text = readFileSync(session.path, "utf8");
28381
+ } catch {
28382
+ return;
28383
+ }
28384
+ for (const raw of text.split("\n")) {
28385
+ if (!raw.trim()) continue;
28386
+ let e;
28387
+ try {
28388
+ e = JSON.parse(raw);
28389
+ } catch {
28390
+ continue;
28391
+ }
28392
+ yield* eventsOfEntry(session.id, e);
28393
+ }
28394
+ }
28395
+ };
28396
+
28397
+ //#endregion
28398
+ //#region src/harness/index.ts
28399
+ /**
28400
+ * index.ts — the adapter registry and the enabled set. Implementations are
28401
+ * listed here; which of them run is config (`harness.adapters` in the main
28402
+ * config, default ["claude-code"]), never a hardcoded enable list.
28403
+ */
28404
+ /** Every implementation PAI knows, in display order. */
28405
+ const ADAPTERS = [claudeCodeAdapter, codexAdapter];
28406
+ /** Names enabled by config: `harness.adapters`, default ["claude-code"]. */
28407
+ function enabledHarnessNames(path = paiConfigFilePath()) {
28408
+ const list = effectiveMainConfig(path).harness?.adapters;
28409
+ if (!Array.isArray(list)) return ["claude-code"];
28410
+ return list.filter((n) => typeof n === "string");
28411
+ }
28412
+
28413
+ //#endregion
28414
+ //#region src/cli/commands/harness.ts
28415
+ /** `pai harness ...`: sessions PAI can read from agent harnesses (read-only). */
28416
+ const fmt = (iso) => Number.isFinite(Date.parse(iso)) ? new Date(iso).toISOString().slice(0, 16).replace("T", " ") : "—";
28417
+ function sessionsFor(harness, cwd) {
28418
+ const a = ADAPTERS.find((x) => x.name === harness);
28419
+ if (!a || !a.detect()) return [];
28420
+ return a.listSessions(cwd ? { cwd } : void 0);
28421
+ }
28422
+ function listCmd$1(opts) {
28423
+ const enabled = new Set(enabledHarnessNames());
28424
+ const rows = ADAPTERS.map((a) => {
28425
+ const detected = a.detect();
28426
+ return [
28427
+ a.name,
28428
+ detected ? ok$1("yes") : dim$1("no"),
28429
+ enabled.has(a.name) ? ok$1("yes") : dim$1("no"),
28430
+ detected ? String(sessionsFor(a.name).length) : dim$1("—")
28431
+ ];
28432
+ });
28433
+ if (opts.json) {
28434
+ console.log(JSON.stringify(ADAPTERS.map((a) => ({
28435
+ adapter: a.name,
28436
+ detected: a.detect(),
28437
+ enabled: enabled.has(a.name),
28438
+ sessions: a.detect() ? sessionsFor(a.name).length : void 0
28439
+ })), null, 2));
28440
+ return 0;
28441
+ }
28442
+ console.log(renderTable$1([
28443
+ "adapter",
28444
+ "detected",
28445
+ "enabled",
28446
+ "sessions"
28447
+ ], rows));
28448
+ console.log(dim$1(` enabled set comes from config key harness.adapters (pai config get harness.adapters)`));
28449
+ return 0;
28450
+ }
28451
+ function sessionsCmd(opts) {
28452
+ const names = opts.harness ? [opts.harness] : enabledHarnessNames();
28453
+ const out = [];
28454
+ for (const name of names) out.push(...sessionsFor(name, opts.cwd));
28455
+ out.sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt));
28456
+ if (opts.json) {
28457
+ console.log(JSON.stringify(out, null, 2));
28458
+ return 0;
28459
+ }
28460
+ if (!out.length) {
28461
+ console.log(dim$1(` no sessions${opts.cwd ? ` under ${opts.cwd}` : ""} (${names.join(", ")})`));
28462
+ return 0;
28463
+ }
28464
+ console.log(renderTable$1([
28465
+ "harness",
28466
+ "id",
28467
+ "updated",
28468
+ "cwd"
28469
+ ], out.map((s) => [
28470
+ s.harness,
28471
+ s.id,
28472
+ fmt(s.updatedAt),
28473
+ s.cwd ? maskSecrets(shortenPath(s.cwd, 48)) : dim$1("—")
28474
+ ])));
28475
+ return 0;
28476
+ }
28477
+ const harnessCommand = {
28478
+ name: "harness",
28479
+ summary: "Sessions PAI can read from agent harnesses (Claude Code, Codex, ...)",
28480
+ description: "Read-only ingestion of other harnesses' session logs. `list` shows each adapter: detected (its store exists), enabled (config key harness.adapters), and its session count. `sessions` lists the sessions themselves.",
28481
+ examples: [
28482
+ "pai harness list",
28483
+ "pai harness sessions",
28484
+ "pai harness sessions --harness codex --cwd ~/proj"
28485
+ ],
28486
+ seeAlso: ["pai config get harness.adapters"],
28487
+ subcommands: [{
28488
+ name: "list",
28489
+ summary: "Each adapter: detected, enabled, session count",
28490
+ description: "One row per adapter PAI knows. Only enabled adapters whose store exists are ever read.",
28491
+ options: [{
28492
+ name: "json",
28493
+ type: "boolean",
28494
+ default: false,
28495
+ summary: "Print JSON instead of a table"
28496
+ }],
28497
+ examples: ["pai harness list", "pai harness list --json"],
28498
+ run: (ctx) => listCmd$1({ json: ctx.opts.json === true })
28499
+ }, {
28500
+ name: "sessions",
28501
+ summary: "List sessions, newest first",
28502
+ description: "Sessions of the enabled adapters (or one harness with --harness), optionally limited to one working directory.",
28503
+ options: [
28504
+ {
28505
+ name: "harness",
28506
+ type: "string",
28507
+ value: "NAME",
28508
+ summary: "Only this adapter (default: the enabled set)"
28509
+ },
28510
+ {
28511
+ name: "cwd",
28512
+ type: "string",
28513
+ value: "DIR",
28514
+ summary: "Only sessions of this working directory"
28515
+ },
28516
+ {
28517
+ name: "json",
28518
+ type: "boolean",
28519
+ default: false,
28520
+ summary: "Print JSON instead of a table"
28521
+ }
28522
+ ],
28523
+ examples: [
28524
+ "pai harness sessions",
28525
+ "pai harness sessions --harness codex",
28526
+ "pai harness sessions --cwd ~/proj"
28527
+ ],
28528
+ run: (ctx) => {
28529
+ const cwd = ctx.opts.cwd;
28530
+ if (cwd && !existsSync(cwd)) {
28531
+ console.log(dim$1(` no such directory: ${cwd}`));
28532
+ return 0;
28533
+ }
28534
+ return sessionsCmd({
28535
+ harness: ctx.opts.harness,
28536
+ cwd,
28537
+ json: ctx.opts.json === true
28538
+ });
28539
+ }
28540
+ }]
28541
+ };
28542
+
28543
+ //#endregion
28544
+ //#region src/hooks/ts/lib/worker-git-guard.ts
28545
+ /** Splits on &&, ||, ;, | and newlines outside quotes. */
28546
+ function splitSegments(command) {
28547
+ const out = [];
28548
+ let cur = "";
28549
+ let quote = "";
28550
+ for (let i = 0; i < command.length; i++) {
28551
+ const c = command[i];
28552
+ if (quote) {
28553
+ if (c === quote) quote = "";
28554
+ cur += c;
28555
+ } else if (c === "\"" || c === "'") {
28556
+ quote = c;
28557
+ cur += c;
28558
+ } else if (c === "&" || c === "|" || c === ";" || c === "\n") {
28559
+ out.push(cur);
28560
+ cur = "";
28561
+ } else cur += c;
28562
+ }
28563
+ out.push(cur);
28564
+ return out.map((s) => s.trim()).filter(Boolean);
28565
+ }
28566
+
28567
+ //#endregion
28568
+ //#region src/hooks/ts/lib/rules/eval.ts
28569
+ /**
28570
+ * eval.ts — run rules against a tool call.
28571
+ *
28572
+ * Order is fixed: the first block hit wins, else the first confirm hit, else
28573
+ * the first warn hit. Text inside quotes is not a command, so rules test the
28574
+ * content with quoted strings removed; rules marked `quoted: true` also test
28575
+ * it as written and with only the quote marks removed — every builtin block
28576
+ * rule whose payload can sit inside `bash -c "..."` is marked, and anchors on
28577
+ * command position so a message that merely mentions the payload stays
28578
+ * allowed. Confirm rules test per-segment, so a quoted `git push -f` in a commit
28579
+ * message stays inert while a quoted `+main` refspec still counts.
28580
+ */
28581
+ /** Quoted strings removed: text inside a commit message or echo is not a command. */
28582
+ function dropQuoted(segment) {
28583
+ return segment.replace(/"(?:\\.|[^"\\])*"|'[^']*'/g, "\"\"");
28584
+ }
28585
+ /** A quoted +refspec is still a forced push, so push segments also test with only the quote marks removed. */
28586
+ function gitPatternHit(pattern, segment) {
28587
+ const bare = dropQuoted(segment);
28588
+ if (pattern.test(bare)) return true;
28589
+ return /\bgit\b.*\spush\b/.test(bare) && pattern.test(segment.replace(/["']/g, ""));
28590
+ }
28591
+ const regexCache = /* @__PURE__ */ new Map();
28592
+ function ruleRegex(rule) {
28593
+ if (!rule.match) return null;
28594
+ const key = `${rule.match} ${rule.flags ?? ""}`;
28595
+ const cached = regexCache.get(key);
28596
+ if (cached) return cached;
28597
+ try {
28598
+ const re = new RegExp(rule.match, (rule.flags ?? "").replace(/[gy]/g, ""));
28599
+ regexCache.set(key, re);
28600
+ return re;
28601
+ } catch {
28602
+ return null;
28603
+ }
28604
+ }
28605
+ function toolMatches(rule, toolName) {
28606
+ if (!rule.tool) return toolName === "Bash";
28607
+ try {
28608
+ return new RegExp(rule.tool).test(toolName);
28609
+ } catch {
28610
+ return false;
28611
+ }
28612
+ }
28613
+ /** First block hit, else first confirm hit, else first warn hit; null when none. */
28614
+ function evaluateRules(rules, toolName, content) {
28615
+ const unquoted = content.replace(/["']/g, "");
28616
+ const bare = dropQuoted(content);
28617
+ const segments = splitSegments(content);
28618
+ for (const severity of [
28619
+ "block",
28620
+ "confirm",
28621
+ "warn"
28622
+ ]) for (const rule of rules) {
28623
+ if (rule.severity !== severity || !toolMatches(rule, toolName)) continue;
28624
+ const re = ruleRegex(rule);
28625
+ if (!re) continue;
28626
+ if (severity === "confirm" ? segments.some((seg) => gitPatternHit(re, seg)) : re.test(bare) || rule.quoted === true && (re.test(content) || re.test(unquoted))) return { rule };
28627
+ }
28628
+ return null;
28629
+ }
28630
+
28631
+ //#endregion
28632
+ //#region src/hooks/ts/lib/rules/lint.ts
28633
+ /**
28634
+ * lint.ts — run each rule's own fixtures.
28635
+ *
28636
+ * Every `tests.match` must hit the rule, every `tests.no_match` must not.
28637
+ * A single-step rule's fixtures are plain strings tested as Bash commands; a
28638
+ * sequence rule's are lists of step contents, checked step by step (the
28639
+ * window and ordering are the scan's business, not the fixtures'). A rule
28640
+ * with no fixtures is ok (nothing to check).
28641
+ */
28642
+ function stepRegex(step) {
28643
+ try {
28644
+ return new RegExp(step.match, step.flags ?? "");
28645
+ } catch {
28646
+ return null;
28647
+ }
28648
+ }
28649
+ /**
28650
+ * A fixture sequence hits when every step's regex matches its content and its
28651
+ * tool pin, when set. A plain string is a Bash-shaped step; an object names
28652
+ * the tool, so a rule whose step is pinned to a fetched result (WebFetch,
28653
+ * Read) can still carry a fixture that hits. A step whose source
28654
+ * backreferences an earlier step's group (`\1`) is checked with that group's
28655
+ * capture substituted in — the same resolution the transcript scan applies —
28656
+ * so a fixture like download-then-execute's "same file twice" checks the way
28657
+ * the scan does.
28658
+ */
28659
+ function sequenceHit(rule, steps) {
28660
+ const seqSteps = rule.sequence?.steps ?? [];
28661
+ if (steps.length !== seqSteps.length) return false;
28662
+ const prior = [];
28663
+ for (let i = 0; i < seqSteps.length; i++) {
28664
+ const step = seqSteps[i];
28665
+ const { tool, content } = steps[i];
28666
+ if (step.tool && !new RegExp(step.tool).test(tool ?? "Bash")) return false;
28667
+ if (step.match.includes("\\1") && prior.length) {
28668
+ const cap = prior[0][1];
28669
+ if (!cap) return false;
28670
+ const lit = cap.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
28671
+ try {
28672
+ if (!new RegExp(step.match.replace(/\\1/g, () => lit), step.flags ?? "").test(content)) return false;
28673
+ } catch {
28674
+ return false;
28675
+ }
28676
+ continue;
28677
+ }
28678
+ const re = stepRegex(step);
28679
+ if (!re) return false;
28680
+ const m = content.match(re);
28681
+ if (!m) return false;
28682
+ prior.push(m);
28683
+ }
28684
+ return true;
28685
+ }
28686
+ function asFixtureSteps(t) {
28687
+ if (!Array.isArray(t)) return null;
28688
+ return t.map((s) => typeof s === "string" ? { content: s } : {
28689
+ tool: s.tool,
28690
+ content: s.content
28691
+ });
28692
+ }
28693
+ function lintRules(rules) {
28694
+ return rules.map((rule) => {
28695
+ const failures = [];
28696
+ const show = (t) => Array.isArray(t) ? t.map((s) => typeof s === "string" ? s : `${s.tool ?? "Bash"}: ${s.content}`).join(" | ") : t;
28697
+ if (rule.sequence) {
28698
+ for (const t of rule.tests?.match ?? []) {
28699
+ const steps = asFixtureSteps(t);
28700
+ if (!steps || !sequenceHit(rule, steps)) failures.push(`tests.match did not hit: ${show(t)}`);
28701
+ }
28702
+ for (const t of rule.tests?.no_match ?? []) {
28703
+ const steps = asFixtureSteps(t);
28704
+ if (steps && sequenceHit(rule, steps)) failures.push(`tests.no_match hit: ${show(t)}`);
28705
+ }
28706
+ return {
28707
+ id: rule.id,
28708
+ ok: failures.length === 0,
28709
+ failures
28710
+ };
28711
+ }
28712
+ for (const text of rule.tests?.match ?? []) {
28713
+ const hit = evaluateRules([rule], "Bash", text);
28714
+ if (!hit || hit.rule.id !== rule.id) failures.push(`tests.match did not hit: ${text}`);
28715
+ }
28716
+ for (const text of rule.tests?.no_match ?? []) {
28717
+ const hit = evaluateRules([rule], "Bash", text);
28718
+ if (hit && hit.rule.id === rule.id) failures.push(`tests.no_match hit: ${text}`);
28719
+ }
28720
+ return {
28721
+ id: rule.id,
28722
+ ok: failures.length === 0,
28723
+ failures
28724
+ };
28725
+ });
28726
+ }
28727
+
28728
+ //#endregion
28729
+ //#region src/cli/commands/rules.ts
28730
+ function listCmd(json) {
28731
+ const { rules, errors, ignored } = listRules(userRuleDirs(process.cwd()), { projectDirs: projectRuleDirs(process.cwd()) });
28732
+ if (json) {
28733
+ console.log(JSON.stringify({
28734
+ rules,
28735
+ errors,
28736
+ ignored
28737
+ }, null, 2));
28738
+ return 0;
28739
+ }
28740
+ console.log(renderTable$1([
28741
+ "id",
28742
+ "severity",
28743
+ "source",
28744
+ "enabled"
28745
+ ], rules.map((r) => [
28746
+ r.id,
28747
+ r.severity,
28748
+ r.source,
28749
+ r.severity === "off" ? "off" : "on"
28750
+ ])));
28751
+ for (const e of errors) console.log(err(`load error: ${e}`));
28752
+ for (const i of ignored) console.log(warn$1(`ignored: ${i}`));
28753
+ return 0;
28754
+ }
28755
+ function lintCmd(json) {
28756
+ const { rules, errors } = listRules(userRuleDirs(process.cwd()), { projectDirs: projectRuleDirs(process.cwd()) });
28757
+ const enabled = rules.filter((r) => r.severity !== "off");
28758
+ const lints = lintRules(enabled);
28759
+ const failed = lints.filter((l) => !l.ok);
28760
+ if (json) {
28761
+ console.log(JSON.stringify({
28762
+ lints,
28763
+ errors
28764
+ }, null, 2));
28765
+ return failed.length || errors.length ? 1 : 0;
28766
+ }
28767
+ for (const l of lints) {
28768
+ if (l.ok) continue;
28769
+ console.log(err(`✖ ${l.id}`));
28770
+ for (const f of l.failures) console.log(` ${f}`);
28771
+ }
28772
+ for (const e of errors) console.log(err(`load error: ${e}`));
28773
+ if (!failed.length && !errors.length) {
28774
+ console.log(ok$1(`✔ ${enabled.length} rules, fixtures pass`));
28775
+ return 0;
28776
+ }
28777
+ return 1;
28778
+ }
28779
+ const rulesCommand = {
28780
+ name: "rules",
28781
+ summary: "Security rules: list them, lint their fixtures",
28782
+ description: "The PreToolUse security rules. Built-ins ship with the package; user rules live in PAI_HOME/rules or .pai/rules as *.rule.yaml files — a user rule with a built-in's id replaces it, severity: off disables it.",
28783
+ examples: ["pai rules list", "pai rules lint"],
28784
+ seeAlso: ["pai audit tokens hooks"],
28785
+ subcommands: [{
28786
+ name: "list",
28787
+ summary: "Every rule with severity, source and enabled state",
28788
+ description: "One row per rule id; a disabled (severity: off) rule shows as off.",
28789
+ options: [{
28790
+ name: "json",
28791
+ type: "boolean",
28792
+ default: false,
28793
+ summary: "Machine-readable JSON output"
28794
+ }],
28795
+ examples: ["pai rules list", "pai rules list --json"],
28796
+ run: (ctx) => listCmd(ctx.opts.json === true)
28797
+ }, {
28798
+ name: "lint",
28799
+ summary: "Run every rule's fixtures; exit 1 on a failure or load error",
28800
+ description: "Each rule's tests.match strings must hit and tests.no_match strings must not. Load errors count as failures.",
28801
+ options: [{
28802
+ name: "json",
28803
+ type: "boolean",
28804
+ default: false,
28805
+ summary: "Machine-readable JSON output"
28806
+ }],
28807
+ examples: ["pai rules lint", "pai rules lint --json"],
28808
+ run: (ctx) => lintCmd(ctx.opts.json === true)
28809
+ }]
28810
+ };
28811
+
28812
+ //#endregion
28813
+ //#region src/hooks/ts/lib/transcript-text.ts
28814
+ /**
28815
+ * transcript-text.ts — small, pure pieces of transcript parsing that are
28816
+ * worth testing in isolation.
28817
+ *
28818
+ * Split out of context-compression-hook.ts (a hook entrypoint that calls
28819
+ * `main()` and `process.exit()` at import time, which makes it unsafe to
28820
+ * import directly from a test) so the two bug fixes that live here can be
28821
+ * covered by a real regression test rather than only a manual before/after
28822
+ * digest.
28823
+ */
28824
+ /**
28825
+ * Turn Claude content (string or content block array) into plain text.
28826
+ *
28827
+ * BUG (found in ~1 in 4 injected digests on this machine): a "user" turn is
28828
+ * not always something the user typed — a tool_result is also delivered as a
28829
+ * user-role message, and its `content` block looks like
28830
+ * `{ type: "tool_result", content: [...] }` where the inner `content` is
28831
+ * itself an array of blocks, not a string. The old code fell back to
28832
+ * `String(c.content)` for anything without a `.text` field, and `String()`
28833
+ * on an array of objects produces literal `"[object Object],[object
28834
+ * Object]"` — which is exactly what showed up under "Recent user requests".
28835
+ * Only ever take the `.text` of a `type: "text"` block; everything else
28836
+ * (tool_use, tool_result, image, and any nested content) is ignored rather
28837
+ * than stringified.
28838
+ */
28839
+ function contentToText(content) {
28840
+ if (typeof content === "string") return content;
28841
+ if (Array.isArray(content)) return content.map((c) => {
28842
+ if (typeof c === "string") return c;
28843
+ if (c && typeof c === "object" && c.type === "text" && typeof c.text === "string") return c.text;
28844
+ return "";
28845
+ }).join(" ").trim();
28846
+ return "";
28847
+ }
28848
+
28849
+ //#endregion
28850
+ //#region src/session/handoff.ts
28851
+ /**
28852
+ * handoff.ts — a portable, bounded, redacted handoff brief for one session.
28853
+ *
28854
+ * `pai handoff export` scrapes a Claude Code transcript into a short Markdown
28855
+ * brief a successor session can continue from: what was asked, what changed,
28856
+ * what ran, and where it stopped. Unlike the model-written context handover
28857
+ * this is mechanical — no model call, no prose to hallucinate — and it is
28858
+ * redacted (secrets replaced by [REDACTED]) before it is bounded, so a brief
28859
+ * can be pasted, mailed or committed without leaking a token.
28860
+ *
28861
+ * The brief is assembled as sections, masked, then cut to `maxBytes` from the
28862
+ * oldest end (Recent commands first, then Last steps) with a marker line, so
28863
+ * the parts a successor needs most (Session, requests, files) survive first.
28864
+ */
28865
+ /** Default brief size: comfortably under what a successor can read in one turn. */
28866
+ const HANDOFF_MAX_BYTES = 49152;
28867
+ /** Most distinct files listed under "Files changed". */
28868
+ const MAX_FILES = 50;
28869
+ /** Most Bash commands listed under "Recent commands". */
28870
+ const MAX_COMMANDS = 20;
28871
+ /** Most assistant text messages listed under "Last steps". */
28872
+ const MAX_STEPS = 10;
28873
+ /** Each "Last steps" entry is truncated to this many characters. */
28874
+ const STEP_CHARS = 400;
28875
+ /** Tools whose input carries a changed file. */
28876
+ const FILE_TOOLS = new Set([
28877
+ "Edit",
28878
+ "Write",
28879
+ "MultiEdit"
28880
+ ]);
28881
+ /** The last assistant text message of a transcript, or null when there is none. */
28882
+ function lastAssistantText(entries) {
28883
+ for (let i = entries.length - 1; i >= 0; i--) {
28884
+ const entry = entries[i];
28885
+ if (entry.type !== "assistant" || !Array.isArray(entry.message?.content)) continue;
28886
+ const text = entry.message.content.filter((p) => p?.type === "text" && typeof p.text === "string").map((p) => p.text).join("\n").trim();
28887
+ if (text) return text;
28888
+ }
28889
+ return null;
28890
+ }
28891
+ /** First user text message that looks like something typed, not an injected block. */
28892
+ function firstUserText(entries) {
28893
+ for (const entry of entries) {
28894
+ if (entry.type !== "user") continue;
28895
+ const content = entry.message?.content;
28896
+ const text = typeof content === "string" ? content : contentToText(content);
28897
+ if (text.trim()) return text.trim();
28898
+ }
28899
+ return null;
28900
+ }
28901
+ /** Last user text message — the request the session was working on. */
28902
+ function lastUserText(entries) {
28903
+ for (let i = entries.length - 1; i >= 0; i--) {
28904
+ const entry = entries[i];
28905
+ if (entry.type !== "user") continue;
28906
+ const content = entry.message?.content;
28907
+ const text = typeof content === "string" ? content : contentToText(content);
28908
+ if (text.trim()) return text.trim();
28909
+ }
28910
+ return null;
28911
+ }
28912
+ /**
28913
+ * Exit code of a Bash tool_result, when the transcript records one.
28914
+ * Claude Code serialises Bash results as plain text; an interrupted command
28915
+ * reads "Exit code N", and a JSON payload carries `interrupted`/`success`.
28916
+ */
28917
+ function bashExitCode(result) {
28918
+ if (result.is_error === true) return null;
28919
+ const text = contentToText(result.content);
28920
+ const m = text.match(/Exit code (\d+)/);
28921
+ if (m) return Number(m[1]);
28922
+ try {
28923
+ const parsed = JSON.parse(text);
28924
+ if (parsed.interrupted === true) return null;
28925
+ if (parsed.success === false) return null;
28926
+ } catch {}
28927
+ return 0;
28928
+ }
28929
+ /**
28930
+ * Build the handoff brief for one transcript: Markdown, secrets masked,
28931
+ * cut to `maxBytes` whole sections from the oldest end with a marker line.
28932
+ *
28933
+ * @param transcriptPath a Claude Code session transcript (.jsonl)
28934
+ */
28935
+ function buildHandoffBrief(transcriptPath, opts = {}) {
28936
+ const maxBytes = opts.maxBytes ?? HANDOFF_MAX_BYTES;
28937
+ const raw = readFileSync(transcriptPath, "utf8");
28938
+ const entries = [];
28939
+ for (const line of raw.split("\n")) {
28940
+ if (!line.trim()) continue;
28941
+ try {
28942
+ entries.push(JSON.parse(line));
28943
+ } catch {}
28944
+ }
28945
+ const sessionId = basename(transcriptPath).replace(/\.jsonl$/, "") || "unknown";
28946
+ const cwd = entries.find((e) => e.cwd)?.cwd ?? "";
28947
+ const first = entries.find((e) => e.timestamp)?.timestamp ?? "";
28948
+ const last = [...entries].reverse().find((e) => e.timestamp)?.timestamp ?? "";
28949
+ const files = [];
28950
+ const seen = /* @__PURE__ */ new Set();
28951
+ const commands = [];
28952
+ const steps = [];
28953
+ const results = /* @__PURE__ */ new Map();
28954
+ for (const entry of entries) {
28955
+ const content = entry.message?.content;
28956
+ if (!Array.isArray(content)) continue;
28957
+ for (const part of content) if (part?.type === "tool_result" && typeof part.tool_use_id === "string") results.set(part.tool_use_id, part);
28958
+ }
28959
+ for (const entry of entries) {
28960
+ const content = entry.message?.content;
28961
+ if (!Array.isArray(content)) continue;
28962
+ for (const part of content) if (part?.type === "tool_use" && part.name && FILE_TOOLS.has(part.name)) {
28963
+ const path = part.input?.file_path;
28964
+ if (typeof path === "string" && !seen.has(path)) {
28965
+ seen.add(path);
28966
+ files.push(path);
28967
+ }
28968
+ } else if (part?.type === "tool_use" && part.name === "Bash") {
28969
+ const command = typeof part.input?.command === "string" ? part.input.command : "";
28970
+ if (command) {
28971
+ const code = part.id ? results.get(part.id) : void 0;
28972
+ const exit = code ? bashExitCode(code) : null;
28973
+ commands.push(exit === null ? command : `${command} [exit ${exit}]`);
28974
+ }
28975
+ } else if (part?.type === "text" && typeof part.text === "string" && entry.type === "assistant") {
28976
+ const text = part.text.trim();
28977
+ if (text) steps.push(text.length > STEP_CHARS ? `${text.slice(0, STEP_CHARS)}…` : text);
28978
+ }
28979
+ }
28980
+ const sections = {
28981
+ Session: [
28982
+ `- id: ${sessionId}`,
28983
+ `- cwd: ${cwd}`,
28984
+ `- start: ${first}`,
28985
+ `- end: ${last}`
28986
+ ].join("\n"),
28987
+ "First request": firstUserText(entries) ?? "(none)",
28988
+ "Latest request": lastUserText(entries) ?? "(none)",
28989
+ "Files changed": files.slice(0, MAX_FILES).map((f) => `- ${f}`).join("\n") || "(none)",
28990
+ "Recent commands": commands.slice(-MAX_COMMANDS).map((c) => `- ${c}`).join("\n") || "(none)",
28991
+ "Last steps": steps.slice(-MAX_STEPS).map((s) => `- ${s}`).join("\n") || "(none)",
28992
+ "Where it stopped": lastAssistantText(entries) ?? "(no assistant message)"
28993
+ };
28994
+ const masked = maskSecrets(Object.entries(sections).map(([title, body]) => `## ${title}\n${body}`).join("\n\n"));
28995
+ if (Buffer.byteLength(masked, "utf8") <= maxBytes) return masked;
28996
+ const note = `\n\n[truncated to ${maxBytes} bytes]`;
28997
+ const budget = maxBytes - Buffer.byteLength(note, "utf8");
28998
+ const render = (parts) => Object.entries(parts).map(([title, body]) => `## ${title}\n${body}`).join("\n\n");
28999
+ const order = ["Recent commands", "Last steps"];
29000
+ const kept = { ...sections };
29001
+ for (const name of order) {
29002
+ delete kept[name];
29003
+ const text = render(kept);
29004
+ if (Buffer.byteLength(text, "utf8") <= budget) return text + note;
29005
+ }
29006
+ kept["Where it stopped"] = (kept["Where it stopped"] ?? "").split("\n")[0].slice(0, 200);
29007
+ const text = render(kept);
29008
+ if (Buffer.byteLength(text, "utf8") <= budget) return text + note;
29009
+ return Buffer.from(text, "utf8").subarray(0, Math.max(0, budget)).toString("utf8") + note;
29010
+ }
29011
+ /** `yyyymmdd-hhmm` in local time, for the handoff file name. */
29012
+ function stamp(d = /* @__PURE__ */ new Date()) {
29013
+ const p = (n) => String(n).padStart(2, "0");
29014
+ return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}`;
29015
+ }
29016
+ /** The handoffs directory under PAI_HOME. */
29017
+ function handoffsDir() {
29018
+ return paiHomePath("handoffs");
29019
+ }
29020
+ /**
29021
+ * Write a handoff brief into <PAI_HOME>/handoffs/<sessionId>-<yyyymmdd-hhmm>.md
29022
+ * (dir 0700, file 0600) and return its path.
29023
+ */
29024
+ function writeHandoff(brief, sessionId) {
29025
+ const dir = handoffsDir();
29026
+ mkdirSync(dir, {
29027
+ recursive: true,
29028
+ mode: 448
29029
+ });
29030
+ const path = join(dir, `${sessionId}-${stamp()}.md`);
29031
+ writeFileSync(path, brief, { mode: 384 });
29032
+ return path;
29033
+ }
29034
+ /** Newest transcript of a cwd, newest first; null when the project has none. */
29035
+ function sessionTranscripts(cwd) {
29036
+ const dir = join(claudeProjectsDir(), encodeDir(cwd));
29037
+ let names;
29038
+ try {
29039
+ names = readdirSync(dir);
29040
+ } catch {
29041
+ return [];
29042
+ }
29043
+ return names.filter((n) => n.endsWith(".jsonl")).map((n) => join(dir, n)).map((path) => ({
29044
+ path,
29045
+ mtime: safeMtime(path)
29046
+ })).filter((f) => f.mtime !== null).sort((a, b) => b.mtime - a.mtime).map((f) => f.path);
29047
+ }
29048
+ function safeMtime(path) {
29049
+ try {
29050
+ return statSync(path).mtimeMs;
29051
+ } catch {
29052
+ return null;
29053
+ }
29054
+ }
29055
+ /** Resolve a session id (or transcript path) in a cwd to its transcript path. */
29056
+ function resolveTranscript(session, cwd) {
29057
+ if (session && session.endsWith(".jsonl")) return session;
29058
+ const dir = join(claudeProjectsDir(), encodeDir(cwd));
29059
+ if (session) {
29060
+ const direct = join(dir, `${session}.jsonl`);
29061
+ try {
29062
+ statSync(direct);
29063
+ return direct;
29064
+ } catch {
29065
+ const archived = join(dir, "sessions", `${session}.jsonl`);
29066
+ try {
29067
+ statSync(archived);
29068
+ return archived;
29069
+ } catch {
29070
+ return null;
29071
+ }
29072
+ }
29073
+ }
29074
+ return sessionTranscripts(cwd)[0] ?? null;
29075
+ }
29076
+ /** The session id a transcript path carries (its file name). */
29077
+ function sessionIdOf(transcriptPath) {
29078
+ return basename(transcriptPath).replace(/\.jsonl$/, "");
29079
+ }
29080
+
29081
+ //#endregion
29082
+ //#region src/cli/commands/handoff.ts
29083
+ /** `pai handoff ...`: export a session's work as a portable, redacted brief. */
29084
+ function exportCmd(ctx) {
29085
+ const cwd = process.cwd();
29086
+ const session = ctx.args.SESSION;
29087
+ const transcript = resolveTranscript(session, cwd);
29088
+ if (!transcript) throw new CliError(session ? `no transcript for session ${session} in ${cwd}` : `no session transcripts in ${cwd}`, "pai handoff export <SESSION> — or run it inside the project the session worked in");
29089
+ const sessionId = sessionIdOf(transcript);
29090
+ const path = writeHandoff(buildHandoffBrief(transcript), sessionId);
29091
+ console.log(path);
29092
+ console.log(`Continue with: claude "Read ${path} and continue. [pai-handoff from=${sessionId}]"`);
29093
+ return 0;
29094
+ }
29095
+ function showCmd(ctx) {
29096
+ const path = ctx.args.PATH;
29097
+ try {
29098
+ process.stdout.write(readFileSync(path, "utf8"));
29099
+ } catch {
29100
+ throw new CliError(`cannot read ${path}`, "pai handoff export first, then pai handoff show <PATH>");
29101
+ }
29102
+ return 0;
29103
+ }
29104
+ const handoffCommand = {
29105
+ name: "handoff",
29106
+ summary: "Carry a session's work to a new one",
29107
+ description: "Exports a session transcript as a short, redacted Markdown brief (requests, files changed, recent commands, where it stopped) and prints the line to continue with. The brief never goes on the command line — only its path does. A `[pai-handoff from=<id>]` marker in the next session's first prompt is recorded in the handoff link log.",
29108
+ examples: [
29109
+ "pai handoff export",
29110
+ "pai handoff export 2026-10-22-session-id",
29111
+ "pai handoff show ~/.pai/handoffs/abc-20261022-1430.md"
29112
+ ],
29113
+ seeAlso: ["pai audit tokens session"],
29114
+ subcommands: [{
29115
+ name: "export",
29116
+ summary: "Write the brief for a session (default: newest here)",
29117
+ description: "Prints the brief's path and the one-line continue hint. Secrets in the transcript are replaced with [REDACTED]; the brief is bounded to 48 KB.",
29118
+ args: [{
29119
+ name: "SESSION",
29120
+ summary: "Session id or transcript path (default: newest transcript of the current directory)"
29121
+ }],
29122
+ examples: ["pai handoff export", "pai handoff export 2026-10-22-session-id"],
29123
+ run: exportCmd
29124
+ }, {
29125
+ name: "show",
29126
+ summary: "Print a handoff brief",
29127
+ description: "Prints a brief written by `pai handoff export`.",
29128
+ args: [{
29129
+ name: "PATH",
29130
+ required: true,
29131
+ summary: "Brief file"
29132
+ }],
29133
+ examples: ["pai handoff show ~/.pai/handoffs/abc-20261022-1430.md"],
29134
+ run: showCmd
27246
29135
  }]
27247
29136
  };
27248
29137
 
@@ -27306,7 +29195,8 @@ const GROUPS = [
27306
29195
  endTop,
27307
29196
  resumeTop,
27308
29197
  clearNamesCmd,
27309
- sessionsCommand
29198
+ sessionsCommand,
29199
+ handoffCommand
27310
29200
  ]
27311
29201
  },
27312
29202
  {
@@ -27344,9 +29234,10 @@ const GROUPS = [
27344
29234
  ]
27345
29235
  }))
27346
29236
  ];
27347
- GROUPS.find((g) => g.id === "memory").commands.push(memoryCommand, kgCommand, zettelCommand, observationCommand, obsidianCommand, skillCommand);
29237
+ GROUPS.find((g) => g.id === "memory").commands.push(memoryCommand, kgCommand, zettelCommand, observationCommand, obsidianCommand, skillCommand, lessonsCommand);
27348
29238
  GROUPS.find((g) => g.id === "workers").commands.push(modelsCommand);
27349
- GROUPS.find((g) => g.id === "daemon").commands.push(auditCommand, reloadCommand);
29239
+ GROUPS.find((g) => g.id === "daemon").commands.push(auditCommand, harnessCommand, reloadCommand, rulesCommand);
29240
+ GROUPS.find((g) => g.id === "memory").commands.push(gitCommand);
27350
29241
 
27351
29242
  //#endregion
27352
29243
  //#region src/cli/index.ts