@fusengine/harness 0.1.89 → 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 (419) 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 +5 -4
  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-DHhukRdh.mjs → handle-C43gA-Pr.mjs} +737 -150
  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/permission-mode-B-SFyR0X.mjs +226 -0
  29. package/dist/policy/index.d.mts +1 -1
  30. package/dist/policy/index.mjs +2 -2
  31. package/dist/prompt/index.d.mts +1 -1
  32. package/dist/{run-CdIlUtjv.mjs → run-DkrzC0gb.mjs} +1 -1
  33. package/dist/{run-TAXNRSpD.mjs → run-GHAMqcOn.mjs} +26 -3
  34. package/dist/runtime/index.d.mts +27 -16
  35. package/dist/runtime/index.mjs +2 -2
  36. package/dist/{types-C0-igRKr.d.mts → types-Bh0jEF3_.d.mts} +2 -0
  37. package/dist/{validate-DZFtAbLP.mjs → validate-KjZ1X9tH.mjs} +1 -1
  38. package/package.json +3 -2
  39. package/src/adapters/claude/index.ts +92 -0
  40. package/src/adapters/cline/index.ts +36 -0
  41. package/src/adapters/codex/apply-patch.ts +94 -0
  42. package/src/adapters/codex/index.ts +84 -0
  43. package/src/adapters/codex/permission-mode.ts +16 -0
  44. package/src/adapters/cursor/context.ts +46 -0
  45. package/src/adapters/cursor/events.ts +38 -0
  46. package/src/adapters/cursor/index.ts +43 -0
  47. package/src/adapters/cursor/interfaces/types.ts +88 -0
  48. package/src/adapters/cursor/native-response.ts +158 -0
  49. package/src/adapters/cursor/normalize.ts +123 -0
  50. package/src/adapters/cursor/respond.ts +123 -0
  51. package/src/adapters/gemini/index.ts +36 -0
  52. package/src/adapters/hermes/index.ts +55 -0
  53. package/src/adapters/hermes/interfaces/types.ts +26 -0
  54. package/src/adapters/kimi/index.ts +96 -0
  55. package/src/adapters/kimi/interfaces/types.ts +55 -0
  56. package/src/cache/compact.ts +55 -0
  57. package/src/cache/index.ts +5 -0
  58. package/src/cache/io.ts +38 -0
  59. package/src/cache/mcp-response.ts +39 -0
  60. package/src/cache/mcp-store.ts +70 -0
  61. package/src/cache/store.ts +93 -0
  62. package/src/changelog/fetch.ts +79 -0
  63. package/src/cli/bin.ts +100 -0
  64. package/src/cli/cursor-event-scanner.ts +175 -0
  65. package/src/cli/cursor-stdin-reader.ts +65 -0
  66. package/src/cli/doctor.ts +96 -0
  67. package/src/cli/hook-io.ts +130 -0
  68. package/src/cli/hook-sound.ts +49 -0
  69. package/src/cli/index.ts +1 -0
  70. package/src/cli/json-primitive-scanner.ts +83 -0
  71. package/src/cli/run.ts +36 -0
  72. package/src/cli/scope.ts +31 -0
  73. package/src/codex-rules/build.ts +63 -0
  74. package/src/codex-rules/index.ts +8 -0
  75. package/src/codex-rules/mapping.ts +42 -0
  76. package/src/codex-rules/rules/absolute-paths.ts +52 -0
  77. package/src/codex-rules/rules/ask.ts +44 -0
  78. package/src/codex-rules/rules/chmod-ask.ts +57 -0
  79. package/src/codex-rules/rules/dd.ts +23 -0
  80. package/src/codex-rules/rules/diskutil.ts +33 -0
  81. package/src/codex-rules/rules/mkfs.ts +42 -0
  82. package/src/codex-rules/rules/privilege.ts +49 -0
  83. package/src/codex-rules/rules/remote-fetch.ts +31 -0
  84. package/src/codex-rules/rules/rm-variants.ts +58 -0
  85. package/src/codex-rules/rules/single-commands.ts +57 -0
  86. package/src/codex-rules/skip-list.ts +25 -0
  87. package/src/codex-rules/starlark.ts +46 -0
  88. package/src/codex-rules/types.ts +36 -0
  89. package/src/config/dotenv.ts +55 -0
  90. package/src/config/env.ts +10 -0
  91. package/src/config/home-dir.ts +36 -0
  92. package/src/config/index.ts +5 -0
  93. package/src/config/layout.ts +52 -0
  94. package/src/config/limits.ts +56 -0
  95. package/src/config/ttl.ts +30 -0
  96. package/src/detect/harness.ts +73 -0
  97. package/src/detect/index.ts +1 -0
  98. package/src/detect/interfaces/types.ts +18 -0
  99. package/src/freshness/agent-evidence-record.ts +95 -0
  100. package/src/freshness/agent-evidence.ts +95 -0
  101. package/src/freshness/codex-spawn-evidence.ts +91 -0
  102. package/src/freshness/doc-helpers.ts +90 -0
  103. package/src/freshness/evidence-harvest-io.ts +72 -0
  104. package/src/freshness/evidence-harvest.ts +61 -0
  105. package/src/freshness/explore-tools.ts +95 -0
  106. package/src/freshness/index.ts +2 -0
  107. package/src/freshness/query-framework.ts +73 -0
  108. package/src/freshness/ref-evidence.ts +49 -0
  109. package/src/freshness/ref-journal.ts +98 -0
  110. package/src/freshness/trivial-edit-counter.ts +30 -0
  111. package/src/index.ts +11 -0
  112. package/src/init/index.ts +2 -0
  113. package/src/init/run.ts +32 -0
  114. package/src/init/templates.ts +75 -0
  115. package/src/memory/gitignore.ts +14 -0
  116. package/src/memory/index.ts +3 -0
  117. package/src/memory/registry.ts +39 -0
  118. package/src/memory/session-roots.ts +99 -0
  119. package/src/memory/state.ts +58 -0
  120. package/src/policy/apex-authorization.ts +100 -0
  121. package/src/policy/apex-gates.ts +91 -0
  122. package/src/policy/apex-target-steps.ts +73 -0
  123. package/src/policy/apex-target.ts +49 -0
  124. package/src/policy/apex-task-context.ts +81 -0
  125. package/src/policy/apex.ts +94 -0
  126. package/src/policy/cartographer/build-tree.ts +69 -0
  127. package/src/policy/cartographer/describe.ts +53 -0
  128. package/src/policy/cartographer/entry.ts +38 -0
  129. package/src/policy/cartographer/frontmatter.ts +48 -0
  130. package/src/policy/cartographer/indicators.ts +34 -0
  131. package/src/policy/claude-md-context.ts +66 -0
  132. package/src/policy/conventions/index.ts +11 -0
  133. package/src/policy/conventions/interfaces.ts +71 -0
  134. package/src/policy/conventions/langs.ts +53 -0
  135. package/src/policy/conventions/query.ts +49 -0
  136. package/src/policy/conventions/react-hooks.ts +18 -0
  137. package/src/policy/conventions/store-or-query.ts +21 -0
  138. package/src/policy/conventions/stores.ts +56 -0
  139. package/src/policy/conventions/strip.ts +87 -0
  140. package/src/policy/conventions/verdict.ts +38 -0
  141. package/src/policy/creation-intent.ts +11 -0
  142. package/src/policy/deny-loop.ts +88 -0
  143. package/src/policy/design/allowed-write.ts +28 -0
  144. package/src/policy/design/content-checks.ts +45 -0
  145. package/src/policy/design/corpus-fs.ts +21 -0
  146. package/src/policy/design/corpus-resolve-cache.ts +49 -0
  147. package/src/policy/design/corpus-resolve.ts +99 -0
  148. package/src/policy/design/corpus.ts +92 -0
  149. package/src/policy/design/design-system-rules.ts +45 -0
  150. package/src/policy/design/flag.ts +30 -0
  151. package/src/policy/design/gates-pipeline.ts +105 -0
  152. package/src/policy/design/gates.ts +87 -0
  153. package/src/policy/design/screenshot-tools.ts +24 -0
  154. package/src/policy/design/skill-evidence.ts +34 -0
  155. package/src/policy/design/skill-gate.ts +81 -0
  156. package/src/policy/design/skill-triggers.ts +51 -0
  157. package/src/policy/design/state.ts +92 -0
  158. package/src/policy/design/template-urls.ts +54 -0
  159. package/src/policy/design/transitions.ts +68 -0
  160. package/src/policy/detect-claude-md-project-type.ts +32 -0
  161. package/src/policy/detect-framework.ts +99 -0
  162. package/src/policy/detect-primitive-checks.ts +100 -0
  163. package/src/policy/detect-primitive-lib.ts +55 -0
  164. package/src/policy/detect-project.ts +99 -0
  165. package/src/policy/edit-outcome.ts +53 -0
  166. package/src/policy/evaluate.ts +81 -0
  167. package/src/policy/expert-agents.ts +60 -0
  168. package/src/policy/file-size-scope.ts +32 -0
  169. package/src/policy/file-size.ts +99 -0
  170. package/src/policy/framework-solid-exclude.ts +52 -0
  171. package/src/policy/framework-solid-extended.ts +93 -0
  172. package/src/policy/framework-solid-gates-systems.ts +99 -0
  173. package/src/policy/framework-solid-gates.ts +98 -0
  174. package/src/policy/framework-solid.ts +88 -0
  175. package/src/policy/gemini-mcp-gate.ts +83 -0
  176. package/src/policy/git-gates.ts +40 -0
  177. package/src/policy/guards/bash-command-anchor.ts +40 -0
  178. package/src/policy/guards/bash-write-patterns.ts +140 -0
  179. package/src/policy/guards/bash-write-redirects.ts +201 -0
  180. package/src/policy/guards/bash-write-safe-paths.ts +62 -0
  181. package/src/policy/guards/bash-write.ts +76 -0
  182. package/src/policy/guards/context.ts +12 -0
  183. package/src/policy/guards/index.ts +61 -0
  184. package/src/policy/guards/install.ts +22 -0
  185. package/src/policy/guards/interface-separation-ext.ts +41 -0
  186. package/src/policy/guards/interface-separation.ts +100 -0
  187. package/src/policy/guards/protected-path.ts +100 -0
  188. package/src/policy/guards/security.ts +97 -0
  189. package/src/policy/index.ts +19 -0
  190. package/src/policy/interfaces/types.ts +46 -0
  191. package/src/policy/js-gate-route.ts +51 -0
  192. package/src/policy/lessons/lesson-gate.ts +0 -0
  193. package/src/policy/lessons/trigger-index.ts +84 -0
  194. package/src/policy/lessons/types.ts +31 -0
  195. package/src/policy/module-layout.ts +31 -0
  196. package/src/policy/nearest-manifest.ts +75 -0
  197. package/src/policy/never-approval.ts +95 -0
  198. package/src/policy/patterns.ts +80 -0
  199. package/src/policy/seo/validate.ts +51 -0
  200. package/src/policy/shadcn-project.ts +22 -0
  201. package/src/policy/shadcn-skill-gate.ts +97 -0
  202. package/src/policy/shell-read-refs.ts +96 -0
  203. package/src/policy/skill-path.ts +46 -0
  204. package/src/policy/skill-patterns/laravel.ts +40 -0
  205. package/src/policy/skill-patterns/nextjs.ts +29 -0
  206. package/src/policy/skill-patterns/react.ts +27 -0
  207. package/src/policy/skill-patterns/shadcn-subskills.ts +33 -0
  208. package/src/policy/skill-patterns/shadcn.ts +66 -0
  209. package/src/policy/skill-patterns/swift.ts +30 -0
  210. package/src/policy/skill-patterns/tailwind.ts +40 -0
  211. package/src/policy/skill-source.ts +52 -0
  212. package/src/policy/skill-trigger-patterns.ts +33 -0
  213. package/src/policy/skill-triggers.ts +108 -0
  214. package/src/policy/tailwind-skill-gate.ts +63 -0
  215. package/src/policy/trivial-edits.ts +52 -0
  216. package/src/policy/verbosity.ts +39 -0
  217. package/src/prompt/index.ts +1 -0
  218. package/src/prompt/types.ts +36 -0
  219. package/src/refs/discover.ts +69 -0
  220. package/src/refs/frontmatter.ts +24 -0
  221. package/src/refs/index.ts +4 -0
  222. package/src/refs/loader.ts +59 -0
  223. package/src/refs/ref-key.ts +41 -0
  224. package/src/refs/router.ts +63 -0
  225. package/src/refs/types.ts +25 -0
  226. package/src/runtime/activity.ts +99 -0
  227. package/src/runtime/apply-patch-gate.ts +41 -0
  228. package/src/runtime/burst-window.ts +21 -0
  229. package/src/runtime/codex-shell-tool.ts +37 -0
  230. package/src/runtime/command-string.ts +37 -0
  231. package/src/runtime/confirm/codex-confirm.ts +126 -0
  232. package/src/runtime/confirm/codex-prompt-origin.ts +16 -0
  233. package/src/runtime/confirm/confirm-code.ts +33 -0
  234. package/src/runtime/confirm/confirm-gate.ts +65 -0
  235. package/src/runtime/confirm/confirm-irreversible.ts +32 -0
  236. package/src/runtime/confirm/confirm-pending.ts +34 -0
  237. package/src/runtime/confirm/confirm-state.ts +56 -0
  238. package/src/runtime/confirm/confirm-subagent.ts +112 -0
  239. package/src/runtime/confirm/confirm-submit.ts +38 -0
  240. package/src/runtime/core-guards-active.ts +84 -0
  241. package/src/runtime/core-guards-wired.ts +47 -0
  242. package/src/runtime/deny-loop-store.ts +94 -0
  243. package/src/runtime/deny-notice.ts +52 -0
  244. package/src/runtime/design-cache-resolve.ts +51 -0
  245. package/src/runtime/design-content-gate.ts +95 -0
  246. package/src/runtime/design-files-gate.ts +114 -0
  247. package/src/runtime/design-helpers.ts +93 -0
  248. package/src/runtime/design-lifecycle.ts +39 -0
  249. package/src/runtime/design-read-credit.ts +28 -0
  250. package/src/runtime/design.ts +116 -0
  251. package/src/runtime/dev-context.ts +43 -0
  252. package/src/runtime/dry-patterns.ts +32 -0
  253. package/src/runtime/dry.ts +96 -0
  254. package/src/runtime/fragment-registry.ts +33 -0
  255. package/src/runtime/framework-skill-gate.ts +79 -0
  256. package/src/runtime/fs-cleanup.ts +71 -0
  257. package/src/runtime/gate-apex.ts +81 -0
  258. package/src/runtime/gate-helpers.ts +52 -0
  259. package/src/runtime/gate-input.ts +26 -0
  260. package/src/runtime/gate.ts +128 -0
  261. package/src/runtime/handle-post.ts +104 -0
  262. package/src/runtime/handle-pre.ts +137 -0
  263. package/src/runtime/handle-scope-async.ts +29 -0
  264. package/src/runtime/handle-types.ts +27 -0
  265. package/src/runtime/handle.ts +116 -0
  266. package/src/runtime/home-state.ts +55 -0
  267. package/src/runtime/index.ts +18 -0
  268. package/src/runtime/inform.ts +28 -0
  269. package/src/runtime/inject-budget-recap.ts +47 -0
  270. package/src/runtime/inject-budget.ts +81 -0
  271. package/src/runtime/inject-context.ts +85 -0
  272. package/src/runtime/inject-dedup-exclusive.ts +98 -0
  273. package/src/runtime/inject-dedup.ts +89 -0
  274. package/src/runtime/is-agent-tool.ts +19 -0
  275. package/src/runtime/lifecycle/agent-files.ts +49 -0
  276. package/src/runtime/lifecycle/agent-memory.ts +82 -0
  277. package/src/runtime/lifecycle/agent-transcript.ts +80 -0
  278. package/src/runtime/lifecycle/aipilot/analytics.ts +99 -0
  279. package/src/runtime/lifecycle/aipilot/apex-task-store.ts +61 -0
  280. package/src/runtime/lifecycle/aipilot/cache-base.ts +48 -0
  281. package/src/runtime/lifecycle/aipilot/cache-doc.ts +100 -0
  282. package/src/runtime/lifecycle/aipilot/cache-lessons.ts +58 -0
  283. package/src/runtime/lifecycle/aipilot/cache-test.ts +70 -0
  284. package/src/runtime/lifecycle/aipilot/curate-lessons.ts +64 -0
  285. package/src/runtime/lifecycle/aipilot/dispatch-aipilot.ts +100 -0
  286. package/src/runtime/lifecycle/aipilot/doc-cache-gate.ts +69 -0
  287. package/src/runtime/lifecycle/aipilot/inject-apex.ts +98 -0
  288. package/src/runtime/lifecycle/aipilot/inject-doc.ts +62 -0
  289. package/src/runtime/lifecycle/aipilot/inject-explore.ts +64 -0
  290. package/src/runtime/lifecycle/aipilot/inject-lessons.ts +61 -0
  291. package/src/runtime/lifecycle/aipilot/inject-test.ts +48 -0
  292. package/src/runtime/lifecycle/aipilot/lesson-archive.ts +60 -0
  293. package/src/runtime/lifecycle/aipilot/lesson-inject.ts +98 -0
  294. package/src/runtime/lifecycle/aipilot/lesson-parse.ts +74 -0
  295. package/src/runtime/lifecycle/aipilot/lessons.ts +72 -0
  296. package/src/runtime/lifecycle/aipilot/promote-global-lessons.ts +92 -0
  297. package/src/runtime/lifecycle/aipilot/solid-compliance.ts +69 -0
  298. package/src/runtime/lifecycle/aipilot/solid-notice.ts +47 -0
  299. package/src/runtime/lifecycle/aipilot/solid-transcript.ts +60 -0
  300. package/src/runtime/lifecycle/aipilot/source-scan.ts +56 -0
  301. package/src/runtime/lifecycle/aipilot/sync-task.ts +75 -0
  302. package/src/runtime/lifecycle/aipilot/transcript.ts +88 -0
  303. package/src/runtime/lifecycle/aipilot/types.ts +72 -0
  304. package/src/runtime/lifecycle/auto-document-reads.ts +73 -0
  305. package/src/runtime/lifecycle/cartographer/detect.ts +57 -0
  306. package/src/runtime/lifecycle/cartographer/ecosystem-map.ts +56 -0
  307. package/src/runtime/lifecycle/cartographer/fs-util.ts +64 -0
  308. package/src/runtime/lifecycle/cartographer/merge.ts +51 -0
  309. package/src/runtime/lifecycle/cartographer/project-map.ts +49 -0
  310. package/src/runtime/lifecycle/cartographer/scan-hooks.ts +48 -0
  311. package/src/runtime/lifecycle/cartographer/scan.ts +81 -0
  312. package/src/runtime/lifecycle/cartographer/session-start.ts +33 -0
  313. package/src/runtime/lifecycle/cartographer/track-enrichment.ts +45 -0
  314. package/src/runtime/lifecycle/cartographer/write-plugin-map.ts +43 -0
  315. package/src/runtime/lifecycle/cartographer/write-tree.ts +44 -0
  316. package/src/runtime/lifecycle/changelog-research.ts +43 -0
  317. package/src/runtime/lifecycle/check-file-size.ts +61 -0
  318. package/src/runtime/lifecycle/codex-resync/fingerprint.ts +63 -0
  319. package/src/runtime/lifecycle/codex-resync/lock.ts +46 -0
  320. package/src/runtime/lifecycle/codex-resync/managed-destination.ts +39 -0
  321. package/src/runtime/lifecycle/codex-resync/materialize-agents.ts +57 -0
  322. package/src/runtime/lifecycle/codex-resync/plugin-files.ts +65 -0
  323. package/src/runtime/lifecycle/codex-resync/plugin-roots.ts +62 -0
  324. package/src/runtime/lifecycle/codex-resync/resync.ts +45 -0
  325. package/src/runtime/lifecycle/codex-resync/symlink-commands.ts +25 -0
  326. package/src/runtime/lifecycle/dispatch.ts +109 -0
  327. package/src/runtime/lifecycle/failure-lesson.ts +51 -0
  328. package/src/runtime/lifecycle/index.ts +34 -0
  329. package/src/runtime/lifecycle/inject-rules.ts +50 -0
  330. package/src/runtime/lifecycle/instructions-loaded.ts +22 -0
  331. package/src/runtime/lifecycle/kimi-rules-native.ts +44 -0
  332. package/src/runtime/lifecycle/lessons/dispatch.ts +88 -0
  333. package/src/runtime/lifecycle/lessons/reminder.ts +89 -0
  334. package/src/runtime/lifecycle/lessons/state.ts +23 -0
  335. package/src/runtime/lifecycle/memory/agent-lesson.ts +35 -0
  336. package/src/runtime/lifecycle/memory/capture-error.ts +41 -0
  337. package/src/runtime/lifecycle/memory/client.ts +65 -0
  338. package/src/runtime/lifecycle/memory/dispatch.ts +42 -0
  339. package/src/runtime/lifecycle/memory/recall.ts +30 -0
  340. package/src/runtime/lifecycle/memory/salience.ts +31 -0
  341. package/src/runtime/lifecycle/memory/state.ts +49 -0
  342. package/src/runtime/lifecycle/memory/track-ops.ts +15 -0
  343. package/src/runtime/lifecycle/post-compact.ts +38 -0
  344. package/src/runtime/lifecycle/post-edit-ts.ts +49 -0
  345. package/src/runtime/lifecycle/post-tracking.ts +40 -0
  346. package/src/runtime/lifecycle/pre-compact.ts +38 -0
  347. package/src/runtime/lifecycle/rules-root.ts +83 -0
  348. package/src/runtime/lifecycle/security/check-skill.ts +63 -0
  349. package/src/runtime/lifecycle/security/scan-patterns.ts +53 -0
  350. package/src/runtime/lifecycle/security/scan.ts +76 -0
  351. package/src/runtime/lifecycle/security/skill-state.ts +47 -0
  352. package/src/runtime/lifecycle/security/track-mcp.ts +29 -0
  353. package/src/runtime/lifecycle/security/track-skill-read.ts +31 -0
  354. package/src/runtime/lifecycle/seo/post-tool-use.ts +45 -0
  355. package/src/runtime/lifecycle/session-end.ts +17 -0
  356. package/src/runtime/lifecycle/session-start.ts +57 -0
  357. package/src/runtime/lifecycle/snapshot/board.ts +27 -0
  358. package/src/runtime/lifecycle/snapshot/format.ts +60 -0
  359. package/src/runtime/lifecycle/snapshot/git.ts +71 -0
  360. package/src/runtime/lifecycle/snapshot/index.ts +52 -0
  361. package/src/runtime/lifecycle/snapshot/version.ts +34 -0
  362. package/src/runtime/lifecycle/solid-detect.ts +56 -0
  363. package/src/runtime/lifecycle/stop-core.ts +35 -0
  364. package/src/runtime/lifecycle/subagent-cache.ts +61 -0
  365. package/src/runtime/lifecycle/task-completed.ts +94 -0
  366. package/src/runtime/lifecycle/teammate-idle-check.ts +63 -0
  367. package/src/runtime/lifecycle/teammate-idle.ts +28 -0
  368. package/src/runtime/lifecycle/tool-failure.ts +24 -0
  369. package/src/runtime/lifecycle/track-changes.ts +61 -0
  370. package/src/runtime/lifecycle/validate-solid.ts +70 -0
  371. package/src/runtime/lifecycle/validate-tailwind.ts +80 -0
  372. package/src/runtime/lifecycle-bridge.ts +74 -0
  373. package/src/runtime/mcp-key.ts +44 -0
  374. package/src/runtime/mcp-tool-name.ts +92 -0
  375. package/src/runtime/mcp.ts +91 -0
  376. package/src/runtime/modular.ts +77 -0
  377. package/src/runtime/normalize.ts +107 -0
  378. package/src/runtime/notices.ts +91 -0
  379. package/src/runtime/notification-sound.ts +49 -0
  380. package/src/runtime/notifications.ts +77 -0
  381. package/src/runtime/paths.ts +59 -0
  382. package/src/runtime/post-fanout.ts +41 -0
  383. package/src/runtime/post-outcome.ts +61 -0
  384. package/src/runtime/pre-allow.ts +97 -0
  385. package/src/runtime/precommit.ts +60 -0
  386. package/src/runtime/prompt-text.ts +38 -0
  387. package/src/runtime/receipt-capture.ts +34 -0
  388. package/src/runtime/record.ts +26 -0
  389. package/src/runtime/respond.ts +92 -0
  390. package/src/runtime/solid-file-size-gate.ts +58 -0
  391. package/src/runtime/solid-pre.ts +53 -0
  392. package/src/runtime/storage.ts +9 -0
  393. package/src/statusline/colors.ts +31 -0
  394. package/src/statusline/constants.ts +17 -0
  395. package/src/statusline/formatters.ts +54 -0
  396. package/src/statusline/index.ts +4 -0
  397. package/src/statusline/progress-bar.ts +46 -0
  398. package/src/tracking/codex-post-failure.ts +73 -0
  399. package/src/tracking/index.ts +2 -0
  400. package/src/tracking/integrity.ts +99 -0
  401. package/src/tracking/one-shot-dedup.ts +34 -0
  402. package/src/tracking/one-shot-failure.ts +42 -0
  403. package/src/tracking/one-shot-store.ts +95 -0
  404. package/src/tracking/one-shot.ts +93 -0
  405. package/src/tracking/receipts.ts +95 -0
  406. package/src/tracking/session-state.ts +94 -0
  407. package/src/tracking/store.ts +96 -0
  408. package/src/tracking/track-compact.ts +100 -0
  409. package/src/tracking/track-diff.ts +50 -0
  410. package/src/tracking/track-journal.ts +97 -0
  411. package/src/tracking/track-lock-sync.ts +83 -0
  412. package/src/tracking/track-lock.ts +67 -0
  413. package/src/util/compact-json.ts +23 -0
  414. package/src/util/index.ts +2 -0
  415. package/src/util/json-io.ts +47 -0
  416. package/src/util/project-root.ts +41 -0
  417. package/src/util/runtime-io.ts +85 -0
  418. package/src/util/semver.ts +30 -0
  419. package/dist/permission-mode-B0MUl7FT.mjs +0 -136
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Cache analytics for the ai-pilot scope: append hit/miss events to
3
+ * `sessions.jsonl` and aggregate them into `summary.json` on SessionEnd.
4
+ * Ports `cache-analytics-save.ts` + the `logCacheEvent` helper.
5
+ */
6
+ import { appendFileSync, mkdirSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { homedir } from "node:os";
9
+ import { readJsonFile, writeJsonFile } from "../../../util/json-io";
10
+ import { readText, writeText, pathExists } from "../../../util/runtime-io";
11
+ import { cacheBaseDir } from "./cache-base";
12
+
13
+ const TOKEN_WEIGHTS: Record<string, number> = { explore: 15000, doc: 10000, lessons: 3000, tests: 5000 };
14
+ const CATEGORIES = ["explore", "doc", "lessons", "tests"] as const;
15
+
16
+ /** A raw analytics event line in `sessions.jsonl`. */
17
+ export interface SessionEntry { ts: string; type: string; action: string; session?: string }
18
+
19
+ /** Aggregated cache analytics persisted to `summary.json`. */
20
+ export interface Summary {
21
+ updated: string;
22
+ total_sessions: number;
23
+ cache_hits: Record<string, number>;
24
+ cache_misses: Record<string, number>;
25
+ hit_rates: Record<string, string>;
26
+ estimated_tokens_saved: number;
27
+ }
28
+
29
+ /** Append a single cache event to `analytics/sessions.jsonl` (best effort). */
30
+ export function logCacheEvent(type: string, action: string, projHash: string, extra: Record<string, unknown> = {}, home: string = homedir()): void {
31
+ try {
32
+ const dir = join(cacheBaseDir(home), "analytics");
33
+ mkdirSync(dir, { recursive: true });
34
+ const entry = { ts: new Date().toISOString(), session: String(Math.floor(Date.now() / 1000)), type, action, project_hash: projHash, ...extra };
35
+ appendFileSync(join(dir, "sessions.jsonl"), JSON.stringify(entry) + "\n");
36
+ } catch { /* best effort */ }
37
+ }
38
+
39
+ /** Count entries matching a `type`+`action`. */
40
+ function countBy(entries: SessionEntry[], type: string, action: string): number {
41
+ return entries.filter((e) => e.type === type && e.action === action).length;
42
+ }
43
+
44
+ /** Format a hit-rate percentage string. */
45
+ function hitRate(hits: number, misses: number): string {
46
+ const total = hits + misses;
47
+ return total === 0 ? "0%" : `${Math.floor((hits * 100) / total)}%`;
48
+ }
49
+
50
+ /** Parse the JSONL session log into typed entries. */
51
+ function parseEntries(raw: string): SessionEntry[] {
52
+ return raw.split("\n").filter(Boolean)
53
+ .map((line) => { try { return JSON.parse(line) as SessionEntry; } catch { return null; } })
54
+ .filter((e): e is SessionEntry => e !== null);
55
+ }
56
+
57
+ /**
58
+ * Aggregate `sessions.jsonl` into `summary.json` and prune entries > 30 days.
59
+ * Ports `cache-analytics-save.ts`. SessionEnd hook output is ignored by Claude,
60
+ * so this returns nothing — it is a pure side-effect.
61
+ * @param home - Home dir (defaults to `~`).
62
+ * @param now - Clock (defaults to `Date.now()`).
63
+ */
64
+ export async function cacheAnalyticsSave(home: string = homedir(), now: number = Date.now()): Promise<void> {
65
+ const dir = join(cacheBaseDir(home), "analytics");
66
+ const sessionsFile = join(dir, "sessions.jsonl");
67
+ if (!pathExists(sessionsFile)) return;
68
+ const raw = readText(sessionsFile);
69
+ if (!raw.trim()) return;
70
+ const entries = parseEntries(raw);
71
+ if (entries.length === 0) return;
72
+
73
+ const hits: Record<string, number> = {};
74
+ const misses: Record<string, number> = {};
75
+ for (const cat of CATEGORIES) {
76
+ hits[cat] = countBy(entries, cat, "hit") + (cat === "doc" ? countBy(entries, cat, "blocked") : 0);
77
+ misses[cat] = countBy(entries, cat, "miss");
78
+ }
79
+ const tokensSaved = CATEGORIES.reduce((sum, c) => sum + (hits[c] ?? 0) * (TOKEN_WEIGHTS[c] ?? 0), 0);
80
+ const sessionCount = new Set(entries.map((e) => e.session).filter(Boolean)).size;
81
+
82
+ const old = await readJsonFile<Summary>(join(dir, "summary.json"));
83
+ const merged: Summary = {
84
+ updated: new Date(now).toISOString(),
85
+ total_sessions: (old?.total_sessions ?? 0) + sessionCount,
86
+ cache_hits: {}, cache_misses: {}, hit_rates: {},
87
+ estimated_tokens_saved: (old?.estimated_tokens_saved ?? 0) + tokensSaved,
88
+ };
89
+ for (const cat of CATEGORIES) {
90
+ merged.cache_hits[cat] = (old?.cache_hits?.[cat] ?? 0) + (hits[cat] ?? 0);
91
+ merged.cache_misses[cat] = (old?.cache_misses?.[cat] ?? 0) + (misses[cat] ?? 0);
92
+ merged.hit_rates[cat] = hitRate(merged.cache_hits[cat] ?? 0, merged.cache_misses[cat] ?? 0);
93
+ }
94
+ await writeJsonFile(join(dir, "summary.json"), merged, true);
95
+
96
+ const cutoff = new Date(now - 30 * 86_400_000).toISOString();
97
+ const kept = entries.filter((e) => e.ts >= cutoff);
98
+ writeText(sessionsFile, kept.map((e) => JSON.stringify(e)).join("\n") + "\n");
99
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * APEX `task.json` mutation helpers + a directory-based lock for the ai-pilot
3
+ * scope. Ported from the ai-pilot plugin's `apex/state.ts` + `apex/task-helpers.ts`
4
+ * (now removed).
5
+ */
6
+ import { mkdir, rmdir } from "node:fs/promises";
7
+ import { readJsonFile, writeJsonFile } from "../../../util/json-io";
8
+ import { sleep } from "../../../util/runtime-io";
9
+ import type { ApexTaskFile } from "./types";
10
+
11
+ /**
12
+ * Acquire a directory-based lock with a timeout.
13
+ * @param lockDir - Path to the lock directory.
14
+ * @param timeoutMs - Max wait in ms (default 5000).
15
+ * @returns A release function, or null if acquisition timed out.
16
+ */
17
+ export async function acquireLock(lockDir: string, timeoutMs = 5000): Promise<(() => Promise<void>) | null> {
18
+ const start = Date.now();
19
+ while (Date.now() - start < timeoutMs) {
20
+ try {
21
+ await mkdir(lockDir, { recursive: false });
22
+ return async () => { try { await rmdir(lockDir); } catch { /* noop */ } };
23
+ } catch { await sleep(100); }
24
+ }
25
+ return null;
26
+ }
27
+
28
+ /** Add a new task to `task.json`. */
29
+ export async function taskCreate(file: string, id: string, subject: string, desc: string): Promise<void> {
30
+ const data = await readJsonFile<ApexTaskFile>(file);
31
+ if (!data) return;
32
+ data.tasks[id] = {
33
+ subject, description: desc, status: "pending", phase: "pending",
34
+ created_at: new Date().toISOString(), doc_consulted: {}, files_modified: [], blockedBy: [],
35
+ };
36
+ await writeJsonFile(file, data);
37
+ }
38
+
39
+ /** Mark a task as in_progress in `task.json`. */
40
+ export async function taskStart(file: string, id: string, subject?: string, desc?: string, blocked?: string): Promise<void> {
41
+ const data = await readJsonFile<ApexTaskFile>(file);
42
+ if (!data) return;
43
+ if (!data.tasks[id]) {
44
+ data.tasks[id] = { subject: "", description: "", status: "in_progress", phase: "analyze", doc_consulted: {}, files_modified: [] };
45
+ }
46
+ data.current_task = id;
47
+ const task = data.tasks[id];
48
+ Object.assign(task, { status: "in_progress", phase: "analyze", started_at: new Date().toISOString() });
49
+ if (subject) task.subject = subject;
50
+ if (desc) task.description = desc;
51
+ if (blocked) task.blockedBy = blocked.split(",");
52
+ await writeJsonFile(file, data);
53
+ }
54
+
55
+ /** Mark a task as completed in `task.json`. */
56
+ export async function taskComplete(file: string, id: string): Promise<void> {
57
+ const data = await readJsonFile<ApexTaskFile>(file);
58
+ if (!data?.tasks[id]) return;
59
+ Object.assign(data.tasks[id], { status: "completed", phase: "completed", completed_at: new Date().toISOString() });
60
+ await writeJsonFile(file, data);
61
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Shared cache-path + age helpers for the ai-pilot scope.
3
+ * Reuses the harness home-state (`fusengineCache`) so the ai-pilot caches live
4
+ * under the same `~/.fuse-harness/cache` tree as the core MCP cache — no
5
+ * second cache layer.
6
+ */
7
+ import { createHash } from "node:crypto";
8
+ import { join } from "node:path";
9
+ import { homedir } from "node:os";
10
+ import { fusengineCache } from "../../home-state";
11
+ import { readText } from "../../../util/runtime-io";
12
+
13
+ /** Doc cache freshness window, in seconds (7 days) — shared by inject-doc.ts (serves the cache) and doc-cache-gate.ts (denies a redundant re-query). */
14
+ export const DOC_CACHE_TTL_SECONDS = 604_800;
15
+
16
+ /** 16-char hex SHA-256 of `text` (project hash / doc topic key). */
17
+ export function hashText16(text: string): string {
18
+ return createHash("sha256").update(text).digest("hex").slice(0, 16);
19
+ }
20
+
21
+ /** 16-char project hash from its absolute path. */
22
+ export function projectHash(projectPath: string): string {
23
+ return hashText16(projectPath);
24
+ }
25
+
26
+ /** `~/.fuse-harness/cache` base dir (shared with the core MCP cache). */
27
+ export function cacheBaseDir(home: string = homedir()): string {
28
+ return fusengineCache(home);
29
+ }
30
+
31
+ /** Per-type, per-project cache dir: `cache/<type>/<projectHash>`. */
32
+ export function cacheDirFor(type: string, projectPath: string, home: string = homedir()): string {
33
+ return join(cacheBaseDir(home), type, projectHash(projectPath));
34
+ }
35
+
36
+ /** Age in seconds from an ISO timestamp (now - ts). */
37
+ export function cacheAge(ts: string, now: number = Date.now()): number {
38
+ return Math.floor((now - new Date(ts).getTime()) / 1000);
39
+ }
40
+
41
+ /** Full SHA-256 hex checksum of a file's text; "" when unreadable. */
42
+ export async function fileChecksum(path: string): Promise<string> {
43
+ try {
44
+ return createHash("sha256").update(readText(path)).digest("hex");
45
+ } catch {
46
+ return "";
47
+ }
48
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * SubagentStop (matcher "research-expert") for the ai-pilot scope: extract the
3
+ * agent's synthesis text from the transcript and cache it as documentation.
4
+ * Ports `cache-doc-from-transcript.ts`. SubagentStop output here is a pure
5
+ * side-effect (no stdout needed).
6
+ */
7
+ import { homedir } from "node:os";
8
+ import { join } from "node:path";
9
+ import { readJsonFile, writeJsonFile } from "../../../util/json-io";
10
+ import { readText, writeText, pathExists, sleep } from "../../../util/runtime-io";
11
+ import { hashText16, cacheDirFor } from "./cache-base";
12
+ import { transcriptFilePaths, projectRootFromPaths } from "./transcript";
13
+ import { canonicalizeMcpToolName } from "../../mcp-tool-name";
14
+ import type { CacheEntry, CacheIndex } from "./types";
15
+
16
+ const TOOL_PATTERN = /context7__query-docs|exa__get_code_context|exa__web_search/;
17
+ const MAX_DOC_SIZE = 20480;
18
+ const MIN_TEXT_SIZE = 200;
19
+ const MAX_DOCS = 15;
20
+ const RETRY_DELAYS = [500, 1000, 2000];
21
+
22
+ /**
23
+ * Extract the longest assistant synthesis + queried library ids from a transcript.
24
+ * @param id - Harness adapter id, used to canonicalize `block.name` (Codex-mangled
25
+ * underscore tool names) before the {@link TOOL_PATTERN} match — see
26
+ * {@link cacheDocFromTranscript} for why this is a defensive, unconfirmed-case cover.
27
+ */
28
+ async function extractSynthesis(path: string, id: string): Promise<{ text: string; libraries: string[] }> {
29
+ const lines = readText(path).split("\n").filter(Boolean);
30
+ const libraries: string[] = [];
31
+ let synthesis = "";
32
+ for (const line of lines) {
33
+ try {
34
+ const entry = JSON.parse(line) as { type?: string; role?: string; message?: { content?: unknown } };
35
+ const role = entry?.type ?? entry?.role;
36
+ const contents = entry?.message?.content;
37
+ if (!Array.isArray(contents)) continue;
38
+ for (const block of contents) {
39
+ if (block.type === "tool_use" && TOOL_PATTERN.test(canonicalizeMcpToolName(id, block.name ?? ""))) {
40
+ const lib = block.input?.libraryId ?? block.input?.query ?? "";
41
+ if (lib && !libraries.includes(lib)) libraries.push(lib);
42
+ }
43
+ if (role === "assistant" && block.type === "text" && typeof block.text === "string" && block.text.length > synthesis.length) {
44
+ synthesis = block.text;
45
+ }
46
+ }
47
+ } catch { /* skip malformed */ }
48
+ }
49
+ return { text: synthesis, libraries };
50
+ }
51
+
52
+ /**
53
+ * SubagentStop research-expert: cache the synthesis text from the transcript.
54
+ * @param transcript - Path to the agent JSONL transcript.
55
+ * @param cwd - Fallback project root.
56
+ * @param home - Home dir (defaults to `~`).
57
+ * @param id - Harness adapter id (defaults to "claude-code"). On `"codex"`,
58
+ * every `tool_use.name` read from the transcript is canonicalized before the
59
+ * {@link TOOL_PATTERN} match, defensively covering the SAME Codex-mangled
60
+ * underscore form as `doc-cache-gate.ts`'s `docCacheGate`. UNLIKE that gate,
61
+ * this is NOT a confirmed-reproduced bug: the proof that Codex emits the
62
+ * underscore form (`sanitize_responses_api_tool_name()`, openai/codex#14605,
63
+ * `SCENARIO_DASH_TOOL` in #18385) is about the live PreToolUse HOOK payload,
64
+ * never observed here against transcript `tool_use.name` content. The
65
+ * canonicalization is a no-op when the transcript already carries the dash
66
+ * form (idempotence proven in `test/mcp-tool-name.test.ts`, test 2), so this
67
+ * costs nothing if the hypothesis turns out false.
68
+ */
69
+ export async function cacheDocFromTranscript(transcript: string | undefined, cwd: string, home: string = homedir(), id: string = "claude-code"): Promise<void> {
70
+ if (!transcript || !pathExists(transcript)) return;
71
+ const allPaths = await transcriptFilePaths(transcript);
72
+ const projPath = projectRootFromPaths(allPaths) ?? process.env.CLAUDE_PROJECT_DIR ?? cwd;
73
+ const cacheDir = cacheDirFor("doc", projPath, home);
74
+ const docsDir = join(cacheDir, "docs");
75
+
76
+ let result = await extractSynthesis(transcript, id);
77
+ for (const delay of RETRY_DELAYS) {
78
+ if (result.text.length >= MIN_TEXT_SIZE && result.libraries.length > 0) break;
79
+ await sleep(delay);
80
+ result = await extractSynthesis(transcript, id);
81
+ }
82
+ const { text, libraries } = result;
83
+ if (text.length < MIN_TEXT_SIZE || libraries.length === 0) return;
84
+
85
+ const indexFile = join(cacheDir, "index.json");
86
+ const index = (await readJsonFile<CacheIndex>(indexFile)) ?? { project: projPath, docs: [] };
87
+ const timestamp = new Date().toISOString();
88
+ const content = text.slice(0, MAX_DOC_SIZE);
89
+ const topic = libraries.join(", ");
90
+ const docHash = hashText16(topic);
91
+ writeText(join(docsDir, `${docHash}.md`), content);
92
+
93
+ const sizeKb = Math.floor(content.length / 1024);
94
+ for (const lib of libraries) {
95
+ index.docs = index.docs.filter((d: CacheEntry) => d.library !== lib);
96
+ index.docs.push({ hash: docHash, library: lib, topic, timestamp, size_kb: sizeKb });
97
+ }
98
+ if (index.docs.length > MAX_DOCS) index.docs = index.docs.slice(-MAX_DOCS);
99
+ await writeJsonFile(indexFile, index, true);
100
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * SubagentStop (matcher "sniper") for the ai-pilot scope: capture error
3
+ * patterns + corrected code from the sniper transcript as lessons. Ports
4
+ * `cache-sniper-lessons.ts`, including the high-frequency promotion to the
5
+ * global stack cache (`promoteGlobalLessons`). Pure side-effect.
6
+ */
7
+ import { homedir } from "node:os";
8
+ import { mkdirSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { writeJsonFile } from "../../../util/json-io";
11
+ import { pathExists } from "../../../util/runtime-io";
12
+ import { cacheDirFor, projectHash } from "./cache-base";
13
+ import { logCacheEvent } from "./analytics";
14
+ import { transcriptEdits, transcriptReport, projectRootFromPaths } from "./transcript";
15
+ import { categorizeEdit } from "./lessons";
16
+ import { detectStack } from "./source-scan";
17
+ import { promoteGlobalLessons } from "./promote-global-lessons";
18
+ import type { LessonEntry } from "./types";
19
+
20
+ /**
21
+ * SubagentStop sniper: extract lessons from the transcript and persist them.
22
+ * @param transcript - Path to the agent JSONL transcript.
23
+ * @param cwd - Fallback project root.
24
+ * @param home - Home dir (defaults to `~`).
25
+ */
26
+ export async function cacheSniperLessons(transcript: string | undefined, cwd: string, home: string = homedir()): Promise<void> {
27
+ if (!transcript || !pathExists(transcript)) return;
28
+ const edits = await transcriptEdits(transcript);
29
+ if (edits.length === 0) return;
30
+
31
+ const editPaths = edits.map((e) => e.file);
32
+ const projectPath = projectRootFromPaths(editPaths) ?? process.env.CLAUDE_PROJECT_DIR ?? cwd;
33
+ const pHash = projectHash(projectPath);
34
+ const cacheDir = cacheDirFor("lessons", projectPath, home);
35
+ mkdirSync(cacheDir, { recursive: true });
36
+
37
+ const report = await transcriptReport(transcript);
38
+ const timestamp = new Date().toISOString();
39
+ const errors: LessonEntry[] = edits.map((edit) => {
40
+ const basename = edit.file.split("/").pop() ?? edit.file;
41
+ const descLine = report.split("\n").find((l) => l.toLowerCase().includes(basename.toLowerCase()));
42
+ const errorType = categorizeEdit(edit);
43
+ return {
44
+ error_type: errorType,
45
+ pattern: descLine ?? `Code correction in ${basename}`,
46
+ fix: `Fix ${errorType} in ${basename}`,
47
+ count: 1,
48
+ last_seen: timestamp,
49
+ files: [edit.file],
50
+ code: { line: (edit.newStr ?? "").split("\n").filter(Boolean).slice(0, 10) },
51
+ };
52
+ });
53
+
54
+ const safeName = timestamp.replace(/:/g, "-");
55
+ await writeJsonFile(join(cacheDir, `${safeName}.json`), { project: projectPath, timestamp, errors }, true);
56
+ await promoteGlobalLessons(cacheDir, detectStack(projectPath), pHash, home);
57
+ logCacheEvent("lessons", "hit", pHash, { count: edits.length }, home);
58
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * SubagentStop (matcher "sniper") for the ai-pilot scope: extract linter
3
+ * results from the transcript and cache per-file checksums. Ports
4
+ * `cache-test-results.ts`. Pure side-effect.
5
+ */
6
+ import { homedir } from "node:os";
7
+ import { mkdirSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { readJsonFile, writeJsonFile } from "../../../util/json-io";
10
+ import { readText, pathExists } from "../../../util/runtime-io";
11
+ import { cacheDirFor, projectHash, fileChecksum } from "./cache-base";
12
+ import { logCacheEvent } from "./analytics";
13
+ import { transcriptFilePaths, projectRootFromPaths } from "./transcript";
14
+ import { scanSourceFiles } from "./source-scan";
15
+ import type { TestCache, TestResult } from "./types";
16
+
17
+ /** Extract linter-related command/output text from a JSONL transcript. */
18
+ async function extractLinterOutput(path: string): Promise<string> {
19
+ const text = readText(path);
20
+ const outputs: string[] = [];
21
+ for (const line of text.split("\n").filter(Boolean)) {
22
+ try {
23
+ const content = (JSON.parse(line) as { message?: { content?: unknown } })?.message?.content;
24
+ if (!Array.isArray(content)) continue;
25
+ for (const block of content) {
26
+ if (block.type === "tool_use" && block.name === "Bash") {
27
+ const cmd = block.input?.command ?? "";
28
+ if (/eslint|tsc|biome|npx.*lint/i.test(cmd)) outputs.push(cmd);
29
+ }
30
+ if (block.type === "tool_result" || block.type === "text") outputs.push(block.text ?? block.content ?? "");
31
+ }
32
+ } catch { /* skip malformed */ }
33
+ }
34
+ return outputs.join("\n");
35
+ }
36
+
37
+ /**
38
+ * SubagentStop sniper: cache linter results per-file with checksums.
39
+ * @param transcript - Path to the agent JSONL transcript.
40
+ * @param cwd - Fallback project root.
41
+ * @param home - Home dir (defaults to `~`).
42
+ */
43
+ export async function cacheTestResults(transcript: string | undefined, cwd: string, home: string = homedir()): Promise<void> {
44
+ if (!transcript || !pathExists(transcript)) return;
45
+ const allPaths = await transcriptFilePaths(transcript);
46
+ const projectPath = projectRootFromPaths(allPaths) ?? process.env.CLAUDE_PROJECT_DIR ?? cwd;
47
+ const pHash = projectHash(projectPath);
48
+ const cacheDir = cacheDirFor("tests", projectPath, home);
49
+ mkdirSync(cacheDir, { recursive: true });
50
+ const resultsPath = join(cacheDir, "results.json");
51
+
52
+ const linterOutput = await extractLinterOutput(transcript);
53
+ if (!linterOutput) return;
54
+ const srcFiles = await scanSourceFiles(projectPath);
55
+ if (srcFiles.length === 0) return;
56
+
57
+ const existing = (await readJsonFile<TestCache>(resultsPath)) ?? { timestamp: "", files: {} };
58
+ const timestamp = new Date().toISOString();
59
+ const newFiles: Record<string, TestResult> = {};
60
+ for (const filepath of srcFiles) {
61
+ const relPath = filepath.replace(`${projectPath}/`, "");
62
+ const checksum = (await fileChecksum(filepath)).slice(0, 16);
63
+ if (!checksum) continue;
64
+ const basename = filepath.split("/").pop() ?? "";
65
+ const hasError = linterOutput.includes(basename) && linterOutput.includes("error");
66
+ newFiles[relPath] = { checksum, eslint: hasError ? "fail" : "pass", tsc: "pass", last_tested: timestamp };
67
+ }
68
+ await writeJsonFile(resultsPath, { timestamp, files: { ...existing.files, ...newFiles } }, true);
69
+ logCacheEvent("tests", "hit", pHash, { count: Object.keys(newFiles).length }, home);
70
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Stage-0 mechanical, LLM-free dedup of MEMORY/LESSON.md bullets + cap→archive
3
+ * orchestration. Strict-dedup near-identical bullets (keep newest, `[TRIGGERS …]`
4
+ * preserved), then hand the deduped set to lesson-archive's cap split. Returns the
5
+ * rewritten LESSON.md content, the archive block to move out, and a human report.
6
+ * Pure: all file I/O (archive-first, fail-safe) lives in the dispatch caller.
7
+ */
8
+ import { existsSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { CAP, DAY_MS, STALE_DAYS, TRIG, type Block, citedPaths, jaccard, parse } from "./lesson-parse";
11
+ import { formatArchive, splitAtCap } from "./lesson-archive";
12
+
13
+ const SIM_THRESHOLD = 0.8;
14
+ const MIN_TOKENS = 4;
15
+
16
+ /** Report lines for bullets older than STALE_DAYS whose only cited path is gone. */
17
+ function staleReport(blocks: Block[], now: number, root: string): string[] {
18
+ const cutoff = now - STALE_DAYS * DAY_MS;
19
+ return blocks.flatMap((b) => {
20
+ if (!(b.ts <= cutoff)) return [];
21
+ const paths = citedPaths(b.raw.join(" "));
22
+ if (paths.length === 0 || paths.some((p) => existsSync(join(root, p)))) return [];
23
+ return [`[STALE?] ${(b.raw[0] ?? "").slice(0, 90)} — missing path(s): ${paths.join(", ")}`];
24
+ });
25
+ }
26
+
27
+ /** Strict-dedup: keep the newest of each near-identical pair (TRIGGERS carried over). Returns kept blocks + merge report lines. */
28
+ function dedup(blocks: Block[]): { kept: Block[]; fused: string[] } {
29
+ const kept: Block[] = [];
30
+ const fused: string[] = [];
31
+ for (const b of blocks) {
32
+ const hit = b.tokens.size >= MIN_TOKENS ? kept.find((k) => k.tokens.size >= MIN_TOKENS && jaccard(k.tokens, b.tokens) >= SIM_THRESHOLD) : undefined;
33
+ if (!hit) { kept.push(b); continue; }
34
+ const [win, drop] = (b.ts > hit.ts || Number.isNaN(hit.ts)) ? [b, hit] : [hit, b];
35
+ if (win !== hit) kept[kept.indexOf(hit)] = win;
36
+ if (!win.raw.some((l) => TRIG.test(l.trim()))) { const t = drop.raw.find((l) => TRIG.test(l.trim())); if (t) win.raw.push(t); }
37
+ fused.push(`merged: kept ${(win.raw[0] ?? "").slice(0, 60)} · dropped ${(drop.raw[0] ?? "").slice(0, 60)}`);
38
+ }
39
+ return { kept, fused };
40
+ }
41
+
42
+ /** Result of a curation pass: rewritten LESSON.md, the block to archive out, a report. */
43
+ export interface CurateResult { content: string; archive: string; report: string; }
44
+
45
+ /**
46
+ * Dedup LESSON.md bullets, then archive the oldest excess over CAP (via
47
+ * lesson-archive). `content` is byte-identical to the input when nothing is
48
+ * deduped or archived. The `archive` block (possibly "") is what the caller must
49
+ * PREPEND to LESSON-archive.md, archive-first, before writing `content`.
50
+ * @param content - Raw LESSON.md text.
51
+ * @param now - Clock (ms) for stale/archival windows.
52
+ * @param root - Project root, for resolving cited paths in the stale report.
53
+ * @returns The rewritten content, the archive block, and the report.
54
+ */
55
+ export function curateLessons(content: string, now: number, root: string = process.cwd()): CurateResult {
56
+ const { preamble, blocks } = parse(content);
57
+ const { kept, fused } = dedup(blocks);
58
+ const { keep, archive } = splitAtCap(kept, now);
59
+ const changed = fused.length > 0 || archive.length > 0;
60
+ const rebuilt = changed ? `${preamble}\n${keep.map((b) => b.raw.join("\n")).join("\n\n")}\n` : content;
61
+ const capReport = archive.length ? [`${kept.length} bullets (> ${CAP}) — ${archive.length} oldest archived → LESSON-archive.md`] : [];
62
+ const report = [...fused, ...capReport, ...staleReport(blocks, now, root)].join("\n");
63
+ return { content: rebuilt, archive: formatArchive(archive, now), report };
64
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * ai-pilot scope dispatcher: routes Claude lifecycle events to the ported
3
+ * cache/injection handlers by (event, agent_type matcher).
4
+ */
5
+ import { homedir } from "node:os";
6
+ import { injectApexSubagentContext } from "./inject-apex";
7
+ import { injectExploreCache } from "./inject-explore";
8
+ import { injectDocCache } from "./inject-doc";
9
+ import { injectLessonsCache } from "./inject-lessons";
10
+ import { injectTestCache } from "./inject-test";
11
+ import { cacheDocFromTranscript } from "./cache-doc";
12
+ import { cacheSniperLessons } from "./cache-lessons";
13
+ import { cacheTestResults } from "./cache-test";
14
+ import { cacheAnalyticsSave } from "./analytics";
15
+ import { syncTaskTracking } from "./sync-task";
16
+ import { docCacheGate } from "./doc-cache-gate";
17
+ import { checkSolidCompliance } from "./solid-compliance";
18
+ import { checkSolidFromTranscript } from "./solid-transcript";
19
+ import { contextResponse } from "../../../adapters/claude";
20
+
21
+ /** The agent_type a SubagentStart/Stop payload reports. */
22
+ function agentTypeOf(payload: Record<string, unknown>): string {
23
+ return String(payload.agent_type ?? payload.subagent_type ?? "");
24
+ }
25
+
26
+ /** Extract the `additionalContext` text from a SubagentStart response, or "". */
27
+ function contextTextOf(response: string): string {
28
+ if (!response) return "";
29
+ try {
30
+ const parsed = JSON.parse(response) as { hookSpecificOutput?: { additionalContext?: string } };
31
+ return parsed.hookSpecificOutput?.additionalContext ?? "";
32
+ } catch { return ""; }
33
+ }
34
+
35
+ /**
36
+ * Combine N SubagentStart responses into one (Claude concatenates every hook's
37
+ * additionalContext; this single dispatch call must do that join itself). "" when all empty.
38
+ */
39
+ function combineContext(...responses: string[]): string {
40
+ const parts = responses.map(contextTextOf).filter(Boolean);
41
+ return parts.length ? contextResponse("SubagentStart", parts.join("\n\n")) : "";
42
+ }
43
+
44
+ /** The transcript path a SubagentStop payload reports (Stop-only field). */
45
+ function transcriptOf(payload: Record<string, unknown>): string | undefined {
46
+ const t = payload.agent_transcript_path;
47
+ return typeof t === "string" ? t : undefined;
48
+ }
49
+
50
+ /** Type-specific SubagentStart cache for an agent, or "" when none applies. */
51
+ async function typeSpecificCache(agent: string, cwd: string, now: number, home: string): Promise<string> {
52
+ if (agent.includes("explore-codebase")) return injectExploreCache(cwd, home, now);
53
+ if (agent.includes("research-expert")) return injectDocCache(cwd, home, now);
54
+ if (agent.includes("sniper")) return injectTestCache(cwd, home, now);
55
+ return "";
56
+ }
57
+
58
+ /**
59
+ * SubagentStart routing (parity with the Python ai-pilot hooks): the two matcher-""
60
+ * entries (APEX context + lessons) fire for EVERY sub-agent, then the type-specific
61
+ * cache is concatenated on top — sniper too, hence no early return.
62
+ */
63
+ async function onSubagentStart(payload: Record<string, unknown>, cwd: string, now: number, home: string, id: string): Promise<string> {
64
+ const agent = agentTypeOf(payload);
65
+ const apex = await injectApexSubagentContext(cwd, home, id);
66
+ const lessons = await injectLessonsCache(cwd, home, now);
67
+ const typeSpecific = await typeSpecificCache(agent, cwd, now, home);
68
+ return combineContext(apex, lessons, typeSpecific);
69
+ }
70
+
71
+ /** SubagentStop routing: transcript-driven cache writers, then the universal SOLID check. `id` selects the harness target (defaults to "claude-code"), threaded into `cacheDocFromTranscript`. */
72
+ async function onSubagentStop(payload: Record<string, unknown>, cwd: string, home: string, id: string): Promise<string> {
73
+ const agent = agentTypeOf(payload);
74
+ const transcript = transcriptOf(payload);
75
+ if (agent.includes("research-expert")) await cacheDocFromTranscript(transcript, cwd, home, id);
76
+ if (agent.includes("sniper")) {
77
+ await cacheSniperLessons(transcript, cwd, home);
78
+ await cacheTestResults(transcript, cwd, home);
79
+ }
80
+ return checkSolidFromTranscript(transcript, cwd);
81
+ }
82
+
83
+ /**
84
+ * Dispatch an ai-pilot-scope lifecycle event. Returns the native stdout, or
85
+ * `null` when unhandled (caller falls through to the default pipeline).
86
+ * @param home - Home dir for cache resolution (defaults to `~`; injectable for test isolation); `id` selects the harness target (defaults to "claude-code").
87
+ */
88
+ export async function dispatchAipilot(event: string, payload: Record<string, unknown>, cwd: string, now: number, home: string = homedir(), id: string = "claude-code"): Promise<string | null> {
89
+ if (event === "SubagentStart") return onSubagentStart(payload, cwd, now, home, id);
90
+ if (event === "SubagentStop") return onSubagentStop(payload, cwd, home, id);
91
+ // Stop too: Codex emits no SessionEnd, so its ai-pilot hooks.json wires Stop here as the sole analytics-flush trigger — reusing the SessionEnd handler verbatim (codex-plugins/docs/reference/hooks.md).
92
+ if (event === "SessionEnd" || event === "Stop") { await cacheAnalyticsSave(home, now); return ""; }
93
+ if (event === "PreToolUse") return docCacheGate(payload, cwd, now, home, id);
94
+ return null;
95
+ }
96
+
97
+ /** PostToolUse (Write/Edit SOLID check, else TaskCreate/TaskUpdate sync) for the ai-pilot scope; `id` selects the harness target (defaults to "claude-code"). */
98
+ export async function aipilotPostToolUse(payload: Record<string, unknown>, cwd: string, id: string = "claude-code"): Promise<string> {
99
+ return checkSolidCompliance(payload) || (await syncTaskTracking(payload, cwd, id));
100
+ }