@fusengine/harness 0.1.90 → 0.1.91

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (417) hide show
  1. package/dist/adapters/claude/index.d.mts +1 -1
  2. package/dist/adapters/claude/index.mjs +1 -1
  3. package/dist/adapters/cline/index.mjs +1 -1
  4. package/dist/adapters/codex/index.d.mts +2 -2
  5. package/dist/adapters/codex/index.mjs +3 -2
  6. package/dist/adapters/cursor/index.d.mts +15 -28
  7. package/dist/adapters/cursor/index.mjs +35 -46
  8. package/dist/adapters/gemini/index.mjs +1 -1
  9. package/dist/adapters/hermes/index.d.mts +1 -1
  10. package/dist/adapters/hermes/index.mjs +1 -1
  11. package/dist/adapters/kimi/index.d.mts +1 -1
  12. package/dist/adapters/kimi/index.mjs +1 -1
  13. package/dist/{claude-5bC8TDAz.mjs → claude-Ckv2_TgP.mjs} +3 -43
  14. package/dist/cli/bin.mjs +429 -16
  15. package/dist/cli/index.mjs +1 -1
  16. package/dist/command-string-CALMTnwN.mjs +42 -0
  17. package/dist/{evaluate-C1VSI5pb.mjs → evaluate-CMiIqHeH.mjs} +261 -13
  18. package/dist/{handle-CBaOcFZq.mjs → handle-C43gA-Pr.mjs} +701 -134
  19. package/dist/{hermes-kP6NUQlG.mjs → hermes-B9-p_3IF.mjs} +3 -2
  20. package/dist/{index-DBgnIZn9.d.mts → index-B8PG82su.d.mts} +1 -1
  21. package/dist/{index-D4C2-IIc.d.mts → index-SmKYkk2N.d.mts} +1 -1
  22. package/dist/index.d.mts +2 -2
  23. package/dist/index.mjs +2 -2
  24. package/dist/init/index.d.mts +1 -1
  25. package/dist/init/index.mjs +1 -1
  26. package/dist/{kimi-D6X7b8wF.mjs → kimi-G2wcSh5-.mjs} +3 -2
  27. package/dist/normalize-BjG6unTj.mjs +317 -0
  28. package/dist/policy/index.d.mts +1 -1
  29. package/dist/policy/index.mjs +2 -2
  30. package/dist/prompt/index.d.mts +1 -1
  31. package/dist/{run-CdIlUtjv.mjs → run-DkrzC0gb.mjs} +1 -1
  32. package/dist/{run-TAXNRSpD.mjs → run-GHAMqcOn.mjs} +26 -3
  33. package/dist/runtime/index.d.mts +27 -16
  34. package/dist/runtime/index.mjs +2 -2
  35. package/dist/{types-C0-igRKr.d.mts → types-Bh0jEF3_.d.mts} +2 -0
  36. package/dist/{validate-DZFtAbLP.mjs → validate-KjZ1X9tH.mjs} +1 -1
  37. package/package.json +3 -2
  38. package/src/adapters/claude/index.ts +92 -0
  39. package/src/adapters/cline/index.ts +36 -0
  40. package/src/adapters/codex/apply-patch.ts +94 -0
  41. package/src/adapters/codex/index.ts +84 -0
  42. package/src/adapters/codex/permission-mode.ts +16 -0
  43. package/src/adapters/cursor/context.ts +46 -0
  44. package/src/adapters/cursor/events.ts +38 -0
  45. package/src/adapters/cursor/index.ts +43 -0
  46. package/src/adapters/cursor/interfaces/types.ts +88 -0
  47. package/src/adapters/cursor/native-response.ts +158 -0
  48. package/src/adapters/cursor/normalize.ts +123 -0
  49. package/src/adapters/cursor/respond.ts +123 -0
  50. package/src/adapters/gemini/index.ts +36 -0
  51. package/src/adapters/hermes/index.ts +55 -0
  52. package/src/adapters/hermes/interfaces/types.ts +26 -0
  53. package/src/adapters/kimi/index.ts +96 -0
  54. package/src/adapters/kimi/interfaces/types.ts +55 -0
  55. package/src/cache/compact.ts +55 -0
  56. package/src/cache/index.ts +5 -0
  57. package/src/cache/io.ts +38 -0
  58. package/src/cache/mcp-response.ts +39 -0
  59. package/src/cache/mcp-store.ts +70 -0
  60. package/src/cache/store.ts +93 -0
  61. package/src/changelog/fetch.ts +79 -0
  62. package/src/cli/bin.ts +100 -0
  63. package/src/cli/cursor-event-scanner.ts +175 -0
  64. package/src/cli/cursor-stdin-reader.ts +65 -0
  65. package/src/cli/doctor.ts +96 -0
  66. package/src/cli/hook-io.ts +130 -0
  67. package/src/cli/hook-sound.ts +49 -0
  68. package/src/cli/index.ts +1 -0
  69. package/src/cli/json-primitive-scanner.ts +83 -0
  70. package/src/cli/run.ts +36 -0
  71. package/src/cli/scope.ts +31 -0
  72. package/src/codex-rules/build.ts +63 -0
  73. package/src/codex-rules/index.ts +8 -0
  74. package/src/codex-rules/mapping.ts +42 -0
  75. package/src/codex-rules/rules/absolute-paths.ts +52 -0
  76. package/src/codex-rules/rules/ask.ts +44 -0
  77. package/src/codex-rules/rules/chmod-ask.ts +57 -0
  78. package/src/codex-rules/rules/dd.ts +23 -0
  79. package/src/codex-rules/rules/diskutil.ts +33 -0
  80. package/src/codex-rules/rules/mkfs.ts +42 -0
  81. package/src/codex-rules/rules/privilege.ts +49 -0
  82. package/src/codex-rules/rules/remote-fetch.ts +31 -0
  83. package/src/codex-rules/rules/rm-variants.ts +58 -0
  84. package/src/codex-rules/rules/single-commands.ts +57 -0
  85. package/src/codex-rules/skip-list.ts +25 -0
  86. package/src/codex-rules/starlark.ts +46 -0
  87. package/src/codex-rules/types.ts +36 -0
  88. package/src/config/dotenv.ts +55 -0
  89. package/src/config/env.ts +10 -0
  90. package/src/config/home-dir.ts +36 -0
  91. package/src/config/index.ts +5 -0
  92. package/src/config/layout.ts +52 -0
  93. package/src/config/limits.ts +56 -0
  94. package/src/config/ttl.ts +30 -0
  95. package/src/detect/harness.ts +73 -0
  96. package/src/detect/index.ts +1 -0
  97. package/src/detect/interfaces/types.ts +18 -0
  98. package/src/freshness/agent-evidence-record.ts +95 -0
  99. package/src/freshness/agent-evidence.ts +95 -0
  100. package/src/freshness/codex-spawn-evidence.ts +91 -0
  101. package/src/freshness/doc-helpers.ts +90 -0
  102. package/src/freshness/evidence-harvest-io.ts +72 -0
  103. package/src/freshness/evidence-harvest.ts +61 -0
  104. package/src/freshness/explore-tools.ts +95 -0
  105. package/src/freshness/index.ts +2 -0
  106. package/src/freshness/query-framework.ts +73 -0
  107. package/src/freshness/ref-evidence.ts +49 -0
  108. package/src/freshness/ref-journal.ts +98 -0
  109. package/src/freshness/trivial-edit-counter.ts +30 -0
  110. package/src/index.ts +11 -0
  111. package/src/init/index.ts +2 -0
  112. package/src/init/run.ts +32 -0
  113. package/src/init/templates.ts +75 -0
  114. package/src/memory/gitignore.ts +14 -0
  115. package/src/memory/index.ts +3 -0
  116. package/src/memory/registry.ts +39 -0
  117. package/src/memory/session-roots.ts +99 -0
  118. package/src/memory/state.ts +58 -0
  119. package/src/policy/apex-authorization.ts +100 -0
  120. package/src/policy/apex-gates.ts +91 -0
  121. package/src/policy/apex-target-steps.ts +73 -0
  122. package/src/policy/apex-target.ts +49 -0
  123. package/src/policy/apex-task-context.ts +81 -0
  124. package/src/policy/apex.ts +94 -0
  125. package/src/policy/cartographer/build-tree.ts +69 -0
  126. package/src/policy/cartographer/describe.ts +53 -0
  127. package/src/policy/cartographer/entry.ts +38 -0
  128. package/src/policy/cartographer/frontmatter.ts +48 -0
  129. package/src/policy/cartographer/indicators.ts +34 -0
  130. package/src/policy/claude-md-context.ts +66 -0
  131. package/src/policy/conventions/index.ts +11 -0
  132. package/src/policy/conventions/interfaces.ts +71 -0
  133. package/src/policy/conventions/langs.ts +53 -0
  134. package/src/policy/conventions/query.ts +49 -0
  135. package/src/policy/conventions/react-hooks.ts +18 -0
  136. package/src/policy/conventions/store-or-query.ts +21 -0
  137. package/src/policy/conventions/stores.ts +56 -0
  138. package/src/policy/conventions/strip.ts +87 -0
  139. package/src/policy/conventions/verdict.ts +38 -0
  140. package/src/policy/creation-intent.ts +11 -0
  141. package/src/policy/deny-loop.ts +88 -0
  142. package/src/policy/design/allowed-write.ts +28 -0
  143. package/src/policy/design/content-checks.ts +45 -0
  144. package/src/policy/design/corpus-fs.ts +21 -0
  145. package/src/policy/design/corpus-resolve-cache.ts +49 -0
  146. package/src/policy/design/corpus-resolve.ts +99 -0
  147. package/src/policy/design/corpus.ts +92 -0
  148. package/src/policy/design/design-system-rules.ts +45 -0
  149. package/src/policy/design/flag.ts +30 -0
  150. package/src/policy/design/gates-pipeline.ts +105 -0
  151. package/src/policy/design/gates.ts +87 -0
  152. package/src/policy/design/screenshot-tools.ts +24 -0
  153. package/src/policy/design/skill-evidence.ts +34 -0
  154. package/src/policy/design/skill-gate.ts +81 -0
  155. package/src/policy/design/skill-triggers.ts +51 -0
  156. package/src/policy/design/state.ts +92 -0
  157. package/src/policy/design/template-urls.ts +54 -0
  158. package/src/policy/design/transitions.ts +68 -0
  159. package/src/policy/detect-claude-md-project-type.ts +32 -0
  160. package/src/policy/detect-framework.ts +99 -0
  161. package/src/policy/detect-primitive-checks.ts +100 -0
  162. package/src/policy/detect-primitive-lib.ts +55 -0
  163. package/src/policy/detect-project.ts +99 -0
  164. package/src/policy/edit-outcome.ts +53 -0
  165. package/src/policy/evaluate.ts +81 -0
  166. package/src/policy/expert-agents.ts +60 -0
  167. package/src/policy/file-size-scope.ts +32 -0
  168. package/src/policy/file-size.ts +99 -0
  169. package/src/policy/framework-solid-exclude.ts +52 -0
  170. package/src/policy/framework-solid-extended.ts +93 -0
  171. package/src/policy/framework-solid-gates-systems.ts +99 -0
  172. package/src/policy/framework-solid-gates.ts +98 -0
  173. package/src/policy/framework-solid.ts +88 -0
  174. package/src/policy/gemini-mcp-gate.ts +83 -0
  175. package/src/policy/git-gates.ts +40 -0
  176. package/src/policy/guards/bash-command-anchor.ts +40 -0
  177. package/src/policy/guards/bash-write-patterns.ts +140 -0
  178. package/src/policy/guards/bash-write-redirects.ts +201 -0
  179. package/src/policy/guards/bash-write-safe-paths.ts +62 -0
  180. package/src/policy/guards/bash-write.ts +76 -0
  181. package/src/policy/guards/context.ts +12 -0
  182. package/src/policy/guards/index.ts +61 -0
  183. package/src/policy/guards/install.ts +22 -0
  184. package/src/policy/guards/interface-separation-ext.ts +41 -0
  185. package/src/policy/guards/interface-separation.ts +100 -0
  186. package/src/policy/guards/protected-path.ts +100 -0
  187. package/src/policy/guards/security.ts +97 -0
  188. package/src/policy/index.ts +19 -0
  189. package/src/policy/interfaces/types.ts +46 -0
  190. package/src/policy/js-gate-route.ts +51 -0
  191. package/src/policy/lessons/lesson-gate.ts +0 -0
  192. package/src/policy/lessons/trigger-index.ts +84 -0
  193. package/src/policy/lessons/types.ts +31 -0
  194. package/src/policy/module-layout.ts +31 -0
  195. package/src/policy/nearest-manifest.ts +75 -0
  196. package/src/policy/never-approval.ts +95 -0
  197. package/src/policy/patterns.ts +80 -0
  198. package/src/policy/seo/validate.ts +51 -0
  199. package/src/policy/shadcn-project.ts +22 -0
  200. package/src/policy/shadcn-skill-gate.ts +97 -0
  201. package/src/policy/shell-read-refs.ts +96 -0
  202. package/src/policy/skill-path.ts +46 -0
  203. package/src/policy/skill-patterns/laravel.ts +40 -0
  204. package/src/policy/skill-patterns/nextjs.ts +29 -0
  205. package/src/policy/skill-patterns/react.ts +27 -0
  206. package/src/policy/skill-patterns/shadcn-subskills.ts +33 -0
  207. package/src/policy/skill-patterns/shadcn.ts +66 -0
  208. package/src/policy/skill-patterns/swift.ts +30 -0
  209. package/src/policy/skill-patterns/tailwind.ts +40 -0
  210. package/src/policy/skill-source.ts +52 -0
  211. package/src/policy/skill-trigger-patterns.ts +33 -0
  212. package/src/policy/skill-triggers.ts +108 -0
  213. package/src/policy/tailwind-skill-gate.ts +63 -0
  214. package/src/policy/trivial-edits.ts +52 -0
  215. package/src/policy/verbosity.ts +39 -0
  216. package/src/prompt/index.ts +1 -0
  217. package/src/prompt/types.ts +36 -0
  218. package/src/refs/discover.ts +69 -0
  219. package/src/refs/frontmatter.ts +24 -0
  220. package/src/refs/index.ts +4 -0
  221. package/src/refs/loader.ts +59 -0
  222. package/src/refs/ref-key.ts +41 -0
  223. package/src/refs/router.ts +63 -0
  224. package/src/refs/types.ts +25 -0
  225. package/src/runtime/activity.ts +99 -0
  226. package/src/runtime/apply-patch-gate.ts +41 -0
  227. package/src/runtime/burst-window.ts +21 -0
  228. package/src/runtime/codex-shell-tool.ts +37 -0
  229. package/src/runtime/command-string.ts +37 -0
  230. package/src/runtime/confirm/codex-confirm.ts +126 -0
  231. package/src/runtime/confirm/codex-prompt-origin.ts +16 -0
  232. package/src/runtime/confirm/confirm-code.ts +33 -0
  233. package/src/runtime/confirm/confirm-gate.ts +65 -0
  234. package/src/runtime/confirm/confirm-irreversible.ts +32 -0
  235. package/src/runtime/confirm/confirm-pending.ts +34 -0
  236. package/src/runtime/confirm/confirm-state.ts +56 -0
  237. package/src/runtime/confirm/confirm-subagent.ts +112 -0
  238. package/src/runtime/confirm/confirm-submit.ts +38 -0
  239. package/src/runtime/core-guards-active.ts +84 -0
  240. package/src/runtime/core-guards-wired.ts +47 -0
  241. package/src/runtime/deny-loop-store.ts +94 -0
  242. package/src/runtime/deny-notice.ts +52 -0
  243. package/src/runtime/design-cache-resolve.ts +51 -0
  244. package/src/runtime/design-content-gate.ts +95 -0
  245. package/src/runtime/design-files-gate.ts +114 -0
  246. package/src/runtime/design-helpers.ts +93 -0
  247. package/src/runtime/design-lifecycle.ts +39 -0
  248. package/src/runtime/design-read-credit.ts +28 -0
  249. package/src/runtime/design.ts +116 -0
  250. package/src/runtime/dev-context.ts +43 -0
  251. package/src/runtime/dry-patterns.ts +32 -0
  252. package/src/runtime/dry.ts +96 -0
  253. package/src/runtime/fragment-registry.ts +33 -0
  254. package/src/runtime/framework-skill-gate.ts +79 -0
  255. package/src/runtime/fs-cleanup.ts +71 -0
  256. package/src/runtime/gate-apex.ts +81 -0
  257. package/src/runtime/gate-helpers.ts +52 -0
  258. package/src/runtime/gate-input.ts +26 -0
  259. package/src/runtime/gate.ts +128 -0
  260. package/src/runtime/handle-post.ts +104 -0
  261. package/src/runtime/handle-pre.ts +137 -0
  262. package/src/runtime/handle-scope-async.ts +29 -0
  263. package/src/runtime/handle-types.ts +27 -0
  264. package/src/runtime/handle.ts +116 -0
  265. package/src/runtime/home-state.ts +55 -0
  266. package/src/runtime/index.ts +18 -0
  267. package/src/runtime/inform.ts +28 -0
  268. package/src/runtime/inject-budget-recap.ts +47 -0
  269. package/src/runtime/inject-budget.ts +81 -0
  270. package/src/runtime/inject-context.ts +85 -0
  271. package/src/runtime/inject-dedup-exclusive.ts +98 -0
  272. package/src/runtime/inject-dedup.ts +89 -0
  273. package/src/runtime/is-agent-tool.ts +19 -0
  274. package/src/runtime/lifecycle/agent-files.ts +49 -0
  275. package/src/runtime/lifecycle/agent-memory.ts +82 -0
  276. package/src/runtime/lifecycle/agent-transcript.ts +80 -0
  277. package/src/runtime/lifecycle/aipilot/analytics.ts +99 -0
  278. package/src/runtime/lifecycle/aipilot/apex-task-store.ts +61 -0
  279. package/src/runtime/lifecycle/aipilot/cache-base.ts +48 -0
  280. package/src/runtime/lifecycle/aipilot/cache-doc.ts +100 -0
  281. package/src/runtime/lifecycle/aipilot/cache-lessons.ts +58 -0
  282. package/src/runtime/lifecycle/aipilot/cache-test.ts +70 -0
  283. package/src/runtime/lifecycle/aipilot/curate-lessons.ts +64 -0
  284. package/src/runtime/lifecycle/aipilot/dispatch-aipilot.ts +100 -0
  285. package/src/runtime/lifecycle/aipilot/doc-cache-gate.ts +69 -0
  286. package/src/runtime/lifecycle/aipilot/inject-apex.ts +98 -0
  287. package/src/runtime/lifecycle/aipilot/inject-doc.ts +62 -0
  288. package/src/runtime/lifecycle/aipilot/inject-explore.ts +64 -0
  289. package/src/runtime/lifecycle/aipilot/inject-lessons.ts +61 -0
  290. package/src/runtime/lifecycle/aipilot/inject-test.ts +48 -0
  291. package/src/runtime/lifecycle/aipilot/lesson-archive.ts +60 -0
  292. package/src/runtime/lifecycle/aipilot/lesson-inject.ts +98 -0
  293. package/src/runtime/lifecycle/aipilot/lesson-parse.ts +74 -0
  294. package/src/runtime/lifecycle/aipilot/lessons.ts +72 -0
  295. package/src/runtime/lifecycle/aipilot/promote-global-lessons.ts +92 -0
  296. package/src/runtime/lifecycle/aipilot/solid-compliance.ts +69 -0
  297. package/src/runtime/lifecycle/aipilot/solid-notice.ts +47 -0
  298. package/src/runtime/lifecycle/aipilot/solid-transcript.ts +60 -0
  299. package/src/runtime/lifecycle/aipilot/source-scan.ts +56 -0
  300. package/src/runtime/lifecycle/aipilot/sync-task.ts +75 -0
  301. package/src/runtime/lifecycle/aipilot/transcript.ts +88 -0
  302. package/src/runtime/lifecycle/aipilot/types.ts +72 -0
  303. package/src/runtime/lifecycle/auto-document-reads.ts +73 -0
  304. package/src/runtime/lifecycle/cartographer/detect.ts +57 -0
  305. package/src/runtime/lifecycle/cartographer/ecosystem-map.ts +56 -0
  306. package/src/runtime/lifecycle/cartographer/fs-util.ts +64 -0
  307. package/src/runtime/lifecycle/cartographer/merge.ts +51 -0
  308. package/src/runtime/lifecycle/cartographer/project-map.ts +49 -0
  309. package/src/runtime/lifecycle/cartographer/scan-hooks.ts +48 -0
  310. package/src/runtime/lifecycle/cartographer/scan.ts +81 -0
  311. package/src/runtime/lifecycle/cartographer/session-start.ts +33 -0
  312. package/src/runtime/lifecycle/cartographer/track-enrichment.ts +45 -0
  313. package/src/runtime/lifecycle/cartographer/write-plugin-map.ts +43 -0
  314. package/src/runtime/lifecycle/cartographer/write-tree.ts +44 -0
  315. package/src/runtime/lifecycle/changelog-research.ts +43 -0
  316. package/src/runtime/lifecycle/check-file-size.ts +61 -0
  317. package/src/runtime/lifecycle/codex-resync/fingerprint.ts +63 -0
  318. package/src/runtime/lifecycle/codex-resync/lock.ts +46 -0
  319. package/src/runtime/lifecycle/codex-resync/managed-destination.ts +39 -0
  320. package/src/runtime/lifecycle/codex-resync/materialize-agents.ts +57 -0
  321. package/src/runtime/lifecycle/codex-resync/plugin-files.ts +65 -0
  322. package/src/runtime/lifecycle/codex-resync/plugin-roots.ts +62 -0
  323. package/src/runtime/lifecycle/codex-resync/resync.ts +45 -0
  324. package/src/runtime/lifecycle/codex-resync/symlink-commands.ts +25 -0
  325. package/src/runtime/lifecycle/dispatch.ts +109 -0
  326. package/src/runtime/lifecycle/failure-lesson.ts +51 -0
  327. package/src/runtime/lifecycle/index.ts +34 -0
  328. package/src/runtime/lifecycle/inject-rules.ts +50 -0
  329. package/src/runtime/lifecycle/instructions-loaded.ts +22 -0
  330. package/src/runtime/lifecycle/kimi-rules-native.ts +44 -0
  331. package/src/runtime/lifecycle/lessons/dispatch.ts +88 -0
  332. package/src/runtime/lifecycle/lessons/reminder.ts +89 -0
  333. package/src/runtime/lifecycle/lessons/state.ts +23 -0
  334. package/src/runtime/lifecycle/memory/agent-lesson.ts +35 -0
  335. package/src/runtime/lifecycle/memory/capture-error.ts +41 -0
  336. package/src/runtime/lifecycle/memory/client.ts +65 -0
  337. package/src/runtime/lifecycle/memory/dispatch.ts +42 -0
  338. package/src/runtime/lifecycle/memory/recall.ts +30 -0
  339. package/src/runtime/lifecycle/memory/salience.ts +31 -0
  340. package/src/runtime/lifecycle/memory/state.ts +49 -0
  341. package/src/runtime/lifecycle/memory/track-ops.ts +15 -0
  342. package/src/runtime/lifecycle/post-compact.ts +38 -0
  343. package/src/runtime/lifecycle/post-edit-ts.ts +49 -0
  344. package/src/runtime/lifecycle/post-tracking.ts +40 -0
  345. package/src/runtime/lifecycle/pre-compact.ts +38 -0
  346. package/src/runtime/lifecycle/rules-root.ts +83 -0
  347. package/src/runtime/lifecycle/security/check-skill.ts +63 -0
  348. package/src/runtime/lifecycle/security/scan-patterns.ts +53 -0
  349. package/src/runtime/lifecycle/security/scan.ts +76 -0
  350. package/src/runtime/lifecycle/security/skill-state.ts +47 -0
  351. package/src/runtime/lifecycle/security/track-mcp.ts +29 -0
  352. package/src/runtime/lifecycle/security/track-skill-read.ts +31 -0
  353. package/src/runtime/lifecycle/seo/post-tool-use.ts +45 -0
  354. package/src/runtime/lifecycle/session-end.ts +17 -0
  355. package/src/runtime/lifecycle/session-start.ts +57 -0
  356. package/src/runtime/lifecycle/snapshot/board.ts +27 -0
  357. package/src/runtime/lifecycle/snapshot/format.ts +60 -0
  358. package/src/runtime/lifecycle/snapshot/git.ts +71 -0
  359. package/src/runtime/lifecycle/snapshot/index.ts +52 -0
  360. package/src/runtime/lifecycle/snapshot/version.ts +34 -0
  361. package/src/runtime/lifecycle/solid-detect.ts +56 -0
  362. package/src/runtime/lifecycle/stop-core.ts +35 -0
  363. package/src/runtime/lifecycle/subagent-cache.ts +61 -0
  364. package/src/runtime/lifecycle/task-completed.ts +94 -0
  365. package/src/runtime/lifecycle/teammate-idle-check.ts +63 -0
  366. package/src/runtime/lifecycle/teammate-idle.ts +28 -0
  367. package/src/runtime/lifecycle/tool-failure.ts +24 -0
  368. package/src/runtime/lifecycle/track-changes.ts +61 -0
  369. package/src/runtime/lifecycle/validate-solid.ts +70 -0
  370. package/src/runtime/lifecycle/validate-tailwind.ts +80 -0
  371. package/src/runtime/lifecycle-bridge.ts +74 -0
  372. package/src/runtime/mcp-key.ts +44 -0
  373. package/src/runtime/mcp-tool-name.ts +92 -0
  374. package/src/runtime/mcp.ts +91 -0
  375. package/src/runtime/modular.ts +77 -0
  376. package/src/runtime/normalize.ts +107 -0
  377. package/src/runtime/notices.ts +91 -0
  378. package/src/runtime/notification-sound.ts +49 -0
  379. package/src/runtime/notifications.ts +77 -0
  380. package/src/runtime/paths.ts +59 -0
  381. package/src/runtime/post-fanout.ts +41 -0
  382. package/src/runtime/post-outcome.ts +61 -0
  383. package/src/runtime/pre-allow.ts +97 -0
  384. package/src/runtime/precommit.ts +60 -0
  385. package/src/runtime/prompt-text.ts +38 -0
  386. package/src/runtime/receipt-capture.ts +34 -0
  387. package/src/runtime/record.ts +26 -0
  388. package/src/runtime/respond.ts +92 -0
  389. package/src/runtime/solid-file-size-gate.ts +58 -0
  390. package/src/runtime/solid-pre.ts +53 -0
  391. package/src/runtime/storage.ts +9 -0
  392. package/src/statusline/colors.ts +31 -0
  393. package/src/statusline/constants.ts +17 -0
  394. package/src/statusline/formatters.ts +54 -0
  395. package/src/statusline/index.ts +4 -0
  396. package/src/statusline/progress-bar.ts +46 -0
  397. package/src/tracking/codex-post-failure.ts +73 -0
  398. package/src/tracking/index.ts +2 -0
  399. package/src/tracking/integrity.ts +99 -0
  400. package/src/tracking/one-shot-dedup.ts +34 -0
  401. package/src/tracking/one-shot-failure.ts +42 -0
  402. package/src/tracking/one-shot-store.ts +95 -0
  403. package/src/tracking/one-shot.ts +93 -0
  404. package/src/tracking/receipts.ts +95 -0
  405. package/src/tracking/session-state.ts +94 -0
  406. package/src/tracking/store.ts +96 -0
  407. package/src/tracking/track-compact.ts +100 -0
  408. package/src/tracking/track-diff.ts +50 -0
  409. package/src/tracking/track-journal.ts +97 -0
  410. package/src/tracking/track-lock-sync.ts +83 -0
  411. package/src/tracking/track-lock.ts +67 -0
  412. package/src/util/compact-json.ts +23 -0
  413. package/src/util/index.ts +2 -0
  414. package/src/util/json-io.ts +47 -0
  415. package/src/util/project-root.ts +41 -0
  416. package/src/util/runtime-io.ts +85 -0
  417. package/src/util/semver.ts +30 -0
