@tekmidian/pai 1.0.4 → 1.0.6

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 (248) 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-Bc8xP6TQ.mjs +11 -0
  5. package/dist/{auto-route-21GdHL70.mjs → auto-route-YaEGVrFl.mjs} +2 -2
  6. package/dist/budget-DEiebT2A.mjs +15 -0
  7. package/dist/{caller-identity-yLFb7CKq.mjs → caller-identity-C6Nc96iM.mjs} +4 -4
  8. package/dist/{capacity-DEveQdFV.mjs → capacity-B2PgKAg4.mjs} +69 -6
  9. package/dist/{chain-D7PFur8Y.mjs → chain-CtK49-Ma.mjs} +5 -5
  10. package/dist/cli/index.mjs +2126 -290
  11. package/dist/{cockpit-actions-BfRMr2sm.mjs → cockpit-actions-C73mJAJ8.mjs} +18 -18
  12. package/dist/{cockpit-actions-OodH_vx_.mjs → cockpit-actions-D1OPP6Es.mjs} +30 -26
  13. package/dist/cockpit-memory-CLHZD64W.mjs +20 -0
  14. package/dist/config/defaults/config.yaml +21 -1
  15. package/dist/{config-XpZWOYOG.mjs → config-Bz8pAU5Q.mjs} +30 -5
  16. package/dist/{config-DtENJ6cL.mjs → config-DHgPx3ER.mjs} +1 -1
  17. package/dist/config-DLmyEK2q.mjs +4 -0
  18. package/dist/{config-Bu1_vbY8.mjs → config-SlZ-Z52i.mjs} +4 -3
  19. package/dist/{controls-JLm6g0yP.mjs → controls-rIItFeIJ.mjs} +59 -66
  20. package/dist/daemon/index.mjs +40 -39
  21. package/dist/daemon-176qA79N.mjs +46 -0
  22. package/dist/{daemon-nXanenDM.mjs → daemon-SoUvPUOf.mjs} +80 -30
  23. package/dist/daemon-mcp/index.mjs +49 -38
  24. package/dist/detect-C8nFP-kR.mjs +10 -0
  25. package/dist/{detect-BLKsUOgG.mjs → detect-DLjeYwnt.mjs} +2 -2
  26. package/dist/{embed-pass-e64AwDCa.mjs → embed-pass-CN1D8MXl.mjs} +5 -5
  27. package/dist/{embedding-gate-Cnk4YCyX.mjs → embedding-gate-BVQvUmox.mjs} +2 -2
  28. package/dist/{embedding-gate-6rZrWRXa.mjs → embedding-gate-iq0oQEkF.mjs} +3 -3
  29. package/dist/{endreason-BC-v05H_.mjs → endreason-C2-hYK0_.mjs} +4 -4
  30. package/dist/{factory-B7nv75rC.mjs → factory-CYwa0jYj.mjs} +6 -6
  31. package/dist/factory-DFwez99M.mjs +9 -0
  32. package/dist/{fallback-CBWpp2F2.mjs → fallback-CwPSCrX8.mjs} +9 -231
  33. package/dist/{health-C4FQmkJn.mjs → health-D0mUAVdw.mjs} +5 -5
  34. package/dist/{helpers-DSNK_1KP.mjs → helpers-Dj2nY56t.mjs} +2 -2
  35. package/dist/{helpers-Dz7mjTSw.mjs → helpers-DyHKAwrW.mjs} +6 -6
  36. package/dist/hooks/block-sleep-poll.mjs +1 -1
  37. package/dist/hooks/capture-all-events.mjs +68 -3
  38. package/dist/hooks/capture-tool-output.mjs +68 -3
  39. package/dist/hooks/handoff-link.mjs +128 -0
  40. package/dist/hooks/load-project-context.mjs +8 -8
  41. package/dist/hooks/local-model-hint.mjs +1 -1
  42. package/dist/hooks/route-agents-to-worker.mjs +1 -1
  43. package/dist/hooks/security-validator.mjs +842 -90
  44. package/dist/hooks/supervision-banner.mjs +186 -48
  45. package/dist/hooks/worker-edit-repair.mjs +2 -2
  46. package/dist/hooks/worker-guard.mjs +65 -9
  47. package/dist/hooks/worker-note.mjs +70 -5
  48. package/dist/hooks/worker-pool-host.mjs +1518 -265
  49. package/dist/hooks/worker-proxy.mjs +1 -1
  50. package/dist/hooks/worker-status-line.mjs +225 -86
  51. package/dist/hooks/worker-supervision.mjs +1 -1
  52. package/dist/hooks/worker-tool-name-repair.mjs +2 -2
  53. package/dist/index.mjs +9 -9
  54. package/dist/{ipc-client-CnYEF31R.mjs → ipc-client-CmagIFLh.mjs} +2 -2
  55. package/dist/{ipc-client-DN7EvoVo.mjs → ipc-client-DmbSIBUK.mjs} +1 -1
  56. package/dist/{kill-tree-DleMNZX3.mjs → kill-tree-DPiMscaY.mjs} +8 -8
  57. package/dist/kill-tree-Dxei6qlR.mjs +10 -0
  58. package/dist/{launch-DlZTKE3u.mjs → launch-BZT3JDoZ.mjs} +3 -3
  59. package/dist/{launch-Di7N-J5X.mjs → launch-Bn2Zf90h.mjs} +3 -3
  60. package/dist/{indexer-backend-BdjeL4P8.mjs → lessons-CFVU0cwA.mjs} +262 -43
  61. package/dist/{local-candidate-DLdwQt0s.mjs → local-candidate-DFd4pp-w.mjs} +3 -3
  62. package/dist/{local-instances-DOxSgT_L.mjs → local-instances-DhF4_sV2.mjs} +2 -2
  63. package/dist/{local-instances-ZeRkkuS5.mjs → local-instances-KTh8IeOT.mjs} +4 -4
  64. package/dist/{local-tiers-BE4jGaMa.mjs → local-tiers-CqwGUcEn.mjs} +3 -3
  65. package/dist/{local-tiers-DNMI5W86.mjs → local-tiers-DSlIYvvm.mjs} +3 -3
  66. package/dist/{main-config-ops-kTXR8Pyv.mjs → main-config-ops-rda08BCD.mjs} +3 -3
  67. package/dist/{main-resolver-yp_puSSb.mjs → main-resolver-BKwI0I6H.mjs} +18 -18
  68. package/dist/{migrate-DrA_OwlZ.mjs → migrate-C2z1YmWC.mjs} +2 -2
  69. package/dist/{model-inventory-h6Z9fN_o.mjs → model-inventory-BXv15B0d.mjs} +13 -13
  70. package/dist/{models-add-TVni3A7V.mjs → models-add-5yXTyz3g.mjs} +38 -37
  71. package/dist/operator-Bmv5tiHQ.mjs +7 -0
  72. package/dist/{operator-CyuB3aVw.mjs → operator-Br6GeswF.mjs} +2 -2
  73. package/dist/{pai-marker-BcZyJguM.mjs → pai-marker-D6OoVbM_.mjs} +2 -2
  74. package/dist/{pane-audit-Lqy9N_Ki.mjs → pane-audit-DLYy9-4g.mjs} +13 -23
  75. package/dist/{paths-CHifubDh.mjs → paths-CypxFTfn.mjs} +4 -4
  76. package/dist/{paths-CFHTloSe.mjs → paths-DIOwNcj-.mjs} +3 -3
  77. package/dist/{planner-j77ndkBf.mjs → planner-BzZ_2lc-.mjs} +29 -29
  78. package/dist/{pool-Dh-q3iJi.mjs → pool-9EsbKD-U.mjs} +2 -2
  79. package/dist/{pool-cli-DQ6OzRy2.mjs → pool-cli-Cy5mlycP.mjs} +6 -6
  80. package/dist/{pool-cli-BggU8GRp.mjs → pool-cli-Db0uDwB9.mjs} +5 -5
  81. package/dist/{postgres-BK4FP9Zg.mjs → postgres-B1B80bcj.mjs} +3 -3
  82. package/dist/{postgres-DNIOQijl.mjs → postgres-BL5oBBgY.mjs} +3 -3
  83. package/dist/{project-setup-CBRtFFh0.mjs → project-setup-DHxdJjEc.mjs} +24 -25
  84. package/dist/providers-BH6hQYbU.mjs +16 -0
  85. package/dist/{providers-C9QC0TtO.mjs → providers-BNV7CzC-.mjs} +6 -6
  86. package/dist/{query-feedback-DN52N9C5.mjs → query-feedback-BWl2HRwN.mjs} +2 -2
  87. package/dist/registrable-BwFiqazy.mjs +9 -0
  88. package/dist/{registrable-DpyTtajg.mjs → registrable-Dxs4oKIj.mjs} +4 -4
  89. package/dist/render-DVGXpfaT.mjs +13 -0
  90. package/dist/render-jPpMrudf.mjs +2240 -0
  91. package/dist/root-C8cxhdn9.mjs +36 -0
  92. package/dist/{root-C4oXlXS_.mjs → root-DmOzMeSv.mjs} +10 -10
  93. package/dist/route-table-COAmc1nB.mjs +15 -0
  94. package/dist/{router-CQSc3lk3.mjs → router-D2476pOp.mjs} +2 -2
  95. package/dist/{router-DwHJRgG9.mjs → router-ghujlEFb.mjs} +1 -1
  96. package/dist/{routing-f2zulGwD.mjs → routing-B_cg1_Xx.mjs} +13 -13
  97. package/dist/{routing-DmkheCRH.mjs → routing-aJKMIagQ.mjs} +16 -16
  98. package/dist/{run-BwZKBJZZ.mjs → run-DE9Q0ldN.mjs} +55 -31
  99. package/dist/{runtime-paths-WsO9sJFc.mjs → runtime-paths-twfV23YL.mjs} +2 -2
  100. package/dist/{scan-BBGvU5EB.mjs → scan-CNMcltOX.mjs} +7 -7
  101. package/dist/{scope-D-h6F6DN.mjs → scope-BTWstJ1B.mjs} +4 -4
  102. package/dist/session-DgZifjLx.mjs +35 -0
  103. package/dist/{session-Dn5OJXlg.mjs → session-HBYOABWU.mjs} +28 -28
  104. package/dist/session-scan-BZxnYN1v.mjs +9 -0
  105. package/dist/{session-scan-DUn10MBd.mjs → session-scan-CX3MKCNg.mjs} +6 -6
  106. package/dist/{session-usage-CPvXvSkw.mjs → session-usage-CCmPFFa6.mjs} +117 -2
  107. package/dist/{status-ggJ7lgVg.mjs → status-Cecau8Ps.mjs} +9 -5
  108. package/dist/status-D52xC_i0.mjs +6 -0
  109. package/dist/{tool-errors-7E13StWd.mjs → tool-errors-BFw1gv-z.mjs} +2 -2
  110. package/dist/{tool-errors-CJWODvlo.mjs → tool-errors-COM2JGEr.mjs} +6 -6
  111. package/dist/viewer-Ce-Xa9L3.mjs +22 -0
  112. package/dist/{viewer-wfKnvnrI.mjs → viewer-DvBg6N22.mjs} +16 -14
  113. package/dist/{wait-ByV0OyMZ.mjs → wait-C5iWILj_.mjs} +3 -3
  114. package/dist/worker-config-BmcJ9R0U.mjs +42 -0
  115. package/dist/{worker-config-eY5Qy4vl.mjs → worker-config-DkONNk_H.mjs} +164 -107
  116. package/dist/workers-config-DwzrtdYu.mjs +4 -0
  117. package/dist/{worktree-CRs2gyg0.mjs → worktree-O_kU6Q3y.mjs} +164 -8
  118. package/dist/{zettelkasten-tpCRBYvH.mjs → zettelkasten-BH8AdNh8.mjs} +4 -4
  119. package/dist/{zettelkasten-_h_tUm2T.mjs → zettelkasten-FFhQFESJ.mjs} +2 -2
  120. package/docs/commands/README.md +33 -2
  121. package/docs/commands/audit.md +34 -3
  122. package/docs/commands/backup.md +1 -1
  123. package/docs/commands/build.md +4 -4
  124. package/docs/commands/cd.md +1 -1
  125. package/docs/commands/clear-names.md +1 -1
  126. package/docs/commands/cockpit.md +1 -1
  127. package/docs/commands/config.md +21 -1
  128. package/docs/commands/daemon.md +1 -1
  129. package/docs/commands/db.md +1 -1
  130. package/docs/commands/doc.md +1 -1
  131. package/docs/commands/end.md +1 -1
  132. package/docs/commands/find.md +1 -1
  133. package/docs/commands/git.md +110 -0
  134. package/docs/commands/handoff.md +70 -0
  135. package/docs/commands/harness.md +74 -0
  136. package/docs/commands/help.md +1 -1
  137. package/docs/commands/home.md +1 -1
  138. package/docs/commands/hooks-db.md +1 -1
  139. package/docs/commands/identity.md +1 -1
  140. package/docs/commands/install.md +1 -1
  141. package/docs/commands/issue.md +1 -1
  142. package/docs/commands/kg.md +1 -1
  143. package/docs/commands/launch.md +1 -1
  144. package/docs/commands/lessons.md +201 -0
  145. package/docs/commands/list.md +1 -1
  146. package/docs/commands/ls.md +1 -1
  147. package/docs/commands/mcp.md +1 -1
  148. package/docs/commands/memory.md +1 -1
  149. package/docs/commands/models.md +1 -1
  150. package/docs/commands/more.md +1 -1
  151. package/docs/commands/new.md +1 -1
  152. package/docs/commands/notes.md +1 -1
  153. package/docs/commands/notify.md +1 -1
  154. package/docs/commands/observation.md +1 -1
  155. package/docs/commands/obsidian.md +1 -1
  156. package/docs/commands/open.md +1 -1
  157. package/docs/commands/pane.md +1 -1
  158. package/docs/commands/pause.md +1 -1
  159. package/docs/commands/plan.md +1 -1
  160. package/docs/commands/project.md +1 -1
  161. package/docs/commands/projects.md +1 -1
  162. package/docs/commands/providers.md +1 -1
  163. package/docs/commands/queue.md +1 -1
  164. package/docs/commands/registry.md +1 -1
  165. package/docs/commands/reload.md +1 -1
  166. package/docs/commands/rename.md +1 -1
  167. package/docs/commands/restore.md +1 -1
  168. package/docs/commands/resume.md +1 -1
  169. package/docs/commands/router.md +1 -1
  170. package/docs/commands/rules.md +70 -0
  171. package/docs/commands/session.md +1 -1
  172. package/docs/commands/sessions.md +1 -1
  173. package/docs/commands/setup.md +1 -1
  174. package/docs/commands/setups.md +1 -1
  175. package/docs/commands/shell-init.md +1 -1
  176. package/docs/commands/skill.md +1 -1
  177. package/docs/commands/task.md +1 -1
  178. package/docs/commands/tasks.md +1 -1
  179. package/docs/commands/topic.md +1 -1
  180. package/docs/commands/update.md +1 -1
  181. package/docs/commands/worker.md +1 -1
  182. package/docs/commands/zettel.md +1 -1
  183. package/docs/concepts/panes.md +3 -2
  184. package/gemini-extension.json +1 -1
  185. package/package.json +2 -2
  186. package/pai-plugin.json +1 -1
  187. package/plugins/context-preservation/plugin.json +1 -1
  188. package/plugins/core/plugin.json +1 -1
  189. package/plugins/creative/plugin.json +1 -1
  190. package/plugins/observability/plugin.json +1 -1
  191. package/plugins/productivity/plugin.json +1 -1
  192. package/plugins/semantic-search/plugin.json +1 -1
  193. package/plugins/ui/plugin.json +1 -1
  194. package/plugins/zettelkasten/plugin.json +1 -1
  195. package/src/hooks/ts/lib/loop-guard.ts +3 -5
  196. package/src/hooks/ts/lib/rules/builtin/base64-decode-to-shell.rule.yaml +13 -0
  197. package/src/hooks/ts/lib/rules/builtin/chmod-world-writable.rule.yaml +13 -0
  198. package/src/hooks/ts/lib/rules/builtin/chmod-zero.rule.yaml +11 -0
  199. package/src/hooks/ts/lib/rules/builtin/credential-file-read.rule.yaml +13 -0
  200. package/src/hooks/ts/lib/rules/builtin/credential-read-then-network.rule.yaml +29 -0
  201. package/src/hooks/ts/lib/rules/builtin/crontab-persistence.rule.yaml +10 -0
  202. package/src/hooks/ts/lib/rules/builtin/curl-piped-to-shell.rule.yaml +15 -0
  203. package/src/hooks/ts/lib/rules/builtin/download-then-execute.rule.yaml +30 -0
  204. package/src/hooks/ts/lib/rules/builtin/find-root-delete.rule.yaml +11 -0
  205. package/src/hooks/ts/lib/rules/builtin/git-push-force.rule.yaml +24 -0
  206. package/src/hooks/ts/lib/rules/builtin/git-push-plus-refspec.rule.yaml +12 -0
  207. package/src/hooks/ts/lib/rules/builtin/git-reset-hard.rule.yaml +12 -0
  208. package/src/hooks/ts/lib/rules/builtin/injection-then-tool.rule.yaml +30 -0
  209. package/src/hooks/ts/lib/rules/builtin/instruction-override-disregard.rule.yaml +12 -0
  210. package/src/hooks/ts/lib/rules/builtin/instruction-override-ignore.rule.yaml +13 -0
  211. package/src/hooks/ts/lib/rules/builtin/launchagents-write-persistence.rule.yaml +12 -0
  212. package/src/hooks/ts/lib/rules/builtin/launchctl-load-persistence.rule.yaml +10 -0
  213. package/src/hooks/ts/lib/rules/builtin/reverse-shell-dev-tcp.rule.yaml +11 -0
  214. package/src/hooks/ts/lib/rules/builtin/reverse-shell-interactive-bash.rule.yaml +11 -0
  215. package/src/hooks/ts/lib/rules/builtin/rm-recursive-parent.rule.yaml +11 -0
  216. package/src/hooks/ts/lib/rules/builtin/rm-recursive-relative.rule.yaml +11 -0
  217. package/src/hooks/ts/lib/rules/builtin/rm-recursive-root.rule.yaml +24 -0
  218. package/src/hooks/ts/lib/rules/builtin/rm-trailing-tilde.rule.yaml +11 -0
  219. package/src/hooks/ts/lib/rules/builtin/sudoers-nopasswd.rule.yaml +15 -0
  220. package/src/hooks/ts/lib/rules/builtin.generated.ts +530 -0
  221. package/src/hooks/ts/lib/rules/eval.ts +74 -0
  222. package/src/hooks/ts/lib/rules/lint.ts +102 -0
  223. package/src/hooks/ts/lib/rules/load.ts +229 -0
  224. package/src/hooks/ts/lib/rules/types.ts +56 -0
  225. package/src/hooks/ts/pre-tool-use/security-validator.ts +27 -96
  226. package/src/hooks/ts/user-prompt/handoff-link.ts +34 -0
  227. package/dist/aibroker-client-BxzeEHPF.mjs +0 -3
  228. package/dist/auto-route-C3S2W3Bc.mjs +0 -11
  229. package/dist/budget-BrloTOZW.mjs +0 -15
  230. package/dist/cockpit-memory-teG9M8bG.mjs +0 -20
  231. package/dist/config-Cxi-aBf4.mjs +0 -4
  232. package/dist/daemon-DeYImF1F.mjs +0 -45
  233. package/dist/detect-Rrimh0xc.mjs +0 -10
  234. package/dist/factory-Bs9imVMq.mjs +0 -9
  235. package/dist/kill-tree-SV6hrIa3.mjs +0 -10
  236. package/dist/operator-BTqvTIc8.mjs +0 -7
  237. package/dist/providers-FvCLzFge.mjs +0 -16
  238. package/dist/registrable-BzH4TObg.mjs +0 -9
  239. package/dist/render-B35bQTE-.mjs +0 -13
  240. package/dist/render-C84BqQRC.mjs +0 -903
  241. package/dist/root-DD1xx_Vu.mjs +0 -36
  242. package/dist/route-table-FEfDuhIV.mjs +0 -15
  243. package/dist/session-BAhtXdxj.mjs +0 -35
  244. package/dist/session-scan-B57dNuA4.mjs +0 -9
  245. package/dist/status-Dm4dYciN.mjs +0 -6
  246. package/dist/viewer-DEySE4c_.mjs +0 -22
  247. package/dist/worker-config-CO5FXLAT.mjs +0 -41
  248. package/dist/workers-config-f8KrkY4L.mjs +0 -4
