@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,77 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join, relative, basename } from "node:path";
3
+ import type { Prompt } from "../prompt/types";
4
+ import { detectModularArchitecture } from "../policy/detect-project";
5
+
6
+ const NEXT_CONVENTION = /^(page|layout|loading|error|not-found|route|template|default|global-error|opengraph-image|twitter-image|icon|apple-icon|sitemap|robots|manifest|middleware)\.(tsx|ts|js|jsx)$/;
7
+ const NEXT_STATIC = /\.(css|ico|png|jpg|svg|json)$/;
8
+ const PHP_BLOCKED_IN_APP: readonly string[] = [
9
+ "/app/Models/", "/app/Services/", "/app/Actions/", "/app/Http/Controllers/",
10
+ "/app/Http/Requests/", "/app/Http/Resources/", "/app/Contracts/", "/app/DTOs/",
11
+ "/app/Repositories/", "/app/Events/", "/app/Listeners/", "/app/Jobs/",
12
+ "/app/Notifications/", "/app/Policies/",
13
+ ];
14
+
15
+ const block = (reason: string): Prompt => ({
16
+ kind: "block", title: "Modular architecture", reason,
17
+ actions: ["Move the code into the correct feature module", "Import only from the shared core module"],
18
+ });
19
+
20
+ /** Next.js `modules/` architecture: `app/` convention + cross-module import rules. */
21
+ function nextModular(filePath: string, content: string, cwd: string): Prompt | null {
22
+ const rel = relative(cwd, filePath);
23
+ const bn = basename(filePath);
24
+ if ((rel.startsWith("app/") || rel.startsWith("src/app/")) && !NEXT_CONVENTION.test(bn) && !NEXT_STATIC.test(bn)) {
25
+ return block(`BLOCKED: modular Next.js — '${bn}' is not an app/ convention file. Move business logic to modules/[feature]/. Only page.tsx, layout.tsx, route.ts allowed in app/.`);
26
+ }
27
+ const mod = filePath.match(/\/modules\/([^/]+)\//);
28
+ if (!mod) return null;
29
+ const current = mod[1] ?? "";
30
+ for (const m of content.matchAll(/from\s+['"][@.][^'"]*?\/modules\/([^/]+)\//g)) {
31
+ const imported = m[1] ?? "";
32
+ if (current === "cores") {
33
+ if (imported !== "cores" && imported !== "core") return block(`BLOCKED: modules/cores/ must not import from modules/${imported}/. Cores must be independent.`);
34
+ } else if (imported !== current && imported !== "cores" && imported !== "core") {
35
+ return block(`BLOCKED: cross-module import — '${current}' imports from '${imported}'. Only modules/cores/ is shared.`);
36
+ }
37
+ }
38
+ return null;
39
+ }
40
+
41
+ /** Laravel FuseCore architecture: `app/` domain ban + module.json + cross-module `use` rules. */
42
+ function fusecore(filePath: string, content: string, cwd: string): Prompt | null {
43
+ for (const b of PHP_BLOCKED_IN_APP) {
44
+ if (filePath.includes(b)) {
45
+ // Parity validate-fusecore.py:52-55 — name the precise target sub-path per
46
+ // entry (blocked.replace("/app/","/FuseCore/{Module}/App/")), not a generic dir.
47
+ const from = b.replace(/^\/|\/$/g, "");
48
+ const to = b.replaceAll("/app/", "/FuseCore/{Module}/App/").replace(/^\/|\/$/g, "");
49
+ return block(`BLOCKED: FuseCore project. Code in '${from}' FORBIDDEN. Move to '${to}'.`);
50
+ }
51
+ }
52
+ const mod = filePath.match(/\/FuseCore\/([A-Za-z]+)\//);
53
+ if (!mod) return null;
54
+ const name = mod[1] ?? "";
55
+ const mj = join(cwd, "FuseCore", name, "module.json");
56
+ // Parity validate-fusecore.py: name the full path to create ("Create {mj} first.").
57
+ if (!existsSync(mj)) return block(`BLOCKED: Module '${name}' missing module.json. Create ${mj} first.`);
58
+ for (const m of content.matchAll(/use\s+FuseCore\\(\w+)\\/g)) {
59
+ const imported = m[1] ?? "";
60
+ if (name === "Core") {
61
+ if (imported !== "Core") return block(`BLOCKED: FuseCore\\Core\\ must not use FuseCore\\${imported}\\.`);
62
+ } else if (imported !== name && imported !== "Core") {
63
+ return block(`BLOCKED: cross-module use — '${name}' uses '${imported}'. Only FuseCore\\Core\\ is shared.`);
64
+ }
65
+ }
66
+ return null;
67
+ }
68
+
69
+ /** Enforce the project's modular architecture (Next.js `modules/` or Laravel FuseCore) on a Write/Edit. */
70
+ export function modularGate(tool: string, filePath: string | undefined, content: string | undefined, cwd: string | undefined): Prompt | null {
71
+ if ((tool !== "Write" && tool !== "Edit") || !filePath || !cwd) return null;
72
+ if (/\/(node_modules|dist|build|\.next|vendor|storage)\//.test(filePath)) return null;
73
+ const arch = detectModularArchitecture(cwd);
74
+ if (arch === "nextjs-modular" && /\.(tsx|ts|jsx|js)$/.test(filePath)) return nextModular(filePath, content ?? "", cwd);
75
+ if (arch === "fusecore" && filePath.endsWith(".php")) return fusecore(filePath, content ?? "", cwd);
76
+ return null;
77
+ }
@@ -0,0 +1,107 @@
1
+ import { parseApplyPatch } from "../adapters/codex/apply-patch";
2
+ import { extractCursorEvent } from "../adapters/cursor/normalize";
3
+ import { commandToString } from "./command-string";
4
+ import { canonicalizeCodexShellTool } from "./codex-shell-tool";
5
+ import { canonicalizeMcpToolName } from "./mcp-tool-name";
6
+
7
+ /** One file/edit fanned out from Codex `apply_patch` or Cursor `afterFileEdit`. */
8
+ export interface NormalizedFile {
9
+ filePath: string;
10
+ content: string;
11
+ oldString?: string;
12
+ op: "add" | "update" | "delete";
13
+ }
14
+
15
+ /** A hook event normalized across harnesses. */
16
+ export interface NormalizedEvent {
17
+ /** Native event name when the adapter exposes one explicitly (Cursor). */
18
+ eventName?: string;
19
+ phase: "pre" | "post";
20
+ tool: string;
21
+ input: Record<string, unknown>;
22
+ sessionId: string;
23
+ filePath?: string;
24
+ content?: string;
25
+ /** Edit only: the tool_input.old_string being replaced — lets the file-size gate (policy/evaluate.ts + policy/edit-outcome.ts) compute the post-edit outcome instead of judging the stale on-disk count alone. Undefined for Write (no such field) and for cline/apply_patch (parsed separately, no equivalent field). */
26
+ oldString?: string;
27
+ command?: string;
28
+ /** Cursor-only independent command candidates from beforeMCPExecution. */
29
+ commandCandidates?: string[];
30
+ /** Subagent type, if the tool-use came from one (Explore/Plan are file-size-exempt). */
31
+ agentType?: string;
32
+ /** Harness-resolved permission mode (Claude emits it natively; Codex maps `AskForApproval::Never` to the same "bypassPermissions" string — see adapters/codex/permission-mode.ts). Generic field, Codex-only consumer today. */
33
+ permissionMode?: string;
34
+ /** Codex logical tool-use identity, shared by sibling hook callbacks. */
35
+ toolUseId?: string;
36
+ /** Harness-reported working directory used to scope Codex authorization. */
37
+ cwd?: string;
38
+ /** Validated Cursor multi-root workspace paths in wire order. */
39
+ workspaceRoots?: string[];
40
+ /**
41
+ * Per-file changes for Codex `apply_patch` and per-edit changes for Cursor
42
+ * `afterFileEdit`; undefined for single-file events.
43
+ */
44
+ files?: NormalizedFile[];
45
+ }
46
+
47
+ function str(v: unknown): string | undefined {
48
+ return typeof v === "string" ? v : undefined;
49
+ }
50
+
51
+ /**
52
+ * Normalize a hook payload, including Cline nesting and native Cursor events.
53
+ */
54
+ export function normalizeEvent(id: string, payload: Record<string, unknown>): NormalizedEvent {
55
+ if (id === "cline") {
56
+ const post = payload.postToolUse as Record<string, unknown> | undefined;
57
+ const node = (post ?? (payload.preToolUse as Record<string, unknown> | undefined)) ?? {};
58
+ const params = (node.parameters as Record<string, unknown> | undefined) ?? {};
59
+ return {
60
+ phase: post ? "post" : "pre",
61
+ tool: str(node.toolName) ?? "",
62
+ input: params,
63
+ sessionId: str(payload.taskId) ?? "",
64
+ filePath: str(params.path),
65
+ content: str(params.content),
66
+ command: str(params.command),
67
+ };
68
+ }
69
+ if (id === "cursor") {
70
+ return {
71
+ ...extractCursorEvent(payload),
72
+ sessionId: str(payload.session_id) ?? str(payload.conversation_id) ?? "",
73
+ agentType: str(payload.agent_type),
74
+ permissionMode: str(payload.permission_mode),
75
+ };
76
+ }
77
+ const event = str(payload.hook_event_name) ?? "";
78
+ const input = (payload.tool_input as Record<string, unknown> | undefined) ?? payload;
79
+ const tool = canonicalizeCodexShellTool(id, canonicalizeMcpToolName(id, str(payload.tool_name) ?? ""));
80
+ const base = {
81
+ phase: (/post|after/i.test(event) ? "post" : "pre") as "pre" | "post",
82
+ tool,
83
+ input,
84
+ sessionId: str(payload.session_id) ?? str(payload.conversation_id) ?? "",
85
+ agentType: str(payload.agent_type) ?? str(input.subagent_type),
86
+ permissionMode: str(payload.permission_mode),
87
+ toolUseId: str(payload.tool_use_id),
88
+ cwd: str(payload.cwd),
89
+ };
90
+ // Codex's `apply_patch` (its PRIMARY edit primitive) carries the whole change
91
+ // set as a freeform patch in `command` — no `file_path`/`content`, so the
92
+ // file-size/DRY/protected-path gates saw nothing (enforcement 0%). Fan it out
93
+ // into `files` and DROP the patch text from `command` so it can't false-match
94
+ // the git guards. Every other tool is untouched below.
95
+ if (tool === "apply_patch") {
96
+ const patch = str(input.command) ?? str(payload.command) ?? "";
97
+ const files = parseApplyPatch(patch).map((f) => ({ filePath: f.path, content: f.content, op: f.op }));
98
+ return { ...base, phase: base.phase, files: files.length > 0 ? files : undefined };
99
+ }
100
+ return {
101
+ ...base,
102
+ filePath: str(input.file_path) ?? str(input.path) ?? str(payload.file_path),
103
+ content: str(input.content) ?? str(input.new_string),
104
+ oldString: str(input.old_string),
105
+ command: commandToString(input.command) ?? commandToString(payload.command),
106
+ };
107
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * @module notices
3
+ * Compact, uniform "compliance" notices for the user-visible `systemMessage`
4
+ * channel — the visual counterpart to the additionalContext-only gate/credit
5
+ * signals, which land in agent-only context and stay invisible to the human in
6
+ * the Claude Code UI (deny reasons show in red; these did not show at all).
7
+ * Owner-reported gap: pass-notices existed in v0.1.49 for design gates, but
8
+ * nothing surfaced skill credits, freshness, or the sniper reminder to the human.
9
+ *
10
+ * The text builders are pure. `refCreditNoticeFor` is the one exception — like
11
+ * `pre-allow.ts` uses the JSON `oncePerWindow`, this one uses the exclusive
12
+ * {@link onceExclusive} mode instead: it fires on the same short burst window
13
+ * as `track-changes.ts`'s sniper reminder, fanned out across every installed
14
+ * plugin's PostToolUse hook for ONE real edit, so the same lost-update race
15
+ * applies (lesson 2026-07-05 16:00). Rendering onto a harness's native stdout
16
+ * always goes through the existing adapter helpers (`respond`/
17
+ * `attachSystemMessage`); a harness with no `systemMessage` channel (e.g.
18
+ * cline) silently drops the notice there (documented no-op, never a crash) —
19
+ * nothing in this module renders directly.
20
+ *
21
+ * CHANNEL CONTRACT — human-only, exactly once: the `userMessage`/`systemMessage`
22
+ * these notices ride is a HUMAN channel by platform contract. It never reaches
23
+ * the agent — `formatPrompt` (src/prompt/types.ts) excludes it — so the emitting
24
+ * sub-agent does NOT see its own notice; only the human owner does, and exactly
25
+ * ONCE even under the multi-plugin fan-out, because the emitter is gated by
26
+ * {@link onceExclusive}. Verified live 2026-07-06: under the real ×11 fan-out a
27
+ * single skill-ref Read surfaced one `✓ SOLID refs read` line in the owner's
28
+ * terminal (not duplicated, and correctly invisible to the agent).
29
+ */
30
+ import { onceExclusive } from "./inject-dedup";
31
+ import { BURST_DEDUP_MS } from "./burst-window";
32
+
33
+ /** One compliance line: `✓ <gate> — <detail>` (detail omitted when empty). */
34
+ export function complianceNotice(gate: string, detail: string): string {
35
+ return detail ? `✓ ${gate} — ${detail}` : `✓ ${gate}`;
36
+ }
37
+
38
+ /** One non-blocking requirement line: `⚠ <requirement> — <detail>`. */
39
+ export function requirementNotice(requirement: string, detail: string): string {
40
+ return detail ? `⚠ ${requirement} — ${detail}` : `⚠ ${requirement}`;
41
+ }
42
+
43
+ /** Extract the skill name from a `.md` ref path (`.../skills/<name>/...`), or null when it isn't a skill reference (a banal doc Read — no notice). */
44
+ export function skillNameFromRefPath(path: string): string | null {
45
+ const m = /skills\/([^/]+)\//.exec(path);
46
+ return m?.[1] ?? null;
47
+ }
48
+
49
+ /** Notice for a SOLID/skill reference credited via an in-session Read, or null for a non-skill `.md`. */
50
+ export function refCreditedNotice(path: string): string | null {
51
+ const skill = skillNameFromRefPath(path);
52
+ return skill ? complianceNotice("SOLID refs read", skill) : null;
53
+ }
54
+
55
+ /** Notice for the APEX freshness gate (explore+research) currently satisfied. */
56
+ export function evidenceFreshNotice(): string {
57
+ return complianceNotice("evidence fresh", "explore+research");
58
+ }
59
+
60
+ /** Notice mirroring the existing sniper-required additionalContext reminder. */
61
+ export function sniperRequiredNotice(fileName: string): string {
62
+ return requirementNotice("sniper required", fileName);
63
+ }
64
+
65
+ /**
66
+ * The one `✓ SOLID refs read (<skill>)` notice to show for this PostToolUse
67
+ * call, or null. Scans the activities `activityFor` recorded from this event
68
+ * for a skill-ref Read, deduped per (session, path) against the same burst
69
+ * window as the sniper reminder — the ~11 sibling-plugin fan-out for one real
70
+ * Read must never repeat it (lesson 2026-07-05 15:21).
71
+ * @param activities - This event's recorded activities (only `ref` entries matter).
72
+ * @param sessionId - Current session id (dedup scope).
73
+ * @param now - Event clock.
74
+ * @param dir - State-dir override for the dedup sidecar (tests MUST pass an
75
+ * isolated dir; production passes the per-project state dir).
76
+ */
77
+ export function refCreditNoticeFor(
78
+ activities: ReadonlyArray<{ kind: string; path?: string }>,
79
+ sessionId: string,
80
+ now: number,
81
+ dir?: string,
82
+ ): string | null {
83
+ for (const a of activities) {
84
+ if (a.kind !== "ref" || !a.path) continue;
85
+ const notice = refCreditedNotice(a.path);
86
+ if (!notice) continue;
87
+ if (!onceExclusive(`ref-credited:${sessionId}:${a.path}`, BURST_DEDUP_MS, { now, dir })) continue;
88
+ return notice;
89
+ }
90
+ return null;
91
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * @module notification-sound
3
+ * Resolve the on-disk sound file for a hook notification kind: a per-kind env
4
+ * override, else the package's own `assets/song/` bundle, else a plugin-root
5
+ * fallback. Returns an absolute path or `null` ("stay silent"). NEVER throws.
6
+ * @packageDocumentation
7
+ */
8
+ import { existsSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { runningVersion } from "../cli/doctor";
11
+
12
+ /** Notification kinds the harness can voice. */
13
+ export type SoundKind = "stop" | "permission" | "human";
14
+
15
+ /** Kind → asset filename (under `assets/song/` and the plugin-root fallback). */
16
+ const FILES: Record<SoundKind, string> = { stop: "finish.mp3", permission: "permission-need.mp3", human: "need-human.mp3" };
17
+
18
+ /** Kind → env var holding an explicit override path. */
19
+ const ENV_VARS: Record<SoundKind, string> = { stop: "FUSE_HARNESS_SOUND_STOP", permission: "FUSE_HARNESS_SOUND_PERMISSION", human: "FUSE_HARNESS_SOUND_HUMAN" };
20
+
21
+ /**
22
+ * Resolve the sound file for `kind`, or `null` when none is available. Cascade:
23
+ * (1) the per-kind env override when present on disk; (2) the package's own
24
+ * `assets/song/<f>.mp3`, located by walking up to the running `package.json`
25
+ * ({@link runningVersion} — survives the flat/hashed `dist/` bundle a hardcoded
26
+ * `../../` of non-deterministic depth would escape); (3) `$CLAUDE_PLUGIN_ROOT/song/<f>.mp3`. Never throws.
27
+ * @param kind - Which event to voice.
28
+ * @param env - Injectable environment (defaults to `process.env`).
29
+ * @param moduleUrl - Injectable module URL (defaults to this module's; locates the package).
30
+ */
31
+ export function resolveSound(kind: SoundKind, env: Record<string, string | undefined> = process.env, moduleUrl: string = import.meta.url): string | null {
32
+ try {
33
+ const override = env[ENV_VARS[kind]];
34
+ if (override && existsSync(override)) return override;
35
+ const pkgRoot = runningVersion(moduleUrl).path;
36
+ if (pkgRoot !== "unknown") {
37
+ const asset = join(pkgRoot, "assets", "song", FILES[kind]);
38
+ if (existsSync(asset)) return asset;
39
+ }
40
+ const pluginRoot = env.CLAUDE_PLUGIN_ROOT;
41
+ if (pluginRoot) {
42
+ const fallback = join(pluginRoot, "song", FILES[kind]);
43
+ if (existsSync(fallback)) return fallback;
44
+ }
45
+ return null;
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * @module notifications
3
+ * Native OS notification sound for lifecycle hook events (turn Stop, permission
4
+ * needed, human needed). ON by default; opt OUT with `FUSE_HARNESS_SOUND=0`.
5
+ *
6
+ * Only two events invoke {@link notify}: the core-scope `Stop` (Codex-only —
7
+ * Claude voices Stop via its own native `afplay` hook, so this never double-
8
+ * sounds) and `TeammateIdle` (Claude-only, which has no native sound). That
9
+ * structural split IS the anti-double-sound guarantee — do NOT add a harness-id
10
+ * gate here: it would silence `TeammateIdle`'s "human" sound, whose only home is
11
+ * claude-code. PermissionRequest/Notification stay native-only and are
12
+ * intentionally not wired here (no dead code).
13
+ *
14
+ * ABSOLUTE fail-open, `command || true` semantics: an opt-out, an unsupported
15
+ * platform, a missing player binary, a missing/undecodable sound file, or a
16
+ * non-zero exit is swallowed — this module NEVER throws and NEVER blocks the
17
+ * caller, so a broken/absent player can never break a hook.
18
+ * @packageDocumentation
19
+ */
20
+ import { spawn } from "node:child_process";
21
+ import { resolveSound, type SoundKind } from "./notification-sound";
22
+
23
+ export type { SoundKind } from "./notification-sound";
24
+
25
+ /** A resolved player invocation (binary + argv). */
26
+ export interface PlayerSpec {
27
+ bin: string;
28
+ args: string[];
29
+ }
30
+
31
+ /**
32
+ * The player command for `platform` playing `file`, or undefined for an
33
+ * unsupported platform. Pure — no filesystem/process access, so it is cheaply
34
+ * unit-testable without spawning anything. mp3 decoding is native on darwin
35
+ * (`afplay`); on linux (`paplay`/libsndfile) and win32 (`SoundPlayer`, wav-only)
36
+ * a bare mp3 may not decode — harmless under the fail-open contract.
37
+ * @param platform - `process.platform`-shaped value.
38
+ * @param file - Absolute path to the sound file to play.
39
+ */
40
+ export function resolvePlayer(platform: string, file: string): PlayerSpec | undefined {
41
+ if (platform === "darwin") return { bin: "afplay", args: [file] };
42
+ if (platform === "linux") return { bin: "paplay", args: [file] };
43
+ if (platform === "win32") return { bin: "powershell", args: ["-NoProfile", "-c", `(New-Object Media.SoundPlayer '${file}').PlaySync()`] };
44
+ return undefined;
45
+ }
46
+
47
+ /** True unless the user opted OUT (`FUSE_HARNESS_SOUND=0`). ON by default. */
48
+ export function soundEnabled(env: Record<string, string | undefined> = process.env): boolean {
49
+ return env.FUSE_HARNESS_SOUND !== "0";
50
+ }
51
+
52
+ /**
53
+ * Fire-and-forget a native notification sound for `kind`. See module doc for
54
+ * the absolute fail-open contract. A `null` resolved sound (nothing on disk) is
55
+ * a silent no-op.
56
+ * @param kind - Which event to voice.
57
+ * @param opts - Injectable platform/env/spawn for tests; production defaults to the real ones.
58
+ */
59
+ export function notify(
60
+ kind: SoundKind,
61
+ opts: { platform?: string; env?: Record<string, string | undefined>; spawnFn?: typeof spawn } = {},
62
+ ): void {
63
+ try {
64
+ const env = opts.env ?? process.env;
65
+ if (!soundEnabled(env)) return;
66
+ const file = resolveSound(kind, env);
67
+ if (!file) return;
68
+ const player = resolvePlayer(opts.platform ?? process.platform, file);
69
+ if (!player) return;
70
+ const run = opts.spawnFn ?? spawn;
71
+ const child = run(player.bin, player.args, { stdio: "ignore", detached: true });
72
+ child.on("error", () => { /* fail-open: a missing binary must never throw */ });
73
+ child.unref();
74
+ } catch {
75
+ /* absolute fail-open: a notification must never break the hook */
76
+ }
77
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * @module paths
3
+ * Runtime path helpers — per-project, out-of-tree harness state.
4
+ *
5
+ * Default base: ~/.fuse-harness/state/<projectHash>/
6
+ * where projectHash = 8-char MD5 of CLAUDE_PROJECT_DIR (or cwd).
7
+ * Persistent and not world-writable (unlike /tmp), and OUTSIDE the repo so the
8
+ * agent has no "legitimate" reason to write it (the protected-path guard denies
9
+ * it, and the gate verifies freshness from the transcript, not this file).
10
+ *
11
+ * @packageDocumentation
12
+ */
13
+
14
+ import { join } from "node:path";
15
+ import { hashText } from "../util/json-io";
16
+ import { fuseHarnessHome } from "./home-state";
17
+
18
+ /** Resolve the project root from the environment or fall back to cwd. */
19
+ function resolveProjectDir(): string {
20
+ return process.env["CLAUDE_PROJECT_DIR"] ?? process.cwd();
21
+ }
22
+
23
+ /**
24
+ * Compute a stable 8-char hex hash for a project directory path.
25
+ * Delegates to `hashText` (MD5, non-cryptographic — used as a stable dir key only).
26
+ *
27
+ * @param projectDir - Absolute path to the project root; defaults to CLAUDE_PROJECT_DIR/cwd.
28
+ * @returns 8-char lowercase hex string.
29
+ */
30
+ export function projectHash(projectDir?: string): string {
31
+ return hashText(projectDir ?? resolveProjectDir());
32
+ }
33
+
34
+ /**
35
+ * Canonical base directory for per-project harness state.
36
+ * Resolves to: ~/.fuse-harness/state/<projectHash>/
37
+ *
38
+ * @param projectDir - Optional override for hashing; defaults to CLAUDE_PROJECT_DIR/cwd.
39
+ * @returns Absolute directory path (not yet created on disk).
40
+ */
41
+ export function defaultStateDir(projectDir?: string): string {
42
+ return join(fuseHarnessHome(), "state", projectHash(projectDir));
43
+ }
44
+
45
+ /**
46
+ * Absolute path to a session's track JSON file.
47
+ *
48
+ * The session identifier is sanitised to `[A-Za-z0-9_-]` before use in the filename.
49
+ *
50
+ * @param sessionId - Claude session identifier (raw value accepted; sanitised internally).
51
+ * @param baseDir - Override the base directory. Omit in production; pass an explicit
52
+ * temp path in unit tests to avoid touching $HOME.
53
+ * @returns Absolute path, e.g. ~/.fuse-harness/state/a1b2c3d4/track-abc123.json
54
+ */
55
+ export function trackFile(sessionId: string, baseDir?: string): string {
56
+ const dir = baseDir ?? defaultStateDir();
57
+ const safe = sessionId.replace(/[^A-Za-z0-9_-]/g, "_") || "default";
58
+ return join(dir, `track-${safe}.json`);
59
+ }
@@ -0,0 +1,41 @@
1
+ import type { NormalizedEvent } from "./normalize";
2
+
3
+ /**
4
+ * Fan Codex `apply_patch` or Cursor `afterFileEdit` `event.files` into one
5
+ * per-file event per touched file, so the per-file PostToolUse gates (SOLID
6
+ * size, Tailwind, tracking, post-edit context) see EACH file instead of the
7
+ * whole patch (whose own `filePath`/`content` are always undefined). `add`
8
+ * maps to `Write`, `update` to `Edit` (mirrors the Pre-phase `applyPatchGate`'s
9
+ * tool mapping); `delete`/`move` map to a tool none of those Write|Edit-gated
10
+ * checks recognize, so they no-op on it without special-casing each gate.
11
+ * Returns `[event]` unchanged when no per-file changes are present.
12
+ * @param event - The normalized PostToolUse event.
13
+ * @returns One event per touched file, or the original event.
14
+ */
15
+ export function fanOutFiles(event: NormalizedEvent): NormalizedEvent[] {
16
+ if (!event.files || event.files.length === 0) return [event];
17
+ return event.files.map((f) => ({
18
+ ...event,
19
+ tool: f.op === "add" ? "Write" : f.op === "update" ? "Edit" : "apply_patch:delete",
20
+ filePath: f.filePath,
21
+ content: f.op === "delete" ? undefined : f.content,
22
+ oldString: f.oldString,
23
+ }));
24
+ }
25
+
26
+ /**
27
+ * Run `check(tool, filePath)` over each fanned-out file, OR-ing the verdict —
28
+ * the first non-empty result wins (parity with `applyPatchGate`'s "one
29
+ * violating hunk blocks the whole envelope").
30
+ * @param files - Per-file events from {@link fanOutFiles}.
31
+ * @param check - A PostToolUse gate keyed on `(tool, filePath)`.
32
+ * @returns The first non-empty result, or `""` when every file is clean.
33
+ */
34
+ export function firstFileMatch(files: readonly NormalizedEvent[], check: (tool: string, filePath: string) => string): string {
35
+ for (const f of files) {
36
+ if (!f.filePath) continue;
37
+ const result = check(f.tool, f.filePath);
38
+ if (result) return result;
39
+ }
40
+ return "";
41
+ }
@@ -0,0 +1,61 @@
1
+ import { attachSystemMessage } from "../adapters/claude";
2
+ import { designPassNotice } from "../policy/design/gates";
3
+ import { refCreditNoticeFor } from "./notices";
4
+ import { defaultStateDir } from "./paths";
5
+ import { respond } from "./respond";
6
+ import type { Activity } from "./record";
7
+ import type { HandleOutcome } from "./handle";
8
+ import type { NormalizedEvent } from "./normalize";
9
+ import { formatPrompt, type Prompt } from "../prompt/types";
10
+
11
+ /** Extract the additional-context body from a Claude-shaped post response. */
12
+ export function additionalContextOf(stdout: string): string {
13
+ if (!stdout) return "";
14
+ try {
15
+ const parsed = JSON.parse(stdout) as { hookSpecificOutput?: { additionalContext?: string } };
16
+ return parsed.hookSpecificOutput?.additionalContext ?? "";
17
+ } catch {
18
+ return "";
19
+ }
20
+ }
21
+
22
+ function cursorPostToolOutput(eventName: string, designWarn: Prompt | null, userMessage: string | undefined, extra: string): string {
23
+ if (!/^postToolUse$/i.test(eventName)) return "";
24
+ const context = [designWarn ? formatPrompt(designWarn) : "", designWarn?.userMessage, userMessage, additionalContextOf(extra)]
25
+ .filter(Boolean)
26
+ .join("\n");
27
+ return context ? JSON.stringify({ additional_context: context }) : "";
28
+ }
29
+
30
+ /**
31
+ * Assemble the final PostToolUse response after all side effects and deny paths.
32
+ * @param input - Final assembly values captured at their original evaluation times.
33
+ * @returns The native hook outcome.
34
+ */
35
+ export function postOutcome(input: {
36
+ id: string; agentId: string; sessionId: string; now: number; cwd: string;
37
+ activities: readonly Activity[]; files: readonly NormalizedEvent[];
38
+ designCacheDir: string; designWarn: Prompt | null; extra: string;
39
+ cursorAfterFileEdit: boolean; cursorEventName: string;
40
+ }): HandleOutcome {
41
+ const { id, agentId, sessionId, now, cwd, activities, files, designCacheDir, designWarn, extra, cursorAfterFileEdit, cursorEventName } = input;
42
+ const noticeLines: string[] = [];
43
+ for (const f of files) {
44
+ const notice = designPassNotice({ agentId, tool: f.tool, filePath: f.filePath ?? "", content: f.content ?? "", url: "", phase: "post" }, designCacheDir);
45
+ if (notice?.userMessage) noticeLines.push(notice.userMessage);
46
+ }
47
+ const notice: Prompt | null = noticeLines.length ? { kind: "inform", title: "Design pipeline", reason: "", userMessage: noticeLines.join("\n") } : null;
48
+ const refNotice = refCreditNoticeFor(activities, sessionId, now, defaultStateDir(cwd));
49
+ const userMessage = [notice?.userMessage, refNotice].filter(Boolean).join("\n") || undefined;
50
+ // Cursor ignores callback fields for afterFileEdit; complete successfully
51
+ // without emitting the permission schema reserved for pre-execution hooks.
52
+ if (cursorAfterFileEdit) return { stdout: "{}", exit: 0 };
53
+ if (id === "cursor") return { stdout: cursorPostToolOutput(cursorEventName, designWarn, userMessage, extra), exit: 0 };
54
+ if (designWarn) return { stdout: respond(id, userMessage ? { ...designWarn, userMessage } : designWarn, "PostToolUse"), exit: 0 };
55
+ if (!userMessage) return { stdout: extra, exit: 0 };
56
+ const withUserMessage: Prompt = notice ? { ...notice, userMessage } : { kind: "inform", title: "Compliance", reason: "", userMessage };
57
+ if (!extra) return { stdout: respond(id, withUserMessage, "PostToolUse"), exit: 0 };
58
+ if (id === "claude-code" || id === "codex") return { stdout: attachSystemMessage(extra, userMessage), exit: 0 };
59
+ if (id === "kimi") return { stdout: respond(id, withUserMessage, "PostToolUse"), exit: 0 };
60
+ return { stdout: respond(id, withUserMessage, "PostToolUse") || extra, exit: 0 };
61
+ }