@@ -0,0 +1,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
+ }
@@ -0,0 +1,51 @@
1
+ import { existsSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { fuseHarnessHome, sanitizeSessionId } from "./home-state";
4
+
5
+ /** `~/.fuse-harness/design-sessions/<sid>` — session-anchored design-state dir. */
6
+ function sessionCacheDir(sid: string, home?: string): string {
7
+ return join(fuseHarnessHome(home), "design-sessions", sid);
8
+ }
9
+
10
+ /** True when `dir` already holds the design-agent flag or a `.design-state-*.json` snapshot. */
11
+ function hasDesignState(dir: string): boolean {
12
+ if (existsSync(join(dir, "design-agent-active"))) return true;
13
+ try {
14
+ return readdirSync(dir).some((name) => name.startsWith(".design-state-"));
15
+ } catch {
16
+ return false;
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Resolve the cache dir the design pipeline (active-agent flag +
22
+ * `.design-state-<agentId>.json`) should read AND write for this hook call.
23
+ *
24
+ * Anchored on the session id, not the invoking process's `cwd`: a design
25
+ * agent's `SubagentStart` and its later tool writes can run with different
26
+ * `process.cwd()` values, and a cwd-keyed cache dir then silently misses the
27
+ * flag/state — every design gate fails open (see MEMORY/LESSON.md).
28
+ *
29
+ * Contract (non-regression): `resolved = sessionDirIfPopulated ??
30
+ * cwdDirIfPopulated ?? sessionDirDefault`. Before this fix the session-keyed
31
+ * dir never existed, so for every session whose state already lives under
32
+ * `cwdCacheDir` the FIRST branch is always empty and resolution falls through
33
+ * to `cwdCacheDir` — byte-identical to the pre-fix value. Only a session with
34
+ * NO state anywhere (today's silent-fail-open case, e.g. cwd drifted between
35
+ * `SubagentStart` and the write) diverges, anchoring on the session dir
36
+ * instead of silently missing the flag. Read and write always resolve through
37
+ * this same function against the same two inputs, so they can never target
38
+ * different locations mid-session.
39
+ * @param sessionId - The hook event's session id (`NormalizedEvent.sessionId`).
40
+ * @param cwdCacheDir - The legacy, cwd-derived cache dir (`projectLayout(cwd).cacheDir`).
41
+ * @param home - OS home override (tests only).
42
+ * @returns The cache dir to pass to the design flag/state/gate functions.
43
+ */
44
+ export function resolveDesignCacheDir(sessionId: string, cwdCacheDir: string, home?: string): string {
45
+ const sid = sanitizeSessionId(sessionId);
46
+ if (!sid) return cwdCacheDir; // no stable session id -> unchanged legacy behavior
47
+ const sessDir = sessionCacheDir(sid, home);
48
+ if (hasDesignState(sessDir)) return sessDir;
49
+ if (hasDesignState(cwdCacheDir)) return cwdCacheDir;
50
+ return sessDir;
51
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * @module design-content-gate
3
+ * PRE content gate for design-system.md — the nominal path (validateDesignSystem
4
+ * used to run only behind the opt-in Gemini branch, so designSystemValid meant
5
+ * "a write happened", not "the content is valid"). Split out of
6
+ * `design-helpers.ts` to keep both files within the SOLID size budget (SRP).
7
+ * @packageDocumentation
8
+ */
9
+ import { readFileSync, existsSync } from "node:fs";
10
+ import type { Prompt } from "../prompt/types";
11
+ import type { DesignState } from "../policy/design/state";
12
+ import { deny, validateDesignSystem } from "../policy/design/gates";
13
+ import { citedCorpusRefs, citationJoinsReads } from "../policy/design/corpus";
14
+ import { substituteLiteral } from "./design-files-gate";
15
+
16
+ /** Inputs for {@link designSystemContentGate} — the PRE write event plus resolved corpus context. */
17
+ interface ContentGateInput {
18
+ filePath: string;
19
+ tool: string;
20
+ content: string;
21
+ oldString?: string;
22
+ replaceAll: boolean;
23
+ state: DesignState;
24
+ corpusRoot: string;
25
+ corpusRequired: boolean;
26
+ }
27
+
28
+ /**
29
+ * Every content violation of a design-system.md text, GRANULAR (one entry per
30
+ * unmet requirement and per unread cited reference — so a worsening check can
31
+ * diff two violation sets): the four hard requirements, plus the citation↔read
32
+ * jointure when the corpus is delivered.
33
+ */
34
+ export function designSystemProblems(content: string, reads: readonly string[], corpusRequired: boolean): string[] {
35
+ const problems = validateDesignSystem(content, corpusRequired);
36
+ if (corpusRequired) {
37
+ for (const ref of citedCorpusRefs(content)) {
38
+ if (!citationJoinsReads(ref, reads)) problems.push(`cited corpus reference never read: ${ref}`);
39
+ }
40
+ }
41
+ return problems;
42
+ }
43
+
44
+ /** Content check for a design-system.md already on disk (the Gemini create_frontend path). */
45
+ export function checkDesignSystemContent(content: string, reads: readonly string[], corpusRequired: boolean): Prompt | null {
46
+ const problems = designSystemProblems(content, reads, corpusRequired);
47
+ return problems.length ? deny(`BLOCKED: design-system.md too generic. Missing: ${problems.join(", ")}.`) : null;
48
+ }
49
+
50
+ /**
51
+ * The content a Write/Edit would LEAVE in design-system.md, and the on-disk
52
+ * original when the file exists (for BOTH tools). Edit reconstructs via
53
+ * literal substitution (first occurrence, or all under replace_all — the Edit
54
+ * tool's own semantics, mirroring edit-outcome.ts). Null when unverifiable
55
+ * (unreadable file or stale old_string) — fail-closed, such an Edit fails at
56
+ * tool level anyway.
57
+ */
58
+ function effectiveContents(i: ContentGateInput): { result: string; original: string | null } | null {
59
+ if (i.tool !== "Edit") {
60
+ let original: string | null = null;
61
+ try {
62
+ if (existsSync(i.filePath)) original = readFileSync(i.filePath, "utf8");
63
+ } catch {
64
+ original = null;
65
+ }
66
+ return { result: i.content, original };
67
+ }
68
+ let disk: string;
69
+ try {
70
+ disk = readFileSync(i.filePath, "utf8");
71
+ } catch {
72
+ return null;
73
+ }
74
+ if (!i.oldString || !disk.includes(i.oldString)) return null;
75
+ return { result: substituteLiteral(disk, i.oldString, i.content, i.replaceAll), original: disk };
76
+ }
77
+
78
+ /**
79
+ * Block only the violations the change INTRODUCES (`problems(result) ⊄
80
+ * problems(original)`): a legacy design-system.md that predates the doctrine
81
+ * stays editable — and rewritable — including byte-identically or toward
82
+ * conformance; a change that adds a NEW problem (or writes an invalid file
83
+ * from scratch) is blocked.
84
+ */
85
+ export function designSystemContentGate(i: ContentGateInput): Prompt | null {
86
+ if (!i.filePath.endsWith("design-system.md")) return null;
87
+ const pair = effectiveContents(i);
88
+ if (pair === null) return deny("BLOCKED: cannot verify the edit result (unreadable file or stale old_string). RECOVERY: re-read design-system.md, then retry.");
89
+ const result = designSystemProblems(pair.result, i.state.corpusReads, i.corpusRequired);
90
+ if (!result.length) return null;
91
+ const original = pair.original !== null ? designSystemProblems(pair.original, i.state.corpusReads, i.corpusRequired) : [];
92
+ const introduced = result.filter((p) => !original.includes(p));
93
+ if (!introduced.length) return null;
94
+ return deny(`BLOCKED: design-system.md too generic. Missing: ${introduced.join(", ")}. RECOVERY: fix the content (read cited refs-design references with the Read tool), then retry.`);
95
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * @module design-files-gate
3
+ * Gates for multi-file write primitives (Codex `apply_patch`, fanned into
4
+ * `event.files` by normalize.ts): apply_patch IS the Write tool under Codex,
5
+ * so the design pipeline must see it. Per op, because `content` means
6
+ * different things (apply-patch.ts:10-15):
7
+ * - add: content IS the full new document — content-gated like a Write;
8
+ * - update: content is the hunk's new side, APPROXIMATE, never the document —
9
+ * only content-independent gates run (path, state-file, phase/quota).
10
+ * No BLOCKING or DEGRADING validation on this path — promote-only:
11
+ * the POST re-reads the real file, a clean content promotes to
12
+ * phase 3, a dirty one changes nothing. And it would not bite
13
+ * anyway: designSystemValid is consumed ONLY under
14
+ * FUSE_DESIGN_GEMINI=1 (off by default — an informational flag),
15
+ * and designSystemExists is never read after init;
16
+ * - delete: skipped (parity with the sibling applyPatchGate).
17
+ * One violating file blocks the whole envelope. `htmlCssOnlyGate` NOW applies
18
+ * here too (owner D2 revised): the two write paths must not diverge on what
19
+ * the design agent may write — an `apply_patch` `.tsx`/`.astro`/etc. add or
20
+ * update is just as much a framework-file write as the same content via
21
+ * `Write`/`Edit`, and letting it through only on this path was an unintended
22
+ * apply_patch-shaped bypass, not a deliberate relaxation. `uiDesignSkillGate`
23
+ * and the Gemini create_frontend precondition NO LONGER bypass apply_patch
24
+ * (fixed): the skill gate is wired in `design.ts` on `event.files` (same
25
+ * any-agent scope as its Write/Edit call site, before the agentId
26
+ * early-return); the Gemini precondition is ported into this function,
27
+ * right below `htmlCssOnlyGate`, gated the same way (opt-in,
28
+ * FUSE_DESIGN_GEMINI, state.geminiCalls === 0). REMAINING KNOWN GAPS
29
+ * (documented, still out of scope):
30
+ * `Design-System.md` (case) escapes every endsWith check, here and on
31
+ * Write — a real bypass on case-insensitive macOS, not widened now; and
32
+ * `*** Move to:` (apply-patch.ts:76-79) unconditionally OVERWRITES `cur.path`
33
+ * on ANY preceding op (`Add File`/`Update File`), so a `*** Update File: a.css`
34
+ * + `Move to: a.tsx` sequence surfaces here as `filePath` ending in `.tsx`
35
+ * (correctly blocked — NOT a bypass, verified against the parser). The
36
+ * un-verified direction is the reverse: `Add File: a.tsx` + `Move to: a.css`
37
+ * would surface only the PERMITTED destination extension while `f.content`
38
+ * is real framework code — the source path is never preserved anywhere, and
39
+ * this gate (like Write's htmlCssOnlyGate) checks only the extension, never
40
+ * the content's actual syntax. Whether Codex's real grammar allows Move-to
41
+ * after an Add (vs. Update-only) is unconfirmed; flagged, not fixed here.
42
+ * Also: the POST side still does NOT run `runDesignChecks` (tsx/jsx/css
43
+ * warnings) on an `apply_patch` file — `handle-post.ts` keeps `designGate` on
44
+ * the RAW, un-fanned envelope there (recordPost's apply_patch branch is
45
+ * promote-only and resolves `design-system.md` via `join(cwd, …)`; routing a
46
+ * fanned Write/Edit-shaped event through it instead would read `filePath`
47
+ * UNRESOLVED and risk degrading the state — a real regression, not a
48
+ * hypothetical one). Restoring the tsx/jsx/css warning would require
49
+ * resolving relative apply_patch paths AND preserving the promote-only
50
+ * doctrine inside `recordPost` at the same time — out of scope here, owner
51
+ * decision.
52
+ * @packageDocumentation
53
+ */
54
+ import type { Prompt } from "../prompt/types";
55
+ import type { NormalizedFile } from "./normalize";
56
+ import type { DesignState } from "../policy/design/state";
57
+ import { pluginsWriteGuard } from "../policy/design/corpus";
58
+ import { htmlCssOnlyGate, stateFileGate, geminiEnabled } from "../policy/design/gates";
59
+ import { designSystemWriteGate, htmlCssPipelineGate } from "../policy/design/gates-pipeline";
60
+ import { designSystemContentGate } from "./design-content-gate";
61
+
62
+ /**
63
+ * Literal substitution, first occurrence or all — NEVER String.replace's
64
+ * replacement-string semantics: an agent-controlled new_string of `$&`, `` $` ``,
65
+ * `$'` or `$$` must be written (and validated) literally, not interpreted as
66
+ * the matched text (that made the gate validate a document the tool would
67
+ * never write — the `$&` bypass). Used on both sides: PRE reconstruction and
68
+ * POST reverse-reconstruction.
69
+ */
70
+ export function substituteLiteral(s: string, from: string, to: string, all: boolean): string {
71
+ return all ? s.split(from).join(to) : s.replace(from, () => to);
72
+ }
73
+
74
+ /**
75
+ * Gate every file of a multi-file write primitive; the first violation blocks
76
+ * the envelope. `designSystemFileExists` is caller-computed (`findDesignSystem`
77
+ * lives in `design-helpers.ts`, which itself imports `substituteLiteral` from
78
+ * THIS module — importing it back here would cycle) so both write paths
79
+ * (Write/Edit in `design.ts`, apply_patch here) consume the SAME disk read.
80
+ */
81
+ export function designFilesGate(
82
+ files: readonly NormalizedFile[],
83
+ state: DesignState,
84
+ pluginsRoot: string,
85
+ corpusRoot: string,
86
+ corpusRequired: boolean,
87
+ cwd: string,
88
+ designSystemFileExists: boolean,
89
+ ): Prompt | null {
90
+ for (const f of files) {
91
+ const hit = pluginsWriteGuard(f.filePath, pluginsRoot, cwd) ?? stateFileGate(f.filePath);
92
+ if (hit) return hit;
93
+ if (f.op === "delete") continue;
94
+ const htmlCssHit = htmlCssOnlyGate(f.filePath);
95
+ if (htmlCssHit) return htmlCssHit;
96
+ // Parity with design.ts's htmlCssPipelineGate call: SAME verdict for the
97
+ // SAME file+state on both write paths (Write/Edit vs apply_patch).
98
+ const pipelineHit = htmlCssPipelineGate(f.filePath, state, designSystemFileExists);
99
+ if (pipelineHit) return pipelineHit;
100
+ // apply_patch parity for the Gemini precondition (design.ts:72-74, D2 gap):
101
+ // same condition, same Prompt shape as the Write/Edit branch — opt-in via
102
+ // FUSE_DESIGN_GEMINI (OFF by default), so a no-op when Gemini is disabled.
103
+ if (geminiEnabled() && state.geminiCalls === 0 && /\.(html|css)$/.test(f.filePath)) {
104
+ return { kind: "block", title: "Design pipeline", reason: "BLOCKED: generate the frontend via create_frontend before hand-writing HTML/CSS.", actions: ["Call mcp__gemini-design__create_frontend first"] };
105
+ }
106
+ if (!f.filePath.endsWith("design-system.md")) continue;
107
+ const gate = designSystemWriteGate(f.filePath, state, corpusRequired)
108
+ ?? (f.op === "add"
109
+ ? designSystemContentGate({ filePath: f.filePath, tool: "Write", content: f.content, oldString: undefined, replaceAll: false, state, corpusRoot, corpusRequired })
110
+ : null);
111
+ if (gate) return gate;
112
+ }
113
+ return null;
114
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * @module design-helpers
3
+ * Effectful helpers for the design pipeline gate: locate design-system.md and
4
+ * apply PostToolUse fuse-browser transitions to the design state. Split out of
5
+ * `design.ts` to keep that file within the SOLID size budget (SRP).
6
+ * @packageDocumentation
7
+ */
8
+ import { existsSync, readFileSync } from "node:fs";
9
+ import { dirname, isAbsolute, join } from "node:path";
10
+ import type { NormalizedEvent } from "./normalize";
11
+ import { type DesignState, saveDesignState } from "../policy/design/state";
12
+ import { recordNavigate, recordScroll, recordScreenshot, recordValidDesignSystem } from "../policy/design/transitions";
13
+ import { SHOT_TOOLS } from "../policy/design/screenshot-tools";
14
+ import { designSystemProblems } from "./design-content-gate";
15
+ import { substituteLiteral } from "./design-files-gate";
16
+ import { creditRead, creditShellReads } from "./design-read-credit";
17
+
18
+ export { designSystemContentGate } from "./design-content-gate";
19
+
20
+ const NAV = "mcp__fuse-browser__browser_navigate";
21
+ const SCROLL = "mcp__fuse-browser__browser_scroll";
22
+ const GEMINI = "mcp__gemini-design__create_frontend";
23
+
24
+ /** Read design-system.md walking up to 6 parents from `cwd` ("" if absent/unreadable). */
25
+ export function findDesignSystem(cwd: string): string {
26
+ let dir = cwd;
27
+ for (let i = 0; i < 6; i++) {
28
+ const p = join(dir, "design-system.md");
29
+ if (existsSync(p)) {
30
+ try {
31
+ return readFileSync(p, "utf8");
32
+ } catch {
33
+ return "";
34
+ }
35
+ }
36
+ const parent = dirname(dir);
37
+ if (parent === dir) break;
38
+ dir = parent;
39
+ }
40
+ return "";
41
+ }
42
+
43
+ /** Apply a PostToolUse fuse-browser transition to the design state. */
44
+ export function recordPost(event: NormalizedEvent, cacheDir: string, state: DesignState, corpusRoot = "", corpusRequired = false, cwd = ""): void {
45
+ if (SHOT_TOOLS.has(event.tool)) saveDesignState(cacheDir, recordScreenshot(state, corpusRequired));
46
+ else if (event.tool === NAV) saveDesignState(cacheDir, recordNavigate(state));
47
+ else if (event.tool === SCROLL) saveDesignState(cacheDir, recordScroll(state));
48
+ else if (event.tool === GEMINI) saveDesignState(cacheDir, { ...state, geminiCalls: state.geminiCalls + 1 });
49
+ else if (event.tool === "Read") creditRead(cacheDir, state, corpusRoot, corpusRequired, event.filePath ?? "");
50
+ // Codex Code Mode exec (`exec_command`, canonicalized to "Bash" by codex-shell-tool.ts).
51
+ else if (event.tool === "Bash" && event.command) creditShellReads(cacheDir, state, corpusRoot, corpusRequired, event.command);
52
+ else if ((event.tool === "Write" || event.tool === "Edit") && (event.filePath ?? "").endsWith("design-system.md")) {
53
+ // Write access ≠ validity, same rule both sides: POST validates only a
54
+ // zero-problem content and DEGRADES only what PRE would have blocked.
55
+ const fp = event.filePath ?? "";
56
+ let content: string;
57
+ try {
58
+ content = readFileSync(fp, "utf8");
59
+ } catch {
60
+ return;
61
+ }
62
+ const problems = designSystemProblems(content, state.corpusReads, corpusRequired);
63
+ if (!problems.length) {
64
+ saveDesignState(cacheDir, recordValidDesignSystem(state));
65
+ return;
66
+ }
67
+ // Edit: degrade only on problems the edit INTRODUCED (original reverse-rebuilt
68
+ // with LITERAL substitution; a deletion edit is not reconstructable — D5).
69
+ if (event.tool === "Edit" && event.oldString && event.content && content.includes(event.content)) {
70
+ const original = substituteLiteral(content, event.content, event.oldString, event.input.replace_all === true);
71
+ const introduced = problems.filter((p) => !designSystemProblems(original, state.corpusReads, corpusRequired).includes(p));
72
+ if (introduced.length) saveDesignState(cacheDir, { ...state, designSystemValid: false });
73
+ }
74
+ // Write / unreconstructable Edit: the PRE gate enforced not-worsening — never degrade here.
75
+ } else if (event.files) {
76
+ // Codex apply_patch: PROMOTE-ONLY, never blocking nor degrading — the POST
77
+ // re-reads the REAL file (the hunk is never the document): clean → phase 3,
78
+ // dirty → nothing. designSystemValid is only consumed under FUSE_DESIGN_GEMINI.
79
+ const ds = event.files.find((f) => f.op !== "delete" && f.filePath.endsWith("design-system.md"));
80
+ if (ds) {
81
+ const abs = isAbsolute(ds.filePath) ? ds.filePath : join(cwd, ds.filePath);
82
+ let content: string;
83
+ try {
84
+ content = readFileSync(abs, "utf8");
85
+ } catch {
86
+ return;
87
+ }
88
+ if (!designSystemProblems(content, state.corpusReads, corpusRequired).length) {
89
+ saveDesignState(cacheDir, recordValidDesignSystem(state));
90
+ }
91
+ }
92
+ }
93
+ }
@@ -0,0 +1,39 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { detectMode } from "../policy/design/transitions";
4
+ import { initDesignState, saveDesignState, cleanupDesignStates } from "../policy/design/state";
5
+ import { setActiveDesignAgent, clearActiveDesignAgent, activeDesignAgent } from "../policy/design/flag";
6
+ import { promptText } from "./prompt-text";
7
+
8
+ /**
9
+ * Handle the design-agent SubagentStart/Stop lifecycle: init the pipeline state +
10
+ * raise the active flag on start, archive/cleanup + clear the flag on stop.
11
+ * Returns true when it handled the event (caller should respond and stop).
12
+ *
13
+ * `SubagentStop` clears the flag whenever `agent_id` matches the currently
14
+ * active one, INDEPENDENTLY of `agent_type` on the stop payload — a stop event
15
+ * is not guaranteed to echo the same `agent_type` string the start event used,
16
+ * and gating the clear on it left the flag stuck (blocking every subsequent
17
+ * top-level Write/Edit in the session — see MEMORY/LESSON.md).
18
+ */
19
+ export function designLifecycle(payload: Record<string, unknown>, cacheDir: string, cwd: string, stamp: string, now: number): boolean {
20
+ const event = typeof payload.hook_event_name === "string" ? payload.hook_event_name : "";
21
+ const agentId = typeof payload.agent_id === "string" ? payload.agent_id : "";
22
+ if (event === "SubagentStop") {
23
+ if (!agentId || agentId !== activeDesignAgent(cacheDir)) return false;
24
+ cleanupDesignStates(cacheDir, agentId, stamp, now);
25
+ clearActiveDesignAgent(cacheDir);
26
+ return true;
27
+ }
28
+ const agentType = typeof payload.agent_type === "string" ? payload.agent_type : "";
29
+ if (!agentType.includes("design")) return false;
30
+ if (event === "SubagentStart") {
31
+ if (!agentId) return false;
32
+ const dsExists = existsSync(join(cwd, "design-system.md"));
33
+ const prompt = promptText(payload.prompt);
34
+ saveDesignState(cacheDir, initDesignState(agentId, detectMode(prompt, dsExists), dsExists));
35
+ setActiveDesignAgent(cacheDir, agentId);
36
+ return true;
37
+ }
38
+ return false;
39
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @module design-read-credit
3
+ * Shared credit path for a design-corpus/ref read, reused by both the native
4
+ * `Read` branch and the shell-read branch (Codex Code Mode `exec_command`,
5
+ * canonicalized to `"Bash"` by `codex-shell-tool.ts`) in
6
+ * `design-helpers.ts::recordPost`. Split out to keep that file within the
7
+ * SOLID size budget — zero new parser, zero new store: both callers hit the
8
+ * SAME `recordCorpusRead`/`recordRead` transitions.
9
+ * @packageDocumentation
10
+ */
11
+ import { existsSync } from "node:fs";
12
+ import type { DesignState } from "../policy/design/state";
13
+ import { saveDesignState } from "../policy/design/state";
14
+ import { recordCorpusRead, recordRead } from "../policy/design/transitions";
15
+ import { classifyCorpusRead } from "../policy/design/corpus";
16
+ import { shellReadRefPaths } from "../policy/shell-read-refs";
17
+
18
+ /** Credit one read path as a corpus read (anchored + on-disk) or a plain ref read. */
19
+ export function creditRead(cacheDir: string, state: DesignState, corpusRoot: string, corpusRequired: boolean, fp: string): void {
20
+ if (classifyCorpusRead(fp, corpusRoot) && existsSync(fp)) {
21
+ saveDesignState(cacheDir, recordCorpusRead(state, fp.slice(corpusRoot.length + 1), corpusRequired));
22
+ } else saveDesignState(cacheDir, recordRead(state, fp, corpusRequired));
23
+ }
24
+
25
+ /** Credit every `.md` path a read-only shell `command` targets, same path as {@link creditRead}. */
26
+ export function creditShellReads(cacheDir: string, state: DesignState, corpusRoot: string, corpusRequired: boolean, command: string): void {
27
+ for (const fp of shellReadRefPaths(command)) creditRead(cacheDir, state, corpusRoot, corpusRequired, fp);
28
+ }
@@ -0,0 +1,116 @@
1
+ import type { Prompt } from "../prompt/types";
2
+ import type { NormalizedEvent } from "./normalize";
3
+ import { loadDesignState, saveDesignState, initDesignState } from "../policy/design/state";
4
+ import { recordValidDesignSystem } from "../policy/design/transitions";
5
+ import { activeDesignAgent } from "../policy/design/flag";
6
+ import { runDesignChecks } from "../policy/design/content-checks";
7
+ import { uiDesignSkillGate } from "../policy/design/skill-gate";
8
+ import { collectDesignEvidence } from "../policy/design/skill-evidence";
9
+ import { findDesignSystem, recordPost, designSystemContentGate } from "./design-helpers";
10
+ import { checkDesignSystemContent } from "./design-content-gate";
11
+ import { designFilesGate } from "./design-files-gate";
12
+ import { resolveCorpusRoot, resolvePluginsRoot, pluginsWriteGuard } from "../policy/design/corpus";
13
+ import {
14
+ htmlCssOnlyGate, stateFileGate, screenshotScrollGate,
15
+ geminiEnabled,
16
+ } from "../policy/design/gates";
17
+ import { designSystemWriteGate, geminiCreateGate, browserNavigateGate, htmlCssPipelineGate } from "../policy/design/gates-pipeline";
18
+
19
+ const NAV = "mcp__fuse-browser__browser_navigate";
20
+ const SHOT = "mcp__fuse-browser__browser_screenshot";
21
+ const GEMINI = "mcp__gemini-design__create_frontend";
22
+
23
+ /**
24
+ * Design-pipeline gate (effectful: reads/writes the design state + design-system.md).
25
+ * Returns a Prompt to block, or null when this isn't a design-agent context / nothing fires.
26
+ */
27
+ export function designGate(payload: Record<string, unknown>, event: NormalizedEvent, cacheDir: string, cwd: string, corpusRootOverride?: string, pluginsRootOverride?: string): Prompt | null {
28
+ // UI design-skill gate (ports check-design-skill.py): fires for ANY agent on a
29
+ // UI write — requires a design-skill read + ANY doc source (Context7/Exa/web).
30
+ // Gemini is NEVER required. Runs before the design-agent pipeline state logic.
31
+ if (event.phase !== "post" && (event.tool === "Write" || event.tool === "Edit")) {
32
+ const skillBlock = uiDesignSkillGate(event.tool, event.filePath ?? "", event.content ?? "", collectDesignEvidence(event.sessionId, cwd));
33
+ if (skillBlock) return skillBlock;
34
+ }
35
+ // Codex apply_patch parity (D2 gap, docstring design-files-gate.ts:17-24): the
36
+ // same UI write can arrive fanned into event.files instead of a single
37
+ // Write/Edit. Map each non-delete file to its Write/Edit-equivalent tool
38
+ // ("add" -> Write, "update" -> Edit) and run the SAME gate, so the skill
39
+ // requirement cannot be bypassed just by routing the write through apply_patch.
40
+ // Scope matches the Write/Edit block above: ANY agent, before the agentId
41
+ // early-return — never narrowed to design-agent-only.
42
+ if (event.phase !== "post" && event.files && event.files.length > 0) {
43
+ const evidence = collectDesignEvidence(event.sessionId, cwd);
44
+ for (const f of event.files) {
45
+ if (f.op === "delete") continue;
46
+ const skillBlock = uiDesignSkillGate(f.op === "add" ? "Write" : "Edit", f.filePath, f.content, evidence);
47
+ if (skillBlock) return skillBlock;
48
+ }
49
+ }
50
+
51
+ const agentId = typeof payload.agent_id === "string" ? payload.agent_id : "";
52
+ if (!agentId) return null; // top-level (lead) calls are never design-agent-scoped
53
+ const active = activeDesignAgent(cacheDir);
54
+ if (active && agentId !== active) return null;
55
+ const id = agentId;
56
+ // P5 fail-open fix: when the design flag is active but the state file is missing
57
+ // (e.g. teammate context), auto-init a fresh state instead of disabling all
58
+ // gating (parity with pipeline-gate.py:38-60). Without the flag, stay inert.
59
+ let state = loadDesignState(cacheDir, id);
60
+ if (!state) {
61
+ if (!active) return null;
62
+ const dsExists = findDesignSystem(cwd) !== "";
63
+ state = initDesignState(id, dsExists ? "page" : "full", dsExists);
64
+ saveDesignState(cacheDir, state);
65
+ }
66
+ // Corpus + plugin roots, resolved INDEPENDENTLY (corpusRoot never decides
67
+ // pluginsRoot — F4). Corpus absent ⇒ requirement waived (pre-doctrine quotas);
68
+ // the plugin-root write guard stays active against refs-design fabrication.
69
+ const corpusRoot = corpusRootOverride ?? resolveCorpusRoot();
70
+ const corpusRequired = corpusRoot !== "";
71
+ const pluginsRoot = pluginsRootOverride ?? resolvePluginsRoot();
72
+
73
+ if (event.phase === "post") {
74
+ recordPost(event, cacheDir, state, corpusRoot, corpusRequired, cwd);
75
+ if ((event.tool === "Write" || event.tool === "Edit") && /\.(tsx|jsx|css)$/.test(event.filePath ?? "")) {
76
+ const warnings = runDesignChecks(event.content ?? "");
77
+ if (warnings.length) return { kind: "inform", title: "Design review", reason: warnings.join(" "), actions: [] };
78
+ }
79
+ return null;
80
+ }
81
+ if (event.tool === "Write" || event.tool === "Edit") {
82
+ const fp = event.filePath ?? "";
83
+ // Parity: only design-system.md is screenshot-quota-gated (designSystemWriteGate).
84
+ const base = pluginsWriteGuard(fp, pluginsRoot) ?? stateFileGate(fp) ?? htmlCssOnlyGate(fp)
85
+ ?? htmlCssPipelineGate(fp, state, findDesignSystem(cwd) !== "") ?? designSystemWriteGate(fp, state, corpusRequired)
86
+ ?? designSystemContentGate({ filePath: fp, tool: event.tool, content: event.content ?? "", oldString: event.oldString, replaceAll: event.input.replace_all === true, state, corpusRoot, corpusRequired });
87
+ if (base) return base;
88
+ if (geminiEnabled() && state.geminiCalls === 0 && /\.(html|css)$/.test(fp)) {
89
+ return { kind: "block", title: "Design pipeline", reason: "BLOCKED: generate the frontend via create_frontend before hand-writing HTML/CSS.", actions: ["Call mcp__gemini-design__create_frontend first"] };
90
+ }
91
+ return null;
92
+ }
93
+ // Codex apply_patch (D2): gate each fanned-out file like a Write.
94
+ if (event.files && event.files.length > 0) return designFilesGate(event.files, state, pluginsRoot, corpusRoot, corpusRequired, cwd, findDesignSystem(cwd) !== "");
95
+ if (event.tool === NAV) {
96
+ return browserNavigateGate(state, typeof event.input.url === "string" ? event.input.url : "");
97
+ }
98
+ if (event.tool === SHOT) return screenshotScrollGate(state);
99
+ if (event.tool === GEMINI) {
100
+ if (!geminiEnabled()) return null;
101
+ const block = geminiCreateGate(state);
102
+ if (block) return block;
103
+ const ds = findDesignSystem(cwd);
104
+ // Parity validate-design-system.py DENY_NOT_FOUND: a MISSING file gets its own
105
+ // recovery message, distinct from the "too generic" message for a present-but-thin one.
106
+ if (ds === "") {
107
+ return { kind: "block", title: "Design pipeline", reason: "BLOCKED: design-system.md not found. RECOVERY: 1) Read the identity templates 2) Read design-inspiration.md 3) Read the refs-design corpus (README.md + relevant tokens-*.md) 4) Screenshot 1-2 real sector sites 5) Write design-system.md, then retry create_frontend.", actions: ["Create design-system.md via the pipeline, then retry create_frontend"] };
108
+ }
109
+ // Same content rules as the nominal write path (jointure included): the
110
+ // Gemini branch must not be weaker than the gate the Write just went through.
111
+ const contentBlock = checkDesignSystemContent(ds, state.corpusReads, corpusRequired);
112
+ if (contentBlock) return contentBlock;
113
+ saveDesignState(cacheDir, recordValidDesignSystem(state));
114
+ }
115
+ return null;
116
+ }
@@ -0,0 +1,43 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { capFragment } from "./inject-budget";
5
+
6
+ /** Run a git subcommand in `cwd`, returning trimmed stdout or "" on error. */
7
+ function git(cwd: string, args: string[]): string {
8
+ try {
9
+ return execFileSync("git", args, { cwd, encoding: "utf-8", stdio: "pipe", timeout: 5000 }).trim();
10
+ } catch {
11
+ return "";
12
+ }
13
+ }
14
+
15
+ /** Build the git portion of the dev context (branch + up to 5 changed files). */
16
+ export function gitContext(cwd: string): string[] {
17
+ if (!existsSync(join(cwd, ".git"))) return [];
18
+ const parts = [`Git branch: ${git(cwd, ["branch", "--show-current"]) || "unknown"}`];
19
+ const status = git(cwd, ["status", "--porcelain"]);
20
+ if (status) parts.push("Modified files:\n" + status.split("\n").slice(0, 5).join("\n"));
21
+ return parts;
22
+ }
23
+
24
+ /** Build the project-type portion (mirrors load-dev-context.py exactly). */
25
+ export function projectContext(cwd: string): string[] {
26
+ const parts: string[] = [];
27
+ const has = (f: string): boolean => existsSync(join(cwd, f));
28
+ if (["next.config.js", "next.config.ts", "next.config.mjs"].some(has)) parts.push("Project: Next.js");
29
+ else if (has("package.json")) parts.push("Project: Node.js");
30
+ if (has("composer.json") && has("artisan")) parts.push("Project: Laravel");
31
+ if (has("Package.swift")) parts.push("Project: Swift");
32
+ return parts;
33
+ }
34
+
35
+ /**
36
+ * Build the SessionStart dev-context block (git + project type), or "" when
37
+ * nothing applies. Ports `core-guards/scripts/session-start/load-dev-context.py`.
38
+ * @param cwd - Project root to inspect.
39
+ * @returns The joined additionalContext text (possibly empty).
40
+ */
41
+ export function devContext(cwd: string): string {
42
+ return capFragment("dev-context", [...gitContext(cwd), ...projectContext(cwd)].join("\n"));
43
+ }