@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,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
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * @module aipilot/doc-cache-gate
3
+ * PreToolUse gate for the ai-pilot scope: DENY a Context7/Exa doc call when the
4
+ * per-project doc cache already holds that library fresh (< 7d). Ports the
5
+ * source `doc-cache-gate.ts`. The inject side (`inject-doc.ts`) serves the same
6
+ * cache on SubagentStart; this side avoids the redundant live re-query.
7
+ * @packageDocumentation
8
+ */
9
+ import { homedir } from "node:os";
10
+ import { join } from "node:path";
11
+ import { readJsonFile } from "../../../util/json-io";
12
+ import { pathExists } from "../../../util/runtime-io";
13
+ import { denyResponse } from "../../../adapters/claude";
14
+ import { cacheDirFor, cacheAge, projectHash, DOC_CACHE_TTL_SECONDS } from "./cache-base";
15
+ import { logCacheEvent } from "./analytics";
16
+ import { canonicalizeMcpToolName } from "../../mcp-tool-name";
17
+ import type { CacheIndex } from "./types";
18
+
19
+ const GATED_TOOLS = /context7__query-docs|exa__get_code_context|exa__web_search/;
20
+
21
+ /** The library key a gated tool call targets (Context7 libraryId / Exa query), or "". */
22
+ function libraryOf(payload: Record<string, unknown>): string {
23
+ const tool = String(payload.tool_name ?? "");
24
+ const input = (payload.tool_input as Record<string, unknown> | undefined) ?? {};
25
+ if (tool.includes("context7")) {
26
+ // Real mcp__context7__query-docs input is {libraryId, query} — no "topic"
27
+ // field exists (confirmed against cache-doc.ts and mcp-key.ts's queryOf,
28
+ // which both key on `query`, never `topic`).
29
+ const libraryId = typeof input.libraryId === "string" ? input.libraryId : "";
30
+ const query = typeof input.query === "string" ? input.query : "";
31
+ return libraryId && query ? libraryId : "";
32
+ }
33
+ if (tool.includes("exa")) return typeof input.query === "string" ? input.query : "";
34
+ return "";
35
+ }
36
+
37
+ /**
38
+ * DENY a redundant doc call when the library is cached fresh, else `null`.
39
+ * @param payload - The raw PreToolUse hook payload.
40
+ * @param cwd - Fallback project root (uses `CLAUDE_PROJECT_DIR` first).
41
+ * @param now - Clock (defaults to `Date.now()`).
42
+ * @param home - Home dir (defaults to `~`).
43
+ * @param id - Harness adapter id (defaults to "claude-code"); on `"codex"`,
44
+ * `payload.tool_name` is canonicalized before the {@link GATED_TOOLS} match
45
+ * so the Codex-mangled underscore form (`mcp__context7__query_docs`) gates
46
+ * exactly like the dash form does on every other harness.
47
+ * @returns A native deny response, or `null` to fall through to the live call.
48
+ */
49
+ export async function docCacheGate(payload: Record<string, unknown>, cwd: string, now: number = Date.now(), home: string = homedir(), id: string = "claude-code"): Promise<string | null> {
50
+ const tool = canonicalizeMcpToolName(id, String(payload.tool_name ?? ""));
51
+ if (!GATED_TOOLS.test(tool)) return null;
52
+ const library = libraryOf(payload);
53
+ if (!library) return null;
54
+
55
+ const projPath = process.env.CLAUDE_PROJECT_DIR ?? cwd;
56
+ const cacheDir = cacheDirFor("doc", projPath, home);
57
+ const index = await readJsonFile<CacheIndex>(join(cacheDir, "index.json"));
58
+ const entry = index?.docs?.find((d) => d.library === library);
59
+ if (!entry?.hash || !entry.timestamp) return null;
60
+
61
+ const docFile = join(cacheDir, "docs", `${entry.hash}.md`);
62
+ if (!pathExists(docFile)) return null;
63
+ const age = cacheAge(entry.timestamp, now);
64
+ if (age >= DOC_CACHE_TTL_SECONDS) return null;
65
+
66
+ logCacheEvent("doc", "blocked", projectHash(projPath), { library }, home);
67
+ const ageH = Math.floor(age / 3600);
68
+ return denyResponse("PreToolUse", `Doc already cached at ${docFile} (age: ${ageH}h). Use the Read tool to access it instead of re-querying.`);
69
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * SubagentStart (matcher "") for the ai-pilot scope: inject APEX AGENTS.md +
3
+ * task context + cartographer paths into every sub-agent. Ports
4
+ * `inject-subagent-context.ts`. The core-scope SubagentStart already surfaces
5
+ * the MCP cache table (`subagent-cache.ts`) in a separate hook entry — this
6
+ * handler only emits the APEX/cartographer block, so there is no double-emit.
7
+ */
8
+ import { existsSync } from "node:fs";
9
+ import { join, resolve } from "node:path";
10
+ import { homedir } from "node:os";
11
+ import { readJsonFile } from "../../../util/json-io";
12
+ import { readText } from "../../../util/runtime-io";
13
+ import { resolveMaxLines } from "../../../config/limits";
14
+ import { contextResponse } from "../../../adapters/claude";
15
+ import { capFragment } from "../../inject-budget";
16
+ import { harnessHomeSegment } from "../../../policy/apex-target";
17
+ import { apexAgentSteps } from "../../../policy/apex-target-steps";
18
+ import type { ApexTaskFile } from "./types";
19
+
20
+ type TaskMap = ApexTaskFile["tasks"];
21
+
22
+ /** Last 3 completed task subjects (newest first), or "none". */
23
+ function completedTasks(tasks: TaskMap): string {
24
+ const done = Object.entries(tasks)
25
+ .filter(([, t]) => t.status === "completed")
26
+ .sort((a, b) => (b[1].completed_at ?? "").localeCompare(a[1].completed_at ?? ""))
27
+ .slice(0, 3)
28
+ .map(([id, t]) => `#${id}: ${t.subject}`);
29
+ return done.length > 0 ? done.join(", ") : "none";
30
+ }
31
+
32
+ /** Pending task subjects, or "none". */
33
+ function pendingTasks(tasks: TaskMap): string {
34
+ const pending = Object.entries(tasks)
35
+ .filter(([, t]) => t.status === "pending")
36
+ .map(([id, t]) => `#${id}: ${t.subject}`);
37
+ return pending.length > 0 ? pending.join(", ") : "none";
38
+ }
39
+
40
+ /** Cartographer navigation block, or "" when no plugin map is present. */
41
+ function cartographerContext(): string {
42
+ const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT;
43
+ if (!pluginRoot) return "";
44
+ const pluginsMap = resolve(pluginRoot, "..", ".cartographer", "index.md");
45
+ if (!existsSync(pluginsMap)) return "";
46
+ return `\n### 7. Cartographer Maps\nNavigate branches (index.md) -> leaves link to real files:\n- Plugin skills: ${pluginsMap}\n- Project files: .cartographer/project/index.md`;
47
+ }
48
+
49
+ /**
50
+ * Build the APEX sub-agent injection for SubagentStart, or "" when the project has no target apex dir (`.claude/apex/`, `.codex/apex/`, ...). Reads AGENTS.md (first 4KB) + task.json.
51
+ * @param cwd - Fallback project root when `CLAUDE_PROJECT_DIR` is unset.
52
+ * @param home - Home dir (unused placeholder; kept for symmetry/testing).
53
+ * @param id - Harness target id (defaults to "claude-code" — zero-regression default).
54
+ * @returns The native hook stdout (possibly empty).
55
+ */
56
+ export async function injectApexSubagentContext(cwd: string, home: string = homedir(), id: string = "claude-code"): Promise<string> {
57
+ void home;
58
+ const projectRoot = process.env.CLAUDE_PROJECT_DIR ?? cwd;
59
+ const seg = harnessHomeSegment(id);
60
+ const apexDir = join(projectRoot, seg, "apex");
61
+ if (!existsSync(apexDir)) return "";
62
+
63
+ const agentsPath = join(apexDir, "AGENTS.md");
64
+ const agents = existsSync(agentsPath) ? readText(agentsPath).slice(0, 4000) : "";
65
+ const taskData = await readJsonFile<ApexTaskFile>(join(apexDir, "task.json"));
66
+ const completed = taskData ? completedTasks(taskData.tasks) : "none";
67
+ const pending = taskData ? pendingTasks(taskData.tasks) : "none";
68
+ const { beforeStart, whenDone } = apexAgentSteps(id);
69
+
70
+ const context = `## APEX Sub-Agent Instructions
71
+
72
+ You are a sub-agent in APEX workflow. Follow these rules:
73
+
74
+ ### 1. AGENTS.md Rules
75
+ ${agents}
76
+
77
+ ### 2. Task Context
78
+ - Last completed: ${completed}
79
+ - Pending: ${pending}
80
+
81
+ ### 3. Before Starting Work
82
+ - ${beforeStart}
83
+
84
+ ### 4. SOLID Rules
85
+ - Files < ${resolveMaxLines()} lines | Interfaces in src/interfaces/ | JSDoc/PHPDoc required
86
+
87
+ ### 5. Research Before Code
88
+ - Use Context7/Exa for docs | Write notes to ${seg}/apex/docs/
89
+
90
+ ### 6. Before Done (NEVER skip)
91
+ - eLicit: self-review with a NAMED elicitation technique; fix findings first
92
+ - Verify: run/functional-check your changes (references⇔declarations)
93
+
94
+ ### 7. When Done
95
+ - ${whenDone}${cartographerContext()}`;
96
+
97
+ return contextResponse("SubagentStart", capFragment("apex-subagent", context));
98
+ }