@fusengine/harness 0.1.90 → 0.1.92

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (422) hide show
  1. package/dist/adapters/claude/index.d.mts +1 -1
  2. package/dist/adapters/claude/index.mjs +1 -1
  3. package/dist/adapters/cline/index.mjs +1 -1
  4. package/dist/adapters/codex/index.d.mts +2 -2
  5. package/dist/adapters/codex/index.mjs +3 -2
  6. package/dist/adapters/cursor/index.d.mts +15 -28
  7. package/dist/adapters/cursor/index.mjs +35 -46
  8. package/dist/adapters/gemini/index.mjs +1 -1
  9. package/dist/adapters/hermes/index.d.mts +1 -1
  10. package/dist/adapters/hermes/index.mjs +1 -1
  11. package/dist/adapters/kimi/index.d.mts +1 -1
  12. package/dist/adapters/kimi/index.mjs +1 -1
  13. package/dist/{claude-5bC8TDAz.mjs → claude-Ckv2_TgP.mjs} +3 -43
  14. package/dist/cli/bin.mjs +429 -16
  15. package/dist/cli/index.mjs +1 -1
  16. package/dist/command-string-CALMTnwN.mjs +42 -0
  17. package/dist/{evaluate-C1VSI5pb.mjs → evaluate-CMiIqHeH.mjs} +261 -13
  18. package/dist/{handle-CBaOcFZq.mjs → handle-BF1dZFjY.mjs} +1194 -144
  19. package/dist/{hermes-kP6NUQlG.mjs → hermes-B9-p_3IF.mjs} +3 -2
  20. package/dist/{index-DBgnIZn9.d.mts → index-B8PG82su.d.mts} +1 -1
  21. package/dist/{index-D4C2-IIc.d.mts → index-SmKYkk2N.d.mts} +1 -1
  22. package/dist/index.d.mts +2 -2
  23. package/dist/index.mjs +2 -2
  24. package/dist/init/index.d.mts +1 -1
  25. package/dist/init/index.mjs +1 -1
  26. package/dist/{kimi-D6X7b8wF.mjs → kimi-G2wcSh5-.mjs} +3 -2
  27. package/dist/normalize-Dy8g9Ybl.mjs +397 -0
  28. package/dist/policy/index.d.mts +1 -1
  29. package/dist/policy/index.mjs +2 -2
  30. package/dist/prompt/index.d.mts +1 -1
  31. package/dist/{run-CdIlUtjv.mjs → run-DkrzC0gb.mjs} +1 -1
  32. package/dist/{run-TAXNRSpD.mjs → run-GHAMqcOn.mjs} +26 -3
  33. package/dist/runtime/index.d.mts +37 -16
  34. package/dist/runtime/index.mjs +2 -2
  35. package/dist/{types-C0-igRKr.d.mts → types-Bh0jEF3_.d.mts} +2 -0
  36. package/dist/{validate-DZFtAbLP.mjs → validate-KjZ1X9tH.mjs} +1 -1
  37. package/package.json +3 -2
  38. package/src/adapters/claude/index.ts +92 -0
  39. package/src/adapters/cline/index.ts +36 -0
  40. package/src/adapters/codex/apply-patch.ts +94 -0
  41. package/src/adapters/codex/index.ts +84 -0
  42. package/src/adapters/codex/permission-mode.ts +16 -0
  43. package/src/adapters/cursor/context-budget.ts +144 -0
  44. package/src/adapters/cursor/context-limit.ts +115 -0
  45. package/src/adapters/cursor/context.ts +65 -0
  46. package/src/adapters/cursor/events.ts +38 -0
  47. package/src/adapters/cursor/index.ts +43 -0
  48. package/src/adapters/cursor/interfaces/context-budget.ts +25 -0
  49. package/src/adapters/cursor/interfaces/types.ts +88 -0
  50. package/src/adapters/cursor/native-response.ts +39 -0
  51. package/src/adapters/cursor/native-schemas.ts +161 -0
  52. package/src/adapters/cursor/normalize.ts +188 -0
  53. package/src/adapters/cursor/plugin-root.ts +103 -0
  54. package/src/adapters/cursor/respond.ts +170 -0
  55. package/src/adapters/gemini/index.ts +36 -0
  56. package/src/adapters/hermes/index.ts +55 -0
  57. package/src/adapters/hermes/interfaces/types.ts +26 -0
  58. package/src/adapters/kimi/index.ts +96 -0
  59. package/src/adapters/kimi/interfaces/types.ts +55 -0
  60. package/src/cache/compact.ts +55 -0
  61. package/src/cache/index.ts +5 -0
  62. package/src/cache/io.ts +38 -0
  63. package/src/cache/mcp-response.ts +39 -0
  64. package/src/cache/mcp-store.ts +70 -0
  65. package/src/cache/store.ts +93 -0
  66. package/src/changelog/fetch.ts +79 -0
  67. package/src/cli/bin.ts +100 -0
  68. package/src/cli/cursor-event-scanner.ts +175 -0
  69. package/src/cli/cursor-stdin-reader.ts +65 -0
  70. package/src/cli/doctor.ts +96 -0
  71. package/src/cli/hook-io.ts +130 -0
  72. package/src/cli/hook-sound.ts +49 -0
  73. package/src/cli/index.ts +1 -0
  74. package/src/cli/json-primitive-scanner.ts +83 -0
  75. package/src/cli/run.ts +36 -0
  76. package/src/cli/scope.ts +31 -0
  77. package/src/codex-rules/build.ts +63 -0
  78. package/src/codex-rules/index.ts +8 -0
  79. package/src/codex-rules/mapping.ts +42 -0
  80. package/src/codex-rules/rules/absolute-paths.ts +52 -0
  81. package/src/codex-rules/rules/ask.ts +44 -0
  82. package/src/codex-rules/rules/chmod-ask.ts +57 -0
  83. package/src/codex-rules/rules/dd.ts +23 -0
  84. package/src/codex-rules/rules/diskutil.ts +33 -0
  85. package/src/codex-rules/rules/mkfs.ts +42 -0
  86. package/src/codex-rules/rules/privilege.ts +49 -0
  87. package/src/codex-rules/rules/remote-fetch.ts +31 -0
  88. package/src/codex-rules/rules/rm-variants.ts +58 -0
  89. package/src/codex-rules/rules/single-commands.ts +57 -0
  90. package/src/codex-rules/skip-list.ts +25 -0
  91. package/src/codex-rules/starlark.ts +46 -0
  92. package/src/codex-rules/types.ts +36 -0
  93. package/src/config/dotenv.ts +55 -0
  94. package/src/config/env.ts +10 -0
  95. package/src/config/home-dir.ts +36 -0
  96. package/src/config/index.ts +5 -0
  97. package/src/config/layout.ts +52 -0
  98. package/src/config/limits.ts +56 -0
  99. package/src/config/ttl.ts +30 -0
  100. package/src/detect/harness.ts +73 -0
  101. package/src/detect/index.ts +1 -0
  102. package/src/detect/interfaces/types.ts +18 -0
  103. package/src/freshness/agent-evidence-record.ts +95 -0
  104. package/src/freshness/agent-evidence.ts +95 -0
  105. package/src/freshness/codex-spawn-evidence.ts +91 -0
  106. package/src/freshness/doc-helpers.ts +90 -0
  107. package/src/freshness/evidence-harvest-io.ts +72 -0
  108. package/src/freshness/evidence-harvest.ts +61 -0
  109. package/src/freshness/explore-tools.ts +95 -0
  110. package/src/freshness/index.ts +2 -0
  111. package/src/freshness/query-framework.ts +73 -0
  112. package/src/freshness/ref-evidence.ts +49 -0
  113. package/src/freshness/ref-journal.ts +98 -0
  114. package/src/freshness/trivial-edit-counter.ts +30 -0
  115. package/src/index.ts +11 -0
  116. package/src/init/index.ts +2 -0
  117. package/src/init/run.ts +32 -0
  118. package/src/init/templates.ts +75 -0
  119. package/src/memory/gitignore.ts +14 -0
  120. package/src/memory/index.ts +3 -0
  121. package/src/memory/registry.ts +39 -0
  122. package/src/memory/session-roots.ts +99 -0
  123. package/src/memory/state.ts +58 -0
  124. package/src/policy/apex-authorization.ts +100 -0
  125. package/src/policy/apex-gates.ts +91 -0
  126. package/src/policy/apex-target-steps.ts +73 -0
  127. package/src/policy/apex-target.ts +49 -0
  128. package/src/policy/apex-task-context.ts +81 -0
  129. package/src/policy/apex.ts +94 -0
  130. package/src/policy/cartographer/build-tree.ts +69 -0
  131. package/src/policy/cartographer/describe.ts +53 -0
  132. package/src/policy/cartographer/entry.ts +38 -0
  133. package/src/policy/cartographer/frontmatter.ts +48 -0
  134. package/src/policy/cartographer/indicators.ts +34 -0
  135. package/src/policy/claude-md-context.ts +66 -0
  136. package/src/policy/conventions/index.ts +11 -0
  137. package/src/policy/conventions/interfaces.ts +71 -0
  138. package/src/policy/conventions/langs.ts +53 -0
  139. package/src/policy/conventions/query.ts +49 -0
  140. package/src/policy/conventions/react-hooks.ts +18 -0
  141. package/src/policy/conventions/store-or-query.ts +21 -0
  142. package/src/policy/conventions/stores.ts +56 -0
  143. package/src/policy/conventions/strip.ts +87 -0
  144. package/src/policy/conventions/verdict.ts +38 -0
  145. package/src/policy/creation-intent.ts +11 -0
  146. package/src/policy/deny-loop.ts +88 -0
  147. package/src/policy/design/allowed-write.ts +28 -0
  148. package/src/policy/design/content-checks.ts +45 -0
  149. package/src/policy/design/corpus-fs.ts +21 -0
  150. package/src/policy/design/corpus-resolve-cache.ts +49 -0
  151. package/src/policy/design/corpus-resolve.ts +99 -0
  152. package/src/policy/design/corpus.ts +92 -0
  153. package/src/policy/design/design-system-rules.ts +45 -0
  154. package/src/policy/design/flag.ts +30 -0
  155. package/src/policy/design/gates-pipeline.ts +105 -0
  156. package/src/policy/design/gates.ts +87 -0
  157. package/src/policy/design/screenshot-tools.ts +24 -0
  158. package/src/policy/design/skill-evidence.ts +34 -0
  159. package/src/policy/design/skill-gate.ts +81 -0
  160. package/src/policy/design/skill-triggers.ts +51 -0
  161. package/src/policy/design/state.ts +92 -0
  162. package/src/policy/design/template-urls.ts +54 -0
  163. package/src/policy/design/transitions.ts +68 -0
  164. package/src/policy/detect-claude-md-project-type.ts +32 -0
  165. package/src/policy/detect-framework.ts +99 -0
  166. package/src/policy/detect-primitive-checks.ts +100 -0
  167. package/src/policy/detect-primitive-lib.ts +55 -0
  168. package/src/policy/detect-project.ts +99 -0
  169. package/src/policy/edit-outcome.ts +53 -0
  170. package/src/policy/evaluate.ts +81 -0
  171. package/src/policy/expert-agents.ts +60 -0
  172. package/src/policy/file-size-scope.ts +32 -0
  173. package/src/policy/file-size.ts +99 -0
  174. package/src/policy/framework-solid-exclude.ts +52 -0
  175. package/src/policy/framework-solid-extended.ts +93 -0
  176. package/src/policy/framework-solid-gates-systems.ts +99 -0
  177. package/src/policy/framework-solid-gates.ts +98 -0
  178. package/src/policy/framework-solid.ts +88 -0
  179. package/src/policy/gemini-mcp-gate.ts +83 -0
  180. package/src/policy/git-gates.ts +40 -0
  181. package/src/policy/guards/bash-command-anchor.ts +40 -0
  182. package/src/policy/guards/bash-write-patterns.ts +140 -0
  183. package/src/policy/guards/bash-write-redirects.ts +201 -0
  184. package/src/policy/guards/bash-write-safe-paths.ts +62 -0
  185. package/src/policy/guards/bash-write.ts +76 -0
  186. package/src/policy/guards/context.ts +12 -0
  187. package/src/policy/guards/index.ts +61 -0
  188. package/src/policy/guards/install.ts +22 -0
  189. package/src/policy/guards/interface-separation-ext.ts +41 -0
  190. package/src/policy/guards/interface-separation.ts +100 -0
  191. package/src/policy/guards/protected-path.ts +100 -0
  192. package/src/policy/guards/security.ts +97 -0
  193. package/src/policy/index.ts +19 -0
  194. package/src/policy/interfaces/types.ts +46 -0
  195. package/src/policy/js-gate-route.ts +51 -0
  196. package/src/policy/lessons/lesson-gate.ts +0 -0
  197. package/src/policy/lessons/trigger-index.ts +84 -0
  198. package/src/policy/lessons/types.ts +31 -0
  199. package/src/policy/module-layout.ts +31 -0
  200. package/src/policy/nearest-manifest.ts +75 -0
  201. package/src/policy/never-approval.ts +95 -0
  202. package/src/policy/patterns.ts +80 -0
  203. package/src/policy/seo/validate.ts +51 -0
  204. package/src/policy/shadcn-project.ts +22 -0
  205. package/src/policy/shadcn-skill-gate.ts +97 -0
  206. package/src/policy/shell-read-refs.ts +96 -0
  207. package/src/policy/skill-path.ts +46 -0
  208. package/src/policy/skill-patterns/laravel.ts +40 -0
  209. package/src/policy/skill-patterns/nextjs.ts +29 -0
  210. package/src/policy/skill-patterns/react.ts +27 -0
  211. package/src/policy/skill-patterns/shadcn-subskills.ts +33 -0
  212. package/src/policy/skill-patterns/shadcn.ts +66 -0
  213. package/src/policy/skill-patterns/swift.ts +30 -0
  214. package/src/policy/skill-patterns/tailwind.ts +40 -0
  215. package/src/policy/skill-source.ts +52 -0
  216. package/src/policy/skill-trigger-patterns.ts +33 -0
  217. package/src/policy/skill-triggers.ts +108 -0
  218. package/src/policy/tailwind-skill-gate.ts +63 -0
  219. package/src/policy/trivial-edits.ts +52 -0
  220. package/src/policy/verbosity.ts +39 -0
  221. package/src/prompt/index.ts +1 -0
  222. package/src/prompt/types.ts +36 -0
  223. package/src/refs/discover.ts +69 -0
  224. package/src/refs/frontmatter.ts +24 -0
  225. package/src/refs/index.ts +4 -0
  226. package/src/refs/loader.ts +59 -0
  227. package/src/refs/ref-key.ts +41 -0
  228. package/src/refs/router.ts +63 -0
  229. package/src/refs/types.ts +25 -0
  230. package/src/runtime/activity.ts +99 -0
  231. package/src/runtime/apply-patch-gate.ts +41 -0
  232. package/src/runtime/burst-window.ts +21 -0
  233. package/src/runtime/codex-shell-tool.ts +37 -0
  234. package/src/runtime/command-string.ts +37 -0
  235. package/src/runtime/confirm/codex-confirm.ts +126 -0
  236. package/src/runtime/confirm/codex-prompt-origin.ts +16 -0
  237. package/src/runtime/confirm/confirm-code.ts +33 -0
  238. package/src/runtime/confirm/confirm-gate.ts +65 -0
  239. package/src/runtime/confirm/confirm-irreversible.ts +32 -0
  240. package/src/runtime/confirm/confirm-pending.ts +34 -0
  241. package/src/runtime/confirm/confirm-state.ts +56 -0
  242. package/src/runtime/confirm/confirm-subagent.ts +112 -0
  243. package/src/runtime/confirm/confirm-submit.ts +38 -0
  244. package/src/runtime/core-guards-active.ts +84 -0
  245. package/src/runtime/core-guards-wired.ts +47 -0
  246. package/src/runtime/deny-loop-store.ts +94 -0
  247. package/src/runtime/deny-notice.ts +52 -0
  248. package/src/runtime/design-cache-resolve.ts +51 -0
  249. package/src/runtime/design-content-gate.ts +95 -0
  250. package/src/runtime/design-files-gate.ts +114 -0
  251. package/src/runtime/design-helpers.ts +93 -0
  252. package/src/runtime/design-lifecycle.ts +39 -0
  253. package/src/runtime/design-read-credit.ts +28 -0
  254. package/src/runtime/design.ts +116 -0
  255. package/src/runtime/dev-context.ts +43 -0
  256. package/src/runtime/dry-patterns.ts +32 -0
  257. package/src/runtime/dry.ts +96 -0
  258. package/src/runtime/fragment-registry.ts +33 -0
  259. package/src/runtime/framework-skill-gate.ts +79 -0
  260. package/src/runtime/fs-cleanup.ts +71 -0
  261. package/src/runtime/gate-apex.ts +81 -0
  262. package/src/runtime/gate-helpers.ts +52 -0
  263. package/src/runtime/gate-input.ts +26 -0
  264. package/src/runtime/gate.ts +128 -0
  265. package/src/runtime/handle-post.ts +104 -0
  266. package/src/runtime/handle-pre.ts +137 -0
  267. package/src/runtime/handle-scope-async.ts +29 -0
  268. package/src/runtime/handle-types.ts +27 -0
  269. package/src/runtime/handle.ts +197 -0
  270. package/src/runtime/home-state.ts +55 -0
  271. package/src/runtime/index.ts +18 -0
  272. package/src/runtime/inform.ts +28 -0
  273. package/src/runtime/inject-budget-recap.ts +47 -0
  274. package/src/runtime/inject-budget.ts +81 -0
  275. package/src/runtime/inject-context.ts +85 -0
  276. package/src/runtime/inject-dedup-exclusive.ts +98 -0
  277. package/src/runtime/inject-dedup.ts +89 -0
  278. package/src/runtime/is-agent-tool.ts +19 -0
  279. package/src/runtime/lifecycle/agent-files.ts +49 -0
  280. package/src/runtime/lifecycle/agent-memory.ts +82 -0
  281. package/src/runtime/lifecycle/agent-transcript.ts +80 -0
  282. package/src/runtime/lifecycle/aipilot/analytics.ts +99 -0
  283. package/src/runtime/lifecycle/aipilot/apex-task-store.ts +61 -0
  284. package/src/runtime/lifecycle/aipilot/cache-base.ts +48 -0
  285. package/src/runtime/lifecycle/aipilot/cache-doc.ts +100 -0
  286. package/src/runtime/lifecycle/aipilot/cache-lessons.ts +58 -0
  287. package/src/runtime/lifecycle/aipilot/cache-test.ts +70 -0
  288. package/src/runtime/lifecycle/aipilot/curate-lessons.ts +64 -0
  289. package/src/runtime/lifecycle/aipilot/dispatch-aipilot.ts +106 -0
  290. package/src/runtime/lifecycle/aipilot/doc-cache-gate.ts +69 -0
  291. package/src/runtime/lifecycle/aipilot/inject-apex.ts +98 -0
  292. package/src/runtime/lifecycle/aipilot/inject-doc.ts +62 -0
  293. package/src/runtime/lifecycle/aipilot/inject-explore.ts +64 -0
  294. package/src/runtime/lifecycle/aipilot/inject-lessons.ts +61 -0
  295. package/src/runtime/lifecycle/aipilot/inject-test.ts +48 -0
  296. package/src/runtime/lifecycle/aipilot/lesson-archive.ts +60 -0
  297. package/src/runtime/lifecycle/aipilot/lesson-inject.ts +98 -0
  298. package/src/runtime/lifecycle/aipilot/lesson-parse.ts +74 -0
  299. package/src/runtime/lifecycle/aipilot/lessons.ts +72 -0
  300. package/src/runtime/lifecycle/aipilot/promote-global-lessons.ts +92 -0
  301. package/src/runtime/lifecycle/aipilot/solid-compliance.ts +69 -0
  302. package/src/runtime/lifecycle/aipilot/solid-notice.ts +47 -0
  303. package/src/runtime/lifecycle/aipilot/solid-transcript.ts +60 -0
  304. package/src/runtime/lifecycle/aipilot/source-scan.ts +56 -0
  305. package/src/runtime/lifecycle/aipilot/sync-task.ts +75 -0
  306. package/src/runtime/lifecycle/aipilot/transcript.ts +88 -0
  307. package/src/runtime/lifecycle/aipilot/types.ts +72 -0
  308. package/src/runtime/lifecycle/auto-document-reads.ts +73 -0
  309. package/src/runtime/lifecycle/cartographer/detect.ts +57 -0
  310. package/src/runtime/lifecycle/cartographer/ecosystem-map.ts +56 -0
  311. package/src/runtime/lifecycle/cartographer/fs-util.ts +64 -0
  312. package/src/runtime/lifecycle/cartographer/merge.ts +51 -0
  313. package/src/runtime/lifecycle/cartographer/project-map.ts +49 -0
  314. package/src/runtime/lifecycle/cartographer/scan-hooks.ts +48 -0
  315. package/src/runtime/lifecycle/cartographer/scan.ts +81 -0
  316. package/src/runtime/lifecycle/cartographer/session-start.ts +33 -0
  317. package/src/runtime/lifecycle/cartographer/track-enrichment.ts +45 -0
  318. package/src/runtime/lifecycle/cartographer/write-plugin-map.ts +43 -0
  319. package/src/runtime/lifecycle/cartographer/write-tree.ts +44 -0
  320. package/src/runtime/lifecycle/changelog-research.ts +43 -0
  321. package/src/runtime/lifecycle/check-file-size.ts +61 -0
  322. package/src/runtime/lifecycle/codex-resync/fingerprint.ts +63 -0
  323. package/src/runtime/lifecycle/codex-resync/lock.ts +46 -0
  324. package/src/runtime/lifecycle/codex-resync/managed-destination.ts +39 -0
  325. package/src/runtime/lifecycle/codex-resync/materialize-agents.ts +57 -0
  326. package/src/runtime/lifecycle/codex-resync/plugin-files.ts +65 -0
  327. package/src/runtime/lifecycle/codex-resync/plugin-roots.ts +62 -0
  328. package/src/runtime/lifecycle/codex-resync/resync.ts +45 -0
  329. package/src/runtime/lifecycle/codex-resync/symlink-commands.ts +25 -0
  330. package/src/runtime/lifecycle/dispatch.ts +109 -0
  331. package/src/runtime/lifecycle/failure-lesson.ts +55 -0
  332. package/src/runtime/lifecycle/index.ts +34 -0
  333. package/src/runtime/lifecycle/inject-rules.ts +50 -0
  334. package/src/runtime/lifecycle/instructions-loaded.ts +22 -0
  335. package/src/runtime/lifecycle/kimi-rules-native.ts +44 -0
  336. package/src/runtime/lifecycle/lessons/dispatch.ts +88 -0
  337. package/src/runtime/lifecycle/lessons/reminder.ts +89 -0
  338. package/src/runtime/lifecycle/lessons/state.ts +23 -0
  339. package/src/runtime/lifecycle/memory/agent-lesson.ts +35 -0
  340. package/src/runtime/lifecycle/memory/capture-error.ts +41 -0
  341. package/src/runtime/lifecycle/memory/client.ts +65 -0
  342. package/src/runtime/lifecycle/memory/dispatch.ts +42 -0
  343. package/src/runtime/lifecycle/memory/recall.ts +30 -0
  344. package/src/runtime/lifecycle/memory/salience.ts +31 -0
  345. package/src/runtime/lifecycle/memory/state.ts +49 -0
  346. package/src/runtime/lifecycle/memory/track-ops.ts +15 -0
  347. package/src/runtime/lifecycle/post-compact.ts +38 -0
  348. package/src/runtime/lifecycle/post-edit-ts.ts +49 -0
  349. package/src/runtime/lifecycle/post-tracking.ts +40 -0
  350. package/src/runtime/lifecycle/pre-compact.ts +38 -0
  351. package/src/runtime/lifecycle/rules-root.ts +99 -0
  352. package/src/runtime/lifecycle/security/check-skill.ts +63 -0
  353. package/src/runtime/lifecycle/security/scan-patterns.ts +53 -0
  354. package/src/runtime/lifecycle/security/scan.ts +76 -0
  355. package/src/runtime/lifecycle/security/skill-state.ts +47 -0
  356. package/src/runtime/lifecycle/security/track-mcp.ts +29 -0
  357. package/src/runtime/lifecycle/security/track-skill-read.ts +31 -0
  358. package/src/runtime/lifecycle/seo/post-tool-use.ts +45 -0
  359. package/src/runtime/lifecycle/session-end.ts +17 -0
  360. package/src/runtime/lifecycle/session-start.ts +57 -0
  361. package/src/runtime/lifecycle/snapshot/board.ts +27 -0
  362. package/src/runtime/lifecycle/snapshot/format.ts +60 -0
  363. package/src/runtime/lifecycle/snapshot/git.ts +71 -0
  364. package/src/runtime/lifecycle/snapshot/index.ts +52 -0
  365. package/src/runtime/lifecycle/snapshot/version.ts +34 -0
  366. package/src/runtime/lifecycle/solid-detect.ts +56 -0
  367. package/src/runtime/lifecycle/stop-core.ts +35 -0
  368. package/src/runtime/lifecycle/subagent-cache.ts +61 -0
  369. package/src/runtime/lifecycle/task-completed.ts +94 -0
  370. package/src/runtime/lifecycle/teammate-idle-check.ts +63 -0
  371. package/src/runtime/lifecycle/teammate-idle.ts +28 -0
  372. package/src/runtime/lifecycle/tool-failure.ts +24 -0
  373. package/src/runtime/lifecycle/track-changes.ts +61 -0
  374. package/src/runtime/lifecycle/validate-solid.ts +70 -0
  375. package/src/runtime/lifecycle/validate-tailwind.ts +80 -0
  376. package/src/runtime/lifecycle-bridge.ts +74 -0
  377. package/src/runtime/mcp-key.ts +44 -0
  378. package/src/runtime/mcp-tool-name.ts +92 -0
  379. package/src/runtime/mcp.ts +91 -0
  380. package/src/runtime/modular.ts +77 -0
  381. package/src/runtime/normalize.ts +107 -0
  382. package/src/runtime/notices.ts +91 -0
  383. package/src/runtime/notification-sound.ts +49 -0
  384. package/src/runtime/notifications.ts +77 -0
  385. package/src/runtime/paths.ts +59 -0
  386. package/src/runtime/post-fanout.ts +41 -0
  387. package/src/runtime/post-outcome.ts +61 -0
  388. package/src/runtime/pre-allow.ts +97 -0
  389. package/src/runtime/precommit.ts +60 -0
  390. package/src/runtime/prompt-text.ts +38 -0
  391. package/src/runtime/receipt-capture.ts +34 -0
  392. package/src/runtime/record.ts +26 -0
  393. package/src/runtime/respond.ts +92 -0
  394. package/src/runtime/solid-file-size-gate.ts +58 -0
  395. package/src/runtime/solid-pre.ts +53 -0
  396. package/src/runtime/storage.ts +9 -0
  397. package/src/statusline/colors.ts +31 -0
  398. package/src/statusline/constants.ts +17 -0
  399. package/src/statusline/formatters.ts +54 -0
  400. package/src/statusline/index.ts +4 -0
  401. package/src/statusline/progress-bar.ts +46 -0
  402. package/src/tracking/codex-post-failure.ts +73 -0
  403. package/src/tracking/index.ts +2 -0
  404. package/src/tracking/integrity.ts +99 -0
  405. package/src/tracking/one-shot-dedup.ts +34 -0
  406. package/src/tracking/one-shot-failure.ts +42 -0
  407. package/src/tracking/one-shot-store.ts +95 -0
  408. package/src/tracking/one-shot.ts +93 -0
  409. package/src/tracking/receipts.ts +95 -0
  410. package/src/tracking/session-state.ts +94 -0
  411. package/src/tracking/store.ts +96 -0
  412. package/src/tracking/track-compact.ts +100 -0
  413. package/src/tracking/track-diff.ts +50 -0
  414. package/src/tracking/track-journal.ts +97 -0
  415. package/src/tracking/track-lock-sync.ts +83 -0
  416. package/src/tracking/track-lock.ts +67 -0
  417. package/src/util/compact-json.ts +23 -0
  418. package/src/util/index.ts +2 -0
  419. package/src/util/json-io.ts +47 -0
  420. package/src/util/project-root.ts +41 -0
  421. package/src/util/runtime-io.ts +85 -0
  422. package/src/util/semver.ts +30 -0
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; }
@@ -0,0 +1,65 @@
1
+ import { readSync } from "node:fs";
2
+ import { CURSOR_SCANNER_LIMITS, CursorEventScanner } from "./cursor-event-scanner";
3
+
4
+ const HEAD_BYTES = 4096;
5
+ const CHUNK_BYTES = 64 * 1024;
6
+
7
+ export type CursorStdinRead =
8
+ | { kind: "ok"; text: string }
9
+ | { kind: "oversize"; head: string };
10
+
11
+ /**
12
+ * Requested independent Buffer allocation/retention lengths and scanner cardinalities.
13
+ * Zero-copy aliases such as `subarray`, backing pools/ArrayBuffers, JS objects/strings,
14
+ * and RSS are excluded; this metric is not a physical or total-memory bound.
15
+ */
16
+ export function cursorReaderBounds(maxBytes: number): {
17
+ bufferAllocationRequestBytes: number;
18
+ scannerTokenEntries: number;
19
+ scannerFrames: number;
20
+ } {
21
+ return {
22
+ bufferAllocationRequestBytes: maxBytes + CHUNK_BYTES + HEAD_BYTES + CURSOR_SCANNER_LIMITS.tokenEntries,
23
+ scannerTokenEntries: CURSOR_SCANNER_LIMITS.tokenEntries,
24
+ scannerFrames: CURSOR_SCANNER_LIMITS.maxDepth,
25
+ };
26
+ }
27
+
28
+ /**
29
+ * Read Cursor stdin through EOF for valid top-level event classification.
30
+ * Cursor's host-configured execution timeout bounds an idle open pipe.
31
+ */
32
+ export function readCursorBounded(fd: number, maxBytes: number): CursorStdinRead {
33
+ const chunk = Buffer.alloc(CHUNK_BYTES);
34
+ const head = Buffer.alloc(HEAD_BYTES);
35
+ const retained = Buffer.alloc(maxBytes);
36
+ const scanner = new CursorEventScanner();
37
+ let headLength = 0;
38
+ let retainedLength = 0;
39
+ let total = 0;
40
+ let oversize = false;
41
+ for (;;) {
42
+ const length = readSync(fd, chunk, 0, CHUNK_BYTES, null);
43
+ if (length === 0) break;
44
+ const view = chunk.subarray(0, length);
45
+ scanner.write(view);
46
+ total += length;
47
+ if (headLength < HEAD_BYTES) {
48
+ const copied = Math.min(length, HEAD_BYTES - headLength);
49
+ view.copy(head, headLength, 0, copied);
50
+ headLength += copied;
51
+ }
52
+ if (!oversize) {
53
+ const copied = Math.min(length, Math.max(0, maxBytes - retainedLength));
54
+ if (copied > 0) view.copy(retained, retainedLength, 0, copied);
55
+ retainedLength += copied;
56
+ if (total > maxBytes) oversize = true;
57
+ }
58
+ }
59
+ if (!oversize) return { kind: "ok", text: retained.subarray(0, total).toString("utf8") };
60
+ const event = scanner.finish();
61
+ return {
62
+ kind: "oversize",
63
+ head: event ? JSON.stringify({ hook_event_name: event }) : head.subarray(0, headLength).toString("utf8"),
64
+ };
65
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * `harness doctor` — diagnose which `@fusengine/harness` is actually running.
3
+ *
4
+ * A confirmed, still-open bun bug (oven-sh/bun #5791; scoped-pkg behaviour
5
+ * reinforced by #32019/#32150) makes `bunx <pkg>` (unpinned) prefer a stale
6
+ * GLOBAL install over npm-latest, so a consumer can silently run an old harness
7
+ * after a publish. This command surfaces the truth: the resolved version +
8
+ * package path of the code executing right now, the runtime binary, and the
9
+ * latest version published on npm. It queries the registry over HTTP (not
10
+ * `npm view`, whose exit code is 0 even on an empty result — npm/cli#6408) and
11
+ * never throws: an offline environment yields `latest: null`, never a crash.
12
+ */
13
+ import { readFileSync } from "node:fs";
14
+ import { fileURLToPath } from "node:url";
15
+ import { dirname, join } from "node:path";
16
+
17
+ const PKG = "@fusengine/harness";
18
+
19
+ /** Diagnostic snapshot printed by `harness doctor`. */
20
+ export interface DoctorReport {
21
+ /** Version from the running `package.json` (`"unknown"` if not resolvable). */
22
+ running: string;
23
+ /** Absolute path to the running package root. */
24
+ packagePath: string;
25
+ /** Runtime binary executing this code (`process.execPath`). */
26
+ runtime: string;
27
+ /** Latest version on npm, or `null` when offline / unreachable. */
28
+ latest: string | null;
29
+ /** True when npm advertises a version different from the one running. */
30
+ stale: boolean;
31
+ }
32
+
33
+ /** Walk up from `startDir` for the `@fusengine/harness` `package.json`. */
34
+ function findPackage(startDir: string): { version: string; path: string } | null {
35
+ let dir = startDir;
36
+ for (let depth = 0; depth < 6; depth++) {
37
+ try {
38
+ const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")) as { name?: string; version?: string };
39
+ if (pkg.name === PKG) return { version: pkg.version ?? "unknown", path: dir };
40
+ } catch { /* not here — keep walking up */ }
41
+ const parent = dirname(dir);
42
+ if (parent === dir) break;
43
+ dir = parent;
44
+ }
45
+ return null;
46
+ }
47
+
48
+ /** Resolve the running version + package path (no network), from a module URL. */
49
+ export function runningVersion(moduleUrl: string): { version: string; path: string } {
50
+ const found = findPackage(dirname(fileURLToPath(moduleUrl)));
51
+ return { version: found?.version ?? "unknown", path: found?.path ?? "unknown" };
52
+ }
53
+
54
+ /** One-line `pkg vX.Y.Z` banner (stderr, only on explicit `--version`/`doctor` commands — never on `hook`, to avoid spamming automated invocations). */
55
+ export function versionBanner(moduleUrl: string): string {
56
+ return `${PKG} v${runningVersion(moduleUrl).version}`;
57
+ }
58
+
59
+ /** Latest published version via the npm registry HTTP API. `null` on any failure. */
60
+ export async function npmLatest(): Promise<string | null> {
61
+ try {
62
+ const res = await fetch(`https://registry.npmjs.org/${PKG}/latest`, { signal: AbortSignal.timeout(8000) });
63
+ if (!res.ok) return null;
64
+ const body = (await res.json()) as { version?: string };
65
+ return body.version ?? null;
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
70
+
71
+ /** Build the full diagnostic report for the module at `moduleUrl`. */
72
+ export async function buildDoctorReport(moduleUrl: string): Promise<DoctorReport> {
73
+ const { version, path } = runningVersion(moduleUrl);
74
+ const latest = await npmLatest();
75
+ return { running: version, packagePath: path, runtime: process.execPath, latest, stale: latest !== null && latest !== version };
76
+ }
77
+
78
+ /** Render a {@link DoctorReport} as human-readable stdout text. */
79
+ export function formatDoctor(r: DoctorReport): string {
80
+ const lines = [
81
+ `${PKG} doctor`,
82
+ ` running: ${r.running}`,
83
+ ` package: ${r.packagePath}`,
84
+ ` runtime: ${r.runtime}`,
85
+ ` npm latest: ${r.latest ?? "(unavailable — offline or unreachable)"}`,
86
+ ];
87
+ if (r.stale) lines.push(` ! stale — npm serves ${r.latest}. Pin "@fusengine/harness@${r.latest}" in hooks.json (see README).`);
88
+ else if (r.latest !== null) lines.push(` ok — running the latest published version.`);
89
+ return lines.join("\n");
90
+ }
91
+
92
+ /** Run `harness doctor`: print the diagnostic to stdout. Always resolves 0 (pure info). */
93
+ export async function runDoctor(moduleUrl: string): Promise<number> {
94
+ process.stdout.write(formatDoctor(await buildDoctorReport(moduleUrl)) + "\n");
95
+ return 0;
96
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Hook stdin reading + debug tracing for `harness hook` — extracted from
3
+ * `bin.ts` so the command dispatcher stays under the SOLID line ceiling.
4
+ * Tracing is stderr-only and active only when FUSE_HARNESS_DEBUG=1 AND
5
+ * CI=true (both set by test/sim/exec.ts; never in an interactive session).
6
+ *
7
+ * `resolveStdinMaxBytes` (default 16 MiB) caps retained payload content.
8
+ * The legacy reader may also retain its overflow chunk; Cursor additionally
9
+ * requests fixed scanner/chunk buffers while reading to EOF. Buffer alias views,
10
+ * JS objects/strings, and RSS are runtime-dependent, so this is not a physical
11
+ * memory guarantee. Unclassified oversized Cursor input fails closed.
12
+ */
13
+ import { readSync } from "node:fs";
14
+ import { resolveStdinMaxBytes } from "../config/limits";
15
+ import { respond } from "../runtime/respond";
16
+ import { cursorEventContract } from "../adapters/cursor/events";
17
+ import { readCursorBounded } from "./cursor-stdin-reader";
18
+ export { cursorReaderBounds, readCursorBounded } from "./cursor-stdin-reader";
19
+
20
+ const hookDebug = process.env.FUSE_HARNESS_DEBUG === "1" && process.env.CI === "true";
21
+
22
+ /** stderr-only trace, no-op outside the debug flag combination above. */
23
+ export function traceHook(label: string, data: unknown): void {
24
+ if (hookDebug) process.stderr.write(`[hook-debug] ${label}: ${typeof data === "string" ? data : JSON.stringify(data)}\n`);
25
+ }
26
+
27
+ /** Result of the bounded stdin read. */
28
+ export type StdinRead =
29
+ | { kind: "ok"; text: string }
30
+ | { kind: "oversize"; head: string };
31
+
32
+ const MALFORMED_STDIN: unique symbol = Symbol("cursor-malformed-stdin");
33
+ type MalformedStdin = { readonly [MALFORMED_STDIN]: true };
34
+
35
+ /** Type guard for the oversize variant (narrows the readStdin union). */
36
+ export function isOversize(x: unknown): x is { kind: "oversize"; head: string } {
37
+ return typeof x === "object" && x !== null && (x as { kind?: unknown }).kind === "oversize";
38
+ }
39
+
40
+ /** Identify Cursor JSON parse failures without accepting a forgeable payload field. */
41
+ export function isMalformedCursorStdin(x: unknown): x is MalformedStdin {
42
+ return typeof x === "object" && x !== null && MALFORMED_STDIN in x;
43
+ }
44
+
45
+ /** First 4 KiB are the fallback diagnostic probe when no event key is found. */
46
+ const HEAD_BYTES = 4096;
47
+ const CHUNK = 64 * 1024;
48
+ const CURSOR_MAX_STDIN_BYTES = 64 * 1024 * 1024;
49
+
50
+ /** Resolve and clamp Cursor's stdin cap while preserving other harness limits. */
51
+ export function resolveCursorStdinMaxBytes(env: Record<string, string | undefined> = process.env): number {
52
+ return Math.min(CURSOR_MAX_STDIN_BYTES, resolveStdinMaxBytes(env));
53
+ }
54
+
55
+ /**
56
+ * Read a file descriptor to EOF, bounded at `maxBytes` (+1 byte to detect
57
+ * the overflow). Injectable fd so tests never touch process stdin.
58
+ * @param fd - The descriptor to read (0 in production).
59
+ * @param maxBytes - Cap from {@link resolveStdinMaxBytes}.
60
+ */
61
+ export function readBounded(fd: number, maxBytes: number): StdinRead {
62
+ const buf = Buffer.alloc(CHUNK);
63
+ const parts: Buffer[] = [];
64
+ let total = 0;
65
+ for (;;) {
66
+ const n = readSync(fd, buf, 0, CHUNK, null);
67
+ if (n === 0) break;
68
+ total += n;
69
+ parts.push(Buffer.from(buf.subarray(0, n)));
70
+ if (total > maxBytes) {
71
+ return { kind: "oversize", head: Buffer.concat(parts).subarray(0, HEAD_BYTES).toString("utf8") };
72
+ }
73
+ }
74
+ traceHook("stdin-text-length", total);
75
+ return { kind: "ok", text: Buffer.concat(parts).toString("utf8") };
76
+ }
77
+
78
+ /** Read hook stdin; Cursor distinguishes malformed non-empty JSON from historical empty input. */
79
+ export async function readStdin(id?: string): Promise<Record<string, unknown> | StdinRead | MalformedStdin> {
80
+ const cap = id === "cursor" ? resolveCursorStdinMaxBytes() : resolveStdinMaxBytes();
81
+ const read = id === "cursor" ? readCursorBounded(0, cap) : readBounded(0, cap);
82
+ if (read.kind === "oversize") return read;
83
+ const text = read.text.trim();
84
+ if (!text) return {};
85
+ try {
86
+ const parsed: unknown = JSON.parse(text);
87
+ return typeof parsed === "object" && parsed !== null ? (parsed as Record<string, unknown>) : {};
88
+ } catch (e) {
89
+ traceHook("stdin-parse-error", e instanceof Error ? e.message : String(e));
90
+ return id === "cursor" ? { [MALFORMED_STDIN]: true } : {};
91
+ }
92
+ }
93
+
94
+ /** Blockable hook events (fail-closed on oversize); others are observation-only. */
95
+ const BLOCKABLE = new Set(["PreToolUse", "UserPromptSubmit", "Stop"]);
96
+
97
+ /**
98
+ * Native stdout for an oversize payload: a deny on blockable (or
99
+ * undeterminable) events — never an uninspected passthrough — and a neutral
100
+ * empty string on observation-only events (no crash, no noise).
101
+ * @param id - Harness id (selects the native deny shape via `respond`).
102
+ * @param head - The first bytes of the payload (event-name sniffing).
103
+ */
104
+ export function oversizeStdout(id: string, head: string): string {
105
+ const event = id === "cursor" ? probeEvent(head) : legacyProbeEvent(head);
106
+ const maxBytes = id === "cursor" ? resolveCursorStdinMaxBytes() : resolveStdinMaxBytes();
107
+ if (id === "cursor" && event) {
108
+ const contract = cursorEventContract(event);
109
+ if (!contract.known || !contract.blockable) return "{}";
110
+ }
111
+ if (id !== "cursor" && event && !BLOCKABLE.has(event)) return "";
112
+ return respond(id, {
113
+ kind: "block",
114
+ title: "Oversize hook payload",
115
+ reason: `stdin payload exceeds ${maxBytes} bytes — denied uninspected`,
116
+ }, id === "cursor" ? (event || "preToolUse") : "PreToolUse");
117
+ }
118
+
119
+ function legacyProbeEvent(head: string): string {
120
+ return /"hook_event_name"\s*:\s*"([^"]+)"/.exec(head)?.[1] ?? "";
121
+ }
122
+
123
+ function probeEvent(head: string): string {
124
+ try {
125
+ const parsed: unknown = JSON.parse(head);
126
+ if (typeof parsed !== "object" || parsed === null) return "";
127
+ const event = (parsed as Record<string, unknown>).hook_event_name;
128
+ return typeof event === "string" ? event : "";
129
+ } catch { return ""; }
130
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * @module hook-sound
3
+ * CLI short-circuit for `harness hook ... --sound <kind>` — play the embedded
4
+ * notification sound and exit, bypassing stdin/handleHook entirely. Lets
5
+ * plugin hooks.json entries call the harness directly instead of a native
6
+ * `afplay`. Harness-agnostic by design: no gate on harness id here — codex,
7
+ * cursor, and hermes hooks.json may call this flag exactly the same way.
8
+ * @packageDocumentation
9
+ */
10
+ import { notify, type SoundKind } from "../runtime/notifications";
11
+
12
+ /** Recognised sound kinds — anything else fails closed (returns `null`). */
13
+ const KINDS = new Set<SoundKind>(["stop", "permission", "human"]);
14
+
15
+ /**
16
+ * Parse a `--sound <kind>` or `--sound=<kind>` flag anywhere in `argv`.
17
+ * Returns the kind only when it is a recognised {@link SoundKind}; an absent
18
+ * flag or an unrecognised value both yield `null` (fail-closed). Pure — no
19
+ * side effects, so it is directly unit-testable.
20
+ * @param argv - Full argv array (e.g. `process.argv`).
21
+ */
22
+ export function soundArg(argv: string[]): SoundKind | null {
23
+ for (let i = 0; i < argv.length; i++) {
24
+ const arg = argv[i];
25
+ if (arg === "--sound") {
26
+ const val = argv[i + 1];
27
+ return val !== undefined && KINDS.has(val as SoundKind) ? (val as SoundKind) : null;
28
+ }
29
+ if (arg !== undefined && arg.startsWith("--sound=")) {
30
+ const val = arg.slice("--sound=".length);
31
+ return KINDS.has(val as SoundKind) ? (val as SoundKind) : null;
32
+ }
33
+ }
34
+ return null;
35
+ }
36
+
37
+ /**
38
+ * Play the sound requested by a `--sound <kind>` flag in `argv`, if present
39
+ * and valid. Fire-and-forget via {@link notify} (fail-open, respects
40
+ * `FUSE_HARNESS_SOUND=0`). Returns whether a sound was triggered, so the
41
+ * caller can short-circuit (exit 0) without ever reading stdin.
42
+ * @param argv - Full argv array (e.g. `process.argv`).
43
+ */
44
+ export function maybePlaySound(argv: string[]): boolean {
45
+ const kind = soundArg(argv);
46
+ if (!kind) return false;
47
+ notify(kind);
48
+ return true;
49
+ }
@@ -0,0 +1 @@
1
+ export * from "./run";