@fusengine/harness 0.1.89 → 0.1.91

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (419) hide show
  1. package/dist/adapters/claude/index.d.mts +1 -1
  2. package/dist/adapters/claude/index.mjs +1 -1
  3. package/dist/adapters/cline/index.mjs +1 -1
  4. package/dist/adapters/codex/index.d.mts +2 -2
  5. package/dist/adapters/codex/index.mjs +5 -4
  6. package/dist/adapters/cursor/index.d.mts +15 -28
  7. package/dist/adapters/cursor/index.mjs +35 -46
  8. package/dist/adapters/gemini/index.mjs +1 -1
  9. package/dist/adapters/hermes/index.d.mts +1 -1
  10. package/dist/adapters/hermes/index.mjs +1 -1
  11. package/dist/adapters/kimi/index.d.mts +1 -1
  12. package/dist/adapters/kimi/index.mjs +1 -1
  13. package/dist/{claude-5bC8TDAz.mjs → claude-Ckv2_TgP.mjs} +3 -43
  14. package/dist/cli/bin.mjs +429 -16
  15. package/dist/cli/index.mjs +1 -1
  16. package/dist/command-string-CALMTnwN.mjs +42 -0
  17. package/dist/{evaluate-C1VSI5pb.mjs → evaluate-CMiIqHeH.mjs} +261 -13
  18. package/dist/{handle-DHhukRdh.mjs → handle-C43gA-Pr.mjs} +737 -150
  19. package/dist/{hermes-kP6NUQlG.mjs → hermes-B9-p_3IF.mjs} +3 -2
  20. package/dist/{index-DBgnIZn9.d.mts → index-B8PG82su.d.mts} +1 -1
  21. package/dist/{index-D4C2-IIc.d.mts → index-SmKYkk2N.d.mts} +1 -1
  22. package/dist/index.d.mts +2 -2
  23. package/dist/index.mjs +2 -2
  24. package/dist/init/index.d.mts +1 -1
  25. package/dist/init/index.mjs +1 -1
  26. package/dist/{kimi-D6X7b8wF.mjs → kimi-G2wcSh5-.mjs} +3 -2
  27. package/dist/normalize-BjG6unTj.mjs +317 -0
  28. package/dist/permission-mode-B-SFyR0X.mjs +226 -0
  29. package/dist/policy/index.d.mts +1 -1
  30. package/dist/policy/index.mjs +2 -2
  31. package/dist/prompt/index.d.mts +1 -1
  32. package/dist/{run-CdIlUtjv.mjs → run-DkrzC0gb.mjs} +1 -1
  33. package/dist/{run-TAXNRSpD.mjs → run-GHAMqcOn.mjs} +26 -3
  34. package/dist/runtime/index.d.mts +27 -16
  35. package/dist/runtime/index.mjs +2 -2
  36. package/dist/{types-C0-igRKr.d.mts → types-Bh0jEF3_.d.mts} +2 -0
  37. package/dist/{validate-DZFtAbLP.mjs → validate-KjZ1X9tH.mjs} +1 -1
  38. package/package.json +3 -2
  39. package/src/adapters/claude/index.ts +92 -0
  40. package/src/adapters/cline/index.ts +36 -0
  41. package/src/adapters/codex/apply-patch.ts +94 -0
  42. package/src/adapters/codex/index.ts +84 -0
  43. package/src/adapters/codex/permission-mode.ts +16 -0
  44. package/src/adapters/cursor/context.ts +46 -0
  45. package/src/adapters/cursor/events.ts +38 -0
  46. package/src/adapters/cursor/index.ts +43 -0
  47. package/src/adapters/cursor/interfaces/types.ts +88 -0
  48. package/src/adapters/cursor/native-response.ts +158 -0
  49. package/src/adapters/cursor/normalize.ts +123 -0
  50. package/src/adapters/cursor/respond.ts +123 -0
  51. package/src/adapters/gemini/index.ts +36 -0
  52. package/src/adapters/hermes/index.ts +55 -0
  53. package/src/adapters/hermes/interfaces/types.ts +26 -0
  54. package/src/adapters/kimi/index.ts +96 -0
  55. package/src/adapters/kimi/interfaces/types.ts +55 -0
  56. package/src/cache/compact.ts +55 -0
  57. package/src/cache/index.ts +5 -0
  58. package/src/cache/io.ts +38 -0
  59. package/src/cache/mcp-response.ts +39 -0
  60. package/src/cache/mcp-store.ts +70 -0
  61. package/src/cache/store.ts +93 -0
  62. package/src/changelog/fetch.ts +79 -0
  63. package/src/cli/bin.ts +100 -0
  64. package/src/cli/cursor-event-scanner.ts +175 -0
  65. package/src/cli/cursor-stdin-reader.ts +65 -0
  66. package/src/cli/doctor.ts +96 -0
  67. package/src/cli/hook-io.ts +130 -0
  68. package/src/cli/hook-sound.ts +49 -0
  69. package/src/cli/index.ts +1 -0
  70. package/src/cli/json-primitive-scanner.ts +83 -0
  71. package/src/cli/run.ts +36 -0
  72. package/src/cli/scope.ts +31 -0
  73. package/src/codex-rules/build.ts +63 -0
  74. package/src/codex-rules/index.ts +8 -0
  75. package/src/codex-rules/mapping.ts +42 -0
  76. package/src/codex-rules/rules/absolute-paths.ts +52 -0
  77. package/src/codex-rules/rules/ask.ts +44 -0
  78. package/src/codex-rules/rules/chmod-ask.ts +57 -0
  79. package/src/codex-rules/rules/dd.ts +23 -0
  80. package/src/codex-rules/rules/diskutil.ts +33 -0
  81. package/src/codex-rules/rules/mkfs.ts +42 -0
  82. package/src/codex-rules/rules/privilege.ts +49 -0
  83. package/src/codex-rules/rules/remote-fetch.ts +31 -0
  84. package/src/codex-rules/rules/rm-variants.ts +58 -0
  85. package/src/codex-rules/rules/single-commands.ts +57 -0
  86. package/src/codex-rules/skip-list.ts +25 -0
  87. package/src/codex-rules/starlark.ts +46 -0
  88. package/src/codex-rules/types.ts +36 -0
  89. package/src/config/dotenv.ts +55 -0
  90. package/src/config/env.ts +10 -0
  91. package/src/config/home-dir.ts +36 -0
  92. package/src/config/index.ts +5 -0
  93. package/src/config/layout.ts +52 -0
  94. package/src/config/limits.ts +56 -0
  95. package/src/config/ttl.ts +30 -0
  96. package/src/detect/harness.ts +73 -0
  97. package/src/detect/index.ts +1 -0
  98. package/src/detect/interfaces/types.ts +18 -0
  99. package/src/freshness/agent-evidence-record.ts +95 -0
  100. package/src/freshness/agent-evidence.ts +95 -0
  101. package/src/freshness/codex-spawn-evidence.ts +91 -0
  102. package/src/freshness/doc-helpers.ts +90 -0
  103. package/src/freshness/evidence-harvest-io.ts +72 -0
  104. package/src/freshness/evidence-harvest.ts +61 -0
  105. package/src/freshness/explore-tools.ts +95 -0
  106. package/src/freshness/index.ts +2 -0
  107. package/src/freshness/query-framework.ts +73 -0
  108. package/src/freshness/ref-evidence.ts +49 -0
  109. package/src/freshness/ref-journal.ts +98 -0
  110. package/src/freshness/trivial-edit-counter.ts +30 -0
  111. package/src/index.ts +11 -0
  112. package/src/init/index.ts +2 -0
  113. package/src/init/run.ts +32 -0
  114. package/src/init/templates.ts +75 -0
  115. package/src/memory/gitignore.ts +14 -0
  116. package/src/memory/index.ts +3 -0
  117. package/src/memory/registry.ts +39 -0
  118. package/src/memory/session-roots.ts +99 -0
  119. package/src/memory/state.ts +58 -0
  120. package/src/policy/apex-authorization.ts +100 -0
  121. package/src/policy/apex-gates.ts +91 -0
  122. package/src/policy/apex-target-steps.ts +73 -0
  123. package/src/policy/apex-target.ts +49 -0
  124. package/src/policy/apex-task-context.ts +81 -0
  125. package/src/policy/apex.ts +94 -0
  126. package/src/policy/cartographer/build-tree.ts +69 -0
  127. package/src/policy/cartographer/describe.ts +53 -0
  128. package/src/policy/cartographer/entry.ts +38 -0
  129. package/src/policy/cartographer/frontmatter.ts +48 -0
  130. package/src/policy/cartographer/indicators.ts +34 -0
  131. package/src/policy/claude-md-context.ts +66 -0
  132. package/src/policy/conventions/index.ts +11 -0
  133. package/src/policy/conventions/interfaces.ts +71 -0
  134. package/src/policy/conventions/langs.ts +53 -0
  135. package/src/policy/conventions/query.ts +49 -0
  136. package/src/policy/conventions/react-hooks.ts +18 -0
  137. package/src/policy/conventions/store-or-query.ts +21 -0
  138. package/src/policy/conventions/stores.ts +56 -0
  139. package/src/policy/conventions/strip.ts +87 -0
  140. package/src/policy/conventions/verdict.ts +38 -0
  141. package/src/policy/creation-intent.ts +11 -0
  142. package/src/policy/deny-loop.ts +88 -0
  143. package/src/policy/design/allowed-write.ts +28 -0
  144. package/src/policy/design/content-checks.ts +45 -0
  145. package/src/policy/design/corpus-fs.ts +21 -0
  146. package/src/policy/design/corpus-resolve-cache.ts +49 -0
  147. package/src/policy/design/corpus-resolve.ts +99 -0
  148. package/src/policy/design/corpus.ts +92 -0
  149. package/src/policy/design/design-system-rules.ts +45 -0
  150. package/src/policy/design/flag.ts +30 -0
  151. package/src/policy/design/gates-pipeline.ts +105 -0
  152. package/src/policy/design/gates.ts +87 -0
  153. package/src/policy/design/screenshot-tools.ts +24 -0
  154. package/src/policy/design/skill-evidence.ts +34 -0
  155. package/src/policy/design/skill-gate.ts +81 -0
  156. package/src/policy/design/skill-triggers.ts +51 -0
  157. package/src/policy/design/state.ts +92 -0
  158. package/src/policy/design/template-urls.ts +54 -0
  159. package/src/policy/design/transitions.ts +68 -0
  160. package/src/policy/detect-claude-md-project-type.ts +32 -0
  161. package/src/policy/detect-framework.ts +99 -0
  162. package/src/policy/detect-primitive-checks.ts +100 -0
  163. package/src/policy/detect-primitive-lib.ts +55 -0
  164. package/src/policy/detect-project.ts +99 -0
  165. package/src/policy/edit-outcome.ts +53 -0
  166. package/src/policy/evaluate.ts +81 -0
  167. package/src/policy/expert-agents.ts +60 -0
  168. package/src/policy/file-size-scope.ts +32 -0
  169. package/src/policy/file-size.ts +99 -0
  170. package/src/policy/framework-solid-exclude.ts +52 -0
  171. package/src/policy/framework-solid-extended.ts +93 -0
  172. package/src/policy/framework-solid-gates-systems.ts +99 -0
  173. package/src/policy/framework-solid-gates.ts +98 -0
  174. package/src/policy/framework-solid.ts +88 -0
  175. package/src/policy/gemini-mcp-gate.ts +83 -0
  176. package/src/policy/git-gates.ts +40 -0
  177. package/src/policy/guards/bash-command-anchor.ts +40 -0
  178. package/src/policy/guards/bash-write-patterns.ts +140 -0
  179. package/src/policy/guards/bash-write-redirects.ts +201 -0
  180. package/src/policy/guards/bash-write-safe-paths.ts +62 -0
  181. package/src/policy/guards/bash-write.ts +76 -0
  182. package/src/policy/guards/context.ts +12 -0
  183. package/src/policy/guards/index.ts +61 -0
  184. package/src/policy/guards/install.ts +22 -0
  185. package/src/policy/guards/interface-separation-ext.ts +41 -0
  186. package/src/policy/guards/interface-separation.ts +100 -0
  187. package/src/policy/guards/protected-path.ts +100 -0
  188. package/src/policy/guards/security.ts +97 -0
  189. package/src/policy/index.ts +19 -0
  190. package/src/policy/interfaces/types.ts +46 -0
  191. package/src/policy/js-gate-route.ts +51 -0
  192. package/src/policy/lessons/lesson-gate.ts +0 -0
  193. package/src/policy/lessons/trigger-index.ts +84 -0
  194. package/src/policy/lessons/types.ts +31 -0
  195. package/src/policy/module-layout.ts +31 -0
  196. package/src/policy/nearest-manifest.ts +75 -0
  197. package/src/policy/never-approval.ts +95 -0
  198. package/src/policy/patterns.ts +80 -0
  199. package/src/policy/seo/validate.ts +51 -0
  200. package/src/policy/shadcn-project.ts +22 -0
  201. package/src/policy/shadcn-skill-gate.ts +97 -0
  202. package/src/policy/shell-read-refs.ts +96 -0
  203. package/src/policy/skill-path.ts +46 -0
  204. package/src/policy/skill-patterns/laravel.ts +40 -0
  205. package/src/policy/skill-patterns/nextjs.ts +29 -0
  206. package/src/policy/skill-patterns/react.ts +27 -0
  207. package/src/policy/skill-patterns/shadcn-subskills.ts +33 -0
  208. package/src/policy/skill-patterns/shadcn.ts +66 -0
  209. package/src/policy/skill-patterns/swift.ts +30 -0
  210. package/src/policy/skill-patterns/tailwind.ts +40 -0
  211. package/src/policy/skill-source.ts +52 -0
  212. package/src/policy/skill-trigger-patterns.ts +33 -0
  213. package/src/policy/skill-triggers.ts +108 -0
  214. package/src/policy/tailwind-skill-gate.ts +63 -0
  215. package/src/policy/trivial-edits.ts +52 -0
  216. package/src/policy/verbosity.ts +39 -0
  217. package/src/prompt/index.ts +1 -0
  218. package/src/prompt/types.ts +36 -0
  219. package/src/refs/discover.ts +69 -0
  220. package/src/refs/frontmatter.ts +24 -0
  221. package/src/refs/index.ts +4 -0
  222. package/src/refs/loader.ts +59 -0
  223. package/src/refs/ref-key.ts +41 -0
  224. package/src/refs/router.ts +63 -0
  225. package/src/refs/types.ts +25 -0
  226. package/src/runtime/activity.ts +99 -0
  227. package/src/runtime/apply-patch-gate.ts +41 -0
  228. package/src/runtime/burst-window.ts +21 -0
  229. package/src/runtime/codex-shell-tool.ts +37 -0
  230. package/src/runtime/command-string.ts +37 -0
  231. package/src/runtime/confirm/codex-confirm.ts +126 -0
  232. package/src/runtime/confirm/codex-prompt-origin.ts +16 -0
  233. package/src/runtime/confirm/confirm-code.ts +33 -0
  234. package/src/runtime/confirm/confirm-gate.ts +65 -0
  235. package/src/runtime/confirm/confirm-irreversible.ts +32 -0
  236. package/src/runtime/confirm/confirm-pending.ts +34 -0
  237. package/src/runtime/confirm/confirm-state.ts +56 -0
  238. package/src/runtime/confirm/confirm-subagent.ts +112 -0
  239. package/src/runtime/confirm/confirm-submit.ts +38 -0
  240. package/src/runtime/core-guards-active.ts +84 -0
  241. package/src/runtime/core-guards-wired.ts +47 -0
  242. package/src/runtime/deny-loop-store.ts +94 -0
  243. package/src/runtime/deny-notice.ts +52 -0
  244. package/src/runtime/design-cache-resolve.ts +51 -0
  245. package/src/runtime/design-content-gate.ts +95 -0
  246. package/src/runtime/design-files-gate.ts +114 -0
  247. package/src/runtime/design-helpers.ts +93 -0
  248. package/src/runtime/design-lifecycle.ts +39 -0
  249. package/src/runtime/design-read-credit.ts +28 -0
  250. package/src/runtime/design.ts +116 -0
  251. package/src/runtime/dev-context.ts +43 -0
  252. package/src/runtime/dry-patterns.ts +32 -0
  253. package/src/runtime/dry.ts +96 -0
  254. package/src/runtime/fragment-registry.ts +33 -0
  255. package/src/runtime/framework-skill-gate.ts +79 -0
  256. package/src/runtime/fs-cleanup.ts +71 -0
  257. package/src/runtime/gate-apex.ts +81 -0
  258. package/src/runtime/gate-helpers.ts +52 -0
  259. package/src/runtime/gate-input.ts +26 -0
  260. package/src/runtime/gate.ts +128 -0
  261. package/src/runtime/handle-post.ts +104 -0
  262. package/src/runtime/handle-pre.ts +137 -0
  263. package/src/runtime/handle-scope-async.ts +29 -0
  264. package/src/runtime/handle-types.ts +27 -0
  265. package/src/runtime/handle.ts +116 -0
  266. package/src/runtime/home-state.ts +55 -0
  267. package/src/runtime/index.ts +18 -0
  268. package/src/runtime/inform.ts +28 -0
  269. package/src/runtime/inject-budget-recap.ts +47 -0
  270. package/src/runtime/inject-budget.ts +81 -0
  271. package/src/runtime/inject-context.ts +85 -0
  272. package/src/runtime/inject-dedup-exclusive.ts +98 -0
  273. package/src/runtime/inject-dedup.ts +89 -0
  274. package/src/runtime/is-agent-tool.ts +19 -0
  275. package/src/runtime/lifecycle/agent-files.ts +49 -0
  276. package/src/runtime/lifecycle/agent-memory.ts +82 -0
  277. package/src/runtime/lifecycle/agent-transcript.ts +80 -0
  278. package/src/runtime/lifecycle/aipilot/analytics.ts +99 -0
  279. package/src/runtime/lifecycle/aipilot/apex-task-store.ts +61 -0
  280. package/src/runtime/lifecycle/aipilot/cache-base.ts +48 -0
  281. package/src/runtime/lifecycle/aipilot/cache-doc.ts +100 -0
  282. package/src/runtime/lifecycle/aipilot/cache-lessons.ts +58 -0
  283. package/src/runtime/lifecycle/aipilot/cache-test.ts +70 -0
  284. package/src/runtime/lifecycle/aipilot/curate-lessons.ts +64 -0
  285. package/src/runtime/lifecycle/aipilot/dispatch-aipilot.ts +100 -0
  286. package/src/runtime/lifecycle/aipilot/doc-cache-gate.ts +69 -0
  287. package/src/runtime/lifecycle/aipilot/inject-apex.ts +98 -0
  288. package/src/runtime/lifecycle/aipilot/inject-doc.ts +62 -0
  289. package/src/runtime/lifecycle/aipilot/inject-explore.ts +64 -0
  290. package/src/runtime/lifecycle/aipilot/inject-lessons.ts +61 -0
  291. package/src/runtime/lifecycle/aipilot/inject-test.ts +48 -0
  292. package/src/runtime/lifecycle/aipilot/lesson-archive.ts +60 -0
  293. package/src/runtime/lifecycle/aipilot/lesson-inject.ts +98 -0
  294. package/src/runtime/lifecycle/aipilot/lesson-parse.ts +74 -0
  295. package/src/runtime/lifecycle/aipilot/lessons.ts +72 -0
  296. package/src/runtime/lifecycle/aipilot/promote-global-lessons.ts +92 -0
  297. package/src/runtime/lifecycle/aipilot/solid-compliance.ts +69 -0
  298. package/src/runtime/lifecycle/aipilot/solid-notice.ts +47 -0
  299. package/src/runtime/lifecycle/aipilot/solid-transcript.ts +60 -0
  300. package/src/runtime/lifecycle/aipilot/source-scan.ts +56 -0
  301. package/src/runtime/lifecycle/aipilot/sync-task.ts +75 -0
  302. package/src/runtime/lifecycle/aipilot/transcript.ts +88 -0
  303. package/src/runtime/lifecycle/aipilot/types.ts +72 -0
  304. package/src/runtime/lifecycle/auto-document-reads.ts +73 -0
  305. package/src/runtime/lifecycle/cartographer/detect.ts +57 -0
  306. package/src/runtime/lifecycle/cartographer/ecosystem-map.ts +56 -0
  307. package/src/runtime/lifecycle/cartographer/fs-util.ts +64 -0
  308. package/src/runtime/lifecycle/cartographer/merge.ts +51 -0
  309. package/src/runtime/lifecycle/cartographer/project-map.ts +49 -0
  310. package/src/runtime/lifecycle/cartographer/scan-hooks.ts +48 -0
  311. package/src/runtime/lifecycle/cartographer/scan.ts +81 -0
  312. package/src/runtime/lifecycle/cartographer/session-start.ts +33 -0
  313. package/src/runtime/lifecycle/cartographer/track-enrichment.ts +45 -0
  314. package/src/runtime/lifecycle/cartographer/write-plugin-map.ts +43 -0
  315. package/src/runtime/lifecycle/cartographer/write-tree.ts +44 -0
  316. package/src/runtime/lifecycle/changelog-research.ts +43 -0
  317. package/src/runtime/lifecycle/check-file-size.ts +61 -0
  318. package/src/runtime/lifecycle/codex-resync/fingerprint.ts +63 -0
  319. package/src/runtime/lifecycle/codex-resync/lock.ts +46 -0
  320. package/src/runtime/lifecycle/codex-resync/managed-destination.ts +39 -0
  321. package/src/runtime/lifecycle/codex-resync/materialize-agents.ts +57 -0
  322. package/src/runtime/lifecycle/codex-resync/plugin-files.ts +65 -0
  323. package/src/runtime/lifecycle/codex-resync/plugin-roots.ts +62 -0
  324. package/src/runtime/lifecycle/codex-resync/resync.ts +45 -0
  325. package/src/runtime/lifecycle/codex-resync/symlink-commands.ts +25 -0
  326. package/src/runtime/lifecycle/dispatch.ts +109 -0
  327. package/src/runtime/lifecycle/failure-lesson.ts +51 -0
  328. package/src/runtime/lifecycle/index.ts +34 -0
  329. package/src/runtime/lifecycle/inject-rules.ts +50 -0
  330. package/src/runtime/lifecycle/instructions-loaded.ts +22 -0
  331. package/src/runtime/lifecycle/kimi-rules-native.ts +44 -0
  332. package/src/runtime/lifecycle/lessons/dispatch.ts +88 -0
  333. package/src/runtime/lifecycle/lessons/reminder.ts +89 -0
  334. package/src/runtime/lifecycle/lessons/state.ts +23 -0
  335. package/src/runtime/lifecycle/memory/agent-lesson.ts +35 -0
  336. package/src/runtime/lifecycle/memory/capture-error.ts +41 -0
  337. package/src/runtime/lifecycle/memory/client.ts +65 -0
  338. package/src/runtime/lifecycle/memory/dispatch.ts +42 -0
  339. package/src/runtime/lifecycle/memory/recall.ts +30 -0
  340. package/src/runtime/lifecycle/memory/salience.ts +31 -0
  341. package/src/runtime/lifecycle/memory/state.ts +49 -0
  342. package/src/runtime/lifecycle/memory/track-ops.ts +15 -0
  343. package/src/runtime/lifecycle/post-compact.ts +38 -0
  344. package/src/runtime/lifecycle/post-edit-ts.ts +49 -0
  345. package/src/runtime/lifecycle/post-tracking.ts +40 -0
  346. package/src/runtime/lifecycle/pre-compact.ts +38 -0
  347. package/src/runtime/lifecycle/rules-root.ts +83 -0
  348. package/src/runtime/lifecycle/security/check-skill.ts +63 -0
  349. package/src/runtime/lifecycle/security/scan-patterns.ts +53 -0
  350. package/src/runtime/lifecycle/security/scan.ts +76 -0
  351. package/src/runtime/lifecycle/security/skill-state.ts +47 -0
  352. package/src/runtime/lifecycle/security/track-mcp.ts +29 -0
  353. package/src/runtime/lifecycle/security/track-skill-read.ts +31 -0
  354. package/src/runtime/lifecycle/seo/post-tool-use.ts +45 -0
  355. package/src/runtime/lifecycle/session-end.ts +17 -0
  356. package/src/runtime/lifecycle/session-start.ts +57 -0
  357. package/src/runtime/lifecycle/snapshot/board.ts +27 -0
  358. package/src/runtime/lifecycle/snapshot/format.ts +60 -0
  359. package/src/runtime/lifecycle/snapshot/git.ts +71 -0
  360. package/src/runtime/lifecycle/snapshot/index.ts +52 -0
  361. package/src/runtime/lifecycle/snapshot/version.ts +34 -0
  362. package/src/runtime/lifecycle/solid-detect.ts +56 -0
  363. package/src/runtime/lifecycle/stop-core.ts +35 -0
  364. package/src/runtime/lifecycle/subagent-cache.ts +61 -0
  365. package/src/runtime/lifecycle/task-completed.ts +94 -0
  366. package/src/runtime/lifecycle/teammate-idle-check.ts +63 -0
  367. package/src/runtime/lifecycle/teammate-idle.ts +28 -0
  368. package/src/runtime/lifecycle/tool-failure.ts +24 -0
  369. package/src/runtime/lifecycle/track-changes.ts +61 -0
  370. package/src/runtime/lifecycle/validate-solid.ts +70 -0
  371. package/src/runtime/lifecycle/validate-tailwind.ts +80 -0
  372. package/src/runtime/lifecycle-bridge.ts +74 -0
  373. package/src/runtime/mcp-key.ts +44 -0
  374. package/src/runtime/mcp-tool-name.ts +92 -0
  375. package/src/runtime/mcp.ts +91 -0
  376. package/src/runtime/modular.ts +77 -0
  377. package/src/runtime/normalize.ts +107 -0
  378. package/src/runtime/notices.ts +91 -0
  379. package/src/runtime/notification-sound.ts +49 -0
  380. package/src/runtime/notifications.ts +77 -0
  381. package/src/runtime/paths.ts +59 -0
  382. package/src/runtime/post-fanout.ts +41 -0
  383. package/src/runtime/post-outcome.ts +61 -0
  384. package/src/runtime/pre-allow.ts +97 -0
  385. package/src/runtime/precommit.ts +60 -0
  386. package/src/runtime/prompt-text.ts +38 -0
  387. package/src/runtime/receipt-capture.ts +34 -0
  388. package/src/runtime/record.ts +26 -0
  389. package/src/runtime/respond.ts +92 -0
  390. package/src/runtime/solid-file-size-gate.ts +58 -0
  391. package/src/runtime/solid-pre.ts +53 -0
  392. package/src/runtime/storage.ts +9 -0
  393. package/src/statusline/colors.ts +31 -0
  394. package/src/statusline/constants.ts +17 -0
  395. package/src/statusline/formatters.ts +54 -0
  396. package/src/statusline/index.ts +4 -0
  397. package/src/statusline/progress-bar.ts +46 -0
  398. package/src/tracking/codex-post-failure.ts +73 -0
  399. package/src/tracking/index.ts +2 -0
  400. package/src/tracking/integrity.ts +99 -0
  401. package/src/tracking/one-shot-dedup.ts +34 -0
  402. package/src/tracking/one-shot-failure.ts +42 -0
  403. package/src/tracking/one-shot-store.ts +95 -0
  404. package/src/tracking/one-shot.ts +93 -0
  405. package/src/tracking/receipts.ts +95 -0
  406. package/src/tracking/session-state.ts +94 -0
  407. package/src/tracking/store.ts +96 -0
  408. package/src/tracking/track-compact.ts +100 -0
  409. package/src/tracking/track-diff.ts +50 -0
  410. package/src/tracking/track-journal.ts +97 -0
  411. package/src/tracking/track-lock-sync.ts +83 -0
  412. package/src/tracking/track-lock.ts +67 -0
  413. package/src/util/compact-json.ts +23 -0
  414. package/src/util/index.ts +2 -0
  415. package/src/util/json-io.ts +47 -0
  416. package/src/util/project-root.ts +41 -0
  417. package/src/util/runtime-io.ts +85 -0
  418. package/src/util/semver.ts +30 -0
  419. package/dist/permission-mode-B0MUl7FT.mjs +0 -136
