@fusengine/harness 0.1.90 → 0.1.92

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (422) hide show
  1. package/dist/adapters/claude/index.d.mts +1 -1
  2. package/dist/adapters/claude/index.mjs +1 -1
  3. package/dist/adapters/cline/index.mjs +1 -1
  4. package/dist/adapters/codex/index.d.mts +2 -2
  5. package/dist/adapters/codex/index.mjs +3 -2
  6. package/dist/adapters/cursor/index.d.mts +15 -28
  7. package/dist/adapters/cursor/index.mjs +35 -46
  8. package/dist/adapters/gemini/index.mjs +1 -1
  9. package/dist/adapters/hermes/index.d.mts +1 -1
  10. package/dist/adapters/hermes/index.mjs +1 -1
  11. package/dist/adapters/kimi/index.d.mts +1 -1
  12. package/dist/adapters/kimi/index.mjs +1 -1
  13. package/dist/{claude-5bC8TDAz.mjs → claude-Ckv2_TgP.mjs} +3 -43
  14. package/dist/cli/bin.mjs +429 -16
  15. package/dist/cli/index.mjs +1 -1
  16. package/dist/command-string-CALMTnwN.mjs +42 -0
  17. package/dist/{evaluate-C1VSI5pb.mjs → evaluate-CMiIqHeH.mjs} +261 -13
  18. package/dist/{handle-CBaOcFZq.mjs → handle-BF1dZFjY.mjs} +1194 -144
  19. package/dist/{hermes-kP6NUQlG.mjs → hermes-B9-p_3IF.mjs} +3 -2
  20. package/dist/{index-DBgnIZn9.d.mts → index-B8PG82su.d.mts} +1 -1
  21. package/dist/{index-D4C2-IIc.d.mts → index-SmKYkk2N.d.mts} +1 -1
  22. package/dist/index.d.mts +2 -2
  23. package/dist/index.mjs +2 -2
  24. package/dist/init/index.d.mts +1 -1
  25. package/dist/init/index.mjs +1 -1
  26. package/dist/{kimi-D6X7b8wF.mjs → kimi-G2wcSh5-.mjs} +3 -2
  27. package/dist/normalize-Dy8g9Ybl.mjs +397 -0
  28. package/dist/policy/index.d.mts +1 -1
  29. package/dist/policy/index.mjs +2 -2
  30. package/dist/prompt/index.d.mts +1 -1
  31. package/dist/{run-CdIlUtjv.mjs → run-DkrzC0gb.mjs} +1 -1
  32. package/dist/{run-TAXNRSpD.mjs → run-GHAMqcOn.mjs} +26 -3
  33. package/dist/runtime/index.d.mts +37 -16
  34. package/dist/runtime/index.mjs +2 -2
  35. package/dist/{types-C0-igRKr.d.mts → types-Bh0jEF3_.d.mts} +2 -0
  36. package/dist/{validate-DZFtAbLP.mjs → validate-KjZ1X9tH.mjs} +1 -1
  37. package/package.json +3 -2
  38. package/src/adapters/claude/index.ts +92 -0
  39. package/src/adapters/cline/index.ts +36 -0
  40. package/src/adapters/codex/apply-patch.ts +94 -0
  41. package/src/adapters/codex/index.ts +84 -0
  42. package/src/adapters/codex/permission-mode.ts +16 -0
  43. package/src/adapters/cursor/context-budget.ts +144 -0
  44. package/src/adapters/cursor/context-limit.ts +115 -0
  45. package/src/adapters/cursor/context.ts +65 -0
  46. package/src/adapters/cursor/events.ts +38 -0
  47. package/src/adapters/cursor/index.ts +43 -0
  48. package/src/adapters/cursor/interfaces/context-budget.ts +25 -0
  49. package/src/adapters/cursor/interfaces/types.ts +88 -0
  50. package/src/adapters/cursor/native-response.ts +39 -0
  51. package/src/adapters/cursor/native-schemas.ts +161 -0
  52. package/src/adapters/cursor/normalize.ts +188 -0
  53. package/src/adapters/cursor/plugin-root.ts +103 -0
  54. package/src/adapters/cursor/respond.ts +170 -0
  55. package/src/adapters/gemini/index.ts +36 -0
  56. package/src/adapters/hermes/index.ts +55 -0
  57. package/src/adapters/hermes/interfaces/types.ts +26 -0
  58. package/src/adapters/kimi/index.ts +96 -0
  59. package/src/adapters/kimi/interfaces/types.ts +55 -0
  60. package/src/cache/compact.ts +55 -0
  61. package/src/cache/index.ts +5 -0
  62. package/src/cache/io.ts +38 -0
  63. package/src/cache/mcp-response.ts +39 -0
  64. package/src/cache/mcp-store.ts +70 -0
  65. package/src/cache/store.ts +93 -0
  66. package/src/changelog/fetch.ts +79 -0
  67. package/src/cli/bin.ts +100 -0
  68. package/src/cli/cursor-event-scanner.ts +175 -0
  69. package/src/cli/cursor-stdin-reader.ts +65 -0
  70. package/src/cli/doctor.ts +96 -0
  71. package/src/cli/hook-io.ts +130 -0
  72. package/src/cli/hook-sound.ts +49 -0
  73. package/src/cli/index.ts +1 -0
  74. package/src/cli/json-primitive-scanner.ts +83 -0
  75. package/src/cli/run.ts +36 -0
  76. package/src/cli/scope.ts +31 -0
  77. package/src/codex-rules/build.ts +63 -0
  78. package/src/codex-rules/index.ts +8 -0
  79. package/src/codex-rules/mapping.ts +42 -0
  80. package/src/codex-rules/rules/absolute-paths.ts +52 -0
  81. package/src/codex-rules/rules/ask.ts +44 -0
  82. package/src/codex-rules/rules/chmod-ask.ts +57 -0
  83. package/src/codex-rules/rules/dd.ts +23 -0
  84. package/src/codex-rules/rules/diskutil.ts +33 -0
  85. package/src/codex-rules/rules/mkfs.ts +42 -0
  86. package/src/codex-rules/rules/privilege.ts +49 -0
  87. package/src/codex-rules/rules/remote-fetch.ts +31 -0
  88. package/src/codex-rules/rules/rm-variants.ts +58 -0
  89. package/src/codex-rules/rules/single-commands.ts +57 -0
  90. package/src/codex-rules/skip-list.ts +25 -0
  91. package/src/codex-rules/starlark.ts +46 -0
  92. package/src/codex-rules/types.ts +36 -0
  93. package/src/config/dotenv.ts +55 -0
  94. package/src/config/env.ts +10 -0
  95. package/src/config/home-dir.ts +36 -0
  96. package/src/config/index.ts +5 -0
  97. package/src/config/layout.ts +52 -0
  98. package/src/config/limits.ts +56 -0
  99. package/src/config/ttl.ts +30 -0
  100. package/src/detect/harness.ts +73 -0
  101. package/src/detect/index.ts +1 -0
  102. package/src/detect/interfaces/types.ts +18 -0
  103. package/src/freshness/agent-evidence-record.ts +95 -0
  104. package/src/freshness/agent-evidence.ts +95 -0
  105. package/src/freshness/codex-spawn-evidence.ts +91 -0
  106. package/src/freshness/doc-helpers.ts +90 -0
  107. package/src/freshness/evidence-harvest-io.ts +72 -0
  108. package/src/freshness/evidence-harvest.ts +61 -0
  109. package/src/freshness/explore-tools.ts +95 -0
  110. package/src/freshness/index.ts +2 -0
  111. package/src/freshness/query-framework.ts +73 -0
  112. package/src/freshness/ref-evidence.ts +49 -0
  113. package/src/freshness/ref-journal.ts +98 -0
  114. package/src/freshness/trivial-edit-counter.ts +30 -0
  115. package/src/index.ts +11 -0
  116. package/src/init/index.ts +2 -0
  117. package/src/init/run.ts +32 -0
  118. package/src/init/templates.ts +75 -0
  119. package/src/memory/gitignore.ts +14 -0
  120. package/src/memory/index.ts +3 -0
  121. package/src/memory/registry.ts +39 -0
  122. package/src/memory/session-roots.ts +99 -0
  123. package/src/memory/state.ts +58 -0
  124. package/src/policy/apex-authorization.ts +100 -0
  125. package/src/policy/apex-gates.ts +91 -0
  126. package/src/policy/apex-target-steps.ts +73 -0
  127. package/src/policy/apex-target.ts +49 -0
  128. package/src/policy/apex-task-context.ts +81 -0
  129. package/src/policy/apex.ts +94 -0
  130. package/src/policy/cartographer/build-tree.ts +69 -0
  131. package/src/policy/cartographer/describe.ts +53 -0
  132. package/src/policy/cartographer/entry.ts +38 -0
  133. package/src/policy/cartographer/frontmatter.ts +48 -0
  134. package/src/policy/cartographer/indicators.ts +34 -0
  135. package/src/policy/claude-md-context.ts +66 -0
  136. package/src/policy/conventions/index.ts +11 -0
  137. package/src/policy/conventions/interfaces.ts +71 -0
  138. package/src/policy/conventions/langs.ts +53 -0
  139. package/src/policy/conventions/query.ts +49 -0
  140. package/src/policy/conventions/react-hooks.ts +18 -0
  141. package/src/policy/conventions/store-or-query.ts +21 -0
  142. package/src/policy/conventions/stores.ts +56 -0
  143. package/src/policy/conventions/strip.ts +87 -0
  144. package/src/policy/conventions/verdict.ts +38 -0
  145. package/src/policy/creation-intent.ts +11 -0
  146. package/src/policy/deny-loop.ts +88 -0
  147. package/src/policy/design/allowed-write.ts +28 -0
  148. package/src/policy/design/content-checks.ts +45 -0
  149. package/src/policy/design/corpus-fs.ts +21 -0
  150. package/src/policy/design/corpus-resolve-cache.ts +49 -0
  151. package/src/policy/design/corpus-resolve.ts +99 -0
  152. package/src/policy/design/corpus.ts +92 -0
  153. package/src/policy/design/design-system-rules.ts +45 -0
  154. package/src/policy/design/flag.ts +30 -0
  155. package/src/policy/design/gates-pipeline.ts +105 -0
  156. package/src/policy/design/gates.ts +87 -0
  157. package/src/policy/design/screenshot-tools.ts +24 -0
  158. package/src/policy/design/skill-evidence.ts +34 -0
  159. package/src/policy/design/skill-gate.ts +81 -0
  160. package/src/policy/design/skill-triggers.ts +51 -0
  161. package/src/policy/design/state.ts +92 -0
  162. package/src/policy/design/template-urls.ts +54 -0
  163. package/src/policy/design/transitions.ts +68 -0
  164. package/src/policy/detect-claude-md-project-type.ts +32 -0
  165. package/src/policy/detect-framework.ts +99 -0
  166. package/src/policy/detect-primitive-checks.ts +100 -0
  167. package/src/policy/detect-primitive-lib.ts +55 -0
  168. package/src/policy/detect-project.ts +99 -0
  169. package/src/policy/edit-outcome.ts +53 -0
  170. package/src/policy/evaluate.ts +81 -0
  171. package/src/policy/expert-agents.ts +60 -0
  172. package/src/policy/file-size-scope.ts +32 -0
  173. package/src/policy/file-size.ts +99 -0
  174. package/src/policy/framework-solid-exclude.ts +52 -0
  175. package/src/policy/framework-solid-extended.ts +93 -0
  176. package/src/policy/framework-solid-gates-systems.ts +99 -0
  177. package/src/policy/framework-solid-gates.ts +98 -0
  178. package/src/policy/framework-solid.ts +88 -0
  179. package/src/policy/gemini-mcp-gate.ts +83 -0
  180. package/src/policy/git-gates.ts +40 -0
  181. package/src/policy/guards/bash-command-anchor.ts +40 -0
  182. package/src/policy/guards/bash-write-patterns.ts +140 -0
  183. package/src/policy/guards/bash-write-redirects.ts +201 -0
  184. package/src/policy/guards/bash-write-safe-paths.ts +62 -0
  185. package/src/policy/guards/bash-write.ts +76 -0
  186. package/src/policy/guards/context.ts +12 -0
  187. package/src/policy/guards/index.ts +61 -0
  188. package/src/policy/guards/install.ts +22 -0
  189. package/src/policy/guards/interface-separation-ext.ts +41 -0
  190. package/src/policy/guards/interface-separation.ts +100 -0
  191. package/src/policy/guards/protected-path.ts +100 -0
  192. package/src/policy/guards/security.ts +97 -0
  193. package/src/policy/index.ts +19 -0
  194. package/src/policy/interfaces/types.ts +46 -0
  195. package/src/policy/js-gate-route.ts +51 -0
  196. package/src/policy/lessons/lesson-gate.ts +0 -0
  197. package/src/policy/lessons/trigger-index.ts +84 -0
  198. package/src/policy/lessons/types.ts +31 -0
  199. package/src/policy/module-layout.ts +31 -0
  200. package/src/policy/nearest-manifest.ts +75 -0
  201. package/src/policy/never-approval.ts +95 -0
  202. package/src/policy/patterns.ts +80 -0
  203. package/src/policy/seo/validate.ts +51 -0
  204. package/src/policy/shadcn-project.ts +22 -0
  205. package/src/policy/shadcn-skill-gate.ts +97 -0
  206. package/src/policy/shell-read-refs.ts +96 -0
  207. package/src/policy/skill-path.ts +46 -0
  208. package/src/policy/skill-patterns/laravel.ts +40 -0
  209. package/src/policy/skill-patterns/nextjs.ts +29 -0
  210. package/src/policy/skill-patterns/react.ts +27 -0
  211. package/src/policy/skill-patterns/shadcn-subskills.ts +33 -0
  212. package/src/policy/skill-patterns/shadcn.ts +66 -0
  213. package/src/policy/skill-patterns/swift.ts +30 -0
  214. package/src/policy/skill-patterns/tailwind.ts +40 -0
  215. package/src/policy/skill-source.ts +52 -0
  216. package/src/policy/skill-trigger-patterns.ts +33 -0
  217. package/src/policy/skill-triggers.ts +108 -0
  218. package/src/policy/tailwind-skill-gate.ts +63 -0
  219. package/src/policy/trivial-edits.ts +52 -0
  220. package/src/policy/verbosity.ts +39 -0
  221. package/src/prompt/index.ts +1 -0
  222. package/src/prompt/types.ts +36 -0
  223. package/src/refs/discover.ts +69 -0
  224. package/src/refs/frontmatter.ts +24 -0
  225. package/src/refs/index.ts +4 -0
  226. package/src/refs/loader.ts +59 -0
  227. package/src/refs/ref-key.ts +41 -0
  228. package/src/refs/router.ts +63 -0
  229. package/src/refs/types.ts +25 -0
  230. package/src/runtime/activity.ts +99 -0
  231. package/src/runtime/apply-patch-gate.ts +41 -0
  232. package/src/runtime/burst-window.ts +21 -0
  233. package/src/runtime/codex-shell-tool.ts +37 -0
  234. package/src/runtime/command-string.ts +37 -0
  235. package/src/runtime/confirm/codex-confirm.ts +126 -0
  236. package/src/runtime/confirm/codex-prompt-origin.ts +16 -0
  237. package/src/runtime/confirm/confirm-code.ts +33 -0
  238. package/src/runtime/confirm/confirm-gate.ts +65 -0
  239. package/src/runtime/confirm/confirm-irreversible.ts +32 -0
  240. package/src/runtime/confirm/confirm-pending.ts +34 -0
  241. package/src/runtime/confirm/confirm-state.ts +56 -0
  242. package/src/runtime/confirm/confirm-subagent.ts +112 -0
  243. package/src/runtime/confirm/confirm-submit.ts +38 -0
  244. package/src/runtime/core-guards-active.ts +84 -0
  245. package/src/runtime/core-guards-wired.ts +47 -0
  246. package/src/runtime/deny-loop-store.ts +94 -0
  247. package/src/runtime/deny-notice.ts +52 -0
  248. package/src/runtime/design-cache-resolve.ts +51 -0
  249. package/src/runtime/design-content-gate.ts +95 -0
  250. package/src/runtime/design-files-gate.ts +114 -0
  251. package/src/runtime/design-helpers.ts +93 -0
  252. package/src/runtime/design-lifecycle.ts +39 -0
  253. package/src/runtime/design-read-credit.ts +28 -0
  254. package/src/runtime/design.ts +116 -0
  255. package/src/runtime/dev-context.ts +43 -0
  256. package/src/runtime/dry-patterns.ts +32 -0
  257. package/src/runtime/dry.ts +96 -0
  258. package/src/runtime/fragment-registry.ts +33 -0
  259. package/src/runtime/framework-skill-gate.ts +79 -0
  260. package/src/runtime/fs-cleanup.ts +71 -0
  261. package/src/runtime/gate-apex.ts +81 -0
  262. package/src/runtime/gate-helpers.ts +52 -0
  263. package/src/runtime/gate-input.ts +26 -0
  264. package/src/runtime/gate.ts +128 -0
  265. package/src/runtime/handle-post.ts +104 -0
  266. package/src/runtime/handle-pre.ts +137 -0
  267. package/src/runtime/handle-scope-async.ts +29 -0
  268. package/src/runtime/handle-types.ts +27 -0
  269. package/src/runtime/handle.ts +197 -0
  270. package/src/runtime/home-state.ts +55 -0
  271. package/src/runtime/index.ts +18 -0
  272. package/src/runtime/inform.ts +28 -0
  273. package/src/runtime/inject-budget-recap.ts +47 -0
  274. package/src/runtime/inject-budget.ts +81 -0
  275. package/src/runtime/inject-context.ts +85 -0
  276. package/src/runtime/inject-dedup-exclusive.ts +98 -0
  277. package/src/runtime/inject-dedup.ts +89 -0
  278. package/src/runtime/is-agent-tool.ts +19 -0
  279. package/src/runtime/lifecycle/agent-files.ts +49 -0
  280. package/src/runtime/lifecycle/agent-memory.ts +82 -0
  281. package/src/runtime/lifecycle/agent-transcript.ts +80 -0
  282. package/src/runtime/lifecycle/aipilot/analytics.ts +99 -0
  283. package/src/runtime/lifecycle/aipilot/apex-task-store.ts +61 -0
  284. package/src/runtime/lifecycle/aipilot/cache-base.ts +48 -0
  285. package/src/runtime/lifecycle/aipilot/cache-doc.ts +100 -0
  286. package/src/runtime/lifecycle/aipilot/cache-lessons.ts +58 -0
  287. package/src/runtime/lifecycle/aipilot/cache-test.ts +70 -0
  288. package/src/runtime/lifecycle/aipilot/curate-lessons.ts +64 -0
  289. package/src/runtime/lifecycle/aipilot/dispatch-aipilot.ts +106 -0
  290. package/src/runtime/lifecycle/aipilot/doc-cache-gate.ts +69 -0
  291. package/src/runtime/lifecycle/aipilot/inject-apex.ts +98 -0
  292. package/src/runtime/lifecycle/aipilot/inject-doc.ts +62 -0
  293. package/src/runtime/lifecycle/aipilot/inject-explore.ts +64 -0
  294. package/src/runtime/lifecycle/aipilot/inject-lessons.ts +61 -0
  295. package/src/runtime/lifecycle/aipilot/inject-test.ts +48 -0
  296. package/src/runtime/lifecycle/aipilot/lesson-archive.ts +60 -0
  297. package/src/runtime/lifecycle/aipilot/lesson-inject.ts +98 -0
  298. package/src/runtime/lifecycle/aipilot/lesson-parse.ts +74 -0
  299. package/src/runtime/lifecycle/aipilot/lessons.ts +72 -0
  300. package/src/runtime/lifecycle/aipilot/promote-global-lessons.ts +92 -0
  301. package/src/runtime/lifecycle/aipilot/solid-compliance.ts +69 -0
  302. package/src/runtime/lifecycle/aipilot/solid-notice.ts +47 -0
  303. package/src/runtime/lifecycle/aipilot/solid-transcript.ts +60 -0
  304. package/src/runtime/lifecycle/aipilot/source-scan.ts +56 -0
  305. package/src/runtime/lifecycle/aipilot/sync-task.ts +75 -0
  306. package/src/runtime/lifecycle/aipilot/transcript.ts +88 -0
  307. package/src/runtime/lifecycle/aipilot/types.ts +72 -0
  308. package/src/runtime/lifecycle/auto-document-reads.ts +73 -0
  309. package/src/runtime/lifecycle/cartographer/detect.ts +57 -0
  310. package/src/runtime/lifecycle/cartographer/ecosystem-map.ts +56 -0
  311. package/src/runtime/lifecycle/cartographer/fs-util.ts +64 -0
  312. package/src/runtime/lifecycle/cartographer/merge.ts +51 -0
  313. package/src/runtime/lifecycle/cartographer/project-map.ts +49 -0
  314. package/src/runtime/lifecycle/cartographer/scan-hooks.ts +48 -0
  315. package/src/runtime/lifecycle/cartographer/scan.ts +81 -0
  316. package/src/runtime/lifecycle/cartographer/session-start.ts +33 -0
  317. package/src/runtime/lifecycle/cartographer/track-enrichment.ts +45 -0
  318. package/src/runtime/lifecycle/cartographer/write-plugin-map.ts +43 -0
  319. package/src/runtime/lifecycle/cartographer/write-tree.ts +44 -0
  320. package/src/runtime/lifecycle/changelog-research.ts +43 -0
  321. package/src/runtime/lifecycle/check-file-size.ts +61 -0
  322. package/src/runtime/lifecycle/codex-resync/fingerprint.ts +63 -0
  323. package/src/runtime/lifecycle/codex-resync/lock.ts +46 -0
  324. package/src/runtime/lifecycle/codex-resync/managed-destination.ts +39 -0
  325. package/src/runtime/lifecycle/codex-resync/materialize-agents.ts +57 -0
  326. package/src/runtime/lifecycle/codex-resync/plugin-files.ts +65 -0
  327. package/src/runtime/lifecycle/codex-resync/plugin-roots.ts +62 -0
  328. package/src/runtime/lifecycle/codex-resync/resync.ts +45 -0
  329. package/src/runtime/lifecycle/codex-resync/symlink-commands.ts +25 -0
  330. package/src/runtime/lifecycle/dispatch.ts +109 -0
  331. package/src/runtime/lifecycle/failure-lesson.ts +55 -0
  332. package/src/runtime/lifecycle/index.ts +34 -0
  333. package/src/runtime/lifecycle/inject-rules.ts +50 -0
  334. package/src/runtime/lifecycle/instructions-loaded.ts +22 -0
  335. package/src/runtime/lifecycle/kimi-rules-native.ts +44 -0
  336. package/src/runtime/lifecycle/lessons/dispatch.ts +88 -0
  337. package/src/runtime/lifecycle/lessons/reminder.ts +89 -0
  338. package/src/runtime/lifecycle/lessons/state.ts +23 -0
  339. package/src/runtime/lifecycle/memory/agent-lesson.ts +35 -0
  340. package/src/runtime/lifecycle/memory/capture-error.ts +41 -0
  341. package/src/runtime/lifecycle/memory/client.ts +65 -0
  342. package/src/runtime/lifecycle/memory/dispatch.ts +42 -0
  343. package/src/runtime/lifecycle/memory/recall.ts +30 -0
  344. package/src/runtime/lifecycle/memory/salience.ts +31 -0
  345. package/src/runtime/lifecycle/memory/state.ts +49 -0
  346. package/src/runtime/lifecycle/memory/track-ops.ts +15 -0
  347. package/src/runtime/lifecycle/post-compact.ts +38 -0
  348. package/src/runtime/lifecycle/post-edit-ts.ts +49 -0
  349. package/src/runtime/lifecycle/post-tracking.ts +40 -0
  350. package/src/runtime/lifecycle/pre-compact.ts +38 -0
  351. package/src/runtime/lifecycle/rules-root.ts +99 -0
  352. package/src/runtime/lifecycle/security/check-skill.ts +63 -0
  353. package/src/runtime/lifecycle/security/scan-patterns.ts +53 -0
  354. package/src/runtime/lifecycle/security/scan.ts +76 -0
  355. package/src/runtime/lifecycle/security/skill-state.ts +47 -0
  356. package/src/runtime/lifecycle/security/track-mcp.ts +29 -0
  357. package/src/runtime/lifecycle/security/track-skill-read.ts +31 -0
  358. package/src/runtime/lifecycle/seo/post-tool-use.ts +45 -0
  359. package/src/runtime/lifecycle/session-end.ts +17 -0
  360. package/src/runtime/lifecycle/session-start.ts +57 -0
  361. package/src/runtime/lifecycle/snapshot/board.ts +27 -0
  362. package/src/runtime/lifecycle/snapshot/format.ts +60 -0
  363. package/src/runtime/lifecycle/snapshot/git.ts +71 -0
  364. package/src/runtime/lifecycle/snapshot/index.ts +52 -0
  365. package/src/runtime/lifecycle/snapshot/version.ts +34 -0
  366. package/src/runtime/lifecycle/solid-detect.ts +56 -0
  367. package/src/runtime/lifecycle/stop-core.ts +35 -0
  368. package/src/runtime/lifecycle/subagent-cache.ts +61 -0
  369. package/src/runtime/lifecycle/task-completed.ts +94 -0
  370. package/src/runtime/lifecycle/teammate-idle-check.ts +63 -0
  371. package/src/runtime/lifecycle/teammate-idle.ts +28 -0
  372. package/src/runtime/lifecycle/tool-failure.ts +24 -0
  373. package/src/runtime/lifecycle/track-changes.ts +61 -0
  374. package/src/runtime/lifecycle/validate-solid.ts +70 -0
  375. package/src/runtime/lifecycle/validate-tailwind.ts +80 -0
  376. package/src/runtime/lifecycle-bridge.ts +74 -0
  377. package/src/runtime/mcp-key.ts +44 -0
  378. package/src/runtime/mcp-tool-name.ts +92 -0
  379. package/src/runtime/mcp.ts +91 -0
  380. package/src/runtime/modular.ts +77 -0
  381. package/src/runtime/normalize.ts +107 -0
  382. package/src/runtime/notices.ts +91 -0
  383. package/src/runtime/notification-sound.ts +49 -0
  384. package/src/runtime/notifications.ts +77 -0
  385. package/src/runtime/paths.ts +59 -0
  386. package/src/runtime/post-fanout.ts +41 -0
  387. package/src/runtime/post-outcome.ts +61 -0
  388. package/src/runtime/pre-allow.ts +97 -0
  389. package/src/runtime/precommit.ts +60 -0
  390. package/src/runtime/prompt-text.ts +38 -0
  391. package/src/runtime/receipt-capture.ts +34 -0
  392. package/src/runtime/record.ts +26 -0
  393. package/src/runtime/respond.ts +92 -0
  394. package/src/runtime/solid-file-size-gate.ts +58 -0
  395. package/src/runtime/solid-pre.ts +53 -0
  396. package/src/runtime/storage.ts +9 -0
  397. package/src/statusline/colors.ts +31 -0
  398. package/src/statusline/constants.ts +17 -0
  399. package/src/statusline/formatters.ts +54 -0
  400. package/src/statusline/index.ts +4 -0
  401. package/src/statusline/progress-bar.ts +46 -0
  402. package/src/tracking/codex-post-failure.ts +73 -0
  403. package/src/tracking/index.ts +2 -0
  404. package/src/tracking/integrity.ts +99 -0
  405. package/src/tracking/one-shot-dedup.ts +34 -0
  406. package/src/tracking/one-shot-failure.ts +42 -0
  407. package/src/tracking/one-shot-store.ts +95 -0
  408. package/src/tracking/one-shot.ts +93 -0
  409. package/src/tracking/receipts.ts +95 -0
  410. package/src/tracking/session-state.ts +94 -0
  411. package/src/tracking/store.ts +96 -0
  412. package/src/tracking/track-compact.ts +100 -0
  413. package/src/tracking/track-diff.ts +50 -0
  414. package/src/tracking/track-journal.ts +97 -0
  415. package/src/tracking/track-lock-sync.ts +83 -0
  416. package/src/tracking/track-lock.ts +67 -0
  417. package/src/util/compact-json.ts +23 -0
  418. package/src/util/index.ts +2 -0
  419. package/src/util/json-io.ts +47 -0
  420. package/src/util/project-root.ts +41 -0
  421. package/src/util/runtime-io.ts +85 -0
  422. package/src/util/semver.ts +30 -0
