@fusengine/harness 0.1.89 → 0.1.91

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (419) hide show
  1. package/dist/adapters/claude/index.d.mts +1 -1
  2. package/dist/adapters/claude/index.mjs +1 -1
  3. package/dist/adapters/cline/index.mjs +1 -1
  4. package/dist/adapters/codex/index.d.mts +2 -2
  5. package/dist/adapters/codex/index.mjs +5 -4
  6. package/dist/adapters/cursor/index.d.mts +15 -28
  7. package/dist/adapters/cursor/index.mjs +35 -46
  8. package/dist/adapters/gemini/index.mjs +1 -1
  9. package/dist/adapters/hermes/index.d.mts +1 -1
  10. package/dist/adapters/hermes/index.mjs +1 -1
  11. package/dist/adapters/kimi/index.d.mts +1 -1
  12. package/dist/adapters/kimi/index.mjs +1 -1
  13. package/dist/{claude-5bC8TDAz.mjs → claude-Ckv2_TgP.mjs} +3 -43
  14. package/dist/cli/bin.mjs +429 -16
  15. package/dist/cli/index.mjs +1 -1
  16. package/dist/command-string-CALMTnwN.mjs +42 -0
  17. package/dist/{evaluate-C1VSI5pb.mjs → evaluate-CMiIqHeH.mjs} +261 -13
  18. package/dist/{handle-DHhukRdh.mjs → handle-C43gA-Pr.mjs} +737 -150
  19. package/dist/{hermes-kP6NUQlG.mjs → hermes-B9-p_3IF.mjs} +3 -2
  20. package/dist/{index-DBgnIZn9.d.mts → index-B8PG82su.d.mts} +1 -1
  21. package/dist/{index-D4C2-IIc.d.mts → index-SmKYkk2N.d.mts} +1 -1
  22. package/dist/index.d.mts +2 -2
  23. package/dist/index.mjs +2 -2
  24. package/dist/init/index.d.mts +1 -1
  25. package/dist/init/index.mjs +1 -1
  26. package/dist/{kimi-D6X7b8wF.mjs → kimi-G2wcSh5-.mjs} +3 -2
  27. package/dist/normalize-BjG6unTj.mjs +317 -0
  28. package/dist/permission-mode-B-SFyR0X.mjs +226 -0
  29. package/dist/policy/index.d.mts +1 -1
  30. package/dist/policy/index.mjs +2 -2
  31. package/dist/prompt/index.d.mts +1 -1
  32. package/dist/{run-CdIlUtjv.mjs → run-DkrzC0gb.mjs} +1 -1
  33. package/dist/{run-TAXNRSpD.mjs → run-GHAMqcOn.mjs} +26 -3
  34. package/dist/runtime/index.d.mts +27 -16
  35. package/dist/runtime/index.mjs +2 -2
  36. package/dist/{types-C0-igRKr.d.mts → types-Bh0jEF3_.d.mts} +2 -0
  37. package/dist/{validate-DZFtAbLP.mjs → validate-KjZ1X9tH.mjs} +1 -1
  38. package/package.json +3 -2
  39. package/src/adapters/claude/index.ts +92 -0
  40. package/src/adapters/cline/index.ts +36 -0
  41. package/src/adapters/codex/apply-patch.ts +94 -0
  42. package/src/adapters/codex/index.ts +84 -0
  43. package/src/adapters/codex/permission-mode.ts +16 -0
  44. package/src/adapters/cursor/context.ts +46 -0
  45. package/src/adapters/cursor/events.ts +38 -0
  46. package/src/adapters/cursor/index.ts +43 -0
  47. package/src/adapters/cursor/interfaces/types.ts +88 -0
  48. package/src/adapters/cursor/native-response.ts +158 -0
  49. package/src/adapters/cursor/normalize.ts +123 -0
  50. package/src/adapters/cursor/respond.ts +123 -0
  51. package/src/adapters/gemini/index.ts +36 -0
  52. package/src/adapters/hermes/index.ts +55 -0
  53. package/src/adapters/hermes/interfaces/types.ts +26 -0
  54. package/src/adapters/kimi/index.ts +96 -0
  55. package/src/adapters/kimi/interfaces/types.ts +55 -0
  56. package/src/cache/compact.ts +55 -0
  57. package/src/cache/index.ts +5 -0
  58. package/src/cache/io.ts +38 -0
  59. package/src/cache/mcp-response.ts +39 -0
  60. package/src/cache/mcp-store.ts +70 -0
  61. package/src/cache/store.ts +93 -0
  62. package/src/changelog/fetch.ts +79 -0
  63. package/src/cli/bin.ts +100 -0
  64. package/src/cli/cursor-event-scanner.ts +175 -0
  65. package/src/cli/cursor-stdin-reader.ts +65 -0
  66. package/src/cli/doctor.ts +96 -0
  67. package/src/cli/hook-io.ts +130 -0
  68. package/src/cli/hook-sound.ts +49 -0
  69. package/src/cli/index.ts +1 -0
  70. package/src/cli/json-primitive-scanner.ts +83 -0
  71. package/src/cli/run.ts +36 -0
  72. package/src/cli/scope.ts +31 -0
  73. package/src/codex-rules/build.ts +63 -0
  74. package/src/codex-rules/index.ts +8 -0
  75. package/src/codex-rules/mapping.ts +42 -0
  76. package/src/codex-rules/rules/absolute-paths.ts +52 -0
  77. package/src/codex-rules/rules/ask.ts +44 -0
  78. package/src/codex-rules/rules/chmod-ask.ts +57 -0
  79. package/src/codex-rules/rules/dd.ts +23 -0
  80. package/src/codex-rules/rules/diskutil.ts +33 -0
  81. package/src/codex-rules/rules/mkfs.ts +42 -0
  82. package/src/codex-rules/rules/privilege.ts +49 -0
  83. package/src/codex-rules/rules/remote-fetch.ts +31 -0
  84. package/src/codex-rules/rules/rm-variants.ts +58 -0
  85. package/src/codex-rules/rules/single-commands.ts +57 -0
  86. package/src/codex-rules/skip-list.ts +25 -0
  87. package/src/codex-rules/starlark.ts +46 -0
  88. package/src/codex-rules/types.ts +36 -0
  89. package/src/config/dotenv.ts +55 -0
  90. package/src/config/env.ts +10 -0
  91. package/src/config/home-dir.ts +36 -0
  92. package/src/config/index.ts +5 -0
  93. package/src/config/layout.ts +52 -0
  94. package/src/config/limits.ts +56 -0
  95. package/src/config/ttl.ts +30 -0
  96. package/src/detect/harness.ts +73 -0
  97. package/src/detect/index.ts +1 -0
  98. package/src/detect/interfaces/types.ts +18 -0
  99. package/src/freshness/agent-evidence-record.ts +95 -0
  100. package/src/freshness/agent-evidence.ts +95 -0
  101. package/src/freshness/codex-spawn-evidence.ts +91 -0
  102. package/src/freshness/doc-helpers.ts +90 -0
  103. package/src/freshness/evidence-harvest-io.ts +72 -0
  104. package/src/freshness/evidence-harvest.ts +61 -0
  105. package/src/freshness/explore-tools.ts +95 -0
  106. package/src/freshness/index.ts +2 -0
  107. package/src/freshness/query-framework.ts +73 -0
  108. package/src/freshness/ref-evidence.ts +49 -0
  109. package/src/freshness/ref-journal.ts +98 -0
  110. package/src/freshness/trivial-edit-counter.ts +30 -0
  111. package/src/index.ts +11 -0
  112. package/src/init/index.ts +2 -0
  113. package/src/init/run.ts +32 -0
  114. package/src/init/templates.ts +75 -0
  115. package/src/memory/gitignore.ts +14 -0
  116. package/src/memory/index.ts +3 -0
  117. package/src/memory/registry.ts +39 -0
  118. package/src/memory/session-roots.ts +99 -0
  119. package/src/memory/state.ts +58 -0
  120. package/src/policy/apex-authorization.ts +100 -0
  121. package/src/policy/apex-gates.ts +91 -0
  122. package/src/policy/apex-target-steps.ts +73 -0
  123. package/src/policy/apex-target.ts +49 -0
  124. package/src/policy/apex-task-context.ts +81 -0
  125. package/src/policy/apex.ts +94 -0
  126. package/src/policy/cartographer/build-tree.ts +69 -0
  127. package/src/policy/cartographer/describe.ts +53 -0
  128. package/src/policy/cartographer/entry.ts +38 -0
  129. package/src/policy/cartographer/frontmatter.ts +48 -0
  130. package/src/policy/cartographer/indicators.ts +34 -0
  131. package/src/policy/claude-md-context.ts +66 -0
  132. package/src/policy/conventions/index.ts +11 -0
  133. package/src/policy/conventions/interfaces.ts +71 -0
  134. package/src/policy/conventions/langs.ts +53 -0
  135. package/src/policy/conventions/query.ts +49 -0
  136. package/src/policy/conventions/react-hooks.ts +18 -0
  137. package/src/policy/conventions/store-or-query.ts +21 -0
  138. package/src/policy/conventions/stores.ts +56 -0
  139. package/src/policy/conventions/strip.ts +87 -0
  140. package/src/policy/conventions/verdict.ts +38 -0
  141. package/src/policy/creation-intent.ts +11 -0
  142. package/src/policy/deny-loop.ts +88 -0
  143. package/src/policy/design/allowed-write.ts +28 -0
  144. package/src/policy/design/content-checks.ts +45 -0
  145. package/src/policy/design/corpus-fs.ts +21 -0
  146. package/src/policy/design/corpus-resolve-cache.ts +49 -0
  147. package/src/policy/design/corpus-resolve.ts +99 -0
  148. package/src/policy/design/corpus.ts +92 -0
  149. package/src/policy/design/design-system-rules.ts +45 -0
  150. package/src/policy/design/flag.ts +30 -0
  151. package/src/policy/design/gates-pipeline.ts +105 -0
  152. package/src/policy/design/gates.ts +87 -0
  153. package/src/policy/design/screenshot-tools.ts +24 -0
  154. package/src/policy/design/skill-evidence.ts +34 -0
  155. package/src/policy/design/skill-gate.ts +81 -0
  156. package/src/policy/design/skill-triggers.ts +51 -0
  157. package/src/policy/design/state.ts +92 -0
  158. package/src/policy/design/template-urls.ts +54 -0
  159. package/src/policy/design/transitions.ts +68 -0
  160. package/src/policy/detect-claude-md-project-type.ts +32 -0
  161. package/src/policy/detect-framework.ts +99 -0
  162. package/src/policy/detect-primitive-checks.ts +100 -0
  163. package/src/policy/detect-primitive-lib.ts +55 -0
  164. package/src/policy/detect-project.ts +99 -0
  165. package/src/policy/edit-outcome.ts +53 -0
  166. package/src/policy/evaluate.ts +81 -0
  167. package/src/policy/expert-agents.ts +60 -0
  168. package/src/policy/file-size-scope.ts +32 -0
  169. package/src/policy/file-size.ts +99 -0
  170. package/src/policy/framework-solid-exclude.ts +52 -0
  171. package/src/policy/framework-solid-extended.ts +93 -0
  172. package/src/policy/framework-solid-gates-systems.ts +99 -0
  173. package/src/policy/framework-solid-gates.ts +98 -0
  174. package/src/policy/framework-solid.ts +88 -0
  175. package/src/policy/gemini-mcp-gate.ts +83 -0
  176. package/src/policy/git-gates.ts +40 -0
  177. package/src/policy/guards/bash-command-anchor.ts +40 -0
  178. package/src/policy/guards/bash-write-patterns.ts +140 -0
  179. package/src/policy/guards/bash-write-redirects.ts +201 -0
  180. package/src/policy/guards/bash-write-safe-paths.ts +62 -0
  181. package/src/policy/guards/bash-write.ts +76 -0
  182. package/src/policy/guards/context.ts +12 -0
  183. package/src/policy/guards/index.ts +61 -0
  184. package/src/policy/guards/install.ts +22 -0
  185. package/src/policy/guards/interface-separation-ext.ts +41 -0
  186. package/src/policy/guards/interface-separation.ts +100 -0
  187. package/src/policy/guards/protected-path.ts +100 -0
  188. package/src/policy/guards/security.ts +97 -0
  189. package/src/policy/index.ts +19 -0
  190. package/src/policy/interfaces/types.ts +46 -0
  191. package/src/policy/js-gate-route.ts +51 -0
  192. package/src/policy/lessons/lesson-gate.ts +0 -0
  193. package/src/policy/lessons/trigger-index.ts +84 -0
  194. package/src/policy/lessons/types.ts +31 -0
  195. package/src/policy/module-layout.ts +31 -0
  196. package/src/policy/nearest-manifest.ts +75 -0
  197. package/src/policy/never-approval.ts +95 -0
  198. package/src/policy/patterns.ts +80 -0
  199. package/src/policy/seo/validate.ts +51 -0
  200. package/src/policy/shadcn-project.ts +22 -0
  201. package/src/policy/shadcn-skill-gate.ts +97 -0
  202. package/src/policy/shell-read-refs.ts +96 -0
  203. package/src/policy/skill-path.ts +46 -0
  204. package/src/policy/skill-patterns/laravel.ts +40 -0
  205. package/src/policy/skill-patterns/nextjs.ts +29 -0
  206. package/src/policy/skill-patterns/react.ts +27 -0
  207. package/src/policy/skill-patterns/shadcn-subskills.ts +33 -0
  208. package/src/policy/skill-patterns/shadcn.ts +66 -0
  209. package/src/policy/skill-patterns/swift.ts +30 -0
  210. package/src/policy/skill-patterns/tailwind.ts +40 -0
  211. package/src/policy/skill-source.ts +52 -0
  212. package/src/policy/skill-trigger-patterns.ts +33 -0
  213. package/src/policy/skill-triggers.ts +108 -0
  214. package/src/policy/tailwind-skill-gate.ts +63 -0
  215. package/src/policy/trivial-edits.ts +52 -0
  216. package/src/policy/verbosity.ts +39 -0
  217. package/src/prompt/index.ts +1 -0
  218. package/src/prompt/types.ts +36 -0
  219. package/src/refs/discover.ts +69 -0
  220. package/src/refs/frontmatter.ts +24 -0
  221. package/src/refs/index.ts +4 -0
  222. package/src/refs/loader.ts +59 -0
  223. package/src/refs/ref-key.ts +41 -0
  224. package/src/refs/router.ts +63 -0
  225. package/src/refs/types.ts +25 -0
  226. package/src/runtime/activity.ts +99 -0
  227. package/src/runtime/apply-patch-gate.ts +41 -0
  228. package/src/runtime/burst-window.ts +21 -0
  229. package/src/runtime/codex-shell-tool.ts +37 -0
  230. package/src/runtime/command-string.ts +37 -0
  231. package/src/runtime/confirm/codex-confirm.ts +126 -0
  232. package/src/runtime/confirm/codex-prompt-origin.ts +16 -0
  233. package/src/runtime/confirm/confirm-code.ts +33 -0
  234. package/src/runtime/confirm/confirm-gate.ts +65 -0
  235. package/src/runtime/confirm/confirm-irreversible.ts +32 -0
  236. package/src/runtime/confirm/confirm-pending.ts +34 -0
  237. package/src/runtime/confirm/confirm-state.ts +56 -0
  238. package/src/runtime/confirm/confirm-subagent.ts +112 -0
  239. package/src/runtime/confirm/confirm-submit.ts +38 -0
  240. package/src/runtime/core-guards-active.ts +84 -0
  241. package/src/runtime/core-guards-wired.ts +47 -0
  242. package/src/runtime/deny-loop-store.ts +94 -0
  243. package/src/runtime/deny-notice.ts +52 -0
  244. package/src/runtime/design-cache-resolve.ts +51 -0
  245. package/src/runtime/design-content-gate.ts +95 -0
  246. package/src/runtime/design-files-gate.ts +114 -0
  247. package/src/runtime/design-helpers.ts +93 -0
  248. package/src/runtime/design-lifecycle.ts +39 -0
  249. package/src/runtime/design-read-credit.ts +28 -0
  250. package/src/runtime/design.ts +116 -0
  251. package/src/runtime/dev-context.ts +43 -0
  252. package/src/runtime/dry-patterns.ts +32 -0
  253. package/src/runtime/dry.ts +96 -0
  254. package/src/runtime/fragment-registry.ts +33 -0
  255. package/src/runtime/framework-skill-gate.ts +79 -0
  256. package/src/runtime/fs-cleanup.ts +71 -0
  257. package/src/runtime/gate-apex.ts +81 -0
  258. package/src/runtime/gate-helpers.ts +52 -0
  259. package/src/runtime/gate-input.ts +26 -0
  260. package/src/runtime/gate.ts +128 -0
  261. package/src/runtime/handle-post.ts +104 -0
  262. package/src/runtime/handle-pre.ts +137 -0
  263. package/src/runtime/handle-scope-async.ts +29 -0
  264. package/src/runtime/handle-types.ts +27 -0
  265. package/src/runtime/handle.ts +116 -0
  266. package/src/runtime/home-state.ts +55 -0
  267. package/src/runtime/index.ts +18 -0
  268. package/src/runtime/inform.ts +28 -0
  269. package/src/runtime/inject-budget-recap.ts +47 -0
  270. package/src/runtime/inject-budget.ts +81 -0
  271. package/src/runtime/inject-context.ts +85 -0
  272. package/src/runtime/inject-dedup-exclusive.ts +98 -0
  273. package/src/runtime/inject-dedup.ts +89 -0
  274. package/src/runtime/is-agent-tool.ts +19 -0
  275. package/src/runtime/lifecycle/agent-files.ts +49 -0
  276. package/src/runtime/lifecycle/agent-memory.ts +82 -0
  277. package/src/runtime/lifecycle/agent-transcript.ts +80 -0
  278. package/src/runtime/lifecycle/aipilot/analytics.ts +99 -0
  279. package/src/runtime/lifecycle/aipilot/apex-task-store.ts +61 -0
  280. package/src/runtime/lifecycle/aipilot/cache-base.ts +48 -0
  281. package/src/runtime/lifecycle/aipilot/cache-doc.ts +100 -0
  282. package/src/runtime/lifecycle/aipilot/cache-lessons.ts +58 -0
  283. package/src/runtime/lifecycle/aipilot/cache-test.ts +70 -0
  284. package/src/runtime/lifecycle/aipilot/curate-lessons.ts +64 -0
  285. package/src/runtime/lifecycle/aipilot/dispatch-aipilot.ts +100 -0
  286. package/src/runtime/lifecycle/aipilot/doc-cache-gate.ts +69 -0
  287. package/src/runtime/lifecycle/aipilot/inject-apex.ts +98 -0
  288. package/src/runtime/lifecycle/aipilot/inject-doc.ts +62 -0
  289. package/src/runtime/lifecycle/aipilot/inject-explore.ts +64 -0
  290. package/src/runtime/lifecycle/aipilot/inject-lessons.ts +61 -0
  291. package/src/runtime/lifecycle/aipilot/inject-test.ts +48 -0
  292. package/src/runtime/lifecycle/aipilot/lesson-archive.ts +60 -0
  293. package/src/runtime/lifecycle/aipilot/lesson-inject.ts +98 -0
  294. package/src/runtime/lifecycle/aipilot/lesson-parse.ts +74 -0
  295. package/src/runtime/lifecycle/aipilot/lessons.ts +72 -0
  296. package/src/runtime/lifecycle/aipilot/promote-global-lessons.ts +92 -0
  297. package/src/runtime/lifecycle/aipilot/solid-compliance.ts +69 -0
  298. package/src/runtime/lifecycle/aipilot/solid-notice.ts +47 -0
  299. package/src/runtime/lifecycle/aipilot/solid-transcript.ts +60 -0
  300. package/src/runtime/lifecycle/aipilot/source-scan.ts +56 -0
  301. package/src/runtime/lifecycle/aipilot/sync-task.ts +75 -0
  302. package/src/runtime/lifecycle/aipilot/transcript.ts +88 -0
  303. package/src/runtime/lifecycle/aipilot/types.ts +72 -0
  304. package/src/runtime/lifecycle/auto-document-reads.ts +73 -0
  305. package/src/runtime/lifecycle/cartographer/detect.ts +57 -0
  306. package/src/runtime/lifecycle/cartographer/ecosystem-map.ts +56 -0
  307. package/src/runtime/lifecycle/cartographer/fs-util.ts +64 -0
  308. package/src/runtime/lifecycle/cartographer/merge.ts +51 -0
  309. package/src/runtime/lifecycle/cartographer/project-map.ts +49 -0
  310. package/src/runtime/lifecycle/cartographer/scan-hooks.ts +48 -0
  311. package/src/runtime/lifecycle/cartographer/scan.ts +81 -0
  312. package/src/runtime/lifecycle/cartographer/session-start.ts +33 -0
  313. package/src/runtime/lifecycle/cartographer/track-enrichment.ts +45 -0
  314. package/src/runtime/lifecycle/cartographer/write-plugin-map.ts +43 -0
  315. package/src/runtime/lifecycle/cartographer/write-tree.ts +44 -0
  316. package/src/runtime/lifecycle/changelog-research.ts +43 -0
  317. package/src/runtime/lifecycle/check-file-size.ts +61 -0
  318. package/src/runtime/lifecycle/codex-resync/fingerprint.ts +63 -0
  319. package/src/runtime/lifecycle/codex-resync/lock.ts +46 -0
  320. package/src/runtime/lifecycle/codex-resync/managed-destination.ts +39 -0
  321. package/src/runtime/lifecycle/codex-resync/materialize-agents.ts +57 -0
  322. package/src/runtime/lifecycle/codex-resync/plugin-files.ts +65 -0
  323. package/src/runtime/lifecycle/codex-resync/plugin-roots.ts +62 -0
  324. package/src/runtime/lifecycle/codex-resync/resync.ts +45 -0
  325. package/src/runtime/lifecycle/codex-resync/symlink-commands.ts +25 -0
  326. package/src/runtime/lifecycle/dispatch.ts +109 -0
  327. package/src/runtime/lifecycle/failure-lesson.ts +51 -0
  328. package/src/runtime/lifecycle/index.ts +34 -0
  329. package/src/runtime/lifecycle/inject-rules.ts +50 -0
  330. package/src/runtime/lifecycle/instructions-loaded.ts +22 -0
  331. package/src/runtime/lifecycle/kimi-rules-native.ts +44 -0
  332. package/src/runtime/lifecycle/lessons/dispatch.ts +88 -0
  333. package/src/runtime/lifecycle/lessons/reminder.ts +89 -0
  334. package/src/runtime/lifecycle/lessons/state.ts +23 -0
  335. package/src/runtime/lifecycle/memory/agent-lesson.ts +35 -0
  336. package/src/runtime/lifecycle/memory/capture-error.ts +41 -0
  337. package/src/runtime/lifecycle/memory/client.ts +65 -0
  338. package/src/runtime/lifecycle/memory/dispatch.ts +42 -0
  339. package/src/runtime/lifecycle/memory/recall.ts +30 -0
  340. package/src/runtime/lifecycle/memory/salience.ts +31 -0
  341. package/src/runtime/lifecycle/memory/state.ts +49 -0
  342. package/src/runtime/lifecycle/memory/track-ops.ts +15 -0
  343. package/src/runtime/lifecycle/post-compact.ts +38 -0
  344. package/src/runtime/lifecycle/post-edit-ts.ts +49 -0
  345. package/src/runtime/lifecycle/post-tracking.ts +40 -0
  346. package/src/runtime/lifecycle/pre-compact.ts +38 -0
  347. package/src/runtime/lifecycle/rules-root.ts +83 -0
  348. package/src/runtime/lifecycle/security/check-skill.ts +63 -0
  349. package/src/runtime/lifecycle/security/scan-patterns.ts +53 -0
  350. package/src/runtime/lifecycle/security/scan.ts +76 -0
  351. package/src/runtime/lifecycle/security/skill-state.ts +47 -0
  352. package/src/runtime/lifecycle/security/track-mcp.ts +29 -0
  353. package/src/runtime/lifecycle/security/track-skill-read.ts +31 -0
  354. package/src/runtime/lifecycle/seo/post-tool-use.ts +45 -0
  355. package/src/runtime/lifecycle/session-end.ts +17 -0
  356. package/src/runtime/lifecycle/session-start.ts +57 -0
  357. package/src/runtime/lifecycle/snapshot/board.ts +27 -0
  358. package/src/runtime/lifecycle/snapshot/format.ts +60 -0
  359. package/src/runtime/lifecycle/snapshot/git.ts +71 -0
  360. package/src/runtime/lifecycle/snapshot/index.ts +52 -0
  361. package/src/runtime/lifecycle/snapshot/version.ts +34 -0
  362. package/src/runtime/lifecycle/solid-detect.ts +56 -0
  363. package/src/runtime/lifecycle/stop-core.ts +35 -0
  364. package/src/runtime/lifecycle/subagent-cache.ts +61 -0
  365. package/src/runtime/lifecycle/task-completed.ts +94 -0
  366. package/src/runtime/lifecycle/teammate-idle-check.ts +63 -0
  367. package/src/runtime/lifecycle/teammate-idle.ts +28 -0
  368. package/src/runtime/lifecycle/tool-failure.ts +24 -0
  369. package/src/runtime/lifecycle/track-changes.ts +61 -0
  370. package/src/runtime/lifecycle/validate-solid.ts +70 -0
  371. package/src/runtime/lifecycle/validate-tailwind.ts +80 -0
  372. package/src/runtime/lifecycle-bridge.ts +74 -0
  373. package/src/runtime/mcp-key.ts +44 -0
  374. package/src/runtime/mcp-tool-name.ts +92 -0
  375. package/src/runtime/mcp.ts +91 -0
  376. package/src/runtime/modular.ts +77 -0
  377. package/src/runtime/normalize.ts +107 -0
  378. package/src/runtime/notices.ts +91 -0
  379. package/src/runtime/notification-sound.ts +49 -0
  380. package/src/runtime/notifications.ts +77 -0
  381. package/src/runtime/paths.ts +59 -0
  382. package/src/runtime/post-fanout.ts +41 -0
  383. package/src/runtime/post-outcome.ts +61 -0
  384. package/src/runtime/pre-allow.ts +97 -0
  385. package/src/runtime/precommit.ts +60 -0
  386. package/src/runtime/prompt-text.ts +38 -0
  387. package/src/runtime/receipt-capture.ts +34 -0
  388. package/src/runtime/record.ts +26 -0
  389. package/src/runtime/respond.ts +92 -0
  390. package/src/runtime/solid-file-size-gate.ts +58 -0
  391. package/src/runtime/solid-pre.ts +53 -0
  392. package/src/runtime/storage.ts +9 -0
  393. package/src/statusline/colors.ts +31 -0
  394. package/src/statusline/constants.ts +17 -0
  395. package/src/statusline/formatters.ts +54 -0
  396. package/src/statusline/index.ts +4 -0
  397. package/src/statusline/progress-bar.ts +46 -0
  398. package/src/tracking/codex-post-failure.ts +73 -0
  399. package/src/tracking/index.ts +2 -0
  400. package/src/tracking/integrity.ts +99 -0
  401. package/src/tracking/one-shot-dedup.ts +34 -0
  402. package/src/tracking/one-shot-failure.ts +42 -0
  403. package/src/tracking/one-shot-store.ts +95 -0
  404. package/src/tracking/one-shot.ts +93 -0
  405. package/src/tracking/receipts.ts +95 -0
  406. package/src/tracking/session-state.ts +94 -0
  407. package/src/tracking/store.ts +96 -0
  408. package/src/tracking/track-compact.ts +100 -0
  409. package/src/tracking/track-diff.ts +50 -0
  410. package/src/tracking/track-journal.ts +97 -0
  411. package/src/tracking/track-lock-sync.ts +83 -0
  412. package/src/tracking/track-lock.ts +67 -0
  413. package/src/util/compact-json.ts +23 -0
  414. package/src/util/index.ts +2 -0
  415. package/src/util/json-io.ts +47 -0
  416. package/src/util/project-root.ts +41 -0
  417. package/src/util/runtime-io.ts +85 -0
  418. package/src/util/semver.ts +30 -0
  419. package/dist/permission-mode-B0MUl7FT.mjs +0 -136
