@fusengine/harness 0.1.89 → 0.1.91

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (419) hide show
  1. package/dist/adapters/claude/index.d.mts +1 -1
  2. package/dist/adapters/claude/index.mjs +1 -1
  3. package/dist/adapters/cline/index.mjs +1 -1
  4. package/dist/adapters/codex/index.d.mts +2 -2
  5. package/dist/adapters/codex/index.mjs +5 -4
  6. package/dist/adapters/cursor/index.d.mts +15 -28
  7. package/dist/adapters/cursor/index.mjs +35 -46
  8. package/dist/adapters/gemini/index.mjs +1 -1
  9. package/dist/adapters/hermes/index.d.mts +1 -1
  10. package/dist/adapters/hermes/index.mjs +1 -1
  11. package/dist/adapters/kimi/index.d.mts +1 -1
  12. package/dist/adapters/kimi/index.mjs +1 -1
  13. package/dist/{claude-5bC8TDAz.mjs → claude-Ckv2_TgP.mjs} +3 -43
  14. package/dist/cli/bin.mjs +429 -16
  15. package/dist/cli/index.mjs +1 -1
  16. package/dist/command-string-CALMTnwN.mjs +42 -0
  17. package/dist/{evaluate-C1VSI5pb.mjs → evaluate-CMiIqHeH.mjs} +261 -13
  18. package/dist/{handle-DHhukRdh.mjs → handle-C43gA-Pr.mjs} +737 -150
  19. package/dist/{hermes-kP6NUQlG.mjs → hermes-B9-p_3IF.mjs} +3 -2
  20. package/dist/{index-DBgnIZn9.d.mts → index-B8PG82su.d.mts} +1 -1
  21. package/dist/{index-D4C2-IIc.d.mts → index-SmKYkk2N.d.mts} +1 -1
  22. package/dist/index.d.mts +2 -2
  23. package/dist/index.mjs +2 -2
  24. package/dist/init/index.d.mts +1 -1
  25. package/dist/init/index.mjs +1 -1
  26. package/dist/{kimi-D6X7b8wF.mjs → kimi-G2wcSh5-.mjs} +3 -2
  27. package/dist/normalize-BjG6unTj.mjs +317 -0
  28. package/dist/permission-mode-B-SFyR0X.mjs +226 -0
  29. package/dist/policy/index.d.mts +1 -1
  30. package/dist/policy/index.mjs +2 -2
  31. package/dist/prompt/index.d.mts +1 -1
  32. package/dist/{run-CdIlUtjv.mjs → run-DkrzC0gb.mjs} +1 -1
  33. package/dist/{run-TAXNRSpD.mjs → run-GHAMqcOn.mjs} +26 -3
  34. package/dist/runtime/index.d.mts +27 -16
  35. package/dist/runtime/index.mjs +2 -2
  36. package/dist/{types-C0-igRKr.d.mts → types-Bh0jEF3_.d.mts} +2 -0
  37. package/dist/{validate-DZFtAbLP.mjs → validate-KjZ1X9tH.mjs} +1 -1
  38. package/package.json +3 -2
  39. package/src/adapters/claude/index.ts +92 -0
  40. package/src/adapters/cline/index.ts +36 -0
  41. package/src/adapters/codex/apply-patch.ts +94 -0
  42. package/src/adapters/codex/index.ts +84 -0
  43. package/src/adapters/codex/permission-mode.ts +16 -0
  44. package/src/adapters/cursor/context.ts +46 -0
  45. package/src/adapters/cursor/events.ts +38 -0
  46. package/src/adapters/cursor/index.ts +43 -0
  47. package/src/adapters/cursor/interfaces/types.ts +88 -0
  48. package/src/adapters/cursor/native-response.ts +158 -0
  49. package/src/adapters/cursor/normalize.ts +123 -0
  50. package/src/adapters/cursor/respond.ts +123 -0
  51. package/src/adapters/gemini/index.ts +36 -0
  52. package/src/adapters/hermes/index.ts +55 -0
  53. package/src/adapters/hermes/interfaces/types.ts +26 -0
  54. package/src/adapters/kimi/index.ts +96 -0
  55. package/src/adapters/kimi/interfaces/types.ts +55 -0
  56. package/src/cache/compact.ts +55 -0
  57. package/src/cache/index.ts +5 -0
  58. package/src/cache/io.ts +38 -0
  59. package/src/cache/mcp-response.ts +39 -0
  60. package/src/cache/mcp-store.ts +70 -0
  61. package/src/cache/store.ts +93 -0
  62. package/src/changelog/fetch.ts +79 -0
  63. package/src/cli/bin.ts +100 -0
  64. package/src/cli/cursor-event-scanner.ts +175 -0
  65. package/src/cli/cursor-stdin-reader.ts +65 -0
  66. package/src/cli/doctor.ts +96 -0
  67. package/src/cli/hook-io.ts +130 -0
  68. package/src/cli/hook-sound.ts +49 -0
  69. package/src/cli/index.ts +1 -0
  70. package/src/cli/json-primitive-scanner.ts +83 -0
  71. package/src/cli/run.ts +36 -0
  72. package/src/cli/scope.ts +31 -0
  73. package/src/codex-rules/build.ts +63 -0
  74. package/src/codex-rules/index.ts +8 -0
  75. package/src/codex-rules/mapping.ts +42 -0
  76. package/src/codex-rules/rules/absolute-paths.ts +52 -0
  77. package/src/codex-rules/rules/ask.ts +44 -0
  78. package/src/codex-rules/rules/chmod-ask.ts +57 -0
  79. package/src/codex-rules/rules/dd.ts +23 -0
  80. package/src/codex-rules/rules/diskutil.ts +33 -0
  81. package/src/codex-rules/rules/mkfs.ts +42 -0
  82. package/src/codex-rules/rules/privilege.ts +49 -0
  83. package/src/codex-rules/rules/remote-fetch.ts +31 -0
  84. package/src/codex-rules/rules/rm-variants.ts +58 -0
  85. package/src/codex-rules/rules/single-commands.ts +57 -0
  86. package/src/codex-rules/skip-list.ts +25 -0
  87. package/src/codex-rules/starlark.ts +46 -0
  88. package/src/codex-rules/types.ts +36 -0
  89. package/src/config/dotenv.ts +55 -0
  90. package/src/config/env.ts +10 -0
  91. package/src/config/home-dir.ts +36 -0
  92. package/src/config/index.ts +5 -0
  93. package/src/config/layout.ts +52 -0
  94. package/src/config/limits.ts +56 -0
  95. package/src/config/ttl.ts +30 -0
  96. package/src/detect/harness.ts +73 -0
  97. package/src/detect/index.ts +1 -0
  98. package/src/detect/interfaces/types.ts +18 -0
  99. package/src/freshness/agent-evidence-record.ts +95 -0
  100. package/src/freshness/agent-evidence.ts +95 -0
  101. package/src/freshness/codex-spawn-evidence.ts +91 -0
  102. package/src/freshness/doc-helpers.ts +90 -0
  103. package/src/freshness/evidence-harvest-io.ts +72 -0
  104. package/src/freshness/evidence-harvest.ts +61 -0
  105. package/src/freshness/explore-tools.ts +95 -0
  106. package/src/freshness/index.ts +2 -0
  107. package/src/freshness/query-framework.ts +73 -0
  108. package/src/freshness/ref-evidence.ts +49 -0
  109. package/src/freshness/ref-journal.ts +98 -0
  110. package/src/freshness/trivial-edit-counter.ts +30 -0
  111. package/src/index.ts +11 -0
  112. package/src/init/index.ts +2 -0
  113. package/src/init/run.ts +32 -0
  114. package/src/init/templates.ts +75 -0
  115. package/src/memory/gitignore.ts +14 -0
  116. package/src/memory/index.ts +3 -0
  117. package/src/memory/registry.ts +39 -0
  118. package/src/memory/session-roots.ts +99 -0
  119. package/src/memory/state.ts +58 -0
  120. package/src/policy/apex-authorization.ts +100 -0
  121. package/src/policy/apex-gates.ts +91 -0
  122. package/src/policy/apex-target-steps.ts +73 -0
  123. package/src/policy/apex-target.ts +49 -0
  124. package/src/policy/apex-task-context.ts +81 -0
  125. package/src/policy/apex.ts +94 -0
  126. package/src/policy/cartographer/build-tree.ts +69 -0
  127. package/src/policy/cartographer/describe.ts +53 -0
  128. package/src/policy/cartographer/entry.ts +38 -0
  129. package/src/policy/cartographer/frontmatter.ts +48 -0
  130. package/src/policy/cartographer/indicators.ts +34 -0
  131. package/src/policy/claude-md-context.ts +66 -0
  132. package/src/policy/conventions/index.ts +11 -0
  133. package/src/policy/conventions/interfaces.ts +71 -0
  134. package/src/policy/conventions/langs.ts +53 -0
  135. package/src/policy/conventions/query.ts +49 -0
  136. package/src/policy/conventions/react-hooks.ts +18 -0
  137. package/src/policy/conventions/store-or-query.ts +21 -0
  138. package/src/policy/conventions/stores.ts +56 -0
  139. package/src/policy/conventions/strip.ts +87 -0
  140. package/src/policy/conventions/verdict.ts +38 -0
  141. package/src/policy/creation-intent.ts +11 -0
  142. package/src/policy/deny-loop.ts +88 -0
  143. package/src/policy/design/allowed-write.ts +28 -0
  144. package/src/policy/design/content-checks.ts +45 -0
  145. package/src/policy/design/corpus-fs.ts +21 -0
  146. package/src/policy/design/corpus-resolve-cache.ts +49 -0
  147. package/src/policy/design/corpus-resolve.ts +99 -0
  148. package/src/policy/design/corpus.ts +92 -0
  149. package/src/policy/design/design-system-rules.ts +45 -0
  150. package/src/policy/design/flag.ts +30 -0
  151. package/src/policy/design/gates-pipeline.ts +105 -0
  152. package/src/policy/design/gates.ts +87 -0
  153. package/src/policy/design/screenshot-tools.ts +24 -0
  154. package/src/policy/design/skill-evidence.ts +34 -0
  155. package/src/policy/design/skill-gate.ts +81 -0
  156. package/src/policy/design/skill-triggers.ts +51 -0
  157. package/src/policy/design/state.ts +92 -0
  158. package/src/policy/design/template-urls.ts +54 -0
  159. package/src/policy/design/transitions.ts +68 -0
  160. package/src/policy/detect-claude-md-project-type.ts +32 -0
  161. package/src/policy/detect-framework.ts +99 -0
  162. package/src/policy/detect-primitive-checks.ts +100 -0
  163. package/src/policy/detect-primitive-lib.ts +55 -0
  164. package/src/policy/detect-project.ts +99 -0
  165. package/src/policy/edit-outcome.ts +53 -0
  166. package/src/policy/evaluate.ts +81 -0
  167. package/src/policy/expert-agents.ts +60 -0
  168. package/src/policy/file-size-scope.ts +32 -0
  169. package/src/policy/file-size.ts +99 -0
  170. package/src/policy/framework-solid-exclude.ts +52 -0
  171. package/src/policy/framework-solid-extended.ts +93 -0
  172. package/src/policy/framework-solid-gates-systems.ts +99 -0
  173. package/src/policy/framework-solid-gates.ts +98 -0
  174. package/src/policy/framework-solid.ts +88 -0
  175. package/src/policy/gemini-mcp-gate.ts +83 -0
  176. package/src/policy/git-gates.ts +40 -0
  177. package/src/policy/guards/bash-command-anchor.ts +40 -0
  178. package/src/policy/guards/bash-write-patterns.ts +140 -0
  179. package/src/policy/guards/bash-write-redirects.ts +201 -0
  180. package/src/policy/guards/bash-write-safe-paths.ts +62 -0
  181. package/src/policy/guards/bash-write.ts +76 -0
  182. package/src/policy/guards/context.ts +12 -0
  183. package/src/policy/guards/index.ts +61 -0
  184. package/src/policy/guards/install.ts +22 -0
  185. package/src/policy/guards/interface-separation-ext.ts +41 -0
  186. package/src/policy/guards/interface-separation.ts +100 -0
  187. package/src/policy/guards/protected-path.ts +100 -0
  188. package/src/policy/guards/security.ts +97 -0
  189. package/src/policy/index.ts +19 -0
  190. package/src/policy/interfaces/types.ts +46 -0
  191. package/src/policy/js-gate-route.ts +51 -0
  192. package/src/policy/lessons/lesson-gate.ts +0 -0
  193. package/src/policy/lessons/trigger-index.ts +84 -0
  194. package/src/policy/lessons/types.ts +31 -0
  195. package/src/policy/module-layout.ts +31 -0
  196. package/src/policy/nearest-manifest.ts +75 -0
  197. package/src/policy/never-approval.ts +95 -0
  198. package/src/policy/patterns.ts +80 -0
  199. package/src/policy/seo/validate.ts +51 -0
  200. package/src/policy/shadcn-project.ts +22 -0
  201. package/src/policy/shadcn-skill-gate.ts +97 -0
  202. package/src/policy/shell-read-refs.ts +96 -0
  203. package/src/policy/skill-path.ts +46 -0
  204. package/src/policy/skill-patterns/laravel.ts +40 -0
  205. package/src/policy/skill-patterns/nextjs.ts +29 -0
  206. package/src/policy/skill-patterns/react.ts +27 -0
  207. package/src/policy/skill-patterns/shadcn-subskills.ts +33 -0
  208. package/src/policy/skill-patterns/shadcn.ts +66 -0
  209. package/src/policy/skill-patterns/swift.ts +30 -0
  210. package/src/policy/skill-patterns/tailwind.ts +40 -0
  211. package/src/policy/skill-source.ts +52 -0
  212. package/src/policy/skill-trigger-patterns.ts +33 -0
  213. package/src/policy/skill-triggers.ts +108 -0
  214. package/src/policy/tailwind-skill-gate.ts +63 -0
  215. package/src/policy/trivial-edits.ts +52 -0
  216. package/src/policy/verbosity.ts +39 -0
  217. package/src/prompt/index.ts +1 -0
  218. package/src/prompt/types.ts +36 -0
  219. package/src/refs/discover.ts +69 -0
  220. package/src/refs/frontmatter.ts +24 -0
  221. package/src/refs/index.ts +4 -0
  222. package/src/refs/loader.ts +59 -0
  223. package/src/refs/ref-key.ts +41 -0
  224. package/src/refs/router.ts +63 -0
  225. package/src/refs/types.ts +25 -0
  226. package/src/runtime/activity.ts +99 -0
  227. package/src/runtime/apply-patch-gate.ts +41 -0
  228. package/src/runtime/burst-window.ts +21 -0
  229. package/src/runtime/codex-shell-tool.ts +37 -0
  230. package/src/runtime/command-string.ts +37 -0
  231. package/src/runtime/confirm/codex-confirm.ts +126 -0
  232. package/src/runtime/confirm/codex-prompt-origin.ts +16 -0
  233. package/src/runtime/confirm/confirm-code.ts +33 -0
  234. package/src/runtime/confirm/confirm-gate.ts +65 -0
  235. package/src/runtime/confirm/confirm-irreversible.ts +32 -0
  236. package/src/runtime/confirm/confirm-pending.ts +34 -0
  237. package/src/runtime/confirm/confirm-state.ts +56 -0
  238. package/src/runtime/confirm/confirm-subagent.ts +112 -0
  239. package/src/runtime/confirm/confirm-submit.ts +38 -0
  240. package/src/runtime/core-guards-active.ts +84 -0
  241. package/src/runtime/core-guards-wired.ts +47 -0
  242. package/src/runtime/deny-loop-store.ts +94 -0
  243. package/src/runtime/deny-notice.ts +52 -0
  244. package/src/runtime/design-cache-resolve.ts +51 -0
  245. package/src/runtime/design-content-gate.ts +95 -0
  246. package/src/runtime/design-files-gate.ts +114 -0
  247. package/src/runtime/design-helpers.ts +93 -0
  248. package/src/runtime/design-lifecycle.ts +39 -0
  249. package/src/runtime/design-read-credit.ts +28 -0
  250. package/src/runtime/design.ts +116 -0
  251. package/src/runtime/dev-context.ts +43 -0
  252. package/src/runtime/dry-patterns.ts +32 -0
  253. package/src/runtime/dry.ts +96 -0
  254. package/src/runtime/fragment-registry.ts +33 -0
  255. package/src/runtime/framework-skill-gate.ts +79 -0
  256. package/src/runtime/fs-cleanup.ts +71 -0
  257. package/src/runtime/gate-apex.ts +81 -0
  258. package/src/runtime/gate-helpers.ts +52 -0
  259. package/src/runtime/gate-input.ts +26 -0
  260. package/src/runtime/gate.ts +128 -0
  261. package/src/runtime/handle-post.ts +104 -0
  262. package/src/runtime/handle-pre.ts +137 -0
  263. package/src/runtime/handle-scope-async.ts +29 -0
  264. package/src/runtime/handle-types.ts +27 -0
  265. package/src/runtime/handle.ts +116 -0
  266. package/src/runtime/home-state.ts +55 -0
  267. package/src/runtime/index.ts +18 -0
  268. package/src/runtime/inform.ts +28 -0
  269. package/src/runtime/inject-budget-recap.ts +47 -0
  270. package/src/runtime/inject-budget.ts +81 -0
  271. package/src/runtime/inject-context.ts +85 -0
  272. package/src/runtime/inject-dedup-exclusive.ts +98 -0
  273. package/src/runtime/inject-dedup.ts +89 -0
  274. package/src/runtime/is-agent-tool.ts +19 -0
  275. package/src/runtime/lifecycle/agent-files.ts +49 -0
  276. package/src/runtime/lifecycle/agent-memory.ts +82 -0
  277. package/src/runtime/lifecycle/agent-transcript.ts +80 -0
  278. package/src/runtime/lifecycle/aipilot/analytics.ts +99 -0
  279. package/src/runtime/lifecycle/aipilot/apex-task-store.ts +61 -0
  280. package/src/runtime/lifecycle/aipilot/cache-base.ts +48 -0
  281. package/src/runtime/lifecycle/aipilot/cache-doc.ts +100 -0
  282. package/src/runtime/lifecycle/aipilot/cache-lessons.ts +58 -0
  283. package/src/runtime/lifecycle/aipilot/cache-test.ts +70 -0
  284. package/src/runtime/lifecycle/aipilot/curate-lessons.ts +64 -0
  285. package/src/runtime/lifecycle/aipilot/dispatch-aipilot.ts +100 -0
  286. package/src/runtime/lifecycle/aipilot/doc-cache-gate.ts +69 -0
  287. package/src/runtime/lifecycle/aipilot/inject-apex.ts +98 -0
  288. package/src/runtime/lifecycle/aipilot/inject-doc.ts +62 -0
  289. package/src/runtime/lifecycle/aipilot/inject-explore.ts +64 -0
  290. package/src/runtime/lifecycle/aipilot/inject-lessons.ts +61 -0
  291. package/src/runtime/lifecycle/aipilot/inject-test.ts +48 -0
  292. package/src/runtime/lifecycle/aipilot/lesson-archive.ts +60 -0
  293. package/src/runtime/lifecycle/aipilot/lesson-inject.ts +98 -0
  294. package/src/runtime/lifecycle/aipilot/lesson-parse.ts +74 -0
  295. package/src/runtime/lifecycle/aipilot/lessons.ts +72 -0
  296. package/src/runtime/lifecycle/aipilot/promote-global-lessons.ts +92 -0
  297. package/src/runtime/lifecycle/aipilot/solid-compliance.ts +69 -0
  298. package/src/runtime/lifecycle/aipilot/solid-notice.ts +47 -0
  299. package/src/runtime/lifecycle/aipilot/solid-transcript.ts +60 -0
  300. package/src/runtime/lifecycle/aipilot/source-scan.ts +56 -0
  301. package/src/runtime/lifecycle/aipilot/sync-task.ts +75 -0
  302. package/src/runtime/lifecycle/aipilot/transcript.ts +88 -0
  303. package/src/runtime/lifecycle/aipilot/types.ts +72 -0
  304. package/src/runtime/lifecycle/auto-document-reads.ts +73 -0
  305. package/src/runtime/lifecycle/cartographer/detect.ts +57 -0
  306. package/src/runtime/lifecycle/cartographer/ecosystem-map.ts +56 -0
  307. package/src/runtime/lifecycle/cartographer/fs-util.ts +64 -0
  308. package/src/runtime/lifecycle/cartographer/merge.ts +51 -0
  309. package/src/runtime/lifecycle/cartographer/project-map.ts +49 -0
  310. package/src/runtime/lifecycle/cartographer/scan-hooks.ts +48 -0
  311. package/src/runtime/lifecycle/cartographer/scan.ts +81 -0
  312. package/src/runtime/lifecycle/cartographer/session-start.ts +33 -0
  313. package/src/runtime/lifecycle/cartographer/track-enrichment.ts +45 -0
  314. package/src/runtime/lifecycle/cartographer/write-plugin-map.ts +43 -0
  315. package/src/runtime/lifecycle/cartographer/write-tree.ts +44 -0
  316. package/src/runtime/lifecycle/changelog-research.ts +43 -0
  317. package/src/runtime/lifecycle/check-file-size.ts +61 -0
  318. package/src/runtime/lifecycle/codex-resync/fingerprint.ts +63 -0
  319. package/src/runtime/lifecycle/codex-resync/lock.ts +46 -0
  320. package/src/runtime/lifecycle/codex-resync/managed-destination.ts +39 -0
  321. package/src/runtime/lifecycle/codex-resync/materialize-agents.ts +57 -0
  322. package/src/runtime/lifecycle/codex-resync/plugin-files.ts +65 -0
  323. package/src/runtime/lifecycle/codex-resync/plugin-roots.ts +62 -0
  324. package/src/runtime/lifecycle/codex-resync/resync.ts +45 -0
  325. package/src/runtime/lifecycle/codex-resync/symlink-commands.ts +25 -0
  326. package/src/runtime/lifecycle/dispatch.ts +109 -0
  327. package/src/runtime/lifecycle/failure-lesson.ts +51 -0
  328. package/src/runtime/lifecycle/index.ts +34 -0
  329. package/src/runtime/lifecycle/inject-rules.ts +50 -0
  330. package/src/runtime/lifecycle/instructions-loaded.ts +22 -0
  331. package/src/runtime/lifecycle/kimi-rules-native.ts +44 -0
  332. package/src/runtime/lifecycle/lessons/dispatch.ts +88 -0
  333. package/src/runtime/lifecycle/lessons/reminder.ts +89 -0
  334. package/src/runtime/lifecycle/lessons/state.ts +23 -0
  335. package/src/runtime/lifecycle/memory/agent-lesson.ts +35 -0
  336. package/src/runtime/lifecycle/memory/capture-error.ts +41 -0
  337. package/src/runtime/lifecycle/memory/client.ts +65 -0
  338. package/src/runtime/lifecycle/memory/dispatch.ts +42 -0
  339. package/src/runtime/lifecycle/memory/recall.ts +30 -0
  340. package/src/runtime/lifecycle/memory/salience.ts +31 -0
  341. package/src/runtime/lifecycle/memory/state.ts +49 -0
  342. package/src/runtime/lifecycle/memory/track-ops.ts +15 -0
  343. package/src/runtime/lifecycle/post-compact.ts +38 -0
  344. package/src/runtime/lifecycle/post-edit-ts.ts +49 -0
  345. package/src/runtime/lifecycle/post-tracking.ts +40 -0
  346. package/src/runtime/lifecycle/pre-compact.ts +38 -0
  347. package/src/runtime/lifecycle/rules-root.ts +83 -0
  348. package/src/runtime/lifecycle/security/check-skill.ts +63 -0
  349. package/src/runtime/lifecycle/security/scan-patterns.ts +53 -0
  350. package/src/runtime/lifecycle/security/scan.ts +76 -0
  351. package/src/runtime/lifecycle/security/skill-state.ts +47 -0
  352. package/src/runtime/lifecycle/security/track-mcp.ts +29 -0
  353. package/src/runtime/lifecycle/security/track-skill-read.ts +31 -0
  354. package/src/runtime/lifecycle/seo/post-tool-use.ts +45 -0
  355. package/src/runtime/lifecycle/session-end.ts +17 -0
  356. package/src/runtime/lifecycle/session-start.ts +57 -0
  357. package/src/runtime/lifecycle/snapshot/board.ts +27 -0
  358. package/src/runtime/lifecycle/snapshot/format.ts +60 -0
  359. package/src/runtime/lifecycle/snapshot/git.ts +71 -0
  360. package/src/runtime/lifecycle/snapshot/index.ts +52 -0
  361. package/src/runtime/lifecycle/snapshot/version.ts +34 -0
  362. package/src/runtime/lifecycle/solid-detect.ts +56 -0
  363. package/src/runtime/lifecycle/stop-core.ts +35 -0
  364. package/src/runtime/lifecycle/subagent-cache.ts +61 -0
  365. package/src/runtime/lifecycle/task-completed.ts +94 -0
  366. package/src/runtime/lifecycle/teammate-idle-check.ts +63 -0
  367. package/src/runtime/lifecycle/teammate-idle.ts +28 -0
  368. package/src/runtime/lifecycle/tool-failure.ts +24 -0
  369. package/src/runtime/lifecycle/track-changes.ts +61 -0
  370. package/src/runtime/lifecycle/validate-solid.ts +70 -0
  371. package/src/runtime/lifecycle/validate-tailwind.ts +80 -0
  372. package/src/runtime/lifecycle-bridge.ts +74 -0
  373. package/src/runtime/mcp-key.ts +44 -0
  374. package/src/runtime/mcp-tool-name.ts +92 -0
  375. package/src/runtime/mcp.ts +91 -0
  376. package/src/runtime/modular.ts +77 -0
  377. package/src/runtime/normalize.ts +107 -0
  378. package/src/runtime/notices.ts +91 -0
  379. package/src/runtime/notification-sound.ts +49 -0
  380. package/src/runtime/notifications.ts +77 -0
  381. package/src/runtime/paths.ts +59 -0
  382. package/src/runtime/post-fanout.ts +41 -0
  383. package/src/runtime/post-outcome.ts +61 -0
  384. package/src/runtime/pre-allow.ts +97 -0
  385. package/src/runtime/precommit.ts +60 -0
  386. package/src/runtime/prompt-text.ts +38 -0
  387. package/src/runtime/receipt-capture.ts +34 -0
  388. package/src/runtime/record.ts +26 -0
  389. package/src/runtime/respond.ts +92 -0
  390. package/src/runtime/solid-file-size-gate.ts +58 -0
  391. package/src/runtime/solid-pre.ts +53 -0
  392. package/src/runtime/storage.ts +9 -0
  393. package/src/statusline/colors.ts +31 -0
  394. package/src/statusline/constants.ts +17 -0
  395. package/src/statusline/formatters.ts +54 -0
  396. package/src/statusline/index.ts +4 -0
  397. package/src/statusline/progress-bar.ts +46 -0
  398. package/src/tracking/codex-post-failure.ts +73 -0
  399. package/src/tracking/index.ts +2 -0
  400. package/src/tracking/integrity.ts +99 -0
  401. package/src/tracking/one-shot-dedup.ts +34 -0
  402. package/src/tracking/one-shot-failure.ts +42 -0
  403. package/src/tracking/one-shot-store.ts +95 -0
  404. package/src/tracking/one-shot.ts +93 -0
  405. package/src/tracking/receipts.ts +95 -0
  406. package/src/tracking/session-state.ts +94 -0
  407. package/src/tracking/store.ts +96 -0
  408. package/src/tracking/track-compact.ts +100 -0
  409. package/src/tracking/track-diff.ts +50 -0
  410. package/src/tracking/track-journal.ts +97 -0
  411. package/src/tracking/track-lock-sync.ts +83 -0
  412. package/src/tracking/track-lock.ts +67 -0
  413. package/src/util/compact-json.ts +23 -0
  414. package/src/util/index.ts +2 -0
  415. package/src/util/json-io.ts +47 -0
  416. package/src/util/project-root.ts +41 -0
  417. package/src/util/runtime-io.ts +85 -0
  418. package/src/util/semver.ts +30 -0
  419. package/dist/permission-mode-B0MUl7FT.mjs +0 -136
