@fusengine/harness 0.1.90 → 0.1.92

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (422) hide show
  1. package/dist/adapters/claude/index.d.mts +1 -1
  2. package/dist/adapters/claude/index.mjs +1 -1
  3. package/dist/adapters/cline/index.mjs +1 -1
  4. package/dist/adapters/codex/index.d.mts +2 -2
  5. package/dist/adapters/codex/index.mjs +3 -2
  6. package/dist/adapters/cursor/index.d.mts +15 -28
  7. package/dist/adapters/cursor/index.mjs +35 -46
  8. package/dist/adapters/gemini/index.mjs +1 -1
  9. package/dist/adapters/hermes/index.d.mts +1 -1
  10. package/dist/adapters/hermes/index.mjs +1 -1
  11. package/dist/adapters/kimi/index.d.mts +1 -1
  12. package/dist/adapters/kimi/index.mjs +1 -1
  13. package/dist/{claude-5bC8TDAz.mjs → claude-Ckv2_TgP.mjs} +3 -43
  14. package/dist/cli/bin.mjs +429 -16
  15. package/dist/cli/index.mjs +1 -1
  16. package/dist/command-string-CALMTnwN.mjs +42 -0
  17. package/dist/{evaluate-C1VSI5pb.mjs → evaluate-CMiIqHeH.mjs} +261 -13
  18. package/dist/{handle-CBaOcFZq.mjs → handle-BF1dZFjY.mjs} +1194 -144
  19. package/dist/{hermes-kP6NUQlG.mjs → hermes-B9-p_3IF.mjs} +3 -2
  20. package/dist/{index-DBgnIZn9.d.mts → index-B8PG82su.d.mts} +1 -1
  21. package/dist/{index-D4C2-IIc.d.mts → index-SmKYkk2N.d.mts} +1 -1
  22. package/dist/index.d.mts +2 -2
  23. package/dist/index.mjs +2 -2
  24. package/dist/init/index.d.mts +1 -1
  25. package/dist/init/index.mjs +1 -1
  26. package/dist/{kimi-D6X7b8wF.mjs → kimi-G2wcSh5-.mjs} +3 -2
  27. package/dist/normalize-Dy8g9Ybl.mjs +397 -0
  28. package/dist/policy/index.d.mts +1 -1
  29. package/dist/policy/index.mjs +2 -2
  30. package/dist/prompt/index.d.mts +1 -1
  31. package/dist/{run-CdIlUtjv.mjs → run-DkrzC0gb.mjs} +1 -1
  32. package/dist/{run-TAXNRSpD.mjs → run-GHAMqcOn.mjs} +26 -3
  33. package/dist/runtime/index.d.mts +37 -16
  34. package/dist/runtime/index.mjs +2 -2
  35. package/dist/{types-C0-igRKr.d.mts → types-Bh0jEF3_.d.mts} +2 -0
  36. package/dist/{validate-DZFtAbLP.mjs → validate-KjZ1X9tH.mjs} +1 -1
  37. package/package.json +3 -2
  38. package/src/adapters/claude/index.ts +92 -0
  39. package/src/adapters/cline/index.ts +36 -0
  40. package/src/adapters/codex/apply-patch.ts +94 -0
  41. package/src/adapters/codex/index.ts +84 -0
  42. package/src/adapters/codex/permission-mode.ts +16 -0
  43. package/src/adapters/cursor/context-budget.ts +144 -0
  44. package/src/adapters/cursor/context-limit.ts +115 -0
  45. package/src/adapters/cursor/context.ts +65 -0
  46. package/src/adapters/cursor/events.ts +38 -0
  47. package/src/adapters/cursor/index.ts +43 -0
  48. package/src/adapters/cursor/interfaces/context-budget.ts +25 -0
  49. package/src/adapters/cursor/interfaces/types.ts +88 -0
  50. package/src/adapters/cursor/native-response.ts +39 -0
  51. package/src/adapters/cursor/native-schemas.ts +161 -0
  52. package/src/adapters/cursor/normalize.ts +188 -0
  53. package/src/adapters/cursor/plugin-root.ts +103 -0
  54. package/src/adapters/cursor/respond.ts +170 -0
  55. package/src/adapters/gemini/index.ts +36 -0
  56. package/src/adapters/hermes/index.ts +55 -0
  57. package/src/adapters/hermes/interfaces/types.ts +26 -0
  58. package/src/adapters/kimi/index.ts +96 -0
  59. package/src/adapters/kimi/interfaces/types.ts +55 -0
  60. package/src/cache/compact.ts +55 -0
  61. package/src/cache/index.ts +5 -0
  62. package/src/cache/io.ts +38 -0
  63. package/src/cache/mcp-response.ts +39 -0
  64. package/src/cache/mcp-store.ts +70 -0
  65. package/src/cache/store.ts +93 -0
  66. package/src/changelog/fetch.ts +79 -0
  67. package/src/cli/bin.ts +100 -0
  68. package/src/cli/cursor-event-scanner.ts +175 -0
  69. package/src/cli/cursor-stdin-reader.ts +65 -0
  70. package/src/cli/doctor.ts +96 -0
  71. package/src/cli/hook-io.ts +130 -0
  72. package/src/cli/hook-sound.ts +49 -0
  73. package/src/cli/index.ts +1 -0
  74. package/src/cli/json-primitive-scanner.ts +83 -0
  75. package/src/cli/run.ts +36 -0
  76. package/src/cli/scope.ts +31 -0
  77. package/src/codex-rules/build.ts +63 -0
  78. package/src/codex-rules/index.ts +8 -0
  79. package/src/codex-rules/mapping.ts +42 -0
  80. package/src/codex-rules/rules/absolute-paths.ts +52 -0
  81. package/src/codex-rules/rules/ask.ts +44 -0
  82. package/src/codex-rules/rules/chmod-ask.ts +57 -0
  83. package/src/codex-rules/rules/dd.ts +23 -0
  84. package/src/codex-rules/rules/diskutil.ts +33 -0
  85. package/src/codex-rules/rules/mkfs.ts +42 -0
  86. package/src/codex-rules/rules/privilege.ts +49 -0
  87. package/src/codex-rules/rules/remote-fetch.ts +31 -0
  88. package/src/codex-rules/rules/rm-variants.ts +58 -0
  89. package/src/codex-rules/rules/single-commands.ts +57 -0
  90. package/src/codex-rules/skip-list.ts +25 -0
  91. package/src/codex-rules/starlark.ts +46 -0
  92. package/src/codex-rules/types.ts +36 -0
  93. package/src/config/dotenv.ts +55 -0
  94. package/src/config/env.ts +10 -0
  95. package/src/config/home-dir.ts +36 -0
  96. package/src/config/index.ts +5 -0
  97. package/src/config/layout.ts +52 -0
  98. package/src/config/limits.ts +56 -0
  99. package/src/config/ttl.ts +30 -0
  100. package/src/detect/harness.ts +73 -0
  101. package/src/detect/index.ts +1 -0
  102. package/src/detect/interfaces/types.ts +18 -0
  103. package/src/freshness/agent-evidence-record.ts +95 -0
  104. package/src/freshness/agent-evidence.ts +95 -0
  105. package/src/freshness/codex-spawn-evidence.ts +91 -0
  106. package/src/freshness/doc-helpers.ts +90 -0
  107. package/src/freshness/evidence-harvest-io.ts +72 -0
  108. package/src/freshness/evidence-harvest.ts +61 -0
  109. package/src/freshness/explore-tools.ts +95 -0
  110. package/src/freshness/index.ts +2 -0
  111. package/src/freshness/query-framework.ts +73 -0
  112. package/src/freshness/ref-evidence.ts +49 -0
  113. package/src/freshness/ref-journal.ts +98 -0
  114. package/src/freshness/trivial-edit-counter.ts +30 -0
  115. package/src/index.ts +11 -0
  116. package/src/init/index.ts +2 -0
  117. package/src/init/run.ts +32 -0
  118. package/src/init/templates.ts +75 -0
  119. package/src/memory/gitignore.ts +14 -0
  120. package/src/memory/index.ts +3 -0
  121. package/src/memory/registry.ts +39 -0
  122. package/src/memory/session-roots.ts +99 -0
  123. package/src/memory/state.ts +58 -0
  124. package/src/policy/apex-authorization.ts +100 -0
  125. package/src/policy/apex-gates.ts +91 -0
  126. package/src/policy/apex-target-steps.ts +73 -0
  127. package/src/policy/apex-target.ts +49 -0
  128. package/src/policy/apex-task-context.ts +81 -0
  129. package/src/policy/apex.ts +94 -0
  130. package/src/policy/cartographer/build-tree.ts +69 -0
  131. package/src/policy/cartographer/describe.ts +53 -0
  132. package/src/policy/cartographer/entry.ts +38 -0
  133. package/src/policy/cartographer/frontmatter.ts +48 -0
  134. package/src/policy/cartographer/indicators.ts +34 -0
  135. package/src/policy/claude-md-context.ts +66 -0
  136. package/src/policy/conventions/index.ts +11 -0
  137. package/src/policy/conventions/interfaces.ts +71 -0
  138. package/src/policy/conventions/langs.ts +53 -0
  139. package/src/policy/conventions/query.ts +49 -0
  140. package/src/policy/conventions/react-hooks.ts +18 -0
  141. package/src/policy/conventions/store-or-query.ts +21 -0
  142. package/src/policy/conventions/stores.ts +56 -0
  143. package/src/policy/conventions/strip.ts +87 -0
  144. package/src/policy/conventions/verdict.ts +38 -0
  145. package/src/policy/creation-intent.ts +11 -0
  146. package/src/policy/deny-loop.ts +88 -0
  147. package/src/policy/design/allowed-write.ts +28 -0
  148. package/src/policy/design/content-checks.ts +45 -0
  149. package/src/policy/design/corpus-fs.ts +21 -0
  150. package/src/policy/design/corpus-resolve-cache.ts +49 -0
  151. package/src/policy/design/corpus-resolve.ts +99 -0
  152. package/src/policy/design/corpus.ts +92 -0
  153. package/src/policy/design/design-system-rules.ts +45 -0
  154. package/src/policy/design/flag.ts +30 -0
  155. package/src/policy/design/gates-pipeline.ts +105 -0
  156. package/src/policy/design/gates.ts +87 -0
  157. package/src/policy/design/screenshot-tools.ts +24 -0
  158. package/src/policy/design/skill-evidence.ts +34 -0
  159. package/src/policy/design/skill-gate.ts +81 -0
  160. package/src/policy/design/skill-triggers.ts +51 -0
  161. package/src/policy/design/state.ts +92 -0
  162. package/src/policy/design/template-urls.ts +54 -0
  163. package/src/policy/design/transitions.ts +68 -0
  164. package/src/policy/detect-claude-md-project-type.ts +32 -0
  165. package/src/policy/detect-framework.ts +99 -0
  166. package/src/policy/detect-primitive-checks.ts +100 -0
  167. package/src/policy/detect-primitive-lib.ts +55 -0
  168. package/src/policy/detect-project.ts +99 -0
  169. package/src/policy/edit-outcome.ts +53 -0
  170. package/src/policy/evaluate.ts +81 -0
  171. package/src/policy/expert-agents.ts +60 -0
  172. package/src/policy/file-size-scope.ts +32 -0
  173. package/src/policy/file-size.ts +99 -0
  174. package/src/policy/framework-solid-exclude.ts +52 -0
  175. package/src/policy/framework-solid-extended.ts +93 -0
  176. package/src/policy/framework-solid-gates-systems.ts +99 -0
  177. package/src/policy/framework-solid-gates.ts +98 -0
  178. package/src/policy/framework-solid.ts +88 -0
  179. package/src/policy/gemini-mcp-gate.ts +83 -0
  180. package/src/policy/git-gates.ts +40 -0
  181. package/src/policy/guards/bash-command-anchor.ts +40 -0
  182. package/src/policy/guards/bash-write-patterns.ts +140 -0
  183. package/src/policy/guards/bash-write-redirects.ts +201 -0
  184. package/src/policy/guards/bash-write-safe-paths.ts +62 -0
  185. package/src/policy/guards/bash-write.ts +76 -0
  186. package/src/policy/guards/context.ts +12 -0
  187. package/src/policy/guards/index.ts +61 -0
  188. package/src/policy/guards/install.ts +22 -0
  189. package/src/policy/guards/interface-separation-ext.ts +41 -0
  190. package/src/policy/guards/interface-separation.ts +100 -0
  191. package/src/policy/guards/protected-path.ts +100 -0
  192. package/src/policy/guards/security.ts +97 -0
  193. package/src/policy/index.ts +19 -0
  194. package/src/policy/interfaces/types.ts +46 -0
  195. package/src/policy/js-gate-route.ts +51 -0
  196. package/src/policy/lessons/lesson-gate.ts +0 -0
  197. package/src/policy/lessons/trigger-index.ts +84 -0
  198. package/src/policy/lessons/types.ts +31 -0
  199. package/src/policy/module-layout.ts +31 -0
  200. package/src/policy/nearest-manifest.ts +75 -0
  201. package/src/policy/never-approval.ts +95 -0
  202. package/src/policy/patterns.ts +80 -0
  203. package/src/policy/seo/validate.ts +51 -0
  204. package/src/policy/shadcn-project.ts +22 -0
  205. package/src/policy/shadcn-skill-gate.ts +97 -0
  206. package/src/policy/shell-read-refs.ts +96 -0
  207. package/src/policy/skill-path.ts +46 -0
  208. package/src/policy/skill-patterns/laravel.ts +40 -0
  209. package/src/policy/skill-patterns/nextjs.ts +29 -0
  210. package/src/policy/skill-patterns/react.ts +27 -0
  211. package/src/policy/skill-patterns/shadcn-subskills.ts +33 -0
  212. package/src/policy/skill-patterns/shadcn.ts +66 -0
  213. package/src/policy/skill-patterns/swift.ts +30 -0
  214. package/src/policy/skill-patterns/tailwind.ts +40 -0
  215. package/src/policy/skill-source.ts +52 -0
  216. package/src/policy/skill-trigger-patterns.ts +33 -0
  217. package/src/policy/skill-triggers.ts +108 -0
  218. package/src/policy/tailwind-skill-gate.ts +63 -0
  219. package/src/policy/trivial-edits.ts +52 -0
  220. package/src/policy/verbosity.ts +39 -0
  221. package/src/prompt/index.ts +1 -0
  222. package/src/prompt/types.ts +36 -0
  223. package/src/refs/discover.ts +69 -0
  224. package/src/refs/frontmatter.ts +24 -0
  225. package/src/refs/index.ts +4 -0
  226. package/src/refs/loader.ts +59 -0
  227. package/src/refs/ref-key.ts +41 -0
  228. package/src/refs/router.ts +63 -0
  229. package/src/refs/types.ts +25 -0
  230. package/src/runtime/activity.ts +99 -0
  231. package/src/runtime/apply-patch-gate.ts +41 -0
  232. package/src/runtime/burst-window.ts +21 -0
  233. package/src/runtime/codex-shell-tool.ts +37 -0
  234. package/src/runtime/command-string.ts +37 -0
  235. package/src/runtime/confirm/codex-confirm.ts +126 -0
  236. package/src/runtime/confirm/codex-prompt-origin.ts +16 -0
  237. package/src/runtime/confirm/confirm-code.ts +33 -0
  238. package/src/runtime/confirm/confirm-gate.ts +65 -0
  239. package/src/runtime/confirm/confirm-irreversible.ts +32 -0
  240. package/src/runtime/confirm/confirm-pending.ts +34 -0
  241. package/src/runtime/confirm/confirm-state.ts +56 -0
  242. package/src/runtime/confirm/confirm-subagent.ts +112 -0
  243. package/src/runtime/confirm/confirm-submit.ts +38 -0
  244. package/src/runtime/core-guards-active.ts +84 -0
  245. package/src/runtime/core-guards-wired.ts +47 -0
  246. package/src/runtime/deny-loop-store.ts +94 -0
  247. package/src/runtime/deny-notice.ts +52 -0
  248. package/src/runtime/design-cache-resolve.ts +51 -0
  249. package/src/runtime/design-content-gate.ts +95 -0
  250. package/src/runtime/design-files-gate.ts +114 -0
  251. package/src/runtime/design-helpers.ts +93 -0
  252. package/src/runtime/design-lifecycle.ts +39 -0
  253. package/src/runtime/design-read-credit.ts +28 -0
  254. package/src/runtime/design.ts +116 -0
  255. package/src/runtime/dev-context.ts +43 -0
  256. package/src/runtime/dry-patterns.ts +32 -0
  257. package/src/runtime/dry.ts +96 -0
  258. package/src/runtime/fragment-registry.ts +33 -0
  259. package/src/runtime/framework-skill-gate.ts +79 -0
  260. package/src/runtime/fs-cleanup.ts +71 -0
  261. package/src/runtime/gate-apex.ts +81 -0
  262. package/src/runtime/gate-helpers.ts +52 -0
  263. package/src/runtime/gate-input.ts +26 -0
  264. package/src/runtime/gate.ts +128 -0
  265. package/src/runtime/handle-post.ts +104 -0
  266. package/src/runtime/handle-pre.ts +137 -0
  267. package/src/runtime/handle-scope-async.ts +29 -0
  268. package/src/runtime/handle-types.ts +27 -0
  269. package/src/runtime/handle.ts +197 -0
  270. package/src/runtime/home-state.ts +55 -0
  271. package/src/runtime/index.ts +18 -0
  272. package/src/runtime/inform.ts +28 -0
  273. package/src/runtime/inject-budget-recap.ts +47 -0
  274. package/src/runtime/inject-budget.ts +81 -0
  275. package/src/runtime/inject-context.ts +85 -0
  276. package/src/runtime/inject-dedup-exclusive.ts +98 -0
  277. package/src/runtime/inject-dedup.ts +89 -0
  278. package/src/runtime/is-agent-tool.ts +19 -0
  279. package/src/runtime/lifecycle/agent-files.ts +49 -0
  280. package/src/runtime/lifecycle/agent-memory.ts +82 -0
  281. package/src/runtime/lifecycle/agent-transcript.ts +80 -0
  282. package/src/runtime/lifecycle/aipilot/analytics.ts +99 -0
  283. package/src/runtime/lifecycle/aipilot/apex-task-store.ts +61 -0
  284. package/src/runtime/lifecycle/aipilot/cache-base.ts +48 -0
  285. package/src/runtime/lifecycle/aipilot/cache-doc.ts +100 -0
  286. package/src/runtime/lifecycle/aipilot/cache-lessons.ts +58 -0
  287. package/src/runtime/lifecycle/aipilot/cache-test.ts +70 -0
  288. package/src/runtime/lifecycle/aipilot/curate-lessons.ts +64 -0
  289. package/src/runtime/lifecycle/aipilot/dispatch-aipilot.ts +106 -0
  290. package/src/runtime/lifecycle/aipilot/doc-cache-gate.ts +69 -0
  291. package/src/runtime/lifecycle/aipilot/inject-apex.ts +98 -0
  292. package/src/runtime/lifecycle/aipilot/inject-doc.ts +62 -0
  293. package/src/runtime/lifecycle/aipilot/inject-explore.ts +64 -0
  294. package/src/runtime/lifecycle/aipilot/inject-lessons.ts +61 -0
  295. package/src/runtime/lifecycle/aipilot/inject-test.ts +48 -0
  296. package/src/runtime/lifecycle/aipilot/lesson-archive.ts +60 -0
  297. package/src/runtime/lifecycle/aipilot/lesson-inject.ts +98 -0
  298. package/src/runtime/lifecycle/aipilot/lesson-parse.ts +74 -0
  299. package/src/runtime/lifecycle/aipilot/lessons.ts +72 -0
  300. package/src/runtime/lifecycle/aipilot/promote-global-lessons.ts +92 -0
  301. package/src/runtime/lifecycle/aipilot/solid-compliance.ts +69 -0
  302. package/src/runtime/lifecycle/aipilot/solid-notice.ts +47 -0
  303. package/src/runtime/lifecycle/aipilot/solid-transcript.ts +60 -0
  304. package/src/runtime/lifecycle/aipilot/source-scan.ts +56 -0
  305. package/src/runtime/lifecycle/aipilot/sync-task.ts +75 -0
  306. package/src/runtime/lifecycle/aipilot/transcript.ts +88 -0
  307. package/src/runtime/lifecycle/aipilot/types.ts +72 -0
  308. package/src/runtime/lifecycle/auto-document-reads.ts +73 -0
  309. package/src/runtime/lifecycle/cartographer/detect.ts +57 -0
  310. package/src/runtime/lifecycle/cartographer/ecosystem-map.ts +56 -0
  311. package/src/runtime/lifecycle/cartographer/fs-util.ts +64 -0
  312. package/src/runtime/lifecycle/cartographer/merge.ts +51 -0
  313. package/src/runtime/lifecycle/cartographer/project-map.ts +49 -0
  314. package/src/runtime/lifecycle/cartographer/scan-hooks.ts +48 -0
  315. package/src/runtime/lifecycle/cartographer/scan.ts +81 -0
  316. package/src/runtime/lifecycle/cartographer/session-start.ts +33 -0
  317. package/src/runtime/lifecycle/cartographer/track-enrichment.ts +45 -0
  318. package/src/runtime/lifecycle/cartographer/write-plugin-map.ts +43 -0
  319. package/src/runtime/lifecycle/cartographer/write-tree.ts +44 -0
  320. package/src/runtime/lifecycle/changelog-research.ts +43 -0
  321. package/src/runtime/lifecycle/check-file-size.ts +61 -0
  322. package/src/runtime/lifecycle/codex-resync/fingerprint.ts +63 -0
  323. package/src/runtime/lifecycle/codex-resync/lock.ts +46 -0
  324. package/src/runtime/lifecycle/codex-resync/managed-destination.ts +39 -0
  325. package/src/runtime/lifecycle/codex-resync/materialize-agents.ts +57 -0
  326. package/src/runtime/lifecycle/codex-resync/plugin-files.ts +65 -0
  327. package/src/runtime/lifecycle/codex-resync/plugin-roots.ts +62 -0
  328. package/src/runtime/lifecycle/codex-resync/resync.ts +45 -0
  329. package/src/runtime/lifecycle/codex-resync/symlink-commands.ts +25 -0
  330. package/src/runtime/lifecycle/dispatch.ts +109 -0
  331. package/src/runtime/lifecycle/failure-lesson.ts +55 -0
  332. package/src/runtime/lifecycle/index.ts +34 -0
  333. package/src/runtime/lifecycle/inject-rules.ts +50 -0
  334. package/src/runtime/lifecycle/instructions-loaded.ts +22 -0
  335. package/src/runtime/lifecycle/kimi-rules-native.ts +44 -0
  336. package/src/runtime/lifecycle/lessons/dispatch.ts +88 -0
  337. package/src/runtime/lifecycle/lessons/reminder.ts +89 -0
  338. package/src/runtime/lifecycle/lessons/state.ts +23 -0
  339. package/src/runtime/lifecycle/memory/agent-lesson.ts +35 -0
  340. package/src/runtime/lifecycle/memory/capture-error.ts +41 -0
  341. package/src/runtime/lifecycle/memory/client.ts +65 -0
  342. package/src/runtime/lifecycle/memory/dispatch.ts +42 -0
  343. package/src/runtime/lifecycle/memory/recall.ts +30 -0
  344. package/src/runtime/lifecycle/memory/salience.ts +31 -0
  345. package/src/runtime/lifecycle/memory/state.ts +49 -0
  346. package/src/runtime/lifecycle/memory/track-ops.ts +15 -0
  347. package/src/runtime/lifecycle/post-compact.ts +38 -0
  348. package/src/runtime/lifecycle/post-edit-ts.ts +49 -0
  349. package/src/runtime/lifecycle/post-tracking.ts +40 -0
  350. package/src/runtime/lifecycle/pre-compact.ts +38 -0
  351. package/src/runtime/lifecycle/rules-root.ts +99 -0
  352. package/src/runtime/lifecycle/security/check-skill.ts +63 -0
  353. package/src/runtime/lifecycle/security/scan-patterns.ts +53 -0
  354. package/src/runtime/lifecycle/security/scan.ts +76 -0
  355. package/src/runtime/lifecycle/security/skill-state.ts +47 -0
  356. package/src/runtime/lifecycle/security/track-mcp.ts +29 -0
  357. package/src/runtime/lifecycle/security/track-skill-read.ts +31 -0
  358. package/src/runtime/lifecycle/seo/post-tool-use.ts +45 -0
  359. package/src/runtime/lifecycle/session-end.ts +17 -0
  360. package/src/runtime/lifecycle/session-start.ts +57 -0
  361. package/src/runtime/lifecycle/snapshot/board.ts +27 -0
  362. package/src/runtime/lifecycle/snapshot/format.ts +60 -0
  363. package/src/runtime/lifecycle/snapshot/git.ts +71 -0
  364. package/src/runtime/lifecycle/snapshot/index.ts +52 -0
  365. package/src/runtime/lifecycle/snapshot/version.ts +34 -0
  366. package/src/runtime/lifecycle/solid-detect.ts +56 -0
  367. package/src/runtime/lifecycle/stop-core.ts +35 -0
  368. package/src/runtime/lifecycle/subagent-cache.ts +61 -0
  369. package/src/runtime/lifecycle/task-completed.ts +94 -0
  370. package/src/runtime/lifecycle/teammate-idle-check.ts +63 -0
  371. package/src/runtime/lifecycle/teammate-idle.ts +28 -0
  372. package/src/runtime/lifecycle/tool-failure.ts +24 -0
  373. package/src/runtime/lifecycle/track-changes.ts +61 -0
  374. package/src/runtime/lifecycle/validate-solid.ts +70 -0
  375. package/src/runtime/lifecycle/validate-tailwind.ts +80 -0
  376. package/src/runtime/lifecycle-bridge.ts +74 -0
  377. package/src/runtime/mcp-key.ts +44 -0
  378. package/src/runtime/mcp-tool-name.ts +92 -0
  379. package/src/runtime/mcp.ts +91 -0
  380. package/src/runtime/modular.ts +77 -0
  381. package/src/runtime/normalize.ts +107 -0
  382. package/src/runtime/notices.ts +91 -0
  383. package/src/runtime/notification-sound.ts +49 -0
  384. package/src/runtime/notifications.ts +77 -0
  385. package/src/runtime/paths.ts +59 -0
  386. package/src/runtime/post-fanout.ts +41 -0
  387. package/src/runtime/post-outcome.ts +61 -0
  388. package/src/runtime/pre-allow.ts +97 -0
  389. package/src/runtime/precommit.ts +60 -0
  390. package/src/runtime/prompt-text.ts +38 -0
  391. package/src/runtime/receipt-capture.ts +34 -0
  392. package/src/runtime/record.ts +26 -0
  393. package/src/runtime/respond.ts +92 -0
  394. package/src/runtime/solid-file-size-gate.ts +58 -0
  395. package/src/runtime/solid-pre.ts +53 -0
  396. package/src/runtime/storage.ts +9 -0
  397. package/src/statusline/colors.ts +31 -0
  398. package/src/statusline/constants.ts +17 -0
  399. package/src/statusline/formatters.ts +54 -0
  400. package/src/statusline/index.ts +4 -0
  401. package/src/statusline/progress-bar.ts +46 -0
  402. package/src/tracking/codex-post-failure.ts +73 -0
  403. package/src/tracking/index.ts +2 -0
  404. package/src/tracking/integrity.ts +99 -0
  405. package/src/tracking/one-shot-dedup.ts +34 -0
  406. package/src/tracking/one-shot-failure.ts +42 -0
  407. package/src/tracking/one-shot-store.ts +95 -0
  408. package/src/tracking/one-shot.ts +93 -0
  409. package/src/tracking/receipts.ts +95 -0
  410. package/src/tracking/session-state.ts +94 -0
  411. package/src/tracking/store.ts +96 -0
  412. package/src/tracking/track-compact.ts +100 -0
  413. package/src/tracking/track-diff.ts +50 -0
  414. package/src/tracking/track-journal.ts +97 -0
  415. package/src/tracking/track-lock-sync.ts +83 -0
  416. package/src/tracking/track-lock.ts +67 -0
  417. package/src/util/compact-json.ts +23 -0
  418. package/src/util/index.ts +2 -0
  419. package/src/util/json-io.ts +47 -0
  420. package/src/util/project-root.ts +41 -0
  421. package/src/util/runtime-io.ts +85 -0
  422. package/src/util/semver.ts +30 -0
