@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,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
+ }
@@ -0,0 +1,175 @@
1
+ import { JsonPrimitiveScanner } from "./json-primitive-scanner";
2
+
3
+ /**
4
+ * Fixed cardinality bounds used by the incremental Cursor JSON scanner.
5
+ * `maxDepth` counts simultaneously open containers. Deeper valid JSON remains
6
+ * indeterminate and the oversized Cursor path fails closed.
7
+ */
8
+ export const CURSOR_SCANNER_LIMITS = { tokenEntries: 256, maxDepth: 1024 } as const;
9
+
10
+ type Frame =
11
+ | { kind: "object"; state: "keyOrEnd" | "key" | "colon" | "value" | "commaOrEnd"; key?: string }
12
+ | { kind: "array"; state: "valueOrEnd" | "value" | "commaOrEnd" };
13
+
14
+ /** Incrementally validates JSON and extracts the last top-level Cursor event name. */
15
+ export class CursorEventScanner {
16
+ private frames: Frame[] = [];
17
+ private mode: "normal" | "string" | "primitive" = "normal";
18
+ private token: number[] = [];
19
+ private tokenOverflow = false;
20
+ private primitive: JsonPrimitiveScanner | undefined;
21
+ private escaped = false;
22
+ private unicode = 0;
23
+ private rootStarted = false;
24
+ private rootComplete = false;
25
+ private invalid = false;
26
+ private event: string | undefined;
27
+
28
+ /** Consume another raw JSON byte chunk without retaining the payload. */
29
+ write(chunk: Uint8Array): void {
30
+ for (const byte of chunk) this.consume(byte);
31
+ }
32
+
33
+ /** Return the event only when the complete stream is valid JSON. */
34
+ finish(): string | undefined {
35
+ if (this.mode === "primitive") this.endPrimitive();
36
+ if (this.mode !== "normal" || this.frames.length > 0 || !this.rootComplete) this.invalid = true;
37
+ return this.invalid ? undefined : this.event;
38
+ }
39
+
40
+ private consume(byte: number): void {
41
+ if (this.invalid) return;
42
+ if (this.mode === "string") { this.stringByte(byte); return; }
43
+ if (this.mode === "primitive") {
44
+ if (!isDelimiter(byte)) {
45
+ if (!this.primitive?.write(byte)) this.invalid = true;
46
+ return;
47
+ }
48
+ this.endPrimitive();
49
+ if (this.invalid) return;
50
+ }
51
+ if (isWhitespace(byte)) return;
52
+ if (byte === 0x22) { this.startString(); return; }
53
+ if (byte === 0x7b || byte === 0x5b) { this.startContainer(byte === 0x7b ? "object" : "array"); return; }
54
+ if (byte === 0x7d || byte === 0x5d) { this.endContainer(byte === 0x7d ? "object" : "array"); return; }
55
+ if (byte === 0x3a) { this.colon(); return; }
56
+ if (byte === 0x2c) { this.comma(); return; }
57
+ if (isPrimitiveStart(byte)) { this.startPrimitive(byte); return; }
58
+ this.invalid = true;
59
+ }
60
+
61
+ private startString(): void {
62
+ this.mode = "string";
63
+ this.token = [0x22];
64
+ this.tokenOverflow = false;
65
+ this.escaped = false;
66
+ this.unicode = 0;
67
+ }
68
+
69
+ private startPrimitive(byte: number): void {
70
+ this.mode = "primitive";
71
+ this.primitive = new JsonPrimitiveScanner(byte);
72
+ }
73
+
74
+ private stringByte(byte: number): void {
75
+ this.pushToken(byte);
76
+ if (this.unicode > 0) {
77
+ if (!isHex(byte)) { this.invalid = true; return; }
78
+ this.unicode -= 1;
79
+ return;
80
+ }
81
+ if (this.escaped) {
82
+ if (byte === 0x75) this.unicode = 4;
83
+ else if (![0x22, 0x5c, 0x2f, 0x62, 0x66, 0x6e, 0x72, 0x74].includes(byte)) this.invalid = true;
84
+ this.escaped = false;
85
+ return;
86
+ }
87
+ if (byte === 0x5c) { this.escaped = true; return; }
88
+ if (byte < 0x20) { this.invalid = true; return; }
89
+ if (byte !== 0x22) return;
90
+ this.mode = "normal";
91
+ const value = this.tokenOverflow ? undefined : decodeString(this.token);
92
+ if (value === null) { this.invalid = true; return; }
93
+ this.acceptString(value);
94
+ }
95
+
96
+ private acceptString(value: string | undefined): void {
97
+ const frame = this.frames.at(-1);
98
+ if (frame?.kind === "object" && (frame.state === "key" || frame.state === "keyOrEnd")) {
99
+ frame.key = value;
100
+ frame.state = "colon";
101
+ return;
102
+ }
103
+ this.acceptValue("string", value);
104
+ }
105
+
106
+ private startContainer(kind: "object" | "array"): void {
107
+ this.acceptValue(kind);
108
+ if (this.invalid) return;
109
+ if (this.frames.length >= CURSOR_SCANNER_LIMITS.maxDepth) { this.invalid = true; return; }
110
+ this.frames.push(kind === "object" ? { kind, state: "keyOrEnd" } : { kind, state: "valueOrEnd" });
111
+ }
112
+
113
+ private endContainer(kind: "object" | "array"): void {
114
+ const frame = this.frames.at(-1);
115
+ const valid = frame?.kind === kind && (kind === "object"
116
+ ? frame.state === "keyOrEnd" || frame.state === "commaOrEnd"
117
+ : frame.state === "valueOrEnd" || frame.state === "commaOrEnd");
118
+ if (!valid) { this.invalid = true; return; }
119
+ this.frames.pop();
120
+ if (this.frames.length === 0) this.rootComplete = true;
121
+ }
122
+
123
+ private acceptValue(kind: "string" | "primitive" | "object" | "array", value?: string): void {
124
+ if (this.rootComplete) { this.invalid = true; return; }
125
+ const frame = this.frames.at(-1);
126
+ if (!frame) {
127
+ if (this.rootStarted) { this.invalid = true; return; }
128
+ this.rootStarted = true;
129
+ if (kind === "string" || kind === "primitive") this.rootComplete = true;
130
+ return;
131
+ }
132
+ const expected = frame.kind === "object" ? frame.state === "value" : frame.state === "value" || frame.state === "valueOrEnd";
133
+ if (!expected) { this.invalid = true; return; }
134
+ if (frame.kind === "object") {
135
+ if (this.frames.length === 1 && frame.key === "hook_event_name") this.event = kind === "string" ? value : undefined;
136
+ frame.key = undefined;
137
+ }
138
+ frame.state = "commaOrEnd";
139
+ }
140
+
141
+ private colon(): void {
142
+ const frame = this.frames.at(-1);
143
+ if (frame?.kind !== "object" || frame.state !== "colon") { this.invalid = true; return; }
144
+ frame.state = "value";
145
+ }
146
+
147
+ private comma(): void {
148
+ const frame = this.frames.at(-1);
149
+ if (!frame || frame.state !== "commaOrEnd") { this.invalid = true; return; }
150
+ frame.state = frame.kind === "object" ? "key" : "value";
151
+ }
152
+
153
+ private endPrimitive(): void {
154
+ const valid = this.primitive?.finish() === true;
155
+ this.mode = "normal";
156
+ this.primitive = undefined;
157
+ if (valid) this.acceptValue("primitive");
158
+ else this.invalid = true;
159
+ }
160
+
161
+ private pushToken(byte: number): void {
162
+ if (this.token.length < CURSOR_SCANNER_LIMITS.tokenEntries) this.token.push(byte);
163
+ else this.tokenOverflow = true;
164
+ }
165
+ }
166
+
167
+ function decodeString(bytes: number[]): string | null {
168
+ try { const value: unknown = JSON.parse(Buffer.from(bytes).toString("utf8")); return typeof value === "string" ? value : null; }
169
+ catch { return null; }
170
+ }
171
+
172
+ function isWhitespace(byte: number): boolean { return byte === 0x20 || byte === 0x09 || byte === 0x0a || byte === 0x0d; }
173
+ function isDelimiter(byte: number): boolean { return isWhitespace(byte) || [0x2c, 0x5d, 0x7d].includes(byte); }
174
+ function isPrimitiveStart(byte: number): boolean { return byte === 0x2d || byte === 0x74 || byte === 0x66 || byte === 0x6e || byte >= 0x30 && byte <= 0x39; }
175
+ function isHex(byte: number): boolean { return byte >= 0x30 && byte <= 0x39 || byte >= 0x41 && byte <= 0x46 || byte >= 0x61 && byte <= 0x66; }