@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,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
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Dynamic expert-agent resolution (fs). Replaces a hardcoded agent-id table:
3
+ * scans installed marketplace plugins for an `agents/*.md` whose frontmatter
4
+ * `name` matches the detected {@link ProjectType}, returning the real
5
+ * `<plugin>:<agent>` id — never a fictional one absent from disk.
6
+ *
7
+ * Reuses the cartographer's own fs primitives ({@link findMarketplacePlugins},
8
+ * {@link readPluginMeta}, {@link scanAgents}) — the same shallow,
9
+ * non-recursive scan already run uncached on every SessionStart
10
+ * (`generateEcosystemMap`), scoped here to just `agents/*.md` (skips
11
+ * skills/commands/hooks) to keep the per-prompt cost minimal.
12
+ */
13
+ import { readdirSync, statSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ import { findMarketplacePlugins, readPluginMeta } from "../runtime/lifecycle/cartographer/detect";
16
+ import { scanAgents } from "../runtime/lifecycle/cartographer/scan";
17
+ import type { ProjectType } from "./detect-project";
18
+
19
+ /** Sorted plugin-dir names directly under `pluginsDir` (dirs only, alpha), or `[]`. */
20
+ function pluginDirs(pluginsDir: string): string[] {
21
+ let names: string[];
22
+ try {
23
+ names = readdirSync(pluginsDir);
24
+ } catch {
25
+ return [];
26
+ }
27
+ return names
28
+ .filter((n) => !n.startsWith(".") && !n.startsWith("_"))
29
+ .filter((n) => {
30
+ try {
31
+ return statSync(join(pluginsDir, n)).isDirectory();
32
+ } catch {
33
+ return false;
34
+ }
35
+ })
36
+ .sort((a, b) => a.localeCompare(b, "en"));
37
+ }
38
+
39
+ /**
40
+ * Resolve the real, installed expert-agent id for `type`.
41
+ * @param type - Detected project type.
42
+ * @param pluginsDirOverride - Override for the marketplace plugins dir
43
+ * (tests); defaults to the auto-detected marketplace root.
44
+ * @returns `<plugin>:<agent>` for the first installed plugin whose agent name
45
+ * starts with `type`, or "general-purpose" when none is installed.
46
+ */
47
+ export function getExpertAgent(type: ProjectType, pluginsDirOverride?: string): string {
48
+ const pluginsDir = pluginsDirOverride ?? findMarketplacePlugins();
49
+ for (const dir of pluginDirs(pluginsDir)) {
50
+ const pluginPath = join(pluginsDir, dir);
51
+ const agent = scanAgents(pluginPath)
52
+ .map(([, name]) => name)
53
+ .find((name) => name.toLowerCase().startsWith(type.toLowerCase()));
54
+ if (agent) {
55
+ const [, pkgName] = readPluginMeta(pluginPath);
56
+ return `${pkgName || dir}:${agent}`;
57
+ }
58
+ }
59
+ return "general-purpose";
60
+ }