@fusengine/harness 0.1.90 → 0.1.91

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 (417) 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-C43gA-Pr.mjs} +701 -134
  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-BjG6unTj.mjs +317 -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 +27 -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.ts +46 -0
  44. package/src/adapters/cursor/events.ts +38 -0
  45. package/src/adapters/cursor/index.ts +43 -0
  46. package/src/adapters/cursor/interfaces/types.ts +88 -0
  47. package/src/adapters/cursor/native-response.ts +158 -0
  48. package/src/adapters/cursor/normalize.ts +123 -0
  49. package/src/adapters/cursor/respond.ts +123 -0
  50. package/src/adapters/gemini/index.ts +36 -0
  51. package/src/adapters/hermes/index.ts +55 -0
  52. package/src/adapters/hermes/interfaces/types.ts +26 -0
  53. package/src/adapters/kimi/index.ts +96 -0
  54. package/src/adapters/kimi/interfaces/types.ts +55 -0
  55. package/src/cache/compact.ts +55 -0
  56. package/src/cache/index.ts +5 -0
  57. package/src/cache/io.ts +38 -0
  58. package/src/cache/mcp-response.ts +39 -0
  59. package/src/cache/mcp-store.ts +70 -0
  60. package/src/cache/store.ts +93 -0
  61. package/src/changelog/fetch.ts +79 -0
  62. package/src/cli/bin.ts +100 -0
  63. package/src/cli/cursor-event-scanner.ts +175 -0
  64. package/src/cli/cursor-stdin-reader.ts +65 -0
  65. package/src/cli/doctor.ts +96 -0
  66. package/src/cli/hook-io.ts +130 -0
  67. package/src/cli/hook-sound.ts +49 -0
  68. package/src/cli/index.ts +1 -0
  69. package/src/cli/json-primitive-scanner.ts +83 -0
  70. package/src/cli/run.ts +36 -0
  71. package/src/cli/scope.ts +31 -0
  72. package/src/codex-rules/build.ts +63 -0
  73. package/src/codex-rules/index.ts +8 -0
  74. package/src/codex-rules/mapping.ts +42 -0
  75. package/src/codex-rules/rules/absolute-paths.ts +52 -0
  76. package/src/codex-rules/rules/ask.ts +44 -0
  77. package/src/codex-rules/rules/chmod-ask.ts +57 -0
  78. package/src/codex-rules/rules/dd.ts +23 -0
  79. package/src/codex-rules/rules/diskutil.ts +33 -0
  80. package/src/codex-rules/rules/mkfs.ts +42 -0
  81. package/src/codex-rules/rules/privilege.ts +49 -0
  82. package/src/codex-rules/rules/remote-fetch.ts +31 -0
  83. package/src/codex-rules/rules/rm-variants.ts +58 -0
  84. package/src/codex-rules/rules/single-commands.ts +57 -0
  85. package/src/codex-rules/skip-list.ts +25 -0
  86. package/src/codex-rules/starlark.ts +46 -0
  87. package/src/codex-rules/types.ts +36 -0
  88. package/src/config/dotenv.ts +55 -0
  89. package/src/config/env.ts +10 -0
  90. package/src/config/home-dir.ts +36 -0
  91. package/src/config/index.ts +5 -0
  92. package/src/config/layout.ts +52 -0
  93. package/src/config/limits.ts +56 -0
  94. package/src/config/ttl.ts +30 -0
  95. package/src/detect/harness.ts +73 -0
  96. package/src/detect/index.ts +1 -0
  97. package/src/detect/interfaces/types.ts +18 -0
  98. package/src/freshness/agent-evidence-record.ts +95 -0
  99. package/src/freshness/agent-evidence.ts +95 -0
  100. package/src/freshness/codex-spawn-evidence.ts +91 -0
  101. package/src/freshness/doc-helpers.ts +90 -0
  102. package/src/freshness/evidence-harvest-io.ts +72 -0
  103. package/src/freshness/evidence-harvest.ts +61 -0
  104. package/src/freshness/explore-tools.ts +95 -0
  105. package/src/freshness/index.ts +2 -0
  106. package/src/freshness/query-framework.ts +73 -0
  107. package/src/freshness/ref-evidence.ts +49 -0
  108. package/src/freshness/ref-journal.ts +98 -0
  109. package/src/freshness/trivial-edit-counter.ts +30 -0
  110. package/src/index.ts +11 -0
  111. package/src/init/index.ts +2 -0
  112. package/src/init/run.ts +32 -0
  113. package/src/init/templates.ts +75 -0
  114. package/src/memory/gitignore.ts +14 -0
  115. package/src/memory/index.ts +3 -0
  116. package/src/memory/registry.ts +39 -0
  117. package/src/memory/session-roots.ts +99 -0
  118. package/src/memory/state.ts +58 -0
  119. package/src/policy/apex-authorization.ts +100 -0
  120. package/src/policy/apex-gates.ts +91 -0
  121. package/src/policy/apex-target-steps.ts +73 -0
  122. package/src/policy/apex-target.ts +49 -0
  123. package/src/policy/apex-task-context.ts +81 -0
  124. package/src/policy/apex.ts +94 -0
  125. package/src/policy/cartographer/build-tree.ts +69 -0
  126. package/src/policy/cartographer/describe.ts +53 -0
  127. package/src/policy/cartographer/entry.ts +38 -0
  128. package/src/policy/cartographer/frontmatter.ts +48 -0
  129. package/src/policy/cartographer/indicators.ts +34 -0
  130. package/src/policy/claude-md-context.ts +66 -0
  131. package/src/policy/conventions/index.ts +11 -0
  132. package/src/policy/conventions/interfaces.ts +71 -0
  133. package/src/policy/conventions/langs.ts +53 -0
  134. package/src/policy/conventions/query.ts +49 -0
  135. package/src/policy/conventions/react-hooks.ts +18 -0
  136. package/src/policy/conventions/store-or-query.ts +21 -0
  137. package/src/policy/conventions/stores.ts +56 -0
  138. package/src/policy/conventions/strip.ts +87 -0
  139. package/src/policy/conventions/verdict.ts +38 -0
  140. package/src/policy/creation-intent.ts +11 -0
  141. package/src/policy/deny-loop.ts +88 -0
  142. package/src/policy/design/allowed-write.ts +28 -0
  143. package/src/policy/design/content-checks.ts +45 -0
  144. package/src/policy/design/corpus-fs.ts +21 -0
  145. package/src/policy/design/corpus-resolve-cache.ts +49 -0
  146. package/src/policy/design/corpus-resolve.ts +99 -0
  147. package/src/policy/design/corpus.ts +92 -0
  148. package/src/policy/design/design-system-rules.ts +45 -0
  149. package/src/policy/design/flag.ts +30 -0
  150. package/src/policy/design/gates-pipeline.ts +105 -0
  151. package/src/policy/design/gates.ts +87 -0
  152. package/src/policy/design/screenshot-tools.ts +24 -0
  153. package/src/policy/design/skill-evidence.ts +34 -0
  154. package/src/policy/design/skill-gate.ts +81 -0
  155. package/src/policy/design/skill-triggers.ts +51 -0
  156. package/src/policy/design/state.ts +92 -0
  157. package/src/policy/design/template-urls.ts +54 -0
  158. package/src/policy/design/transitions.ts +68 -0
  159. package/src/policy/detect-claude-md-project-type.ts +32 -0
  160. package/src/policy/detect-framework.ts +99 -0
  161. package/src/policy/detect-primitive-checks.ts +100 -0
  162. package/src/policy/detect-primitive-lib.ts +55 -0
  163. package/src/policy/detect-project.ts +99 -0
  164. package/src/policy/edit-outcome.ts +53 -0
  165. package/src/policy/evaluate.ts +81 -0
  166. package/src/policy/expert-agents.ts +60 -0
  167. package/src/policy/file-size-scope.ts +32 -0
  168. package/src/policy/file-size.ts +99 -0
  169. package/src/policy/framework-solid-exclude.ts +52 -0
  170. package/src/policy/framework-solid-extended.ts +93 -0
  171. package/src/policy/framework-solid-gates-systems.ts +99 -0
  172. package/src/policy/framework-solid-gates.ts +98 -0
  173. package/src/policy/framework-solid.ts +88 -0
  174. package/src/policy/gemini-mcp-gate.ts +83 -0
  175. package/src/policy/git-gates.ts +40 -0
  176. package/src/policy/guards/bash-command-anchor.ts +40 -0
  177. package/src/policy/guards/bash-write-patterns.ts +140 -0
  178. package/src/policy/guards/bash-write-redirects.ts +201 -0
  179. package/src/policy/guards/bash-write-safe-paths.ts +62 -0
  180. package/src/policy/guards/bash-write.ts +76 -0
  181. package/src/policy/guards/context.ts +12 -0
  182. package/src/policy/guards/index.ts +61 -0
  183. package/src/policy/guards/install.ts +22 -0
  184. package/src/policy/guards/interface-separation-ext.ts +41 -0
  185. package/src/policy/guards/interface-separation.ts +100 -0
  186. package/src/policy/guards/protected-path.ts +100 -0
  187. package/src/policy/guards/security.ts +97 -0
  188. package/src/policy/index.ts +19 -0
  189. package/src/policy/interfaces/types.ts +46 -0
  190. package/src/policy/js-gate-route.ts +51 -0
  191. package/src/policy/lessons/lesson-gate.ts +0 -0
  192. package/src/policy/lessons/trigger-index.ts +84 -0
  193. package/src/policy/lessons/types.ts +31 -0
  194. package/src/policy/module-layout.ts +31 -0
  195. package/src/policy/nearest-manifest.ts +75 -0
  196. package/src/policy/never-approval.ts +95 -0
  197. package/src/policy/patterns.ts +80 -0
  198. package/src/policy/seo/validate.ts +51 -0
  199. package/src/policy/shadcn-project.ts +22 -0
  200. package/src/policy/shadcn-skill-gate.ts +97 -0
  201. package/src/policy/shell-read-refs.ts +96 -0
  202. package/src/policy/skill-path.ts +46 -0
  203. package/src/policy/skill-patterns/laravel.ts +40 -0
  204. package/src/policy/skill-patterns/nextjs.ts +29 -0
  205. package/src/policy/skill-patterns/react.ts +27 -0
  206. package/src/policy/skill-patterns/shadcn-subskills.ts +33 -0
  207. package/src/policy/skill-patterns/shadcn.ts +66 -0
  208. package/src/policy/skill-patterns/swift.ts +30 -0
  209. package/src/policy/skill-patterns/tailwind.ts +40 -0
  210. package/src/policy/skill-source.ts +52 -0
  211. package/src/policy/skill-trigger-patterns.ts +33 -0
  212. package/src/policy/skill-triggers.ts +108 -0
  213. package/src/policy/tailwind-skill-gate.ts +63 -0
  214. package/src/policy/trivial-edits.ts +52 -0
  215. package/src/policy/verbosity.ts +39 -0
  216. package/src/prompt/index.ts +1 -0
  217. package/src/prompt/types.ts +36 -0
  218. package/src/refs/discover.ts +69 -0
  219. package/src/refs/frontmatter.ts +24 -0
  220. package/src/refs/index.ts +4 -0
  221. package/src/refs/loader.ts +59 -0
  222. package/src/refs/ref-key.ts +41 -0
  223. package/src/refs/router.ts +63 -0
  224. package/src/refs/types.ts +25 -0
  225. package/src/runtime/activity.ts +99 -0
  226. package/src/runtime/apply-patch-gate.ts +41 -0
  227. package/src/runtime/burst-window.ts +21 -0
  228. package/src/runtime/codex-shell-tool.ts +37 -0
  229. package/src/runtime/command-string.ts +37 -0
  230. package/src/runtime/confirm/codex-confirm.ts +126 -0
  231. package/src/runtime/confirm/codex-prompt-origin.ts +16 -0
  232. package/src/runtime/confirm/confirm-code.ts +33 -0
  233. package/src/runtime/confirm/confirm-gate.ts +65 -0
  234. package/src/runtime/confirm/confirm-irreversible.ts +32 -0
  235. package/src/runtime/confirm/confirm-pending.ts +34 -0
  236. package/src/runtime/confirm/confirm-state.ts +56 -0
  237. package/src/runtime/confirm/confirm-subagent.ts +112 -0
  238. package/src/runtime/confirm/confirm-submit.ts +38 -0
  239. package/src/runtime/core-guards-active.ts +84 -0
  240. package/src/runtime/core-guards-wired.ts +47 -0
  241. package/src/runtime/deny-loop-store.ts +94 -0
  242. package/src/runtime/deny-notice.ts +52 -0
  243. package/src/runtime/design-cache-resolve.ts +51 -0
  244. package/src/runtime/design-content-gate.ts +95 -0
  245. package/src/runtime/design-files-gate.ts +114 -0
  246. package/src/runtime/design-helpers.ts +93 -0
  247. package/src/runtime/design-lifecycle.ts +39 -0
  248. package/src/runtime/design-read-credit.ts +28 -0
  249. package/src/runtime/design.ts +116 -0
  250. package/src/runtime/dev-context.ts +43 -0
  251. package/src/runtime/dry-patterns.ts +32 -0
  252. package/src/runtime/dry.ts +96 -0
  253. package/src/runtime/fragment-registry.ts +33 -0
  254. package/src/runtime/framework-skill-gate.ts +79 -0
  255. package/src/runtime/fs-cleanup.ts +71 -0
  256. package/src/runtime/gate-apex.ts +81 -0
  257. package/src/runtime/gate-helpers.ts +52 -0
  258. package/src/runtime/gate-input.ts +26 -0
  259. package/src/runtime/gate.ts +128 -0
  260. package/src/runtime/handle-post.ts +104 -0
  261. package/src/runtime/handle-pre.ts +137 -0
  262. package/src/runtime/handle-scope-async.ts +29 -0
  263. package/src/runtime/handle-types.ts +27 -0
  264. package/src/runtime/handle.ts +116 -0
  265. package/src/runtime/home-state.ts +55 -0
  266. package/src/runtime/index.ts +18 -0
  267. package/src/runtime/inform.ts +28 -0
  268. package/src/runtime/inject-budget-recap.ts +47 -0
  269. package/src/runtime/inject-budget.ts +81 -0
  270. package/src/runtime/inject-context.ts +85 -0
  271. package/src/runtime/inject-dedup-exclusive.ts +98 -0
  272. package/src/runtime/inject-dedup.ts +89 -0
  273. package/src/runtime/is-agent-tool.ts +19 -0
  274. package/src/runtime/lifecycle/agent-files.ts +49 -0
  275. package/src/runtime/lifecycle/agent-memory.ts +82 -0
  276. package/src/runtime/lifecycle/agent-transcript.ts +80 -0
  277. package/src/runtime/lifecycle/aipilot/analytics.ts +99 -0
  278. package/src/runtime/lifecycle/aipilot/apex-task-store.ts +61 -0
  279. package/src/runtime/lifecycle/aipilot/cache-base.ts +48 -0
  280. package/src/runtime/lifecycle/aipilot/cache-doc.ts +100 -0
  281. package/src/runtime/lifecycle/aipilot/cache-lessons.ts +58 -0
  282. package/src/runtime/lifecycle/aipilot/cache-test.ts +70 -0
  283. package/src/runtime/lifecycle/aipilot/curate-lessons.ts +64 -0
  284. package/src/runtime/lifecycle/aipilot/dispatch-aipilot.ts +100 -0
  285. package/src/runtime/lifecycle/aipilot/doc-cache-gate.ts +69 -0
  286. package/src/runtime/lifecycle/aipilot/inject-apex.ts +98 -0
  287. package/src/runtime/lifecycle/aipilot/inject-doc.ts +62 -0
  288. package/src/runtime/lifecycle/aipilot/inject-explore.ts +64 -0
  289. package/src/runtime/lifecycle/aipilot/inject-lessons.ts +61 -0
  290. package/src/runtime/lifecycle/aipilot/inject-test.ts +48 -0
  291. package/src/runtime/lifecycle/aipilot/lesson-archive.ts +60 -0
  292. package/src/runtime/lifecycle/aipilot/lesson-inject.ts +98 -0
  293. package/src/runtime/lifecycle/aipilot/lesson-parse.ts +74 -0
  294. package/src/runtime/lifecycle/aipilot/lessons.ts +72 -0
  295. package/src/runtime/lifecycle/aipilot/promote-global-lessons.ts +92 -0
  296. package/src/runtime/lifecycle/aipilot/solid-compliance.ts +69 -0
  297. package/src/runtime/lifecycle/aipilot/solid-notice.ts +47 -0
  298. package/src/runtime/lifecycle/aipilot/solid-transcript.ts +60 -0
  299. package/src/runtime/lifecycle/aipilot/source-scan.ts +56 -0
  300. package/src/runtime/lifecycle/aipilot/sync-task.ts +75 -0
  301. package/src/runtime/lifecycle/aipilot/transcript.ts +88 -0
  302. package/src/runtime/lifecycle/aipilot/types.ts +72 -0
  303. package/src/runtime/lifecycle/auto-document-reads.ts +73 -0
  304. package/src/runtime/lifecycle/cartographer/detect.ts +57 -0
  305. package/src/runtime/lifecycle/cartographer/ecosystem-map.ts +56 -0
  306. package/src/runtime/lifecycle/cartographer/fs-util.ts +64 -0
  307. package/src/runtime/lifecycle/cartographer/merge.ts +51 -0
  308. package/src/runtime/lifecycle/cartographer/project-map.ts +49 -0
  309. package/src/runtime/lifecycle/cartographer/scan-hooks.ts +48 -0
  310. package/src/runtime/lifecycle/cartographer/scan.ts +81 -0
  311. package/src/runtime/lifecycle/cartographer/session-start.ts +33 -0
  312. package/src/runtime/lifecycle/cartographer/track-enrichment.ts +45 -0
  313. package/src/runtime/lifecycle/cartographer/write-plugin-map.ts +43 -0
  314. package/src/runtime/lifecycle/cartographer/write-tree.ts +44 -0
  315. package/src/runtime/lifecycle/changelog-research.ts +43 -0
  316. package/src/runtime/lifecycle/check-file-size.ts +61 -0
  317. package/src/runtime/lifecycle/codex-resync/fingerprint.ts +63 -0
  318. package/src/runtime/lifecycle/codex-resync/lock.ts +46 -0
  319. package/src/runtime/lifecycle/codex-resync/managed-destination.ts +39 -0
  320. package/src/runtime/lifecycle/codex-resync/materialize-agents.ts +57 -0
  321. package/src/runtime/lifecycle/codex-resync/plugin-files.ts +65 -0
  322. package/src/runtime/lifecycle/codex-resync/plugin-roots.ts +62 -0
  323. package/src/runtime/lifecycle/codex-resync/resync.ts +45 -0
  324. package/src/runtime/lifecycle/codex-resync/symlink-commands.ts +25 -0
  325. package/src/runtime/lifecycle/dispatch.ts +109 -0
  326. package/src/runtime/lifecycle/failure-lesson.ts +51 -0
  327. package/src/runtime/lifecycle/index.ts +34 -0
  328. package/src/runtime/lifecycle/inject-rules.ts +50 -0
  329. package/src/runtime/lifecycle/instructions-loaded.ts +22 -0
  330. package/src/runtime/lifecycle/kimi-rules-native.ts +44 -0
  331. package/src/runtime/lifecycle/lessons/dispatch.ts +88 -0
  332. package/src/runtime/lifecycle/lessons/reminder.ts +89 -0
  333. package/src/runtime/lifecycle/lessons/state.ts +23 -0
  334. package/src/runtime/lifecycle/memory/agent-lesson.ts +35 -0
  335. package/src/runtime/lifecycle/memory/capture-error.ts +41 -0
  336. package/src/runtime/lifecycle/memory/client.ts +65 -0
  337. package/src/runtime/lifecycle/memory/dispatch.ts +42 -0
  338. package/src/runtime/lifecycle/memory/recall.ts +30 -0
  339. package/src/runtime/lifecycle/memory/salience.ts +31 -0
  340. package/src/runtime/lifecycle/memory/state.ts +49 -0
  341. package/src/runtime/lifecycle/memory/track-ops.ts +15 -0
  342. package/src/runtime/lifecycle/post-compact.ts +38 -0
  343. package/src/runtime/lifecycle/post-edit-ts.ts +49 -0
  344. package/src/runtime/lifecycle/post-tracking.ts +40 -0
  345. package/src/runtime/lifecycle/pre-compact.ts +38 -0
  346. package/src/runtime/lifecycle/rules-root.ts +83 -0
  347. package/src/runtime/lifecycle/security/check-skill.ts +63 -0
  348. package/src/runtime/lifecycle/security/scan-patterns.ts +53 -0
  349. package/src/runtime/lifecycle/security/scan.ts +76 -0
  350. package/src/runtime/lifecycle/security/skill-state.ts +47 -0
  351. package/src/runtime/lifecycle/security/track-mcp.ts +29 -0
  352. package/src/runtime/lifecycle/security/track-skill-read.ts +31 -0
  353. package/src/runtime/lifecycle/seo/post-tool-use.ts +45 -0
  354. package/src/runtime/lifecycle/session-end.ts +17 -0
  355. package/src/runtime/lifecycle/session-start.ts +57 -0
  356. package/src/runtime/lifecycle/snapshot/board.ts +27 -0
  357. package/src/runtime/lifecycle/snapshot/format.ts +60 -0
  358. package/src/runtime/lifecycle/snapshot/git.ts +71 -0
  359. package/src/runtime/lifecycle/snapshot/index.ts +52 -0
  360. package/src/runtime/lifecycle/snapshot/version.ts +34 -0
  361. package/src/runtime/lifecycle/solid-detect.ts +56 -0
  362. package/src/runtime/lifecycle/stop-core.ts +35 -0
  363. package/src/runtime/lifecycle/subagent-cache.ts +61 -0
  364. package/src/runtime/lifecycle/task-completed.ts +94 -0
  365. package/src/runtime/lifecycle/teammate-idle-check.ts +63 -0
  366. package/src/runtime/lifecycle/teammate-idle.ts +28 -0
  367. package/src/runtime/lifecycle/tool-failure.ts +24 -0
  368. package/src/runtime/lifecycle/track-changes.ts +61 -0
  369. package/src/runtime/lifecycle/validate-solid.ts +70 -0
  370. package/src/runtime/lifecycle/validate-tailwind.ts +80 -0
  371. package/src/runtime/lifecycle-bridge.ts +74 -0
  372. package/src/runtime/mcp-key.ts +44 -0
  373. package/src/runtime/mcp-tool-name.ts +92 -0
  374. package/src/runtime/mcp.ts +91 -0
  375. package/src/runtime/modular.ts +77 -0
  376. package/src/runtime/normalize.ts +107 -0
  377. package/src/runtime/notices.ts +91 -0
  378. package/src/runtime/notification-sound.ts +49 -0
  379. package/src/runtime/notifications.ts +77 -0
  380. package/src/runtime/paths.ts +59 -0
  381. package/src/runtime/post-fanout.ts +41 -0
  382. package/src/runtime/post-outcome.ts +61 -0
  383. package/src/runtime/pre-allow.ts +97 -0
  384. package/src/runtime/precommit.ts +60 -0
  385. package/src/runtime/prompt-text.ts +38 -0
  386. package/src/runtime/receipt-capture.ts +34 -0
  387. package/src/runtime/record.ts +26 -0
  388. package/src/runtime/respond.ts +92 -0
  389. package/src/runtime/solid-file-size-gate.ts +58 -0
  390. package/src/runtime/solid-pre.ts +53 -0
  391. package/src/runtime/storage.ts +9 -0
  392. package/src/statusline/colors.ts +31 -0
  393. package/src/statusline/constants.ts +17 -0
  394. package/src/statusline/formatters.ts +54 -0
  395. package/src/statusline/index.ts +4 -0
  396. package/src/statusline/progress-bar.ts +46 -0
  397. package/src/tracking/codex-post-failure.ts +73 -0
  398. package/src/tracking/index.ts +2 -0
  399. package/src/tracking/integrity.ts +99 -0
  400. package/src/tracking/one-shot-dedup.ts +34 -0
  401. package/src/tracking/one-shot-failure.ts +42 -0
  402. package/src/tracking/one-shot-store.ts +95 -0
  403. package/src/tracking/one-shot.ts +93 -0
  404. package/src/tracking/receipts.ts +95 -0
  405. package/src/tracking/session-state.ts +94 -0
  406. package/src/tracking/store.ts +96 -0
  407. package/src/tracking/track-compact.ts +100 -0
  408. package/src/tracking/track-diff.ts +50 -0
  409. package/src/tracking/track-journal.ts +97 -0
  410. package/src/tracking/track-lock-sync.ts +83 -0
  411. package/src/tracking/track-lock.ts +67 -0
  412. package/src/util/compact-json.ts +23 -0
  413. package/src/util/index.ts +2 -0
  414. package/src/util/json-io.ts +47 -0
  415. package/src/util/project-root.ts +41 -0
  416. package/src/util/runtime-io.ts +85 -0
  417. package/src/util/semver.ts +30 -0