@@ -0,0 +1,45 @@
1
+ /**
2
+ * SEO PostToolUse handler (fs effects). Ports `seo/hooks/validate-seo.ts`: on an
3
+ * edited HTML-like file under a `.fuse-seo` marker, deny when SEO elements are
4
+ * missing. Opt-in only — silent when no marker / non-HTML / file unreadable.
5
+ */
6
+ import { readFileSync } from "node:fs";
7
+ import { dirname } from "node:path";
8
+ import { isHtmlLike, missingSeoElements } from "../../../policy/seo/validate";
9
+ import { blockResponse } from "../../../adapters/claude";
10
+ import { walkUpFor } from "../../../util/project-root";
11
+
12
+ /**
13
+ * Validate the edited file's SEO completeness. Returns a block message (for a
14
+ * top-level `decision: block` response) when HTML-like, opted-in, and missing
15
+ * elements; otherwise `null` (allow).
16
+ * @param payload - The raw PostToolUse payload.
17
+ * @returns The block reason string, or `null` to allow.
18
+ */
19
+ export function seoPostToolUse(payload: Record<string, unknown>): string | null {
20
+ const input = payload.tool_input as { file_path?: string } | undefined;
21
+ const path = input?.file_path;
22
+ if (!path || !isHtmlLike(path)) return null;
23
+ const cwd = typeof payload.cwd === "string" ? payload.cwd : dirname(path);
24
+ if (!walkUpFor(cwd, ".fuse-seo")) return null;
25
+ try {
26
+ const missing = missingSeoElements(readFileSync(path, "utf-8"));
27
+ if (missing.length === 0) return null;
28
+ return `fuse-seo: missing SEO elements in ${path}:\n - ${missing.join("\n - ")}`;
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+
34
+ /**
35
+ * SEO PostToolUse as a ready native response: a top-level `decision: block`
36
+ * string when the edited file is missing SEO elements, else `null` (allow).
37
+ * PostToolUse ignores `permissionDecision` (PreToolUse-only), so this must use
38
+ * the `decision`/`reason` keys to actually feed the failure back to Claude.
39
+ * @param payload - The raw PostToolUse payload.
40
+ * @returns The block response string, or `null` to allow.
41
+ */
42
+ export function seoPostToolUseResponse(payload: Record<string, unknown>): string | null {
43
+ const block = seoPostToolUse(payload);
44
+ return block ? blockResponse(block) : null;
45
+ }
@@ -0,0 +1,17 @@
1
+ import { join } from "node:path";
2
+ import { homedir } from "node:os";
3
+ import { fusengineCache } from "../home-state";
4
+ import { removeOldFiles } from "../fs-cleanup";
5
+
6
+ /**
7
+ * Handle SessionEnd: remove stale `*.tmp` (>1h) under `session-tmp/` and stale
8
+ * legacy `claude_solid_reads_*` / `claude_session_changes_*` files (>2h) under
9
+ * `cache`. Ports `session-end/cleanup-session.py`. No stdout.
10
+ * @param home - Home dir (defaults to `~`).
11
+ * @param now - Clock (defaults to `Date.now()`).
12
+ */
13
+ export function cleanupSession(home: string = homedir(), now: number = Date.now()): void {
14
+ const base = fusengineCache(home);
15
+ removeOldFiles(join(base, "session-tmp"), (n) => n.endsWith(".tmp"), 3600, now);
16
+ removeOldFiles(base, (n) => n.startsWith("claude_solid_reads_") || n.startsWith("claude_session_changes_"), 7200, now);
17
+ }
@@ -0,0 +1,57 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { attachSystemMessage, contextResponse } from "../../adapters/claude";
5
+ import { claudeHome, fusengineCache, sessionsDir } from "../home-state";
6
+ import { devContext } from "../dev-context";
7
+ import { pruneEmptyDirs, purgeTtlTree, removeOldFiles, trimLogFile } from "../fs-cleanup";
8
+ import { apexDocName, harnessHomeSegment } from "../../policy/apex-target";
9
+
10
+ /** TTLs (seconds) for purgeable cache subtrees (cleanup-old-caches.py). */
11
+ const PURGEABLE: Record<string, number> = { sessions: 48 * 3600, webfetch: 24 * 3600, doc: 48 * 3600, explore: 48 * 3600 };
12
+
13
+ /**
14
+ * Read the target's root instructions doc (`~/.claude/CLAUDE.md`,
15
+ * `~/.codex/AGENTS.md`, `~/.kimi-code/AGENTS.md`, ...), or "" when
16
+ * missing/unreadable.
17
+ * @param home - Home dir.
18
+ * @param id - Harness target id (defaults to "claude-code" — zero-regression default).
19
+ */
20
+ function claudeMd(home: string, id: string = "claude-code"): string {
21
+ const path = join(home, harnessHomeSegment(id), apexDocName(id));
22
+ try {
23
+ return existsSync(path) ? readFileSync(path, "utf-8") : "";
24
+ } catch {
25
+ return "";
26
+ }
27
+ }
28
+
29
+ /** Run the legacy SessionStart cleanups (stale states, caches, log trim). */
30
+ export function runSessionStartCleanups(home: string = homedir(), now: number = Date.now()): void {
31
+ const base = fusengineCache(home);
32
+ removeOldFiles(sessionsDir(home), (n) => n.startsWith("session-") && n.endsWith(".json"), 86400, now);
33
+ const user = process.env.USER ?? "unknown";
34
+ removeOldFiles(base, (n) => n === `changes-${user}.json`, 21600, now);
35
+ trimLogFile(join(claudeHome(home), "logs", "hooks.log"), 10485760, 5000);
36
+ removeOldFiles(join(claudeHome(home), "logs", "00-apex"), (n) => n.startsWith("ref-cache-") && n.endsWith(".json"), 86400, now);
37
+ purgeTtlTree(base, PURGEABLE, now);
38
+ pruneEmptyDirs(base, Object.keys(PURGEABLE));
39
+ }
40
+
41
+ /**
42
+ * Handle core-guards SessionStart: inject CLAUDE.md + dev context as
43
+ * `additionalContext`, then run the cache/state cleanups. Ports the four
44
+ * `session-start/*.py` scripts into one harness call.
45
+ * @param cwd - Project root for dev-context detection.
46
+ * @param home - Home dir (defaults to `~`).
47
+ * @param now - Clock for TTL cleanup (defaults to `Date.now()`).
48
+ * @param id - Harness target id (defaults to "claude-code" — zero-regression default).
49
+ * @returns The native hook stdout (possibly empty).
50
+ */
51
+ export function sessionStartCore(cwd: string, home: string = homedir(), now: number = Date.now(), id: string = "claude-code"): string {
52
+ const md = claudeMd(home, id);
53
+ const dev = devContext(cwd);
54
+ runSessionStartCleanups(home, now);
55
+ const ctx = [md, dev].filter(Boolean).join("\n");
56
+ return ctx ? attachSystemMessage(contextResponse("SessionStart", ctx), `${apexDocName(id)} injected`) : "";
57
+ }
@@ -0,0 +1,27 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ /** Max board characters injected — a persistent board should stay small; over-long boards are truncated. */
5
+ const MAX_BOARD = 4000;
6
+
7
+ /**
8
+ * Collect the persistent task board: the contents of `<root>/.claude/BOARD.md`
9
+ * (truncated to {@link MAX_BOARD}) plus an instruction to keep it current. The
10
+ * board lives on disk so it survives context purges — rehydrated every session.
11
+ * Missing/empty/unreadable board → `""` (section omitted).
12
+ * @param root - The project root.
13
+ * @returns The rendered board section body, or `""` when there is no board.
14
+ */
15
+ export function collectBoard(root: string): string {
16
+ const path = join(root, ".claude", "BOARD.md");
17
+ try {
18
+ if (!existsSync(path)) return "";
19
+ let body = readFileSync(path, "utf8").trim();
20
+ if (!body) return "";
21
+ if (body.length > MAX_BOARD) body = `${body.slice(0, MAX_BOARD)}\n… (truncated)`;
22
+ const note = "- .claude/BOARD.md (keep current — Write to it as tasks start/finish):";
23
+ return `${note}\n\n${body}`;
24
+ } catch {
25
+ return "";
26
+ }
27
+ }
@@ -0,0 +1,60 @@
1
+ import { contextResponse } from "../../../adapters/claude";
2
+ import { capFragment, budgetReport } from "../../inject-budget";
3
+
4
+ /** A titled snapshot section — dropped from the render when its `body` is empty. */
5
+ export interface Section {
6
+ title: string;
7
+ body: string;
8
+ }
9
+
10
+ /**
11
+ * Render the non-empty `sections` under one reconciliation heading. Empty
12
+ * sections are dropped; when every section is empty the whole snapshot is `""`.
13
+ * Each surviving section is passed through {@link capFragment} (harness-produced
14
+ * content, not owner CLAUDE.md), and a trailing {@link budgetReport} line gives
15
+ * the owner-requested numeric visibility into what was actually injected.
16
+ * @param sections - The collected sections in display order.
17
+ * @returns The assembled markdown block, or `""` when nothing to report.
18
+ */
19
+ export function renderSections(sections: Section[]): string {
20
+ const kept = sections
21
+ .filter((s) => s.body.trim())
22
+ .map((s) => ({ title: s.title, body: capFragment(s.title, s.body.trim()) }));
23
+ if (!kept.length) return "";
24
+ const header =
25
+ "# Reconciliation snapshot\nReal state of the world at session start — reconcile against this instead of re-discovering it.";
26
+ const parts = kept.map((s) => `### ${s.title}\n${s.body}`);
27
+ const report = budgetReport(kept.map((s) => ({ label: s.title, chars: s.body.length })));
28
+ return `${header}\n\n${parts.join("\n\n")}\n\n_${report}_`;
29
+ }
30
+
31
+ /**
32
+ * Concatenate `snapshot` onto an existing SessionStart stdout's
33
+ * `additionalContext` — it never replaces prior injected context (CLAUDE.md,
34
+ * dev-context). When `stdout` is empty a fresh {@link contextResponse} is made;
35
+ * a non-empty but unparseable `stdout` is returned UNCHANGED (the snapshot is
36
+ * dropped) — fabricating a fresh response there would discard the very CLAUDE.md
37
+ * injection the invariant protects, so preserving prior context always wins.
38
+ * @param stdout - The core SessionStart JSON stdout (may be `""`).
39
+ * @param snapshot - The snapshot markdown to append (no-op when `""`).
40
+ * @returns The merged hook stdout JSON.
41
+ */
42
+ export function attachSnapshot(stdout: string, snapshot: string): string {
43
+ if (!snapshot) return stdout;
44
+ if (!stdout) return contextResponse("SessionStart", snapshot);
45
+ try {
46
+ const parsed = JSON.parse(stdout) as {
47
+ hookSpecificOutput?: { additionalContext?: string };
48
+ };
49
+ const prev = parsed.hookSpecificOutput?.additionalContext ?? "";
50
+ const merged = prev ? `${prev}\n\n${snapshot}` : snapshot;
51
+ return JSON.stringify({
52
+ ...parsed,
53
+ hookSpecificOutput: { ...parsed.hookSpecificOutput, hookEventName: "SessionStart", additionalContext: merged },
54
+ });
55
+ } catch {
56
+ // Unparseable non-empty stdout: keep it verbatim so prior injected context
57
+ // (CLAUDE.md) is never discarded — the snapshot is sacrificed, not the invariant.
58
+ return stdout;
59
+ }
60
+ }
@@ -0,0 +1,71 @@
1
+ import { execSync } from "node:child_process";
2
+
3
+ /**
4
+ * Run a git subcommand at `root` with a short timeout, returning trimmed stdout.
5
+ * Uses `node:child_process` (the Bun shell can hang on some git plumbing) and
6
+ * swallows every failure — a non-repo, missing git, or timeout yields `""` so
7
+ * the caller omits the section instead of throwing inside the hook.
8
+ * @param root - Directory to run git in.
9
+ * @param args - The git args (e.g. `"log --oneline -3"`).
10
+ * @returns Trimmed stdout, or `""` on any error.
11
+ */
12
+ function git(root: string, args: string): string {
13
+ try {
14
+ return execSync(`git ${args}`, {
15
+ cwd: root,
16
+ encoding: "utf8",
17
+ timeout: 150,
18
+ stdio: ["ignore", "pipe", "ignore"],
19
+ }).trim();
20
+ } catch {
21
+ return "";
22
+ }
23
+ }
24
+
25
+ /** Working-tree file counts parsed from `git status --porcelain`. */
26
+ interface Wip {
27
+ staged: number;
28
+ unstaged: number;
29
+ untracked: number;
30
+ }
31
+
32
+ /** Count staged/unstaged/untracked files from porcelain v1 output (skips the `##` branch line). */
33
+ function countWip(porcelain: string): Wip {
34
+ const w: Wip = { staged: 0, unstaged: 0, untracked: 0 };
35
+ for (const line of porcelain.split("\n")) {
36
+ if (!line || line.startsWith("#")) continue;
37
+ if (line.startsWith("??")) { w.untracked++; continue; }
38
+ const x = line[0], y = line[1];
39
+ if (x && x !== " " && x !== "?") w.staged++;
40
+ if (y === "M" || y === "D") w.unstaged++;
41
+ }
42
+ return w;
43
+ }
44
+
45
+ /** Parse the current branch from the leading `## branch...upstream` porcelain line. */
46
+ function parseBranch(porcelain: string): string {
47
+ const head = porcelain.split("\n")[0] ?? "";
48
+ if (!head.startsWith("## ")) return "";
49
+ const rest = head.slice(3);
50
+ const dots = rest.indexOf("...");
51
+ return (dots >= 0 ? rest.slice(0, dots) : rest).split(" ")[0] ?? "";
52
+ }
53
+
54
+ /**
55
+ * Collect a compact git reconciliation section for `root`: current branch, the
56
+ * last three commits (oneline), and staged/unstaged/untracked WIP counts. When
57
+ * `root` is not a git repo (status fails) the whole section is omitted (`""`).
58
+ * @param root - The project/repo root.
59
+ * @returns The rendered git section body, or `""` when not a repo.
60
+ */
61
+ export function collectGit(root: string): string {
62
+ const status = git(root, "status --porcelain=v1 --branch");
63
+ if (!status) return "";
64
+ const branch = parseBranch(status) || "(unknown)";
65
+ const w = countWip(status);
66
+ const log = git(root, "log --oneline -3");
67
+ const lines = [`- branch: ${branch}`];
68
+ if (log) lines.push("- recent:", ...log.split("\n").map((l) => ` ${l}`));
69
+ lines.push(`- WIP: ${w.staged} staged, ${w.unstaged} unstaged, ${w.untracked} untracked`);
70
+ return lines.join("\n");
71
+ }
@@ -0,0 +1,52 @@
1
+ import { projectRootOrNull } from "../../../util/project-root";
2
+ import { oneShotSummary } from "../../../tracking/one-shot";
3
+ import { collectGit } from "./git";
4
+ import { collectVersion } from "./version";
5
+ import { collectBoard } from "./board";
6
+ import { renderSections, attachSnapshot, type Section } from "./format";
7
+
8
+ /** Run `fn`, swallowing any throw into `""` so no single collector can break the hook. */
9
+ function safe(fn: () => string): string {
10
+ try {
11
+ return fn();
12
+ } catch {
13
+ return "";
14
+ }
15
+ }
16
+
17
+ /**
18
+ * Build the reconciliation snapshot markdown for `cwd`: git state, running
19
+ * harness version + drift, the persistent board, and one-shot gate status. Each
20
+ * collector is isolated by {@link safe}; an all-empty result yields `""`.
21
+ * @param cwd - The session working directory.
22
+ * @param moduleUrl - `import.meta.url` of the caller (locates the running package).
23
+ * @returns The snapshot markdown, or `""` when nothing to report.
24
+ */
25
+ export function renderSnapshot(cwd: string, moduleUrl: string): string {
26
+ const root = projectRootOrNull(cwd) ?? cwd;
27
+ const sections: Section[] = [
28
+ { title: "Git", body: safe(() => collectGit(root)) },
29
+ { title: "Version", body: safe(() => collectVersion(root, moduleUrl)) },
30
+ { title: "Board", body: safe(() => collectBoard(root)) },
31
+ // raw `cwd` (not `root`): oneShotSummary derives the state dir via defaultStateDir(cwd),
32
+ // keyed on the same raw hook cwd the runtime writes gate counters under (handle.ts).
33
+ { title: "One-shot gates", body: safe(() => oneShotSummary(cwd)) },
34
+ ];
35
+ return renderSections(sections);
36
+ }
37
+
38
+ /**
39
+ * Concatenate the reconciliation snapshot onto a core SessionStart stdout. Fully
40
+ * fail-safe: any error returns `stdout` unchanged so the hook never breaks.
41
+ * @param stdout - The core SessionStart JSON stdout (may be `""`).
42
+ * @param cwd - The session working directory.
43
+ * @param moduleUrl - `import.meta.url` of the caller.
44
+ * @returns The merged hook stdout.
45
+ */
46
+ export function withSnapshot(stdout: string, cwd: string, moduleUrl: string): string {
47
+ try {
48
+ return attachSnapshot(stdout, renderSnapshot(cwd, moduleUrl));
49
+ } catch {
50
+ return stdout;
51
+ }
52
+ }
@@ -0,0 +1,34 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { runningVersion } from "../../../cli/doctor";
4
+
5
+ /** Read the `version` field of `<root>/package.json`, or `""` if absent/unreadable. */
6
+ function pkgVersion(root: string): string {
7
+ try {
8
+ const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as { version?: string };
9
+ return pkg.version ?? "";
10
+ } catch {
11
+ return "";
12
+ }
13
+ }
14
+
15
+ /**
16
+ * Collect the version reconciliation section: the harness version actually
17
+ * running (resolved from {@link runningVersion}, no network) and, when `root`
18
+ * carries its own `package.json`, whether that project's version has drifted
19
+ * from the running harness.
20
+ * @param root - The project root (cwd repo).
21
+ * @param moduleUrl - `import.meta.url` of the calling module (locates the running package.json).
22
+ * @returns The rendered version section body (never `""`).
23
+ */
24
+ export function collectVersion(root: string, moduleUrl: string): string {
25
+ const running = runningVersion(moduleUrl).version;
26
+ const lines = [`- harness running: v${running}`];
27
+ const project = pkgVersion(root);
28
+ if (project && project !== running) {
29
+ lines.push(`- project package.json: v${project} (DRIFT — running harness differs)`);
30
+ } else if (project) {
31
+ lines.push(`- project package.json: v${project} (in sync)`);
32
+ }
33
+ return lines.join("\n");
34
+ }
@@ -0,0 +1,56 @@
1
+ import { appendFileSync, existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ /** A SOLID project profile: type label, per-file line limit, interface dir. */
5
+ export interface SolidProfile {
6
+ type: string;
7
+ limit: number;
8
+ ifaceDir: string;
9
+ }
10
+
11
+ /** Ordered detection table (mirrors solid/scripts/detect-project.py). */
12
+ const CHECKS: { file: string; grep: string | null; profile: SolidProfile }[] = [
13
+ { file: "package.json", grep: "next", profile: { type: "nextjs", limit: 150, ifaceDir: "modules/cores/interfaces" } },
14
+ { file: "composer.json", grep: "laravel", profile: { type: "laravel", limit: 100, ifaceDir: "app/Contracts" } },
15
+ { file: "go.mod", grep: null, profile: { type: "go", limit: 100, ifaceDir: "internal/interfaces" } },
16
+ { file: "Cargo.toml", grep: null, profile: { type: "rust", limit: 100, ifaceDir: "src/traits" } },
17
+ { file: "pyproject.toml", grep: null, profile: { type: "python", limit: 100, ifaceDir: "src/interfaces" } },
18
+ { file: "requirements.txt", grep: null, profile: { type: "python", limit: 100, ifaceDir: "src/interfaces" } },
19
+ ];
20
+
21
+ /** Detect the SOLID profile for `projectDir`, defaulting to `unknown`. */
22
+ export function detectSolidProfile(projectDir: string): SolidProfile {
23
+ for (const { file, grep, profile } of CHECKS) {
24
+ const path = join(projectDir, file);
25
+ if (!existsSync(path)) continue;
26
+ if (grep !== null) {
27
+ try { if (!readFileSync(path, "utf-8").includes(grep)) continue; } catch { continue; }
28
+ }
29
+ return profile;
30
+ }
31
+ if (existsSync(join(projectDir, "Package.swift"))) return { type: "swift", limit: 150, ifaceDir: "Protocols" };
32
+ try {
33
+ if (readdirSync(projectDir).some((e) => e.endsWith(".xcodeproj") || e.endsWith(".xcworkspace"))) {
34
+ return { type: "swift", limit: 150, ifaceDir: "Protocols" };
35
+ }
36
+ } catch { /* unreadable dir */ }
37
+ return { type: "unknown", limit: 100, ifaceDir: "" };
38
+ }
39
+
40
+ /**
41
+ * Handle solid SessionStart: detect the profile, append SOLID_* exports to
42
+ * `CLAUDE_ENV_FILE`, and return the `SOLID: …` stdout line (or "" for unknown).
43
+ * Ports `solid/scripts/detect-project.py`.
44
+ * @param env - Environment (defaults to `process.env`).
45
+ * @returns The plain-text stdout line (possibly empty).
46
+ */
47
+ export function solidDetectStart(env: Record<string, string | undefined> = process.env): string {
48
+ const profile = detectSolidProfile(env.CLAUDE_PROJECT_DIR ?? ".");
49
+ const envFile = env.CLAUDE_ENV_FILE ?? "";
50
+ if (envFile) {
51
+ try {
52
+ appendFileSync(envFile, `export SOLID_PROJECT_TYPE=${profile.type}\nexport SOLID_FILE_LIMIT=${profile.limit}\nexport SOLID_INTERFACE_DIR=${profile.ifaceDir}\n`, "utf-8");
53
+ } catch { /* best effort */ }
54
+ }
55
+ return profile.type !== "unknown" ? `SOLID: ${profile.type} project (max ${profile.limit} lines)` : "";
56
+ }
@@ -0,0 +1,35 @@
1
+ import { homedir } from "node:os";
2
+ import { cleanupSession } from "./session-end";
3
+ import { validateTaskSolid } from "./task-completed";
4
+ import { defaultStateDir } from "../paths";
5
+ import { notify } from "../notifications";
6
+
7
+ /**
8
+ * Handle the core scope's `Stop` event — Codex parity. Codex never emits
9
+ * `SessionEnd`/`TaskCompleted` (codex-plugins/docs/reference/hooks.md,
10
+ * "Harness Runtime Limits": "lifecycle dispatch includes Claude-only names
11
+ * such as TaskCompleted, PostToolUseFailure, and SessionEnd; Codex does not
12
+ * emit those events") but its own hooks.json wires `Stop` to `hook codex
13
+ * core`, and its own docs table defines `Stop` as "turn finishes — cleanup
14
+ * and completion notification". Both ported behaviors collapse onto it here
15
+ * rather than being invented anew: {@link cleanupSession} (normally
16
+ * SessionEnd-only) for cleanup, {@link validateTaskSolid} (normally
17
+ * TaskCompleted-only) for the SOLID/receipt completion check.
18
+ *
19
+ * Claude-side, this branch is unreachable: core-guards' Claude `Stop` hooks
20
+ * are a native `afplay` sound + an LLM `type:"prompt"` check, neither of
21
+ * which invokes the harness binary (see `hooks/hooks.json` in claude-plugins
22
+ * vs codex-plugins) — `dispatchLifecycle`'s `case "Stop"` for scope `"core"`
23
+ * only ever receives a real payload from Codex.
24
+ * @param payload - The raw Stop hook payload.
25
+ * @param cwd - Project root (drives the state dir).
26
+ * @param now - Clock.
27
+ * @returns The native hook stdout ("" when the session is clean).
28
+ */
29
+ export function stopCore(payload: Record<string, unknown>, cwd: string, now: number): string {
30
+ cleanupSession(undefined, now);
31
+ // Turn finished — voice the "stop" sound (fire-and-forget, fail-open: never
32
+ // throws, never blocks; a silent no-op when opted out or no sound resolves).
33
+ notify("stop");
34
+ return validateTaskSolid(payload, homedir(), now, defaultStateDir(cwd), "Stop");
35
+ }
@@ -0,0 +1,61 @@
1
+ import { join } from "node:path";
2
+ import { homedir } from "node:os";
3
+ import { loadIndex } from "../../cache/io";
4
+ import { contextResponse } from "../../adapters/claude";
5
+ import { sanitizeSessionId, sessionsDir } from "../home-state";
6
+
7
+ const DEFAULT_TTL_MIN = 30;
8
+
9
+ /** Resolve cache TTL (minutes) from `FUSENGINE_CACHE_TTL_MIN` or default. */
10
+ function ttlMinutes(env: Record<string, string | undefined>): number {
11
+ const raw = (env.FUSENGINE_CACHE_TTL_MIN ?? "").trim();
12
+ const val = Number.parseInt(raw, 10);
13
+ return Number.isFinite(val) && val > 0 ? val : DEFAULT_TTL_MIN;
14
+ }
15
+
16
+ /** True when ISO ts `YYYY-MM-DDTHH:MM:SSZ` is within `ttlMin` minutes of now. */
17
+ function isFresh(ts: string, ttlMin: number, now: number): boolean {
18
+ const parsed = Date.parse(ts);
19
+ if (Number.isNaN(parsed)) return false;
20
+ const ageSec = (now - parsed) / 1000;
21
+ return ageSec >= 0 && ageSec <= ttlMin * 60;
22
+ }
23
+
24
+ /** Sanitize + truncate a cell value (replace `|`/newline, ellipsize). */
25
+ function trunc(text: unknown, limit: number): string {
26
+ const t = String(text ?? "").replace(/\|/g, "/").replace(/\n/g, " ");
27
+ return t.length <= limit ? t : t.slice(0, limit - 3) + "...";
28
+ }
29
+
30
+ /** Render fresh cache entries as the markdown injection block. */
31
+ function render(entries: Record<string, unknown>[]): string {
32
+ const lines = [
33
+ "# MCP cache available this session",
34
+ "Before calling mcp__context7/exa, check if the result is already cached.",
35
+ "Read the .md file via Read to retrieve the result.",
36
+ "APEX: reading the MCP cache via Read satisfies the research-expert step.",
37
+ "",
38
+ "| Tool | Query | File |",
39
+ "| --- | --- | --- |",
40
+ ];
41
+ for (const e of entries) lines.push(`| ${trunc(e.tool, 40)} | ${trunc(e.query, 60)} | ${trunc(e.file, 50)} |`);
42
+ return lines.join("\n");
43
+ }
44
+
45
+ /**
46
+ * Handle SubagentStart: surface fresh MCP cache entries for the session as
47
+ * `additionalContext`. Ports `subagent-start/inject-context-cache.py`.
48
+ * @param sessionIdRaw - Raw session id from the payload.
49
+ * @param home - Home dir (defaults to `~`).
50
+ * @param env - Environment (defaults to `process.env`).
51
+ * @param now - Clock (defaults to `Date.now()`).
52
+ * @returns The native hook stdout (possibly empty).
53
+ */
54
+ export function subagentCacheContext(sessionIdRaw: unknown, home: string = homedir(), env: Record<string, string | undefined> = process.env, now: number = Date.now()): string {
55
+ const sid = sanitizeSessionId(sessionIdRaw === "" || sessionIdRaw == null ? "unknown" : sessionIdRaw);
56
+ if (!sid) return "";
57
+ const index = loadIndex(join(sessionsDir(home), sid, "context", "index.json")) as Record<string, unknown>[];
58
+ if (index.length === 0) return "";
59
+ const fresh = index.filter((e) => isFresh(String(e.ts ?? ""), ttlMinutes(env), now));
60
+ return fresh.length ? contextResponse("SubagentStart", render(fresh)) : "";
61
+ }
@@ -0,0 +1,94 @@
1
+ import { basename, extname } from "node:path";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { contextResponse, systemMessage } from "../../adapters/claude";
5
+ import { resolveMaxLines } from "../../config/limits";
6
+ import { resolveTtlSec } from "../../config/ttl";
7
+ import { countLines } from "../../policy/file-size";
8
+ import { loadSessionState, sanitizeSessionId } from "../home-state";
9
+ import { defaultStateDir, trackFile } from "../paths";
10
+ import { freshReceiptFromFile } from "../../tracking/receipts";
11
+
12
+ /** Code-file extensions audited on task completion (mirrors validate-task-solid.py). */
13
+ const CODE_EXTENSIONS = new Set([
14
+ ".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java", ".php",
15
+ ".cpp", ".c", ".rb", ".swift", ".kt", ".dart", ".vue", ".svelte", ".astro",
16
+ ]);
17
+
18
+ /** Freshness multiple on `FUSE_ENFORCE_TTL_SEC` for receipts (no new env var); a tsc+test run precedes the "done" by more than one edit window. */
19
+ const RECEIPT_TTL_MULTIPLIER = 5;
20
+
21
+ /** Files modified during the session, persisted by {@link trackSessionChanges}. */
22
+ interface Changes {
23
+ modifiedFiles?: string[];
24
+ }
25
+
26
+ /** The modified files that are code (by extension) — the receipt gate's trigger set. */
27
+ function codeFiles(files: string[]): string[] {
28
+ return files.filter((fp) => CODE_EXTENSIONS.has(extname(fp)));
29
+ }
30
+
31
+ /**
32
+ * Refuse completion when code files changed but no fresh, passing verification
33
+ * receipt (`tsc`/test, exit 0, zero failures, within TTL×{@link RECEIPT_TTL_MULTIPLIER})
34
+ * exists in the signed track. TaskCompleted does NOT honor `decision:"block"`
35
+ * (verified against the official hooks docs — `TeammateIdle/TaskCreated/
36
+ * TaskCompleted` are excluded from that list); the documented stdout refusal is
37
+ * `{"continue":false,"stopReason":…}`, which halts the teammate with the reason
38
+ * shown to the user. Returns that JSON, or `null` when the session is clear.
39
+ */
40
+ function receiptGate(sid: string, files: string[], now: number, stateDir: string): string | null {
41
+ if (codeFiles(files).length === 0) return null;
42
+ const windowMs = resolveTtlSec(process.env) * 1000 * RECEIPT_TTL_MULTIPLIER;
43
+ if (freshReceiptFromFile(trackFile(sid, stateDir), windowMs, now)) return null;
44
+ const stopReason =
45
+ "VERIFICATION RECEIPT REQUIRED: code files changed but no fresh passing tsc/test receipt " +
46
+ "exists. Run `bun test` + `tsc --noEmit` (exit 0, 0 failures) and re-complete.";
47
+ return JSON.stringify({ continue: false, stopReason });
48
+ }
49
+
50
+ /**
51
+ * Re-count physical lines of every modified code file and collect SOLID
52
+ * violations (`<basename>: <n> lines (max <max>)`) for those exceeding `max`.
53
+ * @param files - Candidate modified file paths.
54
+ * @param max - The SOLID line ceiling.
55
+ * @returns The list of violation strings (empty when all files comply).
56
+ */
57
+ function collectViolations(files: string[], max: number): string[] {
58
+ const violations: string[] = [];
59
+ for (const fp of files) {
60
+ if (!CODE_EXTENSIONS.has(extname(fp)) || !existsSync(fp)) continue;
61
+ try {
62
+ const lines = countLines(readFileSync(fp, "utf-8"));
63
+ if (lines > max) violations.push(`${basename(fp)}: ${lines} lines (max ${max})`);
64
+ } catch { /* unreadable file — skip, matches Python's OSError pass */ }
65
+ }
66
+ return violations;
67
+ }
68
+
69
+ /**
70
+ * Handle TaskCompleted (ports `task-completed/validate-task-solid.py` + the
71
+ * receipt gate); also reused verbatim for Codex's `Stop` via {@link stopCore}
72
+ * (Codex never emits `TaskCompleted`) — `event` stamps the real hook name.
73
+ * @param payload - The TaskCompleted/Stop payload (`task_id`, `task_subject`, `session_id`).
74
+ * @param event - The hook event name to stamp on the SOLID-violation response (default `TaskCompleted`).
75
+ * @returns The native hook stdout, or `""` when the session is clean.
76
+ */
77
+ export function validateTaskSolid(payload: Record<string, unknown>, home: string = homedir(), now: number = Date.now(), stateDir: string = defaultStateDir(process.cwd()), event: string = "TaskCompleted"): string {
78
+ const sid = sanitizeSessionId(payload.session_id ?? "unknown");
79
+ if (!sid) return "";
80
+ const changes = loadSessionState(sid, home).changes as Changes | undefined;
81
+ const files = changes?.modifiedFiles ?? [];
82
+ if (files.length === 0) return "";
83
+ const max = resolveMaxLines();
84
+ const violations = collectViolations(files, max);
85
+ if (violations.length === 0) return receiptGate(sid, files, now, stateDir) ?? "";
86
+ const taskId = String(payload.task_id ?? "");
87
+ const subject = String(payload.task_subject ?? "");
88
+ const msg =
89
+ `SOLID VIOLATION in task '${subject}' (${taskId}): ` +
90
+ `${violations.length} file(s) exceed ${max} lines: ` +
91
+ violations.slice(0, 5).join("; ");
92
+ // TaskCompleted rejects `hookSpecificOutput` (same gap as TeammateIdle) — ride `systemMessage`; `Stop` keeps `additionalContext`, which it DOES accept.
93
+ return event === "TaskCompleted" ? systemMessage(msg) : contextResponse(event, msg);
94
+ }