@@ -0,0 +1,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";
@@ -0,0 +1,83 @@
1
+ type NumberState = "minus" | "zero" | "integer" | "dot" | "fraction" | "exponentMark" | "exponentSign" | "exponent";
2
+
3
+ /** Validate one JSON literal or number incrementally with constant state. */
4
+ export class JsonPrimitiveScanner {
5
+ private literal: "true" | "false" | "null" | undefined;
6
+ private literalIndex = 0;
7
+ private number: NumberState | undefined;
8
+ private valid = true;
9
+
10
+ constructor(firstByte: number) {
11
+ if (firstByte === 0x74) { this.literal = "true"; this.literalIndex = 1; }
12
+ else if (firstByte === 0x66) { this.literal = "false"; this.literalIndex = 1; }
13
+ else if (firstByte === 0x6e) { this.literal = "null"; this.literalIndex = 1; }
14
+ else if (firstByte === 0x2d) this.number = "minus";
15
+ else if (firstByte === 0x30) this.number = "zero";
16
+ else if (isOneToNine(firstByte)) this.number = "integer";
17
+ else this.valid = false;
18
+ }
19
+
20
+ /** Consume one non-delimiter byte and report whether the prefix stays valid. */
21
+ write(byte: number): boolean {
22
+ if (!this.valid) return false;
23
+ if (this.literal) {
24
+ this.valid = this.literal.charCodeAt(this.literalIndex) === byte;
25
+ this.literalIndex += 1;
26
+ return this.valid && this.literalIndex <= this.literal.length;
27
+ }
28
+ this.valid = this.writeNumber(byte);
29
+ return this.valid;
30
+ }
31
+
32
+ /** Return whether the accumulated primitive is complete JSON grammar. */
33
+ finish(): boolean {
34
+ if (!this.valid) return false;
35
+ if (this.literal) return this.literalIndex === this.literal.length;
36
+ return this.number === "zero" || this.number === "integer" || this.number === "fraction" || this.number === "exponent";
37
+ }
38
+
39
+ private writeNumber(byte: number): boolean {
40
+ if (this.number === "minus") return this.digitAfter(byte, "zero", "integer");
41
+ if (this.number === "zero") return this.afterInteger(byte, false);
42
+ if (this.number === "integer") {
43
+ if (isDigit(byte)) return true;
44
+ return this.afterInteger(byte, true);
45
+ }
46
+ if (this.number === "dot") {
47
+ if (!isDigit(byte)) return false;
48
+ this.number = "fraction";
49
+ return true;
50
+ }
51
+ if (this.number === "fraction") {
52
+ if (isDigit(byte)) return true;
53
+ return this.startExponent(byte);
54
+ }
55
+ if (this.number === "exponentMark") {
56
+ if (byte === 0x2b || byte === 0x2d) { this.number = "exponentSign"; return true; }
57
+ return this.digitAfter(byte, "exponent", "exponent");
58
+ }
59
+ if (this.number === "exponentSign") return this.digitAfter(byte, "exponent", "exponent");
60
+ return this.number === "exponent" && isDigit(byte);
61
+ }
62
+
63
+ private afterInteger(byte: number, digitAllowed: boolean): boolean {
64
+ if (digitAllowed && isDigit(byte)) return true;
65
+ if (byte === 0x2e) { this.number = "dot"; return true; }
66
+ return this.startExponent(byte);
67
+ }
68
+
69
+ private startExponent(byte: number): boolean {
70
+ if (byte !== 0x65 && byte !== 0x45) return false;
71
+ this.number = "exponentMark";
72
+ return true;
73
+ }
74
+
75
+ private digitAfter(byte: number, zero: NumberState, nonzero: NumberState): boolean {
76
+ if (byte === 0x30) { this.number = zero; return true; }
77
+ if (isOneToNine(byte)) { this.number = nonzero; return true; }
78
+ return false;
79
+ }
80
+ }
81
+
82
+ function isDigit(byte: number): boolean { return byte >= 0x30 && byte <= 0x39; }
83
+ function isOneToNine(byte: number): boolean { return byte >= 0x31 && byte <= 0x39; }
package/src/cli/run.ts ADDED
@@ -0,0 +1,36 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { evaluate } from "../policy/evaluate";
3
+ import { formatPrompt } from "../prompt/types";
4
+ import { isCodeFile } from "../util/project-root";
5
+
6
+ /** Staged files (Added/Copied/Modified/Renamed). Uses `node:child_process` (Bun shell can hang on `git show`). */
7
+ export function stagedFiles(): string[] {
8
+ const out = execFileSync("git", ["diff", "--cached", "--name-only", "--diff-filter=ACMR"], {
9
+ encoding: "utf8",
10
+ });
11
+ return out.trim().split("\n").filter(Boolean);
12
+ }
13
+
14
+ /**
15
+ * Read a file's staged (index) content — not the working-tree version.
16
+ * Uses `execFileSync` with an argv array (never a shell), so a staged
17
+ * filename containing shell metacharacters (backticks, `$(...)`, quotes)
18
+ * reaches `git show` as a single literal argument — see issue #87.
19
+ */
20
+ export function stagedContent(path: string): string {
21
+ return execFileSync("git", ["show", `:${path}`], { encoding: "utf8" });
22
+ }
23
+
24
+ /**
25
+ * Evaluate staged code files against the policy core; return violation blocks.
26
+ * `read` is injected (the real impl is {@link stagedContent}) so this is pure + testable.
27
+ */
28
+ export function checkStaged(files: string[], read: (path: string) => string): string[] {
29
+ const violations: string[] = [];
30
+ for (const file of files) {
31
+ if (!isCodeFile(file)) continue;
32
+ const r = evaluate({ tool: "Write", filePath: file, content: read(file) });
33
+ if (r.decision === "deny" && r.prompt) violations.push(`${file}\n${formatPrompt(r.prompt)}`);
34
+ }
35
+ return violations;
36
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * CLI scope argument parsing for `harness hook <id> [scope]`. The scope set is
3
+ * closed (plugin-scoped lifecycle behaviors); an unknown value historically
4
+ * fell back to `"core"` SILENTLY, which made a typo'd plugin name apply the
5
+ * core guards with no sign of the mistake. The fallback stays (wiring
6
+ * compatibility) but now warns on stderr — harnesses never parse stderr, so
7
+ * the wire contract is untouched.
8
+ */
9
+ import type { PluginScope } from "../runtime/lifecycle";
10
+
11
+ /** Closed set of plugin scopes accepted after `harness hook <id>`. */
12
+ const VALID_SCOPES = new Set<string>([
13
+ "solid", "rules", "carto", "security", "changelog",
14
+ "aipilot", "lessons", "seo", "memory", "tailwindcss",
15
+ ]);
16
+
17
+ /**
18
+ * Parse the optional scope argument.
19
+ * @param scopeArg - `process.argv[4]`, when present.
20
+ * @param err - Warning sink (defaults to stderr; injectable for tests).
21
+ * @returns The valid scope, or `"core"` (with a warning) otherwise.
22
+ */
23
+ export function parseScope(
24
+ scopeArg: string | undefined,
25
+ err: (msg: string) => void = (m) => process.stderr.write(m),
26
+ ): PluginScope {
27
+ if (scopeArg !== undefined && scopeArg !== "core" && !VALID_SCOPES.has(scopeArg)) {
28
+ err(`harness: unknown scope "${scopeArg}", falling back to "core"\n`);
29
+ }
30
+ return scopeArg !== undefined && VALID_SCOPES.has(scopeArg) ? (scopeArg as PluginScope) : "core";
31
+ }