@@ -0,0 +1,128 @@
1
+ import { evaluate, type PolicyResult } from "../policy/evaluate";
2
+ import { protectedPathGate } from "../policy/trivial-edits";
3
+ import { existingLineCounts, isApexScoped } from "./gate-helpers";
4
+ import { FAIL_CLOSED } from "../policy/guards";
5
+ import { loadTrack } from "../tracking/store";
6
+ import { dryGate } from "./dry";
7
+ import { preCommitGate } from "./precommit";
8
+ import { modularGate } from "./modular";
9
+ import { frameworkSkillGate } from "./framework-skill-gate";
10
+ import { reconcileRefReadsFromTranscript } from "../freshness/ref-evidence";
11
+ import { reconcileRefReadsFromJournal } from "../freshness/ref-journal";
12
+ import { isShadcnWrite, shadcnSkillGate } from "../policy/shadcn-skill-gate";
13
+ import { isTailwindWrite, tailwindSkillGate } from "../policy/tailwind-skill-gate";
14
+ import { geminiMcpGate } from "../policy/gemini-mcp-gate";
15
+ import { apexScopedGate } from "./gate-apex";
16
+ import { withDenyLoop } from "./deny-loop-store";
17
+ import { recordOneShot } from "../tracking/one-shot";
18
+ import { dirname } from "node:path";
19
+ import type { GateInput } from "./gate-input";
20
+ import type { Prompt } from "../prompt/types";
21
+
22
+ export type { GateInput } from "./gate-input";
23
+
24
+ /** Prior agents the freshness gate requires before a code edit. */
25
+ export const REQUIRED_AGENTS: ReadonlyArray<string> = ["explore-codebase", "research-expert"];
26
+
27
+ /**
28
+ * Default freshness window (ms). Matches the plugin's `FUSE_ENFORCE_TTL_SEC`
29
+ * default (120s); only a fallback for callers that omit `windowMs` (e.g. tests) —
30
+ * the real CLI path always supplies it from `resolveTtlSec()` (`src/config/ttl.ts`).
31
+ */
32
+ export const DEFAULT_WINDOW_MS = 120_000;
33
+
34
+ /** Trivial edits allowed within the window before the full APEX gates apply. */
35
+ export const TRIVIAL_BUDGET = 4;
36
+
37
+ /**
38
+ * Full gate: {@link runGates} yields the first blocking prompt (or null); the tail
39
+ * records the one-shot metric ({@link recordOneShot}, observation-only) then lets
40
+ * {@link withDenyLoop} rewrite an identical retried deny (decision unchanged).
41
+ */
42
+ export async function gate(input: GateInput): Promise<Prompt | null> {
43
+ const prompt = await runGates(input);
44
+ return finalizeGate(input, prompt, input.command);
45
+ }
46
+
47
+ /**
48
+ * Gate independent Cursor command candidates without merging their shell syntax.
49
+ * Stateful outcome bookkeeping runs once, against the command that decided the
50
+ * aggregate result. A single distinct candidate follows {@link gate} verbatim.
51
+ */
52
+ export async function gateCommandCandidates(input: GateInput, candidates: readonly string[]): Promise<Prompt | null> {
53
+ const distinct = [...new Set(candidates)];
54
+ if (distinct.length <= 1) return gate(input);
55
+
56
+ let decisive: { prompt: Prompt; command: string } | undefined;
57
+ for (const command of distinct) {
58
+ const prompt = await runGates({ ...input, command });
59
+ if (!prompt) continue;
60
+ if (!decisive || promptRank(prompt) > promptRank(decisive.prompt)) decisive = { prompt, command };
61
+ if (prompt.kind === "block") break;
62
+ }
63
+ return finalizeGate(input, decisive?.prompt ?? null, decisive?.command ?? input.command);
64
+ }
65
+
66
+ /** Persist one final gate outcome and apply deny-loop enrichment exactly once. */
67
+ function finalizeGate(input: GateInput, prompt: Prompt | null, command: string | undefined): Prompt | null {
68
+ const op = { filePath: input.filePath, content: input.content, command };
69
+ const dir = dirname(input.trackFile);
70
+ recordOneShot(prompt, op, { now: input.now, dir, sessionId: input.sessionId });
71
+ return withDenyLoop(prompt, input.tool, op, { now: input.now, dir, windowMs: input.windowMs ?? DEFAULT_WINDOW_MS, sessionId: input.sessionId });
72
+ }
73
+
74
+ /** Higher numeric values dominate while equal values preserve wire order. */
75
+ function promptRank(prompt: Prompt): number {
76
+ return prompt.kind === "block" ? 3 : prompt.kind === "ask" ? 2 : 1;
77
+ }
78
+
79
+ /** Stateless guards, then the trivial fast path, then the stateful APEX gates. */
80
+ async function runGates(input: GateInput): Promise<Prompt | null> {
81
+ // Pre-commit lint hard-block runs FIRST: evaluate()'s GIT_ASK branch would
82
+ // otherwise short-circuit a `git commit` before the linters get to veto it.
83
+ const precommit = preCommitGate(input.tool, input.command, input.cwd);
84
+ if (precommit) return precommit;
85
+ // Hook-managed paths: absolute deny on ALL extensions, BEFORE the code-ext/exempt filters (parity enforce-apex-phases.ts:48-52 — isApexScoped never routes a non-code/exempt path to the guard).
86
+ const protectedDeny = protectedPathGate(input.tool, input.filePath);
87
+ if (protectedDeny) return protectedDeny;
88
+ const { raw: existingLines, code: existingCodeLines, content: existingContent } = existingLineCounts(input.filePath);
89
+ let quick: PolicyResult;
90
+ try {
91
+ quick = evaluate({ tool: input.tool, filePath: input.filePath, content: input.content, command: input.command, agentType: input.agentType, existingLines, existingContent, oldString: input.oldString, isReplaceAll: input.isReplaceAll, neverApproval: input.neverApproval });
92
+ } catch {
93
+ return FAIL_CLOSED;
94
+ }
95
+ if (quick.decision !== "allow" && quick.prompt) return quick.prompt;
96
+ const modular = modularGate(input.tool, input.filePath, input.content, input.cwd);
97
+ if (modular) return modular;
98
+ if (!input.filePath) return null;
99
+ const filePath = input.filePath;
100
+ // Reconcile refsRead BEFORE any consumer from two durable, race-immune sources (racy load→save loses lone
101
+ // writes under the fan-out): the transcript restores the LEAD's flushed read, the append-only journal (ref-journal.ts) the TEAMMATE's un-flushed one (lag > TTL).
102
+ const track = reconcileRefReadsFromJournal(
103
+ reconcileRefReadsFromTranscript(await loadTrack(input.trackFile), input.transcriptPath, input.now),
104
+ dirname(input.trackFile), input.now,
105
+ );
106
+ const solidOrSkill = frameworkSkillGate(input, track.refsRead, existingCodeLines);
107
+ if (solidOrSkill) return solidOrSkill;
108
+ // Standalone shadcn/ui gate (ports check-skill-loaded.py): runs independently of `framework` since a components.json / .css write may detect as "generic", mirroring the real plugin's own PreToolUse hook alongside react's.
109
+ if (isShadcnWrite(input.tool, filePath)) {
110
+ const shadcnBlock = shadcnSkillGate(input.tool, filePath, input.content ?? "", { refsRead: track.refsRead, authorizations: track.authorizations, sessionId: input.sessionId });
111
+ if (shadcnBlock) return shadcnBlock;
112
+ }
113
+ // Standalone Tailwind base-skill gate (ports check-tailwind-skill.py Phase 1): a .tsx/.jsx write with Tailwind classes needs a base Tailwind skill read, independent of framework.
114
+ if (isTailwindWrite(input.tool, filePath, input.content ?? "")) {
115
+ const twBlock = tailwindSkillGate(input.tool, filePath, input.content ?? "", track.refsRead);
116
+ if (twBlock) return twBlock;
117
+ }
118
+ // OPT-IN Gemini Design MCP gate (ports enforce-gemini-mcp.py) — a no-op unless FUSE_ENFORCE_GEMINI_MCP is set, then blocks hand-written Tailwind UI until a mcp__gemini-design__* call is made this session.
119
+ const geminiBlock = geminiMcpGate(input.tool, filePath, input.content ?? "", { authorizations: track.authorizations, sessionId: input.sessionId });
120
+ if (geminiBlock) return geminiBlock;
121
+ // The freshness/doc/SOLID APEX gates only police code files (require-apex-agents.py parity): non-code and exempt paths skip straight to the DRY check below.
122
+ if (isApexScoped(input.filePath)) {
123
+ const apex = await apexScopedGate(input, track, input.windowMs ?? DEFAULT_WINDOW_MS);
124
+ if (apex) return apex;
125
+ }
126
+ // DRY duplication (effectful: greps the codebase) — runs once the APEX gates pass.
127
+ return dryGate(input.tool, input.filePath, input.content, input.cwd);
128
+ }
@@ -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,116 @@
1
+ import { projectLayout } from "../config/layout";
2
+ import { detectFramework } from "../policy/detect-framework";
3
+ import { detectCreationIntent } from "../policy/creation-intent";
4
+ import { recordBrainstormRequired } from "../tracking/session-state";
5
+ import { withTrack } from "../tracking/store";
6
+ import { normalizeEvent } from "./normalize";
7
+ import { defaultStateDir, trackFile } from "./paths";
8
+ import { designLifecycle } from "./design-lifecycle";
9
+ import { promptSubmitContext } from "./inject-context";
10
+ import { lifecycleStdout } from "./lifecycle-bridge";
11
+ import { handlePre } from "./handle-pre";
12
+ import { handlePost } from "./handle-post";
13
+ import { asyncScopeStdout } from "./handle-scope-async";
14
+ import { resolveDesignCacheDir } from "./design-cache-resolve";
15
+ import { resyncCodexAgents } from "./lifecycle/codex-resync/resync";
16
+ import { resetFragmentRegistry } from "./fragment-registry";
17
+ import { attachBudgetRecap } from "./inject-budget-recap";
18
+ import { promptText } from "./prompt-text";
19
+ import { handleConfirmSubmit } from "./confirm/confirm-submit";
20
+ import { submitCodexConfirmation } from "./confirm/codex-confirm";
21
+ import { codexPromptOrigin } from "./confirm/codex-prompt-origin";
22
+ import { cursorProjectCwd } from "../adapters/cursor/context";
23
+ import { toCursorLifecycleResponse } from "../adapters/cursor/respond";
24
+ import type { HandleOptions, HandleOutcome } from "./handle-types";
25
+ export type { HandleOptions, HandleOutcome } from "./handle-types";
26
+
27
+ /** Raw Claude hook event name from a payload (empty when absent). */
28
+ function rawEventName(payload: Record<string, unknown>): string {
29
+ return typeof payload.hook_event_name === "string" ? payload.hook_event_name : "";
30
+ }
31
+
32
+ /**
33
+ * The full hook handler: on a PRE event it gates the tool-use (stateless guards
34
+ * then APEX gates from the session track) and returns the native response; on a
35
+ * POST event it records the activity into the track. The loop that makes the
36
+ * package behave like the Claude plugin, on any harness.
37
+ */
38
+ async function handleHookCore(id: string, payload: Record<string, unknown>, opts: HandleOptions): Promise<HandleOutcome> {
39
+ const event = normalizeEvent(id, payload);
40
+ if (id === "cursor") {
41
+ const cursorCwd = cursorProjectCwd(event.cwd, event.workspaceRoots ?? [], event.filePath, opts.cwd);
42
+ if (cursorCwd !== opts.cwd) opts = { ...opts, cwd: cursorCwd };
43
+ }
44
+ const rawPrompt = payload.prompt;
45
+ const userPrompt = typeof rawPrompt === "string" || Array.isArray(rawPrompt) ? promptText(rawPrompt) : undefined;
46
+ if (id === "codex" && rawEventName(payload) === "UserPromptSubmit" && userPrompt !== undefined) {
47
+ submitCodexConfirmation(event.sessionId, userPrompt, opts.now, opts.home, codexPromptOrigin(payload));
48
+ }
49
+ // Fresh slate for this invocation's capFragment tally — one hook event is
50
+ // exactly one lifecycle branch below (see dispatchLifecycle), so a single
51
+ // reset here can never mix fragments across unrelated events.
52
+ resetFragmentRegistry();
53
+ const layout = projectLayout(opts.cwd);
54
+ const file = trackFile(event.sessionId, defaultStateDir(opts.cwd));
55
+ const mcpDir = layout.cacheDir;
56
+ // Design pipeline (flag + `.design-state-*.json`) is anchored on the session
57
+ // id, not `mcpDir`/cwd — see design-cache-resolve.ts. `mcpDir` itself stays
58
+ // cwd-derived for the unrelated MCP/WebFetch cache.
59
+ const designCacheDir = resolveDesignCacheDir(event.sessionId, mcpDir, opts.home);
60
+ const framework = detectFramework(event.filePath ?? "", event.content ?? "", opts.cwd);
61
+
62
+ // Design-agent lifecycle (SubagentStart/Stop): init/cleanup the pipeline state machine.
63
+ // claude-code + codex only: verified against openai/codex's OWN generated hook schemas
64
+ // (codex-rs/hooks/schema/generated/subagent-{start,stop}.command.input.schema.json @ 385c0a93,
65
+ // superseding the stale 44918ea1 Cargo.toml-only bump) — `agent_id`/`agent_type` are REQUIRED
66
+ // on both events, unconditional on `hide_spawn_agent_metadata` (that flag only reshapes the
67
+ // spawn_agent tool RESULT, not the hook payload). `payload.prompt` (design-mode "component"
68
+ // detection below) is absent from Codex's schema — degrades to detectMode's default, not a
69
+ // break. Cursor/Gemini/Cline/Hermes remain unverified — NOT added without the same proof.
70
+ if ((id === "claude-code" || id === "codex") && designLifecycle(payload, designCacheDir, opts.cwd, String(opts.now), opts.now)) {
71
+ return { stdout: "", exit: 0 };
72
+ }
73
+
74
+ // Codex-only, fail-open: refresh the plugin agents/commands cache on SessionStart when its fingerprint changed (strict no-op on every other harness).
75
+ if (id === "codex" && rawEventName(payload) === "SessionStart") resyncCodexAgents();
76
+
77
+ // Async per-scope lifecycle (aipilot cache handlers + memory-neural Graphiti).
78
+ const asyncOut = await asyncScopeStdout(opts.scope, rawEventName(payload), payload, opts.cwd, opts.now, id);
79
+ if (asyncOut !== null) return { stdout: asyncOut, exit: 0 };
80
+
81
+ // Ported lifecycle/session/context hooks (SessionStart, SubagentStart/Stop, etc.).
82
+ const life = lifecycleStdout(payload, opts.cwd, opts.scope ?? "core", opts.now, id);
83
+ if (life !== null) {
84
+ // Claude-Code-only: attachBudgetRecap's systemMessage envelope assumes the
85
+ // Claude adapter's stdout shape (mirrors the designLifecycle gate above).
86
+ const stdout = id === "claude-code" ? attachBudgetRecap(life, rawEventName(payload), event.sessionId, opts.cwd, opts.now) : life;
87
+ return { stdout, exit: 0 };
88
+ }
89
+
90
+ // UserPromptSubmit (core scope): brainstorm flag + CLAUDE.md injection.
91
+ // `payload.prompt` is a string on Claude Code/Codex, an array of content
92
+ // blocks on Kimi (see promptText) — either shape is normalized to text;
93
+ // anything else (field absent, or an unrecognized type) stays `undefined`
94
+ // so the block below is skipped exactly as before promptText existed.
95
+ if (userPrompt !== undefined) {
96
+ if (id !== "codex") handleConfirmSubmit(event.sessionId, userPrompt, opts.now, opts.home);
97
+ await withTrack(file, (track) => recordBrainstormRequired(track, detectCreationIntent(userPrompt)));
98
+ return { stdout: promptSubmitContext(userPrompt, opts.cwd, id), exit: 0 };
99
+ }
100
+
101
+ if (event.phase === "post") {
102
+ return handlePost({ id, payload, event, framework, mcpDir, designCacheDir, file, opts });
103
+ }
104
+
105
+ return handlePre({ id, payload, event, framework, mcpDir, designCacheDir, file, opts });
106
+ }
107
+
108
+ /**
109
+ * Run one hook and adapt every Cursor scope outcome at the common runtime exit.
110
+ * Other harnesses retain the core handler's stdout and exit status unchanged.
111
+ */
112
+ export async function handleHook(id: string, payload: Record<string, unknown>, opts: HandleOptions): Promise<HandleOutcome> {
113
+ const outcome = await handleHookCore(id, payload, opts);
114
+ if (id !== "cursor") return outcome;
115
+ return { ...outcome, stdout: toCursorLifecycleResponse(outcome.stdout, rawEventName(payload)) };
116
+ }
@@ -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
+ }