@@ -0,0 +1,65 @@
1
+ import type { Prompt } from "../../prompt/types";
2
+ import { displayCodeForAction, hashForAction } from "./confirm-code";
3
+ import { isIrreversible } from "./confirm-irreversible";
4
+ import { recordPendingDeny } from "./confirm-pending";
5
+ import { consumeConfirmToken } from "./confirm-state";
6
+ import { authorizeCodexAction, codexAction } from "./codex-confirm";
7
+
8
+ /**
9
+ * Harnesses where `respond.ts` silently downgrades `kind: "ask"` to a hard
10
+ * deny (Codex: `case "codex"`; Kimi: `toKimiResponse` maps `ask` to the same
11
+ * `permissionDecision:"deny"` envelope as `block`). Claude Code keeps native
12
+ * interactive `ask` — this mechanism NEVER applies there.
13
+ */
14
+ const DEGRADES_ASK_TO_DENY: ReadonlySet<string> = new Set(["codex", "kimi"]);
15
+
16
+ export type ConfirmVerdict = { allow: true } | { allow: false; prompt: Prompt };
17
+
18
+ /**
19
+ * Whether/how a CONFIRM token changes an `ask` prompt about to be downgraded
20
+ * to a deny. Returns `null` when this mechanism doesn't apply AT ALL — any
21
+ * harness other than codex/kimi, any prompt kind other than `ask`, no
22
+ * command to key a hash off, or an irreversible command (G4) — in which case
23
+ * the caller's ORIGINAL prompt/response path runs completely unchanged. That
24
+ * `null` fast-path, hit on every claude-code call and every non-`ask` prompt,
25
+ * IS the non-regression property.
26
+ * @param id - Harness target id.
27
+ * @param prompt - The prompt `gate()` returned.
28
+ * @param command - `event.command` for the tool-use under judgment.
29
+ * @param sessionId - `event.sessionId`.
30
+ * @param now - Epoch ms.
31
+ * @param home - Test-only OS home override.
32
+ */
33
+ export function confirmGate(
34
+ id: string,
35
+ prompt: Prompt,
36
+ command: string | undefined,
37
+ sessionId: string,
38
+ now: number,
39
+ home?: string,
40
+ codex?: Readonly<{ tool: string; cwd: string; toolUseId?: string }>,
41
+ ): ConfirmVerdict | null {
42
+ if (prompt.kind !== "ask" || !DEGRADES_ASK_TO_DENY.has(id) || !command || isIrreversible(command)) return null;
43
+ try {
44
+ if (id === "codex" && codex) {
45
+ const action = codexAction(codex.tool, codex.cwd, command, now);
46
+ if (!action) return null;
47
+ const verdict = authorizeCodexAction(sessionId, action, codex.toolUseId, now, home);
48
+ if (verdict.allow) return verdict;
49
+ const ruleId = prompt.ruleId ?? "policy:ask";
50
+ const diagnostic = `rule ID: ${ruleId}\ncanonical command: ${action.command}\nexpected token: CONFIRM ${action.code}\nrejection: ${verdict.reason}`;
51
+ return { allow: false, prompt: { ...prompt, reason: `${prompt.reason}\nPour autoriser, réponds : CONFIRM ${action.code}\n${diagnostic}` } };
52
+ }
53
+ const hash = hashForAction(command);
54
+ if (consumeConfirmToken(sessionId, hash, now, home)) return { allow: true };
55
+ const code = displayCodeForAction(command);
56
+ recordPendingDeny(sessionId, hash, code, now, home);
57
+ return { allow: false, prompt: { ...prompt, reason: `${prompt.reason}\nPour autoriser, réponds : CONFIRM ${code}` } };
58
+ } catch {
59
+ // A state-io failure (full disk, unwritable home) must fall back to the
60
+ // plain deny, never crash the hook — same invariant as confirm-submit.ts.
61
+ // The caller's `confirm ? confirm.prompt : prompt` treats `null` exactly
62
+ // like "mechanism doesn't apply", i.e. the pre-CONFIRM deny unchanged.
63
+ return null;
64
+ }
65
+ }
@@ -0,0 +1,32 @@
1
+ import { GIT_BLOCKED, matchPatterns } from "../../policy/patterns";
2
+
3
+ /**
4
+ * A single flag token carrying BOTH `r` and `f` in either order (`-rf`,
5
+ * `-fr`, `-rfv`, …) — same lookahead technique as patterns.ts's
6
+ * `CLEAN_FD_FLAG`, reused here rather than re-derived (DRY).
7
+ */
8
+ const RM_COMBINED_FLAG = /\s-(?=[a-zA-Z]*r)(?=[a-zA-Z]*f)[a-zA-Z]+(?:\s|=|$)/;
9
+ /** Split flags `-r ... -f` (either order) within the SAME command segment (no `;&|` between). */
10
+ const RM_SPLIT_FLAG = /\brm\b[^;&|\n]*\s-r\b[^;&|\n]*\s-f\b|\brm\b[^;&|\n]*\s-f\b[^;&|\n]*\s-r\b/;
11
+
12
+ /**
13
+ * Generic `rm -rf`/`-fr` (ANY target, not just `/`/`~`) — deliberately
14
+ * broader than `src/codex-rules/rules/rm-variants.ts`'s root/system-path-only
15
+ * forbidden list: G4 must treat every recursive force-delete as
16
+ * never-confirmable, not only the ones aimed at a system path.
17
+ */
18
+ function isRmRf(cmd: string): boolean {
19
+ return /\brm\b/.test(cmd) && (RM_COMBINED_FLAG.test(cmd) || RM_SPLIT_FLAG.test(cmd));
20
+ }
21
+
22
+ /**
23
+ * G4: commands NEVER unlockable by a CONFIRM token, regardless of a valid,
24
+ * fresh, correctly-scoped one. Reuses {@link GIT_BLOCKED} (push --force,
25
+ * reset --hard, clean -fd, branch -D, rebase --force — DRY, same list the
26
+ * rest of the policy already enforces) plus a generic `rm -rf` check.
27
+ * @param cmd - The command a prompt is about (empty/undefined = not irreversible).
28
+ */
29
+ export function isIrreversible(cmd: string | undefined): boolean {
30
+ if (!cmd) return false;
31
+ return matchPatterns(cmd, GIT_BLOCKED) || isRmRf(cmd);
32
+ }
@@ -0,0 +1,34 @@
1
+ import { homedir } from "node:os";
2
+ import { loadSessionState, saveSessionState, sanitizeSessionId } from "../home-state";
3
+
4
+ /** The last `ask`-turned-deny for a session: its full hash plus the short code shown to the human. */
5
+ export interface PendingDeny {
6
+ hash: string;
7
+ code: string;
8
+ ts: number;
9
+ }
10
+
11
+ /**
12
+ * Record the action a deny prompt just showed (overwrites any previous
13
+ * pending deny for this session — only the LAST blocked action can be
14
+ * confirmed). Pure bookkeeping, no gate: every ask-turned-deny records one,
15
+ * whether or not the user ever confirms it.
16
+ * @param sessionIdRaw - Raw session id from the payload.
17
+ * @param hash - Full {@link import("./confirm-code").hashForAction} of the command.
18
+ * @param code - The short display code shown alongside it.
19
+ * @param now - Epoch ms.
20
+ * @param home - Test-only OS home override.
21
+ */
22
+ export function recordPendingDeny(sessionIdRaw: unknown, hash: string, code: string, now: number, home: string = homedir()): void {
23
+ const sid = sanitizeSessionId(sessionIdRaw);
24
+ if (!sid) return;
25
+ const state = loadSessionState(sid, home);
26
+ saveSessionState(sid, { ...state, pendingDeny: { hash, code, ts: now } satisfies PendingDeny }, home);
27
+ }
28
+
29
+ /** Read back the pending deny for a session (`undefined` when none/invalid session id). */
30
+ export function getPendingDeny(sessionIdRaw: unknown, home: string = homedir()): PendingDeny | undefined {
31
+ const sid = sanitizeSessionId(sessionIdRaw);
32
+ if (!sid) return undefined;
33
+ return loadSessionState(sid, home).pendingDeny as PendingDeny | undefined;
34
+ }
@@ -0,0 +1,56 @@
1
+ import { homedir } from "node:os";
2
+ import { loadSessionState, saveSessionState, sanitizeSessionId } from "../home-state";
3
+ import { isSubagentActive } from "./confirm-subagent";
4
+
5
+ /** A posed confirmation token: the FULL action hash (G3) plus its mint timestamp (G2). */
6
+ interface ConfirmToken {
7
+ hash: string;
8
+ ts: number;
9
+ }
10
+
11
+ /** Confirmation freshness window (G2) — 5 minutes, matches the validated prototype. */
12
+ const TTL_MS = 5 * 60 * 1000;
13
+
14
+ /**
15
+ * Pose a confirmation token for this session's pending action. No-op while
16
+ * {@link isSubagentActive} (G0, confirm-subagent.ts) — the sole gate that
17
+ * stops an agent, which always sees the display code in its own blocked tool
18
+ * result, from typing it back to self-approve.
19
+ */
20
+ export function placeConfirmToken(sessionIdRaw: unknown, hash: string, now: number, home: string = homedir(), env: Record<string, string | undefined> = process.env): void {
21
+ const sid = sanitizeSessionId(sessionIdRaw);
22
+ if (!sid || isSubagentActive(sid, now, home, env)) return;
23
+ const state = loadSessionState(sid, home);
24
+ saveSessionState(sid, { ...state, confirmToken: { hash, ts: now } satisfies ConfirmToken }, home);
25
+ }
26
+
27
+ /** Invalidate any pending token for this session (G5: an explicit refusal). */
28
+ export function dropConfirmToken(sessionIdRaw: unknown, home: string = homedir()): void {
29
+ const sid = sanitizeSessionId(sessionIdRaw);
30
+ if (!sid) return;
31
+ const state = loadSessionState(sid, home);
32
+ if (state.confirmToken === undefined) return;
33
+ const { confirmToken: _drop, ...rest } = state;
34
+ saveSessionState(sid, rest, home);
35
+ }
36
+
37
+ /**
38
+ * Consume a token that matches `hash` exactly (G3), whether or not it's
39
+ * fresh — a mismatched hash leaves the token untouched (it may still be
40
+ * valid for the action it actually confirms). G1 (one-shot) + G2 (5-min TTL)
41
+ * both apply only once the hash matches.
42
+ * @returns true = allow (token consumed); false = deny (nothing changed, or
43
+ * the matching token had expired and was dropped).
44
+ */
45
+ export function consumeConfirmToken(sessionIdRaw: unknown, hash: string, now: number, home: string = homedir()): boolean {
46
+ const sid = sanitizeSessionId(sessionIdRaw);
47
+ if (!sid) return false;
48
+ const tok = loadSessionState(sid, home).confirmToken as ConfirmToken | undefined;
49
+ if (!tok || tok.hash !== hash) return false;
50
+ if (now - tok.ts > TTL_MS) {
51
+ dropConfirmToken(sid, home);
52
+ return false;
53
+ }
54
+ dropConfirmToken(sid, home);
55
+ return true;
56
+ }
@@ -0,0 +1,112 @@
1
+ import { homedir } from "node:os";
2
+ import { parseEnvInt } from "../../config/env";
3
+ import { loadSessionState, saveSessionState, sanitizeSessionId } from "../home-state";
4
+
5
+ /** Env var overriding the G0 cool-down window below (seconds). Unset = the 300s/5min default. */
6
+ const CONFIRM_WINDOW_ENV_KEY = "FUSE_CONFIRM_SUBAGENT_WINDOW_SEC";
7
+ /** Default G0 cool-down (seconds) when {@link CONFIRM_WINDOW_ENV_KEY} is unset/invalid — 300s (5min). */
8
+ const DEFAULT_CONFIRM_WINDOW_SEC = 300;
9
+
10
+ /**
11
+ * G0 cool-down after the last SubagentStart/SubagentStop seen for this
12
+ * session — {@link DEFAULT_CONFIRM_WINDOW_SEC} by default, overridable via
13
+ * `FUSE_CONFIRM_SUBAGENT_WINDOW_SEC` (seconds) without a rebuild. Read this
14
+ * doc before ever touching this value: it is NOT G0's primary protection,
15
+ * and it is not sized to "safely cover a sub-agent's whole runtime".
16
+ *
17
+ * G0's PRIMARY protection is P0, measured live under Codex in a prior
18
+ * session: a sub-agent never receives its own `UserPromptSubmit` — only a
19
+ * human typing into the top-level session does — so a sub-agent is
20
+ * STRUCTURALLY unable to place a CONFIRM token at all, regardless of this
21
+ * window's value. This timestamp is a SECOND belt, covering only the
22
+ * residual case where that structural property does not hold on some future
23
+ * harness (Kimi: not yet measured either way).
24
+ *
25
+ * The default is a deliberate, owner-decided trade-off between over-refusal
26
+ * and usability, NOT a safety-maximizing constant: sub-agents in this
27
+ * ecosystem share the lead session's `session_id`, and the owner runs
28
+ * sub-agents continuously — an earlier 30-minute window froze CONFIRM for
29
+ * the entire session on every single sub-agent call, which neutralizes the
30
+ * feature it protects (a guard that disables what it guards is not a good
31
+ * guard). 5 minutes comfortably covers the case that actually matters — a
32
+ * sub-agent genuinely in flight at the exact moment a human types a CONFIRM
33
+ * code — without pinning a busy session shut. Do not read this value as "the
34
+ * time G0 needs to be safe" — that safety comes from P0.
35
+ *
36
+ * This deliberately reuses {@link parseEnvInt} (`src/config/env.ts`) rather
37
+ * than `resolveTtlSec`/`FUSE_ENFORCE_TTL_SEC` (`src/config/ttl.ts`): that TTL
38
+ * governs APEX evidence freshness (120s default) — an unrelated concern
39
+ * accidentally coupling the two would make lengthening the research-evidence
40
+ * window also lengthen the confirmation cool-down. `resolveTtlSec` also
41
+ * hardcodes its own fallback (`DEFAULT_TTL_SEC` = 120) regardless of which
42
+ * env key is passed, so it cannot express a 300s default either — this key
43
+ * gets its OWN env var and its OWN default via the same underlying
44
+ * `parseEnvInt` primitive, with no new parsing logic.
45
+ *
46
+ * Mechanically this is still a single monotone "last seen" timestamp, NOT a
47
+ * start/stop pair — a symmetric increment/decrement depth counter was tried
48
+ * and rejected: the harness dispatches ONE real SubagentStart/SubagentStop
49
+ * through MULTIPLE concurrent sibling plugin processes (the repo's own
50
+ * "~11-process multi-plugin fan-out"), all doing UNLOCKED read-modify-write
51
+ * on the same session-state file — a duplicated decrement can walk the
52
+ * counter to 0 while a sub-agent is still running, and G0 would then wrongly
53
+ * OPEN. Two ever-growing `starts`/`stops` counters were also rejected: if the
54
+ * fan-out dispatches SubagentStart via N processes and SubagentStop via
55
+ * M ≠ N, `starts > stops` stays true FOREVER and the session is frozen for
56
+ * good — worse than any window.
57
+ *
58
+ * `Math.max(prevSeenAt, now)` avoids both failure modes: an LWW
59
+ * (last-writer-wins) register merged with `max`, which is idempotent,
60
+ * commutative, and associative — under ANY interleaving of concurrent
61
+ * writers (lost updates included), the stored value converges to the
62
+ * largest `now` any writer supplied, and every writer's `now` is a real
63
+ * wall-clock read taken within milliseconds of the true event, so it can
64
+ * never regress below a timestamp it already held. BOTH SubagentStart and
65
+ * SubagentStop bump the SAME field this way — there is no decrement
66
+ * anywhere in this file, so there is nothing for a duplicated event to
67
+ * desynchronize; the flag falls only once this window elapses with no
68
+ * further sighting.
69
+ * @param env - Env map to resolve the override from (defaults to `process.env`; tests inject a plain object).
70
+ */
71
+ function subagentWindowMs(env: Record<string, string | undefined> = process.env): number {
72
+ return parseEnvInt(env[CONFIRM_WINDOW_ENV_KEY], DEFAULT_CONFIRM_WINDOW_SEC) * 1000;
73
+ }
74
+
75
+ /**
76
+ * G0: true within {@link subagentWindowMs} of the last SubagentStart/
77
+ * SubagentStop seen for this session. An invalid/missing session id is
78
+ * treated as active too — fail-closed, never pose a token when in doubt.
79
+ */
80
+ export function isSubagentActive(sessionIdRaw: unknown, now: number, home: string = homedir(), env: Record<string, string | undefined> = process.env): boolean {
81
+ const sid = sanitizeSessionId(sessionIdRaw);
82
+ if (!sid) return true;
83
+ const seenAt = loadSessionState(sid, home).subagentSeenAt;
84
+ return typeof seenAt === "number" && now - seenAt < subagentWindowMs(env);
85
+ }
86
+
87
+ /**
88
+ * Record a SubagentStart OR SubagentStop sighting for this session as a
89
+ * monotone max-write (see {@link subagentWindowMs}'s doc for why this can
90
+ * never desync unsafely under concurrent racy writers). Both events call
91
+ * this the SAME way — there is no separate "clear" path.
92
+ *
93
+ * Called unconditionally, BEFORE scope branching, on every SubagentStart/Stop
94
+ * across every plugin (dispatch.ts) — never allowed to throw: a disk fault
95
+ * here (full disk, unwritable home) must degrade to "sighting not recorded",
96
+ * never crash sub-agent lifecycle dispatch for scopes that have nothing to do
97
+ * with CONFIRM. Safe to swallow: G0's PRIMARY protection is P0 (a sub-agent
98
+ * structurally never receives its own UserPromptSubmit, confirm-gate.ts's
99
+ * doc) — this window is only a second belt, so losing one sighting degrades
100
+ * that belt, it does not open the gate.
101
+ */
102
+ export function markSubagentSeen(sessionIdRaw: unknown, now: number, home: string = homedir()): void {
103
+ try {
104
+ const sid = sanitizeSessionId(sessionIdRaw);
105
+ if (!sid) return;
106
+ const state = loadSessionState(sid, home);
107
+ const prev = typeof state.subagentSeenAt === "number" ? state.subagentSeenAt : 0;
108
+ saveSessionState(sid, { ...state, subagentSeenAt: Math.max(prev, now) }, home);
109
+ } catch {
110
+ // A state-io failure must never break the hook (same invariant as confirm-submit.ts).
111
+ }
112
+ }
@@ -0,0 +1,38 @@
1
+ import { getPendingDeny } from "./confirm-pending";
2
+ import { dropConfirmToken, placeConfirmToken } from "./confirm-state";
3
+
4
+ /** Common explicit-refusal words (fr/en), any of which invalidates a pending token (G5). */
5
+ const REFUSAL_RE = /\b(non|no|stop|annule|cancel|abort|nope|laisse tomber|pas maintenant)\b/i;
6
+ /** `CONFIRM <4-hex-chars>`, case-insensitive, tolerant of `confirm4f2a` / `Confirm-4f2a` / `confirm_4f2a`. */
7
+ const CONFIRM_RE = /confirm[\s_-]*([0-9a-f]{4})\b/i;
8
+
9
+ /**
10
+ * Parse a submitted user prompt for `CONFIRM <code>` or an explicit refusal.
11
+ * A refusal always wins (checked first) and drops any pending token (G5),
12
+ * even if the same text also happens to contain a code. A confirm only
13
+ * places a token when its 4-char code matches this session's LAST pending
14
+ * deny (see confirm-pending.ts) — the code is looked up back to the full
15
+ * hash that deny recorded, never compared as a code-to-code match (that
16
+ * would reopen the collision {@link import("./confirm-code").displayCodeForAction}
17
+ * warns about). Never throws — this runs inside a hook.
18
+ * @param sessionId - The normalized event's session id.
19
+ * @param text - The prompt text ({@link import("../prompt-text").promptText} output).
20
+ * @param now - Epoch ms.
21
+ * @param home - Test-only OS home override.
22
+ */
23
+ export function handleConfirmSubmit(sessionId: string, text: string, now: number, home?: string): void {
24
+ try {
25
+ if (REFUSAL_RE.test(text)) {
26
+ dropConfirmToken(sessionId, home);
27
+ return;
28
+ }
29
+ const m = text.match(CONFIRM_RE);
30
+ const typedCode = m?.[1];
31
+ if (!typedCode) return;
32
+ const pending = getPendingDeny(sessionId, home);
33
+ if (!pending || pending.code.toLowerCase() !== typedCode.toLowerCase()) return;
34
+ placeConfirmToken(sessionId, pending.hash, now, home); // G0 enforced inside
35
+ } catch {
36
+ // A state-io failure must never break the hook.
37
+ }
38
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Per-harness ACTIVATION probing for the core-guards plugin, behind
3
+ * {@link coreGuardsWired}. The owner decision: the solid scope abstains from
4
+ * its file-size deny ONLY when core-guards is installed AND active — an
5
+ * installed-but-disabled plugin must leave the solid deny firing. Safe
6
+ * default, per harness: anything undetectable (unreadable/missing state)
7
+ * reads as NOT active, so the worst case is a duplicate deny — never zero.
8
+ */
9
+ import { readFileSync } from "node:fs";
10
+ import { join } from "node:path";
11
+
12
+ /** File content, or null when unreadable (undetectable → caller's safe default). */
13
+ function readText(path: string): string | null {
14
+ try { return readFileSync(path, "utf8"); } catch { return null; }
15
+ }
16
+
17
+ /** First `core-guards@…` value in a name→bool map; undefined when absent. */
18
+ function enabledEntry(map: unknown): boolean | undefined {
19
+ if (typeof map !== "object" || map === null || Array.isArray(map)) return undefined;
20
+ for (const [k, v] of Object.entries(map as Record<string, unknown>)) {
21
+ if (k.startsWith("core-guards@")) return v !== false;
22
+ }
23
+ return undefined;
24
+ }
25
+
26
+ /**
27
+ * Claude Code: `~/.claude/settings.json` → `enabledPlugins`. core-guards is a
28
+ * REQUIRED marketplace plugin: no entry means active whenever the marketplace
29
+ * copy exists (`installed`); an explicit `false` means disabled. Unreadable
30
+ * settings → false (undetectable).
31
+ * @param home - OS home.
32
+ * @param installed - Whether the marketplace copy of core-guards exists.
33
+ */
34
+ export function claudeCoreGuardsActive(home: string, installed: boolean): boolean {
35
+ const text = readText(join(home, ".claude", "settings.json"));
36
+ if (text === null) return false;
37
+ try {
38
+ const entry = enabledEntry((JSON.parse(text) as { enabledPlugins?: unknown }).enabledPlugins);
39
+ return entry ?? installed;
40
+ } catch { return false; }
41
+ }
42
+
43
+ /**
44
+ * Codex: `[plugins."core-guards@<mkt>"] enabled = …` in `~/.codex/config.toml`
45
+ * (minimal section-scoped extraction — no TOML dependency). Missing section,
46
+ * `enabled = false`, or unreadable file → false.
47
+ * @param home - OS home.
48
+ */
49
+ export function codexCoreGuardsActive(home: string): boolean {
50
+ const text = readText(join(home, ".codex", "config.toml"));
51
+ if (text === null) return false;
52
+ // `^` + `m`: the header must start a real line — an inert one-line comment
53
+ // like `# [plugins."core-guards@mkt"] enabled = true` must never match
54
+ // (verified: without the anchor it does, flipping this to a false "active"
55
+ // and silencing the solid file-size deny — the owner's "never zero deny").
56
+ const m = /^\[plugins\."core-guards@[^"]+"\]\s*enabled\s*=\s*(true|false)/m.exec(text);
57
+ return m?.[1] === "true";
58
+ }
59
+
60
+ /**
61
+ * Kimi: `<KIMI_CODE_HOME>/plugins/installed.json` — map form (`id →
62
+ * {enabled?}`) or array form (`[{id, enabled?}]`); an entry counts active
63
+ * unless `enabled === false`. File absent here today → format read
64
+ * tolerantly; missing/unreadable → false (undetectable).
65
+ * @param root - Kimi data root (`KIMI_CODE_HOME` ?? `~/.kimi-code`).
66
+ */
67
+ export function kimiCoreGuardsActive(root: string): boolean {
68
+ const text = readText(join(root, "plugins", "installed.json"));
69
+ if (text === null) return false;
70
+ try {
71
+ const data = (JSON.parse(text) as { plugins?: unknown }).plugins;
72
+ if (Array.isArray(data)) {
73
+ const hit = data.find((p) => typeof p === "object" && p !== null && (p as { id?: unknown }).id === "core-guards");
74
+ return hit !== undefined && (hit as { enabled?: unknown }).enabled !== false;
75
+ }
76
+ if (typeof data !== "object" || data === null) return false;
77
+ for (const [k, v] of Object.entries(data as Record<string, unknown>)) {
78
+ if (k === "core-guards" || k.startsWith("core-guards@")) {
79
+ return typeof v === "object" && v !== null ? (v as { enabled?: unknown }).enabled !== false : v !== false;
80
+ }
81
+ }
82
+ return false;
83
+ } catch { return false; }
84
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Detect whether the core-guards plugin is installed AND ACTIVE for a harness.
3
+ * When it is, its `core` scope already owns the PreToolUse file-size deny
4
+ * (`policy/evaluate.ts`), so the `solid` scope MUST abstain on the same
5
+ * tool-call — otherwise a user with BOTH plugins wired on PreToolUse
6
+ * Write|Edit receives the identical deny twice. Activation probing is
7
+ * per-harness (`core-guards-active.ts`, owner decision):
8
+ * - claude-code: marketplace copy present + `settings.json` `enabledPlugins`
9
+ * (required core: no entry = active; explicit `false` = disabled);
10
+ * - codex: `[plugins."core-guards@…"] enabled = true` in `~/.codex/config.toml`;
11
+ * - kimi: `<KIMI_CODE_HOME>/plugins/installed.json` entry not disabled.
12
+ * SAFE DEFAULT: anything undetectable (unreadable/missing state) reads as
13
+ * NOT active — the solid scope then fires its own deny, so the worst case is
14
+ * a duplicate deny, NEVER zero deny.
15
+ */
16
+ import { existsSync, readdirSync } from "node:fs";
17
+ import { homedir } from "node:os";
18
+ import { join } from "node:path";
19
+ import { claudeCoreGuardsActive, codexCoreGuardsActive, kimiCoreGuardsActive } from "./core-guards-active";
20
+
21
+ /** Immediate child dir names of `dir`, or [] when unreadable. */
22
+ function children(dir: string): string[] {
23
+ try { return readdirSync(dir); } catch { return []; }
24
+ }
25
+
26
+ /** Claude: marketplace copy `~/.claude/plugins/marketplaces/<m>/plugins/core-guards`. */
27
+ function claudeInstalled(home: string): boolean {
28
+ const markets = join(home, ".claude", "plugins", "marketplaces");
29
+ return children(markets).some((m) => existsSync(join(markets, m, "plugins", "core-guards")));
30
+ }
31
+
32
+ /**
33
+ * True when core-guards is installed AND active for the harness.
34
+ * @param id - Harness id.
35
+ * @param env - Environment (defaults to `process.env`).
36
+ * @param home - OS home (defaults to `env.HOME` / `os.homedir()`; injectable).
37
+ */
38
+ export function coreGuardsWired(
39
+ id: string,
40
+ env: Record<string, string | undefined> = process.env,
41
+ home: string = env.HOME ?? homedir(),
42
+ ): boolean {
43
+ if (id === "claude-code") return claudeCoreGuardsActive(home, claudeInstalled(home));
44
+ if (id === "codex") return codexCoreGuardsActive(home);
45
+ if (id === "kimi") return kimiCoreGuardsActive(env.KIMI_CODE_HOME ?? join(home, ".kimi-code"));
46
+ return false;
47
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * @module deny-loop-store
3
+ * Sidecar state + gate wiring for the mechanical anti-loop ({@link module:deny-loop}).
4
+ *
5
+ * STATE — a standalone sidecar (`deny-loop.json`) in the same per-project state
6
+ * dir as the session track, NOT a field in `session-state.ts`: that file is owned
7
+ * by another concern this batch, so a shared field would couple two owners and
8
+ * contend on one file for two unrelated maps. This mirrors the proven
9
+ * {@link module:inject-dedup} `oncePerWindow` sidecar (atomicWrite, prune-by-window,
10
+ * fail-open) and keeps the anti-loop self-contained.
11
+ *
12
+ * WINDOW — the hash expires with the same freshness window the gate already uses
13
+ * (`FUSE_ENFORCE_TTL_SEC` / `windowMs`): a loop is a burst, not a lifetime, so a
14
+ * stale deny past the window resets and a later identical call is a fresh #1.
15
+ * @packageDocumentation
16
+ */
17
+ import { existsSync, readFileSync } from "node:fs";
18
+ import { join } from "node:path";
19
+ import { atomicWrite } from "../util/json-io";
20
+ import { denyHash, denyLoopCheck, enrichRepeatDeny, type DenyEntry, type DenyLoopResult } from "../policy/deny-loop";
21
+ import { BURST_DEDUP_MS } from "./burst-window";
22
+ import type { Prompt } from "../prompt/types";
23
+
24
+ /** Sidecar basename under the per-project state dir. */
25
+ const SIDECAR = "deny-loop.json";
26
+
27
+ /**
28
+ * Injected clock + state dir + expiry window (no env var; the gate supplies all
29
+ * three). `sessionId` — when present — scopes the map key AND arms the burst
30
+ * dedup ({@link module:burst-window}) so the ~11 sibling plugin hooks of ONE
31
+ * event count once, not once each; two sessions never share a counter. Omitted
32
+ * by mono-process callers/tests → historical per-hash, no-dedup behaviour.
33
+ */
34
+ export interface DenyLoopOpts { now: number; dir: string; windowMs: number; sessionId?: string; }
35
+
36
+ /** Load the `{ hash -> DenyEntry }` map, or `{}` when missing/corrupt. */
37
+ function loadMap(path: string): Record<string, DenyEntry> {
38
+ try {
39
+ if (!existsSync(path)) return {};
40
+ const data: unknown = JSON.parse(readFileSync(path, "utf8"));
41
+ return data && typeof data === "object" && !Array.isArray(data) ? (data as Record<string, DenyEntry>) : {};
42
+ } catch {
43
+ return {};
44
+ }
45
+ }
46
+
47
+ /** Drop entries whose last deny is older than the window (bounds size + resets stale loops). */
48
+ function prune(map: Record<string, DenyEntry>, now: number, windowMs: number): Record<string, DenyEntry> {
49
+ const out: Record<string, DenyEntry> = {};
50
+ for (const [k, e] of Object.entries(map)) {
51
+ if (e && typeof e.lastTs === "number" && now - e.lastTs < windowMs) out[k] = e;
52
+ }
53
+ return out;
54
+ }
55
+
56
+ /**
57
+ * Record a deny for `(tool, input)`: prune, {@link denyLoopCheck}, persist the
58
+ * bumped entry. Fails open (`isRepeat:false`) when the sidecar is unwritable — a
59
+ * broken state dir must never manufacture a false `[REPEAT]`.
60
+ * @param tool - Tool name.
61
+ * @param input - Identifying tool input.
62
+ * @param opts - Clock + state dir + window.
63
+ * @returns `{ isRepeat, count, hash }`.
64
+ */
65
+ export function recordDeny(tool: string, input: Record<string, unknown>, opts: DenyLoopOpts): DenyLoopResult {
66
+ const hash = denyHash(tool, input);
67
+ const sid = opts.sessionId?.trim();
68
+ const key = sid ? `${hash}::${sid}` : hash;
69
+ const path = join(opts.dir, SIDECAR);
70
+ const map = prune(loadMap(path), opts.now, opts.windowMs);
71
+ const res = denyLoopCheck(key, map, { now: opts.now, windowMs: opts.windowMs, dedupMs: sid ? BURST_DEDUP_MS : 0 });
72
+ if (!res.deduped) {
73
+ map[key] = { count: res.count, lastTs: opts.now };
74
+ try { atomicWrite(path, JSON.stringify(map)); } catch { /* fail-open: never fabricate a repeat */ }
75
+ }
76
+ return { ...res, hash };
77
+ }
78
+
79
+ /**
80
+ * Gate tail: record every block deny; on a repeat, return the enriched prompt.
81
+ * Allows (`null`) and non-block prompts (`ask`/`inform`) pass through untouched —
82
+ * a loop is a retried REFUSAL, and only `block` is a refusal. The decision is
83
+ * NEVER changed; only a repeated block's message is rewritten.
84
+ * @param prompt - The gate's outcome.
85
+ * @param tool - Tool name.
86
+ * @param input - Identifying tool input.
87
+ * @param opts - Clock + state dir + window.
88
+ * @returns The prompt, enriched only when it is a repeated block.
89
+ */
90
+ export function withDenyLoop(prompt: Prompt | null, tool: string, input: Record<string, unknown>, opts: DenyLoopOpts): Prompt | null {
91
+ if (!prompt || prompt.kind !== "block") return prompt;
92
+ const { isRepeat, count } = recordDeny(tool, input, opts);
93
+ return isRepeat ? enrichRepeatDeny(prompt, count) : prompt;
94
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * @module deny-notice
3
+ * Attach a user-visible notice to a deny/ask hook response — the owner-reported
4
+ * gap where `permissionDecision: deny/ask` (the agent-only channel) left the
5
+ * human staring at a silent terminal. Mirrors {@link module:notices}'s
6
+ * compliance notices but for the BLOCKING outcomes those never covered.
7
+ * @packageDocumentation
8
+ */
9
+ import figures from "figures";
10
+ import type { Prompt } from "../prompt/types";
11
+ import { attachSystemMessage } from "../adapters/claude";
12
+ import { onceExclusive } from "./inject-dedup";
13
+ import { BURST_DEDUP_MS } from "./burst-window";
14
+
15
+ /**
16
+ * The human-facing line for a block/ask outcome: the gate's own
17
+ * {@link Prompt.userMessage} when it set one, else a generic symbol + title —
18
+ * text-presentation Unicode only (`figures.cross`/`?`), never emoji: terminal-safe,
19
+ * single-cell width, with the Windows fallbacks `figures` already resolves, matching
20
+ * the existing notice family (`notices.ts`'s `✓`/`⚠`, left untouched by this module).
21
+ * Null for `inform` ({@link module:respond}'s own `userMessage` path already
22
+ * covers it) or when neither applies.
23
+ */
24
+ export function denyAskNotice(prompt: Prompt): string | null {
25
+ if (prompt.kind === "block") return prompt.userMessage ?? `${figures.cross} ${prompt.title}`;
26
+ if (prompt.kind === "ask") return prompt.userMessage ?? `? ${prompt.title}`;
27
+ return null;
28
+ }
29
+
30
+ /**
31
+ * Attach {@link denyAskNotice} onto an already-rendered deny/ask `stdout`, for
32
+ * the harnesses whose human channel is `systemMessage` (claude-code/codex —
33
+ * `permissionDecision`/`permissionDecisionReason` stay byte-intact, only the
34
+ * top-level field is added). Every other harness passes `stdout` through
35
+ * unchanged: Cursor already emits `user_message` natively (respond.ts), Hermes
36
+ * and cline have no human channel. Deduped via {@link onceExclusive} against
37
+ * the ~11 sibling-plugin fan-out for one real event (same window as the
38
+ * sniper reminder / compliance notices).
39
+ * @param id - Harness id (`ctx.id` from `PreContext`).
40
+ * @param stdout - The rendered hook response (from {@link module:respond.respond}).
41
+ * @param prompt - The {@link Prompt} that produced `stdout`.
42
+ * @param sessionId - Current session id (dedup scope).
43
+ * @param dir - State-dir for the dedup marker (per-project state dir).
44
+ * @param now - Event clock (tests pass a fake one).
45
+ */
46
+ export function withDenyNotice(id: string, stdout: string, prompt: Prompt, sessionId: string, dir: string, now?: number): string {
47
+ if (id !== "claude-code" && id !== "codex") return stdout;
48
+ const notice = denyAskNotice(prompt);
49
+ if (!notice) return stdout;
50
+ if (!onceExclusive(`deny-notice:${sessionId}:${prompt.title}`, BURST_DEDUP_MS, { now, dir })) return stdout;
51
+ return attachSystemMessage(stdout, notice);
52
+ }