@fusengine/harness 0.1.90 → 0.1.91

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (417) hide show
  1. package/dist/adapters/claude/index.d.mts +1 -1
  2. package/dist/adapters/claude/index.mjs +1 -1
  3. package/dist/adapters/cline/index.mjs +1 -1
  4. package/dist/adapters/codex/index.d.mts +2 -2
  5. package/dist/adapters/codex/index.mjs +3 -2
  6. package/dist/adapters/cursor/index.d.mts +15 -28
  7. package/dist/adapters/cursor/index.mjs +35 -46
  8. package/dist/adapters/gemini/index.mjs +1 -1
  9. package/dist/adapters/hermes/index.d.mts +1 -1
  10. package/dist/adapters/hermes/index.mjs +1 -1
  11. package/dist/adapters/kimi/index.d.mts +1 -1
  12. package/dist/adapters/kimi/index.mjs +1 -1
  13. package/dist/{claude-5bC8TDAz.mjs → claude-Ckv2_TgP.mjs} +3 -43
  14. package/dist/cli/bin.mjs +429 -16
  15. package/dist/cli/index.mjs +1 -1
  16. package/dist/command-string-CALMTnwN.mjs +42 -0
  17. package/dist/{evaluate-C1VSI5pb.mjs → evaluate-CMiIqHeH.mjs} +261 -13
  18. package/dist/{handle-CBaOcFZq.mjs → handle-C43gA-Pr.mjs} +701 -134
  19. package/dist/{hermes-kP6NUQlG.mjs → hermes-B9-p_3IF.mjs} +3 -2
  20. package/dist/{index-DBgnIZn9.d.mts → index-B8PG82su.d.mts} +1 -1
  21. package/dist/{index-D4C2-IIc.d.mts → index-SmKYkk2N.d.mts} +1 -1
  22. package/dist/index.d.mts +2 -2
  23. package/dist/index.mjs +2 -2
  24. package/dist/init/index.d.mts +1 -1
  25. package/dist/init/index.mjs +1 -1
  26. package/dist/{kimi-D6X7b8wF.mjs → kimi-G2wcSh5-.mjs} +3 -2
  27. package/dist/normalize-BjG6unTj.mjs +317 -0
  28. package/dist/policy/index.d.mts +1 -1
  29. package/dist/policy/index.mjs +2 -2
  30. package/dist/prompt/index.d.mts +1 -1
  31. package/dist/{run-CdIlUtjv.mjs → run-DkrzC0gb.mjs} +1 -1
  32. package/dist/{run-TAXNRSpD.mjs → run-GHAMqcOn.mjs} +26 -3
  33. package/dist/runtime/index.d.mts +27 -16
  34. package/dist/runtime/index.mjs +2 -2
  35. package/dist/{types-C0-igRKr.d.mts → types-Bh0jEF3_.d.mts} +2 -0
  36. package/dist/{validate-DZFtAbLP.mjs → validate-KjZ1X9tH.mjs} +1 -1
  37. package/package.json +3 -2
  38. package/src/adapters/claude/index.ts +92 -0
  39. package/src/adapters/cline/index.ts +36 -0
  40. package/src/adapters/codex/apply-patch.ts +94 -0
  41. package/src/adapters/codex/index.ts +84 -0
  42. package/src/adapters/codex/permission-mode.ts +16 -0
  43. package/src/adapters/cursor/context.ts +46 -0
  44. package/src/adapters/cursor/events.ts +38 -0
  45. package/src/adapters/cursor/index.ts +43 -0
  46. package/src/adapters/cursor/interfaces/types.ts +88 -0
  47. package/src/adapters/cursor/native-response.ts +158 -0
  48. package/src/adapters/cursor/normalize.ts +123 -0
  49. package/src/adapters/cursor/respond.ts +123 -0
  50. package/src/adapters/gemini/index.ts +36 -0
  51. package/src/adapters/hermes/index.ts +55 -0
  52. package/src/adapters/hermes/interfaces/types.ts +26 -0
  53. package/src/adapters/kimi/index.ts +96 -0
  54. package/src/adapters/kimi/interfaces/types.ts +55 -0
  55. package/src/cache/compact.ts +55 -0
  56. package/src/cache/index.ts +5 -0
  57. package/src/cache/io.ts +38 -0
  58. package/src/cache/mcp-response.ts +39 -0
  59. package/src/cache/mcp-store.ts +70 -0
  60. package/src/cache/store.ts +93 -0
  61. package/src/changelog/fetch.ts +79 -0
  62. package/src/cli/bin.ts +100 -0
  63. package/src/cli/cursor-event-scanner.ts +175 -0
  64. package/src/cli/cursor-stdin-reader.ts +65 -0
  65. package/src/cli/doctor.ts +96 -0
  66. package/src/cli/hook-io.ts +130 -0
  67. package/src/cli/hook-sound.ts +49 -0
  68. package/src/cli/index.ts +1 -0
  69. package/src/cli/json-primitive-scanner.ts +83 -0
  70. package/src/cli/run.ts +36 -0
  71. package/src/cli/scope.ts +31 -0
  72. package/src/codex-rules/build.ts +63 -0
  73. package/src/codex-rules/index.ts +8 -0
  74. package/src/codex-rules/mapping.ts +42 -0
  75. package/src/codex-rules/rules/absolute-paths.ts +52 -0
  76. package/src/codex-rules/rules/ask.ts +44 -0
  77. package/src/codex-rules/rules/chmod-ask.ts +57 -0
  78. package/src/codex-rules/rules/dd.ts +23 -0
  79. package/src/codex-rules/rules/diskutil.ts +33 -0
  80. package/src/codex-rules/rules/mkfs.ts +42 -0
  81. package/src/codex-rules/rules/privilege.ts +49 -0
  82. package/src/codex-rules/rules/remote-fetch.ts +31 -0
  83. package/src/codex-rules/rules/rm-variants.ts +58 -0
  84. package/src/codex-rules/rules/single-commands.ts +57 -0
  85. package/src/codex-rules/skip-list.ts +25 -0
  86. package/src/codex-rules/starlark.ts +46 -0
  87. package/src/codex-rules/types.ts +36 -0
  88. package/src/config/dotenv.ts +55 -0
  89. package/src/config/env.ts +10 -0
  90. package/src/config/home-dir.ts +36 -0
  91. package/src/config/index.ts +5 -0
  92. package/src/config/layout.ts +52 -0
  93. package/src/config/limits.ts +56 -0
  94. package/src/config/ttl.ts +30 -0
  95. package/src/detect/harness.ts +73 -0
  96. package/src/detect/index.ts +1 -0
  97. package/src/detect/interfaces/types.ts +18 -0
  98. package/src/freshness/agent-evidence-record.ts +95 -0
  99. package/src/freshness/agent-evidence.ts +95 -0
  100. package/src/freshness/codex-spawn-evidence.ts +91 -0
  101. package/src/freshness/doc-helpers.ts +90 -0
  102. package/src/freshness/evidence-harvest-io.ts +72 -0
  103. package/src/freshness/evidence-harvest.ts +61 -0
  104. package/src/freshness/explore-tools.ts +95 -0
  105. package/src/freshness/index.ts +2 -0
  106. package/src/freshness/query-framework.ts +73 -0
  107. package/src/freshness/ref-evidence.ts +49 -0
  108. package/src/freshness/ref-journal.ts +98 -0
  109. package/src/freshness/trivial-edit-counter.ts +30 -0
  110. package/src/index.ts +11 -0
  111. package/src/init/index.ts +2 -0
  112. package/src/init/run.ts +32 -0
  113. package/src/init/templates.ts +75 -0
  114. package/src/memory/gitignore.ts +14 -0
  115. package/src/memory/index.ts +3 -0
  116. package/src/memory/registry.ts +39 -0
  117. package/src/memory/session-roots.ts +99 -0
  118. package/src/memory/state.ts +58 -0
  119. package/src/policy/apex-authorization.ts +100 -0
  120. package/src/policy/apex-gates.ts +91 -0
  121. package/src/policy/apex-target-steps.ts +73 -0
  122. package/src/policy/apex-target.ts +49 -0
  123. package/src/policy/apex-task-context.ts +81 -0
  124. package/src/policy/apex.ts +94 -0
  125. package/src/policy/cartographer/build-tree.ts +69 -0
  126. package/src/policy/cartographer/describe.ts +53 -0
  127. package/src/policy/cartographer/entry.ts +38 -0
  128. package/src/policy/cartographer/frontmatter.ts +48 -0
  129. package/src/policy/cartographer/indicators.ts +34 -0
  130. package/src/policy/claude-md-context.ts +66 -0
  131. package/src/policy/conventions/index.ts +11 -0
  132. package/src/policy/conventions/interfaces.ts +71 -0
  133. package/src/policy/conventions/langs.ts +53 -0
  134. package/src/policy/conventions/query.ts +49 -0
  135. package/src/policy/conventions/react-hooks.ts +18 -0
  136. package/src/policy/conventions/store-or-query.ts +21 -0
  137. package/src/policy/conventions/stores.ts +56 -0
  138. package/src/policy/conventions/strip.ts +87 -0
  139. package/src/policy/conventions/verdict.ts +38 -0
  140. package/src/policy/creation-intent.ts +11 -0
  141. package/src/policy/deny-loop.ts +88 -0
  142. package/src/policy/design/allowed-write.ts +28 -0
  143. package/src/policy/design/content-checks.ts +45 -0
  144. package/src/policy/design/corpus-fs.ts +21 -0
  145. package/src/policy/design/corpus-resolve-cache.ts +49 -0
  146. package/src/policy/design/corpus-resolve.ts +99 -0
  147. package/src/policy/design/corpus.ts +92 -0
  148. package/src/policy/design/design-system-rules.ts +45 -0
  149. package/src/policy/design/flag.ts +30 -0
  150. package/src/policy/design/gates-pipeline.ts +105 -0
  151. package/src/policy/design/gates.ts +87 -0
  152. package/src/policy/design/screenshot-tools.ts +24 -0
  153. package/src/policy/design/skill-evidence.ts +34 -0
  154. package/src/policy/design/skill-gate.ts +81 -0
  155. package/src/policy/design/skill-triggers.ts +51 -0
  156. package/src/policy/design/state.ts +92 -0
  157. package/src/policy/design/template-urls.ts +54 -0
  158. package/src/policy/design/transitions.ts +68 -0
  159. package/src/policy/detect-claude-md-project-type.ts +32 -0
  160. package/src/policy/detect-framework.ts +99 -0
  161. package/src/policy/detect-primitive-checks.ts +100 -0
  162. package/src/policy/detect-primitive-lib.ts +55 -0
  163. package/src/policy/detect-project.ts +99 -0
  164. package/src/policy/edit-outcome.ts +53 -0
  165. package/src/policy/evaluate.ts +81 -0
  166. package/src/policy/expert-agents.ts +60 -0
  167. package/src/policy/file-size-scope.ts +32 -0
  168. package/src/policy/file-size.ts +99 -0
  169. package/src/policy/framework-solid-exclude.ts +52 -0
  170. package/src/policy/framework-solid-extended.ts +93 -0
  171. package/src/policy/framework-solid-gates-systems.ts +99 -0
  172. package/src/policy/framework-solid-gates.ts +98 -0
  173. package/src/policy/framework-solid.ts +88 -0
  174. package/src/policy/gemini-mcp-gate.ts +83 -0
  175. package/src/policy/git-gates.ts +40 -0
  176. package/src/policy/guards/bash-command-anchor.ts +40 -0
  177. package/src/policy/guards/bash-write-patterns.ts +140 -0
  178. package/src/policy/guards/bash-write-redirects.ts +201 -0
  179. package/src/policy/guards/bash-write-safe-paths.ts +62 -0
  180. package/src/policy/guards/bash-write.ts +76 -0
  181. package/src/policy/guards/context.ts +12 -0
  182. package/src/policy/guards/index.ts +61 -0
  183. package/src/policy/guards/install.ts +22 -0
  184. package/src/policy/guards/interface-separation-ext.ts +41 -0
  185. package/src/policy/guards/interface-separation.ts +100 -0
  186. package/src/policy/guards/protected-path.ts +100 -0
  187. package/src/policy/guards/security.ts +97 -0
  188. package/src/policy/index.ts +19 -0
  189. package/src/policy/interfaces/types.ts +46 -0
  190. package/src/policy/js-gate-route.ts +51 -0
  191. package/src/policy/lessons/lesson-gate.ts +0 -0
  192. package/src/policy/lessons/trigger-index.ts +84 -0
  193. package/src/policy/lessons/types.ts +31 -0
  194. package/src/policy/module-layout.ts +31 -0
  195. package/src/policy/nearest-manifest.ts +75 -0
  196. package/src/policy/never-approval.ts +95 -0
  197. package/src/policy/patterns.ts +80 -0
  198. package/src/policy/seo/validate.ts +51 -0
  199. package/src/policy/shadcn-project.ts +22 -0
  200. package/src/policy/shadcn-skill-gate.ts +97 -0
  201. package/src/policy/shell-read-refs.ts +96 -0
  202. package/src/policy/skill-path.ts +46 -0
  203. package/src/policy/skill-patterns/laravel.ts +40 -0
  204. package/src/policy/skill-patterns/nextjs.ts +29 -0
  205. package/src/policy/skill-patterns/react.ts +27 -0
  206. package/src/policy/skill-patterns/shadcn-subskills.ts +33 -0
  207. package/src/policy/skill-patterns/shadcn.ts +66 -0
  208. package/src/policy/skill-patterns/swift.ts +30 -0
  209. package/src/policy/skill-patterns/tailwind.ts +40 -0
  210. package/src/policy/skill-source.ts +52 -0
  211. package/src/policy/skill-trigger-patterns.ts +33 -0
  212. package/src/policy/skill-triggers.ts +108 -0
  213. package/src/policy/tailwind-skill-gate.ts +63 -0
  214. package/src/policy/trivial-edits.ts +52 -0
  215. package/src/policy/verbosity.ts +39 -0
  216. package/src/prompt/index.ts +1 -0
  217. package/src/prompt/types.ts +36 -0
  218. package/src/refs/discover.ts +69 -0
  219. package/src/refs/frontmatter.ts +24 -0
  220. package/src/refs/index.ts +4 -0
  221. package/src/refs/loader.ts +59 -0
  222. package/src/refs/ref-key.ts +41 -0
  223. package/src/refs/router.ts +63 -0
  224. package/src/refs/types.ts +25 -0
  225. package/src/runtime/activity.ts +99 -0
  226. package/src/runtime/apply-patch-gate.ts +41 -0
  227. package/src/runtime/burst-window.ts +21 -0
  228. package/src/runtime/codex-shell-tool.ts +37 -0
  229. package/src/runtime/command-string.ts +37 -0
  230. package/src/runtime/confirm/codex-confirm.ts +126 -0
  231. package/src/runtime/confirm/codex-prompt-origin.ts +16 -0
  232. package/src/runtime/confirm/confirm-code.ts +33 -0
  233. package/src/runtime/confirm/confirm-gate.ts +65 -0
  234. package/src/runtime/confirm/confirm-irreversible.ts +32 -0
  235. package/src/runtime/confirm/confirm-pending.ts +34 -0
  236. package/src/runtime/confirm/confirm-state.ts +56 -0
  237. package/src/runtime/confirm/confirm-subagent.ts +112 -0
  238. package/src/runtime/confirm/confirm-submit.ts +38 -0
  239. package/src/runtime/core-guards-active.ts +84 -0
  240. package/src/runtime/core-guards-wired.ts +47 -0
  241. package/src/runtime/deny-loop-store.ts +94 -0
  242. package/src/runtime/deny-notice.ts +52 -0
  243. package/src/runtime/design-cache-resolve.ts +51 -0
  244. package/src/runtime/design-content-gate.ts +95 -0
  245. package/src/runtime/design-files-gate.ts +114 -0
  246. package/src/runtime/design-helpers.ts +93 -0
  247. package/src/runtime/design-lifecycle.ts +39 -0
  248. package/src/runtime/design-read-credit.ts +28 -0
  249. package/src/runtime/design.ts +116 -0
  250. package/src/runtime/dev-context.ts +43 -0
  251. package/src/runtime/dry-patterns.ts +32 -0
  252. package/src/runtime/dry.ts +96 -0
  253. package/src/runtime/fragment-registry.ts +33 -0
  254. package/src/runtime/framework-skill-gate.ts +79 -0
  255. package/src/runtime/fs-cleanup.ts +71 -0
  256. package/src/runtime/gate-apex.ts +81 -0
  257. package/src/runtime/gate-helpers.ts +52 -0
  258. package/src/runtime/gate-input.ts +26 -0
  259. package/src/runtime/gate.ts +128 -0
  260. package/src/runtime/handle-post.ts +104 -0
  261. package/src/runtime/handle-pre.ts +137 -0
  262. package/src/runtime/handle-scope-async.ts +29 -0
  263. package/src/runtime/handle-types.ts +27 -0
  264. package/src/runtime/handle.ts +116 -0
  265. package/src/runtime/home-state.ts +55 -0
  266. package/src/runtime/index.ts +18 -0
  267. package/src/runtime/inform.ts +28 -0
  268. package/src/runtime/inject-budget-recap.ts +47 -0
  269. package/src/runtime/inject-budget.ts +81 -0
  270. package/src/runtime/inject-context.ts +85 -0
  271. package/src/runtime/inject-dedup-exclusive.ts +98 -0
  272. package/src/runtime/inject-dedup.ts +89 -0
  273. package/src/runtime/is-agent-tool.ts +19 -0
  274. package/src/runtime/lifecycle/agent-files.ts +49 -0
  275. package/src/runtime/lifecycle/agent-memory.ts +82 -0
  276. package/src/runtime/lifecycle/agent-transcript.ts +80 -0
  277. package/src/runtime/lifecycle/aipilot/analytics.ts +99 -0
  278. package/src/runtime/lifecycle/aipilot/apex-task-store.ts +61 -0
  279. package/src/runtime/lifecycle/aipilot/cache-base.ts +48 -0
  280. package/src/runtime/lifecycle/aipilot/cache-doc.ts +100 -0
  281. package/src/runtime/lifecycle/aipilot/cache-lessons.ts +58 -0
  282. package/src/runtime/lifecycle/aipilot/cache-test.ts +70 -0
  283. package/src/runtime/lifecycle/aipilot/curate-lessons.ts +64 -0
  284. package/src/runtime/lifecycle/aipilot/dispatch-aipilot.ts +100 -0
  285. package/src/runtime/lifecycle/aipilot/doc-cache-gate.ts +69 -0
  286. package/src/runtime/lifecycle/aipilot/inject-apex.ts +98 -0
  287. package/src/runtime/lifecycle/aipilot/inject-doc.ts +62 -0
  288. package/src/runtime/lifecycle/aipilot/inject-explore.ts +64 -0
  289. package/src/runtime/lifecycle/aipilot/inject-lessons.ts +61 -0
  290. package/src/runtime/lifecycle/aipilot/inject-test.ts +48 -0
  291. package/src/runtime/lifecycle/aipilot/lesson-archive.ts +60 -0
  292. package/src/runtime/lifecycle/aipilot/lesson-inject.ts +98 -0
  293. package/src/runtime/lifecycle/aipilot/lesson-parse.ts +74 -0
  294. package/src/runtime/lifecycle/aipilot/lessons.ts +72 -0
  295. package/src/runtime/lifecycle/aipilot/promote-global-lessons.ts +92 -0
  296. package/src/runtime/lifecycle/aipilot/solid-compliance.ts +69 -0
  297. package/src/runtime/lifecycle/aipilot/solid-notice.ts +47 -0
  298. package/src/runtime/lifecycle/aipilot/solid-transcript.ts +60 -0
  299. package/src/runtime/lifecycle/aipilot/source-scan.ts +56 -0
  300. package/src/runtime/lifecycle/aipilot/sync-task.ts +75 -0
  301. package/src/runtime/lifecycle/aipilot/transcript.ts +88 -0
  302. package/src/runtime/lifecycle/aipilot/types.ts +72 -0
  303. package/src/runtime/lifecycle/auto-document-reads.ts +73 -0
  304. package/src/runtime/lifecycle/cartographer/detect.ts +57 -0
  305. package/src/runtime/lifecycle/cartographer/ecosystem-map.ts +56 -0
  306. package/src/runtime/lifecycle/cartographer/fs-util.ts +64 -0
  307. package/src/runtime/lifecycle/cartographer/merge.ts +51 -0
  308. package/src/runtime/lifecycle/cartographer/project-map.ts +49 -0
  309. package/src/runtime/lifecycle/cartographer/scan-hooks.ts +48 -0
  310. package/src/runtime/lifecycle/cartographer/scan.ts +81 -0
  311. package/src/runtime/lifecycle/cartographer/session-start.ts +33 -0
  312. package/src/runtime/lifecycle/cartographer/track-enrichment.ts +45 -0
  313. package/src/runtime/lifecycle/cartographer/write-plugin-map.ts +43 -0
  314. package/src/runtime/lifecycle/cartographer/write-tree.ts +44 -0
  315. package/src/runtime/lifecycle/changelog-research.ts +43 -0
  316. package/src/runtime/lifecycle/check-file-size.ts +61 -0
  317. package/src/runtime/lifecycle/codex-resync/fingerprint.ts +63 -0
  318. package/src/runtime/lifecycle/codex-resync/lock.ts +46 -0
  319. package/src/runtime/lifecycle/codex-resync/managed-destination.ts +39 -0
  320. package/src/runtime/lifecycle/codex-resync/materialize-agents.ts +57 -0
  321. package/src/runtime/lifecycle/codex-resync/plugin-files.ts +65 -0
  322. package/src/runtime/lifecycle/codex-resync/plugin-roots.ts +62 -0
  323. package/src/runtime/lifecycle/codex-resync/resync.ts +45 -0
  324. package/src/runtime/lifecycle/codex-resync/symlink-commands.ts +25 -0
  325. package/src/runtime/lifecycle/dispatch.ts +109 -0
  326. package/src/runtime/lifecycle/failure-lesson.ts +51 -0
  327. package/src/runtime/lifecycle/index.ts +34 -0
  328. package/src/runtime/lifecycle/inject-rules.ts +50 -0
  329. package/src/runtime/lifecycle/instructions-loaded.ts +22 -0
  330. package/src/runtime/lifecycle/kimi-rules-native.ts +44 -0
  331. package/src/runtime/lifecycle/lessons/dispatch.ts +88 -0
  332. package/src/runtime/lifecycle/lessons/reminder.ts +89 -0
  333. package/src/runtime/lifecycle/lessons/state.ts +23 -0
  334. package/src/runtime/lifecycle/memory/agent-lesson.ts +35 -0
  335. package/src/runtime/lifecycle/memory/capture-error.ts +41 -0
  336. package/src/runtime/lifecycle/memory/client.ts +65 -0
  337. package/src/runtime/lifecycle/memory/dispatch.ts +42 -0
  338. package/src/runtime/lifecycle/memory/recall.ts +30 -0
  339. package/src/runtime/lifecycle/memory/salience.ts +31 -0
  340. package/src/runtime/lifecycle/memory/state.ts +49 -0
  341. package/src/runtime/lifecycle/memory/track-ops.ts +15 -0
  342. package/src/runtime/lifecycle/post-compact.ts +38 -0
  343. package/src/runtime/lifecycle/post-edit-ts.ts +49 -0
  344. package/src/runtime/lifecycle/post-tracking.ts +40 -0
  345. package/src/runtime/lifecycle/pre-compact.ts +38 -0
  346. package/src/runtime/lifecycle/rules-root.ts +83 -0
  347. package/src/runtime/lifecycle/security/check-skill.ts +63 -0
  348. package/src/runtime/lifecycle/security/scan-patterns.ts +53 -0
  349. package/src/runtime/lifecycle/security/scan.ts +76 -0
  350. package/src/runtime/lifecycle/security/skill-state.ts +47 -0
  351. package/src/runtime/lifecycle/security/track-mcp.ts +29 -0
  352. package/src/runtime/lifecycle/security/track-skill-read.ts +31 -0
  353. package/src/runtime/lifecycle/seo/post-tool-use.ts +45 -0
  354. package/src/runtime/lifecycle/session-end.ts +17 -0
  355. package/src/runtime/lifecycle/session-start.ts +57 -0
  356. package/src/runtime/lifecycle/snapshot/board.ts +27 -0
  357. package/src/runtime/lifecycle/snapshot/format.ts +60 -0
  358. package/src/runtime/lifecycle/snapshot/git.ts +71 -0
  359. package/src/runtime/lifecycle/snapshot/index.ts +52 -0
  360. package/src/runtime/lifecycle/snapshot/version.ts +34 -0
  361. package/src/runtime/lifecycle/solid-detect.ts +56 -0
  362. package/src/runtime/lifecycle/stop-core.ts +35 -0
  363. package/src/runtime/lifecycle/subagent-cache.ts +61 -0
  364. package/src/runtime/lifecycle/task-completed.ts +94 -0
  365. package/src/runtime/lifecycle/teammate-idle-check.ts +63 -0
  366. package/src/runtime/lifecycle/teammate-idle.ts +28 -0
  367. package/src/runtime/lifecycle/tool-failure.ts +24 -0
  368. package/src/runtime/lifecycle/track-changes.ts +61 -0
  369. package/src/runtime/lifecycle/validate-solid.ts +70 -0
  370. package/src/runtime/lifecycle/validate-tailwind.ts +80 -0
  371. package/src/runtime/lifecycle-bridge.ts +74 -0
  372. package/src/runtime/mcp-key.ts +44 -0
  373. package/src/runtime/mcp-tool-name.ts +92 -0
  374. package/src/runtime/mcp.ts +91 -0
  375. package/src/runtime/modular.ts +77 -0
  376. package/src/runtime/normalize.ts +107 -0
  377. package/src/runtime/notices.ts +91 -0
  378. package/src/runtime/notification-sound.ts +49 -0
  379. package/src/runtime/notifications.ts +77 -0
  380. package/src/runtime/paths.ts +59 -0
  381. package/src/runtime/post-fanout.ts +41 -0
  382. package/src/runtime/post-outcome.ts +61 -0
  383. package/src/runtime/pre-allow.ts +97 -0
  384. package/src/runtime/precommit.ts +60 -0
  385. package/src/runtime/prompt-text.ts +38 -0
  386. package/src/runtime/receipt-capture.ts +34 -0
  387. package/src/runtime/record.ts +26 -0
  388. package/src/runtime/respond.ts +92 -0
  389. package/src/runtime/solid-file-size-gate.ts +58 -0
  390. package/src/runtime/solid-pre.ts +53 -0
  391. package/src/runtime/storage.ts +9 -0
  392. package/src/statusline/colors.ts +31 -0
  393. package/src/statusline/constants.ts +17 -0
  394. package/src/statusline/formatters.ts +54 -0
  395. package/src/statusline/index.ts +4 -0
  396. package/src/statusline/progress-bar.ts +46 -0
  397. package/src/tracking/codex-post-failure.ts +73 -0
  398. package/src/tracking/index.ts +2 -0
  399. package/src/tracking/integrity.ts +99 -0
  400. package/src/tracking/one-shot-dedup.ts +34 -0
  401. package/src/tracking/one-shot-failure.ts +42 -0
  402. package/src/tracking/one-shot-store.ts +95 -0
  403. package/src/tracking/one-shot.ts +93 -0
  404. package/src/tracking/receipts.ts +95 -0
  405. package/src/tracking/session-state.ts +94 -0
  406. package/src/tracking/store.ts +96 -0
  407. package/src/tracking/track-compact.ts +100 -0
  408. package/src/tracking/track-diff.ts +50 -0
  409. package/src/tracking/track-journal.ts +97 -0
  410. package/src/tracking/track-lock-sync.ts +83 -0
  411. package/src/tracking/track-lock.ts +67 -0
  412. package/src/util/compact-json.ts +23 -0
  413. package/src/util/index.ts +2 -0
  414. package/src/util/json-io.ts +47 -0
  415. package/src/util/project-root.ts +41 -0
  416. package/src/util/runtime-io.ts +85 -0
  417. package/src/util/semver.ts +30 -0
