@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,56 @@
1
+ /**
2
+ * State-store declaration detection over MASKED content — Zustand (React
3
+ * family) and Pinia (Vue family), per the owner's framework scoping: a rule
4
+ * only fires inside its own family (the gate checks the project deps, this
5
+ * module only reads content). Canonical home: `src/stores/`, `*.store.ts`,
6
+ * one store per domain, budget = ratio 0.4 of the global line limit.
7
+ *
8
+ * Signature-strict (audit lesson): `Object.create(` and home-made
9
+ * `create(` factories never match — Zustand requires the `zustand` import
10
+ * (or the v5 curried `create<T>()(...)` call), Pinia requires the `pinia`
11
+ * import alongside `defineStore(`.
12
+ */
13
+ import { maskCommentsAndStrings, maskCommentsOnly } from "./strip";
14
+
15
+ /** Store library whose declarations are being scanned. */
16
+ export type StoreLib = "zustand" | "pinia";
17
+
18
+ /** Import line of the store package — comments masked (a `// import …` line
19
+ * never matches), strings KEPT (masking would blank the module literal). */
20
+ const STORE_IMPORT_RE: Record<StoreLib, RegExp> = {
21
+ zustand: /^\s*import\s+[^;]*from\s+['"]zustand/m,
22
+ pinia: /^\s*import\s+[^;]*from\s+['"]pinia/m,
23
+ };
24
+
25
+ /** Zustand v5 curried declaration: `export const useX = create<S>()(`. */
26
+ const ZUSTAND_CURRIED_RE = /^\s*export\s+const\s+\w+\s*=\s*create\s*<[^=]*>\s*\(\s*\)\s*\(/m;
27
+ /** Zustand declaration with import context: `export const useX = create(`. */
28
+ const ZUSTAND_DECL_RE = /^\s*export\s+const\s+\w+\s*=\s*create(?:Store)?\s*(?:<[^(]+>)?\s*\(/m;
29
+ /** Pinia declaration: `export const useX = defineStore(`. */
30
+ const PINIA_DECL_RE = /^\s*export\s+const\s+\w+\s*=\s*defineStore\s*\(/m;
31
+
32
+ /**
33
+ * True when the file declares a store of the given library (masked scan,
34
+ * signature-strict).
35
+ * @param content - Raw file content.
36
+ * @param lib - Store library to detect.
37
+ */
38
+ export function declaresStore(content: string, lib: StoreLib): boolean {
39
+ const masked = maskCommentsAndStrings(content, "c");
40
+ const importable = maskCommentsOnly(content, "c");
41
+ if (lib === "pinia") return STORE_IMPORT_RE.pinia.test(importable) && PINIA_DECL_RE.test(masked);
42
+ return ZUSTAND_CURRIED_RE.test(masked) || (STORE_IMPORT_RE.zustand.test(importable) && ZUSTAND_DECL_RE.test(masked));
43
+ }
44
+
45
+ /**
46
+ * Count store declarations in one file (multi-store advisory: one per domain).
47
+ * @param content - Raw file content.
48
+ * @param lib - Store library to count.
49
+ */
50
+ export function countStores(content: string, lib: StoreLib): number {
51
+ const masked = maskCommentsAndStrings(content, "c");
52
+ const re = lib === "pinia" ? PINIA_DECL_RE : ZUSTAND_DECL_RE;
53
+ const curried = masked.match(new RegExp(ZUSTAND_CURRIED_RE.source, "gm"))?.length ?? 0;
54
+ const plain = masked.match(new RegExp(re.source, "gm"))?.length ?? 0;
55
+ return lib === "pinia" ? plain : Math.max(curried, plain);
56
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Lexical comment/string masking for convention detectors. Every convention
3
+ * rule (interfaces, types, hooks, stores, query) matches against MASKED
4
+ * content so a declaration-looking line inside a comment, string, template
5
+ * literal, or heredoc can never trigger a verdict. Masking replaces the
6
+ * covered CHARACTERS with spaces but keeps every newline: line offsets and
7
+ * `^…/m` anchors stay valid, and the output has the same line count.
8
+ * Not a parser — a per-family state machine (c-like, python, ruby, php).
9
+ */
10
+
11
+ /** Lexical profile of a language family. */
12
+ export type LexProfile = "c" | "py" | "rb" | "php";
13
+
14
+ /** Mask PHP heredoc/nowdoc blocks (`<<<TAG` … terminator line), offsets kept. */
15
+ function maskHeredocs(content: string): string {
16
+ return content.replace(/<<<['"]?(\w+)['"]?\r?\n[\s\S]*?^\1;?$/gm, (block) => block.replace(/[^\n]/g, " "));
17
+ }
18
+
19
+ /**
20
+ * Mask comments and strings of `content` according to its lexical profile.
21
+ * @param content - Source text.
22
+ * @param profile - Lexical family (see `langs.ts` `lexProfileOf`).
23
+ * @returns Same-length text with comment/string interiors blanked.
24
+ */
25
+ export function maskCommentsAndStrings(content: string, profile: LexProfile): string {
26
+ return maskScan(content, profile, false);
27
+ }
28
+
29
+ /**
30
+ * Mask COMMENTS only (strings kept) — for detectors whose signal IS a string
31
+ * literal (module imports like `from "zustand"`): a `// import …` comment
32
+ * cannot match, while the real import line stays readable.
33
+ * @param content - Source text.
34
+ * @param profile - Lexical family.
35
+ * @returns Same-length text with comment interiors blanked.
36
+ */
37
+ export function maskCommentsOnly(content: string, profile: LexProfile): string {
38
+ return maskScan(content, profile, true);
39
+ }
40
+
41
+ /** Shared scanner; `keepStrings` skips the string-masking branch. */
42
+ function maskScan(content: string, profile: LexProfile, keepStrings: boolean): string {
43
+ if (profile === "php") content = maskHeredocs(content);
44
+ const out = content.split("");
45
+ const blank = (i: number) => { if (out[i] !== "\n") out[i] = " "; };
46
+ const isPy = profile === "py" || profile === "rb";
47
+ let i = 0;
48
+ while (i < out.length) {
49
+ const two = content.slice(i, i + 2);
50
+ if (!isPy && two === "//") { while (i < out.length && out[i] !== "\n") blank(i++); continue; }
51
+ if (isPy && out[i] === "#") { while (i < out.length && out[i] !== "\n") blank(i++); continue; }
52
+ if (profile === "php" && out[i] === "#") { while (i < out.length && out[i] !== "\n") blank(i++); continue; }
53
+ if (!isPy && two === "/*") { const end = content.indexOf("*/", i + 2); const stop = end === -1 ? out.length : end + 2; while (i < stop) blank(i++); continue; }
54
+ if (isPy && (content.startsWith("'''", i) || content.startsWith('"""', i))) {
55
+ const q = content.slice(i, i + 3);
56
+ const end = content.indexOf(q, i + 3);
57
+ const stop = end === -1 ? out.length : end + 3;
58
+ while (i < stop) blank(i++);
59
+ continue;
60
+ }
61
+ if (!keepStrings && (out[i] === "'" || out[i] === '"' || (profile === "c" && out[i] === "`"))) {
62
+ const q = out[i] as string;
63
+ // Unterminated-quote guard: a single/double quote never opens a string
64
+ // past its own line (JS/TS has no multiline plain strings — a JSX
65
+ // apostrophe like `Don't` must not swallow the rest of the file), and
66
+ // an unterminated template literal masks nothing (unpaired backtick).
67
+ let j = i + 1, closed = -1;
68
+ while (j < out.length && out[j] !== "\n") {
69
+ if (out[j] === "\\") { j += 2; continue; }
70
+ if (out[j] === q) { closed = j; break; }
71
+ j++;
72
+ }
73
+ if (q === "`") {
74
+ if (closed !== -1) { while (i <= closed) blank(i++); continue; }
75
+ const end = content.indexOf("`", i + 1);
76
+ if (end !== -1) { while (i <= end) blank(i++); continue; }
77
+ i++;
78
+ continue;
79
+ }
80
+ if (closed === -1) { i++; continue; }
81
+ while (i <= closed) blank(i++);
82
+ continue;
83
+ }
84
+ i++;
85
+ }
86
+ return out.join("");
87
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Rollout control for NEW convention denies (owner decision, inverted
3
+ * 2026-07): new convention rules BLOCK by default — `FUSE_CONVENTIONS_MODE`
4
+ * is an opt-OUT. Set it to `advisory` to observe without blocking (`inform`
5
+ * output); any absent or invalid value means `deny`. Denies that already
6
+ * existed before the conventions module are unaffected (byte-parity); this
7
+ * module only ever MODULATES the new ones.
8
+ */
9
+ import type { Prompt } from "../../prompt/types";
10
+
11
+ /** Rollout mode for new convention denies. */
12
+ export type ConventionsMode = "advisory" | "deny";
13
+
14
+ /**
15
+ * Current rollout mode: `advisory` ONLY on the exact explicit value —
16
+ * anything else (absent, typo, empty) is `deny` (owner decision: deny-first).
17
+ * @param env - Environment (defaults to `process.env`).
18
+ */
19
+ export function conventionsMode(env: Record<string, string | undefined> = process.env): ConventionsMode {
20
+ return env.FUSE_CONVENTIONS_MODE === "advisory" ? "advisory" : "deny";
21
+ }
22
+
23
+ /**
24
+ * Resolve a convention verdict under the rollout flag: a NEW rule yields an
25
+ * `inform` prompt in advisory mode (explicit opt-out), the blocking prompt
26
+ * otherwise; an EXISTING rule always yields the blocking prompt unchanged.
27
+ * @param prompt - The blocking prompt the rule would emit.
28
+ * @param isNew - True when the rule is new (post-conventions module).
29
+ * @param env - Environment (defaults to `process.env`).
30
+ */
31
+ export function rolloutVerdict(
32
+ prompt: Prompt,
33
+ isNew: boolean,
34
+ env: Record<string, string | undefined> = process.env,
35
+ ): Prompt {
36
+ if (!isNew || conventionsMode(env) === "deny") return prompt;
37
+ return { kind: "inform", title: prompt.title, reason: prompt.reason, actions: prompt.actions };
38
+ }
@@ -0,0 +1,11 @@
1
+ const CREATE_RE = /\b(?:create|implement|add|build|new|feature|component|generate|make|develop|scaffold)\b/i;
2
+ const SKIP_RE = /\b(?:fix|bug|debug|update|refactor|rename|move|delete|remove|commit|push|status|edit|modify|change|adjust|tweak|existing|current)\b/i;
3
+
4
+ /**
5
+ * True when a prompt expresses creation intent (a new feature/component) and is
6
+ * not a fix/refactor — the signal that brainstorming should precede creation.
7
+ * The harness calls this on UserPromptSubmit, then `recordBrainstormRequired`.
8
+ */
9
+ export function detectCreationIntent(prompt: string): boolean {
10
+ return CREATE_RE.test(prompt) && !SKIP_RE.test(prompt);
11
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * @module deny-loop
3
+ * Pure anti-loop logic: hash a tool-call, decide if it repeats a prior deny, and
4
+ * enrich the repeated block's message.
5
+ *
6
+ * The proprietary rule "NEVER propose the same fix twice" is prose a model under
7
+ * pressure ignores. This makes it machine-enforced: when a call whose
8
+ * `(tool + normalized input)` hash was ALREADY denied in-window is retried, the
9
+ * harness keeps the deny but rewrites the message — `[REPEAT]` title, a
10
+ * mid-sentence STOP warning, forced `research-expert` action. State + wiring live in the sidecar
11
+ * store ({@link module:deny-loop-store}); this file is IO-free and pure.
12
+ * @packageDocumentation
13
+ */
14
+ import { hashText } from "../util/json-io";
15
+ import type { Prompt } from "../prompt/types";
16
+
17
+ /** One recorded deny: identical-in-window count, and when the last landed. */
18
+ export interface DenyEntry { count: number; lastTs: number; }
19
+
20
+ /** Loop-check outcome: does the input repeat an in-window deny, and its running count. */
21
+ export interface DenyLoopResult { isRepeat: boolean; count: number; hash: string; deduped?: boolean; }
22
+
23
+ /** Stable JSON: keys sorted at every depth so `{a,b}` and `{b,a}` hash identically. */
24
+ function stableStringify(v: unknown): string {
25
+ if (v === null || typeof v !== "object") return JSON.stringify(v) ?? "null";
26
+ if (Array.isArray(v)) return `[${v.map(stableStringify).join(",")}]`;
27
+ const o = v as Record<string, unknown>;
28
+ return `{${Object.keys(o).sort().map((k) => `${JSON.stringify(k)}:${stableStringify(o[k])}`).join(",")}}`;
29
+ }
30
+
31
+ /**
32
+ * Stable identity hash of a tool-call = tool name + normalized (key-sorted) input,
33
+ * so re-ordered keys never mask a repeat.
34
+ * @param tool - Tool name (e.g. "Write", "Bash").
35
+ * @param input - Identifying tool input (filePath/content/command...).
36
+ * @returns 8-char hex hash.
37
+ */
38
+ export function denyHash(tool: string, input: Record<string, unknown>): string {
39
+ return hashText(`${tool}\n${stableStringify(input)}`);
40
+ }
41
+
42
+ /**
43
+ * Pure loop check: given the already-pruned in-window map, compute the running
44
+ * count for `hash` and whether it repeats (count > 1). No IO — the caller persists.
45
+ *
46
+ * When `dedupMs` is set (>0) and an identical prior deny landed within that
47
+ * window, the current call is a sibling hook echoing the SAME event (see
48
+ * {@link module:burst-window}): it returns the prior verdict VERBATIM with
49
+ * `deduped:true` and does NOT bump the count, so all N fan-out processes agree
50
+ * on one number instead of counting to N. Absent `dedupMs` (mono-process
51
+ * callers / unit tests) the historical increment-every-time behaviour holds.
52
+ * @param hash - {@link denyHash}-derived map key of the current call.
53
+ * @param priorDenies - The `{ hash -> DenyEntry }` map, pruned to `now`/`windowMs`.
54
+ * @param opts - Clock + window, plus an optional burst-dedup window.
55
+ * @returns `{ isRepeat, count, hash, deduped? }`.
56
+ */
57
+ export function denyLoopCheck(hash: string, priorDenies: Record<string, DenyEntry>, opts: { now: number; windowMs: number; dedupMs?: number }): DenyLoopResult {
58
+ const prev = priorDenies[hash];
59
+ const fresh = prev && typeof prev.lastTs === "number" && opts.now - prev.lastTs < opts.windowMs;
60
+ if (!fresh) return { isRepeat: false, count: 1, hash };
61
+ if ((opts.dedupMs ?? 0) > 0 && opts.now - prev.lastTs < (opts.dedupMs ?? 0)) {
62
+ return { isRepeat: prev.count > 1, count: prev.count, hash, deduped: true };
63
+ }
64
+ const count = prev.count + 1;
65
+ return { isRepeat: count > 1, count, hash };
66
+ }
67
+
68
+ /**
69
+ * Enrich a REPEATED block prompt — a NEW object, never a mutation (the input may
70
+ * be a shared const like FAIL_CLOSED). The decision stays `block`; only the
71
+ * message changes, so every harness renders it through the same adapter.
72
+ * @param prompt - The original block prompt.
73
+ * @param count - The running identical-deny count (n).
74
+ * @returns A block prompt with `[REPEAT]` title, STOP-prefixed reason, forced research action.
75
+ */
76
+ export function enrichRepeatDeny(prompt: Prompt, count: number): Prompt {
77
+ // H2b: the counter cannot know whether new reads/actions happened since the
78
+ // last deny, so the hard STOP is reserved for the UNCHANGED retry — a retry
79
+ // after completing the required reads/actions is legitimate and expected.
80
+ const stop = `Identical attempt #${count} already denied for the same reason. Do not retry this same call UNCHANGED — if you have since completed the required reads/actions, retry now; otherwise STOP and pick a DIFFERENT approach. `;
81
+ const action = "Launch fuse-ai-pilot:research-expert to find a DIFFERENT approach";
82
+ return {
83
+ ...prompt,
84
+ title: prompt.title.startsWith("[REPEAT]") ? prompt.title : `[REPEAT] ${prompt.title}`,
85
+ reason: stop + prompt.reason,
86
+ actions: [action, ...(prompt.actions ?? [])],
87
+ };
88
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @module allowed-write
3
+ * The design-agent write allowlist, split out of `gates.ts` to keep that file
4
+ * within the SOLID size budget (SRP).
5
+ * @packageDocumentation
6
+ */
7
+ import { basename } from "node:path";
8
+
9
+ /** Extensions the design agent may always write: .html/.css/.md/.json. */
10
+ export const ALLOWED_WRITE: RegExp = /\.(html|css|md|json)$/;
11
+
12
+ /**
13
+ * C4: `motion*.js` ONLY, matched on the BASENAME — every one of the 11
14
+ * corpus references ships its animation as a separate `motion.js` /
15
+ * `motion-nav.js` / `motion-scroll.js` file, so banning every `.js` forced
16
+ * the agent to inline scripts the corpus explicitly teaches out-of-band.
17
+ * Deliberately NOT `\.js$` in general (that would open arbitrary app code,
18
+ * the exact thing this gate exists to block) — after the `motion` prefix,
19
+ * either nothing (`motion.js`) or `.`/`-` then more (`motion-nav.js`) must
20
+ * lead into `.js`; `app.js` and `motionless-app.js` — a bare `^motion`
21
+ * prefix would wrongly pass this — do not match.
22
+ */
23
+ const MOTION_JS: RegExp = /^motion([.-].*)?\.js$/;
24
+
25
+ /** True when `filePath` is a write the design agent may make: .html/.css/.md/.json, or a `motion*.js` file. */
26
+ export function isAllowedWrite(filePath: string): boolean {
27
+ return ALLOWED_WRITE.test(filePath) || MOTION_JS.test(basename(filePath));
28
+ }
@@ -0,0 +1,45 @@
1
+ /** Accessibility warnings: icon buttons need aria-label, images need alt. */
2
+ function checkAccessibility(content: string): string[] {
3
+ const w: string[] = [];
4
+ if (!/<(button|a|input|img)/.test(content)) return w;
5
+ if (/<button[^>]*>/.test(content) && !/aria-label|aria-labelledby/.test(content) && /<button[^>]*>[^<]*<[^>]*Icon/.test(content)) {
6
+ w.push("Accessibility: icon buttons need an aria-label.");
7
+ }
8
+ for (const m of content.matchAll(/<img[^>]*?>/g)) {
9
+ if (!m[0].includes("alt=")) {
10
+ w.push("Accessibility: images need an alt attribute.");
11
+ break;
12
+ }
13
+ }
14
+ return w;
15
+ }
16
+
17
+ /** Anti-pattern warnings: colored left borders, AI-slop gradients, emoji-as-icons. */
18
+ function checkPatterns(content: string): string[] {
19
+ const w: string[] = [];
20
+ if (/border-l-[0-9]+ border-l-(blue|green|red|purple)/.test(content)) w.push("Design: avoid colored left borders — use shadow/gradient.");
21
+ if (/from-purple|to-purple|via-purple|from-pink.*to-purple/.test(content)) w.push("Design: avoid purple/pink gradients (AI slop) — use brand colors.");
22
+ if (/>[^\x00-\x7F]+</.test(content)) w.push("Design: avoid emojis as icons — use a real icon set.");
23
+ return w;
24
+ }
25
+
26
+ /** Forbidden-font warnings (CSS font-family + Google Fonts import). */
27
+ function checkFonts(content: string): string[] {
28
+ const w: string[] = [];
29
+ if (/font-family:\s*['"]?(Roboto|Inter|Arial|Open Sans|Lato)\b/i.test(content)) w.push("Font: forbidden family (Roboto/Inter/Arial/Open Sans/Lato) — use identity fonts.");
30
+ if (/@import.*fonts\.googleapis.*family=(Roboto|Inter)\b/.test(content)) w.push("Font: Google Fonts import for a forbidden family.");
31
+ return w;
32
+ }
33
+
34
+ /** Hard-coded-color warnings (hex in className or inline style). */
35
+ function checkColors(content: string): string[] {
36
+ const w: string[] = [];
37
+ if (/className="[^"]*#[0-9a-fA-F]{3,8}[^"]*"/.test(content)) w.push("Color: hard-coded hex in className — use CSS variables.");
38
+ if (/(?:color|background(?:-color)?|fill|stroke):\s*['"]?#[0-9a-fA-F]{3,8}/.test(content)) w.push("Color: hard-coded hex in style — use var(--color-*).");
39
+ return w;
40
+ }
41
+
42
+ /** Run all design content checks → non-blocking warnings (empty = clean). */
43
+ export function runDesignChecks(content: string): string[] {
44
+ return [...checkAccessibility(content), ...checkPatterns(content), ...checkFonts(content), ...checkColors(content)];
45
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @module corpus-fs
3
+ * Shared fs primitives for corpus/plugins root resolution, split out of
4
+ * `corpus-resolve.ts` so `corpus-resolve-cache.ts` can reuse them without a
5
+ * circular import (both modules feed `corpus-resolve.ts`'s `probeClaude`).
6
+ * @packageDocumentation
7
+ */
8
+ import { readdirSync } from "node:fs";
9
+ import { join } from "node:path";
10
+
11
+ /** The refs-design/ path suffix common to every design-corpus-bearing plugin tree. */
12
+ export const CORPUS_SUFFIX: string = join("skills", "design-web", "references", "refs-design");
13
+
14
+ /** Immediate child dir names of `dir`, alphabetically sorted, or [] when unreadable. */
15
+ export function children(dir: string): string[] {
16
+ try {
17
+ return readdirSync(dir).sort();
18
+ } catch {
19
+ return [];
20
+ }
21
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * @module corpus-resolve-cache
3
+ * Claude Code's per-plugin VERSIONED CACHE tree, split out of
4
+ * `corpus-resolve.ts` to keep that file within the SOLID size budget (SRP).
5
+ *
6
+ * Claude Code caches every installed marketplace plugin at
7
+ * `~/.claude/plugins/cache/<mkt>/<plugin>/<version>/` — a tree SEPARATE from
8
+ * `~/.claude/plugins/marketplaces/<mkt>/plugins/<plugin>/` (the marketplace's
9
+ * own checkout), one version dir per `plugin.json` version bump, orphaned
10
+ * versions pruned ~14 days after being superseded (docs: "Plugin caching and
11
+ * file resolution"). The design corpus can ship under EITHER tree, and under
12
+ * a DIFFERENT plugin id in each: a machine observed 2026-07-29 has the corpus
13
+ * at `cache/fusengine-plugins/fuse-design/2.2.3/...` while the marketplace
14
+ * checkout is `marketplaces/fusengine-plugins/plugins/design-expert/...`.
15
+ * Identification here is therefore STRUCTURAL, never by plugin name: a
16
+ * candidate `<mkt>/<plugin>/<version>/` dir qualifies iff it itself contains
17
+ * `CORPUS_SUFFIX` — matching a hardcoded name would silently miss a
18
+ * renamed/relocated corpus (the exact defect this module fixes).
19
+ * @packageDocumentation
20
+ */
21
+ import { existsSync } from "node:fs";
22
+ import { join } from "node:path";
23
+ import { maxSemver } from "../../util/semver";
24
+ import { CORPUS_SUFFIX, children } from "./corpus-fs";
25
+
26
+ /** Real (non-prerelease) semver dirs, same filter `probeCodex` uses. */
27
+ const SEMVER_RE = /^\d+\.\d+(\.\d+)?$/;
28
+
29
+ /**
30
+ * Claude: `<home>/.claude/plugins/cache/<mkt>/<plugin>/<highest STABLE semver
31
+ * containing refs-design>` ("" when no plugin under any marketplace cache
32
+ * ships the corpus). Marketplaces and plugin ids are both walked — neither is
33
+ * assumed — so a corpus shipped under any plugin name is found.
34
+ */
35
+ export function probeClaudeCache(home: string): string {
36
+ const cacheRoot = join(home, ".claude", "plugins", "cache");
37
+ for (const mkt of children(cacheRoot)) {
38
+ const mktDir = join(cacheRoot, mkt);
39
+ for (const plugin of children(mktDir)) {
40
+ const pluginDir = join(mktDir, plugin);
41
+ const versions = children(pluginDir).filter(
42
+ (v) => SEMVER_RE.test(v) && existsSync(join(pluginDir, v, CORPUS_SUFFIX)),
43
+ );
44
+ const latest = maxSemver(versions);
45
+ if (latest) return join(pluginDir, latest);
46
+ }
47
+ }
48
+ return "";
49
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Corpus/plugins root resolution (fs) and the write guard over it. Resolution
3
+ * is PER RUNTIME (Claude marketplaces, Codex versioned plugin cache — same
4
+ * pattern as rules-root.ts); runtimes without a plugin structure (kimi today)
5
+ * resolve to "" and the corpus gates stay dormant. There is NO cwd fallback
6
+ * anywhere: an agent-controlled directory can never become its own taste
7
+ * reference (self-attested proof — the failure this gate exists to prevent).
8
+ */
9
+ import { existsSync } from "node:fs";
10
+ import { homedir } from "node:os";
11
+ import { isAbsolute, join, normalize } from "node:path";
12
+ import type { Prompt } from "../../prompt/types";
13
+ import { detectHarness, type HarnessId } from "../../detect/harness";
14
+ import { maxSemver } from "../../util/semver";
15
+ import { CORPUS_SUFFIX, children } from "./corpus-fs";
16
+ import { probeClaudeCache } from "./corpus-resolve-cache";
17
+
18
+ /** Claude marketplace checkout: `<home>/.claude/plugins/marketplaces/<mkt>/plugins/design-expert` ("" when absent). */
19
+ function probeClaudeMarketplace(home: string): string {
20
+ const markets = join(home, ".claude", "plugins", "marketplaces");
21
+ for (const m of children(markets)) {
22
+ const de = join(markets, m, "plugins", "design-expert");
23
+ if (existsSync(de)) return de;
24
+ }
25
+ return "";
26
+ }
27
+
28
+ /**
29
+ * Claude: the marketplace checkout WINS when present (a single unambiguous
30
+ * live git checkout); the versioned plugin CACHE tree is the fallback, since
31
+ * Claude Code keeps orphaned prior-version cache dirs ~14 days after a bump.
32
+ */
33
+ function probeClaude(home: string): string {
34
+ return probeClaudeMarketplace(home) || probeClaudeCache(home);
35
+ }
36
+
37
+ /** Codex: `<CODEX_HOME|~/.codex>/plugins/cache/<mkt>/design-expert/<highest STABLE semver>` ("" when absent). */
38
+ function probeCodex(env: Record<string, string | undefined>, home: string): string {
39
+ const cache = join(env.CODEX_HOME ?? join(home, ".codex"), "plugins", "cache");
40
+ for (const m of children(cache)) {
41
+ const de = join(cache, m, "design-expert");
42
+ // Only real version dirs compete — no pre-release, no stray backup/ dir.
43
+ const latest = maxSemver(children(de).filter((v) => /^\d+\.\d+(\.\d+)?$/.test(v) && existsSync(join(de, v, "skills"))));
44
+ if (latest) return join(de, latest);
45
+ }
46
+ return "";
47
+ }
48
+
49
+ /**
50
+ * The design-expert plugin root for the ACTIVE runtime ("" = not installed).
51
+ * `pluginsDirOverride` short-circuits resolution (tests): a PLUGINS dir whose
52
+ * `design-expert` child is used when present, else itself.
53
+ */
54
+ export function resolvePluginsRoot(
55
+ pluginsDirOverride?: string,
56
+ home: string = homedir(),
57
+ id: HarnessId = detectHarness().id,
58
+ env: Record<string, string | undefined> = process.env,
59
+ ): string {
60
+ if (pluginsDirOverride) {
61
+ const de = join(pluginsDirOverride, "design-expert");
62
+ return existsSync(de) ? de : pluginsDirOverride;
63
+ }
64
+ if (id === "claude-code") return probeClaude(home);
65
+ if (id === "codex") return probeCodex(env, home);
66
+ return "";
67
+ }
68
+
69
+ /** Resolve the delivered refs-design/ root, or "" when absent/unusable (the fail-open signal). */
70
+ export function resolveCorpusRoot(
71
+ pluginsDirOverride?: string,
72
+ home: string = homedir(),
73
+ id: HarnessId = detectHarness().id,
74
+ env: Record<string, string | undefined> = process.env,
75
+ ): string {
76
+ const root = resolvePluginsRoot(pluginsDirOverride, home, id, env);
77
+ if (!root) return "";
78
+ const corpus = join(root, CORPUS_SUFFIX);
79
+ return existsSync(corpus) ? corpus : "";
80
+ }
81
+
82
+ /**
83
+ * Deny Write/Edit/apply_patch-file under the resolved plugins root: the corpus
84
+ * is the artefact the pipeline checks reads against, so it must stay
85
+ * agent-proof — including while refs-design/ is still absent. Relative paths
86
+ * (the normal form of Codex patches) are resolved against `cwd` first. SCOPE:
87
+ * no Bash, no symlinks, no sibling marketplace — the gate protects against
88
+ * oversight, not an adversary (the mandatory screenshot stays the unforgeable
89
+ * proof; forging the corpus only lowers a quota).
90
+ */
91
+ export function pluginsWriteGuard(filePath: string, pluginsRoot: string, cwd = ""): Prompt | null {
92
+ const abs = isAbsolute(filePath) ? filePath : cwd ? normalize(join(cwd, filePath)) : filePath;
93
+ if (!pluginsRoot || !abs.startsWith(`${pluginsRoot}/`)) return null;
94
+ return {
95
+ kind: "block", title: "Design pipeline",
96
+ reason: "BLOCKED: the design-expert plugin dir (refs-design corpus) is read-only — it is the taste reference your reads are checked against. Never create or modify files there.",
97
+ actions: ["Read the corpus with the Read tool; write your own files in the project"],
98
+ };
99
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Corpus anchoring for the design pipeline: refs-design/ read classification,
3
+ * per-mode readiness, citation form, citation↔read jointure. Nothing about
4
+ * the corpus content (slugs, section titles, count) is hardcoded — everything
5
+ * is matched by shape or discovered on disk, so a new reference or a renamed
6
+ * section never requires reopening the harness. Pure string logic only; root
7
+ * resolution and the write guard live in `corpus-resolve.ts`.
8
+ */
9
+ import { basename, join } from "node:path";
10
+ import type { DesignMode } from "./state";
11
+
12
+ export { resolveCorpusRoot, resolvePluginsRoot, pluginsWriteGuard } from "./corpus-resolve";
13
+
14
+ /**
15
+ * What a read under the corpus root is: the index, a tokens-* procedure
16
+ * file, or a per-reference `design-system.md` DIRECTION sheet (C6) — the
17
+ * fiche a reference ships alongside its `tokens-*.md`: register, tone,
18
+ * macrostructure, signature element (the PROCEDURES live in tokens-*;
19
+ * design-system.md is what you read to CHOOSE a reference in the first
20
+ * place). "sheet" is a new variant, not folded into "index" — the two are
21
+ * read for opposite reasons (corpus-wide index vs one reference's direction).
22
+ */
23
+ export type CorpusKind = "index" | "tokens" | "sheet";
24
+
25
+ const TOKENS_RE = /^tokens-.+\.md$/;
26
+ const CORPUS_LINE_RE = /^[-*]\s*Corpus:\s*(.+)$/gim;
27
+
28
+ /** Classify a read path against the DELIVERED corpus root (null = not a corpus read). */
29
+ export function classifyCorpusRead(filePath: string, corpusRoot: string): CorpusKind | null {
30
+ if (!corpusRoot) return null;
31
+ if (filePath === join(corpusRoot, "README.md")) return "index";
32
+ if (!filePath.startsWith(`${corpusRoot}/`)) return null;
33
+ if (TOKENS_RE.test(basename(filePath))) return "tokens";
34
+ if (basename(filePath) === "design-system.md") return "sheet";
35
+ return null;
36
+ }
37
+
38
+ /**
39
+ * Per-mode corpus-read threshold (component >= 1 file, page >= 2 files,
40
+ * full = index + 3 tokens — C5: raised from 2, since 2 references still let
41
+ * an agent pattern-match by tonal affinity without ever opening the one
42
+ * reference that actually answered the brief). `"sheet"` reads (C6,
43
+ * `design-system.md`) never count toward this — only `TOKENS_RE` matches do.
44
+ */
45
+ export function corpusReady(reads: readonly string[], mode: DesignMode): boolean {
46
+ if (mode === "component") return reads.length >= 1;
47
+ if (mode === "page") return reads.length >= 2;
48
+ return reads.includes("README.md") && reads.filter((r) => TOKENS_RE.test(basename(r))).length >= 3;
49
+ }
50
+
51
+ /** True when the content carries a `- Corpus: ref/section` citation line (form only). */
52
+ export function hasCorpusCitation(content: string): boolean {
53
+ return citedCorpusRefs(content).length > 0;
54
+ }
55
+
56
+ /**
57
+ * Extract the reference slugs cited on EVERY `- Corpus:` line (token before
58
+ * `/`, or a bare slug). Anchored at line start with NO indentation
59
+ * (fail-closed: a citation inside a code block does not count — an indented
60
+ * match would also recognize the examples inside the doctrine doc itself).
61
+ * The regex is fence-blind, documented: a `- Corpus:` line at column 0 inside
62
+ * a fenced block WILL be matched — its refs must join like any other.
63
+ */
64
+ export function citedCorpusRefs(content: string): string[] {
65
+ const refs: string[] = [];
66
+ for (const m of content.matchAll(CORPUS_LINE_RE)) {
67
+ const line = m[1] ?? "";
68
+ refs.push(...line.split(",").map((item) => item.trim().match(/^([\w][\w-]*)(?:\/|\s|$)/)?.[1] ?? "").filter(Boolean));
69
+ }
70
+ return refs;
71
+ }
72
+
73
+ /**
74
+ * True when the cited ref names a REFERENCE in a recorded corpus read, in
75
+ * either naming convention (`reve` README-style ↔ `reve-recode/` on disk —
76
+ * the `-recode` suffix is normalized away on BOTH sides) and either layout
77
+ * (per-reference directories, or flat `tokens-<ref>.md` files). Comparison is
78
+ * exact after normalization: no prefix matching, so `acme` never joins
79
+ * `acme-corp`, `tokens` never joins every tokens file, and `README` names no
80
+ * reference.
81
+ */
82
+ export function citationJoinsReads(ref: string, reads: readonly string[]): boolean {
83
+ const norm = (s: string): string => s.replace(/\.md$/, "").replace(/-recode$/, "");
84
+ const target = norm(ref);
85
+ return reads.some((r) => {
86
+ const segs = r.split("/");
87
+ const file = segs[segs.length - 1] ?? "";
88
+ const names = segs.slice(0, -1).map(norm);
89
+ if (file.startsWith("tokens-")) names.push(norm(file.slice("tokens-".length)));
90
+ return names.includes(target);
91
+ });
92
+ }