@fusengine/harness 0.1.90 → 0.1.91

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (417) hide show
  1. package/dist/adapters/claude/index.d.mts +1 -1
  2. package/dist/adapters/claude/index.mjs +1 -1
  3. package/dist/adapters/cline/index.mjs +1 -1
  4. package/dist/adapters/codex/index.d.mts +2 -2
  5. package/dist/adapters/codex/index.mjs +3 -2
  6. package/dist/adapters/cursor/index.d.mts +15 -28
  7. package/dist/adapters/cursor/index.mjs +35 -46
  8. package/dist/adapters/gemini/index.mjs +1 -1
  9. package/dist/adapters/hermes/index.d.mts +1 -1
  10. package/dist/adapters/hermes/index.mjs +1 -1
  11. package/dist/adapters/kimi/index.d.mts +1 -1
  12. package/dist/adapters/kimi/index.mjs +1 -1
  13. package/dist/{claude-5bC8TDAz.mjs → claude-Ckv2_TgP.mjs} +3 -43
  14. package/dist/cli/bin.mjs +429 -16
  15. package/dist/cli/index.mjs +1 -1
  16. package/dist/command-string-CALMTnwN.mjs +42 -0
  17. package/dist/{evaluate-C1VSI5pb.mjs → evaluate-CMiIqHeH.mjs} +261 -13
  18. package/dist/{handle-CBaOcFZq.mjs → handle-C43gA-Pr.mjs} +701 -134
  19. package/dist/{hermes-kP6NUQlG.mjs → hermes-B9-p_3IF.mjs} +3 -2
  20. package/dist/{index-DBgnIZn9.d.mts → index-B8PG82su.d.mts} +1 -1
  21. package/dist/{index-D4C2-IIc.d.mts → index-SmKYkk2N.d.mts} +1 -1
  22. package/dist/index.d.mts +2 -2
  23. package/dist/index.mjs +2 -2
  24. package/dist/init/index.d.mts +1 -1
  25. package/dist/init/index.mjs +1 -1
  26. package/dist/{kimi-D6X7b8wF.mjs → kimi-G2wcSh5-.mjs} +3 -2
  27. package/dist/normalize-BjG6unTj.mjs +317 -0
  28. package/dist/policy/index.d.mts +1 -1
  29. package/dist/policy/index.mjs +2 -2
  30. package/dist/prompt/index.d.mts +1 -1
  31. package/dist/{run-CdIlUtjv.mjs → run-DkrzC0gb.mjs} +1 -1
  32. package/dist/{run-TAXNRSpD.mjs → run-GHAMqcOn.mjs} +26 -3
  33. package/dist/runtime/index.d.mts +27 -16
  34. package/dist/runtime/index.mjs +2 -2
  35. package/dist/{types-C0-igRKr.d.mts → types-Bh0jEF3_.d.mts} +2 -0
  36. package/dist/{validate-DZFtAbLP.mjs → validate-KjZ1X9tH.mjs} +1 -1
  37. package/package.json +3 -2
  38. package/src/adapters/claude/index.ts +92 -0
  39. package/src/adapters/cline/index.ts +36 -0
  40. package/src/adapters/codex/apply-patch.ts +94 -0
  41. package/src/adapters/codex/index.ts +84 -0
  42. package/src/adapters/codex/permission-mode.ts +16 -0
  43. package/src/adapters/cursor/context.ts +46 -0
  44. package/src/adapters/cursor/events.ts +38 -0
  45. package/src/adapters/cursor/index.ts +43 -0
  46. package/src/adapters/cursor/interfaces/types.ts +88 -0
  47. package/src/adapters/cursor/native-response.ts +158 -0
  48. package/src/adapters/cursor/normalize.ts +123 -0
  49. package/src/adapters/cursor/respond.ts +123 -0
  50. package/src/adapters/gemini/index.ts +36 -0
  51. package/src/adapters/hermes/index.ts +55 -0
  52. package/src/adapters/hermes/interfaces/types.ts +26 -0
  53. package/src/adapters/kimi/index.ts +96 -0
  54. package/src/adapters/kimi/interfaces/types.ts +55 -0
  55. package/src/cache/compact.ts +55 -0
  56. package/src/cache/index.ts +5 -0
  57. package/src/cache/io.ts +38 -0
  58. package/src/cache/mcp-response.ts +39 -0
  59. package/src/cache/mcp-store.ts +70 -0
  60. package/src/cache/store.ts +93 -0
  61. package/src/changelog/fetch.ts +79 -0
  62. package/src/cli/bin.ts +100 -0
  63. package/src/cli/cursor-event-scanner.ts +175 -0
  64. package/src/cli/cursor-stdin-reader.ts +65 -0
  65. package/src/cli/doctor.ts +96 -0
  66. package/src/cli/hook-io.ts +130 -0
  67. package/src/cli/hook-sound.ts +49 -0
  68. package/src/cli/index.ts +1 -0
  69. package/src/cli/json-primitive-scanner.ts +83 -0
  70. package/src/cli/run.ts +36 -0
  71. package/src/cli/scope.ts +31 -0
  72. package/src/codex-rules/build.ts +63 -0
  73. package/src/codex-rules/index.ts +8 -0
  74. package/src/codex-rules/mapping.ts +42 -0
  75. package/src/codex-rules/rules/absolute-paths.ts +52 -0
  76. package/src/codex-rules/rules/ask.ts +44 -0
  77. package/src/codex-rules/rules/chmod-ask.ts +57 -0
  78. package/src/codex-rules/rules/dd.ts +23 -0
  79. package/src/codex-rules/rules/diskutil.ts +33 -0
  80. package/src/codex-rules/rules/mkfs.ts +42 -0
  81. package/src/codex-rules/rules/privilege.ts +49 -0
  82. package/src/codex-rules/rules/remote-fetch.ts +31 -0
  83. package/src/codex-rules/rules/rm-variants.ts +58 -0
  84. package/src/codex-rules/rules/single-commands.ts +57 -0
  85. package/src/codex-rules/skip-list.ts +25 -0
  86. package/src/codex-rules/starlark.ts +46 -0
  87. package/src/codex-rules/types.ts +36 -0
  88. package/src/config/dotenv.ts +55 -0
  89. package/src/config/env.ts +10 -0
  90. package/src/config/home-dir.ts +36 -0
  91. package/src/config/index.ts +5 -0
  92. package/src/config/layout.ts +52 -0
  93. package/src/config/limits.ts +56 -0
  94. package/src/config/ttl.ts +30 -0
  95. package/src/detect/harness.ts +73 -0
  96. package/src/detect/index.ts +1 -0
  97. package/src/detect/interfaces/types.ts +18 -0
  98. package/src/freshness/agent-evidence-record.ts +95 -0
  99. package/src/freshness/agent-evidence.ts +95 -0
  100. package/src/freshness/codex-spawn-evidence.ts +91 -0
  101. package/src/freshness/doc-helpers.ts +90 -0
  102. package/src/freshness/evidence-harvest-io.ts +72 -0
  103. package/src/freshness/evidence-harvest.ts +61 -0
  104. package/src/freshness/explore-tools.ts +95 -0
  105. package/src/freshness/index.ts +2 -0
  106. package/src/freshness/query-framework.ts +73 -0
  107. package/src/freshness/ref-evidence.ts +49 -0
  108. package/src/freshness/ref-journal.ts +98 -0
  109. package/src/freshness/trivial-edit-counter.ts +30 -0
  110. package/src/index.ts +11 -0
  111. package/src/init/index.ts +2 -0
  112. package/src/init/run.ts +32 -0
  113. package/src/init/templates.ts +75 -0
  114. package/src/memory/gitignore.ts +14 -0
  115. package/src/memory/index.ts +3 -0
  116. package/src/memory/registry.ts +39 -0
  117. package/src/memory/session-roots.ts +99 -0
  118. package/src/memory/state.ts +58 -0
  119. package/src/policy/apex-authorization.ts +100 -0
  120. package/src/policy/apex-gates.ts +91 -0
  121. package/src/policy/apex-target-steps.ts +73 -0
  122. package/src/policy/apex-target.ts +49 -0
  123. package/src/policy/apex-task-context.ts +81 -0
  124. package/src/policy/apex.ts +94 -0
  125. package/src/policy/cartographer/build-tree.ts +69 -0
  126. package/src/policy/cartographer/describe.ts +53 -0
  127. package/src/policy/cartographer/entry.ts +38 -0
  128. package/src/policy/cartographer/frontmatter.ts +48 -0
  129. package/src/policy/cartographer/indicators.ts +34 -0
  130. package/src/policy/claude-md-context.ts +66 -0
  131. package/src/policy/conventions/index.ts +11 -0
  132. package/src/policy/conventions/interfaces.ts +71 -0
  133. package/src/policy/conventions/langs.ts +53 -0
  134. package/src/policy/conventions/query.ts +49 -0
  135. package/src/policy/conventions/react-hooks.ts +18 -0
  136. package/src/policy/conventions/store-or-query.ts +21 -0
  137. package/src/policy/conventions/stores.ts +56 -0
  138. package/src/policy/conventions/strip.ts +87 -0
  139. package/src/policy/conventions/verdict.ts +38 -0
  140. package/src/policy/creation-intent.ts +11 -0
  141. package/src/policy/deny-loop.ts +88 -0
  142. package/src/policy/design/allowed-write.ts +28 -0
  143. package/src/policy/design/content-checks.ts +45 -0
  144. package/src/policy/design/corpus-fs.ts +21 -0
  145. package/src/policy/design/corpus-resolve-cache.ts +49 -0
  146. package/src/policy/design/corpus-resolve.ts +99 -0
  147. package/src/policy/design/corpus.ts +92 -0
  148. package/src/policy/design/design-system-rules.ts +45 -0
  149. package/src/policy/design/flag.ts +30 -0
  150. package/src/policy/design/gates-pipeline.ts +105 -0
  151. package/src/policy/design/gates.ts +87 -0
  152. package/src/policy/design/screenshot-tools.ts +24 -0
  153. package/src/policy/design/skill-evidence.ts +34 -0
  154. package/src/policy/design/skill-gate.ts +81 -0
  155. package/src/policy/design/skill-triggers.ts +51 -0
  156. package/src/policy/design/state.ts +92 -0
  157. package/src/policy/design/template-urls.ts +54 -0
  158. package/src/policy/design/transitions.ts +68 -0
  159. package/src/policy/detect-claude-md-project-type.ts +32 -0
  160. package/src/policy/detect-framework.ts +99 -0
  161. package/src/policy/detect-primitive-checks.ts +100 -0
  162. package/src/policy/detect-primitive-lib.ts +55 -0
  163. package/src/policy/detect-project.ts +99 -0
  164. package/src/policy/edit-outcome.ts +53 -0
  165. package/src/policy/evaluate.ts +81 -0
  166. package/src/policy/expert-agents.ts +60 -0
  167. package/src/policy/file-size-scope.ts +32 -0
  168. package/src/policy/file-size.ts +99 -0
  169. package/src/policy/framework-solid-exclude.ts +52 -0
  170. package/src/policy/framework-solid-extended.ts +93 -0
  171. package/src/policy/framework-solid-gates-systems.ts +99 -0
  172. package/src/policy/framework-solid-gates.ts +98 -0
  173. package/src/policy/framework-solid.ts +88 -0
  174. package/src/policy/gemini-mcp-gate.ts +83 -0
  175. package/src/policy/git-gates.ts +40 -0
  176. package/src/policy/guards/bash-command-anchor.ts +40 -0
  177. package/src/policy/guards/bash-write-patterns.ts +140 -0
  178. package/src/policy/guards/bash-write-redirects.ts +201 -0
  179. package/src/policy/guards/bash-write-safe-paths.ts +62 -0
  180. package/src/policy/guards/bash-write.ts +76 -0
  181. package/src/policy/guards/context.ts +12 -0
  182. package/src/policy/guards/index.ts +61 -0
  183. package/src/policy/guards/install.ts +22 -0
  184. package/src/policy/guards/interface-separation-ext.ts +41 -0
  185. package/src/policy/guards/interface-separation.ts +100 -0
  186. package/src/policy/guards/protected-path.ts +100 -0
  187. package/src/policy/guards/security.ts +97 -0
  188. package/src/policy/index.ts +19 -0
  189. package/src/policy/interfaces/types.ts +46 -0
  190. package/src/policy/js-gate-route.ts +51 -0
  191. package/src/policy/lessons/lesson-gate.ts +0 -0
  192. package/src/policy/lessons/trigger-index.ts +84 -0
  193. package/src/policy/lessons/types.ts +31 -0
  194. package/src/policy/module-layout.ts +31 -0
  195. package/src/policy/nearest-manifest.ts +75 -0
  196. package/src/policy/never-approval.ts +95 -0
  197. package/src/policy/patterns.ts +80 -0
  198. package/src/policy/seo/validate.ts +51 -0
  199. package/src/policy/shadcn-project.ts +22 -0
  200. package/src/policy/shadcn-skill-gate.ts +97 -0
  201. package/src/policy/shell-read-refs.ts +96 -0
  202. package/src/policy/skill-path.ts +46 -0
  203. package/src/policy/skill-patterns/laravel.ts +40 -0
  204. package/src/policy/skill-patterns/nextjs.ts +29 -0
  205. package/src/policy/skill-patterns/react.ts +27 -0
  206. package/src/policy/skill-patterns/shadcn-subskills.ts +33 -0
  207. package/src/policy/skill-patterns/shadcn.ts +66 -0
  208. package/src/policy/skill-patterns/swift.ts +30 -0
  209. package/src/policy/skill-patterns/tailwind.ts +40 -0
  210. package/src/policy/skill-source.ts +52 -0
  211. package/src/policy/skill-trigger-patterns.ts +33 -0
  212. package/src/policy/skill-triggers.ts +108 -0
  213. package/src/policy/tailwind-skill-gate.ts +63 -0
  214. package/src/policy/trivial-edits.ts +52 -0
  215. package/src/policy/verbosity.ts +39 -0
  216. package/src/prompt/index.ts +1 -0
  217. package/src/prompt/types.ts +36 -0
  218. package/src/refs/discover.ts +69 -0
  219. package/src/refs/frontmatter.ts +24 -0
  220. package/src/refs/index.ts +4 -0
  221. package/src/refs/loader.ts +59 -0
  222. package/src/refs/ref-key.ts +41 -0
  223. package/src/refs/router.ts +63 -0
  224. package/src/refs/types.ts +25 -0
  225. package/src/runtime/activity.ts +99 -0
  226. package/src/runtime/apply-patch-gate.ts +41 -0
  227. package/src/runtime/burst-window.ts +21 -0
  228. package/src/runtime/codex-shell-tool.ts +37 -0
  229. package/src/runtime/command-string.ts +37 -0
  230. package/src/runtime/confirm/codex-confirm.ts +126 -0
  231. package/src/runtime/confirm/codex-prompt-origin.ts +16 -0
  232. package/src/runtime/confirm/confirm-code.ts +33 -0
  233. package/src/runtime/confirm/confirm-gate.ts +65 -0
  234. package/src/runtime/confirm/confirm-irreversible.ts +32 -0
  235. package/src/runtime/confirm/confirm-pending.ts +34 -0
  236. package/src/runtime/confirm/confirm-state.ts +56 -0
  237. package/src/runtime/confirm/confirm-subagent.ts +112 -0
  238. package/src/runtime/confirm/confirm-submit.ts +38 -0
  239. package/src/runtime/core-guards-active.ts +84 -0
  240. package/src/runtime/core-guards-wired.ts +47 -0
  241. package/src/runtime/deny-loop-store.ts +94 -0
  242. package/src/runtime/deny-notice.ts +52 -0
  243. package/src/runtime/design-cache-resolve.ts +51 -0
  244. package/src/runtime/design-content-gate.ts +95 -0
  245. package/src/runtime/design-files-gate.ts +114 -0
  246. package/src/runtime/design-helpers.ts +93 -0
  247. package/src/runtime/design-lifecycle.ts +39 -0
  248. package/src/runtime/design-read-credit.ts +28 -0
  249. package/src/runtime/design.ts +116 -0
  250. package/src/runtime/dev-context.ts +43 -0
  251. package/src/runtime/dry-patterns.ts +32 -0
  252. package/src/runtime/dry.ts +96 -0
  253. package/src/runtime/fragment-registry.ts +33 -0
  254. package/src/runtime/framework-skill-gate.ts +79 -0
  255. package/src/runtime/fs-cleanup.ts +71 -0
  256. package/src/runtime/gate-apex.ts +81 -0
  257. package/src/runtime/gate-helpers.ts +52 -0
  258. package/src/runtime/gate-input.ts +26 -0
  259. package/src/runtime/gate.ts +128 -0
  260. package/src/runtime/handle-post.ts +104 -0
  261. package/src/runtime/handle-pre.ts +137 -0
  262. package/src/runtime/handle-scope-async.ts +29 -0
  263. package/src/runtime/handle-types.ts +27 -0
  264. package/src/runtime/handle.ts +116 -0
  265. package/src/runtime/home-state.ts +55 -0
  266. package/src/runtime/index.ts +18 -0
  267. package/src/runtime/inform.ts +28 -0
  268. package/src/runtime/inject-budget-recap.ts +47 -0
  269. package/src/runtime/inject-budget.ts +81 -0
  270. package/src/runtime/inject-context.ts +85 -0
  271. package/src/runtime/inject-dedup-exclusive.ts +98 -0
  272. package/src/runtime/inject-dedup.ts +89 -0
  273. package/src/runtime/is-agent-tool.ts +19 -0
  274. package/src/runtime/lifecycle/agent-files.ts +49 -0
  275. package/src/runtime/lifecycle/agent-memory.ts +82 -0
  276. package/src/runtime/lifecycle/agent-transcript.ts +80 -0
  277. package/src/runtime/lifecycle/aipilot/analytics.ts +99 -0
  278. package/src/runtime/lifecycle/aipilot/apex-task-store.ts +61 -0
  279. package/src/runtime/lifecycle/aipilot/cache-base.ts +48 -0
  280. package/src/runtime/lifecycle/aipilot/cache-doc.ts +100 -0
  281. package/src/runtime/lifecycle/aipilot/cache-lessons.ts +58 -0
  282. package/src/runtime/lifecycle/aipilot/cache-test.ts +70 -0
  283. package/src/runtime/lifecycle/aipilot/curate-lessons.ts +64 -0
  284. package/src/runtime/lifecycle/aipilot/dispatch-aipilot.ts +100 -0
  285. package/src/runtime/lifecycle/aipilot/doc-cache-gate.ts +69 -0
  286. package/src/runtime/lifecycle/aipilot/inject-apex.ts +98 -0
  287. package/src/runtime/lifecycle/aipilot/inject-doc.ts +62 -0
  288. package/src/runtime/lifecycle/aipilot/inject-explore.ts +64 -0
  289. package/src/runtime/lifecycle/aipilot/inject-lessons.ts +61 -0
  290. package/src/runtime/lifecycle/aipilot/inject-test.ts +48 -0
  291. package/src/runtime/lifecycle/aipilot/lesson-archive.ts +60 -0
  292. package/src/runtime/lifecycle/aipilot/lesson-inject.ts +98 -0
  293. package/src/runtime/lifecycle/aipilot/lesson-parse.ts +74 -0
  294. package/src/runtime/lifecycle/aipilot/lessons.ts +72 -0
  295. package/src/runtime/lifecycle/aipilot/promote-global-lessons.ts +92 -0
  296. package/src/runtime/lifecycle/aipilot/solid-compliance.ts +69 -0
  297. package/src/runtime/lifecycle/aipilot/solid-notice.ts +47 -0
  298. package/src/runtime/lifecycle/aipilot/solid-transcript.ts +60 -0
  299. package/src/runtime/lifecycle/aipilot/source-scan.ts +56 -0
  300. package/src/runtime/lifecycle/aipilot/sync-task.ts +75 -0
  301. package/src/runtime/lifecycle/aipilot/transcript.ts +88 -0
  302. package/src/runtime/lifecycle/aipilot/types.ts +72 -0
  303. package/src/runtime/lifecycle/auto-document-reads.ts +73 -0
  304. package/src/runtime/lifecycle/cartographer/detect.ts +57 -0
  305. package/src/runtime/lifecycle/cartographer/ecosystem-map.ts +56 -0
  306. package/src/runtime/lifecycle/cartographer/fs-util.ts +64 -0
  307. package/src/runtime/lifecycle/cartographer/merge.ts +51 -0
  308. package/src/runtime/lifecycle/cartographer/project-map.ts +49 -0
  309. package/src/runtime/lifecycle/cartographer/scan-hooks.ts +48 -0
  310. package/src/runtime/lifecycle/cartographer/scan.ts +81 -0
  311. package/src/runtime/lifecycle/cartographer/session-start.ts +33 -0
  312. package/src/runtime/lifecycle/cartographer/track-enrichment.ts +45 -0
  313. package/src/runtime/lifecycle/cartographer/write-plugin-map.ts +43 -0
  314. package/src/runtime/lifecycle/cartographer/write-tree.ts +44 -0
  315. package/src/runtime/lifecycle/changelog-research.ts +43 -0
  316. package/src/runtime/lifecycle/check-file-size.ts +61 -0
  317. package/src/runtime/lifecycle/codex-resync/fingerprint.ts +63 -0
  318. package/src/runtime/lifecycle/codex-resync/lock.ts +46 -0
  319. package/src/runtime/lifecycle/codex-resync/managed-destination.ts +39 -0
  320. package/src/runtime/lifecycle/codex-resync/materialize-agents.ts +57 -0
  321. package/src/runtime/lifecycle/codex-resync/plugin-files.ts +65 -0
  322. package/src/runtime/lifecycle/codex-resync/plugin-roots.ts +62 -0
  323. package/src/runtime/lifecycle/codex-resync/resync.ts +45 -0
  324. package/src/runtime/lifecycle/codex-resync/symlink-commands.ts +25 -0
  325. package/src/runtime/lifecycle/dispatch.ts +109 -0
  326. package/src/runtime/lifecycle/failure-lesson.ts +51 -0
  327. package/src/runtime/lifecycle/index.ts +34 -0
  328. package/src/runtime/lifecycle/inject-rules.ts +50 -0
  329. package/src/runtime/lifecycle/instructions-loaded.ts +22 -0
  330. package/src/runtime/lifecycle/kimi-rules-native.ts +44 -0
  331. package/src/runtime/lifecycle/lessons/dispatch.ts +88 -0
  332. package/src/runtime/lifecycle/lessons/reminder.ts +89 -0
  333. package/src/runtime/lifecycle/lessons/state.ts +23 -0
  334. package/src/runtime/lifecycle/memory/agent-lesson.ts +35 -0
  335. package/src/runtime/lifecycle/memory/capture-error.ts +41 -0
  336. package/src/runtime/lifecycle/memory/client.ts +65 -0
  337. package/src/runtime/lifecycle/memory/dispatch.ts +42 -0
  338. package/src/runtime/lifecycle/memory/recall.ts +30 -0
  339. package/src/runtime/lifecycle/memory/salience.ts +31 -0
  340. package/src/runtime/lifecycle/memory/state.ts +49 -0
  341. package/src/runtime/lifecycle/memory/track-ops.ts +15 -0
  342. package/src/runtime/lifecycle/post-compact.ts +38 -0
  343. package/src/runtime/lifecycle/post-edit-ts.ts +49 -0
  344. package/src/runtime/lifecycle/post-tracking.ts +40 -0
  345. package/src/runtime/lifecycle/pre-compact.ts +38 -0
  346. package/src/runtime/lifecycle/rules-root.ts +83 -0
  347. package/src/runtime/lifecycle/security/check-skill.ts +63 -0
  348. package/src/runtime/lifecycle/security/scan-patterns.ts +53 -0
  349. package/src/runtime/lifecycle/security/scan.ts +76 -0
  350. package/src/runtime/lifecycle/security/skill-state.ts +47 -0
  351. package/src/runtime/lifecycle/security/track-mcp.ts +29 -0
  352. package/src/runtime/lifecycle/security/track-skill-read.ts +31 -0
  353. package/src/runtime/lifecycle/seo/post-tool-use.ts +45 -0
  354. package/src/runtime/lifecycle/session-end.ts +17 -0
  355. package/src/runtime/lifecycle/session-start.ts +57 -0
  356. package/src/runtime/lifecycle/snapshot/board.ts +27 -0
  357. package/src/runtime/lifecycle/snapshot/format.ts +60 -0
  358. package/src/runtime/lifecycle/snapshot/git.ts +71 -0
  359. package/src/runtime/lifecycle/snapshot/index.ts +52 -0
  360. package/src/runtime/lifecycle/snapshot/version.ts +34 -0
  361. package/src/runtime/lifecycle/solid-detect.ts +56 -0
  362. package/src/runtime/lifecycle/stop-core.ts +35 -0
  363. package/src/runtime/lifecycle/subagent-cache.ts +61 -0
  364. package/src/runtime/lifecycle/task-completed.ts +94 -0
  365. package/src/runtime/lifecycle/teammate-idle-check.ts +63 -0
  366. package/src/runtime/lifecycle/teammate-idle.ts +28 -0
  367. package/src/runtime/lifecycle/tool-failure.ts +24 -0
  368. package/src/runtime/lifecycle/track-changes.ts +61 -0
  369. package/src/runtime/lifecycle/validate-solid.ts +70 -0
  370. package/src/runtime/lifecycle/validate-tailwind.ts +80 -0
  371. package/src/runtime/lifecycle-bridge.ts +74 -0
  372. package/src/runtime/mcp-key.ts +44 -0
  373. package/src/runtime/mcp-tool-name.ts +92 -0
  374. package/src/runtime/mcp.ts +91 -0
  375. package/src/runtime/modular.ts +77 -0
  376. package/src/runtime/normalize.ts +107 -0
  377. package/src/runtime/notices.ts +91 -0
  378. package/src/runtime/notification-sound.ts +49 -0
  379. package/src/runtime/notifications.ts +77 -0
  380. package/src/runtime/paths.ts +59 -0
  381. package/src/runtime/post-fanout.ts +41 -0
  382. package/src/runtime/post-outcome.ts +61 -0
  383. package/src/runtime/pre-allow.ts +97 -0
  384. package/src/runtime/precommit.ts +60 -0
  385. package/src/runtime/prompt-text.ts +38 -0
  386. package/src/runtime/receipt-capture.ts +34 -0
  387. package/src/runtime/record.ts +26 -0
  388. package/src/runtime/respond.ts +92 -0
  389. package/src/runtime/solid-file-size-gate.ts +58 -0
  390. package/src/runtime/solid-pre.ts +53 -0
  391. package/src/runtime/storage.ts +9 -0
  392. package/src/statusline/colors.ts +31 -0
  393. package/src/statusline/constants.ts +17 -0
  394. package/src/statusline/formatters.ts +54 -0
  395. package/src/statusline/index.ts +4 -0
  396. package/src/statusline/progress-bar.ts +46 -0
  397. package/src/tracking/codex-post-failure.ts +73 -0
  398. package/src/tracking/index.ts +2 -0
  399. package/src/tracking/integrity.ts +99 -0
  400. package/src/tracking/one-shot-dedup.ts +34 -0
  401. package/src/tracking/one-shot-failure.ts +42 -0
  402. package/src/tracking/one-shot-store.ts +95 -0
  403. package/src/tracking/one-shot.ts +93 -0
  404. package/src/tracking/receipts.ts +95 -0
  405. package/src/tracking/session-state.ts +94 -0
  406. package/src/tracking/store.ts +96 -0
  407. package/src/tracking/track-compact.ts +100 -0
  408. package/src/tracking/track-diff.ts +50 -0
  409. package/src/tracking/track-journal.ts +97 -0
  410. package/src/tracking/track-lock-sync.ts +83 -0
  411. package/src/tracking/track-lock.ts +67 -0
  412. package/src/util/compact-json.ts +23 -0
  413. package/src/util/index.ts +2 -0
  414. package/src/util/json-io.ts +47 -0
  415. package/src/util/project-root.ts +41 -0
  416. package/src/util/runtime-io.ts +85 -0
  417. package/src/util/semver.ts +30 -0