@@ -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 { I as LOOPBACK_HOST, Q as relocateWorkersYaml, _t as contextWindowFromModelId, c as WorkersConfigError, f as escalationCloud, ht as DEFAULT_CONTEXT_WINDOW, j as readWorkersSection, m as isLocalProvider, p as getProviderOrNative, t as ANTHROPIC_NATIVE, tt as workersYamlPath, v as nativeAnthropicModels, vt as stripModelVariant, wt as appendLedger } from "../config-XpZWOYOG.mjs";
6
- import { A as assertPgName, C as migrateMainConfigToYaml, D as readJsonStrict, M as postgresConnectionString, N as postgresFullTarget, O as writeJsonAtomic, P as quoteIdent, S as MainConfigError, c as loadConfig, d as paiConfigYamlFilePath, f as readMainConfigRaw, j as isContainerTarget, k as DEFAULT_PG_CONNECTION_TIMEOUT_MS, l as migrateConfigFile, n as CONFIG_FILE$2, o as expandHome, p as writeMainConfigRaw, t as CONFIG_DIR, u as paiConfigFilePath, w as resolvedMainConfigPath } from "../config-Bu1_vbY8.mjs";
7
- import { i as ledgerPath, n as ensureNoMcpConfig, u as workersLogDir } from "../paths-CHifubDh.mjs";
8
- import "../worktree-CRs2gyg0.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-ggJ7lgVg.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-Dh-q3iJi.mjs";
11
- import { a as unregistrableReason, s as worktreeReason } from "../registrable-DpyTtajg.mjs";
12
- import "../pai-marker-BcZyJguM.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-Bz8pAU5Q.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-CypxFTfn.mjs";
8
+ import "../worktree-O_kU6Q3y.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-Cecau8Ps.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-9EsbKD-U.mjs";
11
+ import { a as unregistrableReason, s as worktreeReason } from "../registrable-Dxs4oKIj.mjs";
12
+ import "../pai-marker-D6OoVbM_.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-D-h6F6DN.mjs";
17
- import { At as hostAllowed, Bn as parkConfigFrom, Cn as providerHostPort, Dt as routerPort, E as budgetCache, Et as routerHandler, F as LimitsCache, Gn as aggregateBySession, Hn as sharedParker, Jn as formatStats, Jt as sessionStartTarget, Kn as appendUsage, Ln as setupsPath, Mn as loadSaved, On as allSetups, Ot as defaultV1Guard, Pn as saveSetup, Qn as readUsage, U as computeInventory, Un as recordDuration, Vn as setParkConfigSource, W as currentInventory, Wn as aggregate, Y as inventoryText, Zn as parseSince, at as readInventoryInput, dn as SESSION_NAME_ENV, jt as resolveBindAddress, kn as downLocalProviders, kt as displayHost, m as readRoutingState, mn as readProfiles, pt as isLoopbackProvider, qn as formatSessionStats, x as SESSION_WORKER_PROVIDER_ENV, xt as routeVerdictLine, yn as localProviderForDelegation } from "../routing-DmkheCRH.mjs";
16
+ import { a as ownerSessionName, d as terminalHasName } from "../scope-BTWstJ1B.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-aJKMIagQ.mjs";
18
18
  import { n as minOf } from "../min-max-DrSRUaWQ.mjs";
