@fusengine/harness 0.1.90 → 0.1.92

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 (422) hide show
  1. package/dist/adapters/claude/index.d.mts +1 -1
  2. package/dist/adapters/claude/index.mjs +1 -1
  3. package/dist/adapters/cline/index.mjs +1 -1
  4. package/dist/adapters/codex/index.d.mts +2 -2
  5. package/dist/adapters/codex/index.mjs +3 -2
  6. package/dist/adapters/cursor/index.d.mts +15 -28
  7. package/dist/adapters/cursor/index.mjs +35 -46
  8. package/dist/adapters/gemini/index.mjs +1 -1
  9. package/dist/adapters/hermes/index.d.mts +1 -1
  10. package/dist/adapters/hermes/index.mjs +1 -1
  11. package/dist/adapters/kimi/index.d.mts +1 -1
  12. package/dist/adapters/kimi/index.mjs +1 -1
  13. package/dist/{claude-5bC8TDAz.mjs → claude-Ckv2_TgP.mjs} +3 -43
  14. package/dist/cli/bin.mjs +429 -16
  15. package/dist/cli/index.mjs +1 -1
  16. package/dist/command-string-CALMTnwN.mjs +42 -0
  17. package/dist/{evaluate-C1VSI5pb.mjs → evaluate-CMiIqHeH.mjs} +261 -13
  18. package/dist/{handle-CBaOcFZq.mjs → handle-BF1dZFjY.mjs} +1194 -144
  19. package/dist/{hermes-kP6NUQlG.mjs → hermes-B9-p_3IF.mjs} +3 -2
  20. package/dist/{index-DBgnIZn9.d.mts → index-B8PG82su.d.mts} +1 -1
  21. package/dist/{index-D4C2-IIc.d.mts → index-SmKYkk2N.d.mts} +1 -1
  22. package/dist/index.d.mts +2 -2
  23. package/dist/index.mjs +2 -2
  24. package/dist/init/index.d.mts +1 -1
  25. package/dist/init/index.mjs +1 -1
  26. package/dist/{kimi-D6X7b8wF.mjs → kimi-G2wcSh5-.mjs} +3 -2
  27. package/dist/normalize-Dy8g9Ybl.mjs +397 -0
  28. package/dist/policy/index.d.mts +1 -1
  29. package/dist/policy/index.mjs +2 -2
  30. package/dist/prompt/index.d.mts +1 -1
  31. package/dist/{run-CdIlUtjv.mjs → run-DkrzC0gb.mjs} +1 -1
  32. package/dist/{run-TAXNRSpD.mjs → run-GHAMqcOn.mjs} +26 -3
  33. package/dist/runtime/index.d.mts +37 -16
  34. package/dist/runtime/index.mjs +2 -2
  35. package/dist/{types-C0-igRKr.d.mts → types-Bh0jEF3_.d.mts} +2 -0
  36. package/dist/{validate-DZFtAbLP.mjs → validate-KjZ1X9tH.mjs} +1 -1
  37. package/package.json +3 -2
  38. package/src/adapters/claude/index.ts +92 -0
  39. package/src/adapters/cline/index.ts +36 -0
  40. package/src/adapters/codex/apply-patch.ts +94 -0
  41. package/src/adapters/codex/index.ts +84 -0
  42. package/src/adapters/codex/permission-mode.ts +16 -0
  43. package/src/adapters/cursor/context-budget.ts +144 -0
  44. package/src/adapters/cursor/context-limit.ts +115 -0
  45. package/src/adapters/cursor/context.ts +65 -0
  46. package/src/adapters/cursor/events.ts +38 -0
  47. package/src/adapters/cursor/index.ts +43 -0
  48. package/src/adapters/cursor/interfaces/context-budget.ts +25 -0
  49. package/src/adapters/cursor/interfaces/types.ts +88 -0
  50. package/src/adapters/cursor/native-response.ts +39 -0
  51. package/src/adapters/cursor/native-schemas.ts +161 -0
  52. package/src/adapters/cursor/normalize.ts +188 -0
  53. package/src/adapters/cursor/plugin-root.ts +103 -0
  54. package/src/adapters/cursor/respond.ts +170 -0
  55. package/src/adapters/gemini/index.ts +36 -0
  56. package/src/adapters/hermes/index.ts +55 -0
  57. package/src/adapters/hermes/interfaces/types.ts +26 -0
  58. package/src/adapters/kimi/index.ts +96 -0
  59. package/src/adapters/kimi/interfaces/types.ts +55 -0
  60. package/src/cache/compact.ts +55 -0
  61. package/src/cache/index.ts +5 -0
  62. package/src/cache/io.ts +38 -0
  63. package/src/cache/mcp-response.ts +39 -0
  64. package/src/cache/mcp-store.ts +70 -0
  65. package/src/cache/store.ts +93 -0
  66. package/src/changelog/fetch.ts +79 -0
  67. package/src/cli/bin.ts +100 -0
  68. package/src/cli/cursor-event-scanner.ts +175 -0
  69. package/src/cli/cursor-stdin-reader.ts +65 -0
  70. package/src/cli/doctor.ts +96 -0
  71. package/src/cli/hook-io.ts +130 -0
  72. package/src/cli/hook-sound.ts +49 -0
  73. package/src/cli/index.ts +1 -0
  74. package/src/cli/json-primitive-scanner.ts +83 -0
  75. package/src/cli/run.ts +36 -0
  76. package/src/cli/scope.ts +31 -0
  77. package/src/codex-rules/build.ts +63 -0
  78. package/src/codex-rules/index.ts +8 -0
  79. package/src/codex-rules/mapping.ts +42 -0
  80. package/src/codex-rules/rules/absolute-paths.ts +52 -0
  81. package/src/codex-rules/rules/ask.ts +44 -0
  82. package/src/codex-rules/rules/chmod-ask.ts +57 -0
  83. package/src/codex-rules/rules/dd.ts +23 -0
  84. package/src/codex-rules/rules/diskutil.ts +33 -0
  85. package/src/codex-rules/rules/mkfs.ts +42 -0
  86. package/src/codex-rules/rules/privilege.ts +49 -0
  87. package/src/codex-rules/rules/remote-fetch.ts +31 -0
  88. package/src/codex-rules/rules/rm-variants.ts +58 -0
  89. package/src/codex-rules/rules/single-commands.ts +57 -0
  90. package/src/codex-rules/skip-list.ts +25 -0
  91. package/src/codex-rules/starlark.ts +46 -0
  92. package/src/codex-rules/types.ts +36 -0
  93. package/src/config/dotenv.ts +55 -0
  94. package/src/config/env.ts +10 -0
  95. package/src/config/home-dir.ts +36 -0
  96. package/src/config/index.ts +5 -0
  97. package/src/config/layout.ts +52 -0
  98. package/src/config/limits.ts +56 -0
  99. package/src/config/ttl.ts +30 -0
  100. package/src/detect/harness.ts +73 -0
  101. package/src/detect/index.ts +1 -0
  102. package/src/detect/interfaces/types.ts +18 -0
  103. package/src/freshness/agent-evidence-record.ts +95 -0
  104. package/src/freshness/agent-evidence.ts +95 -0
  105. package/src/freshness/codex-spawn-evidence.ts +91 -0
  106. package/src/freshness/doc-helpers.ts +90 -0
  107. package/src/freshness/evidence-harvest-io.ts +72 -0
  108. package/src/freshness/evidence-harvest.ts +61 -0
  109. package/src/freshness/explore-tools.ts +95 -0
  110. package/src/freshness/index.ts +2 -0
  111. package/src/freshness/query-framework.ts +73 -0
  112. package/src/freshness/ref-evidence.ts +49 -0
  113. package/src/freshness/ref-journal.ts +98 -0
  114. package/src/freshness/trivial-edit-counter.ts +30 -0
  115. package/src/index.ts +11 -0
  116. package/src/init/index.ts +2 -0
  117. package/src/init/run.ts +32 -0
  118. package/src/init/templates.ts +75 -0
  119. package/src/memory/gitignore.ts +14 -0
  120. package/src/memory/index.ts +3 -0
  121. package/src/memory/registry.ts +39 -0
  122. package/src/memory/session-roots.ts +99 -0
  123. package/src/memory/state.ts +58 -0
  124. package/src/policy/apex-authorization.ts +100 -0
  125. package/src/policy/apex-gates.ts +91 -0
  126. package/src/policy/apex-target-steps.ts +73 -0
  127. package/src/policy/apex-target.ts +49 -0
  128. package/src/policy/apex-task-context.ts +81 -0
  129. package/src/policy/apex.ts +94 -0
  130. package/src/policy/cartographer/build-tree.ts +69 -0
  131. package/src/policy/cartographer/describe.ts +53 -0
  132. package/src/policy/cartographer/entry.ts +38 -0
  133. package/src/policy/cartographer/frontmatter.ts +48 -0
  134. package/src/policy/cartographer/indicators.ts +34 -0
  135. package/src/policy/claude-md-context.ts +66 -0
  136. package/src/policy/conventions/index.ts +11 -0
  137. package/src/policy/conventions/interfaces.ts +71 -0
  138. package/src/policy/conventions/langs.ts +53 -0
  139. package/src/policy/conventions/query.ts +49 -0
  140. package/src/policy/conventions/react-hooks.ts +18 -0
  141. package/src/policy/conventions/store-or-query.ts +21 -0
  142. package/src/policy/conventions/stores.ts +56 -0
  143. package/src/policy/conventions/strip.ts +87 -0
  144. package/src/policy/conventions/verdict.ts +38 -0
  145. package/src/policy/creation-intent.ts +11 -0
  146. package/src/policy/deny-loop.ts +88 -0
  147. package/src/policy/design/allowed-write.ts +28 -0
  148. package/src/policy/design/content-checks.ts +45 -0
  149. package/src/policy/design/corpus-fs.ts +21 -0
  150. package/src/policy/design/corpus-resolve-cache.ts +49 -0
  151. package/src/policy/design/corpus-resolve.ts +99 -0
  152. package/src/policy/design/corpus.ts +92 -0
  153. package/src/policy/design/design-system-rules.ts +45 -0
  154. package/src/policy/design/flag.ts +30 -0
  155. package/src/policy/design/gates-pipeline.ts +105 -0
  156. package/src/policy/design/gates.ts +87 -0
  157. package/src/policy/design/screenshot-tools.ts +24 -0
  158. package/src/policy/design/skill-evidence.ts +34 -0
  159. package/src/policy/design/skill-gate.ts +81 -0
  160. package/src/policy/design/skill-triggers.ts +51 -0
  161. package/src/policy/design/state.ts +92 -0
  162. package/src/policy/design/template-urls.ts +54 -0
  163. package/src/policy/design/transitions.ts +68 -0
  164. package/src/policy/detect-claude-md-project-type.ts +32 -0
  165. package/src/policy/detect-framework.ts +99 -0
  166. package/src/policy/detect-primitive-checks.ts +100 -0
  167. package/src/policy/detect-primitive-lib.ts +55 -0
  168. package/src/policy/detect-project.ts +99 -0
  169. package/src/policy/edit-outcome.ts +53 -0
  170. package/src/policy/evaluate.ts +81 -0
  171. package/src/policy/expert-agents.ts +60 -0
  172. package/src/policy/file-size-scope.ts +32 -0
  173. package/src/policy/file-size.ts +99 -0
  174. package/src/policy/framework-solid-exclude.ts +52 -0
  175. package/src/policy/framework-solid-extended.ts +93 -0
  176. package/src/policy/framework-solid-gates-systems.ts +99 -0
  177. package/src/policy/framework-solid-gates.ts +98 -0
  178. package/src/policy/framework-solid.ts +88 -0
  179. package/src/policy/gemini-mcp-gate.ts +83 -0
  180. package/src/policy/git-gates.ts +40 -0
  181. package/src/policy/guards/bash-command-anchor.ts +40 -0
  182. package/src/policy/guards/bash-write-patterns.ts +140 -0
  183. package/src/policy/guards/bash-write-redirects.ts +201 -0
  184. package/src/policy/guards/bash-write-safe-paths.ts +62 -0
  185. package/src/policy/guards/bash-write.ts +76 -0
  186. package/src/policy/guards/context.ts +12 -0
  187. package/src/policy/guards/index.ts +61 -0
  188. package/src/policy/guards/install.ts +22 -0
  189. package/src/policy/guards/interface-separation-ext.ts +41 -0
  190. package/src/policy/guards/interface-separation.ts +100 -0
  191. package/src/policy/guards/protected-path.ts +100 -0
  192. package/src/policy/guards/security.ts +97 -0
  193. package/src/policy/index.ts +19 -0
  194. package/src/policy/interfaces/types.ts +46 -0
  195. package/src/policy/js-gate-route.ts +51 -0
  196. package/src/policy/lessons/lesson-gate.ts +0 -0
  197. package/src/policy/lessons/trigger-index.ts +84 -0
  198. package/src/policy/lessons/types.ts +31 -0
  199. package/src/policy/module-layout.ts +31 -0
  200. package/src/policy/nearest-manifest.ts +75 -0
  201. package/src/policy/never-approval.ts +95 -0
  202. package/src/policy/patterns.ts +80 -0
  203. package/src/policy/seo/validate.ts +51 -0
  204. package/src/policy/shadcn-project.ts +22 -0
  205. package/src/policy/shadcn-skill-gate.ts +97 -0
  206. package/src/policy/shell-read-refs.ts +96 -0
  207. package/src/policy/skill-path.ts +46 -0
  208. package/src/policy/skill-patterns/laravel.ts +40 -0
  209. package/src/policy/skill-patterns/nextjs.ts +29 -0
  210. package/src/policy/skill-patterns/react.ts +27 -0
  211. package/src/policy/skill-patterns/shadcn-subskills.ts +33 -0
  212. package/src/policy/skill-patterns/shadcn.ts +66 -0
  213. package/src/policy/skill-patterns/swift.ts +30 -0
  214. package/src/policy/skill-patterns/tailwind.ts +40 -0
  215. package/src/policy/skill-source.ts +52 -0
  216. package/src/policy/skill-trigger-patterns.ts +33 -0
  217. package/src/policy/skill-triggers.ts +108 -0
  218. package/src/policy/tailwind-skill-gate.ts +63 -0
  219. package/src/policy/trivial-edits.ts +52 -0
  220. package/src/policy/verbosity.ts +39 -0
  221. package/src/prompt/index.ts +1 -0
  222. package/src/prompt/types.ts +36 -0
  223. package/src/refs/discover.ts +69 -0
  224. package/src/refs/frontmatter.ts +24 -0
  225. package/src/refs/index.ts +4 -0
  226. package/src/refs/loader.ts +59 -0
  227. package/src/refs/ref-key.ts +41 -0
  228. package/src/refs/router.ts +63 -0
  229. package/src/refs/types.ts +25 -0
  230. package/src/runtime/activity.ts +99 -0
  231. package/src/runtime/apply-patch-gate.ts +41 -0
  232. package/src/runtime/burst-window.ts +21 -0
  233. package/src/runtime/codex-shell-tool.ts +37 -0
  234. package/src/runtime/command-string.ts +37 -0
  235. package/src/runtime/confirm/codex-confirm.ts +126 -0
  236. package/src/runtime/confirm/codex-prompt-origin.ts +16 -0
  237. package/src/runtime/confirm/confirm-code.ts +33 -0
  238. package/src/runtime/confirm/confirm-gate.ts +65 -0
  239. package/src/runtime/confirm/confirm-irreversible.ts +32 -0
  240. package/src/runtime/confirm/confirm-pending.ts +34 -0
  241. package/src/runtime/confirm/confirm-state.ts +56 -0
  242. package/src/runtime/confirm/confirm-subagent.ts +112 -0
  243. package/src/runtime/confirm/confirm-submit.ts +38 -0
  244. package/src/runtime/core-guards-active.ts +84 -0
  245. package/src/runtime/core-guards-wired.ts +47 -0
  246. package/src/runtime/deny-loop-store.ts +94 -0
  247. package/src/runtime/deny-notice.ts +52 -0
  248. package/src/runtime/design-cache-resolve.ts +51 -0
  249. package/src/runtime/design-content-gate.ts +95 -0
  250. package/src/runtime/design-files-gate.ts +114 -0
  251. package/src/runtime/design-helpers.ts +93 -0
  252. package/src/runtime/design-lifecycle.ts +39 -0
  253. package/src/runtime/design-read-credit.ts +28 -0
  254. package/src/runtime/design.ts +116 -0
  255. package/src/runtime/dev-context.ts +43 -0
  256. package/src/runtime/dry-patterns.ts +32 -0
  257. package/src/runtime/dry.ts +96 -0
  258. package/src/runtime/fragment-registry.ts +33 -0
  259. package/src/runtime/framework-skill-gate.ts +79 -0
  260. package/src/runtime/fs-cleanup.ts +71 -0
  261. package/src/runtime/gate-apex.ts +81 -0
  262. package/src/runtime/gate-helpers.ts +52 -0
  263. package/src/runtime/gate-input.ts +26 -0
  264. package/src/runtime/gate.ts +128 -0
  265. package/src/runtime/handle-post.ts +104 -0
  266. package/src/runtime/handle-pre.ts +137 -0
  267. package/src/runtime/handle-scope-async.ts +29 -0
  268. package/src/runtime/handle-types.ts +27 -0
  269. package/src/runtime/handle.ts +197 -0
  270. package/src/runtime/home-state.ts +55 -0
  271. package/src/runtime/index.ts +18 -0
  272. package/src/runtime/inform.ts +28 -0
  273. package/src/runtime/inject-budget-recap.ts +47 -0
  274. package/src/runtime/inject-budget.ts +81 -0
  275. package/src/runtime/inject-context.ts +85 -0
  276. package/src/runtime/inject-dedup-exclusive.ts +98 -0
  277. package/src/runtime/inject-dedup.ts +89 -0
  278. package/src/runtime/is-agent-tool.ts +19 -0
  279. package/src/runtime/lifecycle/agent-files.ts +49 -0
  280. package/src/runtime/lifecycle/agent-memory.ts +82 -0
  281. package/src/runtime/lifecycle/agent-transcript.ts +80 -0
  282. package/src/runtime/lifecycle/aipilot/analytics.ts +99 -0
  283. package/src/runtime/lifecycle/aipilot/apex-task-store.ts +61 -0
  284. package/src/runtime/lifecycle/aipilot/cache-base.ts +48 -0
  285. package/src/runtime/lifecycle/aipilot/cache-doc.ts +100 -0
  286. package/src/runtime/lifecycle/aipilot/cache-lessons.ts +58 -0
  287. package/src/runtime/lifecycle/aipilot/cache-test.ts +70 -0
  288. package/src/runtime/lifecycle/aipilot/curate-lessons.ts +64 -0
  289. package/src/runtime/lifecycle/aipilot/dispatch-aipilot.ts +106 -0
  290. package/src/runtime/lifecycle/aipilot/doc-cache-gate.ts +69 -0
  291. package/src/runtime/lifecycle/aipilot/inject-apex.ts +98 -0
  292. package/src/runtime/lifecycle/aipilot/inject-doc.ts +62 -0
  293. package/src/runtime/lifecycle/aipilot/inject-explore.ts +64 -0
  294. package/src/runtime/lifecycle/aipilot/inject-lessons.ts +61 -0
  295. package/src/runtime/lifecycle/aipilot/inject-test.ts +48 -0
  296. package/src/runtime/lifecycle/aipilot/lesson-archive.ts +60 -0
  297. package/src/runtime/lifecycle/aipilot/lesson-inject.ts +98 -0
  298. package/src/runtime/lifecycle/aipilot/lesson-parse.ts +74 -0
  299. package/src/runtime/lifecycle/aipilot/lessons.ts +72 -0
  300. package/src/runtime/lifecycle/aipilot/promote-global-lessons.ts +92 -0
  301. package/src/runtime/lifecycle/aipilot/solid-compliance.ts +69 -0
  302. package/src/runtime/lifecycle/aipilot/solid-notice.ts +47 -0
  303. package/src/runtime/lifecycle/aipilot/solid-transcript.ts +60 -0
  304. package/src/runtime/lifecycle/aipilot/source-scan.ts +56 -0
  305. package/src/runtime/lifecycle/aipilot/sync-task.ts +75 -0
  306. package/src/runtime/lifecycle/aipilot/transcript.ts +88 -0
  307. package/src/runtime/lifecycle/aipilot/types.ts +72 -0
  308. package/src/runtime/lifecycle/auto-document-reads.ts +73 -0
  309. package/src/runtime/lifecycle/cartographer/detect.ts +57 -0
  310. package/src/runtime/lifecycle/cartographer/ecosystem-map.ts +56 -0
  311. package/src/runtime/lifecycle/cartographer/fs-util.ts +64 -0
  312. package/src/runtime/lifecycle/cartographer/merge.ts +51 -0
  313. package/src/runtime/lifecycle/cartographer/project-map.ts +49 -0
  314. package/src/runtime/lifecycle/cartographer/scan-hooks.ts +48 -0
  315. package/src/runtime/lifecycle/cartographer/scan.ts +81 -0
  316. package/src/runtime/lifecycle/cartographer/session-start.ts +33 -0
  317. package/src/runtime/lifecycle/cartographer/track-enrichment.ts +45 -0
  318. package/src/runtime/lifecycle/cartographer/write-plugin-map.ts +43 -0
  319. package/src/runtime/lifecycle/cartographer/write-tree.ts +44 -0
  320. package/src/runtime/lifecycle/changelog-research.ts +43 -0
  321. package/src/runtime/lifecycle/check-file-size.ts +61 -0
  322. package/src/runtime/lifecycle/codex-resync/fingerprint.ts +63 -0
  323. package/src/runtime/lifecycle/codex-resync/lock.ts +46 -0
  324. package/src/runtime/lifecycle/codex-resync/managed-destination.ts +39 -0
  325. package/src/runtime/lifecycle/codex-resync/materialize-agents.ts +57 -0
  326. package/src/runtime/lifecycle/codex-resync/plugin-files.ts +65 -0
  327. package/src/runtime/lifecycle/codex-resync/plugin-roots.ts +62 -0
  328. package/src/runtime/lifecycle/codex-resync/resync.ts +45 -0
  329. package/src/runtime/lifecycle/codex-resync/symlink-commands.ts +25 -0
  330. package/src/runtime/lifecycle/dispatch.ts +109 -0
  331. package/src/runtime/lifecycle/failure-lesson.ts +55 -0
  332. package/src/runtime/lifecycle/index.ts +34 -0
  333. package/src/runtime/lifecycle/inject-rules.ts +50 -0
  334. package/src/runtime/lifecycle/instructions-loaded.ts +22 -0
  335. package/src/runtime/lifecycle/kimi-rules-native.ts +44 -0
  336. package/src/runtime/lifecycle/lessons/dispatch.ts +88 -0
  337. package/src/runtime/lifecycle/lessons/reminder.ts +89 -0
  338. package/src/runtime/lifecycle/lessons/state.ts +23 -0
  339. package/src/runtime/lifecycle/memory/agent-lesson.ts +35 -0
  340. package/src/runtime/lifecycle/memory/capture-error.ts +41 -0
  341. package/src/runtime/lifecycle/memory/client.ts +65 -0
  342. package/src/runtime/lifecycle/memory/dispatch.ts +42 -0
  343. package/src/runtime/lifecycle/memory/recall.ts +30 -0
  344. package/src/runtime/lifecycle/memory/salience.ts +31 -0
  345. package/src/runtime/lifecycle/memory/state.ts +49 -0
  346. package/src/runtime/lifecycle/memory/track-ops.ts +15 -0
  347. package/src/runtime/lifecycle/post-compact.ts +38 -0
  348. package/src/runtime/lifecycle/post-edit-ts.ts +49 -0
  349. package/src/runtime/lifecycle/post-tracking.ts +40 -0
  350. package/src/runtime/lifecycle/pre-compact.ts +38 -0
  351. package/src/runtime/lifecycle/rules-root.ts +99 -0
  352. package/src/runtime/lifecycle/security/check-skill.ts +63 -0
  353. package/src/runtime/lifecycle/security/scan-patterns.ts +53 -0
  354. package/src/runtime/lifecycle/security/scan.ts +76 -0
  355. package/src/runtime/lifecycle/security/skill-state.ts +47 -0
  356. package/src/runtime/lifecycle/security/track-mcp.ts +29 -0
  357. package/src/runtime/lifecycle/security/track-skill-read.ts +31 -0
  358. package/src/runtime/lifecycle/seo/post-tool-use.ts +45 -0
  359. package/src/runtime/lifecycle/session-end.ts +17 -0
  360. package/src/runtime/lifecycle/session-start.ts +57 -0
  361. package/src/runtime/lifecycle/snapshot/board.ts +27 -0
  362. package/src/runtime/lifecycle/snapshot/format.ts +60 -0
  363. package/src/runtime/lifecycle/snapshot/git.ts +71 -0
  364. package/src/runtime/lifecycle/snapshot/index.ts +52 -0
  365. package/src/runtime/lifecycle/snapshot/version.ts +34 -0
  366. package/src/runtime/lifecycle/solid-detect.ts +56 -0
  367. package/src/runtime/lifecycle/stop-core.ts +35 -0
  368. package/src/runtime/lifecycle/subagent-cache.ts +61 -0
  369. package/src/runtime/lifecycle/task-completed.ts +94 -0
  370. package/src/runtime/lifecycle/teammate-idle-check.ts +63 -0
  371. package/src/runtime/lifecycle/teammate-idle.ts +28 -0
  372. package/src/runtime/lifecycle/tool-failure.ts +24 -0
  373. package/src/runtime/lifecycle/track-changes.ts +61 -0
  374. package/src/runtime/lifecycle/validate-solid.ts +70 -0
  375. package/src/runtime/lifecycle/validate-tailwind.ts +80 -0
  376. package/src/runtime/lifecycle-bridge.ts +74 -0
  377. package/src/runtime/mcp-key.ts +44 -0
  378. package/src/runtime/mcp-tool-name.ts +92 -0
  379. package/src/runtime/mcp.ts +91 -0
  380. package/src/runtime/modular.ts +77 -0
  381. package/src/runtime/normalize.ts +107 -0
  382. package/src/runtime/notices.ts +91 -0
  383. package/src/runtime/notification-sound.ts +49 -0
  384. package/src/runtime/notifications.ts +77 -0
  385. package/src/runtime/paths.ts +59 -0
  386. package/src/runtime/post-fanout.ts +41 -0
  387. package/src/runtime/post-outcome.ts +61 -0
  388. package/src/runtime/pre-allow.ts +97 -0
  389. package/src/runtime/precommit.ts +60 -0
  390. package/src/runtime/prompt-text.ts +38 -0
  391. package/src/runtime/receipt-capture.ts +34 -0
  392. package/src/runtime/record.ts +26 -0
  393. package/src/runtime/respond.ts +92 -0
  394. package/src/runtime/solid-file-size-gate.ts +58 -0
  395. package/src/runtime/solid-pre.ts +53 -0
  396. package/src/runtime/storage.ts +9 -0
  397. package/src/statusline/colors.ts +31 -0
  398. package/src/statusline/constants.ts +17 -0
  399. package/src/statusline/formatters.ts +54 -0
  400. package/src/statusline/index.ts +4 -0
  401. package/src/statusline/progress-bar.ts +46 -0
  402. package/src/tracking/codex-post-failure.ts +73 -0
  403. package/src/tracking/index.ts +2 -0
  404. package/src/tracking/integrity.ts +99 -0
  405. package/src/tracking/one-shot-dedup.ts +34 -0
  406. package/src/tracking/one-shot-failure.ts +42 -0
  407. package/src/tracking/one-shot-store.ts +95 -0
  408. package/src/tracking/one-shot.ts +93 -0
  409. package/src/tracking/receipts.ts +95 -0
  410. package/src/tracking/session-state.ts +94 -0
  411. package/src/tracking/store.ts +96 -0
  412. package/src/tracking/track-compact.ts +100 -0
  413. package/src/tracking/track-diff.ts +50 -0
  414. package/src/tracking/track-journal.ts +97 -0
  415. package/src/tracking/track-lock-sync.ts +83 -0
  416. package/src/tracking/track-lock.ts +67 -0
  417. package/src/util/compact-json.ts +23 -0
  418. package/src/util/index.ts +2 -0
  419. package/src/util/json-io.ts +47 -0
  420. package/src/util/project-root.ts +41 -0
  421. package/src/util/runtime-io.ts +85 -0
  422. package/src/util/semver.ts +30 -0