@@ -0,0 +1,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
+ }
@@ -0,0 +1,63 @@
1
+ import { ALL_RULES, SKIP_LIST } from "./mapping";
2
+ import { renderPrefixRules, sanitizeComment } from "./starlark";
3
+
4
+ const HEADER =
5
+ '# Codex execpolicy rules — generated from @fusengine/harness src/policy/guards/security.ts\n' +
6
+ "# DO NOT hand-edit: regenerate via `harness codex-rules`.\n" +
7
+ "#\n" +
8
+ "# security.ts is the source of truth for dangerous-command detection; every\n" +
9
+ '# CRITICAL_PATTERNS label maps to decision = "forbidden", every ASK_PATTERNS\n' +
10
+ '# label maps to decision = "prompt". Strictest decision wins across matches.\n' +
11
+ "# The harness remains the AUTHORITATIVE enforcement layer — this file is a\n" +
12
+ "# complementary defense layer on the Codex side, not a replacement.\n" +
13
+ "#\n" +
14
+ "# IRREDUCIBLE gaps (shell syntax, invisible to argv-prefix matching, no\n" +
15
+ "# prefix_rule can express them) — compensated by sandbox_mode +\n" +
16
+ "# approval_policy.granular.sandbox_approval, not by this file:\n" +
17
+ "# - fork bomb: `:(){ :|:& };:`\n" +
18
+ "# - redirect to a raw disk device: `> /dev/sda`\n" +
19
+ "# - write to /etc via redirection: `> /etc/...`\n" +
20
+ "#\n" +
21
+ "# ASSUMED over-width (intentionally broader than security.ts, chosen to\n" +
22
+ "# avoid a silent gap rather than an exact-but-brittle translation):\n" +
23
+ "# - dd: prompts on every dd invocation; security.ts only fires on of=/dev/*\n" +
24
+ "# - rm: prompts even on paths security.ts exempts via a 'trash' substring\n" +
25
+ "# - rm/eval/unlink: prompts even on a bare token with no trailing argument;\n" +
26
+ "# security.ts requires a space (i.e. an argument) after the command\n" +
27
+ "# - chmod 777: prompts on relative paths too, not only absolute ones\n" +
28
+ "# - sudo (/usr/bin/sudo alias): forbids the absolute-path form too;\n" +
29
+ "# security.ts's word-boundary regex does not match a preceding '/'\n" +
30
+ "# - curl/wget: prompts on EVERY curl/wget invocation, not only when piped\n" +
31
+ '# into a shell; security.ts\'s "remote script piped to a shell" label\n' +
32
+ "# only fires on `curl|wget ... | sh`. A simple plain-word pipe IS split\n" +
33
+ "# by Codex into separate argv commands (a bare-shell rule would also\n" +
34
+ "# catch that case), but this rule additionally targets the download\n" +
35
+ "# half for compound chains and non-shell payload execution where\n" +
36
+ "# sh/bash never appears as argv[0] (curl -o x && chmod +x x && ./x,\n" +
37
+ "# curl ... | python3); it replaces a former bare sh/bash/zsh heuristic\n" +
38
+ '# that prompted on ~50% of Codex\'s own `bash -lc "..."` command\n' +
39
+ "# wrapping and rarely caught the actual download\n" +
40
+ "#\n" +
41
+ "# NOT covered (out of scope, not an over-width — a purely local pipe with no\n" +
42
+ "# network fetch is invisible to this rule AND to security.ts, which only\n" +
43
+ "# matches curl/wget as the left-hand side of the pipe):\n" +
44
+ "# - `cat file | sh`: local pipe, no curl/wget involved\n" +
45
+ "# - quoted-variable/redirection installer one-liners, e.g.\n" +
46
+ '# `curl -fsSL "$URL" | sh`: variable substitution disables Codex\'s\n' +
47
+ "# pipe-splitting, collapsing the whole invocation to a single\n" +
48
+ "# ['bash', '-lc', '<script>'] where curl is not argv[0] — this exact\n" +
49
+ "# form was covered by the removed bare sh/bash heuristic and is no\n" +
50
+ "# longer covered by any rule here\n" +
51
+ "#\n" +
52
+ "# The following labels have NO argv-prefix equivalent at all (shell syntax\n" +
53
+ "# not visible to execpolicy's argv matching) and are intentionally NOT\n" +
54
+ "# expressed as prefix_rule() below — covered instead by sandbox_mode/approval_policy:";
55
+
56
+ /** Render the full Codex execpolicy `.rules` Starlark file as a string. */
57
+ export function buildCodexRules(): string {
58
+ const skipComment: string = SKIP_LIST.map(
59
+ (s) => `# - ${sanitizeComment(s.sourceLabel)}: ${sanitizeComment(s.reason)}`,
60
+ ).join("\n");
61
+ const body: string = renderPrefixRules(ALL_RULES);
62
+ return `${HEADER}\n${skipComment}\n\n${body}\n`;
63
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Codex execpolicy `.rules` (Starlark) generator, derived from the harness's
3
+ * own `security.ts` dangerous-command patterns. Pure, no I/O — see
4
+ * `../cli/bin.ts`'s `codex-rules` subcommand for stdout/`--out` wiring.
5
+ */
6
+ export { buildCodexRules } from "./build";
7
+ export { ALL_RULES, CRITICAL_RULES, ASK_RULES, SKIP_LIST } from "./mapping";
8
+ export type { CodexDecision, CodexPrefixRule, CodexSkipEntry } from "./types";
@@ -0,0 +1,42 @@
1
+ import type { CodexPrefixRule } from "./types";
2
+ import { RM_VARIANT_RULES } from "./rules/rm-variants";
3
+ import { MKFS_RULES } from "./rules/mkfs";
4
+ import { DISKUTIL_RULES } from "./rules/diskutil";
5
+ import { DD_RULES } from "./rules/dd";
6
+ import { SINGLE_COMMAND_RULES } from "./rules/single-commands";
7
+ import { PRIVILEGE_RULES } from "./rules/privilege";
8
+ import { ASK_RULES as BASE_ASK_RULES } from "./rules/ask";
9
+ import { CHMOD_ASK_RULES } from "./rules/chmod-ask";
10
+ import { REMOTE_FETCH_RULES } from "./rules/remote-fetch";
11
+ import { withAbsolutePathAliases } from "./rules/absolute-paths";
12
+ import { SKIP_LIST } from "./skip-list";
13
+
14
+ /** Every `decision = "forbidden"` rule, hand-mapped from CRITICAL_PATTERNS. */
15
+ const CRITICAL_BASE: CodexPrefixRule[] = [
16
+ ...RM_VARIANT_RULES,
17
+ ...MKFS_RULES,
18
+ ...DISKUTIL_RULES,
19
+ ...SINGLE_COMMAND_RULES,
20
+ ...PRIVILEGE_RULES,
21
+ ];
22
+
23
+ /** Every `decision = "prompt"` rule, hand-mapped from ASK_PATTERNS (+ dd, downgraded). */
24
+ const ASK_BASE: CodexPrefixRule[] = [...BASE_ASK_RULES, ...CHMOD_ASK_RULES, ...DD_RULES, ...REMOTE_FETCH_RULES];
25
+
26
+ /** Absolute-path mirrors of the bare-name rules above (see `rules/absolute-paths.ts`). */
27
+ const ABSOLUTE_ALIASES: CodexPrefixRule[] = withAbsolutePathAliases([...CRITICAL_BASE, ...ASK_BASE]);
28
+
29
+ export const CRITICAL_RULES: CodexPrefixRule[] = [
30
+ ...CRITICAL_BASE,
31
+ ...ABSOLUTE_ALIASES.filter((r) => r.decision === "forbidden"),
32
+ ];
33
+
34
+ export const ASK_RULES: CodexPrefixRule[] = [
35
+ ...ASK_BASE,
36
+ ...ABSOLUTE_ALIASES.filter((r) => r.decision === "prompt"),
37
+ ];
38
+
39
+ export { SKIP_LIST };
40
+
41
+ /** Every generated `prefix_rule`, critical (`forbidden`) + ask (`prompt`). */
42
+ export const ALL_RULES: CodexPrefixRule[] = [...CRITICAL_RULES, ...ASK_RULES];
@@ -0,0 +1,52 @@
1
+ import type { CodexPrefixRule } from "../types";
2
+
3
+ /**
4
+ * Absolute-path executable aliases — Codex only resolves an absolute path
5
+ * to its basename rule when the CLI is run with `--resolve-host-executables`
6
+ * (opt-in, off by default; this generated `.rules` file cannot assume it).
7
+ * `/bin/rm -rf /` and a bare `rm -rf /` are two DIFFERENT argv prefixes to
8
+ * execpolicy, so every rule keyed on one of these bare names is mirrored
9
+ * onto its common absolute-path forms.
10
+ */
11
+ const ABSOLUTE_PATH_ALIASES: Record<string, string[]> = {
12
+ rm: ["/bin/rm", "/usr/bin/rm"],
13
+ chmod: ["/bin/chmod", "/usr/bin/chmod"],
14
+ diskutil: ["/usr/sbin/diskutil"],
15
+ dd: ["/bin/dd", "/usr/bin/dd"],
16
+ sudo: ["/usr/bin/sudo"],
17
+ };
18
+
19
+ /** Escape regex metacharacters so a dynamic token can be safely embedded in a `RegExp` literal. */
20
+ function escapeRegExp(token: string): string {
21
+ return token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
22
+ }
23
+
24
+ /** Replace a match/notMatch example's leading token with an absolute-path alias. */
25
+ function aliasExample(example: string, from: string, to: string): string {
26
+ return example.replace(new RegExp(`^${escapeRegExp(from)}\\b`), to);
27
+ }
28
+
29
+ /**
30
+ * Clone every rule in `rules` whose first pattern token has a known
31
+ * absolute-path alias, producing one variant per alias (same decision,
32
+ * same sourceLabel — traced back to the same `security.ts` label).
33
+ */
34
+ export function withAbsolutePathAliases(rules: CodexPrefixRule[]): CodexPrefixRule[] {
35
+ const out: CodexPrefixRule[] = [];
36
+ for (const rule of rules) {
37
+ const first = rule.pattern[0];
38
+ if (typeof first !== "string") continue;
39
+ const aliases = ABSOLUTE_PATH_ALIASES[first];
40
+ if (!aliases) continue;
41
+ for (const alias of aliases) {
42
+ out.push({
43
+ ...rule,
44
+ pattern: [alias, ...rule.pattern.slice(1)],
45
+ match: rule.match.map((m) => aliasExample(m, first, alias)),
46
+ notMatch: rule.notMatch?.map((m) => aliasExample(m, first, alias)),
47
+ justification: `${rule.justification} (absolute-path form; Codex does not resolve executable basenames by default).`,
48
+ });
49
+ }
50
+ }
51
+ return out;
52
+ }