@fusengine/harness 0.1.89 → 0.1.91

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (419) hide show
  1. package/dist/adapters/claude/index.d.mts +1 -1
  2. package/dist/adapters/claude/index.mjs +1 -1
  3. package/dist/adapters/cline/index.mjs +1 -1
  4. package/dist/adapters/codex/index.d.mts +2 -2
  5. package/dist/adapters/codex/index.mjs +5 -4
  6. package/dist/adapters/cursor/index.d.mts +15 -28
  7. package/dist/adapters/cursor/index.mjs +35 -46
  8. package/dist/adapters/gemini/index.mjs +1 -1
  9. package/dist/adapters/hermes/index.d.mts +1 -1
  10. package/dist/adapters/hermes/index.mjs +1 -1
  11. package/dist/adapters/kimi/index.d.mts +1 -1
  12. package/dist/adapters/kimi/index.mjs +1 -1
  13. package/dist/{claude-5bC8TDAz.mjs → claude-Ckv2_TgP.mjs} +3 -43
  14. package/dist/cli/bin.mjs +429 -16
  15. package/dist/cli/index.mjs +1 -1
  16. package/dist/command-string-CALMTnwN.mjs +42 -0
  17. package/dist/{evaluate-C1VSI5pb.mjs → evaluate-CMiIqHeH.mjs} +261 -13
  18. package/dist/{handle-DHhukRdh.mjs → handle-C43gA-Pr.mjs} +737 -150
  19. package/dist/{hermes-kP6NUQlG.mjs → hermes-B9-p_3IF.mjs} +3 -2
  20. package/dist/{index-DBgnIZn9.d.mts → index-B8PG82su.d.mts} +1 -1
  21. package/dist/{index-D4C2-IIc.d.mts → index-SmKYkk2N.d.mts} +1 -1
  22. package/dist/index.d.mts +2 -2
  23. package/dist/index.mjs +2 -2
  24. package/dist/init/index.d.mts +1 -1
  25. package/dist/init/index.mjs +1 -1
  26. package/dist/{kimi-D6X7b8wF.mjs → kimi-G2wcSh5-.mjs} +3 -2
  27. package/dist/normalize-BjG6unTj.mjs +317 -0
  28. package/dist/permission-mode-B-SFyR0X.mjs +226 -0
  29. package/dist/policy/index.d.mts +1 -1
  30. package/dist/policy/index.mjs +2 -2
  31. package/dist/prompt/index.d.mts +1 -1
  32. package/dist/{run-CdIlUtjv.mjs → run-DkrzC0gb.mjs} +1 -1
  33. package/dist/{run-TAXNRSpD.mjs → run-GHAMqcOn.mjs} +26 -3
  34. package/dist/runtime/index.d.mts +27 -16
  35. package/dist/runtime/index.mjs +2 -2
  36. package/dist/{types-C0-igRKr.d.mts → types-Bh0jEF3_.d.mts} +2 -0
  37. package/dist/{validate-DZFtAbLP.mjs → validate-KjZ1X9tH.mjs} +1 -1
  38. package/package.json +3 -2
  39. package/src/adapters/claude/index.ts +92 -0
  40. package/src/adapters/cline/index.ts +36 -0
  41. package/src/adapters/codex/apply-patch.ts +94 -0
  42. package/src/adapters/codex/index.ts +84 -0
  43. package/src/adapters/codex/permission-mode.ts +16 -0
  44. package/src/adapters/cursor/context.ts +46 -0
  45. package/src/adapters/cursor/events.ts +38 -0
  46. package/src/adapters/cursor/index.ts +43 -0
  47. package/src/adapters/cursor/interfaces/types.ts +88 -0
  48. package/src/adapters/cursor/native-response.ts +158 -0
  49. package/src/adapters/cursor/normalize.ts +123 -0
  50. package/src/adapters/cursor/respond.ts +123 -0
  51. package/src/adapters/gemini/index.ts +36 -0
  52. package/src/adapters/hermes/index.ts +55 -0
  53. package/src/adapters/hermes/interfaces/types.ts +26 -0
  54. package/src/adapters/kimi/index.ts +96 -0
  55. package/src/adapters/kimi/interfaces/types.ts +55 -0
  56. package/src/cache/compact.ts +55 -0
  57. package/src/cache/index.ts +5 -0
  58. package/src/cache/io.ts +38 -0
  59. package/src/cache/mcp-response.ts +39 -0
  60. package/src/cache/mcp-store.ts +70 -0
  61. package/src/cache/store.ts +93 -0
  62. package/src/changelog/fetch.ts +79 -0
  63. package/src/cli/bin.ts +100 -0
  64. package/src/cli/cursor-event-scanner.ts +175 -0
  65. package/src/cli/cursor-stdin-reader.ts +65 -0
  66. package/src/cli/doctor.ts +96 -0
  67. package/src/cli/hook-io.ts +130 -0
  68. package/src/cli/hook-sound.ts +49 -0
  69. package/src/cli/index.ts +1 -0
  70. package/src/cli/json-primitive-scanner.ts +83 -0
  71. package/src/cli/run.ts +36 -0
  72. package/src/cli/scope.ts +31 -0
  73. package/src/codex-rules/build.ts +63 -0
  74. package/src/codex-rules/index.ts +8 -0
  75. package/src/codex-rules/mapping.ts +42 -0
  76. package/src/codex-rules/rules/absolute-paths.ts +52 -0
  77. package/src/codex-rules/rules/ask.ts +44 -0
  78. package/src/codex-rules/rules/chmod-ask.ts +57 -0
  79. package/src/codex-rules/rules/dd.ts +23 -0
  80. package/src/codex-rules/rules/diskutil.ts +33 -0
  81. package/src/codex-rules/rules/mkfs.ts +42 -0
  82. package/src/codex-rules/rules/privilege.ts +49 -0
  83. package/src/codex-rules/rules/remote-fetch.ts +31 -0
  84. package/src/codex-rules/rules/rm-variants.ts +58 -0
  85. package/src/codex-rules/rules/single-commands.ts +57 -0
  86. package/src/codex-rules/skip-list.ts +25 -0
  87. package/src/codex-rules/starlark.ts +46 -0
  88. package/src/codex-rules/types.ts +36 -0
  89. package/src/config/dotenv.ts +55 -0
  90. package/src/config/env.ts +10 -0
  91. package/src/config/home-dir.ts +36 -0
  92. package/src/config/index.ts +5 -0
  93. package/src/config/layout.ts +52 -0
  94. package/src/config/limits.ts +56 -0
  95. package/src/config/ttl.ts +30 -0
  96. package/src/detect/harness.ts +73 -0
  97. package/src/detect/index.ts +1 -0
  98. package/src/detect/interfaces/types.ts +18 -0
  99. package/src/freshness/agent-evidence-record.ts +95 -0
  100. package/src/freshness/agent-evidence.ts +95 -0
  101. package/src/freshness/codex-spawn-evidence.ts +91 -0
  102. package/src/freshness/doc-helpers.ts +90 -0
  103. package/src/freshness/evidence-harvest-io.ts +72 -0
  104. package/src/freshness/evidence-harvest.ts +61 -0
  105. package/src/freshness/explore-tools.ts +95 -0
  106. package/src/freshness/index.ts +2 -0
  107. package/src/freshness/query-framework.ts +73 -0
  108. package/src/freshness/ref-evidence.ts +49 -0
  109. package/src/freshness/ref-journal.ts +98 -0
  110. package/src/freshness/trivial-edit-counter.ts +30 -0
  111. package/src/index.ts +11 -0
  112. package/src/init/index.ts +2 -0
  113. package/src/init/run.ts +32 -0
  114. package/src/init/templates.ts +75 -0
  115. package/src/memory/gitignore.ts +14 -0
  116. package/src/memory/index.ts +3 -0
  117. package/src/memory/registry.ts +39 -0
  118. package/src/memory/session-roots.ts +99 -0
  119. package/src/memory/state.ts +58 -0
  120. package/src/policy/apex-authorization.ts +100 -0
  121. package/src/policy/apex-gates.ts +91 -0
  122. package/src/policy/apex-target-steps.ts +73 -0
  123. package/src/policy/apex-target.ts +49 -0
  124. package/src/policy/apex-task-context.ts +81 -0
  125. package/src/policy/apex.ts +94 -0
  126. package/src/policy/cartographer/build-tree.ts +69 -0
  127. package/src/policy/cartographer/describe.ts +53 -0
  128. package/src/policy/cartographer/entry.ts +38 -0
  129. package/src/policy/cartographer/frontmatter.ts +48 -0
  130. package/src/policy/cartographer/indicators.ts +34 -0
  131. package/src/policy/claude-md-context.ts +66 -0
  132. package/src/policy/conventions/index.ts +11 -0
  133. package/src/policy/conventions/interfaces.ts +71 -0
  134. package/src/policy/conventions/langs.ts +53 -0
  135. package/src/policy/conventions/query.ts +49 -0
  136. package/src/policy/conventions/react-hooks.ts +18 -0
  137. package/src/policy/conventions/store-or-query.ts +21 -0
  138. package/src/policy/conventions/stores.ts +56 -0
  139. package/src/policy/conventions/strip.ts +87 -0
  140. package/src/policy/conventions/verdict.ts +38 -0
  141. package/src/policy/creation-intent.ts +11 -0
  142. package/src/policy/deny-loop.ts +88 -0
  143. package/src/policy/design/allowed-write.ts +28 -0
  144. package/src/policy/design/content-checks.ts +45 -0
  145. package/src/policy/design/corpus-fs.ts +21 -0
  146. package/src/policy/design/corpus-resolve-cache.ts +49 -0
  147. package/src/policy/design/corpus-resolve.ts +99 -0
  148. package/src/policy/design/corpus.ts +92 -0
  149. package/src/policy/design/design-system-rules.ts +45 -0
  150. package/src/policy/design/flag.ts +30 -0
  151. package/src/policy/design/gates-pipeline.ts +105 -0
  152. package/src/policy/design/gates.ts +87 -0
  153. package/src/policy/design/screenshot-tools.ts +24 -0
  154. package/src/policy/design/skill-evidence.ts +34 -0
  155. package/src/policy/design/skill-gate.ts +81 -0
  156. package/src/policy/design/skill-triggers.ts +51 -0
  157. package/src/policy/design/state.ts +92 -0
  158. package/src/policy/design/template-urls.ts +54 -0
  159. package/src/policy/design/transitions.ts +68 -0
  160. package/src/policy/detect-claude-md-project-type.ts +32 -0
  161. package/src/policy/detect-framework.ts +99 -0
  162. package/src/policy/detect-primitive-checks.ts +100 -0
  163. package/src/policy/detect-primitive-lib.ts +55 -0
  164. package/src/policy/detect-project.ts +99 -0
  165. package/src/policy/edit-outcome.ts +53 -0
  166. package/src/policy/evaluate.ts +81 -0
  167. package/src/policy/expert-agents.ts +60 -0
  168. package/src/policy/file-size-scope.ts +32 -0
  169. package/src/policy/file-size.ts +99 -0
  170. package/src/policy/framework-solid-exclude.ts +52 -0
  171. package/src/policy/framework-solid-extended.ts +93 -0
  172. package/src/policy/framework-solid-gates-systems.ts +99 -0
  173. package/src/policy/framework-solid-gates.ts +98 -0
  174. package/src/policy/framework-solid.ts +88 -0
  175. package/src/policy/gemini-mcp-gate.ts +83 -0
  176. package/src/policy/git-gates.ts +40 -0
  177. package/src/policy/guards/bash-command-anchor.ts +40 -0
  178. package/src/policy/guards/bash-write-patterns.ts +140 -0
  179. package/src/policy/guards/bash-write-redirects.ts +201 -0
  180. package/src/policy/guards/bash-write-safe-paths.ts +62 -0
  181. package/src/policy/guards/bash-write.ts +76 -0
  182. package/src/policy/guards/context.ts +12 -0
  183. package/src/policy/guards/index.ts +61 -0
  184. package/src/policy/guards/install.ts +22 -0
  185. package/src/policy/guards/interface-separation-ext.ts +41 -0
  186. package/src/policy/guards/interface-separation.ts +100 -0
  187. package/src/policy/guards/protected-path.ts +100 -0
  188. package/src/policy/guards/security.ts +97 -0
  189. package/src/policy/index.ts +19 -0
  190. package/src/policy/interfaces/types.ts +46 -0
  191. package/src/policy/js-gate-route.ts +51 -0
  192. package/src/policy/lessons/lesson-gate.ts +0 -0
  193. package/src/policy/lessons/trigger-index.ts +84 -0
  194. package/src/policy/lessons/types.ts +31 -0
  195. package/src/policy/module-layout.ts +31 -0
  196. package/src/policy/nearest-manifest.ts +75 -0
  197. package/src/policy/never-approval.ts +95 -0
  198. package/src/policy/patterns.ts +80 -0
  199. package/src/policy/seo/validate.ts +51 -0
  200. package/src/policy/shadcn-project.ts +22 -0
  201. package/src/policy/shadcn-skill-gate.ts +97 -0
  202. package/src/policy/shell-read-refs.ts +96 -0
  203. package/src/policy/skill-path.ts +46 -0
  204. package/src/policy/skill-patterns/laravel.ts +40 -0
  205. package/src/policy/skill-patterns/nextjs.ts +29 -0
  206. package/src/policy/skill-patterns/react.ts +27 -0
  207. package/src/policy/skill-patterns/shadcn-subskills.ts +33 -0
  208. package/src/policy/skill-patterns/shadcn.ts +66 -0
  209. package/src/policy/skill-patterns/swift.ts +30 -0
  210. package/src/policy/skill-patterns/tailwind.ts +40 -0
  211. package/src/policy/skill-source.ts +52 -0
  212. package/src/policy/skill-trigger-patterns.ts +33 -0
  213. package/src/policy/skill-triggers.ts +108 -0
  214. package/src/policy/tailwind-skill-gate.ts +63 -0
  215. package/src/policy/trivial-edits.ts +52 -0
  216. package/src/policy/verbosity.ts +39 -0
  217. package/src/prompt/index.ts +1 -0
  218. package/src/prompt/types.ts +36 -0
  219. package/src/refs/discover.ts +69 -0
  220. package/src/refs/frontmatter.ts +24 -0
  221. package/src/refs/index.ts +4 -0
  222. package/src/refs/loader.ts +59 -0
  223. package/src/refs/ref-key.ts +41 -0
  224. package/src/refs/router.ts +63 -0
  225. package/src/refs/types.ts +25 -0
  226. package/src/runtime/activity.ts +99 -0
  227. package/src/runtime/apply-patch-gate.ts +41 -0
  228. package/src/runtime/burst-window.ts +21 -0
  229. package/src/runtime/codex-shell-tool.ts +37 -0
  230. package/src/runtime/command-string.ts +37 -0
  231. package/src/runtime/confirm/codex-confirm.ts +126 -0
  232. package/src/runtime/confirm/codex-prompt-origin.ts +16 -0
  233. package/src/runtime/confirm/confirm-code.ts +33 -0
  234. package/src/runtime/confirm/confirm-gate.ts +65 -0
  235. package/src/runtime/confirm/confirm-irreversible.ts +32 -0
  236. package/src/runtime/confirm/confirm-pending.ts +34 -0
  237. package/src/runtime/confirm/confirm-state.ts +56 -0
  238. package/src/runtime/confirm/confirm-subagent.ts +112 -0
  239. package/src/runtime/confirm/confirm-submit.ts +38 -0
  240. package/src/runtime/core-guards-active.ts +84 -0
  241. package/src/runtime/core-guards-wired.ts +47 -0
  242. package/src/runtime/deny-loop-store.ts +94 -0
  243. package/src/runtime/deny-notice.ts +52 -0
  244. package/src/runtime/design-cache-resolve.ts +51 -0
  245. package/src/runtime/design-content-gate.ts +95 -0
  246. package/src/runtime/design-files-gate.ts +114 -0
  247. package/src/runtime/design-helpers.ts +93 -0
  248. package/src/runtime/design-lifecycle.ts +39 -0
  249. package/src/runtime/design-read-credit.ts +28 -0
  250. package/src/runtime/design.ts +116 -0
  251. package/src/runtime/dev-context.ts +43 -0
  252. package/src/runtime/dry-patterns.ts +32 -0
  253. package/src/runtime/dry.ts +96 -0
  254. package/src/runtime/fragment-registry.ts +33 -0
  255. package/src/runtime/framework-skill-gate.ts +79 -0
  256. package/src/runtime/fs-cleanup.ts +71 -0
  257. package/src/runtime/gate-apex.ts +81 -0
  258. package/src/runtime/gate-helpers.ts +52 -0
  259. package/src/runtime/gate-input.ts +26 -0
  260. package/src/runtime/gate.ts +128 -0
  261. package/src/runtime/handle-post.ts +104 -0
  262. package/src/runtime/handle-pre.ts +137 -0
  263. package/src/runtime/handle-scope-async.ts +29 -0
  264. package/src/runtime/handle-types.ts +27 -0
  265. package/src/runtime/handle.ts +116 -0
  266. package/src/runtime/home-state.ts +55 -0
  267. package/src/runtime/index.ts +18 -0
  268. package/src/runtime/inform.ts +28 -0
  269. package/src/runtime/inject-budget-recap.ts +47 -0
  270. package/src/runtime/inject-budget.ts +81 -0
  271. package/src/runtime/inject-context.ts +85 -0
  272. package/src/runtime/inject-dedup-exclusive.ts +98 -0
  273. package/src/runtime/inject-dedup.ts +89 -0
  274. package/src/runtime/is-agent-tool.ts +19 -0
  275. package/src/runtime/lifecycle/agent-files.ts +49 -0
  276. package/src/runtime/lifecycle/agent-memory.ts +82 -0
  277. package/src/runtime/lifecycle/agent-transcript.ts +80 -0
  278. package/src/runtime/lifecycle/aipilot/analytics.ts +99 -0
  279. package/src/runtime/lifecycle/aipilot/apex-task-store.ts +61 -0
  280. package/src/runtime/lifecycle/aipilot/cache-base.ts +48 -0
  281. package/src/runtime/lifecycle/aipilot/cache-doc.ts +100 -0
  282. package/src/runtime/lifecycle/aipilot/cache-lessons.ts +58 -0
  283. package/src/runtime/lifecycle/aipilot/cache-test.ts +70 -0
  284. package/src/runtime/lifecycle/aipilot/curate-lessons.ts +64 -0
  285. package/src/runtime/lifecycle/aipilot/dispatch-aipilot.ts +100 -0
  286. package/src/runtime/lifecycle/aipilot/doc-cache-gate.ts +69 -0
  287. package/src/runtime/lifecycle/aipilot/inject-apex.ts +98 -0
  288. package/src/runtime/lifecycle/aipilot/inject-doc.ts +62 -0
  289. package/src/runtime/lifecycle/aipilot/inject-explore.ts +64 -0
  290. package/src/runtime/lifecycle/aipilot/inject-lessons.ts +61 -0
  291. package/src/runtime/lifecycle/aipilot/inject-test.ts +48 -0
  292. package/src/runtime/lifecycle/aipilot/lesson-archive.ts +60 -0
  293. package/src/runtime/lifecycle/aipilot/lesson-inject.ts +98 -0
  294. package/src/runtime/lifecycle/aipilot/lesson-parse.ts +74 -0
  295. package/src/runtime/lifecycle/aipilot/lessons.ts +72 -0
  296. package/src/runtime/lifecycle/aipilot/promote-global-lessons.ts +92 -0
  297. package/src/runtime/lifecycle/aipilot/solid-compliance.ts +69 -0
  298. package/src/runtime/lifecycle/aipilot/solid-notice.ts +47 -0
  299. package/src/runtime/lifecycle/aipilot/solid-transcript.ts +60 -0
  300. package/src/runtime/lifecycle/aipilot/source-scan.ts +56 -0
  301. package/src/runtime/lifecycle/aipilot/sync-task.ts +75 -0
  302. package/src/runtime/lifecycle/aipilot/transcript.ts +88 -0
  303. package/src/runtime/lifecycle/aipilot/types.ts +72 -0
  304. package/src/runtime/lifecycle/auto-document-reads.ts +73 -0
  305. package/src/runtime/lifecycle/cartographer/detect.ts +57 -0
  306. package/src/runtime/lifecycle/cartographer/ecosystem-map.ts +56 -0
  307. package/src/runtime/lifecycle/cartographer/fs-util.ts +64 -0
  308. package/src/runtime/lifecycle/cartographer/merge.ts +51 -0
  309. package/src/runtime/lifecycle/cartographer/project-map.ts +49 -0
  310. package/src/runtime/lifecycle/cartographer/scan-hooks.ts +48 -0
  311. package/src/runtime/lifecycle/cartographer/scan.ts +81 -0
  312. package/src/runtime/lifecycle/cartographer/session-start.ts +33 -0
  313. package/src/runtime/lifecycle/cartographer/track-enrichment.ts +45 -0
  314. package/src/runtime/lifecycle/cartographer/write-plugin-map.ts +43 -0
  315. package/src/runtime/lifecycle/cartographer/write-tree.ts +44 -0
  316. package/src/runtime/lifecycle/changelog-research.ts +43 -0
  317. package/src/runtime/lifecycle/check-file-size.ts +61 -0
  318. package/src/runtime/lifecycle/codex-resync/fingerprint.ts +63 -0
  319. package/src/runtime/lifecycle/codex-resync/lock.ts +46 -0
  320. package/src/runtime/lifecycle/codex-resync/managed-destination.ts +39 -0
  321. package/src/runtime/lifecycle/codex-resync/materialize-agents.ts +57 -0
  322. package/src/runtime/lifecycle/codex-resync/plugin-files.ts +65 -0
  323. package/src/runtime/lifecycle/codex-resync/plugin-roots.ts +62 -0
  324. package/src/runtime/lifecycle/codex-resync/resync.ts +45 -0
  325. package/src/runtime/lifecycle/codex-resync/symlink-commands.ts +25 -0
  326. package/src/runtime/lifecycle/dispatch.ts +109 -0
  327. package/src/runtime/lifecycle/failure-lesson.ts +51 -0
  328. package/src/runtime/lifecycle/index.ts +34 -0
  329. package/src/runtime/lifecycle/inject-rules.ts +50 -0
  330. package/src/runtime/lifecycle/instructions-loaded.ts +22 -0
  331. package/src/runtime/lifecycle/kimi-rules-native.ts +44 -0
  332. package/src/runtime/lifecycle/lessons/dispatch.ts +88 -0
  333. package/src/runtime/lifecycle/lessons/reminder.ts +89 -0
  334. package/src/runtime/lifecycle/lessons/state.ts +23 -0
  335. package/src/runtime/lifecycle/memory/agent-lesson.ts +35 -0
  336. package/src/runtime/lifecycle/memory/capture-error.ts +41 -0
  337. package/src/runtime/lifecycle/memory/client.ts +65 -0
  338. package/src/runtime/lifecycle/memory/dispatch.ts +42 -0
  339. package/src/runtime/lifecycle/memory/recall.ts +30 -0
  340. package/src/runtime/lifecycle/memory/salience.ts +31 -0
  341. package/src/runtime/lifecycle/memory/state.ts +49 -0
  342. package/src/runtime/lifecycle/memory/track-ops.ts +15 -0
  343. package/src/runtime/lifecycle/post-compact.ts +38 -0
  344. package/src/runtime/lifecycle/post-edit-ts.ts +49 -0
  345. package/src/runtime/lifecycle/post-tracking.ts +40 -0
  346. package/src/runtime/lifecycle/pre-compact.ts +38 -0
  347. package/src/runtime/lifecycle/rules-root.ts +83 -0
  348. package/src/runtime/lifecycle/security/check-skill.ts +63 -0
  349. package/src/runtime/lifecycle/security/scan-patterns.ts +53 -0
  350. package/src/runtime/lifecycle/security/scan.ts +76 -0
  351. package/src/runtime/lifecycle/security/skill-state.ts +47 -0
  352. package/src/runtime/lifecycle/security/track-mcp.ts +29 -0
  353. package/src/runtime/lifecycle/security/track-skill-read.ts +31 -0
  354. package/src/runtime/lifecycle/seo/post-tool-use.ts +45 -0
  355. package/src/runtime/lifecycle/session-end.ts +17 -0
  356. package/src/runtime/lifecycle/session-start.ts +57 -0
  357. package/src/runtime/lifecycle/snapshot/board.ts +27 -0
  358. package/src/runtime/lifecycle/snapshot/format.ts +60 -0
  359. package/src/runtime/lifecycle/snapshot/git.ts +71 -0
  360. package/src/runtime/lifecycle/snapshot/index.ts +52 -0
  361. package/src/runtime/lifecycle/snapshot/version.ts +34 -0
  362. package/src/runtime/lifecycle/solid-detect.ts +56 -0
  363. package/src/runtime/lifecycle/stop-core.ts +35 -0
  364. package/src/runtime/lifecycle/subagent-cache.ts +61 -0
  365. package/src/runtime/lifecycle/task-completed.ts +94 -0
  366. package/src/runtime/lifecycle/teammate-idle-check.ts +63 -0
  367. package/src/runtime/lifecycle/teammate-idle.ts +28 -0
  368. package/src/runtime/lifecycle/tool-failure.ts +24 -0
  369. package/src/runtime/lifecycle/track-changes.ts +61 -0
  370. package/src/runtime/lifecycle/validate-solid.ts +70 -0
  371. package/src/runtime/lifecycle/validate-tailwind.ts +80 -0
  372. package/src/runtime/lifecycle-bridge.ts +74 -0
  373. package/src/runtime/mcp-key.ts +44 -0
  374. package/src/runtime/mcp-tool-name.ts +92 -0
  375. package/src/runtime/mcp.ts +91 -0
  376. package/src/runtime/modular.ts +77 -0
  377. package/src/runtime/normalize.ts +107 -0
  378. package/src/runtime/notices.ts +91 -0
  379. package/src/runtime/notification-sound.ts +49 -0
  380. package/src/runtime/notifications.ts +77 -0
  381. package/src/runtime/paths.ts +59 -0
  382. package/src/runtime/post-fanout.ts +41 -0
  383. package/src/runtime/post-outcome.ts +61 -0
  384. package/src/runtime/pre-allow.ts +97 -0
  385. package/src/runtime/precommit.ts +60 -0
  386. package/src/runtime/prompt-text.ts +38 -0
  387. package/src/runtime/receipt-capture.ts +34 -0
  388. package/src/runtime/record.ts +26 -0
  389. package/src/runtime/respond.ts +92 -0
  390. package/src/runtime/solid-file-size-gate.ts +58 -0
  391. package/src/runtime/solid-pre.ts +53 -0
  392. package/src/runtime/storage.ts +9 -0
  393. package/src/statusline/colors.ts +31 -0
  394. package/src/statusline/constants.ts +17 -0
  395. package/src/statusline/formatters.ts +54 -0
  396. package/src/statusline/index.ts +4 -0
  397. package/src/statusline/progress-bar.ts +46 -0
  398. package/src/tracking/codex-post-failure.ts +73 -0
  399. package/src/tracking/index.ts +2 -0
  400. package/src/tracking/integrity.ts +99 -0
  401. package/src/tracking/one-shot-dedup.ts +34 -0
  402. package/src/tracking/one-shot-failure.ts +42 -0
  403. package/src/tracking/one-shot-store.ts +95 -0
  404. package/src/tracking/one-shot.ts +93 -0
  405. package/src/tracking/receipts.ts +95 -0
  406. package/src/tracking/session-state.ts +94 -0
  407. package/src/tracking/store.ts +96 -0
  408. package/src/tracking/track-compact.ts +100 -0
  409. package/src/tracking/track-diff.ts +50 -0
  410. package/src/tracking/track-journal.ts +97 -0
  411. package/src/tracking/track-lock-sync.ts +83 -0
  412. package/src/tracking/track-lock.ts +67 -0
  413. package/src/util/compact-json.ts +23 -0
  414. package/src/util/index.ts +2 -0
  415. package/src/util/json-io.ts +47 -0
  416. package/src/util/project-root.ts +41 -0
  417. package/src/util/runtime-io.ts +85 -0
  418. package/src/util/semver.ts +30 -0
  419. package/dist/permission-mode-B0MUl7FT.mjs +0 -136