19
- import "../tool-errors-7E13StWd.mjs";
20
- import "../wait-ByV0OyMZ.mjs";
21
- import "../local-tiers-BE4jGaMa.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-yLFb7CKq.mjs";
19
+ import "../tool-errors-BFw1gv-z.mjs";
20
+ import "../wait-C5iWILj_.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-C6Nc96iM.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-DEveQdFV.mjs";
25
- import "../local-instances-DOxSgT_L.mjs";
26
- import { n as tierProviderNames, t as isLocalCandidate } from "../local-candidate-DLdwQt0s.mjs";
24
+ import { i as effectiveCap, o as providerCap, r as capacityOf } from "../capacity-B2PgKAg4.mjs";
25
+ import "../local-instances-DhF4_sV2.mjs";
26
+ import { n as tierProviderNames, t as isLocalCandidate } from "../local-candidate-DFd4pp-w.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-B7nv75rC.mjs";
29
- import { c as resolveFromModule, i as retryTransient, n as embedChunksWithBackend, r as indexProjectWithBackend, s as packageRoot } from "../postgres-DNIOQijl.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-6rZrWRXa.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-BLKsUOgG.mjs";
34
- import { n as getProject$1, o as requireProject, r as getProjectAliases, s as resolveIdentifier } from "../helpers-DSNK_1KP.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-BBGvU5EB.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-Dn5OJXlg.mjs";
37
- import { a as detectCurrentProviderName, i as detectCurrentModel, n as buildLaunchPlan, o as launchableProviderNames, r as buildLaunchRows, s as maskLaunchEnv } from "../launch-Di7N-J5X.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-C4FQmkJn.mjs";
39
- import { a as providerResumePlan, n as launchInDir, o as restoreTopLevel, r as launchPrompt, s as resumeArgvText, t as hasConversation } from "../launch-DlZTKE3u.mjs";
40
- import { _ as setupsTable, a as lastSetup, m as rootCommand } from "../root-C4oXlXS_.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-DUn10MBd.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-BwZKBJZZ.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-JLm6g0yP.mjs";
45
- import "../endreason-BC-v05H_.mjs";
46
- import { N as endBuildWait, j as beginBuildWait } from "../render-C84BqQRC.mjs";
47
- import "../operator-CyuB3aVw.mjs";
48
- import "../kill-tree-DleMNZX3.mjs";
49
- import { s as poolReloadLive } from "../pool-cli-BggU8GRp.mjs";
50
- import "../chain-D7PFur8Y.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-CBRtFFh0.mjs";
52
- import { m as runInPane, r as closeFinishedPanes } from "../pane-audit-Lqy9N_Ki.mjs";
53
- import "../viewer-wfKnvnrI.mjs";
54
- import "../fallback-CBWpp2F2.mjs";
55
- import { g as setWorkersEnabled, s as describeProviders } from "../providers-C9QC0TtO.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-eY5Qy4vl.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-kTXR8Pyv.mjs";
58
- import { H as acquireRun, U as readRun, V as cockpitProbeSettings, k as readRuntimeCommands } from "../cockpit-actions-OodH_vx_.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-e64AwDCa.mjs";
61
- import { i as totalUsageTokens, r as parseSessionUsage } from "../session-usage-CPvXvSkw.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-BdjeL4P8.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-CNMcltOX.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-HBYOABWU.mjs";
37
+ import { a as detectCurrentProviderName, i as detectCurrentModel, n as buildLaunchPlan, o as launchableProviderNames, r as buildLaunchRows, s as maskLaunchEnv } from "../launch-Bn2Zf90h.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-D0mUAVdw.mjs";
39
+ import { a as providerResumePlan, n as launchInDir, o as restoreTopLevel, r as launchPrompt, s as resumeArgvText, t as hasConversation } from "../launch-BZT3JDoZ.mjs";
40
+ import { _ as setupsTable, a as lastSetup, m as rootCommand } from "../root-DmOzMeSv.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-CX3MKCNg.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-DE9Q0ldN.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-rIItFeIJ.mjs";
45
+ import "../endreason-C2-hYK0_.mjs";
46
+ import { B as scanTranscript, H as loadBuildsConfig, L as listRules, N as endBuildWait, P as claudeSessionTranscript, R as loadRules, V as maskSecrets, j as beginBuildWait, q as psTable, z as userRuleDirs } from "../render-jPpMrudf.mjs";
47
+ import "../operator-Br6GeswF.mjs";
48
+ import "../kill-tree-DPiMscaY.mjs";
49
+ import { s as poolReloadLive } from "../pool-cli-Db0uDwB9.mjs";
50
+ import "../chain-CtK49-Ma.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-DHxdJjEc.mjs";
52
+ import { m as runInPane, r as closeFinishedPanes } from "../pane-audit-DLYy9-4g.mjs";
53
+ import "../viewer-DvBg6N22.mjs";
54
+ import { g as setWorkersEnabled, s as describeProviders } from "../providers-BNV7CzC-.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-DkONNk_H.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-rda08BCD.mjs";
57
+ import { H as acquireRun, U as readRun, V as cockpitProbeSettings, k as readRuntimeCommands } from "../cockpit-actions-D1OPP6Es.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-CN1D8MXl.mjs";
60
+ import { a as liveWorkersConfigShadows, i as totalUsageTokens, r as parseSessionUsage } from "../session-usage-CCmPFFa6.mjs";
61
+ import "../fallback-CwPSCrX8.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-CFVU0cwA.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-h6Z9fN_o.mjs");
833
+ const { currentInventory } = await import("../model-inventory-BXv15B0d.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-h6Z9fN_o.mjs");
848
+ const { processTreeRssMb } = await import("../model-inventory-BXv15B0d.mjs");
849
849
  const pid = (await exec("lsof", [
850
850
  "-ti",
851
851
  `tcp:${port}`,
@@ -1161,11 +1161,11 @@ const runCommand = {
1161
1161
  if (!addr) throw new CliError(`cannot resolve router.bind "${bindName}" — use "loopback", "tailscale", or an IPv4 (never 0.0.0.0)`, "pai daemon status");
1162
1162
  const remote = addr !== LOOPBACK_HOST;
1163
1163
  const extraHosts = (loadConfig().router.allowedHosts ?? []).filter((h) => h.trim());
1164
- const hosts = remote ? {
1165
- bind: addr,
1164
+ const hosts = {
1165
+ bind: bindName || addr,
1166
1166
  extraHosts
1167
- } : void 0;
1168
- if (remote && !extraHosts.some((h) => hostAllowed(`${h}:${port}`, addr, extraHosts))) process.stderr.write(`pai: router: router.allowedHosts has no entry matching the bind ${addr} — cockpit requests by hostname will be refused (403)\n`);
1167
+ };
1168
+ if (remote && extraHosts.length === 0) process.stderr.write("pai: router: router.allowedHosts is empty: the cockpit opened by tailnet hostname will be refused (403); add the name to router.allowedHosts\n");
1169
1169
  const rows = cachedRows(usageFile);
1170
1170
  let schedCache;
1171
1171
  setSchedConfigSource(() => {
@@ -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-CO5FXLAT.mjs");
1218
- const { readLocalConfig } = await import("../local-tiers-DNMI5W86.mjs");
1219
- const { workersYamlPath } = await import("../workers-config-f8KrkY4L.mjs");
1217
+ const { startTier, stopAll } = await import("../worker-config-BmcJ9R0U.mjs");
1218
+ const { readLocalConfig } = await import("../local-tiers-DSlIYvvm.mjs");
1219
+ const { workersYamlPath } = await import("../workers-config-DwzrtdYu.mjs");
1220
1220
  const cfg = readLocalConfig(workersYamlPath());
1221
1221
  if (kind === "off") return void await stopAll(cfg);
1222
1222
  const t = cfg.tiers[tier];
@@ -1230,10 +1230,8 @@ const runCommand = {
1230
1230
  setEndpointHealth(endpointHealth);
1231
1231
  const serve = (req, res) => handleCockpit(req, res, limits, rows, sessionMap, system, workerWindows);
1232
1232
  const inner = routerHandler(void 0, void 0, void 0, usageFile ? () => void 0 : (row) => appendUsage(row), serve, {
1233
- ...remote ? {
1234
- remoteAllowed: defaultV1Guard,
1235
- hosts
1236
- } : {},
1233
+ hosts,
1234
+ ...remote ? { remoteAllowed: defaultV1Guard } : {},
1237
1235
  isUp: limits.isUp,
1238
1236
  health: endpointHealth
1239
1237
  });
@@ -1595,7 +1593,7 @@ const modelsCommand = {
1595
1593
  if (words[0] === "add") {
1596
1594
  const source = words[1];
1597
1595
  if (!source) throw new Error("usage: pai models add <hf-id|path> [--name n] [--download] [--try]");
1598
- const { addModel } = await import("../models-add-TVni3A7V.mjs");
1596
+ const { addModel } = await import("../models-add-5yXTyz3g.mjs");
1599
1597
  const r = await addModel({
1600
1598
  source,
1601
1599
  name: typeof ctx.opts.name === "string" ? ctx.opts.name : void 0,
@@ -1610,7 +1608,7 @@ const modelsCommand = {
1610
1608
  if (op) {
1611
1609
  const runtime = words[1];
1612
1610
  if (!runtime) throw new Error(`usage: pai models ${op} <runtime> (configured: ${Object.keys(readRuntimeCommands()).join(", ") || "none"})`);
1613
- const { defaultDeps, runtimeStartStop } = await import("../cockpit-actions-BfRMr2sm.mjs");
1611
+ const { defaultDeps, runtimeStartStop } = await import("../cockpit-actions-C73mJAJ8.mjs");
1614
1612
  const r = await runtimeStartStop(runtime, op, defaultDeps(), { model: typeof ctx.opts.model === "string" ? ctx.opts.model : void 0 });
1615
1613
  process.stdout.write(r.message + "\n");
1616
1614
  if (!r.ok) process.exitCode = 1;
@@ -1814,7 +1812,7 @@ const MAX_PROMPT_CHARS = 220;
1814
1812
  /** Newest prompts kept per transcript — headroom over MAX_PROMPTS for the duplicate collapse. */
1815
1813
  const KEEP_PROMPTS = 64;
1816
1814
  /** Cap on listed working-tree entries, so a big refactor cannot flood TODO.md. */
1817
- const MAX_FILES = 20;
1815
+ const MAX_FILES$1 = 20;
1818
1816
  /**
1819
1817
  * Text of a JSONL message, or null when the entry carries none.
1820
1818
  * Mirrors the shape handled by slug-generator: content is either a plain
@@ -2000,8 +1998,8 @@ function readWorkingTree(cwd) {
2000
1998
  "-1",
2001
1999
  "--pretty=%h %s"
2002
2000
  ]),
2003
- changes: all.slice(0, MAX_FILES),
2004
- 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)
2005
2003
  };
2006
2004
  }
2007
2005
  /**
@@ -2925,7 +2923,7 @@ async function cmdPause(opts) {
2925
2923
  process.exitCode = 1;
2926
2924
  return;
2927
2925
  }
2928
- const { getRegistryBackend } = await import("../factory-Bs9imVMq.mjs");
2926
+ const { getRegistryBackend } = await import("../factory-DFwez99M.mjs");
2929
2927
  const registryBackend = await getRegistryBackend();
2930
2928
  const cwd = process.cwd();
2931
2929
  const project = await resolveProjectByCwd(registryBackend, cwd);
@@ -3025,7 +3023,7 @@ function touchSentinel(rootPath) {
3025
3023
  async function cmdAutosave(opts) {
3026
3024
  const minGap = parseInt(opts.minGap ?? String(DEFAULT_MIN_GAP), 10);
3027
3025
  const cwd = process.cwd();
3028
- const { getRegistryBackend } = await import("../factory-Bs9imVMq.mjs");
3026
+ const { getRegistryBackend } = await import("../factory-DFwez99M.mjs");
3029
3027
  const registryBackend = await getRegistryBackend();
3030
3028
  const project = await resolveProjectByCwd(registryBackend, cwd);
3031
3029
  if (!project) return;
@@ -3251,7 +3249,7 @@ function finalizeNote(notePath) {
3251
3249
  }
3252
3250
  async function cmdEnd(opts) {
3253
3251
  await cmdPause(opts);
3254
- const { getRegistryBackend } = await import("../factory-Bs9imVMq.mjs");
3252
+ const { getRegistryBackend } = await import("../factory-DFwez99M.mjs");
3255
3253
  const registryBackend = await getRegistryBackend();
3256
3254
  const cwd = process.cwd();
3257
3255
  const project = await registryBackend.findProjectByCwdPrefix(cwd);
@@ -3432,7 +3430,7 @@ function buildStopBody(project, sessionId) {
3432
3430
  }
3433
3431
  }
3434
3432
  async function cmdHandover(projectSlug, numberOrLatest, sessionId) {
3435
- const { getRegistryBackend } = await import("../factory-Bs9imVMq.mjs");
3433
+ const { getRegistryBackend } = await import("../factory-DFwez99M.mjs");
3436
3434
  const registryBackend = await getRegistryBackend();
3437
3435
  let project;
3438
3436
  if (projectSlug) {
@@ -4378,8 +4376,8 @@ async function analyzeProject(registryBackend, project) {
4378
4376
  async function countVectorDbPaths(oldPaths) {
4379
4377
  if (oldPaths.length === 0) return 0;
4380
4378
  try {
4381
- const { loadConfig } = await import("../config-DtENJ6cL.mjs");
4382
- const { createStorageBackend } = await import("../factory-Bs9imVMq.mjs");
4379
+ const { loadConfig } = await import("../config-DHgPx3ER.mjs");
4380
+ const { createStorageBackend } = await import("../factory-DFwez99M.mjs");
4383
4381
  const config = loadConfig();
4384
4382
  if (config.storageBackend !== "postgres") return 0;
4385
4383
  const backend = await createStorageBackend(config);
@@ -4396,8 +4394,8 @@ async function countVectorDbPaths(oldPaths) {
4396
4394
  async function updateVectorDbPaths(moves) {
4397
4395
  if (moves.length === 0) return 0;
4398
4396
  try {
4399
- const { loadConfig } = await import("../config-DtENJ6cL.mjs");
4400
- const { createStorageBackend } = await import("../factory-Bs9imVMq.mjs");
4397
+ const { loadConfig } = await import("../config-DHgPx3ER.mjs");
4398
+ const { createStorageBackend } = await import("../factory-DFwez99M.mjs");
4401
4399
  const config = loadConfig();
4402
4400
  if (config.storageBackend !== "postgres") return 0;
4403
4401
  const backend = await createStorageBackend(config);
@@ -4484,7 +4482,7 @@ async function displayDryRun(plans) {
4484
4482
  console.log();
4485
4483
  }
4486
4484
  async function executeCleanup(plans, skipReindex) {
4487
- const { getRegistryBackend } = await import("../factory-Bs9imVMq.mjs");
4485
+ const { getRegistryBackend } = await import("../factory-DFwez99M.mjs");
4488
4486
  const registryBackend = await getRegistryBackend();
4489
4487
  let deleted = 0;
4490
4488
  let renamed = 0;
@@ -4684,7 +4682,7 @@ async function executeCleanup(plans, skipReindex) {
4684
4682
  * them into YYYY/MM/ folders and renumber. Dry run unless `execute` is set.
4685
4683
  */
4686
4684
  async function runCleanup(projectSlug, opts) {
4687
- const { getRegistryBackend } = await import("../factory-Bs9imVMq.mjs");
4685
+ const { getRegistryBackend } = await import("../factory-DFwez99M.mjs");
4688
4686
  const registryBackend = await getRegistryBackend();
4689
4687
  const dryRun = !opts.execute;
4690
4688
  const skipReindex = opts.reindex === false;
@@ -6666,7 +6664,7 @@ async function runRestore(backupPath, opts) {
6666
6664
  * backup is written first. Any other content, or an unknown previous rendering,
6667
6665
  * counts as user-edited: the new version goes beside it as CLAUDE.md.pai-new.
6668
6666
  */
6669
- function stamp(d) {
6667
+ function stamp$1(d) {
6670
6668
  return d.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
6671
6669
  }
6672
6670
  function writeAtomic(path, content) {
@@ -6685,7 +6683,7 @@ function refreshClaudeMd(claudeMd, newRendered, previousRendered, now = /* @__PU
6685
6683
  newPath
6686
6684
  };
6687
6685
  }
6688
- const backup = `${claudeMd}.bak-${stamp(now)}`;
6686
+ const backup = `${claudeMd}.bak-${stamp$1(now)}`;
6689
6687
  copyFileSync(claudeMd, backup);
6690
6688
  writeAtomic(claudeMd, newRendered);
6691
6689
  return {
@@ -8260,6 +8258,10 @@ const SETTINGS_HOOKS = [
8260
8258
  hookType: "UserPromptSubmit",
8261
8259
  command: "${ADAPTER_DIR}/Hooks/local-model-hint.mjs"
8262
8260
  },
8261
+ {
8262
+ hookType: "UserPromptSubmit",
8263
+ command: "${ADAPTER_DIR}/Hooks/handoff-link.mjs"
8264
+ },
8263
8265
  {
8264
8266
  hookType: "UserPromptSubmit",
8265
8267
  command: "${ADAPTER_DIR}/Hooks/capture-all-events.mjs --event-type UserPromptSubmit"
@@ -8967,8 +8969,8 @@ function cmdLogs(opts) {
8967
8969
  return 0;
8968
8970
  }
8969
8971
  async function daemonServeCmd() {
8970
- const { serve } = await import("../daemon-DeYImF1F.mjs");
8971
- const { loadConfig: lc, ensureConfigDir } = await import("../config-DtENJ6cL.mjs");
8972
+ const { serve } = await import("../daemon-176qA79N.mjs");
8973
+ const { loadConfig: lc, ensureConfigDir } = await import("../config-DHgPx3ER.mjs");
8972
8974
  ensureConfigDir();
8973
8975
  await serve(lc());
8974
8976
  return 0;
@@ -11139,6 +11141,232 @@ async function watchTicket(dir, id, deps = {}, intervalMs = 1e3, simctl, reap) {
11139
11141
  }
11140
11142
  }
11141
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
+
11142
11370
  //#endregion
11143
11371
  //#region src/builds/doctor.ts
11144
11372
  /**
@@ -11175,6 +11403,33 @@ function doctor(cwd) {
11175
11403
  findings
11176
11404
  };
11177
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
+ }
11178
11433
 
11179
11434
  //#endregion
11180
11435
  //#region src/cli/commands/build.ts
@@ -11315,14 +11570,19 @@ const nextCmd = {
11315
11570
  };
11316
11571
  const doctorCmd = {
11317
11572
  name: "doctor",
11318
- summary: "Check the project's registered build doors",
11319
- 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).",
11320
11575
  examples: ["pai build doctor"],
11321
- run: (ctx) => {
11576
+ run: async (ctx) => {
11322
11577
  const r = doctor(process.cwd());
11323
- if (!r.root) return out$2("no .pai/builds.yaml here: no doors registered"), 0;
11324
- for (const f of r.findings) out$2(`${f.ok ? "ok " : "FAIL"} ${f.door}${f.problem ? ` — ${f.problem}` : ""}`);
11325
- 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`);
11326
11586
  return r.findings.every((f) => f.ok) ? 0 : 1;
11327
11587
  }
11328
11588
  };
@@ -11999,8 +12259,8 @@ async function escalate(task, headline, detail, state, now, opts) {
11999
12259
  state.alarmedAt[task.id] = now;
12000
12260
  let delivered = false;
12001
12261
  try {
12002
- const { routeNotification } = await import("../router-DwHJRgG9.mjs");
12003
- const { loadConfig } = await import("../config-DtENJ6cL.mjs");
12262
+ const { routeNotification } = await import("../router-ghujlEFb.mjs");
12263
+ const { loadConfig } = await import("../config-DHgPx3ER.mjs");
12004
12264
  delivered = (await routeNotification({
12005
12265
  event: "error",
12006
12266
  title: `PAI: ${headline}`,
@@ -13916,6 +14176,16 @@ function configListCmd(opts) {
13916
14176
  console.log(opts.json ? JSON.stringify(r.data, null, 2) : r.yaml.trimEnd());
13917
14177
  return 0;
13918
14178
  }
14179
+ /** `pai config validate`: shadowed and unused workers keys across config.yaml and workers.yaml (warnings; exit 0). */
14180
+ function configValidateCmd() {
14181
+ const shadows = liveWorkersConfigShadows();
14182
+ if (!shadows.length) {
14183
+ console.log(ok$1("config.yaml and workers.yaml: no shadowed or unused keys"));
14184
+ return 0;
14185
+ }
14186
+ for (const s of shadows) console.log(`warning: ${s.message}`);
14187
+ return 0;
14188
+ }
13919
14189
  function configGetCmd(path, opts) {
13920
14190
  try {
13921
14191
  const r = getConfigValueOp(path);
@@ -14192,6 +14462,14 @@ const configSubcommands = [
14192
14462
  seeAlso: ["pai config migrate", "pai config set"],
14193
14463
  run: (ctx) => configUnsetCmd(String(ctx.args.PATH))
14194
14464
  },
14465
+ {
14466
+ name: "validate",
14467
+ summary: "Warn about workers settings that are shadowed or unused",
14468
+ description: "Warn about workers settings that are shadowed or unused.\n\nThe non-provider workers.* settings (pool, pane, routing, …) are read from config.yaml only; the routing tables (providers, tiers, classes, mcp_sets, …) from workers.yaml only. Names every key written in the file that does not win, and every top-level workers.yaml block nothing reads.",
14469
+ examples: ["pai config validate", "pai config get workers.pool.maxSlots --source"],
14470
+ seeAlso: ["pai config get", "pai config set"],
14471
+ run: () => configValidateCmd()
14472
+ },
14195
14473
  {
14196
14474
  name: "migrate",
14197
14475
  summary: "Move old settings files into the current layout",
@@ -14924,7 +15202,7 @@ async function migrateToPostgres(opts = {}) {
14924
15202
  const dump = pgDump(container, pgConfig, dumpPath);
14925
15203
  if (!dump.ok) return refused(dump.reason, dryRun);
14926
15204
  }
14927
- const { PostgresBackend } = await import("../postgres-BK4FP9Zg.mjs");
15205
+ const { PostgresBackend } = await import("../postgres-B1B80bcj.mjs");
14928
15206
  await PostgresBackend.ensureDatabase(pgConfig);
14929
15207
  const sqlite = new BetterSqlite3(sqliteFederationPath, { readonly: true });
14930
15208
  const registrySqlite = sqliteRegistryPath === sqliteFederationPath ? sqlite : new BetterSqlite3(sqliteRegistryPath, { readonly: true });
@@ -16311,7 +16589,7 @@ const out = (s) => process.stdout.write(s + "\n");
16311
16589
  async function repoOf(project) {
16312
16590
  if (typeof project === "string" && project) {
16313
16591
  if (existsSync(project)) return resolve(project);
16314
- const { getProject } = await import("../helpers-Dz7mjTSw.mjs");
16592
+ const { getProject } = await import("../helpers-DyHKAwrW.mjs");
16315
16593
  const row = await getProject(project);
16316
16594
  if (!row) throw new CliError(`no project ${project}`, "pai projects list");
16317
16595
  return row.root_path;
@@ -16540,7 +16818,7 @@ const reviewCmd = {
16540
16818
  return 0;
16541
16819
  }
16542
16820
  };
16543
- const listCmd = {
16821
+ const listCmd$2 = {
16544
16822
  name: "list",
16545
16823
  summary: "Every item of the project with its state",
16546
16824
  description: "States: pending, running, ready, too-big, needs-decision, cloud, accepted, rejected.",
@@ -16634,7 +16912,7 @@ const queueCommand = {
16634
16912
  addCmd,
16635
16913
  runCmd,
16636
16914
  reviewCmd,
16637
- listCmd,
16915
+ listCmd$2,
16638
16916
  acceptCmd,
16639
16917
  rejectCmd,
16640
16918
  againCmd
@@ -17134,7 +17412,7 @@ async function cmdPromote(opts) {
17134
17412
  console.error(dim$1(` Pick a durable location for the new project.`));
17135
17413
  process.exit(1);
17136
17414
  }
17137
- const { getRegistryBackend } = await import("../factory-Bs9imVMq.mjs");
17415
+ const { getRegistryBackend } = await import("../factory-DFwez99M.mjs");
17138
17416
  const registryBackend = await getRegistryBackend();
17139
17417
  const encodedDir = encodeDir(targetPath);
17140
17418
  if (await registryBackend.getProjectBySlug(slug) ?? await registryBackend.getProjectByRootPath(targetPath) ?? await registryBackend.getProjectByEncodedDir(encodedDir)) {
@@ -18378,7 +18656,7 @@ async function cmdUnregister(slug, opts = {}) {
18378
18656
  * deprecated alias that forwards here with a stderr notice.
18379
18657
  */
18380
18658
  async function projectListCmd(opts) {
18381
- const { cmdMain } = await import("../main-resolver-yp_puSSb.mjs");
18659
+ const { cmdMain } = await import("../main-resolver-BKwI0I6H.mjs");
18382
18660
  await cmdMain(void 0, void 0, opts);
18383
18661
  return 0;
18384
18662
  }
@@ -19801,7 +20079,7 @@ async function registryScanCmd(opts) {
19801
20079
  return 0;
19802
20080
  }
19803
20081
  if (opts.filterReport) {
19804
- const { reportUnregistrable } = await import("../registrable-BzH4TObg.mjs");
20082
+ const { reportUnregistrable } = await import("../registrable-BwFiqazy.mjs");
19805
20083
  const r = await reportUnregistrable(await getRegistryBackend());
19806
20084
  console.log(`Registry rows: ${r.total}`);
19807
20085
  console.log(` matching the registration filter: ${r.matching} (${r.matchingMissing} with a missing path)`);
@@ -21644,7 +21922,7 @@ async function runEmbed(backend, projectId, projectSlug, batchSize = 50, pageSiz
21644
21922
  const label = projectSlug ? `project ${projectSlug}` : "all projects";
21645
21923
  console.log(dim$1(`Generating embeddings for ${label} (this may take a while on first run)...`));
21646
21924
  const { serializeEmbedding } = await import("../embeddings-CIkJBKd-.mjs");
21647
- const { gateBackend } = await import("../embedding-gate-Cnk4YCyX.mjs");
21925
+ const { gateBackend } = await import("../embedding-gate-BVQvUmox.mjs");
21648
21926
  const gate = await gateBackend(backend, { record: true });
21649
21927
  if (!gate.ok) {
21650
21928
  console.error(err(gate.reason));
@@ -21973,7 +22251,7 @@ async function memorySearchCmd(query, opts) {
21973
22251
  let results;
21974
22252
  if (mode === "keyword") results = await backend.searchKeyword(query, searchOpts);
21975
22253
  else {
21976
- const { embedQuery } = await import("../embedding-gate-Cnk4YCyX.mjs");
22254
+ const { embedQuery } = await import("../embedding-gate-BVQvUmox.mjs");
21977
22255
  console.log(dim$1("Generating query embedding..."));
21978
22256
  const q = await embedQuery(backend, query);
21979
22257
  const queryEmbedding = q.vec;
@@ -22053,8 +22331,8 @@ async function memorySearchCmd(query, opts) {
22053
22331
  //#region src/cli/commands/memory/sources-cmd.ts
22054
22332
  /** `pai memory sources` — what the indexer has taken in. */
22055
22333
  async function memorySourcesCmd(opts) {
22056
- const { createStorageBackend } = await import("../factory-Bs9imVMq.mjs");
22057
- const { loadConfig } = await import("../config-DtENJ6cL.mjs");
22334
+ const { createStorageBackend } = await import("../factory-DFwez99M.mjs");
22335
+ const { loadConfig } = await import("../config-DHgPx3ER.mjs");
22058
22336
  const { cmdMemorySources } = await import("../sources-CiE3rZek.mjs");
22059
22337
  let backend;
22060
22338
  try {
@@ -22814,7 +23092,7 @@ async function zettelExploreCmd(note, opts) {
22814
23092
  const depth = opts.depth ?? 3;
22815
23093
  const direction = opts.direction ?? "both";
22816
23094
  const mode = opts.mode ?? "all";
22817
- const { zettelExplore } = await import("../zettelkasten-tpCRBYvH.mjs");
23095
+ const { zettelExplore } = await import("../zettelkasten-BH8AdNh8.mjs");
22818
23096
  const result = await zettelExplore(await getFedBackend(), {
22819
23097
  startNote: note,
22820
23098
  depth,
@@ -22870,7 +23148,7 @@ async function zettelHealthCmd(opts) {
22870
23148
  const projectPath = opts.project;
22871
23149
  const recentDays = opts.days ?? 30;
22872
23150
  const includeTypes = opts.include ? opts.include.split(",").map((s) => s.trim()) : void 0;
22873
- const { zettelHealth } = await import("../zettelkasten-tpCRBYvH.mjs");
23151
+ const { zettelHealth } = await import("../zettelkasten-BH8AdNh8.mjs");
22874
23152
  const result = await zettelHealth(await getFedBackend(), {
22875
23153
  scope,
22876
23154
  projectPath,
@@ -22928,7 +23206,7 @@ async function zettelSurpriseCmd(note, opts) {
22928
23206
  const limit = opts.limit ?? 10;
22929
23207
  const minSimilarity = opts.minSimilarity ?? .3;
22930
23208
  const minGraphDistance = opts.minDistance ?? 3;
22931
- const { zettelSurprise } = await import("../zettelkasten-tpCRBYvH.mjs");
23209
+ const { zettelSurprise } = await import("../zettelkasten-BH8AdNh8.mjs");
22932
23210
  const backend = await getFedBackend();
22933
23211
  console.log();
22934
23212
  console.log(header(" PAI Zettel Surprise"));
@@ -22970,7 +23248,7 @@ async function zettelConverseCmd(question, opts) {
22970
23248
  const vaultProjectId = Number(opts.vaultProjectId);
22971
23249
  const depth = opts.depth ?? 2;
22972
23250
  const limit = opts.limit ?? 15;
22973
- const { zettelConverse } = await import("../zettelkasten-tpCRBYvH.mjs");
23251
+ const { zettelConverse } = await import("../zettelkasten-BH8AdNh8.mjs");
22974
23252
  const backend = await getFedBackend();
22975
23253
  console.log();
22976
23254
  console.log(header(" PAI Zettel Converse"));
@@ -23020,7 +23298,7 @@ async function zettelThemesCmd(opts) {
23020
23298
  const minClusterSize = opts.minSize ?? 3;
23021
23299
  const maxThemes = opts.maxThemes ?? 10;
23022
23300
  const similarityThreshold = opts.threshold ?? .65;
23023
- const { zettelThemes } = await import("../zettelkasten-tpCRBYvH.mjs");
23301
+ const { zettelThemes } = await import("../zettelkasten-BH8AdNh8.mjs");
23024
23302
  const backend = await getFedBackend();
23025
23303
  console.log();
23026
23304
  console.log(header(" PAI Zettel Themes"));
@@ -23068,7 +23346,7 @@ async function zettelSuggestCmd(note, opts) {
23068
23346
  const vaultProjectId = Number(opts.vaultProjectId);
23069
23347
  const limit = opts.limit ?? 5;
23070
23348
  const excludeLinked = opts.excludeLinked !== true;
23071
- const { zettelSuggest } = await import("../zettelkasten-tpCRBYvH.mjs");
23349
+ const { zettelSuggest } = await import("../zettelkasten-BH8AdNh8.mjs");
23072
23350
  const backend = await getFedBackend();
23073
23351
  console.log();
23074
23352
  console.log(header(" PAI Zettel Suggest"));
@@ -24067,28 +24345,6 @@ const reloadCommand = {
24067
24345
  }
24068
24346
  };
24069
24347
 
24070
- //#endregion
24071
- //#region src/audit/tokens.ts
24072
- /**
24073
- * tokens.ts — the one tokenizer every audit section counts through.
24074
- *
24075
- * cl100k_base via js-tiktoken (pure JS, no native build). "all" + [] for the
24076
- * allowed/disallowed-special-token arguments matches Python tiktoken's
24077
- * `disallowed_special=()`: audited text (hook stdout, session transcripts)
24078
- * routinely contains literal "<|...|>"-shaped substrings that are not actual
24079
- * special tokens, and the strict default throws on sight of them.
24080
- */
24081
- const TOKEN_ENCODING = "cl100k_base";
24082
- let cached = null;
24083
- function encoder() {
24084
- if (!cached) cached = getEncoding(TOKEN_ENCODING);
24085
- return cached;
24086
- }
24087
- function countTokens(text) {
24088
- if (!text) return 0;
24089
- return encoder().encode(text, "all", []).length;
24090
- }
24091
-
24092
24348
  //#endregion
24093
24349
  //#region src/audit/files.ts
24094
24350
  /**
@@ -24182,149 +24438,41 @@ function auditFiles(paths) {
24182
24438
  }
24183
24439
 
24184
24440
  //#endregion
24185
- //#region src/audit/hooks.ts
24441
+ //#region src/audit/first-turn.ts
24186
24442
  /**
24187
- * `pai audit tokens hooks` — token cost of the hooks that fire before the
24188
- * model ever sees a token: SessionStart and UserPromptSubmit run on every
24189
- * new session / every prompt respectively, and whatever they print to
24190
- * 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.
24191
24466
  */
24192
- const HOOK_TIMEOUT_MS = 2e4;
24193
- function settingsPaths(cwd) {
24194
- return [join(claudeHomeDir(), "settings.json"), join(cwd, ".claude", "settings.json")].filter(existsSync);
24195
- }
24196
- function readSettings(path) {
24197
- try {
24198
- return JSON.parse(readFileSync(path, "utf8"));
24199
- } catch {
24200
- return {};
24467
+ function parseLines(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 {}
24201
24474
  }
24202
- }
24203
- /** Expand ${PAI_DIR} and $HOME/${HOME} the way settings.json's own hooks do. */
24204
- function expandCommand(command, env) {
24205
- const paiDir = env.PAI_DIR ?? process.env.PAI_DIR ?? claudeHomeDir();
24206
- return command.replace(/\$\{PAI_DIR\}/g, paiDir).replace(/\$\{HOME\}/g, homedir()).replace(/\$HOME\b/g, homedir());
24207
- }
24208
- function syntheticStdin(cwd, eventName) {
24209
- const sessionId = randomUUID();
24210
- return JSON.stringify({
24211
- session_id: sessionId,
24212
- cwd,
24213
- hook_event_name: eventName,
24214
- prompt: "audit",
24215
- transcript_path: join(claudeProjectsDir(), "audit", `${sessionId}.jsonl`)
24216
- });
24217
- }
24218
- function runHook(command, cwd, eventName) {
24219
- const result = spawnSync(command, {
24220
- shell: true,
24221
- cwd,
24222
- input: syntheticStdin(cwd, eventName),
24223
- encoding: "utf8",
24224
- timeout: HOOK_TIMEOUT_MS
24225
- });
24226
- if (result.error) return {
24227
- stdout: "",
24228
- error: result.error.message
24229
- };
24230
- if (result.signal) return {
24231
- stdout: result.stdout ?? "",
24232
- error: `killed by ${result.signal} (timeout?)`
24233
- };
24234
- return { stdout: result.stdout ?? "" };
24235
- }
24236
- const NOT_RUN = "not run (pass --run to execute hooks)";
24237
- const AUDITED_EVENTS = ["SessionStart", "UserPromptSubmit"];
24238
- /**
24239
- * Lists every SessionStart / UserPromptSubmit hook. Without `run` the hooks are only
24240
- * listed (tokens 0, marked not run): a project's settings.json is untrusted input and
24241
- * its hooks execute with shell:true. Pass `runHook` (CLI `--run`) to measure them.
24242
- */
24243
- function auditHooks(cwd, run) {
24244
- const settingsFiles = settingsPaths(cwd);
24245
- const readings = [];
24246
- const totalsByEvent = {};
24247
- const preToolUseHooks = [];
24248
- let preToolUseRewritesBash = false;
24249
- for (const settingsPath of settingsFiles) {
24250
- const settings = readSettings(settingsPath);
24251
- const env = settings.env ?? {};
24252
- for (const eventName of AUDITED_EVENTS) {
24253
- const groups = settings.hooks?.[eventName] ?? [];
24254
- totalsByEvent[eventName] = totalsByEvent[eventName] ?? 0;
24255
- for (const group of groups) for (const entry of group.hooks ?? []) {
24256
- if (!entry.command) continue;
24257
- const command = expandCommand(entry.command, env);
24258
- const { stdout, error } = run ? run(command, cwd, eventName) : {
24259
- stdout: "",
24260
- error: NOT_RUN
24261
- };
24262
- const tokens = countTokens(stdout);
24263
- totalsByEvent[eventName] += tokens;
24264
- readings.push({
24265
- event: eventName,
24266
- matcher: group.matcher,
24267
- command,
24268
- tokens,
24269
- error
24270
- });
24271
- }
24272
- }
24273
- for (const group of settings.hooks?.PreToolUse ?? []) for (const entry of group.hooks ?? []) {
24274
- if (!entry.command) continue;
24275
- preToolUseHooks.push({
24276
- matcher: group.matcher,
24277
- command: expandCommand(entry.command, env)
24278
- });
24279
- if (group.matcher === "Bash") preToolUseRewritesBash = true;
24280
- }
24281
- }
24282
- return {
24283
- encoding: TOKEN_ENCODING,
24284
- settingsFiles,
24285
- readings,
24286
- totalsByEvent,
24287
- preToolUseHooks,
24288
- preToolUseRewritesBash
24289
- };
24290
- }
24291
-
24292
- //#endregion
24293
- //#region src/audit/first-turn.ts
24294
- /**
24295
- * first-turn.ts — where a turn's context actually comes from, attributed
24296
- * line by line from the transcript.
24297
- *
24298
- * The API reports one number per assistant turn (cache_read + cache_creation
24299
- * + input). The transcript holds the pieces Claude Code injected since the
24300
- * previous turn: SessionStart / UserPromptSubmit hook output, the skill
24301
- * listing, the deferred-tool name list, MCP server instructions, the human
24302
- * prompt, any isMeta expansion (a skill body, a pasted slash-command result),
24303
- * tool results, and the previous turn's own visible output (re-sent as
24304
- * context on every later turn). Each of those is counted in cl100k here;
24305
- * what is left over is the system prompt, the built-in tool schemas, agents
24306
- * and memory files, which never appear in the transcript.
24307
- *
24308
- * Only the live lineage counts. Lines chain by parentUuid, and a prompt the
24309
- * user abandoned before a reply (Escape, re-submit) leaves a dead branch
24310
- * whose lines were never sent — run 9 measured a session whose first
24311
- * /Name was abandoned and read 24k where the true baseline is 30k, because
24312
- * the 3.5k-token skill listing sat on the dead branch.
24313
- *
24314
- * `type:"system"` lines with subtype local_command hold a slash command's
24315
- * raw stdout, and the model receives it verbatim in addition to the isMeta
24316
- * markdown copy — run 11 measured both present on the same turn, so both are
24317
- * counted.
24318
- */
24319
- function parseLines(path) {
24320
- const lines = [];
24321
- for (const raw of readFileSync(path, "utf8").split("\n")) {
24322
- if (!raw.trim()) continue;
24323
- try {
24324
- lines.push(JSON.parse(raw));
24325
- } catch {}
24326
- }
24327
- return lines;
24475
+ return lines;
24328
24476
  }
24329
24477
  function classify(line) {
24330
24478
  if (line.type === "attachment") {
@@ -24743,8 +24891,8 @@ function deriveSessionTrigger(transcriptPath, env = process.env, now = Date.now(
24743
24891
  * and the cache-TTL split (ephemeral 5m vs 1h) that says which cache the
24744
24892
  * session is riding.
24745
24893
  */
24746
- /** Newest *.jsonl under ~/.claude/projects, excluding subagents/. */
24747
- function newestSessionLog() {
24894
+ /** Newest *.jsonl under ~/.claude/projects, excluding subagents/. With a session id, that session's file instead. */
24895
+ function newestSessionLog(sessionId) {
24748
24896
  const projectsDir = claudeProjectsDir();
24749
24897
  let best = null;
24750
24898
  let projectDirs;
@@ -24763,6 +24911,7 @@ function newestSessionLog() {
24763
24911
  }
24764
24912
  for (const entry of entries) {
24765
24913
  if (!entry.endsWith(".jsonl")) continue;
24914
+ if (sessionId && entry !== `${sessionId}.jsonl`) continue;
24766
24915
  const filePath = join(dirPath, entry);
24767
24916
  let mtime;
24768
24917
  try {
@@ -26344,12 +26493,14 @@ function hookRunner(run) {
26344
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."));
26345
26494
  return runHook;
26346
26495
  }
26347
- function printHooks(report) {
26496
+ function printHooks(report, sinceHours) {
26348
26497
  console.log(header(`Hook output (encoding: ${report.encoding})`));
26349
26498
  console.log(dim$1(`settings.json: ${report.settingsFiles.join(", ") || "none found"}`));
26350
26499
  const rows = report.readings.map((r) => [
26351
26500
  r.event,
26352
26501
  r.matcher ?? "",
26502
+ r.state,
26503
+ r.lastSeen ?? "",
26353
26504
  String(r.tokens),
26354
26505
  r.error ? err(r.error) : "",
26355
26506
  r.command
@@ -26357,10 +26508,14 @@ function printHooks(report) {
26357
26508
  console.log(renderTable$1([
26358
26509
  "event",
26359
26510
  "matcher",
26511
+ "state",
26512
+ "last seen",
26360
26513
  "tokens",
26361
26514
  "error",
26362
26515
  "command"
26363
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`));
26364
26519
  for (const [event, total] of Object.entries(report.totalsByEvent)) console.log(bold$1(`${event} total: ${total} tokens`));
26365
26520
  console.log(dim$1(`PreToolUse hooks: ${report.preToolUseHooks.length}` + (report.preToolUseRewritesBash ? ", including a Bash-matcher rewrite hook" : "")));
26366
26521
  }
@@ -26686,7 +26841,7 @@ const DEFAULT_CTX_THRESHOLD = 2e5;
26686
26841
  async function gatherCombined() {
26687
26842
  const cwd = process.cwd();
26688
26843
  const files = auditFiles(defaultFileSet(cwd));
26689
- const hooks = auditHooks(cwd);
26844
+ const hooks = auditHooks(cwd, { sinceHours: DEFAULT_SINCE_HOURS });
26690
26845
  const session = await auditSession(newestSessionLog() ?? void 0).catch(() => null);
26691
26846
  const daemon = auditDaemon();
26692
26847
  const env = auditEnv();
@@ -26745,7 +26900,7 @@ function combinedSectionOutputs(data) {
26745
26900
  });
26746
26901
  sections.push({
26747
26902
  name: "hooks",
26748
- text: captureOutput(() => printHooks(data.hooks))
26903
+ text: captureOutput(() => printHooks(data.hooks, DEFAULT_SINCE_HOURS))
26749
26904
  });
26750
26905
  if (data.session) sections.push({
26751
26906
  name: "session",
@@ -26828,6 +26983,71 @@ function recordCombinedReport(dir, data) {
26828
26983
  for (const f of data.findings) counts[f.severity]++;
26829
26984
  appendFileSync(runsPath, `- ${date} run${runNumber}: ${counts.RED} RED, ${counts.AMBER} AMBER, ${counts.GREEN} GREEN — ${baseName}.md\n`, "utf8");
26830
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()));
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
+ }
26831
27051
  /**
26832
27052
  * `--json` is declared once, on `tokensCmd` itself, and read back from there
26833
27053
  * in every subcommand action below — not redeclared per subcommand. The parser
@@ -26848,12 +27068,16 @@ async function auditFilesCmd(paths, opts) {
26848
27068
  }
26849
27069
  /** Exported function for audit tokens hooks command */
26850
27070
  function auditHooksCmd(opts) {
26851
- 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
+ });
26852
27076
  if (opts.json) {
26853
27077
  printJson(report);
26854
27078
  return 0;
26855
27079
  }
26856
- printHooks(report);
27080
+ printHooks(report, sinceHours);
26857
27081
  return 0;
26858
27082
  }
26859
27083
  /** Exported function for audit tokens session command */
@@ -27041,7 +27265,8 @@ const t = (name, summary, description, extra = {}) => a({
27041
27265
  if (name === "files") return auditFilesCmd(ctx.args.PATHS ?? [], { json: ctx.opts.json === true });
27042
27266
  if (name === "hooks") return auditHooksCmd({
27043
27267
  json: ctx.opts.json === true,
27044
- run: ctx.opts.run === true
27268
+ run: ctx.opts.run === true,
27269
+ since: ctx.opts.since
27045
27270
  });
27046
27271
  if (name === "session") return auditSessionCmd(ctx.args.PATH, {
27047
27272
  json: ctx.opts.json === true,
@@ -27114,13 +27339,22 @@ const auditCommand = {
27114
27339
  }],
27115
27340
  examples: ["pai audit tokens files", "pai audit tokens files ./CLAUDE.md --json"]
27116
27341
  }),
27117
- 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.", {
27118
27343
  options: [{
27119
27344
  name: "run",
27120
27345
  type: "boolean",
27121
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)"
27122
27352
  }],
27123
- 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
+ ]
27124
27358
  }),
27125
27359
  t("session", "Token split for one session transcript", "Shows cache, input and output tokens of a session (default: the newest).", {
27126
27360
  args: [{
@@ -27227,6 +27461,1606 @@ const auditCommand = {
27227
27461
  examples: ["pai audit tokens ladder", "pai audit tokens ladder --live --model haiku"]
27228
27462
  })
27229
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 real = (p) => {
27510
+ try {
27511
+ return realpathSync(p);
27512
+ } catch {
27513
+ return resolve(p);
27514
+ }
27515
+ };
27516
+ function jsonlFiles(dir, sinceMs, out = []) {
27517
+ let names;
27518
+ try {
27519
+ names = readdirSync(dir);
27520
+ } catch {
27521
+ return out;
27522
+ }
27523
+ for (const n of names) {
27524
+ const p = join(dir, n);
27525
+ try {
27526
+ const st = statSync(p);
27527
+ if (st.isDirectory()) jsonlFiles(p, sinceMs, out);
27528
+ else if (/\.jsonl(\.1)?$/.test(n) && st.mtimeMs >= sinceMs) out.push(p);
27529
+ } catch {}
27530
+ }
27531
+ return out;
27532
+ }
27533
+ /** Sessions whose Edit/Write-style tool events (after `sinceMs`) touched any of `paths`. */
27534
+ function sessionsTouching(paths, sinceMs, captureDir) {
27535
+ const wanted = new Map(paths.map((p) => [real(p), p]));
27536
+ const bySession = /* @__PURE__ */ new Map();
27537
+ for (const file of jsonlFiles(captureDir, sinceMs)) {
27538
+ let text;
27539
+ try {
27540
+ text = readFileSync(file, "utf8");
27541
+ } catch {
27542
+ continue;
27543
+ }
27544
+ for (const line of text.split("\n")) {
27545
+ if (!line.includes("file_path") && !line.includes("notebook_path")) continue;
27546
+ let e;
27547
+ try {
27548
+ e = JSON.parse(line);
27549
+ } catch {
27550
+ continue;
27551
+ }
27552
+ const tool = e.tool ?? e.payload?.tool_name;
27553
+ if (!EDIT_TOOLS.has(tool)) continue;
27554
+ const ts = typeof e.timestamp === "number" ? e.timestamp : Date.parse(e.timestamp);
27555
+ if (Number.isFinite(ts) && ts < sinceMs) continue;
27556
+ const input = e.input ?? e.payload?.tool_input ?? {};
27557
+ const fp = input.file_path ?? input.notebook_path;
27558
+ const sessionId = e.session ?? e.session_id;
27559
+ if (typeof fp !== "string" || typeof sessionId !== "string") continue;
27560
+ const orig = wanted.get(real(fp));
27561
+ if (!orig) continue;
27562
+ const workerId = e.workerId ?? e.worker_id ?? e.env?.PAI_WORKER_ID ?? e.payload?.env?.PAI_WORKER_ID;
27563
+ const cur = bySession.get(sessionId) ?? {
27564
+ sessionId,
27565
+ files: []
27566
+ };
27567
+ if (typeof workerId === "string" && !cur.workerId) cur.workerId = workerId;
27568
+ if (!cur.files.includes(orig)) cur.files.push(orig);
27569
+ bySession.set(sessionId, cur);
27570
+ }
27571
+ }
27572
+ return [...bySession.values()];
27573
+ }
27574
+ /** Note body: one `pai-session` line per session, plus `pai-worker` where known. */
27575
+ function noteText(entries) {
27576
+ const lines = [];
27577
+ for (const e of entries) {
27578
+ lines.push(`pai-session: ${e.sessionId}`);
27579
+ if (e.workerId) lines.push(`pai-worker: ${e.workerId}`);
27580
+ }
27581
+ return lines.join("\n");
27582
+ }
27583
+ /** Attach `text` to `commit` under refs/notes/pai; false on any error. */
27584
+ function writeNote(repo, commit, text) {
27585
+ try {
27586
+ execFileSync("git", [
27587
+ "notes",
27588
+ "--ref=pai",
27589
+ "add",
27590
+ "-f",
27591
+ "-m",
27592
+ text,
27593
+ commit
27594
+ ], {
27595
+ cwd: repo,
27596
+ timeout: 3e3,
27597
+ stdio: "ignore"
27598
+ });
27599
+ return true;
27600
+ } catch {
27601
+ return false;
27602
+ }
27603
+ }
27604
+ const git = (repo, args, timeout = 1e3) => execFileSync("git", args, {
27605
+ cwd: repo,
27606
+ timeout,
27607
+ encoding: "utf8",
27608
+ stdio: [
27609
+ "ignore",
27610
+ "pipe",
27611
+ "ignore"
27612
+ ]
27613
+ }).trim();
27614
+ const gitOk = (repo, args) => {
27615
+ try {
27616
+ git(repo, args);
27617
+ return true;
27618
+ } catch {
27619
+ return false;
27620
+ }
27621
+ };
27622
+ /** The post-commit work: note the sessions that edited the files of HEAD. Never throws. */
27623
+ function postCommitHook(repo, captureDir, env = process.env, now = Date.now()) {
27624
+ try {
27625
+ if (/rebase/.test(env.GIT_REFLOG_ACTION ?? "")) return false;
27626
+ if (gitOk(repo, [
27627
+ "rev-parse",
27628
+ "-q",
27629
+ "--verify",
27630
+ "REBASE_HEAD"
27631
+ ]) || gitOk(repo, [
27632
+ "rev-parse",
27633
+ "-q",
27634
+ "--verify",
27635
+ "CHERRY_PICK_HEAD"
27636
+ ])) return false;
27637
+ const top = git(repo, ["rev-parse", "--show-toplevel"]);
27638
+ const text = noteText(sessionsTouching(git(repo, [
27639
+ "diff-tree",
27640
+ "--root",
27641
+ "--no-commit-id",
27642
+ "--name-only",
27643
+ "-r",
27644
+ "HEAD"
27645
+ ]).split("\n").filter(Boolean).map((f) => join(top, f)), now - LOOKBACK_MS, captureDir));
27646
+ return text ? writeNote(repo, "HEAD", text) : false;
27647
+ } catch {
27648
+ return false;
27649
+ }
27650
+ }
27651
+ function hookPath(repo) {
27652
+ const p = git(repo, [
27653
+ "rev-parse",
27654
+ "--git-path",
27655
+ "hooks/post-commit"
27656
+ ]);
27657
+ return isAbsolute(p) ? p : resolve(repo, p);
27658
+ }
27659
+ /** Install the post-commit hook, chaining an existing foreign hook as post-commit.pre-pai. */
27660
+ function setupGitNotes(repo) {
27661
+ const hook = hookPath(repo);
27662
+ const old = `${hook}.pre-pai`;
27663
+ let chained = existsSync(old);
27664
+ if (existsSync(hook) && !readFileSync(hook, "utf8").includes(HOOK_MARKER)) {
27665
+ renameSync(hook, old);
27666
+ chained = true;
27667
+ }
27668
+ 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 || true\n`);
27669
+ chmodSync(hook, 493);
27670
+ git(repo, [
27671
+ "config",
27672
+ "notes.rewriteRef",
27673
+ NOTES_REF
27674
+ ]);
27675
+ return {
27676
+ hook,
27677
+ chained
27678
+ };
27679
+ }
27680
+ /** Remove the PAI hook and restore the chained one; false when PAI had no hook here. */
27681
+ function removeGitNotes(repo) {
27682
+ const hook = hookPath(repo);
27683
+ const old = `${hook}.pre-pai`;
27684
+ if (!existsSync(hook) || !readFileSync(hook, "utf8").includes(HOOK_MARKER)) return false;
27685
+ unlinkSync(hook);
27686
+ if (existsSync(old)) renameSync(old, hook);
27687
+ try {
27688
+ if (git(repo, ["config", "notes.rewriteRef"]) === NOTES_REF) git(repo, [
27689
+ "config",
27690
+ "--unset",
27691
+ "notes.rewriteRef"
27692
+ ]);
27693
+ } catch {}
27694
+ return true;
27695
+ }
27696
+ /** The PAI note of `rev`, or undefined. */
27697
+ function showNote(repo, rev = "HEAD") {
27698
+ try {
27699
+ return git(repo, [
27700
+ "notes",
27701
+ "--ref=pai",
27702
+ "show",
27703
+ rev
27704
+ ]) || void 0;
27705
+ } catch {
27706
+ return;
27707
+ }
27708
+ }
27709
+
27710
+ //#endregion
27711
+ //#region src/cli/commands/git.ts
27712
+ /** `pai git ...`: link commits to the session/worker that wrote them (git notes, ref pai). */
27713
+ const repoOrFail = () => process.cwd();
27714
+ const gitCommand = {
27715
+ name: "git",
27716
+ summary: "Link commits to the session that wrote them",
27717
+ description: "Installs a post-commit hook that records the session and worker that edited a commit's files as a git note (ref pai).",
27718
+ examples: [
27719
+ "pai git setup",
27720
+ "pai git notes",
27721
+ "pai git remove"
27722
+ ],
27723
+ seeAlso: ["pai worker ps"],
27724
+ subcommands: [
27725
+ {
27726
+ name: "setup",
27727
+ summary: "Install the post-commit hook in the current repository",
27728
+ 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.",
27729
+ examples: ["pai git setup"],
27730
+ run: () => {
27731
+ try {
27732
+ const r = setupGitNotes(repoOrFail());
27733
+ process.stdout.write(`Installed ${r.hook}${r.chained ? " (existing hook chained)" : ""}\n`);
27734
+ return 0;
27735
+ } catch {
27736
+ throw new CliError("Not a git repository", "Run it inside a repository");
27737
+ }
27738
+ }
27739
+ },
27740
+ {
27741
+ name: "remove",
27742
+ summary: "Remove the hook and restore the previous one",
27743
+ description: "Deletes the PAI post-commit hook, puts a chained hook back and unsets notes.rewriteRef.",
27744
+ examples: ["pai git remove"],
27745
+ run: () => {
27746
+ process.stdout.write(removeGitNotes(repoOrFail()) ? "Removed the PAI post-commit hook\n" : "No PAI hook installed\n");
27747
+ return 0;
27748
+ }
27749
+ },
27750
+ {
27751
+ name: "notes",
27752
+ summary: "Show the PAI note of a commit",
27753
+ description: "Prints the session and worker recorded for a commit (default HEAD), or 'no PAI note'.",
27754
+ args: [{
27755
+ name: "REV",
27756
+ summary: "Commit to show (default HEAD)"
27757
+ }],
27758
+ examples: ["pai git notes", "pai git notes HEAD~1"],
27759
+ run: (ctx) => {
27760
+ process.stdout.write((showNote(repoOrFail(), ctx.args.REV) ?? "no PAI note") + "\n");
27761
+ return 0;
27762
+ }
27763
+ },
27764
+ {
27765
+ name: "hook",
27766
+ summary: "Git hook entry points (called by git)",
27767
+ description: "Internal: `pai git hook post-commit` is what the installed hook runs. Always exits 0.",
27768
+ examples: ["pai git hook post-commit"],
27769
+ hidden: true,
27770
+ subcommands: [{
27771
+ name: "post-commit",
27772
+ summary: "Record the sessions that edited the last commit's files",
27773
+ description: "Skips during rebase and cherry-pick; looks back 6 hours in the raw captures. Always exits 0.",
27774
+ examples: ["pai git hook post-commit"],
27775
+ run: () => {
27776
+ postCommitHook(process.cwd(), join(historyDir(), "raw-outputs"));
27777
+ return 0;
27778
+ }
27779
+ }]
27780
+ }
27781
+ ]
27782
+ };
27783
+
27784
+ //#endregion
27785
+ //#region src/cli/commands/lessons.ts
27786
+ /** `pai lessons ...`: review-gated lessons (candidate, approved, rejected, superseded). */
27787
+ const ID = {
27788
+ name: "ID",
27789
+ required: true,
27790
+ summary: "Lesson id (its file name without .md)"
27791
+ };
27792
+ function state(v) {
27793
+ if (v === void 0) return void 0;
27794
+ if (!LESSON_STATES.includes(v)) throw new CliError(`unknown state "${v}" (${LESSON_STATES.join("|")})`);
27795
+ return v;
27796
+ }
27797
+ function row(l) {
27798
+ return `${l.id} [${l.state}] ${l.kind} ${l.project} ${l.title}`;
27799
+ }
27800
+ function wrap(f) {
27801
+ try {
27802
+ return f();
27803
+ } catch (e) {
27804
+ throw new CliError(e instanceof Error ? e.message : String(e));
27805
+ }
27806
+ }
27807
+ /** Newest transcript of the current directory under ~/.claude/projects. */
27808
+ function newestTranscriptOfCwd() {
27809
+ const dir = join(claudeProjectsDir(), process.cwd().replace(/[^a-zA-Z0-9]/g, "-"));
27810
+ if (!existsSync(dir)) return null;
27811
+ const files = readdirSync(dir).filter((f) => f.endsWith(".jsonl")).map((f) => ({
27812
+ f: join(dir, f),
27813
+ m: statSync(join(dir, f)).mtimeMs
27814
+ }));
27815
+ files.sort((a, b) => b.m - a.m);
27816
+ return files[0]?.f ?? null;
27817
+ }
27818
+ const transition = (name, to, summary) => ({
27819
+ name,
27820
+ summary,
27821
+ description: `${summary}. Only candidates can be ${name}d.`,
27822
+ args: [ID],
27823
+ examples: [`pai lessons ${name} my-lesson`],
27824
+ run: (ctx) => {
27825
+ const l = wrap(() => setState(ctx.args.ID, to));
27826
+ console.log(`${l.id}: ${l.state}`);
27827
+ return 0;
27828
+ }
27829
+ });
27830
+ const lessonsCommand = {
27831
+ name: "lessons",
27832
+ summary: "Review-gated lessons for agents",
27833
+ description: "A short curated memory layer, one Markdown file per lesson in the PAI home. Candidates only reach agents once approved.",
27834
+ examples: [
27835
+ "pai lessons list --state candidate",
27836
+ "pai lessons propose",
27837
+ "pai lessons approve my-lesson"
27838
+ ],
27839
+ seeAlso: ["pai memory"],
27840
+ subcommands: [
27841
+ {
27842
+ name: "list",
27843
+ summary: "List lessons",
27844
+ description: "Lists lessons, optionally filtered by state or project.",
27845
+ options: [
27846
+ {
27847
+ name: "state",
27848
+ type: "string",
27849
+ value: "S",
27850
+ summary: "candidate, approved, rejected or superseded"
27851
+ },
27852
+ {
27853
+ name: "project",
27854
+ type: "string",
27855
+ value: "P",
27856
+ summary: "Project slug or global"
27857
+ },
27858
+ {
27859
+ ...OPT.json,
27860
+ summary: "Print JSON"
27861
+ }
27862
+ ],
27863
+ examples: ["pai lessons list", "pai lessons list --state approved --project global"],
27864
+ run: (ctx) => {
27865
+ const ls = wrap(() => listLessons({
27866
+ state: state(ctx.opts.state),
27867
+ project: ctx.opts.project
27868
+ }));
27869
+ if (ctx.opts.json === true) console.log(JSON.stringify(ls, null, 2));
27870
+ else if (ls.length === 0) console.log("No lessons.");
27871
+ else for (const l of ls) console.log(row(l));
27872
+ return 0;
27873
+ }
27874
+ },
27875
+ {
27876
+ name: "show",
27877
+ summary: "Show one lesson",
27878
+ description: "Prints a lesson with its frontmatter fields and body.",
27879
+ args: [ID],
27880
+ examples: ["pai lessons show my-lesson"],
27881
+ run: (ctx) => {
27882
+ const l = getLesson(ctx.args.ID);
27883
+ if (!l) throw new CliError(`lesson not found: ${ctx.args.ID}`);
27884
+ console.log(row(l));
27885
+ console.log(`applies when: ${l.applies_when || "(not set)"}`);
27886
+ console.log(`evidence: ${l.evidence.join(", ") || "(none)"}`);
27887
+ if (l.superseded_by) console.log(`superseded by: ${l.superseded_by}`);
27888
+ console.log(`\n${l.body}`);
27889
+ return 0;
27890
+ }
27891
+ },
27892
+ {
27893
+ name: "add",
27894
+ summary: "Add a candidate lesson",
27895
+ description: "Creates a lesson in the candidate state; approve it after review.",
27896
+ options: [
27897
+ {
27898
+ name: "title",
27899
+ type: "string",
27900
+ value: "T",
27901
+ summary: "Short title"
27902
+ },
27903
+ {
27904
+ name: "kind",
27905
+ type: "string",
27906
+ value: "K",
27907
+ summary: LESSON_KINDS.join(", ")
27908
+ },
27909
+ {
27910
+ name: "applies-when",
27911
+ type: "string",
27912
+ value: "W",
27913
+ summary: "One sentence: when the lesson applies"
27914
+ },
27915
+ {
27916
+ name: "project",
27917
+ type: "string",
27918
+ value: "P",
27919
+ summary: "Project slug (default: global)"
27920
+ },
27921
+ {
27922
+ name: "body",
27923
+ type: "string",
27924
+ value: "B",
27925
+ summary: "The lesson in 1-5 sentences"
27926
+ }
27927
+ ],
27928
+ examples: ["pai lessons add --title \"Run tsc first\" --kind workflow --applies-when \"before committing\""],
27929
+ run: (ctx) => {
27930
+ const { title, kind } = ctx.opts;
27931
+ const when = ctx.opts["applies-when"];
27932
+ if (!title || !kind || !when) throw new CliError("--title, --kind and --applies-when are required");
27933
+ const l = wrap(() => addLesson({
27934
+ title: String(title),
27935
+ kind,
27936
+ applies_when: String(when),
27937
+ project: ctx.opts.project,
27938
+ body: ctx.opts.body
27939
+ }));
27940
+ console.log(`added ${l.id} [candidate]`);
27941
+ return 0;
27942
+ }
27943
+ },
27944
+ transition("approve", "approved", "Approve a candidate"),
27945
+ transition("reject", "rejected", "Reject a candidate"),
27946
+ {
27947
+ name: "promote",
27948
+ summary: "Promote an approved lesson to a project skill",
27949
+ 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.",
27950
+ args: [ID],
27951
+ options: [
27952
+ {
27953
+ name: "project",
27954
+ type: "string",
27955
+ value: "DIR",
27956
+ summary: "Project directory (default: this directory)"
27957
+ },
27958
+ {
27959
+ name: "force",
27960
+ type: "boolean",
27961
+ summary: "Overwrite an existing SKILL.md"
27962
+ },
27963
+ {
27964
+ name: "agents",
27965
+ type: "boolean",
27966
+ summary: "Also write under .agents/skills"
27967
+ }
27968
+ ],
27969
+ examples: ["pai lessons promote my-lesson --project ~/code/app --agents"],
27970
+ run: (ctx) => {
27971
+ const paths = wrap(() => promoteLesson(ctx.args.ID, {
27972
+ projectDir: ctx.opts.project ?? process.cwd(),
27973
+ force: ctx.opts.force === true,
27974
+ agentsDir: ctx.opts.agents === true
27975
+ }));
27976
+ for (const p of paths) console.log(`promoted ${p}`);
27977
+ return 0;
27978
+ }
27979
+ },
27980
+ {
27981
+ name: "supersede",
27982
+ summary: "Replace an approved lesson by another approved one",
27983
+ description: "Marks ID superseded; the replacement given with --by must exist and be approved.",
27984
+ args: [ID],
27985
+ options: [{
27986
+ name: "by",
27987
+ type: "string",
27988
+ value: "ID2",
27989
+ summary: "The approved lesson that replaces it"
27990
+ }],
27991
+ examples: ["pai lessons supersede old-lesson --by new-lesson"],
27992
+ run: (ctx) => {
27993
+ const by = ctx.opts.by;
27994
+ if (!by) throw new CliError("--by is required");
27995
+ const l = wrap(() => setState(ctx.args.ID, "superseded", { supersededBy: by }));
27996
+ console.log(`${l.id}: superseded by ${by}`);
27997
+ return 0;
27998
+ }
27999
+ },
28000
+ {
28001
+ name: "propose",
28002
+ summary: "Add candidates from a transcript",
28003
+ description: "Finds user messages that correct the assistant (no model call) and adds each as a candidate to review.",
28004
+ args: [{
28005
+ name: "TRANSCRIPT",
28006
+ summary: "Transcript file (default: newest of this directory)"
28007
+ }],
28008
+ examples: ["pai lessons propose", "pai lessons propose ./session.jsonl"],
28009
+ run: (ctx) => {
28010
+ const path = ctx.args.TRANSCRIPT ?? newestTranscriptOfCwd();
28011
+ if (!path) throw new CliError("no transcript found for this directory");
28012
+ const added = wrap(() => proposeFromTranscript(path).map(addLesson));
28013
+ if (added.length === 0) console.log("No corrections found.");
28014
+ for (const l of added) console.log(`added ${l.id} [candidate] ${l.title}`);
28015
+ return 0;
28016
+ }
28017
+ }
28018
+ ]
28019
+ };
28020
+
28021
+ //#endregion
28022
+ //#region src/harness/claude-code.ts
28023
+ /**
28024
+ * claude-code.ts — adapter over Claude Code's JSONL transcripts:
28025
+ * `<claude home>/projects/<encoded cwd>/<session id>.jsonl`. The encoded-cwd
28026
+ * spelling is the one PAI already uses everywhere (encodeDir, src/cli/lib/utils.ts).
28027
+ */
28028
+ /** Text of a tool_result's content: a string or its text blocks joined. */
28029
+ function resultText(c) {
28030
+ if (typeof c === "string") return c;
28031
+ if (Array.isArray(c)) return c.map((b) => b && typeof b === "object" && typeof b.text === "string" ? b.text : "").join("\n");
28032
+ return "";
28033
+ }
28034
+ /** Exit code a Bash result shows ("Exit code N" header or an exitCode field); else undefined. */
28035
+ function exitCodeOf(content) {
28036
+ if (typeof content === "string") {
28037
+ const m = /^Exit code (\d+)/m.exec(content);
28038
+ return m ? Number(m[1]) : void 0;
28039
+ }
28040
+ if (content && typeof content === "object" && !Array.isArray(content)) {
28041
+ const v = content.exitCode;
28042
+ if (typeof v === "number") return v;
28043
+ }
28044
+ }
28045
+ /** Events of one parsed transcript line; nothing for lines that carry no event. */
28046
+ function eventsOfLine(harness, sessionId, obj) {
28047
+ const ts = obj.timestamp ?? "";
28048
+ const content = obj.message?.content;
28049
+ const out = [];
28050
+ const blocks = Array.isArray(content) ? content : [];
28051
+ if (obj.type === "user") {
28052
+ for (const b of blocks) if (b.type === "tool_result") out.push({
28053
+ harness,
28054
+ sessionId,
28055
+ ts,
28056
+ kind: "tool_result",
28057
+ text: resultText(b.content),
28058
+ exitCode: exitCodeOf(b.content),
28059
+ source: "transcript",
28060
+ fidelity: "observed"
28061
+ });
28062
+ if (!obj.isMeta) {
28063
+ const text = typeof content === "string" ? content : blocks.filter((b) => b.type === "text").map((b) => b.text ?? "").join("\n");
28064
+ if (text) out.push({
28065
+ harness,
28066
+ sessionId,
28067
+ ts,
28068
+ kind: "user",
28069
+ text,
28070
+ source: "transcript",
28071
+ fidelity: "observed"
28072
+ });
28073
+ }
28074
+ return out;
28075
+ }
28076
+ if (obj.type === "assistant" && Array.isArray(content)) {
28077
+ for (const b of content) if (b.type === "text" && b.text) out.push({
28078
+ harness,
28079
+ sessionId,
28080
+ ts,
28081
+ kind: "assistant",
28082
+ text: b.text,
28083
+ source: "transcript",
28084
+ fidelity: "observed"
28085
+ });
28086
+ else if (b.type === "tool_use" && b.name) out.push({
28087
+ harness,
28088
+ sessionId,
28089
+ ts,
28090
+ kind: "tool_call",
28091
+ tool: b.name,
28092
+ input: b.input,
28093
+ source: "transcript",
28094
+ fidelity: "observed"
28095
+ });
28096
+ }
28097
+ return out;
28098
+ }
28099
+ /** Session files under a project dir: top-level `<id>.jsonl` only — the
28100
+ * `sessions/` archive holds hardlinks of the same files, and subagent logs
28101
+ * live in their own subdirs. */
28102
+ function sessionFiles(dir) {
28103
+ try {
28104
+ return readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
28105
+ } catch {
28106
+ return [];
28107
+ }
28108
+ }
28109
+ function toSession$1(harness, file, dir) {
28110
+ const id = file.slice(0, -6);
28111
+ let startedAt;
28112
+ let updatedAt;
28113
+ let cwd;
28114
+ try {
28115
+ for (const raw of readFileSync(join(dir, file), "utf8").split("\n")) {
28116
+ if (!raw.trim()) continue;
28117
+ let obj;
28118
+ try {
28119
+ obj = JSON.parse(raw);
28120
+ } catch {
28121
+ continue;
28122
+ }
28123
+ if (typeof obj.cwd === "string" && obj.cwd) cwd ??= obj.cwd;
28124
+ if (typeof obj.timestamp === "string" && obj.timestamp) {
28125
+ startedAt ??= obj.timestamp;
28126
+ updatedAt = obj.timestamp;
28127
+ }
28128
+ }
28129
+ } catch {}
28130
+ const mtime = new Date(statSync(join(dir, file)).mtimeMs).toISOString();
28131
+ return {
28132
+ harness,
28133
+ id,
28134
+ cwd,
28135
+ path: join(dir, file),
28136
+ startedAt,
28137
+ updatedAt: updatedAt ?? mtime
28138
+ };
28139
+ }
28140
+ const claudeCodeAdapter = {
28141
+ name: "claude-code",
28142
+ detect() {
28143
+ return existsSync(claudeProjectsDir());
28144
+ },
28145
+ listSessions(opts = {}) {
28146
+ const projectsDir = claudeProjectsDir();
28147
+ if (!existsSync(projectsDir)) return [];
28148
+ const dirs = opts.cwd ? [encodeDir(opts.cwd)] : readdirSafe(projectsDir);
28149
+ const out = [];
28150
+ for (const d of dirs) {
28151
+ const dir = join(projectsDir, d);
28152
+ if (!existsSync(dir)) continue;
28153
+ for (const f of sessionFiles(dir)) out.push(toSession$1(this.name, f, dir));
28154
+ }
28155
+ const since = opts.sinceMs !== void 0 ? Date.now() - opts.sinceMs : void 0;
28156
+ return out.filter((s) => since === void 0 || Date.parse(s.updatedAt) >= since).sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt));
28157
+ },
28158
+ *readEvents(session) {
28159
+ let text;
28160
+ try {
28161
+ text = readFileSync(session.path, "utf8");
28162
+ } catch {
28163
+ return;
28164
+ }
28165
+ for (const raw of text.split("\n")) {
28166
+ if (!raw.trim()) continue;
28167
+ let obj;
28168
+ try {
28169
+ obj = JSON.parse(raw);
28170
+ } catch {
28171
+ continue;
28172
+ }
28173
+ yield* eventsOfLine(this.name, session.id, obj);
28174
+ }
28175
+ }
28176
+ };
28177
+ function readdirSafe(dir) {
28178
+ try {
28179
+ return readdirSync(dir);
28180
+ } catch {
28181
+ return [];
28182
+ }
28183
+ }
28184
+
28185
+ //#endregion
28186
+ //#region src/harness/codex.ts
28187
+ /**
28188
+ * codex.ts — adapter over Codex CLI session logs:
28189
+ * `<codex home>/sessions/**\/rollout-*.jsonl`. Implemented against the
28190
+ * documented rollout shape (JSON lines; `type:"message"` with role
28191
+ * user/assistant, `function_call` / `function_call_output`); unknown entry
28192
+ * types and malformed lines are skipped rather than failing the session.
28193
+ */
28194
+ /** Codex CLI's config dir: CODEX_HOME if set, else ~/.codex. */
28195
+ function codexHomeDir() {
28196
+ return process.env.CODEX_HOME || join(homedir(), ".codex");
28197
+ }
28198
+ /** Plain text of a message payload's content: a string, or text parts joined. */
28199
+ function contentText(c) {
28200
+ if (typeof c === "string") return c;
28201
+ if (Array.isArray(c)) return c.map((p) => {
28202
+ if (typeof p === "string") return p;
28203
+ if (p && typeof p === "object") {
28204
+ const o = p;
28205
+ if (typeof o.text === "string") return o.text;
28206
+ }
28207
+ return "";
28208
+ }).filter(Boolean).join("\n");
28209
+ return "";
28210
+ }
28211
+ /** function_call arguments: parsed as JSON when they are a JSON string. */
28212
+ function parseArgs(a) {
28213
+ if (typeof a !== "string") return a;
28214
+ try {
28215
+ return JSON.parse(a);
28216
+ } catch {
28217
+ return a;
28218
+ }
28219
+ }
28220
+ /** Events of one parsed rollout entry; nothing for entries that carry none. */
28221
+ function eventsOfEntry(sessionId, e) {
28222
+ const ts = e.timestamp ?? "";
28223
+ const p = e.payload;
28224
+ if (!p || !p.type) return [];
28225
+ if (p.type === "message" && (p.role === "user" || p.role === "assistant")) {
28226
+ const text = contentText(p.content);
28227
+ if (!text) return [];
28228
+ return [{
28229
+ harness: NAME,
28230
+ sessionId,
28231
+ ts,
28232
+ kind: p.role,
28233
+ text,
28234
+ source: "transcript",
28235
+ fidelity: "observed"
28236
+ }];
28237
+ }
28238
+ if (p.type === "function_call" && p.name) return [{
28239
+ harness: NAME,
28240
+ sessionId,
28241
+ ts,
28242
+ kind: "tool_call",
28243
+ tool: p.name,
28244
+ input: parseArgs(p.arguments),
28245
+ source: "transcript",
28246
+ fidelity: "observed"
28247
+ }];
28248
+ if (p.type === "function_call_output") return [{
28249
+ harness: NAME,
28250
+ sessionId,
28251
+ ts,
28252
+ kind: "tool_result",
28253
+ text: contentText(p.output),
28254
+ source: "transcript",
28255
+ fidelity: "observed"
28256
+ }];
28257
+ return [];
28258
+ }
28259
+ const NAME = "codex";
28260
+ /** rollout-*.jsonl under `dir`, recursing into subdirectories. */
28261
+ function rolloutFiles(dir) {
28262
+ const out = [];
28263
+ let entries;
28264
+ try {
28265
+ entries = readdirSync(dir, { withFileTypes: true });
28266
+ } catch {
28267
+ return out;
28268
+ }
28269
+ for (const ent of entries) {
28270
+ const p = join(dir, ent.name);
28271
+ if (ent.isDirectory()) out.push(...rolloutFiles(p));
28272
+ else if (ent.name.startsWith("rollout-") && ent.name.endsWith(".jsonl")) out.push(p);
28273
+ }
28274
+ return out;
28275
+ }
28276
+ function toSession(file) {
28277
+ const id = file.slice(0, -6).split("/").pop() ?? file;
28278
+ let startedAt;
28279
+ let updatedAt;
28280
+ try {
28281
+ for (const raw of readFileSync(file, "utf8").split("\n")) {
28282
+ if (!raw.trim()) continue;
28283
+ let e;
28284
+ try {
28285
+ e = JSON.parse(raw);
28286
+ } catch {
28287
+ continue;
28288
+ }
28289
+ if (typeof e.timestamp === "string" && e.timestamp) {
28290
+ startedAt ??= e.timestamp;
28291
+ updatedAt = e.timestamp;
28292
+ }
28293
+ }
28294
+ } catch {}
28295
+ const mtime = new Date(statSync(file).mtimeMs).toISOString();
28296
+ return {
28297
+ harness: NAME,
28298
+ id,
28299
+ path: file,
28300
+ startedAt,
28301
+ updatedAt: updatedAt ?? mtime
28302
+ };
28303
+ }
28304
+ const codexAdapter = {
28305
+ name: NAME,
28306
+ detect() {
28307
+ return existsSync(join(codexHomeDir(), "sessions"));
28308
+ },
28309
+ listSessions(opts = {}) {
28310
+ const root = join(codexHomeDir(), "sessions");
28311
+ if (!existsSync(root)) return [];
28312
+ const out = rolloutFiles(root).map(toSession);
28313
+ const since = opts.sinceMs !== void 0 ? Date.now() - opts.sinceMs : void 0;
28314
+ return out.filter((s) => since === void 0 || Date.parse(s.updatedAt) >= since).sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt));
28315
+ },
28316
+ *readEvents(session) {
28317
+ let text;
28318
+ try {
28319
+ text = readFileSync(session.path, "utf8");
28320
+ } catch {
28321
+ return;
28322
+ }
28323
+ for (const raw of text.split("\n")) {
28324
+ if (!raw.trim()) continue;
28325
+ let e;
28326
+ try {
28327
+ e = JSON.parse(raw);
28328
+ } catch {
28329
+ continue;
28330
+ }
28331
+ yield* eventsOfEntry(session.id, e);
28332
+ }
28333
+ }
28334
+ };
28335
+
28336
+ //#endregion
28337
+ //#region src/harness/index.ts
28338
+ /**
28339
+ * index.ts — the adapter registry and the enabled set. Implementations are
28340
+ * listed here; which of them run is config (`harness.adapters` in the main
28341
+ * config, default ["claude-code"]), never a hardcoded enable list.
28342
+ */
28343
+ /** Every implementation PAI knows, in display order. */
28344
+ const ADAPTERS = [claudeCodeAdapter, codexAdapter];
28345
+ /** Names enabled by config: `harness.adapters`, default ["claude-code"]. */
28346
+ function enabledHarnessNames(path = paiConfigFilePath()) {
28347
+ const list = effectiveMainConfig(path).harness?.adapters;
28348
+ if (!Array.isArray(list)) return ["claude-code"];
28349
+ return list.filter((n) => typeof n === "string");
28350
+ }
28351
+
28352
+ //#endregion
28353
+ //#region src/cli/commands/harness.ts
28354
+ /** `pai harness ...`: sessions PAI can read from agent harnesses (read-only). */
28355
+ const fmt = (iso) => Number.isFinite(Date.parse(iso)) ? new Date(iso).toISOString().slice(0, 16).replace("T", " ") : "—";
28356
+ function sessionsFor(harness, cwd) {
28357
+ const a = ADAPTERS.find((x) => x.name === harness);
28358
+ if (!a || !a.detect()) return [];
28359
+ return a.listSessions(cwd ? { cwd } : void 0);
28360
+ }
28361
+ function listCmd$1(opts) {
28362
+ const enabled = new Set(enabledHarnessNames());
28363
+ const rows = ADAPTERS.map((a) => {
28364
+ const detected = a.detect();
28365
+ return [
28366
+ a.name,
28367
+ detected ? ok$1("yes") : dim$1("no"),
28368
+ enabled.has(a.name) ? ok$1("yes") : dim$1("no"),
28369
+ detected ? String(sessionsFor(a.name).length) : dim$1("—")
28370
+ ];
28371
+ });
28372
+ if (opts.json) {
28373
+ console.log(JSON.stringify(ADAPTERS.map((a) => ({
28374
+ adapter: a.name,
28375
+ detected: a.detect(),
28376
+ enabled: enabled.has(a.name),
28377
+ sessions: a.detect() ? sessionsFor(a.name).length : void 0
28378
+ })), null, 2));
28379
+ return 0;
28380
+ }
28381
+ console.log(renderTable$1([
28382
+ "adapter",
28383
+ "detected",
28384
+ "enabled",
28385
+ "sessions"
28386
+ ], rows));
28387
+ console.log(dim$1(` enabled set comes from config key harness.adapters (pai config get harness.adapters)`));
28388
+ return 0;
28389
+ }
28390
+ function sessionsCmd(opts) {
28391
+ const names = opts.harness ? [opts.harness] : enabledHarnessNames();
28392
+ const out = [];
28393
+ for (const name of names) out.push(...sessionsFor(name, opts.cwd));
28394
+ out.sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt));
28395
+ if (opts.json) {
28396
+ console.log(JSON.stringify(out, null, 2));
28397
+ return 0;
28398
+ }
28399
+ if (!out.length) {
28400
+ console.log(dim$1(` no sessions${opts.cwd ? ` under ${opts.cwd}` : ""} (${names.join(", ")})`));
28401
+ return 0;
28402
+ }
28403
+ console.log(renderTable$1([
28404
+ "harness",
28405
+ "id",
28406
+ "updated",
28407
+ "cwd"
28408
+ ], out.map((s) => [
28409
+ s.harness,
28410
+ s.id,
28411
+ fmt(s.updatedAt),
28412
+ s.cwd ? maskSecrets(shortenPath(s.cwd, 48)) : dim$1("—")
28413
+ ])));
28414
+ return 0;
28415
+ }
28416
+ const harnessCommand = {
28417
+ name: "harness",
28418
+ summary: "Sessions PAI can read from agent harnesses (Claude Code, Codex, ...)",
28419
+ 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.",
28420
+ examples: [
28421
+ "pai harness list",
28422
+ "pai harness sessions",
28423
+ "pai harness sessions --harness codex --cwd ~/proj"
28424
+ ],
28425
+ seeAlso: ["pai config get harness.adapters"],
28426
+ subcommands: [{
28427
+ name: "list",
28428
+ summary: "Each adapter: detected, enabled, session count",
28429
+ description: "One row per adapter PAI knows. Only enabled adapters whose store exists are ever read.",
28430
+ options: [{
28431
+ name: "json",
28432
+ type: "boolean",
28433
+ default: false,
28434
+ summary: "Print JSON instead of a table"
28435
+ }],
28436
+ examples: ["pai harness list", "pai harness list --json"],
28437
+ run: (ctx) => listCmd$1({ json: ctx.opts.json === true })
28438
+ }, {
28439
+ name: "sessions",
28440
+ summary: "List sessions, newest first",
28441
+ description: "Sessions of the enabled adapters (or one harness with --harness), optionally limited to one working directory.",
28442
+ options: [
28443
+ {
28444
+ name: "harness",
28445
+ type: "string",
28446
+ value: "NAME",
28447
+ summary: "Only this adapter (default: the enabled set)"
28448
+ },
28449
+ {
28450
+ name: "cwd",
28451
+ type: "string",
28452
+ value: "DIR",
28453
+ summary: "Only sessions of this working directory"
28454
+ },
28455
+ {
28456
+ name: "json",
28457
+ type: "boolean",
28458
+ default: false,
28459
+ summary: "Print JSON instead of a table"
28460
+ }
28461
+ ],
28462
+ examples: [
28463
+ "pai harness sessions",
28464
+ "pai harness sessions --harness codex",
28465
+ "pai harness sessions --cwd ~/proj"
28466
+ ],
28467
+ run: (ctx) => {
28468
+ const cwd = ctx.opts.cwd;
28469
+ if (cwd && !existsSync(cwd)) {
28470
+ console.log(dim$1(` no such directory: ${cwd}`));
28471
+ return 0;
28472
+ }
28473
+ return sessionsCmd({
28474
+ harness: ctx.opts.harness,
28475
+ cwd,
28476
+ json: ctx.opts.json === true
28477
+ });
28478
+ }
28479
+ }]
28480
+ };
28481
+
28482
+ //#endregion
28483
+ //#region src/hooks/ts/lib/worker-git-guard.ts
28484
+ /** Splits on &&, ||, ;, | and newlines outside quotes. */
28485
+ function splitSegments(command) {
28486
+ const out = [];
28487
+ let cur = "";
28488
+ let quote = "";
28489
+ for (let i = 0; i < command.length; i++) {
28490
+ const c = command[i];
28491
+ if (quote) {
28492
+ if (c === quote) quote = "";
28493
+ cur += c;
28494
+ } else if (c === "\"" || c === "'") {
28495
+ quote = c;
28496
+ cur += c;
28497
+ } else if (c === "&" || c === "|" || c === ";" || c === "\n") {
28498
+ out.push(cur);
28499
+ cur = "";
28500
+ } else cur += c;
28501
+ }
28502
+ out.push(cur);
28503
+ return out.map((s) => s.trim()).filter(Boolean);
28504
+ }
28505
+
28506
+ //#endregion
28507
+ //#region src/hooks/ts/lib/rules/eval.ts
28508
+ /**
28509
+ * eval.ts — run rules against a tool call.
28510
+ *
28511
+ * Order is fixed: the first block hit wins, else the first confirm hit, else
28512
+ * the first warn hit. Text inside quotes is not a command, so rules test the
28513
+ * content with quoted strings removed; rules marked `quoted: true` also test
28514
+ * it with only the quote marks removed (as detectAttack did before);
28515
+ * confirm rules additionally test per-segment, so a quoted `git push -f` in a
28516
+ * commit message stays inert while a quoted `+main` refspec still counts.
28517
+ */
28518
+ /** Quoted strings removed: text inside a commit message or echo is not a command. */
28519
+ function dropQuoted(segment) {
28520
+ return segment.replace(/"(?:\\.|[^"\\])*"|'[^']*'/g, "\"\"");
28521
+ }
28522
+ /** A quoted +refspec is still a forced push, so push segments also test with only the quote marks removed. */
28523
+ function gitPatternHit(pattern, segment) {
28524
+ const bare = dropQuoted(segment);
28525
+ if (pattern.test(bare)) return true;
28526
+ return /\bgit\b.*\spush\b/.test(bare) && pattern.test(segment.replace(/["']/g, ""));
28527
+ }
28528
+ const regexCache = /* @__PURE__ */ new Map();
28529
+ function ruleRegex(rule) {
28530
+ if (!rule.match) return null;
28531
+ const key = `${rule.match} ${rule.flags ?? ""}`;
28532
+ const cached = regexCache.get(key);
28533
+ if (cached) return cached;
28534
+ try {
28535
+ const re = new RegExp(rule.match, rule.flags ?? "");
28536
+ regexCache.set(key, re);
28537
+ return re;
28538
+ } catch {
28539
+ return null;
28540
+ }
28541
+ }
28542
+ function toolMatches(rule, toolName) {
28543
+ if (!rule.tool) return toolName === "Bash";
28544
+ try {
28545
+ return new RegExp(rule.tool).test(toolName);
28546
+ } catch {
28547
+ return false;
28548
+ }
28549
+ }
28550
+ /** First block hit, else first confirm hit, else first warn hit; null when none. */
28551
+ function evaluateRules(rules, toolName, content) {
28552
+ const unquoted = content.replace(/["']/g, "");
28553
+ const bare = dropQuoted(content);
28554
+ const segments = splitSegments(content);
28555
+ for (const severity of [
28556
+ "block",
28557
+ "confirm",
28558
+ "warn"
28559
+ ]) for (const rule of rules) {
28560
+ if (rule.severity !== severity || !toolMatches(rule, toolName)) continue;
28561
+ const re = ruleRegex(rule);
28562
+ if (!re) continue;
28563
+ if (severity === "confirm" ? segments.some((seg) => gitPatternHit(re, seg)) : re.test(bare) || rule.quoted === true && (re.test(content) || re.test(unquoted))) return { rule };
28564
+ }
28565
+ return null;
28566
+ }
28567
+
28568
+ //#endregion
28569
+ //#region src/hooks/ts/lib/rules/lint.ts
28570
+ /**
28571
+ * lint.ts — run each rule's own fixtures.
28572
+ *
28573
+ * Every `tests.match` must hit the rule, every `tests.no_match` must not.
28574
+ * A single-step rule's fixtures are plain strings tested as Bash commands; a
28575
+ * sequence rule's are lists of step contents, checked step by step (the
28576
+ * window and ordering are the scan's business, not the fixtures'). A rule
28577
+ * with no fixtures is ok (nothing to check).
28578
+ */
28579
+ function stepRegex(step) {
28580
+ try {
28581
+ return new RegExp(step.match, step.flags ?? "");
28582
+ } catch {
28583
+ return null;
28584
+ }
28585
+ }
28586
+ /**
28587
+ * A fixture sequence hits when every step's regex matches its content and its
28588
+ * tool pin, when set. A plain string is a Bash-shaped step; an object names
28589
+ * the tool, so a rule whose step is pinned to a fetched result (WebFetch,
28590
+ * Read) can still carry a fixture that hits. A step whose source
28591
+ * backreferences an earlier step's group (`\1`) is checked with that group's
28592
+ * capture substituted in — the same resolution the transcript scan applies —
28593
+ * so a fixture like download-then-execute's "same file twice" checks the way
28594
+ * the scan does.
28595
+ */
28596
+ function sequenceHit(rule, steps) {
28597
+ const seqSteps = rule.sequence?.steps ?? [];
28598
+ if (steps.length !== seqSteps.length) return false;
28599
+ const prior = [];
28600
+ for (let i = 0; i < seqSteps.length; i++) {
28601
+ const step = seqSteps[i];
28602
+ const { tool, content } = steps[i];
28603
+ if (step.tool && !new RegExp(step.tool).test(tool ?? "Bash")) return false;
28604
+ if (step.match.includes("\\1") && prior.length) {
28605
+ const cap = prior[0][1];
28606
+ if (!cap || !new RegExp(step.match.replace(/\\1/g, cap), step.flags ?? "").test(content)) return false;
28607
+ continue;
28608
+ }
28609
+ const re = stepRegex(step);
28610
+ if (!re) return false;
28611
+ const m = content.match(re);
28612
+ if (!m) return false;
28613
+ prior.push(m);
28614
+ }
28615
+ return true;
28616
+ }
28617
+ function asFixtureSteps(t) {
28618
+ if (!Array.isArray(t)) return null;
28619
+ return t.map((s) => typeof s === "string" ? { content: s } : {
28620
+ tool: s.tool,
28621
+ content: s.content
28622
+ });
28623
+ }
28624
+ function lintRules(rules) {
28625
+ return rules.map((rule) => {
28626
+ const failures = [];
28627
+ const show = (t) => Array.isArray(t) ? t.map((s) => typeof s === "string" ? s : `${s.tool ?? "Bash"}: ${s.content}`).join(" | ") : t;
28628
+ if (rule.sequence) {
28629
+ for (const t of rule.tests?.match ?? []) {
28630
+ const steps = asFixtureSteps(t);
28631
+ if (!steps || !sequenceHit(rule, steps)) failures.push(`tests.match did not hit: ${show(t)}`);
28632
+ }
28633
+ for (const t of rule.tests?.no_match ?? []) {
28634
+ const steps = asFixtureSteps(t);
28635
+ if (steps && sequenceHit(rule, steps)) failures.push(`tests.no_match hit: ${show(t)}`);
28636
+ }
28637
+ return {
28638
+ id: rule.id,
28639
+ ok: failures.length === 0,
28640
+ failures
28641
+ };
28642
+ }
28643
+ for (const text of rule.tests?.match ?? []) {
28644
+ const hit = evaluateRules([rule], "Bash", text);
28645
+ if (!hit || hit.rule.id !== rule.id) failures.push(`tests.match did not hit: ${text}`);
28646
+ }
28647
+ for (const text of rule.tests?.no_match ?? []) {
28648
+ const hit = evaluateRules([rule], "Bash", text);
28649
+ if (hit && hit.rule.id === rule.id) failures.push(`tests.no_match hit: ${text}`);
28650
+ }
28651
+ return {
28652
+ id: rule.id,
28653
+ ok: failures.length === 0,
28654
+ failures
28655
+ };
28656
+ });
28657
+ }
28658
+
28659
+ //#endregion
28660
+ //#region src/cli/commands/rules.ts
28661
+ function listCmd(json) {
28662
+ const { rules, errors } = listRules(userRuleDirs(process.cwd()));
28663
+ if (json) {
28664
+ console.log(JSON.stringify({
28665
+ rules,
28666
+ errors
28667
+ }, null, 2));
28668
+ return 0;
28669
+ }
28670
+ console.log(renderTable$1([
28671
+ "id",
28672
+ "severity",
28673
+ "source",
28674
+ "enabled"
28675
+ ], rules.map((r) => [
28676
+ r.id,
28677
+ r.severity,
28678
+ r.source,
28679
+ r.severity === "off" ? "off" : "on"
28680
+ ])));
28681
+ for (const e of errors) console.log(err(`load error: ${e}`));
28682
+ return 0;
28683
+ }
28684
+ function lintCmd(json) {
28685
+ const { rules, errors } = listRules(userRuleDirs(process.cwd()));
28686
+ const enabled = rules.filter((r) => r.severity !== "off");
28687
+ const lints = lintRules(enabled);
28688
+ const failed = lints.filter((l) => !l.ok);
28689
+ if (json) {
28690
+ console.log(JSON.stringify({
28691
+ lints,
28692
+ errors
28693
+ }, null, 2));
28694
+ return failed.length || errors.length ? 1 : 0;
28695
+ }
28696
+ for (const l of lints) {
28697
+ if (l.ok) continue;
28698
+ console.log(err(`✖ ${l.id}`));
28699
+ for (const f of l.failures) console.log(` ${f}`);
28700
+ }
28701
+ for (const e of errors) console.log(err(`load error: ${e}`));
28702
+ if (!failed.length && !errors.length) {
28703
+ console.log(ok$1(`✔ ${enabled.length} rules, fixtures pass`));
28704
+ return 0;
28705
+ }
28706
+ return 1;
28707
+ }
28708
+ const rulesCommand = {
28709
+ name: "rules",
28710
+ summary: "Security rules: list them, lint their fixtures",
28711
+ 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.",
28712
+ examples: ["pai rules list", "pai rules lint"],
28713
+ seeAlso: ["pai audit tokens hooks"],
28714
+ subcommands: [{
28715
+ name: "list",
28716
+ summary: "Every rule with severity, source and enabled state",
28717
+ description: "One row per rule id; a disabled (severity: off) rule shows as off.",
28718
+ options: [{
28719
+ name: "json",
28720
+ type: "boolean",
28721
+ default: false,
28722
+ summary: "Machine-readable JSON output"
28723
+ }],
28724
+ examples: ["pai rules list", "pai rules list --json"],
28725
+ run: (ctx) => listCmd(ctx.opts.json === true)
28726
+ }, {
28727
+ name: "lint",
28728
+ summary: "Run every rule's fixtures; exit 1 on a failure or load error",
28729
+ description: "Each rule's tests.match strings must hit and tests.no_match strings must not. Load errors count as failures.",
28730
+ options: [{
28731
+ name: "json",
28732
+ type: "boolean",
28733
+ default: false,
28734
+ summary: "Machine-readable JSON output"
28735
+ }],
28736
+ examples: ["pai rules lint", "pai rules lint --json"],
28737
+ run: (ctx) => lintCmd(ctx.opts.json === true)
28738
+ }]
28739
+ };
28740
+
28741
+ //#endregion
28742
+ //#region src/hooks/ts/lib/transcript-text.ts
28743
+ /**
28744
+ * transcript-text.ts — small, pure pieces of transcript parsing that are
28745
+ * worth testing in isolation.
28746
+ *
28747
+ * Split out of context-compression-hook.ts (a hook entrypoint that calls
28748
+ * `main()` and `process.exit()` at import time, which makes it unsafe to
28749
+ * import directly from a test) so the two bug fixes that live here can be
28750
+ * covered by a real regression test rather than only a manual before/after
28751
+ * digest.
28752
+ */
28753
+ /**
28754
+ * Turn Claude content (string or content block array) into plain text.
28755
+ *
28756
+ * BUG (found in ~1 in 4 injected digests on this machine): a "user" turn is
28757
+ * not always something the user typed — a tool_result is also delivered as a
28758
+ * user-role message, and its `content` block looks like
28759
+ * `{ type: "tool_result", content: [...] }` where the inner `content` is
28760
+ * itself an array of blocks, not a string. The old code fell back to
28761
+ * `String(c.content)` for anything without a `.text` field, and `String()`
28762
+ * on an array of objects produces literal `"[object Object],[object
28763
+ * Object]"` — which is exactly what showed up under "Recent user requests".
28764
+ * Only ever take the `.text` of a `type: "text"` block; everything else
28765
+ * (tool_use, tool_result, image, and any nested content) is ignored rather
28766
+ * than stringified.
28767
+ */
28768
+ function contentToText(content) {
28769
+ if (typeof content === "string") return content;
28770
+ if (Array.isArray(content)) return content.map((c) => {
28771
+ if (typeof c === "string") return c;
28772
+ if (c && typeof c === "object" && c.type === "text" && typeof c.text === "string") return c.text;
28773
+ return "";
28774
+ }).join(" ").trim();
28775
+ return "";
28776
+ }
28777
+
28778
+ //#endregion
28779
+ //#region src/session/handoff.ts
28780
+ /**
28781
+ * handoff.ts — a portable, bounded, redacted handoff brief for one session.
28782
+ *
28783
+ * `pai handoff export` scrapes a Claude Code transcript into a short Markdown
28784
+ * brief a successor session can continue from: what was asked, what changed,
28785
+ * what ran, and where it stopped. Unlike the model-written context handover
28786
+ * this is mechanical — no model call, no prose to hallucinate — and it is
28787
+ * redacted (secrets replaced by [REDACTED]) before it is bounded, so a brief
28788
+ * can be pasted, mailed or committed without leaking a token.
28789
+ *
28790
+ * The brief is assembled as sections, masked, then cut to `maxBytes` from the
28791
+ * oldest end (Recent commands first, then Last steps) with a marker line, so
28792
+ * the parts a successor needs most (Session, requests, files) survive first.
28793
+ */
28794
+ /** Default brief size: comfortably under what a successor can read in one turn. */
28795
+ const HANDOFF_MAX_BYTES = 49152;
28796
+ /** Most distinct files listed under "Files changed". */
28797
+ const MAX_FILES = 50;
28798
+ /** Most Bash commands listed under "Recent commands". */
28799
+ const MAX_COMMANDS = 20;
28800
+ /** Most assistant text messages listed under "Last steps". */
28801
+ const MAX_STEPS = 10;
28802
+ /** Each "Last steps" entry is truncated to this many characters. */
28803
+ const STEP_CHARS = 400;
28804
+ /** Tools whose input carries a changed file. */
28805
+ const FILE_TOOLS = new Set([
28806
+ "Edit",
28807
+ "Write",
28808
+ "MultiEdit"
28809
+ ]);
28810
+ /** The last assistant text message of a transcript, or null when there is none. */
28811
+ function lastAssistantText(entries) {
28812
+ for (let i = entries.length - 1; i >= 0; i--) {
28813
+ const entry = entries[i];
28814
+ if (entry.type !== "assistant" || !Array.isArray(entry.message?.content)) continue;
28815
+ const text = entry.message.content.filter((p) => p?.type === "text" && typeof p.text === "string").map((p) => p.text).join("\n").trim();
28816
+ if (text) return text;
28817
+ }
28818
+ return null;
28819
+ }
28820
+ /** First user text message that looks like something typed, not an injected block. */
28821
+ function firstUserText(entries) {
28822
+ for (const entry of entries) {
28823
+ if (entry.type !== "user") continue;
28824
+ const content = entry.message?.content;
28825
+ const text = typeof content === "string" ? content : contentToText(content);
28826
+ if (text.trim()) return text.trim();
28827
+ }
28828
+ return null;
28829
+ }
28830
+ /** Last user text message — the request the session was working on. */
28831
+ function lastUserText(entries) {
28832
+ for (let i = entries.length - 1; i >= 0; i--) {
28833
+ const entry = entries[i];
28834
+ if (entry.type !== "user") continue;
28835
+ const content = entry.message?.content;
28836
+ const text = typeof content === "string" ? content : contentToText(content);
28837
+ if (text.trim()) return text.trim();
28838
+ }
28839
+ return null;
28840
+ }
28841
+ /**
28842
+ * Exit code of a Bash tool_result, when the transcript records one.
28843
+ * Claude Code serialises Bash results as plain text; an interrupted command
28844
+ * reads "Exit code N", and a JSON payload carries `interrupted`/`success`.
28845
+ */
28846
+ function bashExitCode(result) {
28847
+ if (result.is_error === true) return null;
28848
+ const text = contentToText(result.content);
28849
+ const m = text.match(/Exit code (\d+)/);
28850
+ if (m) return Number(m[1]);
28851
+ try {
28852
+ const parsed = JSON.parse(text);
28853
+ if (parsed.interrupted === true) return null;
28854
+ if (parsed.success === false) return null;
28855
+ } catch {}
28856
+ return 0;
28857
+ }
28858
+ /**
28859
+ * Build the handoff brief for one transcript: Markdown, secrets masked,
28860
+ * cut to `maxBytes` whole sections from the oldest end with a marker line.
28861
+ *
28862
+ * @param transcriptPath a Claude Code session transcript (.jsonl)
28863
+ */
28864
+ function buildHandoffBrief(transcriptPath, opts = {}) {
28865
+ const maxBytes = opts.maxBytes ?? HANDOFF_MAX_BYTES;
28866
+ const raw = readFileSync(transcriptPath, "utf8");
28867
+ const entries = [];
28868
+ for (const line of raw.split("\n")) {
28869
+ if (!line.trim()) continue;
28870
+ try {
28871
+ entries.push(JSON.parse(line));
28872
+ } catch {}
28873
+ }
28874
+ const sessionId = basename(transcriptPath).replace(/\.jsonl$/, "") || "unknown";
28875
+ const cwd = entries.find((e) => e.cwd)?.cwd ?? "";
28876
+ const first = entries.find((e) => e.timestamp)?.timestamp ?? "";
28877
+ const last = [...entries].reverse().find((e) => e.timestamp)?.timestamp ?? "";
28878
+ const files = [];
28879
+ const seen = /* @__PURE__ */ new Set();
28880
+ const commands = [];
28881
+ const steps = [];
28882
+ const results = /* @__PURE__ */ new Map();
28883
+ for (const entry of entries) {
28884
+ const content = entry.message?.content;
28885
+ if (!Array.isArray(content)) continue;
28886
+ for (const part of content) if (part?.type === "tool_result" && typeof part.tool_use_id === "string") results.set(part.tool_use_id, part);
28887
+ }
28888
+ for (const entry of entries) {
28889
+ const content = entry.message?.content;
28890
+ if (!Array.isArray(content)) continue;
28891
+ for (const part of content) if (part?.type === "tool_use" && part.name && FILE_TOOLS.has(part.name)) {
28892
+ const path = part.input?.file_path;
28893
+ if (typeof path === "string" && !seen.has(path)) {
28894
+ seen.add(path);
28895
+ files.push(path);
28896
+ }
28897
+ } else if (part?.type === "tool_use" && part.name === "Bash") {
28898
+ const command = typeof part.input?.command === "string" ? part.input.command : "";
28899
+ if (command) {
28900
+ const code = part.id ? results.get(part.id) : void 0;
28901
+ const exit = code ? bashExitCode(code) : null;
28902
+ commands.push(exit === null ? command : `${command} [exit ${exit}]`);
28903
+ }
28904
+ } else if (part?.type === "text" && typeof part.text === "string" && entry.type === "assistant") {
28905
+ const text = part.text.trim();
28906
+ if (text) steps.push(text.length > STEP_CHARS ? `${text.slice(0, STEP_CHARS)}…` : text);
28907
+ }
28908
+ }
28909
+ const sections = {
28910
+ Session: [
28911
+ `- id: ${sessionId}`,
28912
+ `- cwd: ${cwd}`,
28913
+ `- start: ${first}`,
28914
+ `- end: ${last}`
28915
+ ].join("\n"),
28916
+ "First request": firstUserText(entries) ?? "(none)",
28917
+ "Latest request": lastUserText(entries) ?? "(none)",
28918
+ "Files changed": files.slice(0, MAX_FILES).map((f) => `- ${f}`).join("\n") || "(none)",
28919
+ "Recent commands": commands.slice(-MAX_COMMANDS).map((c) => `- ${c}`).join("\n") || "(none)",
28920
+ "Last steps": steps.slice(-MAX_STEPS).map((s) => `- ${s}`).join("\n") || "(none)",
28921
+ "Where it stopped": lastAssistantText(entries) ?? "(no assistant message)"
28922
+ };
28923
+ const masked = maskSecrets(Object.entries(sections).map(([title, body]) => `## ${title}\n${body}`).join("\n\n"));
28924
+ if (Buffer.byteLength(masked, "utf8") <= maxBytes) return masked;
28925
+ const note = `\n\n[truncated to ${maxBytes} bytes]`;
28926
+ const budget = maxBytes - Buffer.byteLength(note, "utf8");
28927
+ const render = (parts) => Object.entries(parts).map(([title, body]) => `## ${title}\n${body}`).join("\n\n");
28928
+ const order = ["Recent commands", "Last steps"];
28929
+ const kept = { ...sections };
28930
+ for (const name of order) {
28931
+ delete kept[name];
28932
+ const text = render(kept);
28933
+ if (Buffer.byteLength(text, "utf8") <= budget) return text + note;
28934
+ }
28935
+ kept["Where it stopped"] = (kept["Where it stopped"] ?? "").split("\n")[0].slice(0, 200);
28936
+ const text = render(kept);
28937
+ if (Buffer.byteLength(text, "utf8") <= budget) return text + note;
28938
+ return Buffer.from(text, "utf8").subarray(0, Math.max(0, budget)).toString("utf8") + note;
28939
+ }
28940
+ /** `yyyymmdd-hhmm` in local time, for the handoff file name. */
28941
+ function stamp(d = /* @__PURE__ */ new Date()) {
28942
+ const p = (n) => String(n).padStart(2, "0");
28943
+ return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}`;
28944
+ }
28945
+ /** The handoffs directory under PAI_HOME. */
28946
+ function handoffsDir() {
28947
+ return paiHomePath("handoffs");
28948
+ }
28949
+ /**
28950
+ * Write a handoff brief into <PAI_HOME>/handoffs/<sessionId>-<yyyymmdd-hhmm>.md
28951
+ * (dir 0700, file 0600) and return its path.
28952
+ */
28953
+ function writeHandoff(brief, sessionId) {
28954
+ const dir = handoffsDir();
28955
+ mkdirSync(dir, {
28956
+ recursive: true,
28957
+ mode: 448
28958
+ });
28959
+ const path = join(dir, `${sessionId}-${stamp()}.md`);
28960
+ writeFileSync(path, brief, { mode: 384 });
28961
+ return path;
28962
+ }
28963
+ /** Newest transcript of a cwd, newest first; null when the project has none. */
28964
+ function sessionTranscripts(cwd) {
28965
+ const dir = join(claudeProjectsDir(), encodeDir(cwd));
28966
+ let names;
28967
+ try {
28968
+ names = readdirSync(dir);
28969
+ } catch {
28970
+ return [];
28971
+ }
28972
+ return names.filter((n) => n.endsWith(".jsonl")).map((n) => join(dir, n)).map((path) => ({
28973
+ path,
28974
+ mtime: safeMtime(path)
28975
+ })).filter((f) => f.mtime !== null).sort((a, b) => b.mtime - a.mtime).map((f) => f.path);
28976
+ }
28977
+ function safeMtime(path) {
28978
+ try {
28979
+ return statSync(path).mtimeMs;
28980
+ } catch {
28981
+ return null;
28982
+ }
28983
+ }
28984
+ /** Resolve a session id (or transcript path) in a cwd to its transcript path. */
28985
+ function resolveTranscript(session, cwd) {
28986
+ if (session && session.endsWith(".jsonl")) return session;
28987
+ const dir = join(claudeProjectsDir(), encodeDir(cwd));
28988
+ if (session) {
28989
+ const direct = join(dir, `${session}.jsonl`);
28990
+ try {
28991
+ statSync(direct);
28992
+ return direct;
28993
+ } catch {
28994
+ const archived = join(dir, "sessions", `${session}.jsonl`);
28995
+ try {
28996
+ statSync(archived);
28997
+ return archived;
28998
+ } catch {
28999
+ return null;
29000
+ }
29001
+ }
29002
+ }
29003
+ return sessionTranscripts(cwd)[0] ?? null;
29004
+ }
29005
+ /** The session id a transcript path carries (its file name). */
29006
+ function sessionIdOf(transcriptPath) {
29007
+ return basename(transcriptPath).replace(/\.jsonl$/, "");
29008
+ }
29009
+
29010
+ //#endregion
29011
+ //#region src/cli/commands/handoff.ts
29012
+ /** `pai handoff ...`: export a session's work as a portable, redacted brief. */
29013
+ function exportCmd(ctx) {
29014
+ const cwd = process.cwd();
29015
+ const session = ctx.args.SESSION;
29016
+ const transcript = resolveTranscript(session, cwd);
29017
+ 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");
29018
+ const sessionId = sessionIdOf(transcript);
29019
+ const path = writeHandoff(buildHandoffBrief(transcript), sessionId);
29020
+ console.log(path);
29021
+ console.log(`Continue with: claude "Read ${path} and continue. [pai-handoff from=${sessionId}]"`);
29022
+ return 0;
29023
+ }
29024
+ function showCmd(ctx) {
29025
+ const path = ctx.args.PATH;
29026
+ try {
29027
+ process.stdout.write(readFileSync(path, "utf8"));
29028
+ } catch {
29029
+ throw new CliError(`cannot read ${path}`, "pai handoff export first, then pai handoff show <PATH>");
29030
+ }
29031
+ return 0;
29032
+ }
29033
+ const handoffCommand = {
29034
+ name: "handoff",
29035
+ summary: "Carry a session's work to a new one",
29036
+ 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.",
29037
+ examples: [
29038
+ "pai handoff export",
29039
+ "pai handoff export 2026-10-22-session-id",
29040
+ "pai handoff show ~/.pai/handoffs/abc-20261022-1430.md"
29041
+ ],
29042
+ seeAlso: ["pai audit tokens session"],
29043
+ subcommands: [{
29044
+ name: "export",
29045
+ summary: "Write the brief for a session (default: newest here)",
29046
+ 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.",
29047
+ args: [{
29048
+ name: "SESSION",
29049
+ summary: "Session id or transcript path (default: newest transcript of the current directory)"
29050
+ }],
29051
+ examples: ["pai handoff export", "pai handoff export 2026-10-22-session-id"],
29052
+ run: exportCmd
29053
+ }, {
29054
+ name: "show",
29055
+ summary: "Print a handoff brief",
29056
+ description: "Prints a brief written by `pai handoff export`.",
29057
+ args: [{
29058
+ name: "PATH",
29059
+ required: true,
29060
+ summary: "Brief file"
29061
+ }],
29062
+ examples: ["pai handoff show ~/.pai/handoffs/abc-20261022-1430.md"],
29063
+ run: showCmd
27230
29064
  }]
27231
29065
  };
27232
29066
 
@@ -27290,7 +29124,8 @@ const GROUPS = [
27290
29124
  endTop,
27291
29125
  resumeTop,
27292
29126
  clearNamesCmd,
27293
- sessionsCommand
29127
+ sessionsCommand,
29128
+ handoffCommand
27294
29129
  ]
27295
29130
  },
27296
29131
  {
@@ -27328,9 +29163,10 @@ const GROUPS = [
27328
29163
  ]
27329
29164
  }))
27330
29165
  ];
27331
- GROUPS.find((g) => g.id === "memory").commands.push(memoryCommand, kgCommand, zettelCommand, observationCommand, obsidianCommand, skillCommand);
29166
+ GROUPS.find((g) => g.id === "memory").commands.push(memoryCommand, kgCommand, zettelCommand, observationCommand, obsidianCommand, skillCommand, lessonsCommand);
27332
29167
  GROUPS.find((g) => g.id === "workers").commands.push(modelsCommand);
27333
- GROUPS.find((g) => g.id === "daemon").commands.push(auditCommand, reloadCommand);
29168
+ GROUPS.find((g) => g.id === "daemon").commands.push(auditCommand, harnessCommand, reloadCommand, rulesCommand);
29169
+ GROUPS.find((g) => g.id === "memory").commands.push(gitCommand);
27334
29170
 
27335
29171
  //#endregion
27336
29172
  //#region src/cli/index.ts