@@ -0,0 +1,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
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * PreToolUse ALLOW-path response assembly. Reached only after every gate
3
+ * allowed (a deny/ask already returned upstream), so nothing here can block nor
4
+ * override a decision. Combines the Python-parity pass notice (systemMessage)
5
+ * with the single most-specific decision-time lesson (additionalContext), plus
6
+ * a compact "evidence fresh" compliance notice on the first APEX-scoped code
7
+ * Write/Edit that clears the freshness gate.
8
+ */
9
+ import { designPassNotice } from "../policy/design/gates";
10
+ import { resolveCorpusRoot } from "../policy/design/corpus";
11
+ import { lessonFor } from "../policy/lessons/lesson-gate";
12
+ import { lessonsFileFor } from "./lifecycle/lessons/state";
13
+ import { projectRoot } from "../util/project-root";
14
+ import { oncePerWindow } from "./inject-dedup";
15
+ import { defaultStateDir } from "./paths";
16
+ import { isApexScoped } from "./gate-helpers";
17
+ import { REQUIRED_AGENTS, DEFAULT_WINDOW_MS } from "./gate";
18
+ import { agentsFreshInTrack } from "../freshness/agent-evidence-record";
19
+ import { loadTrack } from "../tracking/store";
20
+ import { evidenceFreshNotice } from "./notices";
21
+ import { respond } from "./respond";
22
+ import type { NormalizedEvent } from "./normalize";
23
+ import type { HandleOutcome } from "./handle";
24
+ import type { Prompt } from "../prompt/types";
25
+
26
+ /** Session-track + clock inputs the "evidence fresh" notice needs (absent = feature off, e.g. no caller-provided track file). */
27
+ export interface EvidenceCheck {
28
+ /** Session track file path (same one the gate chain reads/writes). */
29
+ trackFile: string;
30
+ /** APEX freshness window in ms (defaults to {@link DEFAULT_WINDOW_MS}). */
31
+ windowMs?: number;
32
+ /** Event clock. */
33
+ now: number;
34
+ }
35
+
36
+ /**
37
+ * `✓ evidence fresh (explore+research)` for the first APEX-scoped Write/Edit
38
+ * that finds explore-codebase + research-expert evidence still within the
39
+ * freshness window — the user-visible confirmation for a gate that, until now,
40
+ * only ever spoke up when it BLOCKED. Deduped once per freshness window per
41
+ * session (same window the gate itself re-validates on), so it reads as "just
42
+ * confirmed", not a notice on every edit. Returns null for anything but a
43
+ * Write/Edit on an APEX-scoped path, or when evidence isn't fresh.
44
+ */
45
+ async function freshEvidenceNotice(event: NormalizedEvent, evidence: EvidenceCheck, cwd: string): Promise<string | null> {
46
+ if ((event.tool !== "Write" && event.tool !== "Edit") || !isApexScoped(event.filePath)) return null;
47
+ const windowMs = evidence.windowMs ?? DEFAULT_WINDOW_MS;
48
+ const track = await loadTrack(evidence.trackFile);
49
+ if (!agentsFreshInTrack(track, REQUIRED_AGENTS, windowMs, evidence.now)) return null;
50
+ if (!oncePerWindow(`evidence-fresh:${event.sessionId}`, windowMs, { now: evidence.now, dir: defaultStateDir(cwd) })) return null;
51
+ return evidenceFreshNotice();
52
+ }
53
+
54
+ /**
55
+ * Build the native outcome for a PreToolUse call that passed every gate: emit a
56
+ * user-visible pass notice (once per allowed call), the "evidence fresh"
57
+ * compliance notice when applicable, and, when its TRIGGERS match this call,
58
+ * the one cooldown-guarded decision-time lesson. All 3 channels ride a single
59
+ * response (lesson → additionalContext, notices → systemMessage).
60
+ * @param id - Harness id for {@link respond}.
61
+ * @param event - The normalized PreToolUse event.
62
+ * @param payload - The raw hook payload (for `agent_id`).
63
+ * @param mcpDir - MCP state dir backing the pass-notice throttle.
64
+ * @param cwd - Project root (lesson file + notice scope).
65
+ * @param evidence - Session track + clock for the "evidence fresh" notice (omit to disable it).
66
+ * @returns The native hook outcome (empty stdout when nothing to emit).
67
+ */
68
+ export async function allowOutcome(
69
+ id: string,
70
+ event: NormalizedEvent,
71
+ payload: Record<string, unknown>,
72
+ mcpDir: string,
73
+ cwd: string,
74
+ evidence?: EvidenceCheck,
75
+ corpusRoot?: string,
76
+ ): Promise<HandleOutcome> {
77
+ const agentId = typeof payload.agent_id === "string" ? payload.agent_id : "";
78
+ const notice = designPassNotice({
79
+ agentId,
80
+ tool: event.tool, filePath: event.filePath ?? "", content: event.content ?? "",
81
+ url: typeof event.input.url === "string" ? event.input.url : "", phase: "pre",
82
+ corpusMissing: agentId !== "" && (corpusRoot ?? resolveCorpusRoot()) === "",
83
+ }, mcpDir);
84
+ const lesson = lessonFor(event.tool, event.input, { file: lessonsFileFor(projectRoot(cwd)), once: oncePerWindow });
85
+ const evidenceNotice = evidence ? await freshEvidenceNotice(event, evidence, cwd) : null;
86
+ if (lesson) {
87
+ const userMessage = [notice?.userMessage, evidenceNotice].filter(Boolean).join("\n") || undefined;
88
+ const merged = userMessage ? { ...lesson, userMessage } : lesson;
89
+ return { stdout: respond(id, merged, event.eventName ?? "PreToolUse"), exit: 0 };
90
+ }
91
+ if (evidenceNotice) {
92
+ const userMessage = [notice?.userMessage, evidenceNotice].filter(Boolean).join("\n");
93
+ const prompt: Prompt = notice ? { ...notice, userMessage } : { kind: "inform", title: "APEX freshness", reason: "", userMessage };
94
+ return { stdout: respond(id, prompt, event.eventName ?? "PreToolUse"), exit: 0 };
95
+ }
96
+ return { stdout: notice ? respond(id, notice, event.eventName ?? "PreToolUse") : "", exit: 0 };
97
+ }
@@ -0,0 +1,60 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import type { Prompt } from "../prompt/types";
5
+
6
+ const TIMEOUT_MS = 30_000;
7
+ const ESLINT_CONFIGS: readonly string[] = [".eslintrc.json", ".eslintrc.js", "eslint.config.js", "eslint.config.mjs", "eslint.config.ts"];
8
+ const PRETTIER_CONFIGS: readonly string[] = [".prettierrc", ".prettierrc.json", "prettier.config.js"];
9
+
10
+ /** Run a linter; returns its error output, or "" if it passed / spawn-failed / timed out (fail-open). */
11
+ function runLinter(file: string, args: string[], label: string, cwd: string): string {
12
+ try {
13
+ execFileSync(file, args, { cwd, timeout: TIMEOUT_MS, stdio: ["ignore", "pipe", "pipe"] });
14
+ return "";
15
+ } catch (e: unknown) {
16
+ const err = e as { status?: number | null; stdout?: Buffer; stderr?: Buffer };
17
+ if (err.status === undefined || err.status === null) return "";
18
+ const out = (err.stdout?.toString() ?? err.stderr?.toString() ?? "").trim();
19
+ return out ? `[${label}]\n${out}` : "";
20
+ }
21
+ }
22
+
23
+ /** Run the applicable linters in `cwd`, returning a block of errors per failing tool. */
24
+ function collectErrors(cwd: string): string[] {
25
+ const has = (f: string): boolean => existsSync(join(cwd, f));
26
+ const errors: string[] = [];
27
+ if (has("package.json")) {
28
+ if (ESLINT_CONFIGS.some(has)) {
29
+ const m = runLinter("bunx", ["eslint", ".", "--max-warnings", "0"], "ESLint", cwd);
30
+ if (m) errors.push(m);
31
+ }
32
+ if (has("tsconfig.json")) {
33
+ const m = runLinter("bunx", ["tsc", "--noEmit"], "TypeScript", cwd);
34
+ if (m) errors.push(m);
35
+ }
36
+ if (PRETTIER_CONFIGS.some(has)) {
37
+ const m = runLinter("bunx", ["prettier", "--check", "."], "Prettier", cwd);
38
+ if (m) errors.push(m);
39
+ }
40
+ }
41
+ if (has("requirements.txt") || has("pyproject.toml")) {
42
+ const m = runLinter("ruff", ["check", "."], "Ruff", cwd);
43
+ if (m) errors.push(m);
44
+ }
45
+ return errors;
46
+ }
47
+
48
+ /** Block a `git commit` when linters fail (effectful: runs eslint/tsc/prettier/ruff, never auto-fixes). */
49
+ export function preCommitGate(tool: string, command: string | undefined, cwd: string | undefined): Prompt | null {
50
+ if (tool !== "Bash" || !command || !cwd) return null;
51
+ if (!command.startsWith("git") || !command.includes("commit")) return null;
52
+ const errors = collectErrors(cwd);
53
+ if (!errors.length) return null;
54
+ return {
55
+ kind: "block",
56
+ title: "Pre-commit checks failed",
57
+ reason: `COMMIT BLOCKED — fix then retry:\n\n${errors.join("\n\n")}`,
58
+ actions: ["Fix the linter/type errors above", "Re-run the commit"],
59
+ };
60
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Normalizes a hook payload's `prompt` field across harness shapes: a plain
3
+ * string (Claude Code, Codex) or an array of content blocks (Kimi 0.31.1 —
4
+ * captured live: `[{"type":"text","text":"..."}]` on `UserPromptSubmit`).
5
+ * Never throws: this runs inside a hook, where an exception breaks the harness.
6
+ */
7
+
8
+ /** A Kimi-shaped prompt content block: any object carrying a `text` string. */
9
+ interface PromptTextBlock {
10
+ text: string;
11
+ }
12
+
13
+ /** True for an object whose `text` property is a string (the only field this reads). */
14
+ function hasTextField(value: unknown): value is PromptTextBlock {
15
+ return typeof value === "object" && value !== null && typeof (value as { text?: unknown }).text === "string";
16
+ }
17
+
18
+ /**
19
+ * Coerce a hook payload's `prompt` field to plain text.
20
+ *
21
+ * - A string is returned AS-IS (same reference, no trim/normalize) — this is
22
+ * the identity branch that guarantees zero regression on Claude Code/Codex,
23
+ * whose `prompt` is already a string.
24
+ * - An array (Kimi's content-block shape) is flattened: every element with a
25
+ * string `.text` is kept, joined with `"\n"` — one block per line, since
26
+ * downstream consumers ({@link detectCreationIntent}, {@link detectMode})
27
+ * only run `\b`-anchored regexes with no line-start/end assumption, so a
28
+ * newline join is safe and reads like the multi-turn/multi-block source.
29
+ * - Anything else (`undefined`, `null`, a number, a plain object) yields `""`.
30
+ *
31
+ * @param value - The raw `prompt` field from a hook payload (unknown shape).
32
+ * @returns The prompt text, or `""` when it cannot be recovered.
33
+ */
34
+ export function promptText(value: unknown): string {
35
+ if (typeof value === "string") return value;
36
+ if (Array.isArray(value)) return value.filter(hasTextField).map((b) => b.text).join("\n");
37
+ return "";
38
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Bash verification-receipt extraction for PostToolUse: a `tsc`/`bun test`
3
+ * run is parsed (exit code + output) into a signed receipt the TaskCompleted
4
+ * gate later demands. Structured responses ONLY: Kimi Code CLI sends
5
+ * `tool_output` as a truncated STRING (no exit_code, no stdout/stderr
6
+ * channels) — parsing it would forge a success receipt (`exit 0`, empty
7
+ * output) for possibly-failed runs, so a non-object source is skipped.
8
+ */
9
+ import { captureReceipt } from "../tracking/receipts";
10
+
11
+ /**
12
+ * Record a verification receipt for a Bash run when the response is structured.
13
+ * @param file - Session track file.
14
+ * @param tool - The tool name (only "Bash" receipts exist).
15
+ * @param command - The executed command.
16
+ * @param toolResult - `payload.tool_result` (preferred), when present.
17
+ * @param response - `payload.tool_response ?? payload.tool_output` fallback.
18
+ * @param now - Clock.
19
+ */
20
+ export async function captureBashReceipt(
21
+ file: string,
22
+ tool: string,
23
+ command: string | undefined,
24
+ toolResult: unknown,
25
+ response: unknown,
26
+ now: number,
27
+ ): Promise<void> {
28
+ if (tool !== "Bash" || !command) return;
29
+ const r = (toolResult ?? response) as { exit_code?: unknown; stdout?: unknown; stderr?: unknown } | undefined;
30
+ if (!r || typeof r !== "object") return;
31
+ const out = `${typeof r.stdout === "string" ? r.stdout : ""}\n${typeof r.stderr === "string" ? r.stderr : ""}`;
32
+ const exit = Number(r.exit_code ?? 0);
33
+ await captureReceipt(file, command, out, Number.isFinite(exit) ? exit : 0, now);
34
+ }
@@ -0,0 +1,26 @@
1
+ import { dirname } from "node:path";
2
+ import { withTrack } from "../tracking/store";
3
+ import { recordAgent, recordDoc, recordRefRead, type AgentQuality } from "../tracking/session-state";
4
+ import { appendRefRead } from "../freshness/ref-journal";
5
+
6
+ /** A unit of session activity to record (discriminated union on `kind`). */
7
+ export type Activity =
8
+ | { kind: "agent"; name: string; ts: number; quality?: AgentQuality }
9
+ | { kind: "doc"; framework: string; sessionId: string; source: string; ts?: number }
10
+ | { kind: "ref"; path: string; ts?: number };
11
+
12
+ /** Apply an activity to a session's track and persist it (PostToolUse path). */
13
+ export async function recordActivity(file: string, activity: Activity): Promise<void> {
14
+ await withTrack(file, (track) =>
15
+ activity.kind === "agent"
16
+ ? recordAgent(track, activity.name, activity.ts, activity.quality)
17
+ : activity.kind === "doc"
18
+ ? recordDoc(track, activity.framework, activity.sessionId, activity.source, activity.ts)
19
+ : recordRefRead(track, activity.path, activity.ts),
20
+ );
21
+ // Mirror a `.md` ref read into the append-only journal: the racy load→save above
22
+ // loses a lone write under the hook fan-out, and a teammate's read has not yet
23
+ // flushed to the platform transcript at edit time (multi-minute lag > TTL) — the
24
+ // journal is the fresh, race-immune source the gate folds back (see ref-journal.ts).
25
+ if (activity.kind === "ref") appendRefRead(dirname(file), activity.path, activity.ts ?? Date.now());
26
+ }
@@ -0,0 +1,92 @@
1
+ import { formatPrompt, type Prompt } from "../prompt/types";
2
+ import { denyResponse, contextResponse, informResponse } from "../adapters/claude";
3
+ import { toHermesResponse } from "../adapters/hermes";
4
+ import { toKimiResponse } from "../adapters/kimi";
5
+ import { toCursorResponse } from "../adapters/cursor/respond";
6
+
7
+ /**
8
+ * Map a portable {@link Prompt} to a harness's native hook response, honoring
9
+ * all 3 prompt kinds (`block`/`ask`/`inform`) instead of collapsing `ask` and
10
+ * `inform` together. Reuses the Claude adapter's `denyResponse`/
11
+ * `contextResponse`/`informResponse` builders for claude-code/codex (same hook
12
+ * shape) to avoid duplicating the JSON.
13
+ * - claude-code/codex: `block` -> deny, `ask` -> interactive
14
+ * `permissionDecision:"ask"`, `inform` -> non-blocking `additionalContext`.
15
+ * - gemini-cli/cline: their real hook schemas have no interactive "ask"
16
+ * state (deny is the only blocking outcome), so `ask` and `inform` already
17
+ * both resolve to non-blocking context injection — unchanged.
18
+ * - cursor: `ask` degrades to a native `permission:"deny"` because Cursor does
19
+ * not reliably apply `ask` on `preToolUse`; `inform` stays non-blocking.
20
+ * - hermes: delegated to the adapter's `toHermesResponse` — `block` ->
21
+ * `{decision:"block",reason}`; `ask`/`inform` degrade to non-blocking
22
+ * `{context}` (Hermes has no interactive "ask" state).
23
+ * - kimi: delegated to the adapter's `toKimiResponse` — `block`/`ask` ->
24
+ * `hookSpecificOutput.permissionDecision:"deny"` (Kimi Code CLI's hooks
25
+ * contract documents only "deny"; `ask` downgrades with a prefix, mirroring
26
+ * the codex branch above), `inform` -> raw text on stdout (Kimi appends
27
+ * stdout to context on exit 0 — no `hookSpecificOutput` envelope documented
28
+ * for non-blocking context injection).
29
+ *
30
+ * User-visible pass notices (`prompt.userMessage`, Python `hook_output.allow_pass`
31
+ * / `post_pass` parity), by harness:
32
+ * - claude-code/codex: top-level `systemMessage` ("warning message shown to the
33
+ * user"); a pure notice (empty `reason`) emits `{systemMessage}` alone. Codex
34
+ * consumes the Claude hook schema, so it rides the same field.
35
+ * - gemini-cli: common `systemMessage` output field ("displayed immediately to
36
+ * the user in the terminal") — the notice rides there.
37
+ * - cursor: `user_message` is its user-visible channel (see case comment).
38
+ * - cline: NO user-visible channel (`cancel`/`contextModification`/
39
+ * `errorMessage`-on-cancel only) — the notice is dropped silently (a pure
40
+ * notice returns "" = plain allow).
41
+ * @param id - Harness id.
42
+ * @param prompt - The portable prompt to render.
43
+ * @param event - The firing hook event name for the claude-code/codex branch
44
+ * (defaults to `"PreToolUse"`, the only phase every existing caller but
45
+ * `handle-post.ts` renders) — a POST-phase caller MUST pass `"PostToolUse"`
46
+ * (or its real raw event name) so `hookEventName` matches the event that
47
+ * actually fired instead of a hardcoded, wrong `"PreToolUse"`.
48
+ */
49
+ export function respond(id: string, prompt: Prompt, event: string = "PreToolUse"): string {
50
+ const message = formatPrompt(prompt);
51
+ const { kind, userMessage, reason } = prompt;
52
+ switch (id) {
53
+ case "claude-code":
54
+ case "codex":
55
+ if (kind === "block") return denyResponse(event, message);
56
+ if (kind === "inform") {
57
+ return userMessage ? informResponse(event, userMessage, reason ? message : "") : contextResponse(event, message);
58
+ }
59
+ // Codex parses but NEVER honors `permissionDecision: "ask"` (deny-only) — an
60
+ // `ask` would silently fail open. Downgrade it to an explicit deny so the
61
+ // gate still bites; the reason is prefixed so the honest limit is visible.
62
+ if (id === "codex") {
63
+ return denyResponse(event, `[downgraded from ask — Codex has no interactive approval]\n${message}`);
64
+ }
65
+ return JSON.stringify({
66
+ hookSpecificOutput: {
67
+ hookEventName: event,
68
+ permissionDecision: "ask",
69
+ permissionDecisionReason: message,
70
+ },
71
+ });
72
+ case "gemini-cli":
73
+ if (kind === "block") return JSON.stringify({ decision: "deny", reason: message });
74
+ if (userMessage) return JSON.stringify({ ...(reason ? { hookSpecificOutput: { additionalContext: message } } : {}), systemMessage: userMessage });
75
+ return JSON.stringify({ hookSpecificOutput: { additionalContext: message } });
76
+ case "cursor":
77
+ return toCursorResponse(prompt, event === "PreToolUse" ? "preToolUse" : event);
78
+ case "hermes":
79
+ // Single source of truth for the Hermes wire shape lives in the adapter
80
+ // (same JSON.stringify contract as the inline cases above).
81
+ return toHermesResponse(prompt);
82
+ case "kimi":
83
+ // Single source of truth for the Kimi wire shape lives in the adapter.
84
+ return toKimiResponse(prompt);
85
+ case "cline":
86
+ if (kind === "block") return JSON.stringify({ cancel: true, errorMessage: message });
87
+ if (userMessage && !reason) return ""; // pure notice: no user-visible channel — silent allow
88
+ return JSON.stringify({ contextModification: message });
89
+ default:
90
+ return "";
91
+ }
92
+ }