@@ -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
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Lesson aggregation, dedup, merge, and edit categorization for the ai-pilot
3
+ * scope. Ported from the ai-pilot plugin's `cache/lesson-aggregator.ts` +
4
+ * `cache/lesson-helpers.ts` (now removed).
5
+ */
6
+ import { existsSync, readdirSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { homedir } from "node:os";
9
+ import { readJsonFile } from "../../../util/json-io";
10
+ import { cacheBaseDir } from "./cache-base";
11
+ import type { EditEntry, LessonEntry } from "./types";
12
+
13
+ /** Deduplicate lessons by `error_type:pattern`, summing counts. */
14
+ export function dedupLessons(lessons: LessonEntry[]): LessonEntry[] {
15
+ const groups = new Map<string, LessonEntry[]>();
16
+ for (const l of lessons) {
17
+ const key = `${l.error_type}:${l.pattern}`;
18
+ const group = groups.get(key);
19
+ if (group) group.push(l); else groups.set(key, [l]);
20
+ }
21
+ return [...groups.values()].map((entries) => {
22
+ const first = entries[0] as LessonEntry;
23
+ return {
24
+ error_type: first.error_type,
25
+ pattern: first.pattern,
26
+ fix: first.fix,
27
+ last_seen: first.last_seen,
28
+ count: entries.reduce((s, e) => s + e.count, 0),
29
+ files: [...new Set(entries.flatMap((e) => e.files))],
30
+ code: { line: [...new Set(entries.flatMap((e) => e.code?.line ?? []))].slice(0, 5) },
31
+ };
32
+ }).sort((a, b) => b.count - a.count);
33
+ }
34
+
35
+ /** Aggregate all local lesson JSON files into a flat deduplicated list. */
36
+ export async function aggregateLocalLessons(cacheDir: string): Promise<LessonEntry[]> {
37
+ if (!existsSync(cacheDir)) return [];
38
+ const files = readdirSync(cacheDir).filter((f) => f.endsWith(".json"));
39
+ const all: LessonEntry[] = [];
40
+ for (const f of files) {
41
+ const data = await readJsonFile<{ errors?: LessonEntry[] }>(join(cacheDir, f));
42
+ if (data?.errors) all.push(...data.errors);
43
+ }
44
+ return dedupLessons(all);
45
+ }
46
+
47
+ /** Load global lessons for a stack (stack-specific + universal). */
48
+ export async function loadGlobalLessons(stack: string, home: string = homedir()): Promise<LessonEntry[]> {
49
+ const globalDir = join(cacheBaseDir(home), "lessons", "_global");
50
+ const result: LessonEntry[] = [];
51
+ for (const name of [`${stack}.json`, "universal.json"]) {
52
+ const data = await readJsonFile<LessonEntry[]>(join(globalDir, name));
53
+ if (data) result.push(...data);
54
+ }
55
+ return result;
56
+ }
57
+
58
+ /** Merge local + global lessons, dedup by type+pattern, sort by count desc. */
59
+ export function mergeLessons(local: LessonEntry[], global: LessonEntry[]): LessonEntry[] {
60
+ return dedupLessons([...local, ...global]);
61
+ }
62
+
63
+ /** Categorize an edit entry by analyzing the new code content. */
64
+ export function categorizeEdit(edit: EditEntry): string {
65
+ const n = edit.newStr.toLowerCase();
66
+ if (n.includes("use client")) return "missing_directive";
67
+ if (n.includes("displayname")) return "missing_display_name";
68
+ if (/onkeydown|tabindex|role=/.test(n)) return "missing_a11y";
69
+ if (/try|catch/.test(n)) return "missing_error_handling";
70
+ if (/\?\?|if.*null/.test(n)) return "null_safety";
71
+ return "code_fix";
72
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Promote high-frequency project lessons (occurrence count >= 3) to the global,
3
+ * stack-scoped cache (`cache/lessons/_global/<stack>.json`, cap 25).
4
+ * Ports the ai-pilot `promote-global-lessons.ts` (a spawned subprocess) as an
5
+ * in-process call from `cacheSniperLessons`. Pure side-effect.
6
+ */
7
+ import { homedir } from "node:os";
8
+ import { mkdirSync, readdirSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { readJsonFile, writeJsonFile } from "../../../util/json-io";
11
+ import { cacheBaseDir } from "./cache-base";
12
+ import { logCacheEvent } from "./analytics";
13
+ import type { LessonEntry } from "./types";
14
+
15
+ /** A promoted global lesson tagged with the project hashes that produced it. */
16
+ export interface GlobalLesson extends LessonEntry {
17
+ source_projects: string[];
18
+ }
19
+
20
+ const MIN_COUNT = 3;
21
+ const MAX_GLOBAL = 25;
22
+
23
+ /** Group lessons by `error_type:pattern`. */
24
+ function groupByKey<T extends LessonEntry>(lessons: T[]): T[][] {
25
+ const groups = new Map<string, T[]>();
26
+ for (const l of lessons) {
27
+ const key = `${l.error_type}:${l.pattern}`;
28
+ (groups.get(key) ?? groups.set(key, []).get(key)!).push(l);
29
+ }
30
+ return [...groups.values()];
31
+ }
32
+
33
+ /** Read every project lesson file's `errors` into one flat (un-deduped) list. */
34
+ async function aggregateErrors(cacheDir: string): Promise<LessonEntry[]> {
35
+ let files: string[];
36
+ try { files = readdirSync(cacheDir).filter((f) => f.endsWith(".json")); } catch { return []; }
37
+ const all: LessonEntry[] = [];
38
+ for (const f of files) {
39
+ const data = await readJsonFile<{ errors?: LessonEntry[] }>(join(cacheDir, f));
40
+ if (data?.errors) all.push(...data.errors);
41
+ }
42
+ return all;
43
+ }
44
+
45
+ /** Keep groups seen >= MIN_COUNT times, collapsed to a single lesson each. */
46
+ function filterFrequent(lessons: LessonEntry[]): LessonEntry[] {
47
+ return groupByKey(lessons).filter((e) => e.length >= MIN_COUNT).map((entries) => {
48
+ const first = entries[0] as LessonEntry;
49
+ return {
50
+ error_type: first.error_type, pattern: first.pattern, fix: first.fix, last_seen: first.last_seen,
51
+ count: entries.length,
52
+ files: [...new Set(entries.flatMap((e) => e.files))],
53
+ code: { line: [...new Set(entries.flatMap((e) => e.code?.line ?? []))].slice(0, 5) },
54
+ };
55
+ });
56
+ }
57
+
58
+ /** Merge tagged candidates into existing global lessons; dedup, sum, sort, cap. */
59
+ function mergeGlobal(existing: GlobalLesson[], candidates: LessonEntry[], projHash: string): GlobalLesson[] {
60
+ const tagged: GlobalLesson[] = candidates.map((c) => ({ ...c, source_projects: [projHash] }));
61
+ return groupByKey([...existing, ...tagged]).map((entries) => {
62
+ const first = entries[0] as GlobalLesson;
63
+ return {
64
+ error_type: first.error_type, pattern: first.pattern, fix: first.fix, last_seen: first.last_seen,
65
+ count: entries.reduce((sum, e) => sum + e.count, 0),
66
+ files: [...new Set(entries.flatMap((e) => e.files))],
67
+ code: { line: [...new Set(entries.flatMap((e) => e.code?.line ?? []))].slice(0, 5) },
68
+ source_projects: [...new Set(entries.flatMap((e) => e.source_projects ?? []))],
69
+ };
70
+ }).sort((a, b) => b.count - a.count).slice(0, MAX_GLOBAL);
71
+ }
72
+
73
+ /**
74
+ * Promote high-frequency lessons from `cacheDir` to the global stack file.
75
+ * @param cacheDir - Per-project lessons cache dir to scan.
76
+ * @param stack - Detected project stack (`nextjs`, `laravel`, …, `universal`).
77
+ * @param projHash - Source project hash (tags promoted entries).
78
+ * @param home - Home dir (defaults to `~`).
79
+ */
80
+ export async function promoteGlobalLessons(cacheDir: string, stack: string, projHash: string, home: string = homedir()): Promise<void> {
81
+ const candidates = filterFrequent(await aggregateErrors(cacheDir));
82
+ if (candidates.length === 0) return;
83
+
84
+ const globalDir = join(cacheBaseDir(home), "lessons", "_global");
85
+ mkdirSync(globalDir, { recursive: true });
86
+ const globalFile = join(globalDir, `${stack}.json`);
87
+ const existing = (await readJsonFile<GlobalLesson[]>(globalFile)) ?? [];
88
+ const merged = mergeGlobal(existing, candidates, projHash);
89
+
90
+ await writeJsonFile(globalFile, merged, true);
91
+ logCacheEvent("lessons", "promoted", projHash, { count: merged.length, stack }, home);
92
+ }