@fusengine/harness 0.1.90 → 0.1.92

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (422) hide show
  1. package/dist/adapters/claude/index.d.mts +1 -1
  2. package/dist/adapters/claude/index.mjs +1 -1
  3. package/dist/adapters/cline/index.mjs +1 -1
  4. package/dist/adapters/codex/index.d.mts +2 -2
  5. package/dist/adapters/codex/index.mjs +3 -2
  6. package/dist/adapters/cursor/index.d.mts +15 -28
  7. package/dist/adapters/cursor/index.mjs +35 -46
  8. package/dist/adapters/gemini/index.mjs +1 -1
  9. package/dist/adapters/hermes/index.d.mts +1 -1
  10. package/dist/adapters/hermes/index.mjs +1 -1
  11. package/dist/adapters/kimi/index.d.mts +1 -1
  12. package/dist/adapters/kimi/index.mjs +1 -1
  13. package/dist/{claude-5bC8TDAz.mjs → claude-Ckv2_TgP.mjs} +3 -43
  14. package/dist/cli/bin.mjs +429 -16
  15. package/dist/cli/index.mjs +1 -1
  16. package/dist/command-string-CALMTnwN.mjs +42 -0
  17. package/dist/{evaluate-C1VSI5pb.mjs → evaluate-CMiIqHeH.mjs} +261 -13
  18. package/dist/{handle-CBaOcFZq.mjs → handle-BF1dZFjY.mjs} +1194 -144
  19. package/dist/{hermes-kP6NUQlG.mjs → hermes-B9-p_3IF.mjs} +3 -2
  20. package/dist/{index-DBgnIZn9.d.mts → index-B8PG82su.d.mts} +1 -1
  21. package/dist/{index-D4C2-IIc.d.mts → index-SmKYkk2N.d.mts} +1 -1
  22. package/dist/index.d.mts +2 -2
  23. package/dist/index.mjs +2 -2
  24. package/dist/init/index.d.mts +1 -1
  25. package/dist/init/index.mjs +1 -1
  26. package/dist/{kimi-D6X7b8wF.mjs → kimi-G2wcSh5-.mjs} +3 -2
  27. package/dist/normalize-Dy8g9Ybl.mjs +397 -0
  28. package/dist/policy/index.d.mts +1 -1
  29. package/dist/policy/index.mjs +2 -2
  30. package/dist/prompt/index.d.mts +1 -1
  31. package/dist/{run-CdIlUtjv.mjs → run-DkrzC0gb.mjs} +1 -1
  32. package/dist/{run-TAXNRSpD.mjs → run-GHAMqcOn.mjs} +26 -3
  33. package/dist/runtime/index.d.mts +37 -16
  34. package/dist/runtime/index.mjs +2 -2
  35. package/dist/{types-C0-igRKr.d.mts → types-Bh0jEF3_.d.mts} +2 -0
  36. package/dist/{validate-DZFtAbLP.mjs → validate-KjZ1X9tH.mjs} +1 -1
  37. package/package.json +3 -2
  38. package/src/adapters/claude/index.ts +92 -0
  39. package/src/adapters/cline/index.ts +36 -0
  40. package/src/adapters/codex/apply-patch.ts +94 -0
  41. package/src/adapters/codex/index.ts +84 -0
  42. package/src/adapters/codex/permission-mode.ts +16 -0
  43. package/src/adapters/cursor/context-budget.ts +144 -0
  44. package/src/adapters/cursor/context-limit.ts +115 -0
  45. package/src/adapters/cursor/context.ts +65 -0
  46. package/src/adapters/cursor/events.ts +38 -0
  47. package/src/adapters/cursor/index.ts +43 -0
  48. package/src/adapters/cursor/interfaces/context-budget.ts +25 -0
  49. package/src/adapters/cursor/interfaces/types.ts +88 -0
  50. package/src/adapters/cursor/native-response.ts +39 -0
  51. package/src/adapters/cursor/native-schemas.ts +161 -0
  52. package/src/adapters/cursor/normalize.ts +188 -0
  53. package/src/adapters/cursor/plugin-root.ts +103 -0
  54. package/src/adapters/cursor/respond.ts +170 -0
  55. package/src/adapters/gemini/index.ts +36 -0
  56. package/src/adapters/hermes/index.ts +55 -0
  57. package/src/adapters/hermes/interfaces/types.ts +26 -0
  58. package/src/adapters/kimi/index.ts +96 -0
  59. package/src/adapters/kimi/interfaces/types.ts +55 -0
  60. package/src/cache/compact.ts +55 -0
  61. package/src/cache/index.ts +5 -0
  62. package/src/cache/io.ts +38 -0
  63. package/src/cache/mcp-response.ts +39 -0
  64. package/src/cache/mcp-store.ts +70 -0
  65. package/src/cache/store.ts +93 -0
  66. package/src/changelog/fetch.ts +79 -0
  67. package/src/cli/bin.ts +100 -0
  68. package/src/cli/cursor-event-scanner.ts +175 -0
  69. package/src/cli/cursor-stdin-reader.ts +65 -0
  70. package/src/cli/doctor.ts +96 -0
  71. package/src/cli/hook-io.ts +130 -0
  72. package/src/cli/hook-sound.ts +49 -0
  73. package/src/cli/index.ts +1 -0
  74. package/src/cli/json-primitive-scanner.ts +83 -0
  75. package/src/cli/run.ts +36 -0
  76. package/src/cli/scope.ts +31 -0
  77. package/src/codex-rules/build.ts +63 -0
  78. package/src/codex-rules/index.ts +8 -0
  79. package/src/codex-rules/mapping.ts +42 -0
  80. package/src/codex-rules/rules/absolute-paths.ts +52 -0
  81. package/src/codex-rules/rules/ask.ts +44 -0
  82. package/src/codex-rules/rules/chmod-ask.ts +57 -0
  83. package/src/codex-rules/rules/dd.ts +23 -0
  84. package/src/codex-rules/rules/diskutil.ts +33 -0
  85. package/src/codex-rules/rules/mkfs.ts +42 -0
  86. package/src/codex-rules/rules/privilege.ts +49 -0
  87. package/src/codex-rules/rules/remote-fetch.ts +31 -0
  88. package/src/codex-rules/rules/rm-variants.ts +58 -0
  89. package/src/codex-rules/rules/single-commands.ts +57 -0
  90. package/src/codex-rules/skip-list.ts +25 -0
  91. package/src/codex-rules/starlark.ts +46 -0
  92. package/src/codex-rules/types.ts +36 -0
  93. package/src/config/dotenv.ts +55 -0
  94. package/src/config/env.ts +10 -0
  95. package/src/config/home-dir.ts +36 -0
  96. package/src/config/index.ts +5 -0
  97. package/src/config/layout.ts +52 -0
  98. package/src/config/limits.ts +56 -0
  99. package/src/config/ttl.ts +30 -0
  100. package/src/detect/harness.ts +73 -0
  101. package/src/detect/index.ts +1 -0
  102. package/src/detect/interfaces/types.ts +18 -0
  103. package/src/freshness/agent-evidence-record.ts +95 -0
  104. package/src/freshness/agent-evidence.ts +95 -0
  105. package/src/freshness/codex-spawn-evidence.ts +91 -0
  106. package/src/freshness/doc-helpers.ts +90 -0
  107. package/src/freshness/evidence-harvest-io.ts +72 -0
  108. package/src/freshness/evidence-harvest.ts +61 -0
  109. package/src/freshness/explore-tools.ts +95 -0
  110. package/src/freshness/index.ts +2 -0
  111. package/src/freshness/query-framework.ts +73 -0
  112. package/src/freshness/ref-evidence.ts +49 -0
  113. package/src/freshness/ref-journal.ts +98 -0
  114. package/src/freshness/trivial-edit-counter.ts +30 -0
  115. package/src/index.ts +11 -0
  116. package/src/init/index.ts +2 -0
  117. package/src/init/run.ts +32 -0
  118. package/src/init/templates.ts +75 -0
  119. package/src/memory/gitignore.ts +14 -0
  120. package/src/memory/index.ts +3 -0
  121. package/src/memory/registry.ts +39 -0
  122. package/src/memory/session-roots.ts +99 -0
  123. package/src/memory/state.ts +58 -0
  124. package/src/policy/apex-authorization.ts +100 -0
  125. package/src/policy/apex-gates.ts +91 -0
  126. package/src/policy/apex-target-steps.ts +73 -0
  127. package/src/policy/apex-target.ts +49 -0
  128. package/src/policy/apex-task-context.ts +81 -0
  129. package/src/policy/apex.ts +94 -0
  130. package/src/policy/cartographer/build-tree.ts +69 -0
  131. package/src/policy/cartographer/describe.ts +53 -0
  132. package/src/policy/cartographer/entry.ts +38 -0
  133. package/src/policy/cartographer/frontmatter.ts +48 -0
  134. package/src/policy/cartographer/indicators.ts +34 -0
  135. package/src/policy/claude-md-context.ts +66 -0
  136. package/src/policy/conventions/index.ts +11 -0
  137. package/src/policy/conventions/interfaces.ts +71 -0
  138. package/src/policy/conventions/langs.ts +53 -0
  139. package/src/policy/conventions/query.ts +49 -0
  140. package/src/policy/conventions/react-hooks.ts +18 -0
  141. package/src/policy/conventions/store-or-query.ts +21 -0
  142. package/src/policy/conventions/stores.ts +56 -0
  143. package/src/policy/conventions/strip.ts +87 -0
  144. package/src/policy/conventions/verdict.ts +38 -0
  145. package/src/policy/creation-intent.ts +11 -0
  146. package/src/policy/deny-loop.ts +88 -0
  147. package/src/policy/design/allowed-write.ts +28 -0
  148. package/src/policy/design/content-checks.ts +45 -0
  149. package/src/policy/design/corpus-fs.ts +21 -0
  150. package/src/policy/design/corpus-resolve-cache.ts +49 -0
  151. package/src/policy/design/corpus-resolve.ts +99 -0
  152. package/src/policy/design/corpus.ts +92 -0
  153. package/src/policy/design/design-system-rules.ts +45 -0
  154. package/src/policy/design/flag.ts +30 -0
  155. package/src/policy/design/gates-pipeline.ts +105 -0
  156. package/src/policy/design/gates.ts +87 -0
  157. package/src/policy/design/screenshot-tools.ts +24 -0
  158. package/src/policy/design/skill-evidence.ts +34 -0
  159. package/src/policy/design/skill-gate.ts +81 -0
  160. package/src/policy/design/skill-triggers.ts +51 -0
  161. package/src/policy/design/state.ts +92 -0
  162. package/src/policy/design/template-urls.ts +54 -0
  163. package/src/policy/design/transitions.ts +68 -0
  164. package/src/policy/detect-claude-md-project-type.ts +32 -0
  165. package/src/policy/detect-framework.ts +99 -0
  166. package/src/policy/detect-primitive-checks.ts +100 -0
  167. package/src/policy/detect-primitive-lib.ts +55 -0
  168. package/src/policy/detect-project.ts +99 -0
  169. package/src/policy/edit-outcome.ts +53 -0
  170. package/src/policy/evaluate.ts +81 -0
  171. package/src/policy/expert-agents.ts +60 -0
  172. package/src/policy/file-size-scope.ts +32 -0
  173. package/src/policy/file-size.ts +99 -0
  174. package/src/policy/framework-solid-exclude.ts +52 -0
  175. package/src/policy/framework-solid-extended.ts +93 -0
  176. package/src/policy/framework-solid-gates-systems.ts +99 -0
  177. package/src/policy/framework-solid-gates.ts +98 -0
  178. package/src/policy/framework-solid.ts +88 -0
  179. package/src/policy/gemini-mcp-gate.ts +83 -0
  180. package/src/policy/git-gates.ts +40 -0
  181. package/src/policy/guards/bash-command-anchor.ts +40 -0
  182. package/src/policy/guards/bash-write-patterns.ts +140 -0
  183. package/src/policy/guards/bash-write-redirects.ts +201 -0
  184. package/src/policy/guards/bash-write-safe-paths.ts +62 -0
  185. package/src/policy/guards/bash-write.ts +76 -0
  186. package/src/policy/guards/context.ts +12 -0
  187. package/src/policy/guards/index.ts +61 -0
  188. package/src/policy/guards/install.ts +22 -0
  189. package/src/policy/guards/interface-separation-ext.ts +41 -0
  190. package/src/policy/guards/interface-separation.ts +100 -0
  191. package/src/policy/guards/protected-path.ts +100 -0
  192. package/src/policy/guards/security.ts +97 -0
  193. package/src/policy/index.ts +19 -0
  194. package/src/policy/interfaces/types.ts +46 -0
  195. package/src/policy/js-gate-route.ts +51 -0
  196. package/src/policy/lessons/lesson-gate.ts +0 -0
  197. package/src/policy/lessons/trigger-index.ts +84 -0
  198. package/src/policy/lessons/types.ts +31 -0
  199. package/src/policy/module-layout.ts +31 -0
  200. package/src/policy/nearest-manifest.ts +75 -0
  201. package/src/policy/never-approval.ts +95 -0
  202. package/src/policy/patterns.ts +80 -0
  203. package/src/policy/seo/validate.ts +51 -0
  204. package/src/policy/shadcn-project.ts +22 -0
  205. package/src/policy/shadcn-skill-gate.ts +97 -0
  206. package/src/policy/shell-read-refs.ts +96 -0
  207. package/src/policy/skill-path.ts +46 -0
  208. package/src/policy/skill-patterns/laravel.ts +40 -0
  209. package/src/policy/skill-patterns/nextjs.ts +29 -0
  210. package/src/policy/skill-patterns/react.ts +27 -0
  211. package/src/policy/skill-patterns/shadcn-subskills.ts +33 -0
  212. package/src/policy/skill-patterns/shadcn.ts +66 -0
  213. package/src/policy/skill-patterns/swift.ts +30 -0
  214. package/src/policy/skill-patterns/tailwind.ts +40 -0
  215. package/src/policy/skill-source.ts +52 -0
  216. package/src/policy/skill-trigger-patterns.ts +33 -0
  217. package/src/policy/skill-triggers.ts +108 -0
  218. package/src/policy/tailwind-skill-gate.ts +63 -0
  219. package/src/policy/trivial-edits.ts +52 -0
  220. package/src/policy/verbosity.ts +39 -0
  221. package/src/prompt/index.ts +1 -0
  222. package/src/prompt/types.ts +36 -0
  223. package/src/refs/discover.ts +69 -0
  224. package/src/refs/frontmatter.ts +24 -0
  225. package/src/refs/index.ts +4 -0
  226. package/src/refs/loader.ts +59 -0
  227. package/src/refs/ref-key.ts +41 -0
  228. package/src/refs/router.ts +63 -0
  229. package/src/refs/types.ts +25 -0
  230. package/src/runtime/activity.ts +99 -0
  231. package/src/runtime/apply-patch-gate.ts +41 -0
  232. package/src/runtime/burst-window.ts +21 -0
  233. package/src/runtime/codex-shell-tool.ts +37 -0
  234. package/src/runtime/command-string.ts +37 -0
  235. package/src/runtime/confirm/codex-confirm.ts +126 -0
  236. package/src/runtime/confirm/codex-prompt-origin.ts +16 -0
  237. package/src/runtime/confirm/confirm-code.ts +33 -0
  238. package/src/runtime/confirm/confirm-gate.ts +65 -0
  239. package/src/runtime/confirm/confirm-irreversible.ts +32 -0
  240. package/src/runtime/confirm/confirm-pending.ts +34 -0
  241. package/src/runtime/confirm/confirm-state.ts +56 -0
  242. package/src/runtime/confirm/confirm-subagent.ts +112 -0
  243. package/src/runtime/confirm/confirm-submit.ts +38 -0
  244. package/src/runtime/core-guards-active.ts +84 -0
  245. package/src/runtime/core-guards-wired.ts +47 -0
  246. package/src/runtime/deny-loop-store.ts +94 -0
  247. package/src/runtime/deny-notice.ts +52 -0
  248. package/src/runtime/design-cache-resolve.ts +51 -0
  249. package/src/runtime/design-content-gate.ts +95 -0
  250. package/src/runtime/design-files-gate.ts +114 -0
  251. package/src/runtime/design-helpers.ts +93 -0
  252. package/src/runtime/design-lifecycle.ts +39 -0
  253. package/src/runtime/design-read-credit.ts +28 -0
  254. package/src/runtime/design.ts +116 -0
  255. package/src/runtime/dev-context.ts +43 -0
  256. package/src/runtime/dry-patterns.ts +32 -0
  257. package/src/runtime/dry.ts +96 -0
  258. package/src/runtime/fragment-registry.ts +33 -0
  259. package/src/runtime/framework-skill-gate.ts +79 -0
  260. package/src/runtime/fs-cleanup.ts +71 -0
  261. package/src/runtime/gate-apex.ts +81 -0
  262. package/src/runtime/gate-helpers.ts +52 -0
  263. package/src/runtime/gate-input.ts +26 -0
  264. package/src/runtime/gate.ts +128 -0
  265. package/src/runtime/handle-post.ts +104 -0
  266. package/src/runtime/handle-pre.ts +137 -0
  267. package/src/runtime/handle-scope-async.ts +29 -0
  268. package/src/runtime/handle-types.ts +27 -0
  269. package/src/runtime/handle.ts +197 -0
  270. package/src/runtime/home-state.ts +55 -0
  271. package/src/runtime/index.ts +18 -0
  272. package/src/runtime/inform.ts +28 -0
  273. package/src/runtime/inject-budget-recap.ts +47 -0
  274. package/src/runtime/inject-budget.ts +81 -0
  275. package/src/runtime/inject-context.ts +85 -0
  276. package/src/runtime/inject-dedup-exclusive.ts +98 -0
  277. package/src/runtime/inject-dedup.ts +89 -0
  278. package/src/runtime/is-agent-tool.ts +19 -0
  279. package/src/runtime/lifecycle/agent-files.ts +49 -0
  280. package/src/runtime/lifecycle/agent-memory.ts +82 -0
  281. package/src/runtime/lifecycle/agent-transcript.ts +80 -0
  282. package/src/runtime/lifecycle/aipilot/analytics.ts +99 -0
  283. package/src/runtime/lifecycle/aipilot/apex-task-store.ts +61 -0
  284. package/src/runtime/lifecycle/aipilot/cache-base.ts +48 -0
  285. package/src/runtime/lifecycle/aipilot/cache-doc.ts +100 -0
  286. package/src/runtime/lifecycle/aipilot/cache-lessons.ts +58 -0
  287. package/src/runtime/lifecycle/aipilot/cache-test.ts +70 -0
  288. package/src/runtime/lifecycle/aipilot/curate-lessons.ts +64 -0
  289. package/src/runtime/lifecycle/aipilot/dispatch-aipilot.ts +106 -0
  290. package/src/runtime/lifecycle/aipilot/doc-cache-gate.ts +69 -0
  291. package/src/runtime/lifecycle/aipilot/inject-apex.ts +98 -0
  292. package/src/runtime/lifecycle/aipilot/inject-doc.ts +62 -0
  293. package/src/runtime/lifecycle/aipilot/inject-explore.ts +64 -0
  294. package/src/runtime/lifecycle/aipilot/inject-lessons.ts +61 -0
  295. package/src/runtime/lifecycle/aipilot/inject-test.ts +48 -0
  296. package/src/runtime/lifecycle/aipilot/lesson-archive.ts +60 -0
  297. package/src/runtime/lifecycle/aipilot/lesson-inject.ts +98 -0
  298. package/src/runtime/lifecycle/aipilot/lesson-parse.ts +74 -0
  299. package/src/runtime/lifecycle/aipilot/lessons.ts +72 -0
  300. package/src/runtime/lifecycle/aipilot/promote-global-lessons.ts +92 -0
  301. package/src/runtime/lifecycle/aipilot/solid-compliance.ts +69 -0
  302. package/src/runtime/lifecycle/aipilot/solid-notice.ts +47 -0
  303. package/src/runtime/lifecycle/aipilot/solid-transcript.ts +60 -0
  304. package/src/runtime/lifecycle/aipilot/source-scan.ts +56 -0
  305. package/src/runtime/lifecycle/aipilot/sync-task.ts +75 -0
  306. package/src/runtime/lifecycle/aipilot/transcript.ts +88 -0
  307. package/src/runtime/lifecycle/aipilot/types.ts +72 -0
  308. package/src/runtime/lifecycle/auto-document-reads.ts +73 -0
  309. package/src/runtime/lifecycle/cartographer/detect.ts +57 -0
  310. package/src/runtime/lifecycle/cartographer/ecosystem-map.ts +56 -0
  311. package/src/runtime/lifecycle/cartographer/fs-util.ts +64 -0
  312. package/src/runtime/lifecycle/cartographer/merge.ts +51 -0
  313. package/src/runtime/lifecycle/cartographer/project-map.ts +49 -0
  314. package/src/runtime/lifecycle/cartographer/scan-hooks.ts +48 -0
  315. package/src/runtime/lifecycle/cartographer/scan.ts +81 -0
  316. package/src/runtime/lifecycle/cartographer/session-start.ts +33 -0
  317. package/src/runtime/lifecycle/cartographer/track-enrichment.ts +45 -0
  318. package/src/runtime/lifecycle/cartographer/write-plugin-map.ts +43 -0
  319. package/src/runtime/lifecycle/cartographer/write-tree.ts +44 -0
  320. package/src/runtime/lifecycle/changelog-research.ts +43 -0
  321. package/src/runtime/lifecycle/check-file-size.ts +61 -0
  322. package/src/runtime/lifecycle/codex-resync/fingerprint.ts +63 -0
  323. package/src/runtime/lifecycle/codex-resync/lock.ts +46 -0
  324. package/src/runtime/lifecycle/codex-resync/managed-destination.ts +39 -0
  325. package/src/runtime/lifecycle/codex-resync/materialize-agents.ts +57 -0
  326. package/src/runtime/lifecycle/codex-resync/plugin-files.ts +65 -0
  327. package/src/runtime/lifecycle/codex-resync/plugin-roots.ts +62 -0
  328. package/src/runtime/lifecycle/codex-resync/resync.ts +45 -0
  329. package/src/runtime/lifecycle/codex-resync/symlink-commands.ts +25 -0
  330. package/src/runtime/lifecycle/dispatch.ts +109 -0
  331. package/src/runtime/lifecycle/failure-lesson.ts +55 -0
  332. package/src/runtime/lifecycle/index.ts +34 -0
  333. package/src/runtime/lifecycle/inject-rules.ts +50 -0
  334. package/src/runtime/lifecycle/instructions-loaded.ts +22 -0
  335. package/src/runtime/lifecycle/kimi-rules-native.ts +44 -0
  336. package/src/runtime/lifecycle/lessons/dispatch.ts +88 -0
  337. package/src/runtime/lifecycle/lessons/reminder.ts +89 -0
  338. package/src/runtime/lifecycle/lessons/state.ts +23 -0
  339. package/src/runtime/lifecycle/memory/agent-lesson.ts +35 -0
  340. package/src/runtime/lifecycle/memory/capture-error.ts +41 -0
  341. package/src/runtime/lifecycle/memory/client.ts +65 -0
  342. package/src/runtime/lifecycle/memory/dispatch.ts +42 -0
  343. package/src/runtime/lifecycle/memory/recall.ts +30 -0
  344. package/src/runtime/lifecycle/memory/salience.ts +31 -0
  345. package/src/runtime/lifecycle/memory/state.ts +49 -0
  346. package/src/runtime/lifecycle/memory/track-ops.ts +15 -0
  347. package/src/runtime/lifecycle/post-compact.ts +38 -0
  348. package/src/runtime/lifecycle/post-edit-ts.ts +49 -0
  349. package/src/runtime/lifecycle/post-tracking.ts +40 -0
  350. package/src/runtime/lifecycle/pre-compact.ts +38 -0
  351. package/src/runtime/lifecycle/rules-root.ts +99 -0
  352. package/src/runtime/lifecycle/security/check-skill.ts +63 -0
  353. package/src/runtime/lifecycle/security/scan-patterns.ts +53 -0
  354. package/src/runtime/lifecycle/security/scan.ts +76 -0
  355. package/src/runtime/lifecycle/security/skill-state.ts +47 -0
  356. package/src/runtime/lifecycle/security/track-mcp.ts +29 -0
  357. package/src/runtime/lifecycle/security/track-skill-read.ts +31 -0
  358. package/src/runtime/lifecycle/seo/post-tool-use.ts +45 -0
  359. package/src/runtime/lifecycle/session-end.ts +17 -0
  360. package/src/runtime/lifecycle/session-start.ts +57 -0
  361. package/src/runtime/lifecycle/snapshot/board.ts +27 -0
  362. package/src/runtime/lifecycle/snapshot/format.ts +60 -0
  363. package/src/runtime/lifecycle/snapshot/git.ts +71 -0
  364. package/src/runtime/lifecycle/snapshot/index.ts +52 -0
  365. package/src/runtime/lifecycle/snapshot/version.ts +34 -0
  366. package/src/runtime/lifecycle/solid-detect.ts +56 -0
  367. package/src/runtime/lifecycle/stop-core.ts +35 -0
  368. package/src/runtime/lifecycle/subagent-cache.ts +61 -0
  369. package/src/runtime/lifecycle/task-completed.ts +94 -0
  370. package/src/runtime/lifecycle/teammate-idle-check.ts +63 -0
  371. package/src/runtime/lifecycle/teammate-idle.ts +28 -0
  372. package/src/runtime/lifecycle/tool-failure.ts +24 -0
  373. package/src/runtime/lifecycle/track-changes.ts +61 -0
  374. package/src/runtime/lifecycle/validate-solid.ts +70 -0
  375. package/src/runtime/lifecycle/validate-tailwind.ts +80 -0
  376. package/src/runtime/lifecycle-bridge.ts +74 -0
  377. package/src/runtime/mcp-key.ts +44 -0
  378. package/src/runtime/mcp-tool-name.ts +92 -0
  379. package/src/runtime/mcp.ts +91 -0
  380. package/src/runtime/modular.ts +77 -0
  381. package/src/runtime/normalize.ts +107 -0
  382. package/src/runtime/notices.ts +91 -0
  383. package/src/runtime/notification-sound.ts +49 -0
  384. package/src/runtime/notifications.ts +77 -0
  385. package/src/runtime/paths.ts +59 -0
  386. package/src/runtime/post-fanout.ts +41 -0
  387. package/src/runtime/post-outcome.ts +61 -0
  388. package/src/runtime/pre-allow.ts +97 -0
  389. package/src/runtime/precommit.ts +60 -0
  390. package/src/runtime/prompt-text.ts +38 -0
  391. package/src/runtime/receipt-capture.ts +34 -0
  392. package/src/runtime/record.ts +26 -0
  393. package/src/runtime/respond.ts +92 -0
  394. package/src/runtime/solid-file-size-gate.ts +58 -0
  395. package/src/runtime/solid-pre.ts +53 -0
  396. package/src/runtime/storage.ts +9 -0
  397. package/src/statusline/colors.ts +31 -0
  398. package/src/statusline/constants.ts +17 -0
  399. package/src/statusline/formatters.ts +54 -0
  400. package/src/statusline/index.ts +4 -0
  401. package/src/statusline/progress-bar.ts +46 -0
  402. package/src/tracking/codex-post-failure.ts +73 -0
  403. package/src/tracking/index.ts +2 -0
  404. package/src/tracking/integrity.ts +99 -0
  405. package/src/tracking/one-shot-dedup.ts +34 -0
  406. package/src/tracking/one-shot-failure.ts +42 -0
  407. package/src/tracking/one-shot-store.ts +95 -0
  408. package/src/tracking/one-shot.ts +93 -0
  409. package/src/tracking/receipts.ts +95 -0
  410. package/src/tracking/session-state.ts +94 -0
  411. package/src/tracking/store.ts +96 -0
  412. package/src/tracking/track-compact.ts +100 -0
  413. package/src/tracking/track-diff.ts +50 -0
  414. package/src/tracking/track-journal.ts +97 -0
  415. package/src/tracking/track-lock-sync.ts +83 -0
  416. package/src/tracking/track-lock.ts +67 -0
  417. package/src/util/compact-json.ts +23 -0
  418. package/src/util/index.ts +2 -0
  419. package/src/util/json-io.ts +47 -0
  420. package/src/util/project-root.ts +41 -0
  421. package/src/util/runtime-io.ts +85 -0
  422. package/src/util/semver.ts +30 -0
