@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,45 @@
1
+ /**
2
+ * Content rules for design-system.md — the four hard requirements the
3
+ * pipeline validates on the nominal write path (and the opt-in Gemini one).
4
+ * Split out of `gates.ts` to keep both files within the SOLID size budget.
5
+ */
6
+ import { hasCorpusCitation } from "./corpus";
7
+
8
+ const OKLCH_RE = /oklch\(\s*[\d.]+%?\s+0\.0*[1-9]/;
9
+
10
+ /**
11
+ * Forbidden fonts, matched in USAGE contexts — never in free prose: a design
12
+ * system may SAY "Inter" ("## Interaction states", "we never use Inter"), it
13
+ * may not USE it as a font. Covered forms: CSS declarations (`font:`,
14
+ * `font-family:` in any case, multiline values), JS config (`fontFamily:`),
15
+ * custom properties named in English (`--*font*`/`--*ff*`/`--*type*`, value
16
+ * unquoted or quoted), ANY custom property whose value quotes a forbidden
17
+ * name (`--police-texte: "Inter", …` — a non-English property name has no
18
+ * keyword to key off, so the quote itself is the usage signal: a quoted
19
+ * token in a `--*` value is a font-family entry, not prose), `@import`
20
+ * URLs, and Markdown table cells (`| Body | Inter |`). Explicitly OUT of
21
+ * scope (chosen, not overlooked): unquoted prose in bullet lines —
22
+ * distinguishing "usage" from "mention" there is prose parsing, not a
23
+ * regex's job. Accepted trade-off of the quote signal: `--comment: "we
24
+ * never use Inter"` is flagged too — a quoted mention reads identically to
25
+ * a quoted usage once the property name carries no keyword to disambiguate.
26
+ */
27
+ const FONT_DECL_RE = /(?:font(?:-family)?|fontFamily|--[\w-]*(?:font|ff|type)[\w-]*)\s*:[^;\n]*(?:\n\s*)?[^;\n]*\b(?:Inter|Roboto|Arial|Open Sans)\b/i;
28
+ const FONT_IMPORT_RE = /family=[^&"')]*\b(?:Inter|Roboto|Arial|Open Sans)\b/i;
29
+ const FONT_TABLE_RE = /\|\s*\*{0,2}(?:Inter|Roboto|Arial|Open Sans)\*{0,2}\s*\|/;
30
+ const FONT_QUOTED_PROP_RE = /--[\w-]+\s*:[^;\n]*["'][^;\n]*(?:\n\s*)?[^;\n]*\b(?:Inter|Roboto|Arial|Open Sans)\b/i;
31
+
32
+ /**
33
+ * Return the requirements missing from a design-system.md (empty = valid).
34
+ * The corpus citation satisfies the source requirement ONLY when the corpus
35
+ * is actually delivered (`corpusCitationOk`) — absent corpus, the URL is
36
+ * mandatory again, exactly the pre-doctrine behavior (fallback never weaker).
37
+ */
38
+ export function validateDesignSystem(content: string, corpusCitationOk = false): string[] {
39
+ const missing: string[] = [];
40
+ if (!content.includes("## Design Reference")) missing.push("## Design Reference section");
41
+ if (!/https?:\/\//.test(content) && !(corpusCitationOk && hasCorpusCitation(content))) missing.push("reference URL (https://…) or Corpus citation");
42
+ if (!OKLCH_RE.test(content)) missing.push("oklch() color with chroma > 0");
43
+ if (FONT_DECL_RE.test(content) || FONT_IMPORT_RE.test(content) || FONT_TABLE_RE.test(content) || FONT_QUOTED_PROP_RE.test(content)) missing.push("forbidden font (Inter/Roboto/Arial/Open Sans)");
44
+ return missing;
45
+ }
@@ -0,0 +1,30 @@
1
+ import { existsSync, readFileSync, writeFileSync, rmSync, mkdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ const flagPath = (cacheDir: string): string => join(cacheDir, "design-agent-active");
5
+
6
+ /** The active design agent id (the flag), or "" when no design agent is running. */
7
+ export function activeDesignAgent(cacheDir: string): string {
8
+ const path = flagPath(cacheDir);
9
+ if (!existsSync(path)) return "";
10
+ try {
11
+ return readFileSync(path, "utf8").trim();
12
+ } catch {
13
+ return "";
14
+ }
15
+ }
16
+
17
+ /** Mark a design agent active (writes its id to the flag file). */
18
+ export function setActiveDesignAgent(cacheDir: string, agentId: string): void {
19
+ mkdirSync(cacheDir, { recursive: true });
20
+ writeFileSync(flagPath(cacheDir), agentId);
21
+ }
22
+
23
+ /** Clear the active-design-agent flag. */
24
+ export function clearActiveDesignAgent(cacheDir: string): void {
25
+ try {
26
+ rmSync(flagPath(cacheDir));
27
+ } catch {
28
+ /* already absent */
29
+ }
30
+ }
@@ -0,0 +1,105 @@
1
+ import type { Prompt } from "../../prompt/types";
2
+ import { type DesignState, quotaFor } from "./state";
3
+ import { PLUGINS_DIR } from "../file-size";
4
+ import { deny } from "./gates";
5
+ import { isTemplateUrl } from "./template-urls";
6
+
7
+ const SKILLS = `${PLUGINS_DIR}/design-expert/skills`;
8
+
9
+ /** Gate writing design-system.md: requires phase >= 2 and the per-mode screenshot quota. */
10
+ export function designSystemWriteGate(filePath: string, state: DesignState, corpusRequired = false): Prompt | null {
11
+ if (!filePath.endsWith("design-system.md")) return null;
12
+ if (state.currentPhase < 2) {
13
+ return deny(
14
+ `BLOCKED: Cannot write design-system.md at phase ${state.currentPhase}. ` +
15
+ "RECOVERY: 1) Read identity templates from skills/design-system/ " +
16
+ "2) Read design-inspiration.md 3) Read the refs-design corpus (README.md + relevant tokens-*.md) with the Read tool " +
17
+ "4) Screenshot real sector sites with mcp__fuse-browser__browser_screenshot on a LIVE session " +
18
+ "(browser_shots_batch/browser_site_shots also count, 1 credit per call) " +
19
+ "5) Then write design-system.md",
20
+ );
21
+ }
22
+ const needed = quotaFor(state.mode, corpusRequired);
23
+ if (state.screenshotsCount < needed) {
24
+ return deny(
25
+ `BLOCKED: ${state.screenshotsCount}/${needed} screenshots for mode '${state.mode}'. ` +
26
+ `RECOVERY: 1) Read the refs-design corpus (README.md + tokens-*.md) if not done ` +
27
+ `2) Take ${needed - state.screenshotsCount} more screenshots of REAL sector sites with ` +
28
+ "mcp__fuse-browser__browser_screenshot (browser_shots_batch/browser_site_shots also count, 1 credit per call) " +
29
+ "3) Use browser_open + browser_navigate + browser_screenshot fullPage:true 4) Then write design-system.md",
30
+ );
31
+ }
32
+ return null;
33
+ }
34
+
35
+ /**
36
+ * Gate writing .html/.css: PIPELINE gate (never `uiDesignSkillGate`/`UI_FILE_RE`
37
+ * — owner-scoped out of the skill gate's remit). Requires phase >= 3 AND
38
+ * designSystemValid (same two-condition defense as {@link geminiCreateGate})
39
+ * AND `designSystemFileExists` (caller-computed via `findDesignSystem` — a
40
+ * state can say phase 3 after the file was later deleted, or the file can
41
+ * exist while a fresh agent's state is still phase 0).
42
+ */
43
+ export function htmlCssPipelineGate(filePath: string, state: DesignState, designSystemFileExists: boolean): Prompt | null {
44
+ if (!/\.(html|css)$/.test(filePath)) return null;
45
+ if (state.currentPhase >= 3 && state.designSystemValid && designSystemFileExists) return null;
46
+ const gaps: string[] = [];
47
+ if (state.currentPhase < 3) gaps.push(`phase ${state.currentPhase}/3`);
48
+ if (!state.designSystemValid) gaps.push("design-system.md not validated");
49
+ if (!designSystemFileExists) gaps.push("design-system.md not found on disk");
50
+ return deny(
51
+ `BLOCKED: cannot write '${filePath}' before the design-system pipeline is complete (${gaps.join(", ")}). ` +
52
+ "RECOVERY: 1) Read identity templates from skills/design-system/ 2) Read design-inspiration.md " +
53
+ "3) Read the refs-design corpus with the Read tool 4) Screenshot real sector sites with " +
54
+ "mcp__fuse-browser__browser_screenshot on a LIVE session 5) Write a valid design-system.md " +
55
+ "6) Then write .html/.css",
56
+ );
57
+ }
58
+
59
+ /** Gate Gemini create_frontend: requires phase >= 3 and a validated design system. */
60
+ export function geminiCreateGate(state: DesignState): Prompt | null {
61
+ if (state.currentPhase < 3) {
62
+ return deny(
63
+ "BLOCKED: Cannot call Gemini create_frontend before phase 3. " +
64
+ "RECOVERY: 1) Complete the inspiration phase (refs-design corpus + sector screenshots) 2) Write a valid design-system.md " +
65
+ "3) Then call mcp__gemini-design__create_frontend",
66
+ );
67
+ }
68
+ if (!state.designSystemValid) {
69
+ return deny(
70
+ "BLOCKED: design-system.md not validated. " +
71
+ "RECOVERY: 1) Ensure design-system.md has ## Design Reference, OKLCH tokens, typography pair, reference URL or Corpus citation " +
72
+ "2) Then retry mcp__gemini-design__create_frontend",
73
+ );
74
+ }
75
+ return null;
76
+ }
77
+
78
+ /**
79
+ * Gate fuse-browser navigate: phase >= 1, inspiration read, no template source in phase 1.
80
+ * The allowlist→denylist inversion is the DELIBERATE doctrine change (not a
81
+ * weakening): the gate was never the proof — the mandatory screenshot is,
82
+ * unchanged and unforgeable in both configurations. Corpus-absent has its
83
+ * counterpart already (pre-doctrine quotas + visible warning); what the
84
+ * denylist cannot cover (sector-browse quality) belongs to the doc, per the
85
+ * rule "the gate requires what is verifiable, the doc prescribes zeal".
86
+ */
87
+ export function browserNavigateGate(state: DesignState, url: string): Prompt | null {
88
+ if (state.currentPhase < 1) {
89
+ return deny(`BLOCKED: Phase 0 not done. READ: ${SKILLS}/design-system/SKILL.md first.`);
90
+ }
91
+ if (!state.inspirationRead) {
92
+ return deny(
93
+ `BLOCKED: Read the inspiration doctrine first. READ: ${SKILLS}/design-web/references/design-inspiration.md, then the refs-design corpus (README.md + relevant tokens-*.md) with the Read tool`,
94
+ );
95
+ }
96
+ if (state.currentPhase === 1 && url && isTemplateUrl(url)) {
97
+ return deny(
98
+ `BLOCKED: '${url}' is a template source — templates are BUILT to be interchangeable, ` +
99
+ "so extracting taste from them converges every page to the same body. " +
100
+ "RECOVERY: 1) taste comes from the refs-design corpus (README.md + relevant tokens-*.md) " +
101
+ "2) browse 1-2 REAL sites in the client's sector, for register only (award galleries are fine as outbound-link finders)",
102
+ );
103
+ }
104
+ return null;
105
+ }
@@ -0,0 +1,87 @@
1
+ import { basename } from "node:path";
2
+ import type { Prompt } from "../../prompt/types";
3
+ import { loadDesignState, type DesignState } from "./state";
4
+ import { activeDesignAgent } from "./flag";
5
+ import { isUiWrite } from "./skill-gate";
6
+ import { SKILL_TRIGGERS } from "./skill-triggers";
7
+ import { ALLOWED_WRITE, isAllowedWrite } from "./allowed-write";
8
+
9
+ export { validateDesignSystem } from "./design-system-rules";
10
+ export { ALLOWED_WRITE, isAllowedWrite } from "./allowed-write";
11
+
12
+ export const EXEMPT_DIRS: readonly string[] = ["node_modules/", "dist/", "build/", ".claude/"];
13
+ const NAV = "mcp__fuse-browser__browser_navigate";
14
+ const SHOT = "mcp__fuse-browser__browser_screenshot";
15
+ const GEMINI = "mcp__gemini-design__create_frontend";
16
+
17
+ export const deny = (reason: string): Prompt => ({
18
+ kind: "block", title: "Design pipeline", reason,
19
+ actions: ["Follow the design pipeline phases (0→identity, 1→inspiration, 2→screenshots, 3→design-system, 4→generate) in order"],
20
+ });
21
+
22
+ /** Block the design agent from writing anything but .html/.css/.md/.json (+ motion*.js). */
23
+ export function htmlCssOnlyGate(filePath: string): Prompt | null {
24
+ if (EXEMPT_DIRS.some((d) => filePath.includes(d)) || isAllowedWrite(filePath)) return null;
25
+ return deny(
26
+ "BLOCKED: design-expert can only write .html, .css, .json, .md, and motion*.js files. " +
27
+ "Framework files (.tsx, .astro, .vue, .swift, .php) must be written by the domain expert " +
28
+ "(astro-expert, react-expert, etc.) AFTER design validation.",
29
+ );
30
+ }
31
+
32
+ /** Block edits to the harness-managed `.design-state-*` files (read-only to the agent). */
33
+ export function stateFileGate(filePath: string): Prompt | null {
34
+ return filePath.includes(".design-state-")
35
+ ? deny(
36
+ "BLOCKED: .design-state files are READ-ONLY for you. Hooks update them automatically as you progress. " +
37
+ "Do NOT try to modify them — it will not unblock you. Follow the pipeline: Phase 0→1→2→3→4→5→6 in order.",
38
+ )
39
+ : null;
40
+ }
41
+
42
+ /** Gate a screenshot: require a scroll since the last navigate (lazy-load content). */
43
+ export function screenshotScrollGate(state: DesignState): Prompt | null {
44
+ return state.scrolledSinceNav
45
+ ? null
46
+ : deny("BLOCKED: scroll the page before a screenshot — browser_scroll to:'end', wait, scroll back, then fullPage screenshot.");
47
+ }
48
+
49
+ /** The Gemini design gates are OPT-IN: off unless `FUSE_DESIGN_GEMINI` is `1`/`true`. */
50
+ export function geminiEnabled(): boolean {
51
+ const v = process.env.FUSE_DESIGN_GEMINI;
52
+ return v === "1" || v === "true";
53
+ }
54
+
55
+ /** Inputs for {@link designPassNotice} — primitives only, so policy stays runtime-free. */
56
+ export interface PassNoticeInput { agentId: string; tool: string; filePath: string; content: string; url: string; phase: "pre" | "post" }
57
+
58
+ /**
59
+ * Python-parity pass notices (`hook_output.allow_pass`/`post_pass`): the
60
+ * user-visible `script: detail` lines the old plugins printed on EVERY passing
61
+ * design hook. Call AFTER the gates allowed the event; returns an `inform`
62
+ * Prompt carrying only `userMessage` (for the human, never agent context), or
63
+ * null when nothing applies. The check-design-skill line fires for ANY agent
64
+ * on a UI write; the rest only inside the active design-agent context.
65
+ */
66
+ export function designPassNotice(i: PassNoticeInput & { corpusMissing?: boolean }, cacheDir: string): Prompt | null {
67
+ const isWrite = i.tool === "Write" || i.tool === "Edit";
68
+ const fp = i.filePath;
69
+ const lines: string[] = [];
70
+ if (i.phase === "pre" && isUiWrite(i.tool, fp, i.content)) {
71
+ const domains = SKILL_TRIGGERS.filter((t) => t.patterns.some((re) => re.test(i.content))).map((t) => t.skill).join(", ");
72
+ lines.push(`check-design-skill: pass (domain: ${domains || "base"})`);
73
+ }
74
+ const state = i.agentId && activeDesignAgent(cacheDir) === i.agentId ? loadDesignState(cacheDir, i.agentId) : null;
75
+ if (state && i.phase === "pre") {
76
+ if (i.corpusMissing) lines.push("design-corpus: refs-design corpus not found — corpus requirement waived (screenshot-only fallback, install defect)");
77
+ if (isWrite && fp && !EXEMPT_DIRS.some((d) => fp.includes(d)) && ALLOWED_WRITE.test(fp)) lines.push(`enforce-html-css-only: allowed: ${basename(fp)}`);
78
+ if (i.tool === NAV) lines.push(`check-inspiration-read: pass (${i.url})`);
79
+ if (i.tool === GEMINI && geminiEnabled()) lines.push("validate-design-system: design-system.md ok");
80
+ if (isWrite || i.tool === NAV || i.tool === SHOT || i.tool === GEMINI) lines.push(`pipeline-gate: phase ${state.currentPhase} ok`);
81
+ }
82
+ if (state && i.phase === "post" && isWrite) {
83
+ if (basename(fp) === "design-system.md") lines.push("validate-design: design-system.md → phase 3");
84
+ else if (/\.(tsx|jsx|css)$/.test(fp)) lines.push("validate-design: design ok");
85
+ }
86
+ return lines.length ? { kind: "inform", title: "Design pipeline", reason: "", userMessage: lines.join("\n") } : null;
87
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @module screenshot-tools
3
+ * Screenshot-credit tool set (C1), split out of `runtime/design-helpers.ts` to
4
+ * keep that file within the SOLID size budget (SRP).
5
+ * @packageDocumentation
6
+ */
7
+
8
+ /**
9
+ * Screenshot-credit tools: the single-shot capture PLUS the two fuse-browser
10
+ * batch capture primitives (`browser_shots_batch`, `browser_site_shots`) — a
11
+ * batch call that captures N urls/viewports must credit the quota too, or
12
+ * capturing efficiently would make the quota infranchissable. LIMITATION
13
+ * (deliberate, documented): this repo's cross-harness `NormalizedEvent` (see
14
+ * `runtime/normalize.ts`) never surfaces `tool_response` for any tool — by
15
+ * design, since its shape is undocumented for MCP tools and harness-specific
16
+ * for built-ins — so no per-call item count reaches this gate. A batch call
17
+ * therefore credits EXACTLY 1, same as a unit screenshot, never the N it may
18
+ * have captured.
19
+ */
20
+ export const SHOT_TOOLS: ReadonlySet<string> = new Set([
21
+ "mcp__fuse-browser__browser_screenshot",
22
+ "mcp__fuse-browser__browser_shots_batch",
23
+ "mcp__fuse-browser__browser_site_shots",
24
+ ]);
@@ -0,0 +1,34 @@
1
+ /**
2
+ * @module design/skill-evidence
3
+ * IO side of the UI design-skill gate: read the verified session track and
4
+ * derive {@link DesignEvidence} (read references + doc-consulted) for
5
+ * {@link uiDesignSkillGate}. Kept separate from the pure gate logic (SRP).
6
+ * @packageDocumentation
7
+ */
8
+ import type { SessionTrack } from "../../tracking/session-state";
9
+ import { trackJournalEnabled } from "../../tracking/store";
10
+ import { readTrackSync } from "../../tracking/track-compact";
11
+ import { defaultStateDir, trackFile } from "../../runtime/paths";
12
+ import { isDocConsulted } from "../../freshness/doc-helpers";
13
+ import type { DesignEvidence } from "./skill-gate";
14
+
15
+ /**
16
+ * Read the verified session track (snapshot ⊕ journal) and derive
17
+ * {@link DesignEvidence}. Fail-closed on a missing/corrupt/tampered track (no
18
+ * evidence → the gate blocks).
19
+ * @param sessionId - the Claude session id.
20
+ * @param cwd - the project root (selects the per-project state dir).
21
+ * @param baseDir - override the track base dir (tests); defaults to the project state dir.
22
+ */
23
+ export function collectDesignEvidence(sessionId: string, cwd: string, baseDir: string = defaultStateDir(cwd)): DesignEvidence {
24
+ let track: SessionTrack | null = null;
25
+ try {
26
+ track = readTrackSync(trackFile(sessionId, baseDir), trackJournalEnabled());
27
+ } catch {
28
+ track = null;
29
+ }
30
+ return {
31
+ refsRead: track?.refsRead ?? [],
32
+ docConsulted: track ? isDocConsulted(track.authorizations, sessionId) : false,
33
+ };
34
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * @module design/skill-gate
3
+ * UI design-skill gate — ports `check-design-skill.py` WITHOUT requiring Gemini.
4
+ *
5
+ * Fires for ANY agent writing a UI file (.tsx/.jsx/.scss/.vue/.svelte) in a
6
+ * UI-ish path (or with Tailwind classes). Blocks unless BOTH: (1) a design skill
7
+ * reference was read this session (reuses the `refsRead` track), and (2) both
8
+ * Context7 AND Exa were consulted via {@link isDocConsulted}.
9
+ * Gemini is NEVER a condition — doc research alone satisfies the gate.
10
+ * @packageDocumentation
11
+ */
12
+ import type { Prompt } from "../../prompt/types";
13
+ import { missingDomainSkills } from "./skill-triggers";
14
+
15
+ /** UI source files the gate polices (.html/.css are handled by the pipeline gate, not here). */
16
+ const UI_FILE_RE = /\.(tsx|jsx|scss|vue|svelte)$/;
17
+ /** Generated/vendored paths that never need a design skill (match at start or mid-path). */
18
+ const EXEMPT_RE = /(^|\/)(node_modules|dist|build)\/|\.claude\//;
19
+ /** UI-intent path fragments (ports the Python `UI_PATH_PATTERNS`). */
20
+ const UI_PATH_RE = /(components|ui|styles|page|layout|content|view|feature|section|hero|footer|header|sidebar|nav|modal|dialog)/;
21
+ /** Tailwind utility usage in JSX `className` (ports the Python `has_jsx_tailwind`). */
22
+ const TAILWIND_RE = /className\s*=.*(?:flex|grid|p-|m-|bg-|text-|rounded|shadow|border|gap-|w-|h-)/;
23
+ /** A read reference path that counts as "a design skill was consulted". */
24
+ const DESIGN_SKILL_RE = /skills\/(design-method|design-system|design-web|design-webapp|design-motion|design-review|ux-copy|design-ios|design-android)/i;
25
+
26
+ /** Session evidence the gate consumes: read references + whether docs were consulted. */
27
+ export interface DesignEvidence {
28
+ refsRead: readonly string[];
29
+ docConsulted: boolean;
30
+ }
31
+
32
+ /** True when this Write/Edit targets a UI file in a UI-ish path or with Tailwind classes. */
33
+ export function isUiWrite(tool: string, filePath: string, content: string): boolean {
34
+ if (tool !== "Write" && tool !== "Edit") return false;
35
+ if (!UI_FILE_RE.test(filePath) || EXEMPT_RE.test(filePath)) return false;
36
+ return UI_PATH_RE.test(filePath) || TAILWIND_RE.test(content);
37
+ }
38
+
39
+ /** True when a design skill reference was read this session (scans the `refsRead` track). */
40
+ export function designSkillRead(refsRead: readonly string[]): boolean {
41
+ return refsRead.some((p) => DESIGN_SKILL_RE.test(p));
42
+ }
43
+
44
+ const block = (reason: string, action: string): Prompt => ({
45
+ kind: "block", title: "Design skill", reason, actions: [action],
46
+ });
47
+
48
+ /**
49
+ * Gate a UI write: require a design-skill read AND any doc source. Returns a
50
+ * blocking {@link Prompt}, or `null` to allow. NEVER requires Gemini.
51
+ * @param tool - the tool name ("Write" | "Edit" | ...).
52
+ * @param filePath - the written file path.
53
+ * @param content - the written content (snippet on Edit, full file on Write).
54
+ * @param ev - the session evidence (refsRead + docConsulted).
55
+ */
56
+ export function uiDesignSkillGate(tool: string, filePath: string, content: string, ev: DesignEvidence): Prompt | null {
57
+ if (!isUiWrite(tool, filePath, content)) return null;
58
+ if (!designSkillRead(ev.refsRead)) {
59
+ return block(
60
+ "BLOCKED: design skill not consulted before writing UI. Read a design SKILL.md "
61
+ + "(design-system, design-web, or design-motion), then retry.",
62
+ "Read a design skill reference, then retry",
63
+ );
64
+ }
65
+ const missing = missingDomainSkills(content, ev.refsRead);
66
+ if (missing.length) {
67
+ return block(
68
+ `BLOCKED: code uses ${missing.join(", ")} but the matching design skill(s) were not consulted. `
69
+ + `Read the SKILL.md FILE (with the Read tool) for: ${missing.join(", ")}, then retry.`,
70
+ `Read design skill(s): ${missing.join(", ")}, then retry`,
71
+ );
72
+ }
73
+ if (!ev.docConsulted) {
74
+ return block(
75
+ "BLOCKED: no documentation consulted. Use BOTH mcp__context7__query-docs AND "
76
+ + "mcp__exa__web_search_exa, or a web fallback alone (WebSearch/WebFetch). Gemini is NOT required.",
77
+ "Consult Context7+Exa, or web docs alone (Gemini not required), then retry",
78
+ );
79
+ }
80
+ return null;
81
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @module design/skill-triggers
3
+ * Domain-specific design-skill detection: maps written code patterns to the
4
+ * precise design SKILL.md that must have been consulted. Ports the Python
5
+ * `design_skill_triggers.py` `SKILL_TRIGGERS` table + `detect_required_skills`.
6
+ * @packageDocumentation
7
+ */
8
+
9
+ /** A domain skill + the code patterns that require it + its `refsRead` path fragment. */
10
+ export interface SkillTrigger {
11
+ /** Skill id for the block message, e.g. "design-motion". */
12
+ skill: string;
13
+ /** Path fragment proving the skill was read (matched against `refsRead`). */
14
+ readFragment: string;
15
+ /** Code patterns that trigger the requirement (case-insensitive). */
16
+ patterns: readonly RegExp[];
17
+ }
18
+
19
+ /** Domain skill triggers (ported 1:1 from `design_skill_triggers.py`). */
20
+ export const SKILL_TRIGGERS: readonly SkillTrigger[] = [
21
+ { skill: "design-web", readFragment: "design-web", patterns: [
22
+ /className\s*=\s*["{]/i, /<(div|section|main|header|footer|nav|aside)\s/i,
23
+ /flex|grid|gap-|p-|m-|bg-|text-|rounded|shadow|border/i,
24
+ /(Button|Card|Dialog|Sheet|Input|Select|Table)\b/i,
25
+ /cva|class-variance-authority|variants\b/i, /VariantProps|variant.*:\s*\{/i,
26
+ /children|slots|asChild|Slot|render.?prop/i, /forwardRef|React\.cloneElement|compound/i,
27
+ ] },
28
+ { skill: "design-system", readFragment: "design-system", patterns: [
29
+ /--(\w+-)+color:|:root\s*\{|@theme\b/i, /design.?system|token|palette|typography.?scale/i,
30
+ /--(\w+)-(foreground|background|primary|muted|accent):/i, /cssVariables|themeConfig|colorScheme/i,
31
+ /dark:|prefers-color-scheme|next-themes|useTheme/i, /ThemeProvider|data-theme|color-scheme/i,
32
+ /sm:|md:|lg:|xl:|2xl:/i, /@container|container-type|@media/i, /clamp\(|fluid|min-width:/i,
33
+ ] },
34
+ { skill: "design-motion", readFragment: "design-motion", patterns: [
35
+ /motion\.|framer-motion|animate|variants/i, /whileHover|whileTap|AnimatePresence|transition/i,
36
+ /@keyframes\b|animation:\s/i, /backdrop-blur|bg-.*\/([\d]+)|glass/i, /backdrop-filter:\s*blur/i,
37
+ /hover:|focus:|active:|disabled:|focus-visible:/i, /data-\[state=|data-\[disabled\]/i,
38
+ /bg-gradient|from-|via-|to-/i, /radial-gradient|conic-gradient|bg-\[url/i, /blur-.*xl|opacity-|mix-blend/i,
39
+ ] },
40
+ { skill: "design-review", readFragment: "design-review", patterns: [
41
+ /aria-|role=|sr-only|tabIndex|alt=/i, /WCAG|a11y|contrast|screen.?reader/i,
42
+ ] },
43
+ ];
44
+
45
+ /** Return the domain skills required by `content` but NOT present in `refsRead`. */
46
+ export function missingDomainSkills(content: string, refsRead: readonly string[]): string[] {
47
+ return SKILL_TRIGGERS
48
+ .filter((t) => t.patterns.some((re) => re.test(content)))
49
+ .filter((t) => !refsRead.some((p) => p.includes(t.readFragment)))
50
+ .map((t) => t.skill);
51
+ }
@@ -0,0 +1,92 @@
1
+ import { existsSync, readFileSync, renameSync, readdirSync, statSync, rmSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { atomicWrite } from "../../util/json-io";
4
+
5
+ /** Pipeline mode — drives the screenshot quota and phase expectations. */
6
+ export type DesignMode = "full" | "page" | "component";
7
+
8
+ /** The design-agent pipeline state-machine snapshot (phases 0→4). */
9
+ export interface DesignState {
10
+ agentId: string;
11
+ mode: DesignMode;
12
+ currentPhase: number;
13
+ phasesCompleted: string[];
14
+ inspirationRead: boolean;
15
+ scrolledSinceNav: boolean;
16
+ screenshotsCount: number;
17
+ /** Distinct refs-design files read (paths relative to the corpus root), deduped. */
18
+ corpusReads: string[];
19
+ designSystemExists: boolean;
20
+ designSystemValid: boolean;
21
+ geminiCalls: number;
22
+ }
23
+
24
+ /** Minimum fuse-browser screenshots required before writing design-system.md, per mode (corpus present). */
25
+ export const MIN_SCREENSHOTS: Readonly<Record<DesignMode, number>> = { full: 2, page: 1, component: 1 };
26
+
27
+ /**
28
+ * Screenshot quotas when the refs-design corpus is ABSENT (install defect →
29
+ * fail-open): exactly today's effective behavior, so the fallback can never be
30
+ * weaker than the pre-doctrine gate. Component is 1 explicitly — the effective
31
+ * quota today (the first screenshot trips `1 >= 0`), no longer a hidden quirk.
32
+ */
33
+ export const MIN_SCREENSHOTS_NO_CORPUS: Readonly<Record<DesignMode, number>> = { full: 4, page: 2, component: 1 };
34
+
35
+ /** Screenshot quota for `mode` — the SINGLE choice point between the two tables, driven by corpus availability. */
36
+ export function quotaFor(mode: DesignMode, corpusRequired: boolean): number {
37
+ return (corpusRequired ? MIN_SCREENSHOTS : MIN_SCREENSHOTS_NO_CORPUS)[mode];
38
+ }
39
+
40
+ const stateFile = (cacheDir: string, agentId: string): string => join(cacheDir, `.design-state-${agentId}.json`);
41
+
42
+ /** Load the design state for `agentId`, or null when absent/corrupt (fail-open). */
43
+ export function loadDesignState(cacheDir: string, agentId: string): DesignState | null {
44
+ const path = stateFile(cacheDir, agentId);
45
+ if (!existsSync(path)) return null;
46
+ try {
47
+ const parsed = JSON.parse(readFileSync(path, "utf8")) as DesignState;
48
+ // Retrocompat: states persisted before corpusReads existed load with an
49
+ // empty list (never undefined — an undefined count would silently
50
+ // fail-open or fail-closed the corpus conjunction downstream).
51
+ return { ...parsed, corpusReads: parsed.corpusReads ?? [] };
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+
57
+ /** Persist the design state under its agent id. */
58
+ export function saveDesignState(cacheDir: string, state: DesignState): void {
59
+ atomicWrite(stateFile(cacheDir, state.agentId), JSON.stringify(state, null, 2));
60
+ }
61
+
62
+ /** Build the initial state for a design agent starting a run. */
63
+ export function initDesignState(agentId: string, mode: DesignMode, designSystemExists: boolean): DesignState {
64
+ return {
65
+ agentId, mode, currentPhase: 0, phasesCompleted: [], inspirationRead: false,
66
+ scrolledSinceNav: false, screenshotsCount: 0, corpusReads: [], designSystemExists, designSystemValid: false, geminiCalls: 0,
67
+ };
68
+ }
69
+
70
+ /** Archive the active state file (timestamp suffix) and drop archives older than 7 days. */
71
+ export function cleanupDesignStates(cacheDir: string, agentId: string, stamp: string, now: number): void {
72
+ if (agentId) {
73
+ const src = stateFile(cacheDir, agentId);
74
+ if (existsSync(src)) renameSync(src, join(cacheDir, `.design-state-${agentId}-${stamp}.json`));
75
+ }
76
+ let entries: string[];
77
+ try {
78
+ entries = readdirSync(cacheDir);
79
+ } catch {
80
+ return;
81
+ }
82
+ const cutoff = now - 7 * 86_400_000;
83
+ for (const name of entries) {
84
+ if (!name.startsWith(".design-state-")) continue;
85
+ const path = join(cacheDir, name);
86
+ try {
87
+ if (statSync(path).mtimeMs < cutoff) rmSync(path);
88
+ } catch {
89
+ /* ignore unlink races */
90
+ }
91
+ }
92
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Template-platform detection for the phase-1 navigate gate
3
+ * (design-inspiration.md §FORBIDDEN Navigation Targets). Two rules, because
4
+ * neither alone matches the doctrine:
5
+ * - template SHOPS are banned outright, on the host (subdomains included);
6
+ * - the path rule (/templates, /themes) applies ONLY to real product
7
+ * platforms that also host a template catalog — never to the whole web,
8
+ * or sothebys.com/en/marketplace (register research) would be denied.
9
+ * The host is normalized (lowercase, port, userinfo, trailing dot stripped).
10
+ * HONEST SCOPE: this is a LEXICAL filter — it follows no redirect, is not and
11
+ * will never be exhaustive, and it is NOT the taste gate: the source of taste
12
+ * is the refs-design corpus. This list exists to catch the reflex, not to map
13
+ * the template economy. Frozen by owner decision (C5): no host or pattern may
14
+ * be added or removed without re-opening that decision.
15
+ * Award galleries are deliberately absent: they pass, as outbound-link finders.
16
+ */
17
+
18
+ /** Template shops: banned outright, any path (subdomains included). */
19
+ const TEMPLATE_HOSTS: readonly string[] = [
20
+ "themeforest.net", "themes.shopify.com", "marketplace.atlassian.com",
21
+ "templatemonster.com", "creativemarket.com", "ui8.net", "html5up.net",
22
+ "colorlib.com", "bootstrapmade.com", "startbootstrap.com", "cruip.com",
23
+ "uideck.com", "envato.com", "tailwindui.com",
24
+ ];
25
+
26
+ /** Real-product platforms whose template CATALOGS are banned via the path rule. */
27
+ const PLATFORM_HOSTS: readonly string[] = [
28
+ "framer.com", "webflow.com", "vercel.com", "astro.build", "squarespace.com", "wix.com",
29
+ ];
30
+
31
+ /**
32
+ * Template-catalog path segments — evaluated ONLY on PLATFORM_HOSTS, and
33
+ * deliberately WITHOUT `/marketplace`: those platforms run real, non-template
34
+ * marketplaces (vercel.com/marketplace is infra). Atlassian/Shopify template
35
+ * stores are already covered by the full host ban above.
36
+ */
37
+ const TEMPLATE_PATH_RE = /^\/(templates|themes)(\/|$)/i;
38
+
39
+ /** Host part of `url`, normalized: lowercase, no port, no userinfo, no trailing dot. */
40
+ function hostOf(url: string): string {
41
+ return (/^https?:\/\/(?:[^@/?#]*@)?([^/:?#]+)/i.exec(url)?.[1] ?? "").replace(/\.$/, "").toLowerCase();
42
+ }
43
+
44
+ const under = (host: string, domain: string): boolean => host === domain || host.endsWith(`.${domain}`);
45
+
46
+ /** True when `url` points at a banned template source (host rule, then platform path rule). */
47
+ export function isTemplateUrl(url: string): boolean {
48
+ const host = hostOf(url);
49
+ if (!host) return false;
50
+ if (TEMPLATE_HOSTS.some((d) => under(host, d))) return true;
51
+ if (!PLATFORM_HOSTS.some((d) => under(host, d))) return false;
52
+ const path = /^https?:\/\/(?:[^@/?#]*@)?[^/?#]*(\/[^?#]*)/i.exec(url)?.[1] ?? "/";
53
+ return TEMPLATE_PATH_RE.test(path);
54
+ }