@fusengine/harness 0.1.89 → 0.1.91

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (419) hide show
  1. package/dist/adapters/claude/index.d.mts +1 -1
  2. package/dist/adapters/claude/index.mjs +1 -1
  3. package/dist/adapters/cline/index.mjs +1 -1
  4. package/dist/adapters/codex/index.d.mts +2 -2
  5. package/dist/adapters/codex/index.mjs +5 -4
  6. package/dist/adapters/cursor/index.d.mts +15 -28
  7. package/dist/adapters/cursor/index.mjs +35 -46
  8. package/dist/adapters/gemini/index.mjs +1 -1
  9. package/dist/adapters/hermes/index.d.mts +1 -1
  10. package/dist/adapters/hermes/index.mjs +1 -1
  11. package/dist/adapters/kimi/index.d.mts +1 -1
  12. package/dist/adapters/kimi/index.mjs +1 -1
  13. package/dist/{claude-5bC8TDAz.mjs → claude-Ckv2_TgP.mjs} +3 -43
  14. package/dist/cli/bin.mjs +429 -16
  15. package/dist/cli/index.mjs +1 -1
  16. package/dist/command-string-CALMTnwN.mjs +42 -0
  17. package/dist/{evaluate-C1VSI5pb.mjs → evaluate-CMiIqHeH.mjs} +261 -13
  18. package/dist/{handle-DHhukRdh.mjs → handle-C43gA-Pr.mjs} +737 -150
  19. package/dist/{hermes-kP6NUQlG.mjs → hermes-B9-p_3IF.mjs} +3 -2
  20. package/dist/{index-DBgnIZn9.d.mts → index-B8PG82su.d.mts} +1 -1
  21. package/dist/{index-D4C2-IIc.d.mts → index-SmKYkk2N.d.mts} +1 -1
  22. package/dist/index.d.mts +2 -2
  23. package/dist/index.mjs +2 -2
  24. package/dist/init/index.d.mts +1 -1
  25. package/dist/init/index.mjs +1 -1
  26. package/dist/{kimi-D6X7b8wF.mjs → kimi-G2wcSh5-.mjs} +3 -2
  27. package/dist/normalize-BjG6unTj.mjs +317 -0
  28. package/dist/permission-mode-B-SFyR0X.mjs +226 -0
  29. package/dist/policy/index.d.mts +1 -1
  30. package/dist/policy/index.mjs +2 -2
  31. package/dist/prompt/index.d.mts +1 -1
  32. package/dist/{run-CdIlUtjv.mjs → run-DkrzC0gb.mjs} +1 -1
  33. package/dist/{run-TAXNRSpD.mjs → run-GHAMqcOn.mjs} +26 -3
  34. package/dist/runtime/index.d.mts +27 -16
  35. package/dist/runtime/index.mjs +2 -2
  36. package/dist/{types-C0-igRKr.d.mts → types-Bh0jEF3_.d.mts} +2 -0
  37. package/dist/{validate-DZFtAbLP.mjs → validate-KjZ1X9tH.mjs} +1 -1
  38. package/package.json +3 -2
  39. package/src/adapters/claude/index.ts +92 -0
  40. package/src/adapters/cline/index.ts +36 -0
  41. package/src/adapters/codex/apply-patch.ts +94 -0
  42. package/src/adapters/codex/index.ts +84 -0
  43. package/src/adapters/codex/permission-mode.ts +16 -0
  44. package/src/adapters/cursor/context.ts +46 -0
  45. package/src/adapters/cursor/events.ts +38 -0
  46. package/src/adapters/cursor/index.ts +43 -0
  47. package/src/adapters/cursor/interfaces/types.ts +88 -0
  48. package/src/adapters/cursor/native-response.ts +158 -0
  49. package/src/adapters/cursor/normalize.ts +123 -0
  50. package/src/adapters/cursor/respond.ts +123 -0
  51. package/src/adapters/gemini/index.ts +36 -0
  52. package/src/adapters/hermes/index.ts +55 -0
  53. package/src/adapters/hermes/interfaces/types.ts +26 -0
  54. package/src/adapters/kimi/index.ts +96 -0
  55. package/src/adapters/kimi/interfaces/types.ts +55 -0
  56. package/src/cache/compact.ts +55 -0
  57. package/src/cache/index.ts +5 -0
  58. package/src/cache/io.ts +38 -0
  59. package/src/cache/mcp-response.ts +39 -0
  60. package/src/cache/mcp-store.ts +70 -0
  61. package/src/cache/store.ts +93 -0
  62. package/src/changelog/fetch.ts +79 -0
  63. package/src/cli/bin.ts +100 -0
  64. package/src/cli/cursor-event-scanner.ts +175 -0
  65. package/src/cli/cursor-stdin-reader.ts +65 -0
  66. package/src/cli/doctor.ts +96 -0
  67. package/src/cli/hook-io.ts +130 -0
  68. package/src/cli/hook-sound.ts +49 -0
  69. package/src/cli/index.ts +1 -0
  70. package/src/cli/json-primitive-scanner.ts +83 -0
  71. package/src/cli/run.ts +36 -0
  72. package/src/cli/scope.ts +31 -0
  73. package/src/codex-rules/build.ts +63 -0
  74. package/src/codex-rules/index.ts +8 -0
  75. package/src/codex-rules/mapping.ts +42 -0
  76. package/src/codex-rules/rules/absolute-paths.ts +52 -0
  77. package/src/codex-rules/rules/ask.ts +44 -0
  78. package/src/codex-rules/rules/chmod-ask.ts +57 -0
  79. package/src/codex-rules/rules/dd.ts +23 -0
  80. package/src/codex-rules/rules/diskutil.ts +33 -0
  81. package/src/codex-rules/rules/mkfs.ts +42 -0
  82. package/src/codex-rules/rules/privilege.ts +49 -0
  83. package/src/codex-rules/rules/remote-fetch.ts +31 -0
  84. package/src/codex-rules/rules/rm-variants.ts +58 -0
  85. package/src/codex-rules/rules/single-commands.ts +57 -0
  86. package/src/codex-rules/skip-list.ts +25 -0
  87. package/src/codex-rules/starlark.ts +46 -0
  88. package/src/codex-rules/types.ts +36 -0
  89. package/src/config/dotenv.ts +55 -0
  90. package/src/config/env.ts +10 -0
  91. package/src/config/home-dir.ts +36 -0
  92. package/src/config/index.ts +5 -0
  93. package/src/config/layout.ts +52 -0
  94. package/src/config/limits.ts +56 -0
  95. package/src/config/ttl.ts +30 -0
  96. package/src/detect/harness.ts +73 -0
  97. package/src/detect/index.ts +1 -0
  98. package/src/detect/interfaces/types.ts +18 -0
  99. package/src/freshness/agent-evidence-record.ts +95 -0
  100. package/src/freshness/agent-evidence.ts +95 -0
  101. package/src/freshness/codex-spawn-evidence.ts +91 -0
  102. package/src/freshness/doc-helpers.ts +90 -0
  103. package/src/freshness/evidence-harvest-io.ts +72 -0
  104. package/src/freshness/evidence-harvest.ts +61 -0
  105. package/src/freshness/explore-tools.ts +95 -0
  106. package/src/freshness/index.ts +2 -0
  107. package/src/freshness/query-framework.ts +73 -0
  108. package/src/freshness/ref-evidence.ts +49 -0
  109. package/src/freshness/ref-journal.ts +98 -0
  110. package/src/freshness/trivial-edit-counter.ts +30 -0
  111. package/src/index.ts +11 -0
  112. package/src/init/index.ts +2 -0
  113. package/src/init/run.ts +32 -0
  114. package/src/init/templates.ts +75 -0
  115. package/src/memory/gitignore.ts +14 -0
  116. package/src/memory/index.ts +3 -0
  117. package/src/memory/registry.ts +39 -0
  118. package/src/memory/session-roots.ts +99 -0
  119. package/src/memory/state.ts +58 -0
  120. package/src/policy/apex-authorization.ts +100 -0
  121. package/src/policy/apex-gates.ts +91 -0
  122. package/src/policy/apex-target-steps.ts +73 -0
  123. package/src/policy/apex-target.ts +49 -0
  124. package/src/policy/apex-task-context.ts +81 -0
  125. package/src/policy/apex.ts +94 -0
  126. package/src/policy/cartographer/build-tree.ts +69 -0
  127. package/src/policy/cartographer/describe.ts +53 -0
  128. package/src/policy/cartographer/entry.ts +38 -0
  129. package/src/policy/cartographer/frontmatter.ts +48 -0
  130. package/src/policy/cartographer/indicators.ts +34 -0
  131. package/src/policy/claude-md-context.ts +66 -0
  132. package/src/policy/conventions/index.ts +11 -0
  133. package/src/policy/conventions/interfaces.ts +71 -0
  134. package/src/policy/conventions/langs.ts +53 -0
  135. package/src/policy/conventions/query.ts +49 -0
  136. package/src/policy/conventions/react-hooks.ts +18 -0
  137. package/src/policy/conventions/store-or-query.ts +21 -0
  138. package/src/policy/conventions/stores.ts +56 -0
  139. package/src/policy/conventions/strip.ts +87 -0
  140. package/src/policy/conventions/verdict.ts +38 -0
  141. package/src/policy/creation-intent.ts +11 -0
  142. package/src/policy/deny-loop.ts +88 -0
  143. package/src/policy/design/allowed-write.ts +28 -0
  144. package/src/policy/design/content-checks.ts +45 -0
  145. package/src/policy/design/corpus-fs.ts +21 -0
  146. package/src/policy/design/corpus-resolve-cache.ts +49 -0
  147. package/src/policy/design/corpus-resolve.ts +99 -0
  148. package/src/policy/design/corpus.ts +92 -0
  149. package/src/policy/design/design-system-rules.ts +45 -0
  150. package/src/policy/design/flag.ts +30 -0
  151. package/src/policy/design/gates-pipeline.ts +105 -0
  152. package/src/policy/design/gates.ts +87 -0
  153. package/src/policy/design/screenshot-tools.ts +24 -0
  154. package/src/policy/design/skill-evidence.ts +34 -0
  155. package/src/policy/design/skill-gate.ts +81 -0
  156. package/src/policy/design/skill-triggers.ts +51 -0
  157. package/src/policy/design/state.ts +92 -0
  158. package/src/policy/design/template-urls.ts +54 -0
  159. package/src/policy/design/transitions.ts +68 -0
  160. package/src/policy/detect-claude-md-project-type.ts +32 -0
  161. package/src/policy/detect-framework.ts +99 -0
  162. package/src/policy/detect-primitive-checks.ts +100 -0
  163. package/src/policy/detect-primitive-lib.ts +55 -0
  164. package/src/policy/detect-project.ts +99 -0
  165. package/src/policy/edit-outcome.ts +53 -0
  166. package/src/policy/evaluate.ts +81 -0
  167. package/src/policy/expert-agents.ts +60 -0
  168. package/src/policy/file-size-scope.ts +32 -0
  169. package/src/policy/file-size.ts +99 -0
  170. package/src/policy/framework-solid-exclude.ts +52 -0
  171. package/src/policy/framework-solid-extended.ts +93 -0
  172. package/src/policy/framework-solid-gates-systems.ts +99 -0
  173. package/src/policy/framework-solid-gates.ts +98 -0
  174. package/src/policy/framework-solid.ts +88 -0
  175. package/src/policy/gemini-mcp-gate.ts +83 -0
  176. package/src/policy/git-gates.ts +40 -0
  177. package/src/policy/guards/bash-command-anchor.ts +40 -0
  178. package/src/policy/guards/bash-write-patterns.ts +140 -0
  179. package/src/policy/guards/bash-write-redirects.ts +201 -0
  180. package/src/policy/guards/bash-write-safe-paths.ts +62 -0
  181. package/src/policy/guards/bash-write.ts +76 -0
  182. package/src/policy/guards/context.ts +12 -0
  183. package/src/policy/guards/index.ts +61 -0
  184. package/src/policy/guards/install.ts +22 -0
  185. package/src/policy/guards/interface-separation-ext.ts +41 -0
  186. package/src/policy/guards/interface-separation.ts +100 -0
  187. package/src/policy/guards/protected-path.ts +100 -0
  188. package/src/policy/guards/security.ts +97 -0
  189. package/src/policy/index.ts +19 -0
  190. package/src/policy/interfaces/types.ts +46 -0
  191. package/src/policy/js-gate-route.ts +51 -0
  192. package/src/policy/lessons/lesson-gate.ts +0 -0
  193. package/src/policy/lessons/trigger-index.ts +84 -0
  194. package/src/policy/lessons/types.ts +31 -0
  195. package/src/policy/module-layout.ts +31 -0
  196. package/src/policy/nearest-manifest.ts +75 -0
  197. package/src/policy/never-approval.ts +95 -0
  198. package/src/policy/patterns.ts +80 -0
  199. package/src/policy/seo/validate.ts +51 -0
  200. package/src/policy/shadcn-project.ts +22 -0
  201. package/src/policy/shadcn-skill-gate.ts +97 -0
  202. package/src/policy/shell-read-refs.ts +96 -0
  203. package/src/policy/skill-path.ts +46 -0
  204. package/src/policy/skill-patterns/laravel.ts +40 -0
  205. package/src/policy/skill-patterns/nextjs.ts +29 -0
  206. package/src/policy/skill-patterns/react.ts +27 -0
  207. package/src/policy/skill-patterns/shadcn-subskills.ts +33 -0
  208. package/src/policy/skill-patterns/shadcn.ts +66 -0
  209. package/src/policy/skill-patterns/swift.ts +30 -0
  210. package/src/policy/skill-patterns/tailwind.ts +40 -0
  211. package/src/policy/skill-source.ts +52 -0
  212. package/src/policy/skill-trigger-patterns.ts +33 -0
  213. package/src/policy/skill-triggers.ts +108 -0
  214. package/src/policy/tailwind-skill-gate.ts +63 -0
  215. package/src/policy/trivial-edits.ts +52 -0
  216. package/src/policy/verbosity.ts +39 -0
  217. package/src/prompt/index.ts +1 -0
  218. package/src/prompt/types.ts +36 -0
  219. package/src/refs/discover.ts +69 -0
  220. package/src/refs/frontmatter.ts +24 -0
  221. package/src/refs/index.ts +4 -0
  222. package/src/refs/loader.ts +59 -0
  223. package/src/refs/ref-key.ts +41 -0
  224. package/src/refs/router.ts +63 -0
  225. package/src/refs/types.ts +25 -0
  226. package/src/runtime/activity.ts +99 -0
  227. package/src/runtime/apply-patch-gate.ts +41 -0
  228. package/src/runtime/burst-window.ts +21 -0
  229. package/src/runtime/codex-shell-tool.ts +37 -0
  230. package/src/runtime/command-string.ts +37 -0
  231. package/src/runtime/confirm/codex-confirm.ts +126 -0
  232. package/src/runtime/confirm/codex-prompt-origin.ts +16 -0
  233. package/src/runtime/confirm/confirm-code.ts +33 -0
  234. package/src/runtime/confirm/confirm-gate.ts +65 -0
  235. package/src/runtime/confirm/confirm-irreversible.ts +32 -0
  236. package/src/runtime/confirm/confirm-pending.ts +34 -0
  237. package/src/runtime/confirm/confirm-state.ts +56 -0
  238. package/src/runtime/confirm/confirm-subagent.ts +112 -0
  239. package/src/runtime/confirm/confirm-submit.ts +38 -0
  240. package/src/runtime/core-guards-active.ts +84 -0
  241. package/src/runtime/core-guards-wired.ts +47 -0
  242. package/src/runtime/deny-loop-store.ts +94 -0
  243. package/src/runtime/deny-notice.ts +52 -0
  244. package/src/runtime/design-cache-resolve.ts +51 -0
  245. package/src/runtime/design-content-gate.ts +95 -0
  246. package/src/runtime/design-files-gate.ts +114 -0
  247. package/src/runtime/design-helpers.ts +93 -0
  248. package/src/runtime/design-lifecycle.ts +39 -0
  249. package/src/runtime/design-read-credit.ts +28 -0
  250. package/src/runtime/design.ts +116 -0
  251. package/src/runtime/dev-context.ts +43 -0
  252. package/src/runtime/dry-patterns.ts +32 -0
  253. package/src/runtime/dry.ts +96 -0
  254. package/src/runtime/fragment-registry.ts +33 -0
  255. package/src/runtime/framework-skill-gate.ts +79 -0
  256. package/src/runtime/fs-cleanup.ts +71 -0
  257. package/src/runtime/gate-apex.ts +81 -0
  258. package/src/runtime/gate-helpers.ts +52 -0
  259. package/src/runtime/gate-input.ts +26 -0
  260. package/src/runtime/gate.ts +128 -0
  261. package/src/runtime/handle-post.ts +104 -0
  262. package/src/runtime/handle-pre.ts +137 -0
  263. package/src/runtime/handle-scope-async.ts +29 -0
  264. package/src/runtime/handle-types.ts +27 -0
  265. package/src/runtime/handle.ts +116 -0
  266. package/src/runtime/home-state.ts +55 -0
  267. package/src/runtime/index.ts +18 -0
  268. package/src/runtime/inform.ts +28 -0
  269. package/src/runtime/inject-budget-recap.ts +47 -0
  270. package/src/runtime/inject-budget.ts +81 -0
  271. package/src/runtime/inject-context.ts +85 -0
  272. package/src/runtime/inject-dedup-exclusive.ts +98 -0
  273. package/src/runtime/inject-dedup.ts +89 -0
  274. package/src/runtime/is-agent-tool.ts +19 -0
  275. package/src/runtime/lifecycle/agent-files.ts +49 -0
  276. package/src/runtime/lifecycle/agent-memory.ts +82 -0
  277. package/src/runtime/lifecycle/agent-transcript.ts +80 -0
  278. package/src/runtime/lifecycle/aipilot/analytics.ts +99 -0
  279. package/src/runtime/lifecycle/aipilot/apex-task-store.ts +61 -0
  280. package/src/runtime/lifecycle/aipilot/cache-base.ts +48 -0
  281. package/src/runtime/lifecycle/aipilot/cache-doc.ts +100 -0
  282. package/src/runtime/lifecycle/aipilot/cache-lessons.ts +58 -0
  283. package/src/runtime/lifecycle/aipilot/cache-test.ts +70 -0
  284. package/src/runtime/lifecycle/aipilot/curate-lessons.ts +64 -0
  285. package/src/runtime/lifecycle/aipilot/dispatch-aipilot.ts +100 -0
  286. package/src/runtime/lifecycle/aipilot/doc-cache-gate.ts +69 -0
  287. package/src/runtime/lifecycle/aipilot/inject-apex.ts +98 -0
  288. package/src/runtime/lifecycle/aipilot/inject-doc.ts +62 -0
  289. package/src/runtime/lifecycle/aipilot/inject-explore.ts +64 -0
  290. package/src/runtime/lifecycle/aipilot/inject-lessons.ts +61 -0
  291. package/src/runtime/lifecycle/aipilot/inject-test.ts +48 -0
  292. package/src/runtime/lifecycle/aipilot/lesson-archive.ts +60 -0
  293. package/src/runtime/lifecycle/aipilot/lesson-inject.ts +98 -0
  294. package/src/runtime/lifecycle/aipilot/lesson-parse.ts +74 -0
  295. package/src/runtime/lifecycle/aipilot/lessons.ts +72 -0
  296. package/src/runtime/lifecycle/aipilot/promote-global-lessons.ts +92 -0
  297. package/src/runtime/lifecycle/aipilot/solid-compliance.ts +69 -0
  298. package/src/runtime/lifecycle/aipilot/solid-notice.ts +47 -0
  299. package/src/runtime/lifecycle/aipilot/solid-transcript.ts +60 -0
  300. package/src/runtime/lifecycle/aipilot/source-scan.ts +56 -0
  301. package/src/runtime/lifecycle/aipilot/sync-task.ts +75 -0
  302. package/src/runtime/lifecycle/aipilot/transcript.ts +88 -0
  303. package/src/runtime/lifecycle/aipilot/types.ts +72 -0
  304. package/src/runtime/lifecycle/auto-document-reads.ts +73 -0
  305. package/src/runtime/lifecycle/cartographer/detect.ts +57 -0
  306. package/src/runtime/lifecycle/cartographer/ecosystem-map.ts +56 -0
  307. package/src/runtime/lifecycle/cartographer/fs-util.ts +64 -0
  308. package/src/runtime/lifecycle/cartographer/merge.ts +51 -0
  309. package/src/runtime/lifecycle/cartographer/project-map.ts +49 -0
  310. package/src/runtime/lifecycle/cartographer/scan-hooks.ts +48 -0
  311. package/src/runtime/lifecycle/cartographer/scan.ts +81 -0
  312. package/src/runtime/lifecycle/cartographer/session-start.ts +33 -0
  313. package/src/runtime/lifecycle/cartographer/track-enrichment.ts +45 -0
  314. package/src/runtime/lifecycle/cartographer/write-plugin-map.ts +43 -0
  315. package/src/runtime/lifecycle/cartographer/write-tree.ts +44 -0
  316. package/src/runtime/lifecycle/changelog-research.ts +43 -0
  317. package/src/runtime/lifecycle/check-file-size.ts +61 -0
  318. package/src/runtime/lifecycle/codex-resync/fingerprint.ts +63 -0
  319. package/src/runtime/lifecycle/codex-resync/lock.ts +46 -0
  320. package/src/runtime/lifecycle/codex-resync/managed-destination.ts +39 -0
  321. package/src/runtime/lifecycle/codex-resync/materialize-agents.ts +57 -0
  322. package/src/runtime/lifecycle/codex-resync/plugin-files.ts +65 -0
  323. package/src/runtime/lifecycle/codex-resync/plugin-roots.ts +62 -0
  324. package/src/runtime/lifecycle/codex-resync/resync.ts +45 -0
  325. package/src/runtime/lifecycle/codex-resync/symlink-commands.ts +25 -0
  326. package/src/runtime/lifecycle/dispatch.ts +109 -0
  327. package/src/runtime/lifecycle/failure-lesson.ts +51 -0
  328. package/src/runtime/lifecycle/index.ts +34 -0
  329. package/src/runtime/lifecycle/inject-rules.ts +50 -0
  330. package/src/runtime/lifecycle/instructions-loaded.ts +22 -0
  331. package/src/runtime/lifecycle/kimi-rules-native.ts +44 -0
  332. package/src/runtime/lifecycle/lessons/dispatch.ts +88 -0
  333. package/src/runtime/lifecycle/lessons/reminder.ts +89 -0
  334. package/src/runtime/lifecycle/lessons/state.ts +23 -0
  335. package/src/runtime/lifecycle/memory/agent-lesson.ts +35 -0
  336. package/src/runtime/lifecycle/memory/capture-error.ts +41 -0
  337. package/src/runtime/lifecycle/memory/client.ts +65 -0
  338. package/src/runtime/lifecycle/memory/dispatch.ts +42 -0
  339. package/src/runtime/lifecycle/memory/recall.ts +30 -0
  340. package/src/runtime/lifecycle/memory/salience.ts +31 -0
  341. package/src/runtime/lifecycle/memory/state.ts +49 -0
  342. package/src/runtime/lifecycle/memory/track-ops.ts +15 -0
  343. package/src/runtime/lifecycle/post-compact.ts +38 -0
  344. package/src/runtime/lifecycle/post-edit-ts.ts +49 -0
  345. package/src/runtime/lifecycle/post-tracking.ts +40 -0
  346. package/src/runtime/lifecycle/pre-compact.ts +38 -0
  347. package/src/runtime/lifecycle/rules-root.ts +83 -0
  348. package/src/runtime/lifecycle/security/check-skill.ts +63 -0
  349. package/src/runtime/lifecycle/security/scan-patterns.ts +53 -0
  350. package/src/runtime/lifecycle/security/scan.ts +76 -0
  351. package/src/runtime/lifecycle/security/skill-state.ts +47 -0
  352. package/src/runtime/lifecycle/security/track-mcp.ts +29 -0
  353. package/src/runtime/lifecycle/security/track-skill-read.ts +31 -0
  354. package/src/runtime/lifecycle/seo/post-tool-use.ts +45 -0
  355. package/src/runtime/lifecycle/session-end.ts +17 -0
  356. package/src/runtime/lifecycle/session-start.ts +57 -0
  357. package/src/runtime/lifecycle/snapshot/board.ts +27 -0
  358. package/src/runtime/lifecycle/snapshot/format.ts +60 -0
  359. package/src/runtime/lifecycle/snapshot/git.ts +71 -0
  360. package/src/runtime/lifecycle/snapshot/index.ts +52 -0
  361. package/src/runtime/lifecycle/snapshot/version.ts +34 -0
  362. package/src/runtime/lifecycle/solid-detect.ts +56 -0
  363. package/src/runtime/lifecycle/stop-core.ts +35 -0
  364. package/src/runtime/lifecycle/subagent-cache.ts +61 -0
  365. package/src/runtime/lifecycle/task-completed.ts +94 -0
  366. package/src/runtime/lifecycle/teammate-idle-check.ts +63 -0
  367. package/src/runtime/lifecycle/teammate-idle.ts +28 -0
  368. package/src/runtime/lifecycle/tool-failure.ts +24 -0
  369. package/src/runtime/lifecycle/track-changes.ts +61 -0
  370. package/src/runtime/lifecycle/validate-solid.ts +70 -0
  371. package/src/runtime/lifecycle/validate-tailwind.ts +80 -0
  372. package/src/runtime/lifecycle-bridge.ts +74 -0
  373. package/src/runtime/mcp-key.ts +44 -0
  374. package/src/runtime/mcp-tool-name.ts +92 -0
  375. package/src/runtime/mcp.ts +91 -0
  376. package/src/runtime/modular.ts +77 -0
  377. package/src/runtime/normalize.ts +107 -0
  378. package/src/runtime/notices.ts +91 -0
  379. package/src/runtime/notification-sound.ts +49 -0
  380. package/src/runtime/notifications.ts +77 -0
  381. package/src/runtime/paths.ts +59 -0
  382. package/src/runtime/post-fanout.ts +41 -0
  383. package/src/runtime/post-outcome.ts +61 -0
  384. package/src/runtime/pre-allow.ts +97 -0
  385. package/src/runtime/precommit.ts +60 -0
  386. package/src/runtime/prompt-text.ts +38 -0
  387. package/src/runtime/receipt-capture.ts +34 -0
  388. package/src/runtime/record.ts +26 -0
  389. package/src/runtime/respond.ts +92 -0
  390. package/src/runtime/solid-file-size-gate.ts +58 -0
  391. package/src/runtime/solid-pre.ts +53 -0
  392. package/src/runtime/storage.ts +9 -0
  393. package/src/statusline/colors.ts +31 -0
  394. package/src/statusline/constants.ts +17 -0
  395. package/src/statusline/formatters.ts +54 -0
  396. package/src/statusline/index.ts +4 -0
  397. package/src/statusline/progress-bar.ts +46 -0
  398. package/src/tracking/codex-post-failure.ts +73 -0
  399. package/src/tracking/index.ts +2 -0
  400. package/src/tracking/integrity.ts +99 -0
  401. package/src/tracking/one-shot-dedup.ts +34 -0
  402. package/src/tracking/one-shot-failure.ts +42 -0
  403. package/src/tracking/one-shot-store.ts +95 -0
  404. package/src/tracking/one-shot.ts +93 -0
  405. package/src/tracking/receipts.ts +95 -0
  406. package/src/tracking/session-state.ts +94 -0
  407. package/src/tracking/store.ts +96 -0
  408. package/src/tracking/track-compact.ts +100 -0
  409. package/src/tracking/track-diff.ts +50 -0
  410. package/src/tracking/track-journal.ts +97 -0
  411. package/src/tracking/track-lock-sync.ts +83 -0
  412. package/src/tracking/track-lock.ts +67 -0
  413. package/src/util/compact-json.ts +23 -0
  414. package/src/util/index.ts +2 -0
  415. package/src/util/json-io.ts +47 -0
  416. package/src/util/project-root.ts +41 -0
  417. package/src/util/runtime-io.ts +85 -0
  418. package/src/util/semver.ts +30 -0
  419. package/dist/permission-mode-B0MUl7FT.mjs +0 -136
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Hermes Agent (NousResearch/hermes-agent) shell-hook wire types. Verified 2026
3
+ * against hermes-agent.nousresearch.com/docs/user-guide/features/hooks: hooks
4
+ * are configured in `~/.hermes/config.yaml` under `hooks:`, receive a JSON
5
+ * payload on stdin, and answer with JSON on stdout.
6
+ */
7
+
8
+ /** `pre_tool_call` stdin payload (subset). Same wire shape as Claude Code. */
9
+ export interface HermesHookInput {
10
+ hook_event_name?: string;
11
+ tool_name?: string;
12
+ tool_input?: { path?: string; file_path?: string; content?: string; command?: string | string[] };
13
+ session_id?: string;
14
+ cwd?: string;
15
+ }
16
+
17
+ /**
18
+ * Hook stdout response. `{}` allows; `{ decision: "block", reason }` cancels
19
+ * the tool call; `{ context }` injects LLM context (honored on `pre_llm_call`).
20
+ * Hermes has NO Claude-style `permissionDecision` and no interactive "ask".
21
+ */
22
+ export interface HermesResponse {
23
+ decision?: "block";
24
+ reason?: string;
25
+ context?: string;
26
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Kimi Code CLI adapter (hook-mode). Moonshot AI's Kimi Code CLI (v0.27.0,
3
+ * 2026) configures hooks in `~/.kimi-code/config.toml` (`[[hooks]]`: event,
4
+ * matcher, command, timeout — exactly 4 fields, any extra breaks config
5
+ * loading) and pipes a JSON payload to `command` on stdin, reading JSON back
6
+ * on stdout. Verified against kimi.com/code/docs/en/kimi-code-cli/
7
+ * customization/hooks.html (2026): the INPUT base shape
8
+ * (`hook_event_name`/`session_id`/`cwd`, `PreToolUse`/`Bash` carrying
9
+ * `tool_name` + `tool_input.command`) matches Claude Code's snake_case wire
10
+ * shape, so the Claude reader is reused — verified live against kimi-code
11
+ * v0.27.0 for both `PreToolUse`/`Bash` and `PreToolUse`/`Write`. Tool fields
12
+ * verified: `Bash` takes `command`; `Write` takes `path` + `content`; `Edit`
13
+ * takes `path` + `old_string`/`new_string` (per `docs/en/reference/tools.md`,
14
+ * moonshotai/kimi-code). The path key is `path`, NOT Claude's `file_path`
15
+ * (see interfaces/types.ts).
16
+ *
17
+ * OUTPUT does not match Claude: only `UserPromptSubmit`, `PreToolUse`, `Stop`
18
+ * are blockable, and the only documented decision is
19
+ * `{"hookSpecificOutput":{"permissionDecision":"deny","permissionDecisionReason"}}`
20
+ * (camelCase, despite the snake_case input — a real Kimi inconsistency). No
21
+ * `"ask"`/`"allow"`/`additionalContext` is documented. Verified live against
22
+ * kimi-code v0.27.0: that deny envelope on stdout at exit 0 blocks the call
23
+ * (exit 2 is NOT required) and `permissionDecisionReason` is re-injected to
24
+ * the model; empty stdout at exit 0 lets the call run. Exit 2 = block, stderr
25
+ * = reason; any other exit/timeout/crash fails open (allow). Whether raw-text
26
+ * stdout at exit 0 (the `inform` case) reaches the context is documented only
27
+ * as "may be" — unverified, needs live test.
28
+ */
29
+ import { evaluate } from "../../policy/evaluate";
30
+ import { formatPrompt, type Prompt } from "../../prompt/types";
31
+ import { readClaudeInput } from "../claude";
32
+ import { commandToString } from "../../runtime/command-string";
33
+ import type { KimiHookInput, KimiResponse } from "./interfaces/types";
34
+
35
+ export type { KimiHookInput, KimiResponse } from "./interfaces/types";
36
+
37
+ /**
38
+ * Downgrade prefix for `ask` — Kimi has no interactive approval channel
39
+ * reachable from a hook. Kimi DOES have an `ask` decision, but only via
40
+ * `[[permission.rules]] decision = "ask"` in config.toml, a separate system
41
+ * a hook's return value cannot invoke.
42
+ */
43
+ const ASK_PREFIX = "[downgraded from ask — Kimi Code has no interactive approval]";
44
+
45
+ /** Read & parse the Kimi hook stdin payload (same wire shape as Claude for the fields we consume). */
46
+ export async function readKimiInput(): Promise<KimiHookInput> {
47
+ return await readClaudeInput();
48
+ }
49
+
50
+ /**
51
+ * Render a portable {@link Prompt} as a Kimi hook response string:
52
+ * `block`/`ask` -> `{hookSpecificOutput:{permissionDecision:"deny",...}}`
53
+ * (only `"deny"` is documented; `ask` is downgraded with {@link ASK_PREFIX});
54
+ * `inform` -> raw text (Kimi appends stdout to context on exit 0 — no
55
+ * documented JSON envelope for non-blocking context injection).
56
+ * @param prompt - The portable policy prompt.
57
+ * @returns The native Kimi response string.
58
+ */
59
+ export function toKimiResponse(prompt: Prompt): string {
60
+ const msg = formatPrompt(prompt);
61
+ if (prompt.kind === "inform") return msg;
62
+ const reason = prompt.kind === "ask" ? `${ASK_PREFIX}\n${msg}` : msg;
63
+ const res: KimiResponse = { hookSpecificOutput: { permissionDecision: "deny", permissionDecisionReason: reason } };
64
+ return JSON.stringify(res);
65
+ }
66
+
67
+ /**
68
+ * Render a raw deny reason as a Kimi hook response — for callers that already
69
+ * hold a fully-formatted message (e.g. the MCP cache-hit notice) rather than a
70
+ * portable {@link Prompt}. Same envelope as {@link toKimiResponse}, minus the
71
+ * `[BLOCKED]` prompt framing and without `hookEventName` (undocumented upstream).
72
+ * @param reason - The fully-formatted deny message.
73
+ * @returns The native Kimi response string.
74
+ */
75
+ export function kimiDenyResponse(reason: string): string {
76
+ const res: KimiResponse = { hookSpecificOutput: { permissionDecision: "deny", permissionDecisionReason: reason } };
77
+ return JSON.stringify(res);
78
+ }
79
+
80
+ /**
81
+ * Run the bundled policy over a Kimi hook payload and return the native
82
+ * response string, or null to allow (the hook then exits 0 with empty stdout).
83
+ * @param input - Parsed hook stdin payload.
84
+ * @returns Native response string, or null when the tool use is allowed.
85
+ */
86
+ export function guard(input: KimiHookInput): string | null {
87
+ const t = input.tool_input;
88
+ const r = evaluate({
89
+ tool: input.tool_name ?? "Write",
90
+ filePath: t?.path ?? t?.file_path,
91
+ content: t?.content ?? t?.new_string,
92
+ command: commandToString(t?.command),
93
+ });
94
+ if (r.decision === "allow" || !r.prompt) return null;
95
+ return toKimiResponse(r.prompt);
96
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Kimi Code CLI shell-hook wire types. Verified 2026 against
3
+ * kimi.com/code/docs/en/kimi-code-cli/customization/hooks.html (v0.27.0):
4
+ * hooks are configured in `~/.kimi-code/config.toml` under `[[hooks]]`,
5
+ * receive a snake_case JSON payload on stdin, and answer with a camelCase
6
+ * JSON envelope on stdout (a real, documented casing inconsistency).
7
+ */
8
+
9
+ /**
10
+ * `PreToolUse`/`UserPromptSubmit`/`Stop` stdin payload (subset). Same base
11
+ * wire shape as Claude Code (`hook_event_name`/`tool_name`/`tool_input`/
12
+ * `cwd`); `tool_input.command` is confirmed for `Bash` by the docs' example
13
+ * script (`payload.tool_input?.command`). Verified live against kimi-code
14
+ * v0.27.0 on `PreToolUse`/`Bash`: `tool_name` is present at top level (the
15
+ * docs never name it as a stdin key) and `tool_input.command` carries the
16
+ * shell string. The live payload also carries an UNDOCUMENTED `tool_call_id`
17
+ * (e.g. `"tool_LsTnl0gBxM62XuWvKw0GFGHs"`); the harness does not consume it,
18
+ * so it is deliberately absent from this interface. `cwd` is the
19
+ * symlink-resolved path (macOS: `/private/tmp/…` for a shell in `/tmp/…`).
20
+ */
21
+ export interface KimiHookInput {
22
+ hook_event_name?: string;
23
+ tool_name?: string;
24
+ /**
25
+ * Verified against the official tool reference (`docs/en/reference/tools.md`,
26
+ * moonshotai/kimi-code) AND live against kimi-code v0.27.0: `Write` sends
27
+ * `{ path, content, mode? }`, `Edit` sends
28
+ * `{ path, old_string, new_string, replace_all? }` — the path key is `path`,
29
+ * NOT Claude's `file_path`. `file_path` is kept as a tolerated alias (read
30
+ * second) for Claude-shaped replay payloads, mirroring
31
+ * `runtime/normalize.ts` which resolves both on the CLI path.
32
+ */
33
+ tool_input?: {
34
+ path?: string;
35
+ file_path?: string;
36
+ content?: string;
37
+ new_string?: string;
38
+ command?: string | string[];
39
+ };
40
+ session_id?: string;
41
+ cwd?: string;
42
+ }
43
+
44
+ /**
45
+ * Hook stdout response. Only `"deny"` is documented for `permissionDecision`
46
+ * — no `"ask"`/`"allow"` value, and no `additionalContext` field exists on
47
+ * this envelope. Blocking events are `UserPromptSubmit`, `PreToolUse`, and
48
+ * `Stop` only; every other event's return value is ignored.
49
+ */
50
+ export interface KimiResponse {
51
+ hookSpecificOutput?: {
52
+ permissionDecision?: "deny";
53
+ permissionDecisionReason?: string;
54
+ };
55
+ }
@@ -0,0 +1,55 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ const HTML_ENTITIES: Record<string, string> = {
4
+ "&amp;": "&", "&lt;": "<", "&gt;": ">", "&quot;": '"',
5
+ "&#x27;": "'", "&#39;": "'", "&nbsp;": " ", "&apos;": "'",
6
+ };
7
+
8
+ const BOILERPLATE: RegExp[] = [
9
+ /^.*cookie.*(accept|consent|banner).*$/gim,
10
+ /^.*was this (helpful|page helpful|article helpful).*$/gim,
11
+ /^.*©\s*\d{4}.*all rights reserved.*$/gim,
12
+ /^\s*(home|about|contact|privacy|terms)\s*\|\s*.*$/gim,
13
+ /^.*subscribe to (our )?newsletter.*$/gim,
14
+ /^.*follow us on (twitter|facebook|linkedin).*$/gim,
15
+ ];
16
+
17
+ const MAX_BYTES = 5 * 1024;
18
+
19
+ function decodeEntities(text: string): string {
20
+ let out = text;
21
+ for (const [ent, ch] of Object.entries(HTML_ENTITIES)) out = out.split(ent).join(ch);
22
+ out = out.replace(/&#x([0-9a-fA-F]+);/g, (_m, h: string) => String.fromCodePoint(parseInt(h, 16)));
23
+ out = out.replace(/&#(\d+);/g, (_m, d: string) => String.fromCodePoint(parseInt(d, 10)));
24
+ return out;
25
+ }
26
+
27
+ /** Strip HTML entities + boilerplate, normalize blank lines, truncate to ~5KB. */
28
+ export function compactMarkdown(content: string): string {
29
+ let text = decodeEntities(content);
30
+ for (const re of BOILERPLATE) text = text.replace(re, "");
31
+ text = text.replace(/\n{3,}/g, "\n\n").trim();
32
+ const enc = new TextEncoder().encode(text);
33
+ if (enc.length > MAX_BYTES) {
34
+ const truncated = new TextDecoder().decode(enc.slice(0, MAX_BYTES));
35
+ const remaining = text.slice(truncated.length).split("\n").length - 1;
36
+ text = `${truncated}\n\n[... truncated, ${remaining} lines]`;
37
+ }
38
+ return text;
39
+ }
40
+
41
+ /** 8-char MD5 of `${toolName}::${query}`. */
42
+ export function queryHash(toolName: string, query: string): string {
43
+ return createHash("md5").update(`${toolName}::${query}`).digest("hex").slice(0, 8);
44
+ }
45
+
46
+ /** Bag-of-words Jaccard similarity strictly greater than `threshold`. */
47
+ export function jaccardSimilar(a: string, b: string, threshold = 0.8): boolean {
48
+ const ta = new Set(a.toLowerCase().split(/\s+/).filter(Boolean));
49
+ const tb = new Set(b.toLowerCase().split(/\s+/).filter(Boolean));
50
+ if (ta.size === 0 || tb.size === 0) return false;
51
+ let inter = 0;
52
+ for (const t of ta) if (tb.has(t)) inter++;
53
+ const union = ta.size + tb.size - inter;
54
+ return inter / union > threshold;
55
+ }
@@ -0,0 +1,5 @@
1
+ export * from "./compact";
2
+ export * from "./io";
3
+ export * from "./mcp-response";
4
+ export * from "./mcp-store";
5
+ export * from "./store";
@@ -0,0 +1,38 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+
3
+ /** Read a JSON array from `path`; [] on missing/corrupt/non-array. */
4
+ export function loadIndex(path: string): unknown[] {
5
+ try {
6
+ if (!existsSync(path)) return [];
7
+ const data: unknown = JSON.parse(readFileSync(path, "utf8"));
8
+ return Array.isArray(data) ? data : [];
9
+ } catch {
10
+ return [];
11
+ }
12
+ }
13
+
14
+ /** Summary of a cache index. */
15
+ export interface IndexSummary {
16
+ total: number;
17
+ byTool: Record<string, number>;
18
+ oldestTs: string | null;
19
+ newestTs: string | null;
20
+ }
21
+
22
+ /** Summarize an index of `{ tool?, ts? }` entries. */
23
+ export function summarizeIndex(index: unknown[]): IndexSummary {
24
+ const byTool: Record<string, number> = {};
25
+ const timestamps: string[] = [];
26
+ for (const entry of index) {
27
+ if (typeof entry !== "object" || entry === null) continue;
28
+ const e = entry as { tool?: unknown; ts?: unknown };
29
+ if (typeof e.tool === "string") byTool[e.tool] = (byTool[e.tool] ?? 0) + 1;
30
+ if (typeof e.ts === "string") timestamps.push(e.ts);
31
+ }
32
+ return {
33
+ total: index.length,
34
+ byTool,
35
+ oldestTs: timestamps.length ? timestamps.reduce((a, b) => (a < b ? a : b)) : null,
36
+ newestTs: timestamps.length ? timestamps.reduce((a, b) => (a > b ? a : b)) : null,
37
+ };
38
+ }
@@ -0,0 +1,39 @@
1
+ const MAX_DEPTH = 5;
2
+
3
+ interface TextBlock {
4
+ type?: string;
5
+ text?: string;
6
+ }
7
+
8
+ /**
9
+ * Extract usable markdown from an MCP `tool_response`: a string, a list of
10
+ * content blocks (non-text blocks skipped), or any JSON structure (fallback).
11
+ * Recurses up to depth 5 to guard against pathological/cyclic structures.
12
+ */
13
+ export function extractText(resp: unknown, depth = 0): string {
14
+ if (depth >= MAX_DEPTH) return "";
15
+ if (typeof resp === "string") return resp;
16
+ if (Array.isArray(resp)) {
17
+ const parts = resp
18
+ .filter((b): b is TextBlock => typeof b === "object" && b !== null)
19
+ .filter((b) => b.type === "text")
20
+ .map((b) => b.text ?? "");
21
+ if (parts.length) return parts.join("\n\n");
22
+ const nested = resp
23
+ .filter((b) => Array.isArray(b) || (typeof b === "object" && b !== null))
24
+ .map((b) => extractText(b, depth + 1));
25
+ const joined = nested.filter(Boolean).join("\n\n");
26
+ if (joined) return joined;
27
+ }
28
+ if (!resp) return "";
29
+ try {
30
+ // Stable key order (mirrors Python `json.dumps(sort_keys=True)`) so cache
31
+ // keys derived from this fallback text are deterministic across key orders.
32
+ return JSON.stringify(resp, (_k, v) =>
33
+ v !== null && typeof v === "object" && !Array.isArray(v)
34
+ ? Object.fromEntries(Object.entries(v as Record<string, unknown>).sort(([a], [b]) => a.localeCompare(b)))
35
+ : v);
36
+ } catch {
37
+ return "";
38
+ }
39
+ }
@@ -0,0 +1,70 @@
1
+ import { join } from "node:path";
2
+ import { atomicWrite } from "../util/json-io";
3
+ import { compactMarkdown, jaccardSimilar, queryHash } from "./compact";
4
+ import { loadIndex } from "./io";
5
+ import { cacheStore } from "./store";
6
+
7
+ /** A persisted MCP cache index entry. */
8
+ interface IndexEntry {
9
+ tool: string;
10
+ query: string;
11
+ hash: string;
12
+ ts: string;
13
+ }
14
+
15
+ /** UTC timestamp `YYYY-MM-DDTHH:MM:SSZ` (no millis), mirroring the Python store. */
16
+ function stamp(now: number): string {
17
+ return new Date(now).toISOString().replace(/\.\d{3}Z$/, "Z");
18
+ }
19
+
20
+ /** True when a Jaccard-similar query already exists for `tool` in `index`. */
21
+ function isDuplicate(index: unknown[], tool: string, query: string): boolean {
22
+ return index.some((e) => {
23
+ if (typeof e !== "object" || e === null) return false;
24
+ const r = e as { tool?: unknown; query?: unknown };
25
+ return r.tool === tool && typeof r.query === "string" && jaccardSimilar(r.query, query);
26
+ });
27
+ }
28
+
29
+ /**
30
+ * Persist an MCP doc result the way `cache-mcp-result.py` does: compact the body,
31
+ * skip Jaccard-duplicate queries, write a front-matter `.md` (exact FNV key, so
32
+ * {@link cacheLookup} still hits) carrying the query for substring lookups, and
33
+ * append the entry to `index.json`. No-op on empty body or duplicate query.
34
+ * @param dir - Cache directory (also holds `index.json`).
35
+ * @param tool - MCP tool id.
36
+ * @param query - Doc query that keys the entry.
37
+ * @param body - Raw response text (compacted before write).
38
+ * @param now - Current epoch ms (timestamp source).
39
+ */
40
+ export function mcpCacheWrite(dir: string, tool: string, query: string, body: string, now: number): void {
41
+ const compacted = compactMarkdown(body);
42
+ if (!compacted) return;
43
+ const indexPath = join(dir, "index.json");
44
+ const index = loadIndex(indexPath);
45
+ if (isDuplicate(index, tool, query)) return;
46
+ const hash = queryHash(tool, query);
47
+ const ts = stamp(now);
48
+ const front = `---\ntool: ${tool}\nquery: ${JSON.stringify(query)}\nts: ${ts}\nhash: ${hash}\n---\n\n`;
49
+ cacheStore(dir, tool, query, front + compacted);
50
+ index.push({ tool, query, hash, ts } satisfies IndexEntry);
51
+ atomicWrite(indexPath, JSON.stringify(index, null, 2));
52
+ }
53
+
54
+ /**
55
+ * Persist a WebFetch result the way `webfetch-cache-store.py` does: compact the
56
+ * body, write a front-matter `.md` keyed by `key` (url + prompt[:500]). No index
57
+ * and no dedup — WebFetch entries are exact-key only. No-op on empty body.
58
+ * @param dir - Cache directory.
59
+ * @param tool - Always `WebFetch`.
60
+ * @param key - The url+prompt cache key string.
61
+ * @param body - Raw response text (compacted before write).
62
+ * @param now - Current epoch ms (timestamp source).
63
+ */
64
+ export function webfetchCacheWrite(dir: string, tool: string, key: string, body: string, now: number): void {
65
+ const compacted = compactMarkdown(body);
66
+ if (!compacted) return;
67
+ const hash = queryHash(tool, key);
68
+ const front = `---\ntool: ${tool}\nkey: ${JSON.stringify(key)}\nts: ${stamp(now)}\nhash: ${hash}\n---\n\n`;
69
+ cacheStore(dir, tool, key, front + compacted);
70
+ }
@@ -0,0 +1,93 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+
4
+ /** Max body bytes read on a substring lookup (parity with mcp-cache-lookup.py MAX_BODY). */
5
+ const MAX_BODY = 8 * 1024;
6
+ /** Needle length scanned on a substring lookup (first 80 query chars, ripgrep-aligned). */
7
+ const NEEDLE_LEN = 80;
8
+
9
+ /** Stable 16-char key from a tool name + query (FNV-1a, non-crypto). */
10
+ export function mcpCacheKey(tool: string, query: string): string {
11
+ const s = `${tool}\n${query}`;
12
+ let h = 0x811c9dc5;
13
+ for (let i = 0; i < s.length; i++) {
14
+ h ^= s.charCodeAt(i);
15
+ h = Math.imul(h, 0x01000193);
16
+ }
17
+ return (h >>> 0).toString(16).padStart(8, "0") + (s.length >>> 0).toString(16).padStart(8, "0");
18
+ }
19
+
20
+ /** Path of a cache entry under `dir`. */
21
+ export function cachePath(dir: string, tool: string, query: string): string {
22
+ return join(dir, `${mcpCacheKey(tool, query)}.md`);
23
+ }
24
+
25
+ /** A cache hit's body plus its age in ms (now − mtime), for caller-side reporting. */
26
+ export interface CacheHit {
27
+ body: string;
28
+ ageMs: number;
29
+ }
30
+
31
+ /** Read a cached entry if it exists and is fresh (mtime within `ttlMs`), else null. */
32
+ export function cacheLookup(dir: string, tool: string, query: string, ttlMs: number, now: number): string | null {
33
+ return cacheLookupMeta(dir, tool, query, ttlMs, now)?.body ?? null;
34
+ }
35
+
36
+ /** Like {@link cacheLookup}, but also reports the entry's age (for `CACHE HIT` wrapper text). */
37
+ export function cacheLookupMeta(dir: string, tool: string, query: string, ttlMs: number, now: number): CacheHit | null {
38
+ const path = cachePath(dir, tool, query);
39
+ try {
40
+ if (!existsSync(path)) return null;
41
+ const ageMs = now - statSync(path).mtimeMs;
42
+ if (ageMs > ttlMs) return null;
43
+ return { body: readFileSync(path, "utf8").slice(0, MAX_BODY), ageMs };
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * First fresh cache file whose body contains the (normalized, case-insensitive)
51
+ * query substring, else null. Relaxes the exact-key match to lift the hit-rate —
52
+ * parity with `mcp-cache-lookup.py` (`rg -i -F` over the first 80 query chars).
53
+ * @param dir - Cache directory to scan.
54
+ * @param query - Raw query; newlines folded to spaces, truncated to 80 chars.
55
+ * @param ttlMs - Freshness window (mtime-based).
56
+ * @param now - Current epoch ms.
57
+ */
58
+ export function cacheLookupSubstring(dir: string, query: string, ttlMs: number, now: number): string | null {
59
+ return cacheLookupSubstringMeta(dir, query, ttlMs, now)?.body ?? null;
60
+ }
61
+
62
+ /** Like {@link cacheLookupSubstring}, but also reports the matched entry's age. */
63
+ export function cacheLookupSubstringMeta(dir: string, query: string, ttlMs: number, now: number): CacheHit | null {
64
+ const needle = query.replace(/[\r\n]+/g, " ").slice(0, NEEDLE_LEN).trim().toLowerCase();
65
+ if (!needle) return null;
66
+ let names: string[];
67
+ try {
68
+ names = readdirSync(dir);
69
+ } catch {
70
+ return null;
71
+ }
72
+ for (const name of names) {
73
+ if (!name.endsWith(".md")) continue;
74
+ const path = join(dir, name);
75
+ try {
76
+ const ageMs = now - statSync(path).mtimeMs;
77
+ if (ageMs > ttlMs) continue;
78
+ const body = readFileSync(path, "utf8").slice(0, MAX_BODY);
79
+ if (body.toLowerCase().includes(needle)) return { body, ageMs };
80
+ } catch {
81
+ continue;
82
+ }
83
+ }
84
+ return null;
85
+ }
86
+
87
+ /** Store a cache entry (creates the dir; no-op on empty content). */
88
+ export function cacheStore(dir: string, tool: string, query: string, content: string): void {
89
+ if (!content) return;
90
+ const path = cachePath(dir, tool, query);
91
+ mkdirSync(dirname(path), { recursive: true });
92
+ writeFileSync(path, content);
93
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Changelog scanner — ports the changelog-watcher plugin's `fetch-changelog`
3
+ * into the harness (exposed as the `harness changelog` CLI verb). Fetches the
4
+ * official Claude Code changelog, detects how many versions are new since the
5
+ * last check, persists per-day state, and returns a JSON summary. Dual-runtime:
6
+ * global `fetch` + `node:fs` (works under Node 20+ and Bun, no imports needed).
7
+ */
8
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
9
+ import { homedir } from "node:os";
10
+ import { join } from "node:path";
11
+ import { claudeHome } from "../runtime/home-state";
12
+ import { todayUtc } from "../runtime/lifecycle/security/skill-state";
13
+
14
+ const CHANGELOG_URL = "https://code.claude.com/docs/en/changelog.md";
15
+
16
+ /** Summary printed by the CLI (and consumed by the changelog-scan skill). */
17
+ export interface ChangelogScan {
18
+ latest: string;
19
+ new_since_last_check: number;
20
+ recent_versions: string[];
21
+ }
22
+
23
+ /** Persisted per-day state under `~/.claude/logs/00-changelog/`. */
24
+ interface ChangelogState { last_version: string; previous: string; new_versions: number; checked: string }
25
+
26
+ /**
27
+ * Parse up to 10 semver versions from the changelog, newest first. Matches the
28
+ * current docs format (`<Update label="X.Y.Z" …>` MDX blocks) AND the legacy
29
+ * markdown headers (`## vX.Y.Z` / `## X.Y.Z`) so it survives a format rollback.
30
+ */
31
+ export function parseVersions(md: string): string[] {
32
+ const re = /<Update\s+label="v?(\d+\.\d+\.\d+)"|^##\s+v?(\d+\.\d+\.\d+)/gm;
33
+ return [...md.matchAll(re)].map((m) => m[1] ?? m[2] ?? "").filter(Boolean).slice(0, 10);
34
+ }
35
+
36
+ /** Count versions newer than `lastKnown` (stops at the first match). */
37
+ export function countNew(versions: string[], lastKnown: string): number {
38
+ if (!lastKnown) return 0;
39
+ let n = 0;
40
+ for (const v of versions) { if (v === lastKnown) break; n++; }
41
+ return n;
42
+ }
43
+
44
+ /** Read the saved `last_version` for today's state file ("" when absent/corrupt). */
45
+ function lastKnownVersion(stateFile: string): string {
46
+ if (!existsSync(stateFile)) return "";
47
+ try {
48
+ return (JSON.parse(readFileSync(stateFile, "utf8")) as ChangelogState).last_version ?? "";
49
+ } catch {
50
+ return "";
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Fetch + parse the changelog, diff against the saved state, persist, and return
56
+ * the scan summary. Throws on network failure (the CLI maps it to exit 1).
57
+ * @param now - Clock (ms).
58
+ * @param home - Home dir.
59
+ */
60
+ export async function scanChangelog(now: number = Date.now(), home: string = homedir()): Promise<ChangelogScan> {
61
+ const res = await fetch(CHANGELOG_URL, { signal: AbortSignal.timeout(10_000) });
62
+ if (!res.ok) throw new Error(`changelog fetch failed: ${res.status}`);
63
+ const versions = parseVersions(await res.text());
64
+ const latest = versions[0] ?? "";
65
+
66
+ const dir = join(claudeHome(home), "logs", "00-changelog");
67
+ const today = todayUtc(now);
68
+ const stateFile = join(dir, `${today}-state.json`);
69
+ const lastKnown = lastKnownVersion(stateFile);
70
+ const newCount = countNew(versions, lastKnown);
71
+
72
+ try {
73
+ mkdirSync(dir, { recursive: true });
74
+ const state: ChangelogState = { last_version: latest, previous: lastKnown, new_versions: newCount, checked: today };
75
+ writeFileSync(stateFile, JSON.stringify(state, null, 2));
76
+ } catch { /* best effort — the summary is still returned */ }
77
+
78
+ return { latest, new_since_last_check: newCount, recent_versions: versions };
79
+ }
package/src/cli/bin.ts ADDED
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * harness — CLI for @fusengine/harness.
4
+ * harness check cli-mode: check staged files (pre-commit), exit non-zero on a violation
5
+ * harness init [id] write the wiring file for a harness (defaults to the detected one)
6
+ * harness hook <id> runtime: read a hook payload on stdin, route to the adapter, print the response
7
+ * harness changelog fetch + diff the Claude Code changelog, print a JSON summary (changelog-watcher)
8
+ * harness codex-rules generate a Codex execpolicy .rules (Starlark) file from security.ts; stdout or --out <path>
9
+ */
10
+ import { detectHarness, type HarnessId } from "../detect/harness";
11
+ import { initFor, writeInitFile } from "../init/run";
12
+ import { scanChangelog } from "../changelog/fetch";
13
+ import { runSecurityScan } from "../runtime/lifecycle/security/scan";
14
+ import { buildCodexRules } from "../codex-rules";
15
+ import { writeFileSync } from "node:fs";
16
+ import { handleHook } from "../runtime/handle";
17
+ import { resolveTtlSec } from "../config/ttl";
18
+ import { loadDotenv } from "../config/dotenv";
19
+ import { discoverRefs } from "../refs/discover";
20
+ import { homedir } from "node:os";
21
+ import { checkStaged, stagedContent, stagedFiles } from "./run";
22
+ import { runDoctor, runningVersion, versionBanner } from "./doctor";
23
+ import { parseScope } from "./scope";
24
+ import { isMalformedCursorStdin, isOversize, oversizeStdout, readStdin, traceHook } from "./hook-io";
25
+ import { maybePlaySound } from "./hook-sound";
26
+
27
+ const cmd = process.argv[2];
28
+
29
+ if (cmd === "--version" || cmd === "-v") {
30
+ process.stderr.write(versionBanner(import.meta.url) + "\n");
31
+ process.stdout.write(runningVersion(import.meta.url).version + "\n");
32
+ process.exit(0);
33
+ } else if (cmd === "doctor") {
34
+ process.stderr.write(versionBanner(import.meta.url) + "\n");
35
+ process.exit(await runDoctor(import.meta.url));
36
+ } else if (cmd === "hook") {
37
+ const id = process.argv[3] ?? detectHarness().id;
38
+ loadDotenv(id as HarnessId);
39
+ const scope = parseScope(process.argv[4]);
40
+ if (maybePlaySound(process.argv)) process.exit(0);
41
+ const marketplaces = (process.env.FUSE_HARNESS_MARKETPLACES ?? "fusengine-plugins").split(",").map((s) => s.trim()).filter(Boolean);
42
+ const refsDir = process.env.FUSE_HARNESS_REFS || discoverRefs(homedir(), process.cwd(), marketplaces) || undefined;
43
+ traceHook("args", { id, scope });
44
+ let outcome: Awaited<ReturnType<typeof handleHook>>;
45
+ try {
46
+ const stdin = await readStdin(id);
47
+ if (isOversize(stdin)) {
48
+ const stdout = oversizeStdout(id, stdin.head);
49
+ if (stdout) process.stdout.write(stdout);
50
+ process.exit(0);
51
+ }
52
+ if (isMalformedCursorStdin(stdin)) process.exit(1);
53
+ outcome = await handleHook(id, stdin, { now: Date.now(), cwd: process.cwd(), refsDir, windowMs: resolveTtlSec(process.env) * 1000, scope });
54
+ } catch (e) { traceHook("handleHook-threw", e instanceof Error ? `${e.message}\n${e.stack}` : String(e)); throw e; }
55
+ traceHook("outcome", { stdoutLength: outcome.stdout.length, exit: outcome.exit });
56
+ if (outcome.stdout) process.stdout.write(outcome.stdout);
57
+ process.exit(outcome.exit);
58
+ } else if (cmd === "init") {
59
+ const id = (process.argv[3] as HarnessId | undefined) ?? detectHarness().id;
60
+ const files = initFor(id);
61
+ if (!files) {
62
+ process.stderr.write(`harness: no hook integration for "${id}" — use \`harness check\` in a pre-commit step\n`);
63
+ process.exit(1);
64
+ }
65
+ const written = files.map((f) => writeInitFile(process.cwd(), f));
66
+ process.stdout.write(`harness: wired ${id} -> ${written.join(", ")}\n`);
67
+ process.exit(0);
68
+ } else if (cmd === "changelog") {
69
+ try {
70
+ process.stdout.write(JSON.stringify(await scanChangelog()) + "\n");
71
+ process.exit(0);
72
+ } catch (e) {
73
+ process.stdout.write(JSON.stringify({ status: "error", message: e instanceof Error ? e.message : "changelog fetch failed" }) + "\n");
74
+ process.exit(1);
75
+ }
76
+ } else if (cmd === "scan") {
77
+ const dir = process.argv[3] ?? process.cwd();
78
+ process.stdout.write(JSON.stringify(runSecurityScan(dir), null, 2) + "\n");
79
+ process.exit(0);
80
+ } else if (cmd === "codex-rules") {
81
+ const outIdx = process.argv.indexOf("--out");
82
+ const outPath = outIdx !== -1 ? process.argv[outIdx + 1] : undefined;
83
+ const rules = buildCodexRules();
84
+ if (outPath) {
85
+ writeFileSync(outPath, rules);
86
+ process.stderr.write(`harness: wrote Codex execpolicy rules -> ${outPath}\n`);
87
+ } else {
88
+ process.stdout.write(rules);
89
+ }
90
+ process.exit(0);
91
+ } else {
92
+ const files = stagedFiles();
93
+ if (files.length === 0) process.exit(0);
94
+ const violations = checkStaged(files, stagedContent);
95
+ if (violations.length > 0) {
96
+ process.stderr.write(`harness check: policy violations\n\n${violations.join("\n\n")}\n`);
97
+ process.exit(1);
98
+ }
99
+ process.exit(0);
100
+ }