@@ -0,0 +1,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
+ }
@@ -0,0 +1,68 @@
1
+ import { type DesignState, type DesignMode, quotaFor } from "./state";
2
+ import { corpusReady } from "./corpus";
3
+
4
+ /** Infer the pipeline mode from the launch prompt + whether a design-system.md already exists. */
5
+ export function detectMode(prompt: string, designSystemExists: boolean): DesignMode {
6
+ const p = prompt.toLowerCase();
7
+ if (["component", "composant", "snippet"].some((k) => p.includes(k))) return "component";
8
+ return designSystemExists ? "page" : "full";
9
+ }
10
+
11
+ /**
12
+ * Re-evaluate the phase-2 conjunction MONOTONICALLY: corpus reads (when the
13
+ * corpus is delivered) AND the screenshot quota must both hold, and the phase
14
+ * is never written downwards (a Read after phase 3 changes nothing).
15
+ */
16
+ function maybePhase2(state: DesignState, corpusRequired: boolean): DesignState {
17
+ if (state.currentPhase >= 2) return state;
18
+ const corpusOk = !corpusRequired || corpusReady(state.corpusReads, state.mode);
19
+ if (!corpusOk || state.screenshotsCount < quotaFor(state.mode, corpusRequired)) return state;
20
+ return { ...state, currentPhase: 2, phasesCompleted: [...new Set([...state.phasesCompleted, "identity", "research"])] };
21
+ }
22
+
23
+ /** Record a screenshot: bump the count, then re-evaluate the phase-2 conjunction. */
24
+ export function recordScreenshot(state: DesignState, corpusRequired: boolean): DesignState {
25
+ return maybePhase2({ ...state, screenshotsCount: state.screenshotsCount + 1 }, corpusRequired);
26
+ }
27
+
28
+ /** Record a fuse-browser navigate (resets the scroll-before-screenshot guard). */
29
+ export function recordNavigate(state: DesignState): DesignState {
30
+ return { ...state, scrolledSinceNav: false };
31
+ }
32
+
33
+ /** Record a fuse-browser scroll (satisfies the scroll-before-screenshot guard). */
34
+ export function recordScroll(state: DesignState): DesignState {
35
+ return { ...state, scrolledSinceNav: true };
36
+ }
37
+
38
+ /** Mark the design system validated and advance to phase 3 (after a passing create_frontend check). */
39
+ export function recordValidDesignSystem(state: DesignState): DesignState {
40
+ return {
41
+ ...state, designSystemExists: true, designSystemValid: true,
42
+ currentPhase: Math.max(state.currentPhase, 3),
43
+ phasesCompleted: [...new Set([...state.phasesCompleted, "design-system"])],
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Record a corpus read: distinct files only (a re-read never inflates the
49
+ * list), then re-evaluate the phase-2 conjunction. `relPath` is relative to
50
+ * the corpus root, keeping the persisted state small and prefix-independent.
51
+ */
52
+ export function recordCorpusRead(state: DesignState, relPath: string, corpusRequired: boolean): DesignState {
53
+ return maybePhase2({ ...state, corpusReads: [...new Set([...state.corpusReads, relPath])] }, corpusRequired);
54
+ }
55
+
56
+ /**
57
+ * Record a skill-file Read: reading the identity templates enters phase 1 (browsing
58
+ * allowed); reading the inspiration catalog satisfies the browse prerequisite.
59
+ */
60
+ export function recordRead(state: DesignState, filePath: string, corpusRequired = false): DesignState {
61
+ const next: DesignState = { ...state };
62
+ if (filePath.includes("design-system/SKILL.md")) {
63
+ next.currentPhase = Math.max(state.currentPhase, 1);
64
+ next.phasesCompleted = [...new Set([...state.phasesCompleted, "identity"])];
65
+ }
66
+ if (filePath.includes("design-inspiration")) next.inspirationRead = true;
67
+ return maybePhase2(next, corpusRequired);
68
+ }
@@ -0,0 +1,32 @@
1
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import type { ProjectType } from "./detect-project";
4
+
5
+ /**
6
+ * Detect the project type from the cwd, reproducing the legacy Python logic:
7
+ * package.json containing "next" → nextjs, else "react" → react; else
8
+ * composer.json+artisan → laravel; else Package.swift / *.xcodeproj → swift;
9
+ * else generic.
10
+ * @param cwd - Project root to scan.
11
+ * @returns The detected project type label.
12
+ */
13
+ export function detectClaudeMdProjectType(cwd: string): ProjectType {
14
+ const pkg = join(cwd, "package.json");
15
+ if (existsSync(pkg)) {
16
+ try {
17
+ const content = readFileSync(pkg, "utf-8");
18
+ if (content.includes("next")) return "nextjs";
19
+ if (content.includes("react")) return "react";
20
+ } catch {
21
+ /* unreadable package.json → fall through */
22
+ }
23
+ }
24
+ if (existsSync(join(cwd, "composer.json")) && existsSync(join(cwd, "artisan"))) return "laravel";
25
+ if (existsSync(join(cwd, "Package.swift"))) return "swift";
26
+ try {
27
+ if (readdirSync(cwd).some((f) => f.endsWith(".xcodeproj"))) return "swift";
28
+ } catch {
29
+ /* unreadable dir → ignore */
30
+ }
31
+ return "generic";
32
+ }
@@ -0,0 +1,99 @@
1
+ import { dirname, resolve } from "node:path";
2
+ import { maskCommentsAndStrings } from "./conventions/strip";
3
+ import { nearestManifestDir, projectCaps, type Cap } from "./nearest-manifest";
4
+
5
+ /** File-derived signal: a definitive non-JS framework, or a JS-family hint. */
6
+ interface FileSignal {
7
+ /** Definitive framework from extension/content (non-JS) — wins outright. */
8
+ definitive?: string;
9
+ /** JS-family hint needing project caps to confirm (react/nextjs/tanstack-start). */
10
+ jsHint?: "react" | "nextjs" | "tanstack-start";
11
+ }
12
+
13
+ /** react markers in a plain .ts/.js file (avoids TS-generic false positives). */
14
+ const REACT_CONTENT = /from ['"]react['"]|\buse(State|Effect|Context|Ref|Memo|Callback|Reducer|LayoutEffect)\b|className=/;
15
+ /** next.js markers in content. */
16
+ const NEXT_CONTENT = /use client|use server|NextRequest|NextResponse|from ['"]next|getServerSideProps|getStaticProps/;
17
+ /** next.js route-file conventions. */
18
+ const NEXT_ROUTE = /(page|layout|loading|error|route|middleware)\.(ts|tsx|js|jsx)$/;
19
+ /** TanStack Start markers (file routes, server fns, start/router imports). */
20
+ const TANSTACK_CONTENT = /createFileRoute|createServerFn|from ['"]@tanstack\/react-(start|router)/;
21
+ /**
22
+ * C7: Tailwind CSS-file signals, verified against official docs (v3 + v4) —
23
+ * `@tailwind` (v3 layer directive, removed in v4), `@apply` (both versions),
24
+ * `@config` (v3 native / v4 compat, JS config path), `@source` (v4 content
25
+ * globbing), `@theme` (v4 design-token block), `theme(` (the function, both
26
+ * versions' syntax). Deliberately NOT a bare utility-class-in-selector scan
27
+ * (e.g. `.flex{}`) — an ordinary hand-written class named `.flex` is exactly
28
+ * the false-positive shape this fix removes; only genuine Tailwind-specific
29
+ * at-rules/functions count.
30
+ */
31
+ const TAILWIND_CSS_CONTENT = /@tailwind\b|@apply\b|@config\b|@source\b|@theme\b|\btheme\(/;
32
+
33
+ /** Derive the raw signal an extension + content carry (no filesystem access). */
34
+ function fileSignal(filePath: string, content: string): FileSignal {
35
+ if (/\.swift$/.test(filePath)) return { definitive: "swift" };
36
+ if (/\.php$/.test(filePath)) return { definitive: "laravel" };
37
+ if (/\.java$/.test(filePath)) return { definitive: "java" };
38
+ if (/\.go$/.test(filePath)) return { definitive: "go" };
39
+ if (/\.rb$/.test(filePath)) return { definitive: "ruby" };
40
+ if (/\.rs$/.test(filePath)) return { definitive: "rust" };
41
+ // C7: a .css file is "tailwind" only when its CONTENT shows it — bare
42
+ // extension used to classify EVERY .css as tailwind, forcing 6 unrelated
43
+ // skill reads on hand-written CSS. Empty content (file-size-scope.ts's
44
+ // caller) can never prove Tailwind usage, so it falls through to the
45
+ // JS-family checks below (all false for .css) and returns "generic" — the
46
+ // function's existing fail-open contract, not a special case. That caller
47
+ // never reaches here in practice: .css is already outside its
48
+ // FILE_SIZE_CODE_EXT scope, so this is inert there either way.
49
+ if (/\.css$/.test(filePath) && TAILWIND_CSS_CONTENT.test(content)) return { definitive: "tailwind" };
50
+ // JS branch: content signals ride MASKED content (a `// migrated from
51
+ // createServerFn pattern` comment must not flip nextjs → tanstack-start).
52
+ const masked = maskCommentsAndStrings(content, "c");
53
+ const isTs = TANSTACK_CONTENT.test(masked);
54
+ const isNext = !isTs && (NEXT_ROUTE.test(filePath) || NEXT_CONTENT.test(masked));
55
+ if (/\.(tsx|jsx)$/.test(filePath)) return { jsHint: isTs ? "tanstack-start" : isNext ? "nextjs" : "react" };
56
+ if (/\.(ts|js)$/.test(filePath)) {
57
+ if (isTs) return { jsHint: "tanstack-start" };
58
+ if (isNext) return { jsHint: "nextjs" };
59
+ if (REACT_CONTENT.test(masked)) return { jsHint: "react" };
60
+ }
61
+ return {};
62
+ }
63
+
64
+ /** Reconcile the JS signal against the project's REAL capabilities. */
65
+ function reconcile(caps: Set<Cap>, hint: "react" | "nextjs" | "tanstack-start"): string {
66
+ const nextCap = caps.has("nextjs");
67
+ const reactCap = caps.has("react") || nextCap; // next always ships react at runtime
68
+ if (hint === "tanstack-start") {
69
+ return caps.has("tanstack-start") || caps.has("tanstack-router") ? "tanstack-start" : reactCap ? "react" : "generic";
70
+ }
71
+ if (hint === "nextjs") return nextCap ? "nextjs" : reactCap ? "react" : "generic";
72
+ return reactCap ? "react" : "generic";
73
+ }
74
+
75
+ /**
76
+ * Detect a file's framework as the intersection of the REAL project (its nearest
77
+ * manifest's capabilities) and the file's own extension/content signal. A
78
+ * backend `.ts` in a react project is `generic` (no react signal) — fixing the
79
+ * old extension-only default that flagged every `.ts` as react. Fail-open: any
80
+ * error yields `"generic"`, never throws. Return values stay within the label
81
+ * union ("react" | "nextjs" | "tanstack-start" | "laravel" | "swift" |
82
+ * "tailwind" | "java" | "go" | "ruby" | "rust" | "generic") — `tanstack-start`
83
+ * added 2026-07 (owner spec: first-class framework, Next.js-equivalent).
84
+ * @param filePath - Path of the file being written/edited.
85
+ * @param content - Its (incoming) content, for JS content signals.
86
+ * @param cwd - Root to resolve a relative `filePath` against (default cwd).
87
+ * @returns A framework label from the union.
88
+ */
89
+ export function detectFramework(filePath: string, content: string, cwd: string = process.cwd()): string {
90
+ try {
91
+ const sig = fileSignal(filePath, content);
92
+ if (sig.definitive) return sig.definitive;
93
+ if (!sig.jsHint) return "generic";
94
+ const caps = projectCaps(nearestManifestDir(dirname(resolve(cwd, filePath))));
95
+ return reconcile(caps, sig.jsHint);
96
+ } catch {
97
+ return "generic";
98
+ }
99
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Detection checks for Radix UI vs Base UI primitives, ported verbatim from
3
+ * `detect_primitive_checks.py` (the shadcn-expert plugin's standalone
4
+ * detection script — not wired into any hook; called on demand by the agent
5
+ * to decide which shadcn-detection reference to read).
6
+ */
7
+ import { existsSync, readFileSync, statSync } from "node:fs";
8
+ import { execFileSync } from "node:child_process";
9
+ import { join } from "node:path";
10
+
11
+ /** Package manager inferred from the project's lockfile. */
12
+ export type PackageManager = "bun" | "pnpm" | "yarn" | "npm";
13
+
14
+ /** `grep -rq <pattern> <path>`; true on a match, false on no-match/timeout/error. */
15
+ function grepQuiet(pattern: string, path: string): boolean {
16
+ try {
17
+ execFileSync("grep", ["-rq", pattern, path], { timeout: 10_000, stdio: "ignore" });
18
+ return true;
19
+ } catch {
20
+ return false;
21
+ }
22
+ }
23
+
24
+ /** Check `package.json` for UI library deps (weight: 40). Returns `[radix, baseui]`. */
25
+ export function checkPkgJson(root: string, signals: string[]): [number, number] {
26
+ const pkg = join(root, "package.json");
27
+ if (!existsSync(pkg)) return [0, 0];
28
+ let radix = 0;
29
+ let baseui = 0;
30
+ try {
31
+ const content = readFileSync(pkg, "utf8");
32
+ if (content.includes('"@radix-ui/react-')) { radix = 40; signals.push("pkg:radix-ui"); }
33
+ if (content.includes('"@base-ui/react')) { baseui = 40; signals.push("pkg:base-ui"); }
34
+ } catch {
35
+ return [0, 0];
36
+ }
37
+ return [radix, baseui];
38
+ }
39
+
40
+ /** Check `components.json` style (weight: 20). Returns `[radix, baseui]`. */
41
+ export function checkComponentsJson(root: string, signals: string[]): [number, number] {
42
+ const cjson = join(root, "components.json");
43
+ if (!existsSync(cjson)) return [0, 0];
44
+ try {
45
+ const data = JSON.parse(readFileSync(cjson, "utf8")) as { style?: string };
46
+ const style = data.style ?? "";
47
+ if (style === "new-york" || style === "default") {
48
+ signals.push(`style:${style}`);
49
+ return [20, 0];
50
+ }
51
+ if (style === "base-vega") {
52
+ signals.push("style:base-vega");
53
+ return [0, 20];
54
+ }
55
+ } catch {
56
+ return [0, 0];
57
+ }
58
+ return [0, 0];
59
+ }
60
+
61
+ /** Dirs scanned for import/data-attribute signals. */
62
+ const SCAN_DIRS: ReadonlyArray<string> = ["src", "components", "app"];
63
+
64
+ function existingScanDirs(root: string): string[] {
65
+ return SCAN_DIRS.map((d) => join(root, d)).filter((d) => {
66
+ try { return statSync(d).isDirectory(); } catch { return false; }
67
+ });
68
+ }
69
+
70
+ /** Scan imports (weight: 25) and data attributes (weight: 15). Returns `[radix, baseui]`. */
71
+ export function scanImportsAndAttrs(root: string, signals: string[]): [number, number] {
72
+ const dirs = existingScanDirs(root);
73
+ let radix = 0;
74
+ let baseui = 0;
75
+ if (dirs.some((d) => grepQuiet("@radix-ui/react-", d))) { radix += 25; signals.push("import:radix"); }
76
+ if (dirs.some((d) => grepQuiet("@base-ui/react", d))) { baseui += 25; signals.push("import:base-ui"); }
77
+ if (dirs.some((d) => grepQuiet("data-state=", d))) { radix += 15; signals.push("attr:data-state"); }
78
+ if (dirs.some((d) => grepQuiet("data-\\[open\\]", d))) { baseui += 15; signals.push("attr:data-[open]"); }
79
+ return [radix, baseui];
80
+ }
81
+
82
+ /** Lockfile → package manager + runner, checked in priority order. */
83
+ const LOCKFILES: ReadonlyArray<readonly [string, PackageManager, string]> = [
84
+ ["bun.lockb", "bun", "bunx"],
85
+ ["bun.lock", "bun", "bunx"],
86
+ ["pnpm-lock.yaml", "pnpm", "pnpm dlx"],
87
+ ["yarn.lock", "yarn", "yarn dlx"],
88
+ ];
89
+
90
+ /** Detect the package manager from lockfiles present at `root` (defaults to npm/npx). */
91
+ export function detectPm(root: string, signals: string[]): [PackageManager, string] {
92
+ for (const [lockfile, pm, runner] of LOCKFILES) {
93
+ if (existsSync(join(root, lockfile))) {
94
+ signals.push(`pm:${pm}`);
95
+ return [pm, runner];
96
+ }
97
+ }
98
+ signals.push("pm:npm");
99
+ return ["npm", "npx"];
100
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Compose the Radix vs Base UI weighted detection, ported verbatim from
3
+ * `detect-primitive-lib.py`'s `main()`. Pure aggregation over
4
+ * {@link checkPkgJson}/{@link checkComponentsJson}/{@link scanImportsAndAttrs}/
5
+ * {@link detectPm} (`./detect-primitive-checks`).
6
+ */
7
+ import { checkComponentsJson, checkPkgJson, detectPm, scanImportsAndAttrs, type PackageManager } from "./detect-primitive-checks";
8
+
9
+ export type { PackageManager } from "./detect-primitive-checks";
10
+
11
+ /** Which shadcn/ui primitive library the project uses, per the weighted signal score. */
12
+ export type PrimitiveLib = "radix" | "base-ui" | "mixed" | "none";
13
+
14
+ /** Result of {@link detectPrimitiveLib}. */
15
+ export interface PrimitiveDetection {
16
+ primitive: PrimitiveLib;
17
+ confidence: number;
18
+ pm: PackageManager;
19
+ runner: string;
20
+ signals: string[];
21
+ }
22
+
23
+ /**
24
+ * Detect whether `root` uses Radix UI or Base UI as its shadcn/ui primitive,
25
+ * from weighted signals (package.json 40, components.json style 20, imports 25,
26
+ * data attributes 15). `"mixed"` when both accumulate signal, `"none"` when
27
+ * neither does.
28
+ * @param root - project root to scan (defaults to `process.cwd()`).
29
+ */
30
+ export function detectPrimitiveLib(root: string = process.cwd()): PrimitiveDetection {
31
+ const signals: string[] = [];
32
+ const [r1, b1] = checkPkgJson(root, signals);
33
+ const [r2, b2] = checkComponentsJson(root, signals);
34
+ const [r3, b3] = scanImportsAndAttrs(root, signals);
35
+ const [pm, runner] = detectPm(root, signals);
36
+ const radix = r1 + r2 + r3;
37
+ const baseui = b1 + b2 + b3;
38
+
39
+ let primitive: PrimitiveLib;
40
+ let confidence: number;
41
+ if (radix > 0 && baseui > 0) {
42
+ primitive = "mixed";
43
+ confidence = Math.floor((radix + baseui) / 2);
44
+ } else if (radix > baseui) {
45
+ primitive = "radix";
46
+ confidence = radix;
47
+ } else if (baseui > radix) {
48
+ primitive = "base-ui";
49
+ confidence = baseui;
50
+ } else {
51
+ primitive = "none";
52
+ confidence = 0;
53
+ }
54
+ return { primitive, confidence, pm, runner, signals };
55
+ }
@@ -0,0 +1,99 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ /** Project types detected from filesystem indicators. */
5
+ export type ProjectType =
6
+ | "nextjs" | "nuxt" | "angular" | "svelte" | "vue" | "react" | "tailwind"
7
+ | "laravel" | "rails" | "django" | "python" | "go" | "rust" | "swift"
8
+ | "java" | "scala" | "elixir" | "ruby" | "generic";
9
+
10
+ /** Keywords that signal a development task (APEX trigger). */
11
+ export const DEV_KEYWORDS: RegExp =
12
+ /\b(implement|create|build|fix|add|refactor|develop|feature|bug|update|modify|change|write|code)\b/i;
13
+
14
+ /** True when the prompt invokes the /apex command. */
15
+ export function isApexCommand(prompt: string): boolean {
16
+ return /(?:^|\s)\/apex|\/fuse-ai-pilot:apex/i.test(prompt);
17
+ }
18
+
19
+ /** Modular architecture variants layered on top of the framework. */
20
+ export type ModularArchitecture = "fusecore" | "nextjs-modular" | null;
21
+
22
+ /**
23
+ * Detect a project-internal modular architecture (a sub-architecture the
24
+ * framework-level {@link detectProjectType} doesn't capture): Fusengine's
25
+ * FuseCore (Laravel) or a `modules/`-based Next.js layout.
26
+ */
27
+ export function detectModularArchitecture(dir: string): ModularArchitecture {
28
+ const has = (f: string): boolean => existsSync(join(dir, f));
29
+ if (has("FuseCore") && has("artisan")) return "fusecore";
30
+ if (has("modules") && (has("next.config.js") || has("next.config.ts") || has("next.config.mjs"))) {
31
+ return "nextjs-modular";
32
+ }
33
+ return null;
34
+ }
35
+
36
+ /**
37
+ * Resolve the skill a detected modular architecture forces.
38
+ *
39
+ * Ports the Python `check-nextjs-skill.py` / `check-laravel-skill.py` gates:
40
+ * when the project is detected on disk as a modular architecture, a specific
41
+ * skill is required ('solid-nextjs' for nextjs-modular, 'fusecore' for
42
+ * fusecore). Returns `null` when no modular architecture is detected.
43
+ *
44
+ * @param cwd - Project root directory to scan.
45
+ * @returns The forced skill name, or `null` when none applies.
46
+ */
47
+ export function requiredArchSkill(cwd: string): string | null {
48
+ switch (detectModularArchitecture(cwd)) {
49
+ case "nextjs-modular":
50
+ return "solid-nextjs";
51
+ case "fusecore":
52
+ return "fusecore";
53
+ default:
54
+ return null;
55
+ }
56
+ }
57
+
58
+ /** Config file names that mark a project as using Tailwind (v3 JS config or legacy). */
59
+ const TAILWIND_CONFIG_FILES = ["tailwind.config.js", "tailwind.config.ts", "tailwind.config.mjs", "tailwind.config.cjs"];
60
+
61
+ /** True when `package.json` lists `tailwindcss` as a dep (Tailwind v4 CSS-first, no config file). */
62
+ function hasTailwindDependency(dir: string): boolean {
63
+ const pkgPath = join(dir, "package.json");
64
+ if (!existsSync(pkgPath)) return false;
65
+ try {
66
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as {
67
+ dependencies?: Record<string, string>;
68
+ devDependencies?: Record<string, string>;
69
+ };
70
+ return "tailwindcss" in { ...pkg.dependencies, ...pkg.devDependencies };
71
+ } catch {
72
+ return false;
73
+ }
74
+ }
75
+
76
+ /** Detect the project type by scanning config files in `dir`. */
77
+ export function detectProjectType(dir: string): ProjectType {
78
+ const has = (f: string): boolean => existsSync(join(dir, f));
79
+ if (has("next.config.js") || has("next.config.ts") || has("next.config.mjs")) return "nextjs";
80
+ if (has("nuxt.config.ts") || has("nuxt.config.js")) return "nuxt";
81
+ if (has("angular.json")) return "angular";
82
+ if (has("svelte.config.js") || has("svelte.config.ts")) return "svelte";
83
+ if (has("vite.config.ts") && has("src/App.vue")) return "vue";
84
+ if (has("vite.config.ts") || has("vite.config.js")) return "react";
85
+ if (TAILWIND_CONFIG_FILES.some(has) || hasTailwindDependency(dir)) return "tailwind";
86
+ if (has("composer.json") && has("artisan")) return "laravel";
87
+ if (has("Gemfile") && has("config/routes.rb")) return "rails";
88
+ if (has("requirements.txt") || has("pyproject.toml") || has("setup.py")) {
89
+ return has("manage.py") ? "django" : "python";
90
+ }
91
+ if (has("go.mod")) return "go";
92
+ if (has("Cargo.toml")) return "rust";
93
+ if (has("Package.swift")) return "swift";
94
+ if (has("pom.xml") || has("build.gradle") || has("build.gradle.kts")) return "java";
95
+ if (has("build.sbt")) return "scala";
96
+ if (has("mix.exs")) return "elixir";
97
+ if (has("Gemfile")) return "ruby";
98
+ return "generic";
99
+ }
@@ -0,0 +1,53 @@
1
+ import { countLines } from "./file-size";
2
+
3
+ /**
4
+ * Count non-overlapping literal occurrences of `needle` in `haystack`. An
5
+ * empty `needle` returns 0 — treated as "not found" by callers, since Edit's
6
+ * `old_string` is never legitimately empty and an empty needle has no
7
+ * well-defined occurrence count.
8
+ * @param haystack - The on-disk file content to search.
9
+ * @param needle - The literal substring to count.
10
+ */
11
+ function countOccurrences(haystack: string, needle: string): number {
12
+ if (needle === "") return 0;
13
+ let count = 0;
14
+ let from = 0;
15
+ for (;;) {
16
+ const at = haystack.indexOf(needle, from);
17
+ if (at === -1) break;
18
+ count++;
19
+ from = at + needle.length;
20
+ }
21
+ return count;
22
+ }
23
+
24
+ /**
25
+ * Compute the line count an Edit would PRODUCE, before it lands on disk —
26
+ * `existingLines − countLines(old_string) + countLines(new_string)`, scaled
27
+ * by how many `old_string` occurrences are actually replaced (1 normally, or
28
+ * every occurrence under `replace_all`). Pure arithmetic on the literal
29
+ * strings — no regex/fuzzy matching, matching the real Edit tool's own
30
+ * exact-substring semantics.
31
+ *
32
+ * Returns `null` (fail-closed — caller must fall back to judging the stale
33
+ * on-disk count alone) when `old_string` is missing/empty, or does not occur
34
+ * in `existingContent` at all: an Edit whose `old_string` doesn't match the
35
+ * real file is stale or adversarial, never a case to special-case an allow for.
36
+ * @param existingContent - Full on-disk content of the file being edited.
37
+ * @param oldString - The Edit's `old_string` (tool_input, unmodified).
38
+ * @param newString - The Edit's `new_string` (tool_input, unmodified).
39
+ * @param replaceAll - The Edit's `replace_all` flag.
40
+ */
41
+ export function computeEditResultLines(
42
+ existingContent: string,
43
+ oldString: string | undefined,
44
+ newString: string,
45
+ replaceAll: boolean,
46
+ ): number | null {
47
+ if (!oldString) return null;
48
+ const occurrences = countOccurrences(existingContent, oldString);
49
+ if (occurrences === 0) return null;
50
+ const times = replaceAll ? occurrences : 1;
51
+ const delta = countLines(newString) - countLines(oldString);
52
+ return countLines(existingContent) + delta * times;
53
+ }
@@ -0,0 +1,81 @@
1
+ import { isFileSizeScoped, resolveSolidRefFramework } from "./file-size-scope";
2
+ import { countLines, evaluateFileSize } from "./file-size";
3
+ import { computeEditResultLines } from "./edit-outcome";
4
+ import { resolveMaxLines } from "../config/limits";
5
+ import { matchPatterns, GIT_BLOCKED, RALPH_SAFE, isRalphMode } from "./patterns";
6
+ import { runGuards } from "./guards";
7
+ import { isSingleCommand, buildNeverApprovalPrompt, NEVER_SAFE, isSafePushForm } from "./never-approval";
8
+ import { evaluateGitGates } from "./git-gates";
9
+ import type { PolicyContext, PolicyResult } from "./interfaces/types";
10
+
11
+ export type { PolicyContext, PolicyResult } from "./interfaces/types";
12
+
13
+ /**
14
+ * Evaluate a single tool-use against the bundled policies, returning a pure
15
+ * decision plus a portable {@link Prompt}. Adapters translate the prompt into
16
+ * their harness's native response (Claude `permissionDecision`, etc.).
17
+ */
18
+ export function evaluate(ctx: PolicyContext): PolicyResult {
19
+ const guard = runGuards(ctx);
20
+ if (guard) return { decision: "deny", message: guard.reason, prompt: guard };
21
+ // Ralph mode (opt-in via RALPH_MODE) exempts the SAFE git commands from the git
22
+ // block/ask gates for autonomous runs — parity git-guard.py:48-51. Destructive
23
+ // commands are NOT in RALPH_SAFE, so force-push / reset --hard stay blocked.
24
+ const cmd = ctx.command;
25
+ const ralphSafe = !!cmd && isRalphMode() && RALPH_SAFE.some((s) => cmd.startsWith(s));
26
+ // Codex `approval_policy=never` has no ask channel: for the NEVER_SAFE subset
27
+ // (RALPH_SAFE + "git push"), never destructive (GIT_BLOCKED) and never chained
28
+ // (isSingleCommand — `git commit -m x && git push --force` must NOT slip
29
+ // through), auto-approve with a visible `warn` instead of falling through to
30
+ // GIT_ASK's normal `ask`. isSafePushForm additionally narrows the "git push"
31
+ // entry: it excludes --delete/--mirror/--prune and a `:refspec` remote-delete,
32
+ // none of which GIT_BLOCKED catches (that gate only covers --force). RALPH_MODE
33
+ // takes priority (ralphSafe already silent-allows above this) — its own path
34
+ // stays on RALPH_SAFE, unaffected by this push exemption.
35
+ const neverExempt = !ralphSafe && !!ctx.neverApproval && !!cmd && !matchPatterns(cmd, GIT_BLOCKED) && isSingleCommand(cmd) && NEVER_SAFE.some((s) => cmd.startsWith(s)) && isSafePushForm(cmd);
36
+ if (neverExempt && cmd) {
37
+ const prompt = buildNeverApprovalPrompt(cmd);
38
+ return { decision: "warn", message: prompt.reason, prompt };
39
+ }
40
+ const gitGate = evaluateGitGates(ctx.command, ralphSafe);
41
+ if (gitGate) return gitGate;
42
+ if (ctx.filePath && isFileSizeScoped(ctx.filePath) && ctx.agentType !== "Explore" && ctx.agentType !== "Plan") {
43
+ const incoming = ctx.content !== undefined ? countLines(ctx.content) : 0;
44
+ const existing = ctx.existingLines ?? 0;
45
+ const max = ctx.maxLines ?? resolveMaxLines();
46
+ // A computable Edit outcome bypasses the on-disk-only ceiling below: either
47
+ // the result itself is compliant, or the file was already oversized and
48
+ // this Edit strictly shrinks it (see policy/edit-outcome.ts). Fails closed
49
+ // to the ceiling whenever the outcome isn't computable.
50
+ const editResult = ctx.tool === "Edit" && ctx.existingContent !== undefined && ctx.content !== undefined
51
+ ? computeEditResultLines(ctx.existingContent, ctx.oldString, ctx.content, ctx.isReplaceAll === true)
52
+ : null;
53
+ if (editResult !== null && (editResult <= max || editResult < existing)) {
54
+ return { decision: "allow", message: null };
55
+ }
56
+ // Write provides the full new content → judge it. Edit judges the computed
57
+ // outcome when known (closes the old grow-via-Edit hole), else the on-disk file.
58
+ const lines = ctx.tool === "Edit" ? (editResult ?? Math.max(incoming, existing)) : incoming || existing;
59
+ const framework = resolveSolidRefFramework(ctx.filePath);
60
+ // Python parity (enforce-file-size.py:44-57): the block message always
61
+ // reports the pre-existing on-disk count, even for a Write whose new
62
+ // content is itself over the limit — the incoming count only ever gates
63
+ // an early "shrunk to compliant" allow, it's never what gets displayed.
64
+ const displayLines = ctx.tool === "Write" ? ctx.existingLines ?? lines : lines;
65
+ const verdict = evaluateFileSize(lines, max, ctx.filePath, framework, displayLines);
66
+ if (lines > 0 && !verdict.ok) {
67
+ return {
68
+ decision: "deny",
69
+ message: verdict.message,
70
+ prompt: {
71
+ kind: "block",
72
+ title: "SOLID file-size limit",
73
+ reason: verdict.message ?? "",
74
+ actions: [`Split into modules under ${verdict.max} lines (Single Responsibility)`, "Then re-run the write"],
75
+ },
76
+ meta: { framework, lines: verdict.lines, max: verdict.max },
77
+ };
78
+ }
79
+ }
80
+ return { decision: "allow", message: null };
81
+ }