@@ -0,0 +1,104 @@
1
+ import { extractText } from "../cache/mcp-response";
2
+ import { activityFor } from "./activity";
3
+ import { mcpPostStore } from "./mcp";
4
+ import { recordActivity } from "./record";
5
+ import { designGate } from "./design";
6
+ import { postEditContext } from "./lifecycle-bridge";
7
+ import { postTrackingSideEffects } from "./lifecycle/post-tracking";
8
+ import { aipilotPostToolUse, checkFileSize, validateTailwind } from "./lifecycle";
9
+ import { seoPostToolUseResponse } from "./lifecycle/seo/post-tool-use";
10
+ import { classifyAgentEvidence, recordAgentEvidence } from "../freshness/agent-evidence-record";
11
+ import { recordCodexSpawnEvidence } from "../freshness/codex-spawn-evidence";
12
+ import { captureBashReceipt } from "./receipt-capture";
13
+ import { recordCodexPostFailure } from "../tracking/codex-post-failure";
14
+ import { defaultStateDir } from "./paths";
15
+ import { fanOutFiles, firstFileMatch } from "./post-fanout";
16
+ import { postOutcome } from "./post-outcome";
17
+ import type { PreContext } from "./handle-pre";
18
+ import type { HandleOutcome } from "./handle";
19
+
20
+ function isCursorAfterFileEdit(id: string, payload: Record<string, unknown>): boolean {
21
+ if (id !== "cursor") return false;
22
+ const hookEvent = typeof payload.hook_event_name === "string" ? payload.hook_event_name : "";
23
+ if (hookEvent) return /^afterFileEdit$/i.test(hookEvent);
24
+ return typeof payload.file_path === "string" && Array.isArray(payload.edits);
25
+ }
26
+
27
+ /**
28
+ * Run the PostToolUse pipeline: store the MCP response, emit a design warning,
29
+ * record the activity into the session track, apply per-scope side-effects (SEO
30
+ * deny, aipilot task cache), then inject the post-edit context.
31
+ *
32
+ * POST is advisory-only for the design pipeline: it can never undo a tool
33
+ * that already ran (the hard block lives in the PreToolUse `designFilesGate`).
34
+ * `designGate` therefore stays on the RAW, un-fanned event — its
35
+ * `recordPost` apply_patch branch is promote-only and resolves relative
36
+ * `design-system.md` paths via `join(cwd, …)` (design-helpers.ts); fanning
37
+ * that call would instead route the file through the Write/Edit branch,
38
+ * which reads `event.filePath` UNRESOLVED (breaking cwd-relative promotion)
39
+ * and can DEGRADE the state — both forbidden by the apply_patch promote-only
40
+ * doctrine (see `design-files-gate.ts` module doc). Only `designPassNotice`
41
+ * (pure formatting, no disk access, no state write) is fanned via
42
+ * {@link fanOutFiles}, so `apply_patch` gets one notice line per real file
43
+ * instead of none.
44
+ * @param ctx - The resolved context (same shape as the pre pipeline).
45
+ * @returns The native hook outcome.
46
+ */
47
+ export async function handlePost(ctx: PreContext): Promise<HandleOutcome> {
48
+ const { id, payload, event, framework, mcpDir, file, opts } = ctx;
49
+ const cursorAfterFileEdit = isCursorAfterFileEdit(id, payload);
50
+ const designCacheDir = ctx.designCacheDir ?? mcpDir;
51
+ let response = payload.tool_response ?? payload.tool_output;
52
+ if (id === "cursor" && /^afterMCPExecution$/i.test(event.eventName ?? "")) {
53
+ try {
54
+ response = typeof payload.result_json === "string" ? JSON.parse(payload.result_json) : payload.result_json;
55
+ } catch {
56
+ response = undefined;
57
+ }
58
+ }
59
+ mcpPostStore(event.tool, event.input, response, mcpDir);
60
+ const designWarn = designGate(payload, event, designCacheDir, opts.cwd, opts.corpusRoot);
61
+ const activities = activityFor({ tool: event.tool, input: event.input, sessionId: event.sessionId, framework, now: opts.now, responseLength: extractText(response).length });
62
+ for (const activity of activities) await recordActivity(file, activity);
63
+ // Session-scoped evidence (parity track-subagent-research.py): sub-agent hooks
64
+ // carry the LEAD's session_id — Task/Agent launches excluded (credited above).
65
+ const evidence = classifyAgentEvidence(event.tool, event.input, response);
66
+ if (evidence) await recordAgentEvidence(file, evidence, opts.now, typeof payload.agent_id === "string" ? payload.agent_id : undefined);
67
+ // Codex multi_agent_v2 `spawn_agent` -> same session track (no-op for every
68
+ // other harness / non-spawn tool / missing `agent_type`; see module doc).
69
+ await recordCodexSpawnEvidence(file, id, event.tool, event.input, opts.now);
70
+ // Verification receipts (tsc/bun test runs) — structured responses only
71
+ // (Kimi's string `tool_output` would forge a success receipt; see module).
72
+ await captureBashReceipt(file, event.tool, event.command, payload.tool_result, response, opts.now);
73
+ if (id === "codex") recordCodexPostFailure(event.tool, payload.tool_result ?? response, { now: opts.now, dir: defaultStateDir(opts.cwd), sessionId: event.sessionId });
74
+ // Codex `apply_patch` and Cursor `afterFileEdit` fan into per-file events for
75
+ // tracking, validation, post-edit context, and notices.
76
+ const files = fanOutFiles(event);
77
+ for (const f of files) postTrackingSideEffects(opts.scope ?? "core", f, f.input, opts.now, payload, opts.cwd);
78
+ const seoDeny = opts.scope === "seo" ? seoPostToolUseResponse(payload) : null;
79
+ if (seoDeny && !cursorAfterFileEdit) return { stdout: seoDeny, exit: 0 };
80
+ if (opts.scope === "solid") {
81
+ const solidWarn = firstFileMatch(files, checkFileSize);
82
+ if (solidWarn && !cursorAfterFileEdit) return { stdout: solidWarn, exit: 0 };
83
+ }
84
+ if (opts.scope === "tailwindcss") {
85
+ const tailwindWarn = firstFileMatch(files, validateTailwind);
86
+ if (tailwindWarn && !cursorAfterFileEdit) return { stdout: tailwindWarn, exit: 0 };
87
+ }
88
+ if (opts.scope === "aipilot" && (event.tool === "TaskCreate" || event.tool === "TaskUpdate" || event.tool === "Write" || event.tool === "Edit")) {
89
+ const out = await aipilotPostToolUse(payload, opts.cwd, id);
90
+ if (out && !cursorAfterFileEdit) return { stdout: out, exit: 0 };
91
+ }
92
+ let extra = "";
93
+ for (const f of files) {
94
+ extra = await postEditContext(opts.scope ?? "core", f, opts.now, id);
95
+ if (extra) break;
96
+ }
97
+ return postOutcome({
98
+ id, agentId: typeof payload.agent_id === "string" ? payload.agent_id : "", sessionId: event.sessionId,
99
+ now: opts.now, cwd: opts.cwd, activities, files,
100
+ designCacheDir, designWarn, extra,
101
+ cursorAfterFileEdit,
102
+ cursorEventName: typeof payload.hook_event_name === "string" ? payload.hook_event_name : "",
103
+ });
104
+ }
@@ -0,0 +1,137 @@
1
+ import { dirname } from "node:path";
2
+ import { loadRefs } from "../refs/loader";
3
+ import { gate, gateCommandCandidates } from "./gate";
4
+ import { MCP_TTL_MS, mcpPreIntercept } from "./mcp";
5
+ import type { NormalizedEvent } from "./normalize";
6
+ import { recordActivity } from "./record";
7
+ import { respond } from "./respond";
8
+ import { withDenyNotice } from "./deny-notice";
9
+ import { designGate } from "./design";
10
+ import { taskContext } from "./inject-context";
11
+ import { securityAdvisory, securityAdvisoryForPatch } from "./lifecycle/security/check-skill";
12
+ import { solidScopeOutcome } from "./solid-pre";
13
+ import { isAgentTool } from "./is-agent-tool";
14
+ import { allowOutcome } from "./pre-allow";
15
+ import { applyPatchGate } from "./apply-patch-gate";
16
+ import { isBypassPermissions } from "../adapters/codex/permission-mode";
17
+ import { evaluate } from "../policy/evaluate";
18
+ import { confirmGate } from "./confirm/confirm-gate";
19
+ import type { HandleOptions, HandleOutcome } from "./handle";
20
+
21
+ /** Context the PreToolUse pipeline needs (resolved once by {@link handleHook}). */
22
+ export interface PreContext {
23
+ id: string;
24
+ payload: Record<string, unknown>;
25
+ event: NormalizedEvent;
26
+ framework: string;
27
+ mcpDir: string;
28
+ /**
29
+ * Session-anchored design-pipeline cache dir (see design-cache-resolve.ts) —
30
+ * distinct from `mcpDir`. Optional: pre-existing context literals (tests
31
+ * built before this field existed) omit it and fall back to `mcpDir`
32
+ * unchanged, so they keep their old, already-passing behavior verbatim.
33
+ */
34
+ designCacheDir?: string;
35
+ file: string;
36
+ opts: HandleOptions;
37
+ }
38
+
39
+ /**
40
+ * Run the PreToolUse pipeline: MCP/WebFetch cache intercept, design gate, APEX Task context injection, then the stateless+APEX gate chain, returning the native hook outcome (deny/ask/inject or allow).
41
+ * @param ctx - The resolved pre-context.
42
+ * @returns The hook outcome.
43
+ */
44
+ export async function handlePre(ctx: PreContext): Promise<HandleOutcome> {
45
+ const { id, payload, event, framework, mcpDir, file, opts } = ctx;
46
+ const designCacheDir = ctx.designCacheDir ?? mcpDir;
47
+ const intercept = mcpPreIntercept(id, event.tool, event.input, mcpDir, MCP_TTL_MS, opts.now);
48
+ if (intercept !== null) {
49
+ if (intercept.docSource) await recordActivity(file, { kind: "doc", framework, sessionId: event.sessionId, source: intercept.docSource });
50
+ return { stdout: intercept.stdout, exit: 0 };
51
+ }
52
+
53
+ const designBlock = designGate(payload, event, designCacheDir, opts.cwd, opts.corpusRoot);
54
+ if (designBlock) return { stdout: withDenyNotice(id, respond(id, designBlock, event.eventName ?? "PreToolUse"), designBlock, event.sessionId, dirname(file), opts.now), exit: 0 };
55
+
56
+ if (id === "cursor" && (event.eventName === "beforeReadFile" || event.eventName === "beforeTabFileRead")) {
57
+ const readPolicy = evaluate({ tool: event.tool, filePath: event.filePath });
58
+ return {
59
+ stdout: readPolicy.prompt
60
+ ? respond(id, readPolicy.prompt, event.eventName)
61
+ : JSON.stringify({ permission: "allow" }),
62
+ exit: 0,
63
+ };
64
+ }
65
+
66
+ // Security scope is advisory-only (ports check-security-skill.py): emit the
67
+ // non-blocking advisory when the skill is unread, else allow — NEVER run the
68
+ // core APEX/SOLID/file-size gate chain (the security plugin never did).
69
+ if (opts.scope === "security") {
70
+ return { stdout: event.files?.length ? securityAdvisoryForPatch(event.files, opts.now) : securityAdvisory(event.tool, event.filePath ?? "", opts.now), exit: 0 };
71
+ }
72
+
73
+ // Solid scope mirrors security above: run ONLY the solid-scope gates
74
+ // (file-size deny, then Go/Python interface-location) and ALWAYS return —
75
+ // NEVER the core gate chain: core-guards owns it, so falling through would
76
+ // run gate() twice per edit (duplicate denies + added latency).
77
+ if (opts.scope === "solid") {
78
+ return solidScopeOutcome(id, event, file, opts.now);
79
+ }
80
+
81
+ // PreToolUse sub-agent dispatch (Task on claude/codex, Agent/AgentSwarm on
82
+ // kimi): inject APEX sub-agent context when the target apex dir exists.
83
+ if (isAgentTool(event.tool)) {
84
+ const taskCtx = taskContext(opts.cwd, id);
85
+ if (taskCtx) return { stdout: taskCtx, exit: 0 };
86
+ }
87
+
88
+ // Codex `apply_patch`: normalize.ts fanned the freeform patch into per-file
89
+ // changes. OR their static verdicts — one violating hunk blocks the whole
90
+ // envelope. `event.files` is undefined for every other tool/harness.
91
+ if (event.files && event.files.length > 0) {
92
+ const patchPrompt = applyPatchGate(event.files, opts.cwd);
93
+ if (patchPrompt) return { stdout: withDenyNotice(id, respond(id, patchPrompt, event.eventName ?? "PreToolUse"), patchPrompt, event.sessionId, dirname(file), opts.now), exit: 0 };
94
+ }
95
+
96
+ const refs = opts.refsDir ? await loadRefs(opts.refsDir) : undefined;
97
+ const gateInput = {
98
+ sessionId: event.sessionId,
99
+ framework,
100
+ tool: event.tool,
101
+ filePath: event.filePath,
102
+ content: event.content,
103
+ command: event.command,
104
+ cwd: opts.cwd,
105
+ refs,
106
+ isReplaceAll: event.input.replace_all === true,
107
+ oldString: event.oldString,
108
+ agentType: event.agentType,
109
+ agentId: typeof payload.agent_id === "string" ? payload.agent_id : undefined,
110
+ windowMs: opts.windowMs,
111
+ now: opts.now,
112
+ trackFile: file,
113
+ transcriptPath: typeof payload.transcript_path === "string" ? payload.transcript_path : undefined,
114
+ neverApproval: id === "codex" && isBypassPermissions(event.permissionMode),
115
+ };
116
+ const prompt = id === "cursor" && (event.commandCandidates?.length ?? 0) > 1
117
+ ? await gateCommandCandidates(gateInput, event.commandCandidates!)
118
+ : await gate(gateInput);
119
+ if (prompt) {
120
+ // CONFIRM <code> flow: ONLY changes anything when Codex/Kimi are about to
121
+ // downgrade THIS `ask` to a hard deny (confirmGate returns null in every
122
+ // other case — including every claude-code call, unconditionally, and
123
+ // every non-`ask` prompt kind — so the line below is byte-identical to
124
+ // the pre-CONFIRM behavior whenever it applies).
125
+ const confirm = confirmGate(id, prompt, event.command, event.sessionId, opts.now, opts.home,
126
+ id === "codex" ? { tool: event.tool, cwd: event.cwd ?? opts.cwd, toolUseId: event.toolUseId } : undefined);
127
+ if (confirm?.allow) {
128
+ return allowOutcome(id, event, payload, designCacheDir, opts.cwd, { trackFile: file, windowMs: opts.windowMs, now: opts.now }, opts.corpusRoot);
129
+ }
130
+ const finalPrompt = confirm ? confirm.prompt : prompt;
131
+ return { stdout: withDenyNotice(id, respond(id, finalPrompt, event.eventName ?? "PreToolUse"), finalPrompt, event.sessionId, dirname(file), opts.now), exit: 0 };
132
+ }
133
+ // Every gate allowed: hand off to the ALLOW-path assembly (pass notice +
134
+ // decision-time lesson + evidence-fresh notice). A deny/ask already returned
135
+ // above, so nothing it emits can block nor override a decision.
136
+ return allowOutcome(id, event, payload, designCacheDir, opts.cwd, { trackFile: file, windowMs: opts.windowMs, now: opts.now }, opts.corpusRoot);
137
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Pre-pipeline async scope interception for {@link handleHook}. The aipilot and
3
+ * memory scopes reach external resources (cache files / Graphiti HTTP) and may
4
+ * emit stdout for lifecycle events, so they run before the sync gate pipeline.
5
+ */
6
+ import { dispatchAipilot } from "./lifecycle";
7
+ import { dispatchMemory } from "./lifecycle/memory/dispatch";
8
+ import { cursorEventContract } from "../adapters/cursor/events";
9
+ import { toCursorLifecycleResponse } from "../adapters/cursor/respond";
10
+ import type { PluginScope } from "./lifecycle";
11
+
12
+ /**
13
+ * Run the async per-scope dispatcher for the invoking scope, if any.
14
+ * @param scope - The invoking plugin scope.
15
+ * @param event - The raw hook event name.
16
+ * @param payload - The raw hook payload.
17
+ * @param cwd - Project root.
18
+ * @param now - Clock.
19
+ * @param id - Harness target id (defaults to "claude-code" — zero-regression default).
20
+ * @returns The native stdout when intercepted, or `null` to fall through.
21
+ */
22
+ export async function asyncScopeStdout(scope: PluginScope | undefined, event: string, payload: Record<string, unknown>, cwd: string, now: number, id: string = "claude-code"): Promise<string | null> {
23
+ const dispatchedEvent = id === "cursor" ? cursorEventContract(event).lifecycle : event;
24
+ if (dispatchedEvent === null) return null;
25
+ let stdout: string | null = null;
26
+ if (scope === "aipilot") stdout = await dispatchAipilot(dispatchedEvent, payload, cwd, now, undefined, id);
27
+ if (scope === "memory") stdout = await dispatchMemory(dispatchedEvent, payload, cwd, now);
28
+ return id === "cursor" && stdout !== null ? toCursorLifecycleResponse(stdout, event) : stdout;
29
+ }
@@ -0,0 +1,27 @@
1
+ import type { PluginScope } from "./lifecycle";
2
+
3
+ /** Options for {@link handleHook} (caller supplies the clock + project root). */
4
+ export interface HandleOptions {
5
+ now: number;
6
+ cwd: string;
7
+ /** Directory of SOLID reference `.md` files for `solidReadGate` (else inert). */
8
+ refsDir?: string;
9
+ /** APEX freshness window in ms (from `FUSE_ENFORCE_TTL_SEC`). */
10
+ windowMs?: number;
11
+ /** Which plugin's hooks.json invoked the harness (selects lifecycle behavior). */
12
+ scope?: PluginScope;
13
+ /**
14
+ * Test/integration seam: the resolved refs-design corpus root ("" = absent).
15
+ * Undefined = resolve from disk (the production behavior); supplied = used
16
+ * as-is, so tests can drive the corpus-present branch deterministically.
17
+ */
18
+ corpusRoot?: string;
19
+ /** Test-only OS home override for the session-anchored design cache dir (see design-cache-resolve.ts). Undefined = real `os.homedir()`. */
20
+ home?: string;
21
+ }
22
+
23
+ /** What the hook bin should print + exit with. */
24
+ export interface HandleOutcome {
25
+ stdout: string;
26
+ exit: number;
27
+ }
@@ -0,0 +1,197 @@
1
+ import { join } from "node:path";
2
+ import { projectLayout } from "../config/layout";
3
+ import { detectFramework } from "../policy/detect-framework";
4
+ import { detectCreationIntent } from "../policy/creation-intent";
5
+ import { recordBrainstormRequired } from "../tracking/session-state";
6
+ import { withTrack } from "../tracking/store";
7
+ import { normalizeEvent } from "./normalize";
8
+ import { defaultStateDir, projectHash, trackFile } from "./paths";
9
+ import { fuseHarnessHome } from "./home-state";
10
+ import { designLifecycle } from "./design-lifecycle";
11
+ import { promptSubmitContext } from "./inject-context";
12
+ import { lifecycleStdout } from "./lifecycle-bridge";
13
+ import { handlePre } from "./handle-pre";
14
+ import { handlePost } from "./handle-post";
15
+ import { asyncScopeStdout } from "./handle-scope-async";
16
+ import { resolveDesignCacheDir } from "./design-cache-resolve";
17
+ import { resyncCodexAgents } from "./lifecycle/codex-resync/resync";
18
+ import { resetFragmentRegistry } from "./fragment-registry";
19
+ import { attachBudgetRecap } from "./inject-budget-recap";
20
+ import { promptText } from "./prompt-text";
21
+ import { handleConfirmSubmit } from "./confirm/confirm-submit";
22
+ import { submitCodexConfirmation } from "./confirm/codex-confirm";
23
+ import { codexPromptOrigin } from "./confirm/codex-prompt-origin";
24
+ import { cursorProjectCwd } from "../adapters/cursor/context";
25
+ import { toCursorLifecycleResponse } from "../adapters/cursor/respond";
26
+ import type { HandleOptions, HandleOutcome } from "./handle-types";
27
+ import type { NormalizedEvent } from "./normalize";
28
+ export type { HandleOptions, HandleOutcome } from "./handle-types";
29
+
30
+ /** Raw Claude hook event name from a payload (empty when absent). */
31
+ function rawEventName(payload: Record<string, unknown>): string {
32
+ return typeof payload.hook_event_name === "string" ? payload.hook_event_name : "";
33
+ }
34
+
35
+ /**
36
+ * `payload.tool_input` parsed into an object when it's a JSON STRING —
37
+ * Cursor's real wire format for `beforeMCPExecution`/`afterMCPExecution`
38
+ * (ground truth), unlike every other harness (and Cursor's own
39
+ * `preToolUse`/`postToolUse`), which always sends it as an object already.
40
+ * `undefined` when `tool_input` is already an object, absent, or fails to
41
+ * parse into one (fail-open — the caller then keeps the original value).
42
+ * @param payload - The raw hook payload.
43
+ */
44
+ function cursorParsedToolInput(payload: Record<string, unknown>): Record<string, unknown> | undefined {
45
+ const raw = payload.tool_input;
46
+ if (typeof raw !== "string") return undefined;
47
+ try {
48
+ const parsed: unknown = JSON.parse(raw);
49
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? (parsed as Record<string, unknown>) : undefined;
50
+ } catch {
51
+ return undefined;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * `id === "cursor"` only: project the already-resolved canonical `tool_name`
57
+ * (`event.tool`, normalized by {@link normalizeEvent}) and `cwd` (the project
58
+ * root resolved via `cursorProjectCwd`, already applied to `opts.cwd`) onto a
59
+ * shallow payload copy — the single passage point for every downstream
60
+ * consumer that reads `payload.tool_name`/`payload.cwd`/`payload.tool_input`
61
+ * RAW instead of `event.tool`/`opts.cwd`/`event.input` (lifecycle-bridge's
62
+ * `failure-lesson.ts`/`agent-memory.ts`, handle-scope-async's aipilot/memory
63
+ * dispatchers — including `doc-cache-gate.ts`'s `libraryOf`, which never
64
+ * `JSON.parse`s a string `tool_input` itself — and the seo scope's
65
+ * `post-tool-use.ts`). `tool_input` is additionally replaced by its parsed
66
+ * object form via {@link cursorParsedToolInput} when Cursor sent it as a
67
+ * JSON string (`beforeMCPExecution`/`afterMCPExecution`). Cursor's own wire
68
+ * values ("Shell", `MCP:<tool>`, a bare `workspace_roots` array with no
69
+ * `cwd` field, a stringified `tool_input`, …) are preserved under
70
+ * `cursor_tool_name`/`cursor_cwd`/`cursor_tool_input` so nothing is lost.
71
+ * Every other harness id is untouched (returns the SAME object,
72
+ * byte-identical).
73
+ * @param payload - The raw hook payload.
74
+ * @param event - The already-normalized event (`event.tool` is canonical).
75
+ * @param cwd - The resolved project root for this invocation.
76
+ * @param id - Harness adapter id.
77
+ */
78
+ function cursorRawPayloadProjection(payload: Record<string, unknown>, event: NormalizedEvent, cwd: string, id: string): Record<string, unknown> {
79
+ if (id !== "cursor") return payload;
80
+ const parsedToolInput = cursorParsedToolInput(payload);
81
+ return {
82
+ ...payload,
83
+ cursor_tool_name: payload.tool_name,
84
+ cursor_cwd: payload.cwd,
85
+ tool_name: event.tool,
86
+ cwd,
87
+ ...(parsedToolInput ? { cursor_tool_input: payload.tool_input, tool_input: parsedToolInput } : {}),
88
+ };
89
+ }
90
+
91
+ /**
92
+ * The full hook handler: on a PRE event it gates the tool-use (stateless guards
93
+ * then APEX gates from the session track) and returns the native response; on a
94
+ * POST event it records the activity into the track. The loop that makes the
95
+ * package behave like the Claude plugin, on any harness.
96
+ */
97
+ async function handleHookCore(id: string, payload: Record<string, unknown>, opts: HandleOptions): Promise<HandleOutcome> {
98
+ const event = normalizeEvent(id, payload);
99
+ if (id === "cursor") {
100
+ const cursorCwd = cursorProjectCwd(event.cwd, event.workspaceRoots ?? [], event.filePath, opts.cwd);
101
+ if (cursorCwd !== opts.cwd) opts = { ...opts, cwd: cursorCwd };
102
+ }
103
+ // Single passage point (see cursorRawPayloadProjection doc): every raw-payload
104
+ // consumer below this line gets the canonical tool_name/cwd on Cursor; every
105
+ // other harness id gets `payload` back untouched (byte-identical object).
106
+ const hookPayload = cursorRawPayloadProjection(payload, event, opts.cwd, id);
107
+ const rawPrompt = payload.prompt;
108
+ const userPrompt = typeof rawPrompt === "string" || Array.isArray(rawPrompt) ? promptText(rawPrompt) : undefined;
109
+ if (id === "codex" && rawEventName(payload) === "UserPromptSubmit" && userPrompt !== undefined) {
110
+ submitCodexConfirmation(event.sessionId, userPrompt, opts.now, opts.home, codexPromptOrigin(payload));
111
+ }
112
+ // Fresh slate for this invocation's capFragment tally — one hook event is
113
+ // exactly one lifecycle branch below (see dispatchLifecycle), so a single
114
+ // reset here can never mix fragments across unrelated events.
115
+ resetFragmentRegistry();
116
+ const layout = projectLayout(opts.cwd);
117
+ const file = trackFile(event.sessionId, defaultStateDir(opts.cwd));
118
+ const mcpDir = layout.cacheDir;
119
+ // Design pipeline (flag + `.design-state-*.json`) is anchored on the session
120
+ // id, not `mcpDir`/cwd — see design-cache-resolve.ts. `mcpDir` itself stays
121
+ // cwd-derived for the unrelated MCP/WebFetch cache.
122
+ const designCacheDir = resolveDesignCacheDir(event.sessionId, mcpDir, opts.home);
123
+ const framework = detectFramework(event.filePath ?? "", event.content ?? "", opts.cwd);
124
+
125
+ // Design-agent lifecycle (SubagentStart/Stop): init/cleanup the pipeline state machine.
126
+ // claude-code + codex only: verified against openai/codex's OWN generated hook schemas
127
+ // (codex-rs/hooks/schema/generated/subagent-{start,stop}.command.input.schema.json @ 385c0a93,
128
+ // superseding the stale 44918ea1 Cargo.toml-only bump) — `agent_id`/`agent_type` are REQUIRED
129
+ // on both events, unconditional on `hide_spawn_agent_metadata` (that flag only reshapes the
130
+ // spawn_agent tool RESULT, not the hook payload). `payload.prompt` (design-mode "component"
131
+ // detection below) is absent from Codex's schema — degrades to detectMode's default, not a
132
+ // break. Cursor/Gemini/Cline/Hermes remain unverified — NOT added without the same proof.
133
+ if ((id === "claude-code" || id === "codex") && designLifecycle(payload, designCacheDir, opts.cwd, String(opts.now), opts.now)) {
134
+ return { stdout: "", exit: 0 };
135
+ }
136
+
137
+ // Codex-only, fail-open: refresh the plugin agents/commands cache on SessionStart when its fingerprint changed (strict no-op on every other harness).
138
+ if (id === "codex" && rawEventName(payload) === "SessionStart") resyncCodexAgents();
139
+
140
+ // Async per-scope lifecycle (aipilot cache handlers + memory-neural Graphiti).
141
+ const asyncOut = await asyncScopeStdout(opts.scope, rawEventName(payload), hookPayload, opts.cwd, opts.now, id);
142
+ if (asyncOut !== null) return { stdout: asyncOut, exit: 0 };
143
+
144
+ // Ported lifecycle/session/context hooks (SessionStart, SubagentStart/Stop, etc.).
145
+ const life = lifecycleStdout(hookPayload, opts.cwd, opts.scope ?? "core", opts.now, id);
146
+ if (life !== null) {
147
+ // Claude-Code-only: attachBudgetRecap's systemMessage envelope assumes the
148
+ // Claude adapter's stdout shape (mirrors the designLifecycle gate above).
149
+ const stdout = id === "claude-code" ? attachBudgetRecap(life, rawEventName(payload), event.sessionId, opts.cwd, opts.now) : life;
150
+ return { stdout, exit: 0 };
151
+ }
152
+
153
+ // UserPromptSubmit (core scope): brainstorm flag + CLAUDE.md injection.
154
+ // `payload.prompt` is a string on Claude Code/Codex, an array of content
155
+ // blocks on Kimi (see promptText) — either shape is normalized to text;
156
+ // anything else (field absent, or an unrecognized type) stays `undefined`
157
+ // so the block below is skipped exactly as before promptText existed.
158
+ if (userPrompt !== undefined) {
159
+ if (id !== "codex") handleConfirmSubmit(event.sessionId, userPrompt, opts.now, opts.home);
160
+ await withTrack(file, (track) => recordBrainstormRequired(track, detectCreationIntent(userPrompt)));
161
+ return { stdout: promptSubmitContext(userPrompt, opts.cwd, id), exit: 0 };
162
+ }
163
+
164
+ if (event.phase === "post") {
165
+ return handlePost({ id, payload: hookPayload, event, framework, mcpDir, designCacheDir, file, opts });
166
+ }
167
+
168
+ return handlePre({ id, payload: hookPayload, event, framework, mcpDir, designCacheDir, file, opts });
169
+ }
170
+
171
+ /**
172
+ * Run one hook and adapt every Cursor scope outcome at the common runtime exit.
173
+ * Other harnesses retain the core handler's stdout and exit status unchanged.
174
+ * Cursor's shared `additional_context` budget context (see
175
+ * `../adapters/cursor/context-budget.ts`) is assembled here too — this is
176
+ * the single point every Cursor stdout passes through exactly once, so it's
177
+ * also the single point that reserves from and records into the registry.
178
+ * With no `session_id`/`conversation_id` at all, `sessionId` is `""` — the
179
+ * registry key would degenerate to one bucket shared by every session-less
180
+ * call on the same (cwd, event) pair, so `budget` stays `undefined` instead
181
+ * (falls back to the flat per-response cap in `toCursorLifecycleResponse`,
182
+ * with zero registry I/O). `stateDir` honors `opts.home` (test-only OS home
183
+ * override, see `HandleOptions`) so tests never need the real `os.homedir()`.
184
+ */
185
+ export async function handleHook(id: string, payload: Record<string, unknown>, opts: HandleOptions): Promise<HandleOutcome> {
186
+ const outcome = await handleHookCore(id, payload, opts);
187
+ if (id !== "cursor") return outcome;
188
+ const eventName = rawEventName(payload);
189
+ const cursorEvent = normalizeEvent(id, payload);
190
+ const cwd = cursorProjectCwd(cursorEvent.cwd, cursorEvent.workspaceRoots ?? [], cursorEvent.filePath, opts.cwd);
191
+ const sessionId = cursorEvent.sessionId;
192
+ const generationId = typeof payload.generation_id === "string" && payload.generation_id ? payload.generation_id : undefined;
193
+ const toolUseId = typeof payload.tool_use_id === "string" && payload.tool_use_id ? payload.tool_use_id : undefined;
194
+ const stateDir = join(fuseHarnessHome(opts.home), "state", projectHash(cwd));
195
+ const budget = sessionId ? { stateDir, sessionId, event: eventName, generationId, toolUseId } : undefined;
196
+ return { ...outcome, stdout: toCursorLifecycleResponse(outcome.stdout, eventName, budget) };
197
+ }
@@ -0,0 +1,55 @@
1
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { atomicWrite } from "../util/json-io";
5
+
6
+ /** Home `~/.claude` dir — per-harness config (CLAUDE.md, logs, plugins). */
7
+ export function claudeHome(home: string = homedir()): string {
8
+ return join(home, ".claude");
9
+ }
10
+
11
+ /** Neutral, harness-agnostic home for fuse-harness's OWN cache/state: `~/.fuse-harness`. */
12
+ export function fuseHarnessHome(home: string = homedir()): string {
13
+ return join(home, ".fuse-harness");
14
+ }
15
+
16
+ /** `~/.fuse-harness/cache` base dir for session/cache state (shared across harnesses). */
17
+ export function fusengineCache(home: string = homedir()): string {
18
+ return join(fuseHarnessHome(home), "cache");
19
+ }
20
+
21
+ /** `~/.fuse-harness/cache/sessions` — per-session JSON state dir. */
22
+ export function sessionsDir(home: string = homedir()): string {
23
+ return join(fusengineCache(home), "sessions");
24
+ }
25
+
26
+ const SID_RE = /^[a-zA-Z0-9_-]{1,128}$/;
27
+
28
+ /** Validate a session id (1-128 url-safe chars); null when invalid. */
29
+ export function sanitizeSessionId(sid: unknown): string | null {
30
+ const s = String(sid ?? "").trim();
31
+ return SID_RE.test(s) ? s : null;
32
+ }
33
+
34
+ /** Unified per-session state file path: `sessions/session-<sid>.json`. */
35
+ export function sessionStatePath(sid: string, home: string = homedir()): string {
36
+ return join(sessionsDir(home), `session-${sid}.json`);
37
+ }
38
+
39
+ /** Load a session-state dict, or `{}` when missing/corrupt (mirrors Python). */
40
+ export function loadSessionState(sid: string, home: string = homedir()): Record<string, unknown> {
41
+ const path = sessionStatePath(sid, home);
42
+ try {
43
+ if (!existsSync(path)) return {};
44
+ const data: unknown = JSON.parse(readFileSync(path, "utf-8"));
45
+ return typeof data === "object" && data !== null && !Array.isArray(data) ? (data as Record<string, unknown>) : {};
46
+ } catch {
47
+ return {};
48
+ }
49
+ }
50
+
51
+ /** Atomically persist a session-state dict (0o600 via atomicWrite, indent 2). */
52
+ export function saveSessionState(sid: string, state: Record<string, unknown>, home: string = homedir()): void {
53
+ mkdirSync(sessionsDir(home), { recursive: true, mode: 0o700 });
54
+ atomicWrite(sessionStatePath(sid, home), JSON.stringify(state, null, 2));
55
+ }
@@ -0,0 +1,18 @@
1
+ export * from "./paths";
2
+ export * from "./storage";
3
+ export * from "./record";
4
+ export * from "./activity";
5
+ export * from "./gate";
6
+ export * from "./dry";
7
+ export * from "./precommit";
8
+ export * from "./normalize";
9
+ export * from "./respond";
10
+ export * from "./mcp";
11
+ export * from "./inject-context";
12
+ export * from "./home-state";
13
+ export * from "./fs-cleanup";
14
+ export * from "./dev-context";
15
+ export * from "./lifecycle";
16
+ export * from "./lifecycle-bridge";
17
+ export * from "./handle-pre";
18
+ export * from "./handle";
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Lifecycle context-injection renderer. Every emitter that injects text into
3
+ * the model's context (rules, lessons, CLAUDE.md preamble, ...) goes through
4
+ * here instead of calling the Claude builders directly:
5
+ * - kimi: raw text on stdout — Kimi Code CLI appends stdout to the context on
6
+ * exit 0 (UserPromptSubmit is blockable; the Claude-shaped
7
+ * `additionalContext` envelope would be ignored or leak as raw JSON text).
8
+ * - every other harness: the historical Claude-shaped envelope, byte-identical
9
+ * to what the call sites produced before this module existed (zero-regression
10
+ * default — `attachSystemMessage` concatenates notices with "\n", so a single
11
+ * combined notice equals the previous chained calls).
12
+ */
13
+ import { attachSystemMessage, contextResponse } from "../adapters/claude";
14
+
15
+ /**
16
+ * Render a context injection for the firing harness.
17
+ * @param id - Harness id.
18
+ * @param event - The firing hook event name (envelope tag for Claude shapes).
19
+ * @param text - The context body to inject.
20
+ * @param notice - Optional human-facing notice (systemMessage on Claude
21
+ * shapes; appended as plain text for kimi).
22
+ * @returns The native hook stdout.
23
+ */
24
+ export function renderInform(id: string, event: string, text: string, notice?: string): string {
25
+ if (id === "kimi") return notice ? `${text}\n\n${notice}` : text;
26
+ const base = contextResponse(event, text);
27
+ return notice ? attachSystemMessage(base, notice) : base;
28
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * @module inject-budget-recap
3
+ * ONE event-level recap of every {@link module:inject-budget.capFragment}
4
+ * -tracked fragment injected for a SessionStart/SubagentStart event — the
5
+ * aggregated view the per-fragment caps never gave: each injection point
6
+ * (dev-context, snapshot sections, lessons, apex-subagent, ...) caps and
7
+ * reports itself in ISOLATION, with no total across the whole event.
8
+ *
9
+ * Rides the user-visible `systemMessage` channel, Claude-Code-only (gate at
10
+ * the call site — mirrors the existing `id === "claude-code"` pattern for
11
+ * `designLifecycle` in handle.ts): other adapters do not all re-export
12
+ * `attachSystemMessage`, and stamping a Claude-shaped envelope onto another
13
+ * harness's stdout shape would be silently wrong there.
14
+ *
15
+ * Deduped via {@link module:inject-dedup.onceExclusive}, not the JSON
16
+ * `oncePerWindow`: SessionStart/SubagentStart fan out across every installed
17
+ * plugin exactly like PostToolUse does (see burst-window.ts) — a shared-JSON
18
+ * read-modify-write here would risk the same lost-update race already fixed
19
+ * for the sniper reminder (lesson 2026-07-05 16:00).
20
+ * @packageDocumentation
21
+ */
22
+ import { attachSystemMessage } from "../adapters/claude";
23
+ import { budgetReport } from "./inject-budget";
24
+ import { fragmentRegistry } from "./fragment-registry";
25
+ import { onceExclusive } from "./inject-dedup";
26
+ import { BURST_DEDUP_MS } from "./burst-window";
27
+ import { defaultStateDir } from "./paths";
28
+
29
+ /**
30
+ * Attach the aggregated recap onto `stdout` when 2+ fragments were recorded
31
+ * for `rawEvent`. A lone fragment already carries its own visibility (its
32
+ * producer's own per-fragment report, when it has one) — no recap is added
33
+ * for that case, so the common single-fragment event stays noise-free.
34
+ * @param stdout - The already-rendered hook stdout for this event.
35
+ * @param rawEvent - The raw hook event name (only SessionStart/SubagentStart qualify; others pass through unchanged).
36
+ * @param sessionId - Current session id (dedup scope).
37
+ * @param cwd - Project root (state-dir scope for the dedup marker).
38
+ * @param now - Clock.
39
+ * @returns `stdout` with a `systemMessage` recap attached, or `stdout` unchanged.
40
+ */
41
+ export function attachBudgetRecap(stdout: string, rawEvent: string, sessionId: string, cwd: string, now: number): string {
42
+ if (rawEvent !== "SessionStart" && rawEvent !== "SubagentStart") return stdout;
43
+ const fragments = fragmentRegistry();
44
+ if (fragments.length <= 1) return stdout;
45
+ if (!onceExclusive(`budget:${sessionId}:${rawEvent}`, BURST_DEDUP_MS, { now, dir: defaultStateDir(cwd) })) return stdout;
46
+ return attachSystemMessage(stdout, budgetReport(fragments));
47
+ }