@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,58 @@
1
+ /**
2
+ * PreToolUse file-size deny for the solid scope — the same SOLID ceiling the
3
+ * core gate chain enforces (`policy/evaluate.ts`), extracted so
4
+ * `harness hook <id> solid` blocks oversized Write/Edit WITHOUT the core-only
5
+ * APEX freshness gates (they require explore/research agents that not every
6
+ * harness runs). Pure and read-only (on-disk read via {@link existingLineCounts},
7
+ * no session state), so the sibling-plugin fan-out stays idempotent.
8
+ */
9
+ import { countLines, evaluateFileSize } from "../policy/file-size";
10
+ import { isFileSizeScoped, resolveSolidRefFramework } from "../policy/file-size-scope";
11
+ import { computeEditResultLines } from "../policy/edit-outcome";
12
+ import { resolveMaxLines } from "../config/limits";
13
+ import { existingLineCounts } from "./gate-helpers";
14
+ import type { Prompt } from "../prompt/types";
15
+
16
+ /**
17
+ * Deny a Write/Edit whose resulting file would exceed the SOLID line ceiling.
18
+ * Mirrors `policy/evaluate.ts` exactly: a computable Edit outcome passes when
19
+ * the result is compliant OR strictly shrinks an already-oversized file; the
20
+ * block message reports the pre-existing on-disk count for a Write.
21
+ * @param tool - The tool name (only "Write"/"Edit" are checked).
22
+ * @param filePath - The written file's absolute path.
23
+ * @param content - Write: the full new content; Edit: the `new_string` snippet.
24
+ * @param oldString - Edit only: the `old_string` being replaced.
25
+ * @param isReplaceAll - Edit only: whether every occurrence is replaced.
26
+ * @param agentType - Subagent type, when known (Explore/Plan are exempt).
27
+ * @returns The blocking prompt, or null when compliant or out of scope.
28
+ */
29
+ export function solidFileSizeGate(
30
+ tool: string,
31
+ filePath: string | undefined,
32
+ content: string | undefined,
33
+ oldString?: string,
34
+ isReplaceAll: boolean = false,
35
+ agentType?: string,
36
+ ): Prompt | null {
37
+ if (tool !== "Write" && tool !== "Edit") return null;
38
+ if (!filePath || !isFileSizeScoped(filePath)) return null;
39
+ if (agentType === "Explore" || agentType === "Plan") return null;
40
+ const { raw: existingLines, content: existingContent } = existingLineCounts(filePath);
41
+ const incoming = content !== undefined ? countLines(content) : 0;
42
+ const existing = existingLines ?? 0;
43
+ const max = resolveMaxLines();
44
+ const editResult = tool === "Edit" && existingContent !== undefined && content !== undefined
45
+ ? computeEditResultLines(existingContent, oldString, content, isReplaceAll)
46
+ : null;
47
+ if (editResult !== null && (editResult <= max || editResult < existing)) return null;
48
+ const lines = tool === "Edit" ? (editResult ?? Math.max(incoming, existing)) : incoming || existing;
49
+ const displayLines = tool === "Write" ? (existingLines ?? lines) : lines;
50
+ const verdict = evaluateFileSize(lines, max, filePath, resolveSolidRefFramework(filePath), displayLines);
51
+ if (lines === 0 || verdict.ok) return null;
52
+ return {
53
+ kind: "block",
54
+ title: "SOLID file-size limit",
55
+ reason: verdict.message ?? "",
56
+ actions: [`Split into modules under ${verdict.max} lines (Single Responsibility)`, "Then re-run the write"],
57
+ };
58
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Solid-scope PreToolUse outcome: the file-size deny ({@link solidFileSizeGate})
3
+ * first — rendered via {@link respond} so every harness gets its native shape
4
+ * (Kimi finally receives a real `hookSpecificOutput` deny instead of a leaked
5
+ * Claude envelope) — then the ported Go/Python interface-location check
6
+ * ({@link validateSolidGate}, kept byte-identical for claude-code/codex).
7
+ * Owns the whole solid-scope branch so `handle-pre` stays a dispatcher and the
8
+ * branch NEVER falls through to the core APEX/SOLID gate chain: core-guards
9
+ * owns it, so falling through would run `gate()` twice per edit when both
10
+ * plugins wire PreToolUse Write|Edit (duplicate denies + added latency). For
11
+ * the same reason the file-size gate itself ABSTAINS when core-guards is
12
+ * installed ({@link coreGuardsWired}): one tool-call, one deny, whoever fires.
13
+ */
14
+ import { dirname } from "node:path";
15
+ import { withDenyNotice } from "./deny-notice";
16
+ import { respond } from "./respond";
17
+ import { validateSolidGate } from "./lifecycle";
18
+ import { solidFileSizeGate } from "./solid-file-size-gate";
19
+ import { coreGuardsWired } from "./core-guards-wired";
20
+ import type { NormalizedEvent } from "./normalize";
21
+ import type { HandleOutcome } from "./handle-types";
22
+
23
+ /**
24
+ * Run the solid-scope PreToolUse gates and render the native hook outcome.
25
+ * @param id - Harness id (selects the response shape via {@link respond}).
26
+ * @param event - The normalized hook event.
27
+ * @param file - Session track file (the deny-notice state derives from its dir).
28
+ * @param now - Current time in ms.
29
+ * @param env - Environment for the core-guards wiring probe (injectable).
30
+ * @returns The hook outcome: file-size deny, Go/Python deny, or allow.
31
+ */
32
+ export function solidScopeOutcome(
33
+ id: string,
34
+ event: NormalizedEvent,
35
+ file: string,
36
+ now: number,
37
+ env: Record<string, string | undefined> = process.env,
38
+ ): HandleOutcome {
39
+ const prompt = coreGuardsWired(id, env)
40
+ ? null
41
+ : solidFileSizeGate(
42
+ event.tool,
43
+ event.filePath,
44
+ event.content,
45
+ event.oldString,
46
+ event.input.replace_all === true,
47
+ event.agentType,
48
+ );
49
+ if (prompt) {
50
+ return { stdout: withDenyNotice(id, respond(id, prompt), prompt, event.sessionId, dirname(file), now), exit: 0 };
51
+ }
52
+ return { stdout: validateSolidGate(event.tool, event.filePath ?? "", event.content ?? ""), exit: 0 };
53
+ }
@@ -0,0 +1,9 @@
1
+ import { projectLayout } from "../config/layout";
2
+
3
+ /**
4
+ * The project's single state dir (`<root>/.harness`) — neutral + harness-agnostic,
5
+ * derived from {@link projectLayout}. (Was the per-harness `.claude/harness`.)
6
+ */
7
+ export function harnessStateDir(root: string): string {
8
+ return projectLayout(root).stateDir;
9
+ }
@@ -0,0 +1,31 @@
1
+ import { COLOR_THRESHOLDS } from "./constants";
2
+
3
+ /** A text-decorating function. */
4
+ export type ColorFn = (text: string) => string;
5
+
6
+ const ansi = (code: string): ColorFn => (text) => `\x1b[${code}m${text}\x1b[0m`;
7
+ const ansi256 = (code: number): ColorFn => (text) => `\x1b[38;5;${code}m${text}\x1b[0m`;
8
+
9
+ /** Forced ANSI palette (every entry is a colorizer, plus reset + support flag). */
10
+ export interface Palette {
11
+ blue: ColorFn; cyan: ColorFn; green: ColorFn; yellow: ColorFn; red: ColorFn;
12
+ magenta: ColorFn; white: ColorFn; gray: ColorFn; purple: ColorFn; orange: ColorFn;
13
+ brightRed: ColorFn; brightYellow: ColorFn; brightGreen: ColorFn;
14
+ bold: ColorFn; dim: ColorFn; reset: string; isSupported: boolean;
15
+ }
16
+
17
+ /** Forced ANSI color helpers (ignore TTY detection). */
18
+ export const colors: Palette = {
19
+ blue: ansi("0;34"), cyan: ansi("0;36"), green: ansi("0;32"),
20
+ yellow: ansi("0;33"), red: ansi("0;31"), magenta: ansi("0;35"),
21
+ white: ansi("0;37"), gray: ansi256(240), purple: ansi256(135), orange: ansi256(208),
22
+ brightRed: ansi("1;91"), brightYellow: ansi("1;93"), brightGreen: ansi("1;92"),
23
+ bold: ansi("1"), dim: ansi("2"), reset: "\x1b[0m", isSupported: true,
24
+ };
25
+
26
+ /** Color `text` by threshold: green < WARNING <= yellow < CRITICAL <= red. */
27
+ export function progressiveColor(value: number, text: string): string {
28
+ if (value >= COLOR_THRESHOLDS.CRITICAL) return colors.brightRed(text);
29
+ if (value >= COLOR_THRESHOLDS.WARNING) return colors.brightYellow(text);
30
+ return colors.green(text);
31
+ }
@@ -0,0 +1,17 @@
1
+ /** Time interval constants (ms). */
2
+ export const TIME_INTERVALS = { MINUTE_MS: 60_000, HOUR_MS: 3_600_000 } as const;
3
+
4
+ /** Percentage thresholds for progressive coloring. */
5
+ export const COLOR_THRESHOLDS = { WARNING: 70, CRITICAL: 90 } as const;
6
+
7
+ /** Fill/empty glyphs per progress-bar style. */
8
+ export const PROGRESS_CHARS = {
9
+ blocks: { fill: "█", empty: "░" },
10
+ bars: { fill: "▰", empty: "▱" },
11
+ } as const;
12
+
13
+ /** Defaults for {@link generateProgressBar}. */
14
+ export const PROGRESS_BAR_DEFAULTS = { STYLE: "blocks", LENGTH: 10 } as const;
15
+
16
+ /** 9 gradient blocks from empty to full. */
17
+ export const GRADIENT_BLOCKS = [" ", "▏", "▎", "▍", "▌", "▋", "▊", "▉", "█"] as const;
@@ -0,0 +1,54 @@
1
+ import { basename } from "node:path";
2
+ import { TIME_INTERVALS } from "./constants";
3
+
4
+ /** Format a path: truncated (default), full, relative (~), or basename. */
5
+ export function formatPath(
6
+ path: string,
7
+ style: "truncated" | "full" | "relative" | "basename" = "truncated",
8
+ ): string {
9
+ const home = process.env.HOME || "";
10
+ const isUnderHome = home !== "" && path.startsWith(home);
11
+ const withTilde = isUnderHome ? path.replace(home, "~") : path;
12
+ const parts = withTilde.split("/").filter(Boolean);
13
+ const name = parts[parts.length - 1] || withTilde;
14
+ switch (style) {
15
+ case "full": return path;
16
+ case "basename": return name;
17
+ case "relative": return withTilde;
18
+ default:
19
+ if (isUnderHome && parts.length > 2) return `~/../${name}`;
20
+ if (isUnderHome) return withTilde;
21
+ if (parts.length > 3) return `/../${name}`;
22
+ return withTilde;
23
+ }
24
+ }
25
+
26
+ /** Basename of a path. */
27
+ export function formatBasename(path: string): string {
28
+ return basename(path);
29
+ }
30
+
31
+ /** Humanize a remaining duration in ms (e.g. "2h - 5m"). */
32
+ export function formatTimeLeft(ms: number): string {
33
+ if (ms <= 0) return "0m";
34
+ const dayMs = TIME_INTERVALS.HOUR_MS * 24;
35
+ const days = Math.floor(ms / dayMs);
36
+ const hours = Math.floor((ms % dayMs) / TIME_INTERVALS.HOUR_MS);
37
+ const minutes = Math.floor((ms % TIME_INTERVALS.HOUR_MS) / TIME_INTERVALS.MINUTE_MS);
38
+ if (days > 0 && hours > 0) return `${days}d - ${hours}h`;
39
+ if (days > 0) return `${days}d`;
40
+ if (hours > 0 && minutes > 0) return `${hours}h - ${minutes}m`;
41
+ if (hours > 0) return `${hours}h`;
42
+ return `${minutes}m`;
43
+ }
44
+
45
+ /** Format a token count as "12K" (or "12.3K" with decimals). */
46
+ export function formatTokens(tokens: number, showDecimals = false): string {
47
+ const k = tokens / 1000;
48
+ return showDecimals ? `${k.toFixed(1)}K` : `${Math.round(k)}K`;
49
+ }
50
+
51
+ /** Format a cost as "$1.23". */
52
+ export function formatCost(cost: number, decimals = 2): string {
53
+ return `$${cost.toFixed(decimals)}`;
54
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./constants";
2
+ export * from "./colors";
3
+ export * from "./formatters";
4
+ export * from "./progress-bar";
@@ -0,0 +1,46 @@
1
+ import { GRADIENT_BLOCKS, PROGRESS_BAR_DEFAULTS, PROGRESS_CHARS } from "./constants";
2
+ import { progressiveColor } from "./colors";
3
+
4
+ /** Options for {@link generateProgressBar}. */
5
+ export interface ProgressBarOptions {
6
+ style?: keyof typeof PROGRESS_CHARS;
7
+ length?: number;
8
+ useProgressiveColor?: boolean;
9
+ showPercentage?: boolean;
10
+ }
11
+
12
+ const clampPct = (p: number): number => Math.max(0, Math.min(100, Number.isNaN(p) ? 0 : p));
13
+
14
+ /** Render a fill/empty progress bar. */
15
+ export function generateProgressBar(percentage: number, options: ProgressBarOptions = {}): string {
16
+ const {
17
+ style = PROGRESS_BAR_DEFAULTS.STYLE, length = PROGRESS_BAR_DEFAULTS.LENGTH,
18
+ useProgressiveColor = false, showPercentage = false,
19
+ } = options;
20
+ const pct = clampPct(percentage);
21
+ const filled = Math.round((pct / 100) * length);
22
+ const empty = Math.max(0, length - filled);
23
+ const chars = PROGRESS_CHARS[style];
24
+ let bar = chars.fill.repeat(filled) + chars.empty.repeat(empty);
25
+ if (useProgressiveColor) bar = progressiveColor(pct, bar);
26
+ if (showPercentage) {
27
+ const pctText = `${Math.round(pct)}%`;
28
+ bar += ` ${useProgressiveColor ? progressiveColor(pct, pctText) : pctText}`;
29
+ }
30
+ return bar;
31
+ }
32
+
33
+ /** Render a fine-grained gradient bar (sub-block resolution). */
34
+ export function generateGradientBar(percentage: number, length = 10): string {
35
+ const pct = clampPct(percentage);
36
+ const exact = (pct / 100) * length;
37
+ const full = Math.floor(exact);
38
+ const remainder = exact - full;
39
+ let bar = GRADIENT_BLOCKS[8].repeat(full);
40
+ if (full < length && remainder > 0) {
41
+ bar += GRADIENT_BLOCKS[Math.floor(remainder * 8)] ?? "";
42
+ }
43
+ const remaining = length - bar.length;
44
+ if (remaining > 0) bar += " ".repeat(remaining);
45
+ return progressiveColor(pct, bar);
46
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * @module codex-post-failure
3
+ * Codex has no native `PostToolUseFailure` event (Claude Code's own failure
4
+ * signal, ported in `src/runtime/lifecycle/tool-failure.ts`'s `logToolFailure`)
5
+ * — every Codex tool outcome, success or failure, arrives on the SAME
6
+ * `PostToolUse` event. This infers a failure from the regular payload's
7
+ * `tool_result`/`tool_response` and journals it into the EXISTING one-shot
8
+ * failure tally ({@link recordFailure}) — no new store, no new gate.
9
+ *
10
+ * Design choice (fail-open): a payload this module cannot positively prove
11
+ * failed — `null`/`undefined`, a non-object shape, or any parse exception —
12
+ * classifies as `"success"` (ignored), NEVER `"failure"`. Many legitimate
13
+ * successful tool calls return no structured result at all (see
14
+ * `handle-post.ts`'s `spawn_agent` `tool_response: { nickname: "scout" }`),
15
+ * so treating "can't tell" as a failure would flood the tally with false
16
+ * positives. Only an EXPLICIT signal (non-zero exit, an error field) counts.
17
+ * @packageDocumentation
18
+ */
19
+ import { recordFailure } from "./one-shot-failure";
20
+ import type { OneShotOpts } from "./one-shot";
21
+
22
+ /** Truthy interruption markers across harness result shapes (parity `tool-failure.ts`'s `is_interrupt`). */
23
+ const INTERRUPT_KEYS = ["is_interrupt", "isInterrupted", "interrupted", "aborted", "cancelled", "canceled"];
24
+
25
+ /** True when `result` carries any known interruption marker — an interruption is never a failure. */
26
+ function isInterruption(result: Record<string, unknown>): boolean {
27
+ return INTERRUPT_KEYS.some((k) => result[k] === true);
28
+ }
29
+
30
+ /** True when `result` reports a non-zero exit code (Bash-shaped tool result). */
31
+ function hasNonZeroExit(result: Record<string, unknown>): boolean {
32
+ const exit = result.exit_code ?? result.exitCode;
33
+ return typeof exit === "number" && exit !== 0;
34
+ }
35
+
36
+ /** True when `result` carries an explicit error signal (`error`, `is_error`/`isError`, or `success: false`). */
37
+ function hasErrorField(result: Record<string, unknown>): boolean {
38
+ if (result.success === false) return true;
39
+ if (result.is_error === true || result.isError === true) return true;
40
+ const err = result.error;
41
+ return typeof err === "string" ? err.length > 0 : err !== undefined && err !== null;
42
+ }
43
+
44
+ /**
45
+ * Classify a Codex PostToolUse `tool_response`/`tool_result` payload.
46
+ * Fail-open (see module doc): unprovable shapes resolve to `"success"`,
47
+ * never `"failure"`, and this NEVER throws.
48
+ * @param result - The raw result value off the payload (any shape).
49
+ * @returns `"success"` | `"failure"` | `"interrupted"`.
50
+ */
51
+ export function classifyCodexOutcome(result: unknown): "success" | "failure" | "interrupted" {
52
+ try {
53
+ if (result === null || typeof result !== "object" || Array.isArray(result)) return "success";
54
+ const r = result as Record<string, unknown>;
55
+ if (isInterruption(r)) return "interrupted";
56
+ if (hasNonZeroExit(r) || hasErrorField(r)) return "failure";
57
+ return "success";
58
+ } catch {
59
+ return "success";
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Journal a Codex tool failure into the existing one-shot failure tally.
65
+ * No-op for `"success"`/`"interrupted"` — only a genuine failure is recorded.
66
+ * @param tool - The tool name.
67
+ * @param result - The raw PostToolUse result payload.
68
+ * @param opts - Clock + state dir (+ optional session id), forwarded as-is to {@link recordFailure}.
69
+ */
70
+ export function recordCodexPostFailure(tool: string, result: unknown, opts: OneShotOpts): void {
71
+ if (classifyCodexOutcome(result) !== "failure") return;
72
+ recordFailure(tool, opts);
73
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./session-state";
2
+ export * from "./store";
@@ -0,0 +1,99 @@
1
+ /**
2
+ * @module integrity
3
+ * HMAC-SHA256 tamper-evident wrapping for {@link SessionTrack}.
4
+ *
5
+ * Failure policy: ONLY a MAC mismatch causes fail-closed (returns null).
6
+ * The nonce is included in the signed payload and written to disk for advisory
7
+ * diagnostics, but is NOT checked during verification — concurrent hook
8
+ * invocations (PostToolUse, SubagentStop, …) legitimately load the same
9
+ * envelope after the nonce watermark advances; a monotonic check would trigger
10
+ * spurious fail-closed behaviour mid-session.
11
+ *
12
+ * The machine key is readable by the same agent process, so this deters naive
13
+ * out-of-band tampering — not a determined re-sign. Item B (transcript-grounded
14
+ * freshness) is the primary guarantee.
15
+ * @packageDocumentation
16
+ */
17
+ import { createHmac, randomBytes } from "node:crypto";
18
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
19
+ import { join } from "node:path";
20
+ import { fuseHarnessHome } from "../runtime/home-state";
21
+ import type { SessionTrack } from "./session-state";
22
+
23
+ const HARNESS_DIR = fuseHarnessHome();
24
+ const KEY_PATH = join(HARNESS_DIR, ".key");
25
+ const NONCE_PATH = join(HARNESS_DIR, ".nonce");
26
+
27
+ /** Signed envelope wrapping a JSON-serialised {@link SessionTrack}. */
28
+ export interface TrackEnvelope {
29
+ /** JSON-serialised SessionTrack. */
30
+ data: string;
31
+ /** Advisory timestamp (Date.now() at sign time); embedded in the MAC. */
32
+ nonce: number;
33
+ /** HMAC-SHA256 hex digest over `"${nonce}:${data}"`. */
34
+ mac: string;
35
+ }
36
+
37
+ /** Load (or create on first use) the per-machine HMAC key stored at mode 0600. */
38
+ export function loadOrCreateKey(): string {
39
+ mkdirSync(HARNESS_DIR, { recursive: true });
40
+ if (existsSync(KEY_PATH)) return readFileSync(KEY_PATH, "utf8").trim();
41
+ // Race-tolerant creation ("wx" crowns ONE creator): before, N concurrent
42
+ // first-time processes (virgin home — CI) each signed with their OWN key.
43
+ const candidate = randomBytes(32).toString("hex");
44
+ try {
45
+ writeFileSync(KEY_PATH, candidate, { encoding: "utf8", mode: 0o600, flag: "wx" });
46
+ return candidate;
47
+ } catch (e) {
48
+ if ((e as NodeJS.ErrnoException).code !== "EEXIST") throw e;
49
+ }
50
+ // The winner's 64-byte write is one write(2); bounded re-reads cover the gap.
51
+ for (let i = 0; i < 5; i++) {
52
+ const existing = readFileSync(KEY_PATH, "utf8").trim();
53
+ if (existing) return existing;
54
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
55
+ }
56
+ const key = readFileSync(KEY_PATH, "utf8").trim();
57
+ if (!key) throw new Error("fuse: empty .key after write contention");
58
+ return key;
59
+ }
60
+
61
+ /**
62
+ * Persist the last-seen nonce for advisory diagnostics (mode 0600).
63
+ * Never read during {@link verifyTrack} — see module JSDoc for rationale.
64
+ */
65
+ export function writeLastNonce(nonce: number): void {
66
+ mkdirSync(HARNESS_DIR, { recursive: true });
67
+ writeFileSync(NONCE_PATH, String(nonce), { encoding: "utf8", mode: 0o600 });
68
+ }
69
+
70
+ /** Compute HMAC-SHA256 over `"${nonce}:${data}"` (nonce: envelope number or journal per-line string). */
71
+ export function computeMac(key: string, data: string, nonce: number | string): string {
72
+ return createHmac("sha256", key).update(`${nonce}:${data}`).digest("hex");
73
+ }
74
+
75
+ /**
76
+ * Sign a {@link SessionTrack} into a tamper-evident envelope. The nonce
77
+ * (`Date.now()`) is embedded in the MAC to bind the timestamp to the payload.
78
+ */
79
+ export function signTrack(track: SessionTrack): TrackEnvelope {
80
+ const key = loadOrCreateKey();
81
+ const data = JSON.stringify(track);
82
+ const nonce = Date.now();
83
+ return { data, nonce, mac: computeMac(key, data, nonce) };
84
+ }
85
+
86
+ /**
87
+ * Verify a {@link TrackEnvelope}. Returns the parsed {@link SessionTrack} on
88
+ * success, or `null` ONLY on MAC mismatch / parse failure (fail-closed on
89
+ * tampering). The nonce is NOT checked — see module JSDoc.
90
+ */
91
+ export function verifyTrack(envelope: TrackEnvelope): SessionTrack | null {
92
+ try {
93
+ const key = loadOrCreateKey();
94
+ if (envelope.mac !== computeMac(key, envelope.data, envelope.nonce)) return null;
95
+ return JSON.parse(envelope.data) as SessionTrack;
96
+ } catch {
97
+ return null;
98
+ }
99
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * @module one-shot-dedup
3
+ * Burst-dedup guard for the one-shot metric ({@link module:one-shot}).
4
+ *
5
+ * ONE Claude tool event fans out to ~11 sibling plugin-hook processes, each
6
+ * calling {@link recordOneShot}; without this the metric would count a single
7
+ * deny/allow ~11×. Reuses the proven {@link oncePerWindow} cooldown sidecar:
8
+ * the FIRST process in the {@link module:burst-window} window mutates the
9
+ * metric, the rest skip. The dedup key includes the outcome KIND (deny-title vs
10
+ * allow) so a deny and its later fix — different kinds — are never folded into
11
+ * each other. No `sessionId` → always the first (mono-process + unit-test
12
+ * parity; a burst can only exist when a real session drives the fan-out).
13
+ * @packageDocumentation
14
+ */
15
+ import { oncePerWindow } from "../runtime/inject-dedup";
16
+ import { BURST_DEDUP_MS } from "../runtime/burst-window";
17
+
18
+ /** Opts needed to decide + persist a burst claim (a structural subset of `OneShotOpts`). */
19
+ export interface BurstOpts { now: number; dir: string; sessionId?: string; }
20
+
21
+ /**
22
+ * True when this `(op, kind)` is the FIRST of its burst for the session — the
23
+ * process that should actually mutate the metric. Sibling processes firing the
24
+ * SAME event within {@link BURST_DEDUP_MS} return false and skip the write.
25
+ * @param op - Content-free operation key ({@link denyHash}("op", …)).
26
+ * @param kind - Outcome discriminator (`deny:<title>` or `allow`).
27
+ * @param opts - Clock + state dir + optional session id.
28
+ * @returns `true` to apply the record, `false` to skip (already counted).
29
+ */
30
+ export function burstFirst(op: string, kind: string, opts: BurstOpts): boolean {
31
+ const sid = opts.sessionId?.trim();
32
+ if (!sid) return true;
33
+ return oncePerWindow(`oneshot:${sid}:${op}:${kind}`, BURST_DEDUP_MS, { now: opts.now, dir: opts.dir });
34
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * @module one-shot-failure
3
+ * PostToolUseFailure tally for the one-shot sidecar. A pure per-tool counter
4
+ * ({@link applyFailure}) plus its burst-deduped IO writer ({@link recordFailure}),
5
+ * kept out of {@link module:one-shot-store} so that file stays under the SOLID
6
+ * size limit. Reuses the SAME sidecar, prune window, and burst-dedup as
7
+ * {@link module:one-shot} — failures never touch the deny/allow gate rate.
8
+ * @packageDocumentation
9
+ */
10
+ import { join } from "node:path";
11
+ import { atomicWrite } from "../util/json-io";
12
+ import { pruneState, type OneShotState } from "./one-shot-store";
13
+ import { SIDECAR, WINDOW_MS, loadState, type OneShotOpts } from "./one-shot";
14
+ import { burstFirst } from "./one-shot-dedup";
15
+
16
+ /**
17
+ * Bump the per-tool failure count. Orthogonal to gates/pending, so it can never
18
+ * distort the one-shot rate.
19
+ * @param s - Current state.
20
+ * @param tool - The failing tool name (`unknown` when absent).
21
+ * @param now - Clock.
22
+ * @returns The next state with `failures[tool]` incremented.
23
+ */
24
+ export function applyFailure(s: OneShotState, tool: string, now: number): OneShotState {
25
+ return { ...s, failures: { ...(s.failures ?? {}), [tool]: (s.failures?.[tool] ?? 0) + 1 }, updatedAt: now };
26
+ }
27
+
28
+ /**
29
+ * Persist a PostToolUseFailure into the one-shot sidecar, burst-deduped across the
30
+ * ~11-process fan-out (same window/store as {@link recordOneShot}). Fail-safe: a
31
+ * write error never propagates out of the hook.
32
+ * @param tool - The failing tool name.
33
+ * @param opts - Clock + state dir + optional session id (arms the burst dedup).
34
+ */
35
+ export function recordFailure(tool: string, opts: OneShotOpts): void {
36
+ try {
37
+ if (!burstFirst(`fail:${tool}`, "failure", opts)) return;
38
+ const path = join(opts.dir, SIDECAR);
39
+ const next = applyFailure(pruneState(loadState(path), opts.now, WINDOW_MS), tool, opts.now);
40
+ atomicWrite(path, JSON.stringify(next));
41
+ } catch { /* fail-safe: metrics never break the hook */ }
42
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * @module one-shot-store
3
+ * Pure, IO-free state model for the per-gate one-shot metric ({@link module:one-shot}).
4
+ *
5
+ * Measures the owner's "precision / one-shot" criterion: per gate (keyed by the
6
+ * block prompt's title) how many times it DENIED and how many were later cleared
7
+ * by an allow of the same operation (a fix). Globally: `firstTry` (a gateable op
8
+ * that passed with NO prior deny = one-shot) vs `corrected` (passed after a deny).
9
+ * The store ({@link module:one-shot}) loads, persists, and wires this into the gate.
10
+ * @packageDocumentation
11
+ */
12
+
13
+ /** Per-gate tally: total denies, denies later fixed by an allow, last-seen clock. */
14
+ export interface GateStat { denies: number; corrected: number; lastTs: number; }
15
+
16
+ /** One open deny awaiting its fix: the blocking gate title and when it landed. */
17
+ export interface Pending { title: string; ts: number; }
18
+
19
+ /** Aggregate one-shot state (a sidecar JSON). `pending` links a deny to its later allow. */
20
+ export interface OneShotState {
21
+ gates: Record<string, GateStat>;
22
+ firstTry: number;
23
+ corrected: number;
24
+ pending: Record<string, Pending>;
25
+ updatedAt: number;
26
+ /** Per-tool PostToolUseFailure tally. Optional — absent in pre-0.1.57 sidecars (additive, back-compatible). */
27
+ failures?: Record<string, number>;
28
+ }
29
+
30
+ /** A fresh, empty state — always spread (`{ ...EMPTY }`) so the const is never shared. */
31
+ export const EMPTY: OneShotState = { gates: {}, firstTry: 0, corrected: 0, pending: {}, updatedAt: 0 };
32
+
33
+ /**
34
+ * Drop stale data: whole-state idle reset past the window, else per-entry prune of
35
+ * gates/pending older than `windowMs`. Keeps the "7d" window honest, bounds size.
36
+ */
37
+ export function pruneState(s: OneShotState, now: number, windowMs: number): OneShotState {
38
+ if (now - s.updatedAt >= windowMs) return { ...EMPTY };
39
+ const gates: Record<string, GateStat> = {};
40
+ for (const [k, g] of Object.entries(s.gates)) if (now - g.lastTs < windowMs) gates[k] = g;
41
+ const pending: Record<string, Pending> = {};
42
+ for (const [k, p] of Object.entries(s.pending)) if (now - p.ts < windowMs) pending[k] = p;
43
+ return { ...s, gates, pending };
44
+ }
45
+
46
+ /**
47
+ * Record a deny for gate `title` on operation `op` (content-free tool identity):
48
+ * bump the gate's deny count and mark `op` pending for a later fix.
49
+ */
50
+ export function applyDeny(s: OneShotState, title: string, op: string, now: number): OneShotState {
51
+ const g = s.gates[title] ?? { denies: 0, corrected: 0, lastTs: 0 };
52
+ return {
53
+ ...s,
54
+ gates: { ...s.gates, [title]: { denies: g.denies + 1, corrected: g.corrected, lastTs: now } },
55
+ pending: { ...s.pending, [op]: { title, ts: now } },
56
+ updatedAt: now,
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Record an allow for a gateable `op`. A non-gateable allow (Read/Task/MCP) leaves
62
+ * state untouched — it never counts and never clears a pending deny. Otherwise: a
63
+ * pending deny → `corrected` (a fix, credited to the blocking gate); no pending →
64
+ * `firstTry` (one-shot).
65
+ */
66
+ export function applyAllow(s: OneShotState, op: string, now: number, gateable: boolean): OneShotState {
67
+ if (!gateable) return s;
68
+ const pend = s.pending[op];
69
+ if (pend) {
70
+ const g = s.gates[pend.title] ?? { denies: 0, corrected: 0, lastTs: 0 };
71
+ const { [op]: _drop, ...pending } = s.pending;
72
+ return {
73
+ ...s,
74
+ gates: { ...s.gates, [pend.title]: { ...g, corrected: g.corrected + 1, lastTs: now } },
75
+ corrected: s.corrected + 1, pending, updatedAt: now,
76
+ };
77
+ }
78
+ return gateable ? { ...s, firstTry: s.firstTry + 1, updatedAt: now } : s;
79
+ }
80
+
81
+ /**
82
+ * Compact injectable summary (one line); "" when there is nothing to report.
83
+ * @returns e.g. `gates 7d: 88% one-shot (44/50 clean); SOLID file-size limit 4den/3fix`.
84
+ */
85
+ export function formatSummary(s: OneShotState): string {
86
+ const keys = Object.keys(s.gates);
87
+ const total = s.firstTry + s.corrected;
88
+ if (keys.length === 0 && total === 0) return "";
89
+ const head = total > 0 ? `${Math.round((s.firstTry / total) * 100)}% one-shot (${s.firstTry}/${total} clean)` : "no clean pass yet";
90
+ const parts = keys
91
+ .map((k) => ({ k, g: s.gates[k]! }))
92
+ .sort((a, b) => b.g.denies - a.g.denies)
93
+ .map(({ k, g }) => `${k} ${g.denies}den/${g.corrected}fix`);
94
+ return `gates 7d: ${head}${parts.length ? `; ${parts.join("; ")}` : ""}`;
95
+ }