@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,38 @@
1
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { harnessHomeSegment } from "../../policy/apex-target";
4
+
5
+ /** Two-digit zero-pad. */
6
+ function pad(n: number): string {
7
+ return String(n).padStart(2, "0");
8
+ }
9
+
10
+ /** Compact local timestamp `YYYYMMDD-HHMMSS` (mirrors Python strftime). */
11
+ function stamp(now: number): string {
12
+ const d = new Date(now);
13
+ return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
14
+ }
15
+
16
+ /**
17
+ * Handle PreCompact: back up the target apex `task.json` (`.claude/apex/`,
18
+ * `.codex/apex/`, ...) to `backups/`, keep only the 5 newest, and emit a
19
+ * confirmation. Ports `pre-compact/save-apex-state.py`.
20
+ * @param cwd - Project root.
21
+ * @param now - Clock (defaults to `Date.now()`).
22
+ * @param id - Harness target id (defaults to "claude-code" — zero-regression default).
23
+ * @returns The native hook stdout (possibly empty when no task.json).
24
+ */
25
+ export function saveApexState(cwd: string, now: number = Date.now(), id: string = "claude-code"): string {
26
+ const seg = harnessHomeSegment(id);
27
+ const apexDir = join(cwd, seg, "apex");
28
+ const stateFile = join(apexDir, "task.json");
29
+ if (!existsSync(stateFile)) return "";
30
+ const backupDir = join(apexDir, "backups");
31
+ mkdirSync(backupDir, { recursive: true });
32
+ copyFileSync(stateFile, join(backupDir, `task-${stamp(now)}.json`));
33
+ const backups = readdirSync(backupDir).filter((n) => n.startsWith("task-") && n.endsWith(".json")).sort().reverse();
34
+ for (const old of backups.slice(5)) {
35
+ try { rmSync(join(backupDir, old), { force: true }); } catch { /* best effort */ }
36
+ }
37
+ return JSON.stringify({ additionalContext: `APEX state saved before compaction. Previous task state preserved in ${seg}/apex/backups/` });
38
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Dynamic rules-plugin root resolution. The historical `CLAUDE_PLUGIN_ROOT ??
3
+ * cwd` chain only worked when the harness exported the plugin root — Kimi
4
+ * injects `KIMI_PLUGIN_ROOT` instead, and a bare cwd fallback never held a
5
+ * `rules/` dir. Resolution order (first hit wins):
6
+ * 1. `CLAUDE_PLUGIN_ROOT` (claude-code/codex plugin-declared hooks);
7
+ * 2. `KIMI_PLUGIN_ROOT` (kimi plugin-declared hooks);
8
+ * 3. Per-harness install probe (claude marketplace, codex versioned cache,
9
+ * kimi managed plugins) — first `<plugin>/rules` dir whose plugin folder
10
+ * name contains "rules";
11
+ * 4. `cwd` (historical fallback, unchanged behavior).
12
+ */
13
+ import { existsSync, readdirSync } from "node:fs";
14
+ import { homedir } from "node:os";
15
+ import { join } from "node:path";
16
+ import { maxSemver } from "../../util/semver";
17
+
18
+ /** Immediate child dir names of `dir`, or [] when unreadable. */
19
+ function children(dir: string): string[] {
20
+ try { return readdirSync(dir); } catch { return []; }
21
+ }
22
+
23
+ /** First existing `<base>/<child>/rules` where `child` names a rules plugin. */
24
+ function probe(base: string, childFilter: (name: string) => boolean): string | null {
25
+ for (const child of children(base).sort()) {
26
+ if (!childFilter(child)) continue;
27
+ const dir = join(base, child, "rules");
28
+ if (existsSync(dir)) return join(base, child);
29
+ }
30
+ return null;
31
+ }
32
+
33
+ /** Claude: `~/.claude/plugins/marketplaces/<marketplace>/plugins/<rules-plugin>/`. */
34
+ function probeClaude(home: string): string | null {
35
+ const markets = join(home, ".claude", "plugins", "marketplaces");
36
+ for (const m of children(markets).sort()) {
37
+ const hit = probe(join(markets, m, "plugins"), (n) => n.includes("rules"));
38
+ if (hit) return hit;
39
+ }
40
+ return null;
41
+ }
42
+
43
+ /** Codex: `~/.codex/plugins/cache/<marketplace>/<rules-plugin>/<version>/` (highest semver). */
44
+ function probeCodex(home: string): string | null {
45
+ const cache = join(home, ".codex", "plugins", "cache");
46
+ for (const m of children(cache).sort()) {
47
+ for (const p of children(join(cache, m)).sort()) {
48
+ if (!p.includes("rules")) continue;
49
+ const versions = children(join(cache, m, p)).filter((v) => existsSync(join(cache, m, p, v, "rules")));
50
+ const latest = maxSemver(versions);
51
+ if (latest) return join(cache, m, p, latest);
52
+ }
53
+ }
54
+ return null;
55
+ }
56
+
57
+ /** Kimi: `<KIMI_CODE_HOME>/plugins/managed/<rules-plugin>/` (managed install copy). */
58
+ function probeKimi(env: Record<string, string | undefined>, home: string): string | null {
59
+ const root = env.KIMI_CODE_HOME ?? join(home, ".kimi-code");
60
+ return probe(join(root, "plugins", "managed"), (n) => n.includes("rules"));
61
+ }
62
+
63
+ /**
64
+ * Resolve the directory whose `rules/` subtree holds the `*.md` rule files.
65
+ * @param id - Harness id (selects the install-layout probe).
66
+ * @param cwd - Historical fallback root.
67
+ * @param env - Environment (defaults to `process.env`).
68
+ * @returns The plugin root to read `rules/` from.
69
+ */
70
+ export function resolveRulesRoot(
71
+ id: string,
72
+ cwd: string,
73
+ env: Record<string, string | undefined> = process.env,
74
+ ): string {
75
+ if (env.CLAUDE_PLUGIN_ROOT) return env.CLAUDE_PLUGIN_ROOT;
76
+ if (env.KIMI_PLUGIN_ROOT) return env.KIMI_PLUGIN_ROOT;
77
+ const home = env.HOME ?? homedir();
78
+ const hit = id === "codex" ? probeCodex(home)
79
+ : id === "kimi" ? probeKimi(env, home)
80
+ : id === "claude-code" ? probeClaude(home)
81
+ : null;
82
+ return hit ?? cwd;
83
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Security advisory (PreToolUse Write/Edit on code files) — NON-BLOCKING.
3
+ * Ports `check-security-skill.py`: nudge the agent to read the security skill,
4
+ * but always allow the edit.
5
+ */
6
+ import { existsSync, readFileSync } from "node:fs";
7
+ import { homedir } from "node:os";
8
+ import { securityStatePath } from "./skill-state";
9
+ import { contextResponse } from "../../../adapters/claude";
10
+ import type { NormalizedFile } from "../../normalize";
11
+
12
+ const CODE_RE = /\.(ts|tsx|js|jsx|py|php|swift|go|rs|rb|java)$/;
13
+ const ADVISORY = "SECURITY: Read security skill references before modifying code. Use: Read skills/security-scan/references/scan-patterns.md";
14
+
15
+ /** True once today's security skill is marked read. Fail-open: missing/corrupt state reads as unread. */
16
+ function skillAlreadyRead(now: number, home: string): boolean {
17
+ const path = securityStatePath(now, home);
18
+ if (!existsSync(path)) return false;
19
+ try {
20
+ const state = JSON.parse(readFileSync(path, "utf-8")) as { skill_read?: boolean };
21
+ return state.skill_read === true;
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Build a non-blocking PreToolUse advisory when editing a code file before the
29
+ * security skill has been read, "" otherwise. Renders through the shared
30
+ * {@link contextResponse} builder (`additionalContext` only) — NEVER a naked
31
+ * `permissionDecision: "allow"`, which the Codex adapter's own hook shape
32
+ * (`src/adapters/claude/index.ts`) never emits and which Codex rejects.
33
+ * @param tool - The tool name (`Write`/`Edit`).
34
+ * @param filePath - The target file path.
35
+ * @param now - Clock.
36
+ * @param home - Home dir.
37
+ * @returns The advisory response JSON, or "".
38
+ */
39
+ export function securityAdvisory(tool: string, filePath: string, now: number = Date.now(), home: string = homedir()): string {
40
+ if (tool !== "Write" && tool !== "Edit") return "";
41
+ if (!CODE_RE.test(filePath)) return "";
42
+ if (skillAlreadyRead(now, home)) return "";
43
+ return contextResponse("PreToolUse", ADVISORY);
44
+ }
45
+
46
+ /**
47
+ * Multi-file counterpart for a Codex `apply_patch` envelope: evaluate EACH
48
+ * add/update file (delete ignored outright; non-code filtered by the same
49
+ * `CODE_RE` inside {@link securityAdvisory}) and return the advisory
50
+ * triggered by the FIRST qualifying file, or "" once the skill has been read
51
+ * or no file qualifies.
52
+ * @param files - The patch's per-file changes ({@link NormalizedFile}).
53
+ * @param now - Clock.
54
+ * @param home - Home dir.
55
+ */
56
+ export function securityAdvisoryForPatch(files: readonly NormalizedFile[], now: number = Date.now(), home: string = homedir()): string {
57
+ for (const f of files) {
58
+ if (f.op === "delete") continue;
59
+ const advisory = securityAdvisory(f.op === "add" ? "Write" : "Edit", f.filePath, now, home);
60
+ if (advisory) return advisory;
61
+ }
62
+ return "";
63
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @module security/scan-patterns
3
+ * OWASP scan pattern table by language. Ports the Python
4
+ * `security_scan_patterns.py`: each entry is (severity, category, regex, glob).
5
+ * Go/Rust/unknown intentionally have no patterns (parity: empty result).
6
+ * @packageDocumentation
7
+ */
8
+
9
+ /** Vulnerability severity level (drives the summary counters). */
10
+ export type Severity = "CRITICAL" | "HIGH" | "MEDIUM" | "LOW";
11
+
12
+ /** A single scan rule: a regex applied to files matching `glob`. */
13
+ export interface ScanPattern {
14
+ severity: Severity;
15
+ category: string;
16
+ regex: RegExp;
17
+ glob: string;
18
+ }
19
+
20
+ /** OWASP scan patterns keyed by detected language (ported 1:1 from Python). */
21
+ const PATTERNS: Record<string, ScanPattern[]> = {
22
+ javascript: [
23
+ { severity: "HIGH", category: "XSS", regex: /innerHTML\s*=/, glob: "*.js" },
24
+ { severity: "HIGH", category: "XSS", regex: /dangerouslySetInnerHTML/, glob: "*.js" },
25
+ { severity: "CRITICAL", category: "CODE_EXEC", regex: /eval\(/, glob: "*.js" },
26
+ { severity: "CRITICAL", category: "CODE_EXEC", regex: /new Function\(/, glob: "*.js" },
27
+ { severity: "CRITICAL", category: "CMD_INJECTION", regex: /child_process/, glob: "*.js" },
28
+ { severity: "HIGH", category: "CMD_INJECTION", regex: /shell:\s*true/, glob: "*.js" },
29
+ { severity: "MEDIUM", category: "WEAK_CRYPTO", regex: /Math\.random\(\)/, glob: "*.js" },
30
+ { severity: "CRITICAL", category: "SECRETS", regex: /AKIA[0-9A-Z]{16}/, glob: "*.js" },
31
+ ],
32
+ php: [
33
+ { severity: "CRITICAL", category: "RCE", regex: /shell_exec|system\(|passthru\(/, glob: "*.php" },
34
+ { severity: "CRITICAL", category: "CODE_EXEC", regex: /eval\(|assert\(/, glob: "*.php" },
35
+ { severity: "HIGH", category: "SQL_INJECTION", regex: /mysql_query\(/, glob: "*.php" },
36
+ ],
37
+ python: [
38
+ { severity: "CRITICAL", category: "CODE_EXEC", regex: /eval\(|exec\(/, glob: "*.py" },
39
+ { severity: "CRITICAL", category: "CMD_INJECTION", regex: /os\.system\(|subprocess.*shell=True/, glob: "*.py" },
40
+ { severity: "HIGH", category: "DESERIALIZATION", regex: /pickle\.loads\(/, glob: "*.py" },
41
+ { severity: "HIGH", category: "TLS", regex: /verify=False|ssl\.CERT_NONE/, glob: "*.py" },
42
+ ],
43
+ swift: [
44
+ { severity: "HIGH", category: "INSECURE_STORAGE", regex: /UserDefaults.*password|token|secret/, glob: "*.swift" },
45
+ { severity: "MEDIUM", category: "INSECURE_HTTP", regex: /"http:\/\//, glob: "*.swift" },
46
+ { severity: "HIGH", category: "WEAK_KEYCHAIN", regex: /kSecAttrAccessibleAlways/, glob: "*.swift" },
47
+ ],
48
+ };
49
+
50
+ /** Return the scan patterns for a language ("" / go / rust / unknown → none). */
51
+ export function getPatterns(lang: string): ScanPattern[] {
52
+ return PATTERNS[lang] ?? [];
53
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * @module security/scan
3
+ * OWASP vulnerability scanner. Ports the Python `security-scan.py`: detect the
4
+ * project language, walk source files (excluding vendored/build dirs), match the
5
+ * per-language {@link getPatterns} rules, and aggregate a JSON report. Invoked by
6
+ * the `harness scan` CLI command (skill `/fuse-security:scan`), NOT a hook.
7
+ * @packageDocumentation
8
+ */
9
+ import { existsSync, readdirSync, readFileSync, type Dirent } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { getPatterns, type ScanPattern, type Severity } from "./scan-patterns";
12
+
13
+ const EXCLUDE_RE = /(^|\/)(node_modules|vendor|\.git|dist|build)(\/|$)/;
14
+ const LANG_FILES: readonly (readonly [string, string])[] = [
15
+ ["package.json", "javascript"], ["composer.json", "php"], ["requirements.txt", "python"],
16
+ ["pyproject.toml", "python"], ["Package.swift", "swift"], ["go.mod", "go"], ["Cargo.toml", "rust"],
17
+ ];
18
+
19
+ /** A single vulnerability match in the scanned tree. */
20
+ export interface Finding {
21
+ severity: Severity; category: string; pattern: string; file: string; line: number;
22
+ }
23
+
24
+ /** The full scan report (mirrors the Python JSON shape). */
25
+ export interface ScanReport {
26
+ language: string; directory: string;
27
+ summary: Record<string, number>; findings: Finding[];
28
+ }
29
+
30
+ /** Detect the project language from its config files (first match wins). */
31
+ export function detectLanguage(dir: string): string {
32
+ for (const [file, lang] of LANG_FILES) if (existsSync(join(dir, file))) return lang;
33
+ return "unknown";
34
+ }
35
+
36
+ /** Recursively collect files whose name ends with `ext`, skipping vendored dirs. */
37
+ function collectFiles(dir: string, ext: string, out: string[] = []): string[] {
38
+ let entries: Dirent[];
39
+ try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return out; }
40
+ for (const e of entries) {
41
+ const full = join(dir, e.name);
42
+ if (EXCLUDE_RE.test(full)) continue;
43
+ if (e.isDirectory()) collectFiles(full, ext, out);
44
+ else if (e.name.endsWith(ext)) out.push(full);
45
+ }
46
+ return out;
47
+ }
48
+
49
+ /** Apply one pattern to every matching file and collect line-level findings. */
50
+ function scanPattern(dir: string, p: ScanPattern): Finding[] {
51
+ const ext = p.glob.replace(/^\*/, "");
52
+ const findings: Finding[] = [];
53
+ for (const file of collectFiles(dir, ext)) {
54
+ let text: string;
55
+ try { text = readFileSync(file, "utf8"); } catch { continue; }
56
+ text.split("\n").forEach((line, i) => {
57
+ if (p.regex.test(line)) findings.push({ severity: p.severity, category: p.category, pattern: p.regex.source, file, line: i + 1 });
58
+ });
59
+ }
60
+ return findings;
61
+ }
62
+
63
+ /** Run the OWASP scan over `dir` and return the aggregated {@link ScanReport}. */
64
+ export function runSecurityScan(dir: string): ScanReport {
65
+ const language = detectLanguage(dir);
66
+ const counts: Record<string, number> = { critical: 0, high: 0, medium: 0, low: 0 };
67
+ const findings: Finding[] = [];
68
+ for (const p of getPatterns(language)) {
69
+ const hits = scanPattern(dir, p);
70
+ findings.push(...hits);
71
+ const key = p.severity.toLowerCase();
72
+ counts[key] = (counts[key] ?? 0) + hits.length;
73
+ }
74
+ const total = Object.values(counts).reduce((a, b) => a + b, 0);
75
+ return { language, directory: dir, summary: { ...counts, total }, findings };
76
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Shared security-tracker state: per-UTC-day JSON under
3
+ * `~/.claude/logs/00-security`. Ports the state helpers of
4
+ * `check-security-skill.py` / `track-skill-read.py` / `track-mcp-research.py`.
5
+ */
6
+ import { existsSync, readFileSync } from "node:fs";
7
+ import { homedir } from "node:os";
8
+ import { join } from "node:path";
9
+ import { claudeHome } from "../../home-state";
10
+ import { atomicWrite } from "../../../util/json-io";
11
+
12
+ /** `~/.claude/logs/00-security` state directory. */
13
+ export function securityStateDir(home: string = homedir()): string {
14
+ return join(claudeHome(home), "logs", "00-security");
15
+ }
16
+
17
+ /** Current UTC date as `YYYY-MM-DD`. */
18
+ export function todayUtc(now: number = Date.now()): string {
19
+ return new Date(now).toISOString().slice(0, 10);
20
+ }
21
+
22
+ /** Current UTC instant as `YYYY-MM-DDTHH:MM:SSZ` (seconds, no millis). */
23
+ export function isoUtc(now: number = Date.now()): string {
24
+ return new Date(now).toISOString().replace(/\.\d{3}Z$/, "Z");
25
+ }
26
+
27
+ /** Today's security-state file path. */
28
+ export function securityStatePath(now: number = Date.now(), home: string = homedir()): string {
29
+ return join(securityStateDir(home), `${todayUtc(now)}-state.json`);
30
+ }
31
+
32
+ /** Load today's security state, or `{}` when missing/corrupt. */
33
+ export function loadSecurityState(now: number = Date.now(), home: string = homedir()): Record<string, unknown> {
34
+ const path = securityStatePath(now, home);
35
+ try {
36
+ if (!existsSync(path)) return {};
37
+ const data: unknown = JSON.parse(readFileSync(path, "utf-8"));
38
+ return typeof data === "object" && data !== null && !Array.isArray(data) ? (data as Record<string, unknown>) : {};
39
+ } catch {
40
+ return {};
41
+ }
42
+ }
43
+
44
+ /** Persist today's security state (indent 2, no trailing newline). */
45
+ export function saveSecurityState(state: Record<string, unknown>, now: number = Date.now(), home: string = homedir()): void {
46
+ atomicWrite(securityStatePath(now, home), JSON.stringify(state, null, 2));
47
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Security MCP-research tracker (PostToolUse context7/exa). Ports
3
+ * `track-mcp-research.py`: logs documentation queries to today's state.
4
+ */
5
+ import { homedir } from "node:os";
6
+ import { isoUtc, loadSecurityState, saveSecurityState } from "./skill-state";
7
+
8
+ /** A recorded MCP research call. */
9
+ interface ResearchEntry {
10
+ timestamp: string;
11
+ tool: string;
12
+ query: string;
13
+ }
14
+
15
+ /**
16
+ * Append a context7/exa research call to today's security state. No-op for other
17
+ * tools. No stdout.
18
+ * @param tool - The tool name.
19
+ * @param input - The tool input (query/libraryId/libraryName).
20
+ * @param now - Clock.
21
+ * @param home - Home dir.
22
+ */
23
+ export function trackMcpResearch(tool: string, input: Record<string, unknown>, now: number = Date.now(), home: string = homedir()): void {
24
+ if (!tool.includes("context7") && !tool.includes("exa")) return;
25
+ const query = String(input.query ?? input.libraryId ?? input.libraryName ?? "");
26
+ const state = loadSecurityState(now, home) as { skill_read?: boolean; reads?: unknown[]; research?: ResearchEntry[] };
27
+ (state.research ??= []).push({ timestamp: isoUtc(now), tool, query });
28
+ saveSecurityState(state, now, home);
29
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Security skill-read tracker (PostToolUse Read). Ports `track-skill-read.py`:
3
+ * flips `skill_read` once a security skill reference is read.
4
+ */
5
+ import { homedir } from "node:os";
6
+ import { isoUtc, loadSecurityState, saveSecurityState } from "./skill-state";
7
+
8
+ const SKILL_RE = /skills\/(security-scan|cve-research|dependency-audit|security-headers|auth-audit)\//;
9
+
10
+ /** A recorded skill-reference read. */
11
+ interface SkillReadEntry {
12
+ timestamp: string;
13
+ file: string;
14
+ }
15
+
16
+ /**
17
+ * Mark the security skill as read when a Read hits a security skill reference.
18
+ * No-op for other tools/paths. No stdout.
19
+ * @param tool - The tool name.
20
+ * @param filePath - The read file path.
21
+ * @param now - Clock.
22
+ * @param home - Home dir.
23
+ */
24
+ export function trackSkillRead(tool: string, filePath: string, now: number = Date.now(), home: string = homedir()): void {
25
+ if (tool !== "Read") return;
26
+ if (!SKILL_RE.test(filePath)) return;
27
+ const state = loadSecurityState(now, home) as { skill_read?: boolean; reads?: SkillReadEntry[] };
28
+ state.skill_read = true;
29
+ (state.reads ??= []).push({ timestamp: isoUtc(now), file: filePath });
30
+ saveSecurityState(state, now, home);
31
+ }
@@ -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
+ }