@@ -0,0 +1,69 @@
1
+ /**
2
+ * @module aipilot/doc-cache-gate
3
+ * PreToolUse gate for the ai-pilot scope: DENY a Context7/Exa doc call when the
4
+ * per-project doc cache already holds that library fresh (< 7d). Ports the
5
+ * source `doc-cache-gate.ts`. The inject side (`inject-doc.ts`) serves the same
6
+ * cache on SubagentStart; this side avoids the redundant live re-query.
7
+ * @packageDocumentation
8
+ */
9
+ import { homedir } from "node:os";
10
+ import { join } from "node:path";
11
+ import { readJsonFile } from "../../../util/json-io";
12
+ import { pathExists } from "../../../util/runtime-io";
13
+ import { denyResponse } from "../../../adapters/claude";
14
+ import { cacheDirFor, cacheAge, projectHash, DOC_CACHE_TTL_SECONDS } from "./cache-base";
15
+ import { logCacheEvent } from "./analytics";
16
+ import { canonicalizeMcpToolName } from "../../mcp-tool-name";
17
+ import type { CacheIndex } from "./types";
18
+
19
+ const GATED_TOOLS = /context7__query-docs|exa__get_code_context|exa__web_search/;
20
+
21
+ /** The library key a gated tool call targets (Context7 libraryId / Exa query), or "". */
22
+ function libraryOf(payload: Record<string, unknown>): string {
23
+ const tool = String(payload.tool_name ?? "");
24
+ const input = (payload.tool_input as Record<string, unknown> | undefined) ?? {};
25
+ if (tool.includes("context7")) {
26
+ // Real mcp__context7__query-docs input is {libraryId, query} — no "topic"
27
+ // field exists (confirmed against cache-doc.ts and mcp-key.ts's queryOf,
28
+ // which both key on `query`, never `topic`).
29
+ const libraryId = typeof input.libraryId === "string" ? input.libraryId : "";
30
+ const query = typeof input.query === "string" ? input.query : "";
31
+ return libraryId && query ? libraryId : "";
32
+ }
33
+ if (tool.includes("exa")) return typeof input.query === "string" ? input.query : "";
34
+ return "";
35
+ }
36
+
37
+ /**
38
+ * DENY a redundant doc call when the library is cached fresh, else `null`.
39
+ * @param payload - The raw PreToolUse hook payload.
40
+ * @param cwd - Fallback project root (uses `CLAUDE_PROJECT_DIR` first).
41
+ * @param now - Clock (defaults to `Date.now()`).
42
+ * @param home - Home dir (defaults to `~`).
43
+ * @param id - Harness adapter id (defaults to "claude-code"); on `"codex"`,
44
+ * `payload.tool_name` is canonicalized before the {@link GATED_TOOLS} match
45
+ * so the Codex-mangled underscore form (`mcp__context7__query_docs`) gates
46
+ * exactly like the dash form does on every other harness.
47
+ * @returns A native deny response, or `null` to fall through to the live call.
48
+ */
49
+ export async function docCacheGate(payload: Record<string, unknown>, cwd: string, now: number = Date.now(), home: string = homedir(), id: string = "claude-code"): Promise<string | null> {
50
+ const tool = canonicalizeMcpToolName(id, String(payload.tool_name ?? ""));
51
+ if (!GATED_TOOLS.test(tool)) return null;
52
+ const library = libraryOf(payload);
53
+ if (!library) return null;
54
+
55
+ const projPath = process.env.CLAUDE_PROJECT_DIR ?? cwd;
56
+ const cacheDir = cacheDirFor("doc", projPath, home);
57
+ const index = await readJsonFile<CacheIndex>(join(cacheDir, "index.json"));
58
+ const entry = index?.docs?.find((d) => d.library === library);
59
+ if (!entry?.hash || !entry.timestamp) return null;
60
+
61
+ const docFile = join(cacheDir, "docs", `${entry.hash}.md`);
62
+ if (!pathExists(docFile)) return null;
63
+ const age = cacheAge(entry.timestamp, now);
64
+ if (age >= DOC_CACHE_TTL_SECONDS) return null;
65
+
66
+ logCacheEvent("doc", "blocked", projectHash(projPath), { library }, home);
67
+ const ageH = Math.floor(age / 3600);
68
+ return denyResponse("PreToolUse", `Doc already cached at ${docFile} (age: ${ageH}h). Use the Read tool to access it instead of re-querying.`);
69
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * SubagentStart (matcher "") for the ai-pilot scope: inject APEX AGENTS.md +
3
+ * task context + cartographer paths into every sub-agent. Ports
4
+ * `inject-subagent-context.ts`. The core-scope SubagentStart already surfaces
5
+ * the MCP cache table (`subagent-cache.ts`) in a separate hook entry — this
6
+ * handler only emits the APEX/cartographer block, so there is no double-emit.
7
+ */
8
+ import { existsSync } from "node:fs";
9
+ import { join, resolve } from "node:path";
10
+ import { homedir } from "node:os";
11
+ import { readJsonFile } from "../../../util/json-io";
12
+ import { readText } from "../../../util/runtime-io";
13
+ import { resolveMaxLines } from "../../../config/limits";
14
+ import { contextResponse } from "../../../adapters/claude";
15
+ import { capFragment } from "../../inject-budget";
16
+ import { harnessHomeSegment } from "../../../policy/apex-target";
17
+ import { apexAgentSteps } from "../../../policy/apex-target-steps";
18
+ import type { ApexTaskFile } from "./types";
19
+
20
+ type TaskMap = ApexTaskFile["tasks"];
21
+
22
+ /** Last 3 completed task subjects (newest first), or "none". */
23
+ function completedTasks(tasks: TaskMap): string {
24
+ const done = Object.entries(tasks)
25
+ .filter(([, t]) => t.status === "completed")
26
+ .sort((a, b) => (b[1].completed_at ?? "").localeCompare(a[1].completed_at ?? ""))
27
+ .slice(0, 3)
28
+ .map(([id, t]) => `#${id}: ${t.subject}`);
29
+ return done.length > 0 ? done.join(", ") : "none";
30
+ }
31
+
32
+ /** Pending task subjects, or "none". */
33
+ function pendingTasks(tasks: TaskMap): string {
34
+ const pending = Object.entries(tasks)
35
+ .filter(([, t]) => t.status === "pending")
36
+ .map(([id, t]) => `#${id}: ${t.subject}`);
37
+ return pending.length > 0 ? pending.join(", ") : "none";
38
+ }
39
+
40
+ /** Cartographer navigation block, or "" when no plugin map is present. */
41
+ function cartographerContext(): string {
42
+ const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT;
43
+ if (!pluginRoot) return "";
44
+ const pluginsMap = resolve(pluginRoot, "..", ".cartographer", "index.md");
45
+ if (!existsSync(pluginsMap)) return "";
46
+ return `\n### 7. Cartographer Maps\nNavigate branches (index.md) -> leaves link to real files:\n- Plugin skills: ${pluginsMap}\n- Project files: .cartographer/project/index.md`;
47
+ }
48
+
49
+ /**
50
+ * Build the APEX sub-agent injection for SubagentStart, or "" when the project has no target apex dir (`.claude/apex/`, `.codex/apex/`, ...). Reads AGENTS.md (first 4KB) + task.json.
51
+ * @param cwd - Fallback project root when `CLAUDE_PROJECT_DIR` is unset.
52
+ * @param home - Home dir (unused placeholder; kept for symmetry/testing).
53
+ * @param id - Harness target id (defaults to "claude-code" — zero-regression default).
54
+ * @returns The native hook stdout (possibly empty).
55
+ */
56
+ export async function injectApexSubagentContext(cwd: string, home: string = homedir(), id: string = "claude-code"): Promise<string> {
57
+ void home;
58
+ const projectRoot = process.env.CLAUDE_PROJECT_DIR ?? cwd;
59
+ const seg = harnessHomeSegment(id);
60
+ const apexDir = join(projectRoot, seg, "apex");
61
+ if (!existsSync(apexDir)) return "";
62
+
63
+ const agentsPath = join(apexDir, "AGENTS.md");
64
+ const agents = existsSync(agentsPath) ? readText(agentsPath).slice(0, 4000) : "";
65
+ const taskData = await readJsonFile<ApexTaskFile>(join(apexDir, "task.json"));
66
+ const completed = taskData ? completedTasks(taskData.tasks) : "none";
67
+ const pending = taskData ? pendingTasks(taskData.tasks) : "none";
68
+ const { beforeStart, whenDone } = apexAgentSteps(id);
69
+
70
+ const context = `## APEX Sub-Agent Instructions
71
+
72
+ You are a sub-agent in APEX workflow. Follow these rules:
73
+
74
+ ### 1. AGENTS.md Rules
75
+ ${agents}
76
+
77
+ ### 2. Task Context
78
+ - Last completed: ${completed}
79
+ - Pending: ${pending}
80
+
81
+ ### 3. Before Starting Work
82
+ - ${beforeStart}
83
+
84
+ ### 4. SOLID Rules
85
+ - Files < ${resolveMaxLines()} lines | Interfaces in src/interfaces/ | JSDoc/PHPDoc required
86
+
87
+ ### 5. Research Before Code
88
+ - Use Context7/Exa for docs | Write notes to ${seg}/apex/docs/
89
+
90
+ ### 6. Before Done (NEVER skip)
91
+ - eLicit: self-review with a NAMED elicitation technique; fix findings first
92
+ - Verify: run/functional-check your changes (references⇔declarations)
93
+
94
+ ### 7. When Done
95
+ - ${whenDone}${cartographerContext()}`;
96
+
97
+ return contextResponse("SubagentStart", capFragment("apex-subagent", context));
98
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * SubagentStart (matcher "research-expert") for the ai-pilot scope: inject
3
+ * cached documentation summaries. Ports `doc-cache-inject.ts`. Doc *saving*
4
+ * happens on SubagentStop (`cache-doc.ts`).
5
+ */
6
+ import { homedir } from "node:os";
7
+ import { join } from "node:path";
8
+ import { readJsonFile } from "../../../util/json-io";
9
+ import { readText, pathExists } from "../../../util/runtime-io";
10
+ import { contextResponse } from "../../../adapters/claude";
11
+ import { cacheDirFor, cacheAge, projectHash, DOC_CACHE_TTL_SECONDS } from "./cache-base";
12
+ import { logCacheEvent } from "./analytics";
13
+ import type { CacheEntry, CacheIndex } from "./types";
14
+
15
+ const MAX_SIZE = 8192;
16
+
17
+ /** Concatenate fresh, dedup-by-hash cached doc bodies. */
18
+ async function buildDocsContext(entries: CacheEntry[], docsDir: string, now: number): Promise<{ ctx: string; count: number; maxAge: number }> {
19
+ let ctx = "";
20
+ let count = 0;
21
+ let maxAge = 0;
22
+ const seen = new Set<string>();
23
+ for (const entry of entries) {
24
+ if (!entry.timestamp) continue;
25
+ const age = cacheAge(entry.timestamp, now);
26
+ if (age >= DOC_CACHE_TTL_SECONDS) continue;
27
+ if (age > maxAge) maxAge = age;
28
+ if (!entry.hash || seen.has(entry.hash)) continue;
29
+ seen.add(entry.hash);
30
+ const docPath = join(docsDir, `${entry.hash}.md`);
31
+ if (!pathExists(docPath)) continue;
32
+ const content = readText(docPath);
33
+ if (!content) continue;
34
+ ctx += `\n${content}\n`;
35
+ count++;
36
+ }
37
+ return { ctx, count, maxAge };
38
+ }
39
+
40
+ /**
41
+ * SubagentStart for research-expert: inject cached doc summaries, or "".
42
+ * @param cwd - Fallback project root (uses `CLAUDE_PROJECT_DIR` first).
43
+ * @param home - Home dir (defaults to `~`).
44
+ * @param now - Clock (defaults to `Date.now()`).
45
+ * @returns The native hook stdout (possibly empty).
46
+ */
47
+ export async function injectDocCache(cwd: string, home: string = homedir(), now: number = Date.now()): Promise<string> {
48
+ const projPath = process.env.CLAUDE_PROJECT_DIR ?? cwd;
49
+ const pHash = projectHash(projPath);
50
+ const cacheDir = cacheDirFor("doc", projPath, home);
51
+ const index = await readJsonFile<CacheIndex>(join(cacheDir, "index.json"));
52
+ if (!index?.docs?.length) return "";
53
+
54
+ const { ctx, count, maxAge } = await buildDocsContext(index.docs, join(cacheDir, "docs"), now);
55
+ if (count === 0) return "";
56
+ logCacheEvent("doc", "hit", pHash, { docs_injected: count }, home);
57
+
58
+ const ageH = Math.ceil(maxAge / 3600);
59
+ const header = `## CACHED DOCUMENTATION (${count} docs, ${ageH}h ago)\nUse this knowledge. Only query Context7 for topics NOT covered below.\n`;
60
+ const full = `${header}${ctx}Full docs: ${join(cacheDir, "docs")}/`.slice(0, MAX_SIZE);
61
+ return contextResponse("SubagentStart", full);
62
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * SubagentStart (matcher "explore-codebase") for the ai-pilot scope: serve a
3
+ * cached architecture report when fresh + config-matching, else inject save
4
+ * instructions. Ports `explore-cache-check.ts`.
5
+ */
6
+ import { homedir } from "node:os";
7
+ import { join } from "node:path";
8
+ import { readJsonFile } from "../../../util/json-io";
9
+ import { readText, pathExists, spawnCapture } from "../../../util/runtime-io";
10
+ import { contextResponse } from "../../../adapters/claude";
11
+ import { hashText16, cacheDirFor, cacheAge, projectHash } from "./cache-base";
12
+ import { logCacheEvent } from "./analytics";
13
+
14
+ const TTL_SECONDS = 86_400;
15
+ const CONFIG_FILES = ["package.json", "tsconfig.json", "composer.json", "go.mod", "Cargo.toml", "Package.swift", "biome.json", ".eslintrc.js", ".eslintrc.json"];
16
+
17
+ /** Compute a config hash from git-tracked config files; "noconfig" on failure. */
18
+ async function configHash(cwd: string): Promise<string> {
19
+ try {
20
+ const output = spawnCapture("git", ["ls-tree", "HEAD", ...CONFIG_FILES], cwd);
21
+ return output.trim() ? hashText16(output) : "noconfig";
22
+ } catch {
23
+ return "noconfig";
24
+ }
25
+ }
26
+
27
+ /** Build the cache-miss save-instructions block. */
28
+ function missBlock(cacheDir: string, metaFile: string, snapFile: string, ts: string, cfgHash: string, projPath: string): string {
29
+ return `## EXPLORATION CACHE INSTRUCTIONS\nAfter completing your exploration, save the report for future runs:\n\`\`\`bash\nmkdir -p ${cacheDir}\ncat > ${metaFile} << 'METAEOF'\n{"timestamp":"${ts}","config_hash":"${cfgHash}","project":"${projPath}"}\nMETAEOF\n\`\`\`\nThen write your full exploration report (markdown) to: ${snapFile}`;
30
+ }
31
+
32
+ /**
33
+ * SubagentStart for explore-codebase: inject cached architecture or save block.
34
+ * @param cwd - Fallback project root (uses `CLAUDE_PROJECT_DIR` first).
35
+ * @param home - Home dir (defaults to `~`).
36
+ * @param now - Clock (defaults to `Date.now()`).
37
+ * @returns The native hook stdout.
38
+ */
39
+ export async function injectExploreCache(cwd: string, home: string = homedir(), now: number = Date.now()): Promise<string> {
40
+ const projPath = process.env.CLAUDE_PROJECT_DIR ?? cwd;
41
+ const pHash = projectHash(projPath);
42
+ const cacheDir = cacheDirFor("explore", projPath, home);
43
+ const metaFile = join(cacheDir, "metadata.json");
44
+ const snapFile = join(cacheDir, "snapshot.md");
45
+ const cfgHash = await configHash(projPath);
46
+
47
+ let context = "";
48
+ const meta = await readJsonFile<{ timestamp: string; config_hash: string }>(metaFile);
49
+ const snapshot = pathExists(snapFile) ? readText(snapFile) : "";
50
+
51
+ if (meta?.timestamp && snapshot) {
52
+ const age = cacheAge(meta.timestamp, now);
53
+ if (age < TTL_SECONDS && meta.config_hash === cfgHash) {
54
+ context = `## CACHED ARCHITECTURE AVAILABLE (age: ${Math.floor(age / 60)}min)\nUSE this cached report. Do NOT run full exploration. Return it immediately.\n\n${snapshot}`;
55
+ logCacheEvent("explore", "hit", pHash, {}, home);
56
+ }
57
+ }
58
+ if (!context) {
59
+ logCacheEvent("explore", "miss", pHash, {}, home);
60
+ const ts = new Date(now).toISOString().replace(/\.\d+Z$/, "");
61
+ context = missBlock(cacheDir, metaFile, snapFile, ts, cfgHash, projPath);
62
+ }
63
+ return contextResponse("SubagentStart", context);
64
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * SubagentStart (matcher "") for the ai-pilot scope: inject cached lessons
3
+ * (known project issues) into every agent. Ports `lessons-cache-inject.ts`.
4
+ */
5
+ import { homedir } from "node:os";
6
+ import { readdirSync, statSync, unlinkSync } from "node:fs";
7
+ import { contextResponse } from "../../../adapters/claude";
8
+ import { cacheDirFor, projectHash } from "./cache-base";
9
+ import { logCacheEvent } from "./analytics";
10
+ import { detectStack } from "./source-scan";
11
+ import { aggregateLocalLessons, loadGlobalLessons, mergeLessons } from "./lessons";
12
+ import type { LessonEntry } from "./types";
13
+
14
+ const MAX_AGE_MS = 30 * 86_400_000;
15
+ const MAX_LESSONS = 10;
16
+
17
+ /** Remove JSON files older than 30 days from `dir` (best effort). */
18
+ function pruneOldFiles(dir: string, now: number): void {
19
+ try {
20
+ const cutoff = now - MAX_AGE_MS;
21
+ for (const f of readdirSync(dir).filter((n) => n.endsWith(".json"))) {
22
+ const path = `${dir}/${f}`;
23
+ try { if (statSync(path).mtimeMs < cutoff) unlinkSync(path); } catch { /* skip */ }
24
+ }
25
+ } catch { /* dir may not exist */ }
26
+ }
27
+
28
+ /** Render lessons as a numbered known-issues list. */
29
+ function formatLessonList(lessons: LessonEntry[]): string {
30
+ return lessons.map((l, i) => {
31
+ const code = l.code?.line?.length ? `\n Code: ${l.code.line.join(" | ").slice(0, 200)}` : "";
32
+ return `${i + 1}. [${l.count}x] | ${l.pattern ?? "unknown"} -> ${l.fix ?? "see docs"}${code}`;
33
+ }).join("\n");
34
+ }
35
+
36
+ /**
37
+ * SubagentStart lessons injection: aggregate local + global lessons, or "".
38
+ * @param cwd - Fallback project root (uses `CLAUDE_PROJECT_DIR` first).
39
+ * @param home - Home dir (defaults to `~`).
40
+ * @param now - Clock (defaults to `Date.now()`).
41
+ * @returns The native hook stdout (possibly empty).
42
+ */
43
+ export async function injectLessonsCache(cwd: string, home: string = homedir(), now: number = Date.now()): Promise<string> {
44
+ const projectPath = process.env.CLAUDE_PROJECT_DIR ?? cwd;
45
+ const pHash = projectHash(projectPath);
46
+ const cacheDir = cacheDirFor("lessons", projectPath, home);
47
+ const stack = detectStack(projectPath);
48
+
49
+ pruneOldFiles(cacheDir, now);
50
+ let localLessons: LessonEntry[] = [];
51
+ try { localLessons = await aggregateLocalLessons(cacheDir); } catch { /* none */ }
52
+ const globalLessons = await loadGlobalLessons(stack, home);
53
+ if (localLessons.length === 0 && globalLessons.length === 0) return "";
54
+
55
+ const merged = mergeLessons(localLessons, globalLessons).slice(0, MAX_LESSONS);
56
+ if (merged.length === 0) return "";
57
+ logCacheEvent("lessons", "hit", pHash, { count: merged.length, stack }, home);
58
+
59
+ const ctx = `## KNOWN PROJECT ISSUES (from previous sniper validations)\nThese errors have been found and fixed before. AVOID them:\n${formatLessonList(merged)}\n\nINSTRUCTION: Check your code against these known issues BEFORE submitting.`;
60
+ return contextResponse("SubagentStart", ctx);
61
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * SubagentStart (matcher "sniper") for the ai-pilot scope: tell sniper which
3
+ * source files changed vs. already-validated. Ports `test-cache-inject.ts`.
4
+ */
5
+ import { homedir } from "node:os";
6
+ import { join } from "node:path";
7
+ import { readJsonFile } from "../../../util/json-io";
8
+ import { contextResponse } from "../../../adapters/claude";
9
+ import { cacheDirFor, cacheAge, fileChecksum } from "./cache-base";
10
+ import { scanSourceFiles } from "./source-scan";
11
+ import type { TestCache } from "./types";
12
+
13
+ const TTL_SECONDS = 172_800;
14
+
15
+ /**
16
+ * SubagentStart for sniper: inject the changed-file list, or "" when nothing
17
+ * useful to say (no cache / no unchanged files).
18
+ * @param cwd - Fallback project root (uses `CLAUDE_PROJECT_DIR` first).
19
+ * @param home - Home dir (defaults to `~`).
20
+ * @param now - Clock (defaults to `Date.now()`).
21
+ * @returns The native hook stdout (possibly empty).
22
+ */
23
+ export async function injectTestCache(cwd: string, home: string = homedir(), now: number = Date.now()): Promise<string> {
24
+ const projectPath = process.env.CLAUDE_PROJECT_DIR ?? cwd;
25
+ const resultsPath = join(cacheDirFor("tests", projectPath, home), "results.json");
26
+ const cache = await readJsonFile<TestCache>(resultsPath);
27
+ if (!cache?.files || Object.keys(cache.files).length === 0) return "";
28
+
29
+ const srcFiles = await scanSourceFiles(projectPath);
30
+ if (srcFiles.length === 0) return "";
31
+
32
+ const changed: string[] = [];
33
+ let unchanged = 0;
34
+ for (const filepath of srcFiles) {
35
+ const relPath = filepath.replace(`${projectPath}/`, "");
36
+ const cached = cache.files[relPath];
37
+ if (!cached) { changed.push(relPath); continue; }
38
+ const currentSum = (await fileChecksum(filepath)).slice(0, 16);
39
+ if (currentSum !== cached.checksum) { changed.push(relPath); continue; }
40
+ if (cached.last_tested && cacheAge(cached.last_tested, now) > TTL_SECONDS) { changed.push(relPath); continue; }
41
+ unchanged++;
42
+ }
43
+ if (unchanged === 0) return "";
44
+
45
+ const changedList = changed.map((f) => `- ${f}`).join("\n");
46
+ const ctx = `## TEST CACHE (${unchanged}/${srcFiles.length} files already validated)\nOnly run linters on these CHANGED files:\n${changedList}\nSKIP linting on ${unchanged} unchanged files - already PASS.`;
47
+ return contextResponse("SubagentStart", ctx);
48
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Stage 1 — cap→archive split for LESSON.md. When deduped bullets exceed CAP the
3
+ * OLDEST excess is MOVED (never deleted) to LESSON-archive.md, EXCEPT a
4
+ * `[TRIGGERS …]` bullet younger than STALE_DAYS: archiving it would blind the
5
+ * PreToolUse trigger index (src/policy/lessons/trigger-index reads LESSON.md), so
6
+ * it stays even past the cap. Pure: this module decides the partition and renders
7
+ * the archive block; the fail-safe, archive-first file write is the caller's job.
8
+ */
9
+ import { CAP, DAY_MS, STALE_DAYS, type Block, hasTrigger } from "./lesson-parse";
10
+
11
+ /** Sort key: undated bullets sort oldest, so malformed entries archive first. */
12
+ function age(b: Block): number { return Number.isNaN(b.ts) ? -Infinity : b.ts; }
13
+
14
+ /** A TRIGGERS bullet is protected from archival until older than STALE_DAYS. */
15
+ function isProtected(b: Block, staleBefore: number): boolean {
16
+ return hasTrigger(b) && !(b.ts <= staleBefore);
17
+ }
18
+
19
+ /**
20
+ * Partition deduped `blocks` (newest-first file order) into the bullets that
21
+ * stay in LESSON.md and the oldest excess to archive. Archives only enough to
22
+ * reach CAP, skipping protected TRIGGERS bullets (so the file MAY stay slightly
23
+ * over cap by design). Order is preserved in both halves; `keep ∪ archive` is
24
+ * exactly `blocks` with no loss and no mutation.
25
+ * @param blocks - Deduped bullets, newest first.
26
+ * @param now - Clock (ms) for the STALE_DAYS protection window.
27
+ * @returns `{ keep, archive }` — a lossless partition of `blocks`.
28
+ */
29
+ export function splitAtCap(blocks: Block[], now: number): { keep: Block[]; archive: Block[] } {
30
+ if (blocks.length <= CAP) return { keep: blocks, archive: [] };
31
+ const staleBefore = now - STALE_DAYS * DAY_MS;
32
+ const oldestFirst = [...blocks].sort((a, b) => age(a) - age(b));
33
+ const toArchive = new Set<Block>();
34
+ let excess = blocks.length - CAP;
35
+ for (const b of oldestFirst) {
36
+ if (excess <= 0) break;
37
+ if (isProtected(b, staleBefore)) continue;
38
+ toArchive.add(b);
39
+ excess--;
40
+ }
41
+ return {
42
+ keep: blocks.filter((b) => !toArchive.has(b)),
43
+ archive: blocks.filter((b) => toArchive.has(b)),
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Render `archive` bullets as a dated block to PREPEND to LESSON-archive.md
49
+ * (newest archive session on top). Bullets are emitted BYTE-IDENTICAL (raw lines
50
+ * rejoined) — zero mutation, so the move stays reversible/auditable.
51
+ * @param archive - Bullets chosen by {@link splitAtCap}.
52
+ * @param now - Clock (ms) for the archival header date.
53
+ * @returns The block text (trailing newline), or "" when nothing is archived.
54
+ */
55
+ export function formatArchive(archive: Block[], now: number): string {
56
+ if (archive.length === 0) return "";
57
+ const day = new Date(now).toISOString().slice(0, 10);
58
+ const header = `<!-- archived ${day}: ${archive.length} bullet(s) moved from LESSON.md at cap ${CAP} -->`;
59
+ return `${header}\n${archive.map((b) => b.raw.join("\n")).join("\n\n")}\n`;
60
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Stage 2 — compress the LESSON.md SessionStart/SubagentStart block (the big
3
+ * token win). The RECENT_FULL newest bullets pass verbatim; every older bullet
4
+ * collapses to ONE line — its date plus the FIRST SENTENCE of the actionable
5
+ * rule after the bullet's LAST "→" (the file's `narrative → rule` format), or of
6
+ * the whole bullet when there is no arrow, hard-capped at RULE_CAP chars. The
7
+ * FILE keeps full bullets (human source of truth); only this injected view is
8
+ * compressed, so compression applies even under the cap.
9
+ */
10
+ import { type Block, parse } from "./lesson-parse";
11
+
12
+ /**
13
+ * Newest bullets kept verbatim. Rationale: the most recent lessons are the ones
14
+ * still tied to live work and worth full narrative context. Real bullets run
15
+ * 0.5–1.5 kB each, so 10 (not 15) verbatim clears the >70% shrink target on the
16
+ * live file while still preserving the actively-relevant recent set whole.
17
+ */
18
+ export const RECENT_FULL = 10;
19
+
20
+ /** Max chars of a distilled rule — keeps every compressed bullet to one line. */
21
+ export const RULE_CAP = 200;
22
+
23
+ /**
24
+ * Min readable length of a distilled rule. The old "text after the LAST →" rule
25
+ * produced illegible stubs when a bullet ended on a short trailing arrow segment
26
+ * (e.g. `- [2026-07-02 00:02] lecture).`, 30 chars). Below this we fall back to
27
+ * the whole rule's first sentence rather than ship a meaningless fragment.
28
+ */
29
+ export const MIN_RULE = 40;
30
+
31
+ /** The `[YYYY-MM-DD HH:MM]` (or date-only) stamp of a bullet, "" if absent. */
32
+ function stamp(block: Block): string {
33
+ const m = (block.raw[0] ?? "").match(/\[(\d{4}-\d{2}-\d{2}(?:[ T]\d{2}:\d{2})?)\]/);
34
+ return m?.[1] ?? "";
35
+ }
36
+
37
+ /** Bullet text: raw lines joined, leading "- ", date stamp & TRIGGERS lines stripped. */
38
+ function bodyText(block: Block): string {
39
+ const kept = block.raw.filter((l) => !/^\s*\[TRIGGERS\s/.test(l));
40
+ return kept.join(" ").replace(/^-\s*/, "").replace(/\[\d{4}-\d{2}-\d{2}[^\]]*\]\s*/, "").trim();
41
+ }
42
+
43
+ /** First sentence of `s` (split on a period + whitespace), whole string if none. */
44
+ function firstSentence(s: string): string {
45
+ return (s.split(/(?<=\.)\s/)[0] ?? s).trim();
46
+ }
47
+
48
+ /**
49
+ * The bullet's `narrative → rule` delimiter: a SPACED arrow only. A GLUED arrow
50
+ * between tokens (e.g. `120s→300s`, `s→3`) is prose the author wrote, never a
51
+ * delimiter — matching on `→` alone chopped rules mid-token (bug: `300s) pensant
52
+ * corriger…`). Also used to split rule-internal clauses.
53
+ */
54
+ const RULE_ARROW = /\s+→\s+/;
55
+
56
+ /**
57
+ * Distil the actionable rule from a bullet body. With no spaced arrow the whole
58
+ * bullet is the rule → its first sentence. Otherwise the rule is everything after
59
+ * the FIRST spaced arrow; its spaced-arrow-delimited segments are kept whole
60
+ * except for TRAILING short asides (< {@link MIN_RULE} chars, e.g. `→ (cf.
61
+ * lecture).`) which are dropped — so an arrow used as PROSE inside a rule (`maps
62
+ * X → Y doit…`) is preserved intact rather than chopped at the arrow. When the
63
+ * kept rule is still under {@link MIN_RULE} chars, fall back to the first sentence
64
+ * of the WHOLE rule part (never the narrative), avoiding an illegible stub.
65
+ */
66
+ function distillRule(text: string): string {
67
+ const sep = text.search(RULE_ARROW);
68
+ if (sep < 0) return firstSentence(text);
69
+ const rulePart = text.slice(sep).replace(RULE_ARROW, "").trim();
70
+ const segments = rulePart.split(RULE_ARROW).map((s) => s.trim()).filter(Boolean);
71
+ while (segments.length > 1 && (segments[segments.length - 1]?.length ?? 0) < MIN_RULE) segments.pop();
72
+ const rule = firstSentence(segments.join(" → "));
73
+ return rule.length >= MIN_RULE ? rule : firstSentence(rulePart);
74
+ }
75
+
76
+ /** Collapse one older bullet to `- [date] <rule>`: {@link distillRule}, capped. */
77
+ function compressBullet(block: Block): string {
78
+ let rule = distillRule(bodyText(block));
79
+ if (rule.length > RULE_CAP) rule = `${rule.slice(0, RULE_CAP - 1).trimEnd()}…`;
80
+ const date = stamp(block);
81
+ return `- ${date ? `[${date}] ` : ""}${rule}`;
82
+ }
83
+
84
+ /**
85
+ * Build the compressed injection body for `content`. The preamble comments are
86
+ * dropped (format docs, noise for the reader); the `recentFull` newest bullets
87
+ * stay whole, every older bullet becomes one distilled rule-line.
88
+ * @param content - Raw LESSON.md text.
89
+ * @param recentFull - Count of newest bullets to keep verbatim.
90
+ * @returns The compressed block (bullets only), or the trimmed content when there are no bullets.
91
+ */
92
+ export function compressInjection(content: string, recentFull: number = RECENT_FULL): string {
93
+ const { blocks } = parse(content);
94
+ if (blocks.length === 0) return content.trim();
95
+ const full = blocks.slice(0, recentFull).map((b) => b.raw.join("\n"));
96
+ const rest = blocks.slice(recentFull).map(compressBullet);
97
+ return [...full, ...rest].join("\n");
98
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Shared, LLM-free primitives for LESSON.md curation: bullet parsing, timestamp
3
+ * extraction, tokenization/Jaccard (dedup), cited-path detection, and the
4
+ * cap/stale constants. Consumed by curate-lessons (dedup), lesson-archive
5
+ * (cap→archive) and lesson-inject (compressed SessionStart block) — one source
6
+ * of truth so the three stages can never disagree on what a "bullet" is.
7
+ */
8
+
9
+ /** Bullet count above which the oldest lessons are archived out of LESSON.md. */
10
+ export const CAP = 50;
11
+ /** Age (days) past which a lesson is eligible for archival / stale-flagging. */
12
+ export const STALE_DAYS = 90;
13
+ /** Milliseconds in a day. */
14
+ export const DAY_MS = 86400000;
15
+ /** Case-sensitive decision-time tag line (`[TRIGGERS …]`) — opus-lessons format. */
16
+ export const TRIG: RegExp = /^\[TRIGGERS\s+.+\]$/;
17
+
18
+ /** A bullet plus any continuation lines (e.g. a `[TRIGGERS …]` line). */
19
+ export interface Block { raw: string[]; ts: number; tokens: Set<string>; }
20
+
21
+ /** Epoch ms for a `[YYYY-MM-DD HH:MM]` stamp; `NaN` if absent or out of range. */
22
+ export function parseTs(line: string): number {
23
+ const m = line.match(/\[(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2}))?/);
24
+ if (!m) return NaN;
25
+ const mo = +(m[2] ?? 0), d = +(m[3] ?? 0);
26
+ if (mo < 1 || mo > 12 || d < 1 || d > 31) return NaN;
27
+ return Date.UTC(+(m[1] ?? 0), mo - 1, d, +(m[4] ?? 0), +(m[5] ?? 0));
28
+ }
29
+
30
+ /** Content words (>=4 chars), timestamp & TRIGGERS marker stripped. */
31
+ export function tokenize(text: string): Set<string> {
32
+ return new Set(
33
+ text.toLowerCase()
34
+ .replace(/\[triggers[^\]]*\]/g, " ")
35
+ .replace(/\[\d{4}-\d{2}-\d{2}[^\]]*\]/g, " ")
36
+ .replace(/[^a-z0-9àâäéèêëîïôöùûüç/._-]+/gi, " ")
37
+ .split(/\s+/).filter((t) => t.length >= 4),
38
+ );
39
+ }
40
+
41
+ /** Jaccard overlap of two token sets (0 when both empty). */
42
+ export function jaccard(a: Set<string>, b: Set<string>): number {
43
+ if (a.size === 0 && b.size === 0) return 0;
44
+ const inter = [...a].filter((t) => b.has(t)).length;
45
+ return inter / (a.size + b.size - inter);
46
+ }
47
+
48
+ /** Repo-relative cited paths (slash + extension) referenced in a block. */
49
+ export function citedPaths(text: string): string[] {
50
+ const out = new Set<string>();
51
+ for (const m of text.matchAll(/`([^`]+)`/g)) { if (m[1]) out.add(m[1]); }
52
+ for (const m of text.matchAll(/[\w./@-]+\.\w{1,5}/g)) { if (m[0]) out.add(m[0]); }
53
+ return [...out].filter((p) => p.includes("/") && /\.\w{1,5}$/.test(p));
54
+ }
55
+
56
+ /** True when a block carries a `[TRIGGERS …]` continuation line. */
57
+ export function hasTrigger(b: Block): boolean {
58
+ return b.raw.some((l) => TRIG.test(l.trim()));
59
+ }
60
+
61
+ /** Split content into a verbatim preamble and one Block per `- ` bullet. */
62
+ export function parse(content: string): { preamble: string; blocks: Block[] } {
63
+ const lines = content.split("\n");
64
+ const blocks: Block[] = [];
65
+ let i = 0;
66
+ while (i < lines.length && !/^-\s/.test(lines[i] ?? "")) i++;
67
+ const preamble = lines.slice(0, i).join("\n");
68
+ for (; i < lines.length; i++) {
69
+ const l = lines[i] ?? "", last = blocks[blocks.length - 1];
70
+ if (/^-\s/.test(l)) blocks.push({ raw: [l], ts: parseTs(l), tokens: tokenize(l) });
71
+ else if (l.trim() && last) last.raw.push(l);
72
+ }
73
+ return { preamble, blocks };
74
+ }