@@ -0,0 +1,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
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * @module one-shot
3
+ * Sidecar store + gate wiring for the per-gate one-shot metric.
4
+ *
5
+ * STATE — a standalone sidecar (`one-shot.json`) in the same per-project state dir
6
+ * as the session track, mirroring {@link module:deny-loop-store} (atomicWrite,
7
+ * prune-by-window, fail-safe). A write error NEVER changes a gate decision nor its
8
+ * prompt — metrics are pure observation.
9
+ *
10
+ * KEY — the operation identity is content-FREE (`tool + filePath/command`): a fix
11
+ * changes the content, so a content hash would make every retry a new op and hide
12
+ * the deny→allow transition this metric exists to see. The pure model lives in
13
+ * {@link module:one-shot-store}; this file is the only IO surface.
14
+ * @packageDocumentation
15
+ */
16
+ import { existsSync, readFileSync } from "node:fs";
17
+ import { join } from "node:path";
18
+ import { atomicWrite } from "../util/json-io";
19
+ import { denyHash } from "../policy/deny-loop";
20
+ import { defaultStateDir } from "../runtime/paths";
21
+ import { applyAllow, applyDeny, EMPTY, formatSummary, pruneState, type OneShotState } from "./one-shot-store";
22
+ import { burstFirst } from "./one-shot-dedup";
23
+ import type { Prompt } from "../prompt/types";
24
+
25
+ /** Sidecar basename under the per-project state dir. */
26
+ export const SIDECAR = "one-shot.json";
27
+
28
+ /** Retention window: 7 days. Aggregates and pending denies older than this are pruned. */
29
+ export const WINDOW_MS: number = 7 * 24 * 60 * 60 * 1000;
30
+
31
+ /**
32
+ * Injected clock + state dir (no env var; the gate supplies both). `sessionId` —
33
+ * when present — arms the burst dedup ({@link module:one-shot-dedup}) so the ~11
34
+ * sibling plugin hooks of ONE event count once, not once each. Omitted by
35
+ * mono-process callers/tests → un-deduped historical behaviour.
36
+ */
37
+ export interface OneShotOpts { now: number; dir: string; sessionId?: string; }
38
+
39
+ /** Identifying tool input (content-free); `content` only decides gateability. */
40
+ export interface OneShotInput { filePath?: string; content?: string; command?: string; }
41
+
42
+ /** Load the state, or a fresh copy when missing/corrupt. */
43
+ export function loadState(path: string): OneShotState {
44
+ try {
45
+ if (!existsSync(path)) return { ...EMPTY };
46
+ const d: unknown = JSON.parse(readFileSync(path, "utf8"));
47
+ return d && typeof d === "object" && !Array.isArray(d) ? { ...EMPTY, ...(d as OneShotState) } : { ...EMPTY };
48
+ } catch {
49
+ return { ...EMPTY };
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Record a gate outcome: a `block` is a deny for its gate title; a `null` allow is
55
+ * a fix (if the op was pending) or a one-shot (if gateable). `ask`/`inform` are
56
+ * neither and are skipped. Fails silently — a metric write NEVER affects a decision.
57
+ *
58
+ * The op key is tool-INDEPENDENT (`filePath`/`command` only, constant `"op"` tool):
59
+ * a deny (a `Write`) and its fix (an `Edit`) on the same file must link.
60
+ * @param prompt - The gate's outcome (block, allow=null, or ask/inform).
61
+ * @param input - Identifying tool input (content decides gateability only).
62
+ * @param opts - Clock + state dir.
63
+ */
64
+ export function recordOneShot(prompt: Prompt | null, input: OneShotInput, opts: OneShotOpts): void {
65
+ try {
66
+ if (prompt && prompt.kind !== "block") return;
67
+ const op = denyHash("op", { filePath: input.filePath, command: input.command });
68
+ if (!burstFirst(op, prompt ? `deny:${prompt.title}` : "allow", opts)) return;
69
+ const path = join(opts.dir, SIDECAR);
70
+ let s = pruneState(loadState(path), opts.now, WINDOW_MS);
71
+ s = prompt
72
+ ? applyDeny(s, prompt.title, op, opts.now)
73
+ : applyAllow(s, op, opts.now, input.content != null || input.command != null);
74
+ atomicWrite(path, JSON.stringify(s));
75
+ } catch { /* fail-safe: metrics never change a gate decision */ }
76
+ }
77
+
78
+ /**
79
+ * Compact, injection-ready one-shot summary for the project rooted at `cwd`. The
80
+ * state dir is derived EXACTLY like the runtime writer ({@link defaultStateDir},
81
+ * mirroring `handle.ts` `trackFile(sid, defaultStateDir(cwd))`), so the file read
82
+ * here is the same one {@link recordOneShot} wrote. "" when no data or read error.
83
+ * @param cwd - The project working directory (Claude `cwd`), NOT the state dir.
84
+ * @returns One line, e.g. `gates 7d: 88% one-shot (44/50 clean); ...`, or "".
85
+ */
86
+ export function oneShotSummary(cwd: string): string {
87
+ try {
88
+ const s = pruneState(loadState(join(defaultStateDir(cwd), SIDECAR)), Date.now(), WINDOW_MS);
89
+ return formatSummary(s);
90
+ } catch {
91
+ return "";
92
+ }
93
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * @module receipts
3
+ * Verification receipts: capture `tsc`/test runs at PostToolUse and query the
4
+ * freshest PASSING one for the TaskCompleted (hard) and SubagentStop (advisory)
5
+ * gates. A "done" over modified code files is refused unless such a receipt
6
+ * exists — mechanising "no proof, no done".
7
+ * @packageDocumentation
8
+ */
9
+ import { withTrack, trackJournalEnabled } from "./store";
10
+ import { readTrackSync } from "./track-compact";
11
+ import type { SessionTrack } from "./session-state";
12
+
13
+ /**
14
+ * A verification receipt captured at PostToolUse from a Bash verification
15
+ * command. Feeds the TaskCompleted receipt gate: a "done" over modified code
16
+ * files is refused unless a fresh, passing receipt (`exitCode === 0`, `fail === 0`)
17
+ * exists. `pass`/`fail` are parsed only for test runs; `tsc` carries the code alone.
18
+ */
19
+ export interface Receipt {
20
+ kind: "tsc" | "test";
21
+ exitCode: number;
22
+ pass?: number;
23
+ fail?: number;
24
+ ts: number;
25
+ }
26
+
27
+ /** `tsc` / `bunx tsc` / `npx tsc -p .` — a type-check invocation. */
28
+ const TSC_RE = /(?:^|\s|\/)(?:bunx\s+|npx\s+|pnpm\s+|yarn\s+)?tsc\b/;
29
+ /** `bun test`, `vitest`, `jest`, `npm test`, `npm run test`, … — a test run. */
30
+ const TEST_RE = /\b(?:bun\s+test|vitest|jest|(?:npm|pnpm|yarn)\s+(?:run\s+)?test)\b/;
31
+
32
+ /** Append a verification receipt to the track. Immutable. */
33
+ export function recordReceipt(track: SessionTrack, receipt: Receipt): SessionTrack {
34
+ return { ...track, receipts: [...(track.receipts ?? []), receipt] };
35
+ }
36
+
37
+ /** Parse `N pass` / `M fail` counts from bun/vitest/jest output (undefined when absent). */
38
+ function parseCounts(output: string): { pass?: number; fail?: number } {
39
+ const pass = output.match(/(\d+)\s+pass/i);
40
+ const fail = output.match(/(\d+)\s+fail/i);
41
+ return { pass: pass ? Number(pass[1]) : undefined, fail: fail ? Number(fail[1]) : undefined };
42
+ }
43
+
44
+ /**
45
+ * Classify a Bash command as a verification receipt, or `null` when it is not a
46
+ * recognised `tsc`/test invocation. Test runs additionally carry parsed
47
+ * pass/fail counts.
48
+ * @param command - The Bash command line.
49
+ * @param output - Combined stdout+stderr (bun writes its summary to stderr).
50
+ * @param exitCode - The command's exit code.
51
+ * @param now - Capture timestamp (epoch ms).
52
+ */
53
+ export function classifyReceipt(command: string, output: string, exitCode: number, now: number): Receipt | null {
54
+ if (TEST_RE.test(command)) {
55
+ const { pass, fail } = parseCounts(output);
56
+ return { kind: "test", exitCode, pass, fail, ts: now };
57
+ }
58
+ if (TSC_RE.test(command)) return { kind: "tsc", exitCode, ts: now };
59
+ return null;
60
+ }
61
+
62
+ /** A receipt PROVES success: exit 0 and (for tests) zero reported failures. */
63
+ function isPassing(r: Receipt): boolean {
64
+ return r.exitCode === 0 && (r.fail ?? 0) === 0;
65
+ }
66
+
67
+ /** The newest passing receipt within `windowMs`, or `null`. */
68
+ export function freshPassingReceipt(track: SessionTrack, windowMs: number, now: number): Receipt | null {
69
+ const cutoff = now - windowMs;
70
+ const hits = (track.receipts ?? []).filter((r) => r.ts > cutoff && isPassing(r));
71
+ return hits.length ? hits.reduce((a, b) => (b.ts > a.ts ? b : a)) : null;
72
+ }
73
+
74
+ /**
75
+ * Sync variant reading the track directly (snapshot ⊕ journal) — for the sync
76
+ * gates (TaskCompleted / SubagentStop). Returns the newest passing receipt, or
77
+ * `null` on any read/verify failure (fail-closed: no proof ⇒ unverified).
78
+ */
79
+ export function freshReceiptFromFile(file: string, windowMs: number, now: number): Receipt | null {
80
+ try {
81
+ return freshPassingReceipt(readTrackSync(file, trackJournalEnabled()), windowMs, now);
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Capture a verification receipt from a PostToolUse Bash command into the signed
89
+ * track (best effort — a non-verification command is a no-op).
90
+ */
91
+ export async function captureReceipt(file: string, command: string, output: string, exitCode: number, now: number): Promise<void> {
92
+ const receipt = classifyReceipt(command, output, exitCode, now);
93
+ if (!receipt) return;
94
+ await withTrack(file, (track) => recordReceipt(track, receipt));
95
+ }
@@ -0,0 +1,94 @@
1
+ import type { AuthEntry } from "../freshness/doc-helpers";
2
+ import { creditDocConsultation, type SessionTarget } from "../policy/apex-authorization";
3
+ import type { Receipt } from "./receipts";
4
+
5
+ /** Quality of a recorded agent call (the freshness gate ignores insufficient ones). */
6
+ export type AgentQuality = "sufficient" | "insufficient";
7
+
8
+ /** Recorded session activity that feeds the APEX gates. */
9
+ export interface SessionTrack {
10
+ authorizations: Record<string, AuthEntry>;
11
+ refsRead: string[];
12
+ /**
13
+ * Epoch-ms timestamp of each reference read, keyed by `refsRead` path (parity
14
+ * track-solid-reads.py `solid_reads[].timestamp`). Optional: tracks persisted
15
+ * before this field existed only carry `refsRead`, and such paths count as
16
+ * read (backward compat). PARITY: only the SOLID-read gate TTL-checks these —
17
+ * Python TTL-izes SOLID reads exclusively (require-solid-read.py); the other
18
+ * refsRead consumers (skill-trigger/shadcn/tailwind/design) stay session-scoped.
19
+ */
20
+ refsReadAt?: Record<string, number>;
21
+ /** Framework awaiting doc credit after a Check-1 deny. PERSISTS — {@link recordDoc} cross-credits it on EVERY consultation, never clears it; only the next deny replaces it via {@link recordTarget} (parity track-doc-consultation.py:62). */
22
+ target?: SessionTarget;
23
+ agents: { name: string; ts: number; quality?: AgentQuality }[];
24
+ trivialEdits: number[];
25
+ brainstormRequired?: boolean;
26
+ /** Verification receipts (tsc/test) at PostToolUse; absent/empty reads as unverified in the TaskCompleted gate (backward compat, fail-closed). See {@link Receipt}. */
27
+ receipts?: Receipt[];
28
+ }
29
+ /** A fresh, empty track. */
30
+ export function emptyTrack(): SessionTrack {
31
+ return { authorizations: {}, refsRead: [], agents: [], trivialEdits: [] };
32
+ }
33
+
34
+ /**
35
+ * Record a doc consultation (Context7/Exa/web) for a framework in this session:
36
+ * the Check-2 credit (`doc_sessions` + `sources`) plus the Check-1 stamp
37
+ * (`sessions` + `doc_consulted` = ISO of `now`). PARITY track-doc-consultation.py
38
+ * :62-70 — a `target` left by a Check-1 deny (enforce-apex-phases.ts:80) is
39
+ * cross-credited on EVERY consultation, with NO TTL on `target.set_at`, and the
40
+ * target PERSISTS (only the next deny replaces it via {@link recordTarget});
41
+ * single-shot clearing or TTL-gating the target re-opened the deny loop when a
42
+ * consultation landed > TTL after the deny. Immutable.
43
+ */
44
+ export function recordDoc(track: SessionTrack, framework: string, sessionId: string, source: string, now: number = Date.now()): SessionTrack {
45
+ const nowIso = new Date(now).toISOString();
46
+ const authorizations = { ...track.authorizations, [framework]: creditDocConsultation(track.authorizations[framework], sessionId, source, nowIso) };
47
+ const t = track.target;
48
+ if (t && t.framework !== framework) {
49
+ authorizations[t.framework] = creditDocConsultation(track.authorizations[t.framework], sessionId, source, nowIso);
50
+ }
51
+ return { ...track, authorizations };
52
+ }
53
+ /** Set the pending doc-credit target (written by the runtime on a Check-1 deny). Immutable. */
54
+ export function recordTarget(track: SessionTrack, target: SessionTarget): SessionTrack {
55
+ return { ...track, target };
56
+ }
57
+
58
+ /**
59
+ * Record that a SOLID reference file was read (deduped). Immutable. When `now`
60
+ * (epoch ms — the tool event's own timestamp, never a fresh `Date.now()`) is
61
+ * supplied, the read is stamped in `refsReadAt`, refreshed on re-reads so the
62
+ * LATEST read drives the SOLID TTL (parity track-solid-reads.py, which appends
63
+ * a timestamped entry per read; require-solid-read.py checks the most recent).
64
+ * Callers that omit `now` keep the legacy untimestamped behavior.
65
+ */
66
+ export function recordRefRead(track: SessionTrack, path: string, now?: number): SessionTrack {
67
+ const refsRead = track.refsRead.includes(path) ? track.refsRead : [...track.refsRead, path];
68
+ if (now === undefined) return refsRead === track.refsRead ? track : { ...track, refsRead };
69
+ return { ...track, refsRead, refsReadAt: { ...track.refsReadAt, [path]: now } };
70
+ }
71
+ /** Record an agent/tool call with a timestamp + optional quality. Immutable. */
72
+ export function recordAgent(track: SessionTrack, name: string, ts: number, quality?: AgentQuality): SessionTrack {
73
+ const entry = quality ? { name, ts, quality } : { name, ts };
74
+ return { ...track, agents: [...track.agents, entry] };
75
+ }
76
+ /** True when ALL of `names` ran within `windowMs` with non-insufficient quality. */
77
+ export function agentsFresh(track: SessionTrack, names: string[], windowMs: number, now: number): boolean {
78
+ const cutoff = now - windowMs;
79
+ return names.every((n) => track.agents.some((a) => a.name === n && a.ts > cutoff && a.quality !== "insufficient"));
80
+ }
81
+ /** Record a trivial edit timestamp (sliding window; old evicted). Immutable. */
82
+ export function recordTrivialEdit(track: SessionTrack, ts: number, windowMs: number, now: number): SessionTrack {
83
+ const cutoff = now - windowMs;
84
+ return { ...track, trivialEdits: [...(track.trivialEdits ?? []).filter((t) => t > cutoff), ts] };
85
+ }
86
+ /** Count trivial edits within the sliding window. */
87
+ export function trivialCount(track: SessionTrack, windowMs: number, now: number): number {
88
+ const cutoff = now - windowMs;
89
+ return (track.trivialEdits ?? []).filter((t) => t > cutoff).length;
90
+ }
91
+ /** Set the brainstorm-required flag (from creation-intent detection). Immutable. */
92
+ export function recordBrainstormRequired(track: SessionTrack, required: boolean): SessionTrack {
93
+ return { ...track, brainstormRequired: required };
94
+ }