@fusengine/harness 0.1.90 → 0.1.92

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 (422) 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-BF1dZFjY.mjs} +1194 -144
  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-Dy8g9Ybl.mjs +397 -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 +37 -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-budget.ts +144 -0
  44. package/src/adapters/cursor/context-limit.ts +115 -0
  45. package/src/adapters/cursor/context.ts +65 -0
  46. package/src/adapters/cursor/events.ts +38 -0
  47. package/src/adapters/cursor/index.ts +43 -0
  48. package/src/adapters/cursor/interfaces/context-budget.ts +25 -0
  49. package/src/adapters/cursor/interfaces/types.ts +88 -0
  50. package/src/adapters/cursor/native-response.ts +39 -0
  51. package/src/adapters/cursor/native-schemas.ts +161 -0
  52. package/src/adapters/cursor/normalize.ts +188 -0
  53. package/src/adapters/cursor/plugin-root.ts +103 -0
  54. package/src/adapters/cursor/respond.ts +170 -0
  55. package/src/adapters/gemini/index.ts +36 -0
  56. package/src/adapters/hermes/index.ts +55 -0
  57. package/src/adapters/hermes/interfaces/types.ts +26 -0
  58. package/src/adapters/kimi/index.ts +96 -0
  59. package/src/adapters/kimi/interfaces/types.ts +55 -0
  60. package/src/cache/compact.ts +55 -0
  61. package/src/cache/index.ts +5 -0
  62. package/src/cache/io.ts +38 -0
  63. package/src/cache/mcp-response.ts +39 -0
  64. package/src/cache/mcp-store.ts +70 -0
  65. package/src/cache/store.ts +93 -0
  66. package/src/changelog/fetch.ts +79 -0
  67. package/src/cli/bin.ts +100 -0
  68. package/src/cli/cursor-event-scanner.ts +175 -0
  69. package/src/cli/cursor-stdin-reader.ts +65 -0
  70. package/src/cli/doctor.ts +96 -0
  71. package/src/cli/hook-io.ts +130 -0
  72. package/src/cli/hook-sound.ts +49 -0
  73. package/src/cli/index.ts +1 -0
  74. package/src/cli/json-primitive-scanner.ts +83 -0
  75. package/src/cli/run.ts +36 -0
  76. package/src/cli/scope.ts +31 -0
  77. package/src/codex-rules/build.ts +63 -0
  78. package/src/codex-rules/index.ts +8 -0
  79. package/src/codex-rules/mapping.ts +42 -0
  80. package/src/codex-rules/rules/absolute-paths.ts +52 -0
  81. package/src/codex-rules/rules/ask.ts +44 -0
  82. package/src/codex-rules/rules/chmod-ask.ts +57 -0
  83. package/src/codex-rules/rules/dd.ts +23 -0
  84. package/src/codex-rules/rules/diskutil.ts +33 -0
  85. package/src/codex-rules/rules/mkfs.ts +42 -0
  86. package/src/codex-rules/rules/privilege.ts +49 -0
  87. package/src/codex-rules/rules/remote-fetch.ts +31 -0
  88. package/src/codex-rules/rules/rm-variants.ts +58 -0
  89. package/src/codex-rules/rules/single-commands.ts +57 -0
  90. package/src/codex-rules/skip-list.ts +25 -0
  91. package/src/codex-rules/starlark.ts +46 -0
  92. package/src/codex-rules/types.ts +36 -0
  93. package/src/config/dotenv.ts +55 -0
  94. package/src/config/env.ts +10 -0
  95. package/src/config/home-dir.ts +36 -0
  96. package/src/config/index.ts +5 -0
  97. package/src/config/layout.ts +52 -0
  98. package/src/config/limits.ts +56 -0
  99. package/src/config/ttl.ts +30 -0
  100. package/src/detect/harness.ts +73 -0
  101. package/src/detect/index.ts +1 -0
  102. package/src/detect/interfaces/types.ts +18 -0
  103. package/src/freshness/agent-evidence-record.ts +95 -0
  104. package/src/freshness/agent-evidence.ts +95 -0
  105. package/src/freshness/codex-spawn-evidence.ts +91 -0
  106. package/src/freshness/doc-helpers.ts +90 -0
  107. package/src/freshness/evidence-harvest-io.ts +72 -0
  108. package/src/freshness/evidence-harvest.ts +61 -0
  109. package/src/freshness/explore-tools.ts +95 -0
  110. package/src/freshness/index.ts +2 -0
  111. package/src/freshness/query-framework.ts +73 -0
  112. package/src/freshness/ref-evidence.ts +49 -0
  113. package/src/freshness/ref-journal.ts +98 -0
  114. package/src/freshness/trivial-edit-counter.ts +30 -0
  115. package/src/index.ts +11 -0
  116. package/src/init/index.ts +2 -0
  117. package/src/init/run.ts +32 -0
  118. package/src/init/templates.ts +75 -0
  119. package/src/memory/gitignore.ts +14 -0
  120. package/src/memory/index.ts +3 -0
  121. package/src/memory/registry.ts +39 -0
  122. package/src/memory/session-roots.ts +99 -0
  123. package/src/memory/state.ts +58 -0
  124. package/src/policy/apex-authorization.ts +100 -0
  125. package/src/policy/apex-gates.ts +91 -0
  126. package/src/policy/apex-target-steps.ts +73 -0
  127. package/src/policy/apex-target.ts +49 -0
  128. package/src/policy/apex-task-context.ts +81 -0
  129. package/src/policy/apex.ts +94 -0
  130. package/src/policy/cartographer/build-tree.ts +69 -0
  131. package/src/policy/cartographer/describe.ts +53 -0
  132. package/src/policy/cartographer/entry.ts +38 -0
  133. package/src/policy/cartographer/frontmatter.ts +48 -0
  134. package/src/policy/cartographer/indicators.ts +34 -0
  135. package/src/policy/claude-md-context.ts +66 -0
  136. package/src/policy/conventions/index.ts +11 -0
  137. package/src/policy/conventions/interfaces.ts +71 -0
  138. package/src/policy/conventions/langs.ts +53 -0
  139. package/src/policy/conventions/query.ts +49 -0
  140. package/src/policy/conventions/react-hooks.ts +18 -0
  141. package/src/policy/conventions/store-or-query.ts +21 -0
  142. package/src/policy/conventions/stores.ts +56 -0
  143. package/src/policy/conventions/strip.ts +87 -0
  144. package/src/policy/conventions/verdict.ts +38 -0
  145. package/src/policy/creation-intent.ts +11 -0
  146. package/src/policy/deny-loop.ts +88 -0
  147. package/src/policy/design/allowed-write.ts +28 -0
  148. package/src/policy/design/content-checks.ts +45 -0
  149. package/src/policy/design/corpus-fs.ts +21 -0
  150. package/src/policy/design/corpus-resolve-cache.ts +49 -0
  151. package/src/policy/design/corpus-resolve.ts +99 -0
  152. package/src/policy/design/corpus.ts +92 -0
  153. package/src/policy/design/design-system-rules.ts +45 -0
  154. package/src/policy/design/flag.ts +30 -0
  155. package/src/policy/design/gates-pipeline.ts +105 -0
  156. package/src/policy/design/gates.ts +87 -0
  157. package/src/policy/design/screenshot-tools.ts +24 -0
  158. package/src/policy/design/skill-evidence.ts +34 -0
  159. package/src/policy/design/skill-gate.ts +81 -0
  160. package/src/policy/design/skill-triggers.ts +51 -0
  161. package/src/policy/design/state.ts +92 -0
  162. package/src/policy/design/template-urls.ts +54 -0
  163. package/src/policy/design/transitions.ts +68 -0
  164. package/src/policy/detect-claude-md-project-type.ts +32 -0
  165. package/src/policy/detect-framework.ts +99 -0
  166. package/src/policy/detect-primitive-checks.ts +100 -0
  167. package/src/policy/detect-primitive-lib.ts +55 -0
  168. package/src/policy/detect-project.ts +99 -0
  169. package/src/policy/edit-outcome.ts +53 -0
  170. package/src/policy/evaluate.ts +81 -0
  171. package/src/policy/expert-agents.ts +60 -0
  172. package/src/policy/file-size-scope.ts +32 -0
  173. package/src/policy/file-size.ts +99 -0
  174. package/src/policy/framework-solid-exclude.ts +52 -0
  175. package/src/policy/framework-solid-extended.ts +93 -0
  176. package/src/policy/framework-solid-gates-systems.ts +99 -0
  177. package/src/policy/framework-solid-gates.ts +98 -0
  178. package/src/policy/framework-solid.ts +88 -0
  179. package/src/policy/gemini-mcp-gate.ts +83 -0
  180. package/src/policy/git-gates.ts +40 -0
  181. package/src/policy/guards/bash-command-anchor.ts +40 -0
  182. package/src/policy/guards/bash-write-patterns.ts +140 -0
  183. package/src/policy/guards/bash-write-redirects.ts +201 -0
  184. package/src/policy/guards/bash-write-safe-paths.ts +62 -0
  185. package/src/policy/guards/bash-write.ts +76 -0
  186. package/src/policy/guards/context.ts +12 -0
  187. package/src/policy/guards/index.ts +61 -0
  188. package/src/policy/guards/install.ts +22 -0
  189. package/src/policy/guards/interface-separation-ext.ts +41 -0
  190. package/src/policy/guards/interface-separation.ts +100 -0
  191. package/src/policy/guards/protected-path.ts +100 -0
  192. package/src/policy/guards/security.ts +97 -0
  193. package/src/policy/index.ts +19 -0
  194. package/src/policy/interfaces/types.ts +46 -0
  195. package/src/policy/js-gate-route.ts +51 -0
  196. package/src/policy/lessons/lesson-gate.ts +0 -0
  197. package/src/policy/lessons/trigger-index.ts +84 -0
  198. package/src/policy/lessons/types.ts +31 -0
  199. package/src/policy/module-layout.ts +31 -0
  200. package/src/policy/nearest-manifest.ts +75 -0
  201. package/src/policy/never-approval.ts +95 -0
  202. package/src/policy/patterns.ts +80 -0
  203. package/src/policy/seo/validate.ts +51 -0
  204. package/src/policy/shadcn-project.ts +22 -0
  205. package/src/policy/shadcn-skill-gate.ts +97 -0
  206. package/src/policy/shell-read-refs.ts +96 -0
  207. package/src/policy/skill-path.ts +46 -0
  208. package/src/policy/skill-patterns/laravel.ts +40 -0
  209. package/src/policy/skill-patterns/nextjs.ts +29 -0
  210. package/src/policy/skill-patterns/react.ts +27 -0
  211. package/src/policy/skill-patterns/shadcn-subskills.ts +33 -0
  212. package/src/policy/skill-patterns/shadcn.ts +66 -0
  213. package/src/policy/skill-patterns/swift.ts +30 -0
  214. package/src/policy/skill-patterns/tailwind.ts +40 -0
  215. package/src/policy/skill-source.ts +52 -0
  216. package/src/policy/skill-trigger-patterns.ts +33 -0
  217. package/src/policy/skill-triggers.ts +108 -0
  218. package/src/policy/tailwind-skill-gate.ts +63 -0
  219. package/src/policy/trivial-edits.ts +52 -0
  220. package/src/policy/verbosity.ts +39 -0
  221. package/src/prompt/index.ts +1 -0
  222. package/src/prompt/types.ts +36 -0
  223. package/src/refs/discover.ts +69 -0
  224. package/src/refs/frontmatter.ts +24 -0
  225. package/src/refs/index.ts +4 -0
  226. package/src/refs/loader.ts +59 -0
  227. package/src/refs/ref-key.ts +41 -0
  228. package/src/refs/router.ts +63 -0
  229. package/src/refs/types.ts +25 -0
  230. package/src/runtime/activity.ts +99 -0
  231. package/src/runtime/apply-patch-gate.ts +41 -0
  232. package/src/runtime/burst-window.ts +21 -0
  233. package/src/runtime/codex-shell-tool.ts +37 -0
  234. package/src/runtime/command-string.ts +37 -0
  235. package/src/runtime/confirm/codex-confirm.ts +126 -0
  236. package/src/runtime/confirm/codex-prompt-origin.ts +16 -0
  237. package/src/runtime/confirm/confirm-code.ts +33 -0
  238. package/src/runtime/confirm/confirm-gate.ts +65 -0
  239. package/src/runtime/confirm/confirm-irreversible.ts +32 -0
  240. package/src/runtime/confirm/confirm-pending.ts +34 -0
  241. package/src/runtime/confirm/confirm-state.ts +56 -0
  242. package/src/runtime/confirm/confirm-subagent.ts +112 -0
  243. package/src/runtime/confirm/confirm-submit.ts +38 -0
  244. package/src/runtime/core-guards-active.ts +84 -0
  245. package/src/runtime/core-guards-wired.ts +47 -0
  246. package/src/runtime/deny-loop-store.ts +94 -0
  247. package/src/runtime/deny-notice.ts +52 -0
  248. package/src/runtime/design-cache-resolve.ts +51 -0
  249. package/src/runtime/design-content-gate.ts +95 -0
  250. package/src/runtime/design-files-gate.ts +114 -0
  251. package/src/runtime/design-helpers.ts +93 -0
  252. package/src/runtime/design-lifecycle.ts +39 -0
  253. package/src/runtime/design-read-credit.ts +28 -0
  254. package/src/runtime/design.ts +116 -0
  255. package/src/runtime/dev-context.ts +43 -0
  256. package/src/runtime/dry-patterns.ts +32 -0
  257. package/src/runtime/dry.ts +96 -0
  258. package/src/runtime/fragment-registry.ts +33 -0
  259. package/src/runtime/framework-skill-gate.ts +79 -0
  260. package/src/runtime/fs-cleanup.ts +71 -0
  261. package/src/runtime/gate-apex.ts +81 -0
  262. package/src/runtime/gate-helpers.ts +52 -0
  263. package/src/runtime/gate-input.ts +26 -0
  264. package/src/runtime/gate.ts +128 -0
  265. package/src/runtime/handle-post.ts +104 -0
  266. package/src/runtime/handle-pre.ts +137 -0
  267. package/src/runtime/handle-scope-async.ts +29 -0
  268. package/src/runtime/handle-types.ts +27 -0
  269. package/src/runtime/handle.ts +197 -0
  270. package/src/runtime/home-state.ts +55 -0
  271. package/src/runtime/index.ts +18 -0
  272. package/src/runtime/inform.ts +28 -0
  273. package/src/runtime/inject-budget-recap.ts +47 -0
  274. package/src/runtime/inject-budget.ts +81 -0
  275. package/src/runtime/inject-context.ts +85 -0
  276. package/src/runtime/inject-dedup-exclusive.ts +98 -0
  277. package/src/runtime/inject-dedup.ts +89 -0
  278. package/src/runtime/is-agent-tool.ts +19 -0
  279. package/src/runtime/lifecycle/agent-files.ts +49 -0
  280. package/src/runtime/lifecycle/agent-memory.ts +82 -0
  281. package/src/runtime/lifecycle/agent-transcript.ts +80 -0
  282. package/src/runtime/lifecycle/aipilot/analytics.ts +99 -0
  283. package/src/runtime/lifecycle/aipilot/apex-task-store.ts +61 -0
  284. package/src/runtime/lifecycle/aipilot/cache-base.ts +48 -0
  285. package/src/runtime/lifecycle/aipilot/cache-doc.ts +100 -0
  286. package/src/runtime/lifecycle/aipilot/cache-lessons.ts +58 -0
  287. package/src/runtime/lifecycle/aipilot/cache-test.ts +70 -0
  288. package/src/runtime/lifecycle/aipilot/curate-lessons.ts +64 -0
  289. package/src/runtime/lifecycle/aipilot/dispatch-aipilot.ts +106 -0
  290. package/src/runtime/lifecycle/aipilot/doc-cache-gate.ts +69 -0
  291. package/src/runtime/lifecycle/aipilot/inject-apex.ts +98 -0
  292. package/src/runtime/lifecycle/aipilot/inject-doc.ts +62 -0
  293. package/src/runtime/lifecycle/aipilot/inject-explore.ts +64 -0
  294. package/src/runtime/lifecycle/aipilot/inject-lessons.ts +61 -0
  295. package/src/runtime/lifecycle/aipilot/inject-test.ts +48 -0
  296. package/src/runtime/lifecycle/aipilot/lesson-archive.ts +60 -0
  297. package/src/runtime/lifecycle/aipilot/lesson-inject.ts +98 -0
  298. package/src/runtime/lifecycle/aipilot/lesson-parse.ts +74 -0
  299. package/src/runtime/lifecycle/aipilot/lessons.ts +72 -0
  300. package/src/runtime/lifecycle/aipilot/promote-global-lessons.ts +92 -0
  301. package/src/runtime/lifecycle/aipilot/solid-compliance.ts +69 -0
  302. package/src/runtime/lifecycle/aipilot/solid-notice.ts +47 -0
  303. package/src/runtime/lifecycle/aipilot/solid-transcript.ts +60 -0
  304. package/src/runtime/lifecycle/aipilot/source-scan.ts +56 -0
  305. package/src/runtime/lifecycle/aipilot/sync-task.ts +75 -0
  306. package/src/runtime/lifecycle/aipilot/transcript.ts +88 -0
  307. package/src/runtime/lifecycle/aipilot/types.ts +72 -0
  308. package/src/runtime/lifecycle/auto-document-reads.ts +73 -0
  309. package/src/runtime/lifecycle/cartographer/detect.ts +57 -0
  310. package/src/runtime/lifecycle/cartographer/ecosystem-map.ts +56 -0
  311. package/src/runtime/lifecycle/cartographer/fs-util.ts +64 -0
  312. package/src/runtime/lifecycle/cartographer/merge.ts +51 -0
  313. package/src/runtime/lifecycle/cartographer/project-map.ts +49 -0
  314. package/src/runtime/lifecycle/cartographer/scan-hooks.ts +48 -0
  315. package/src/runtime/lifecycle/cartographer/scan.ts +81 -0
  316. package/src/runtime/lifecycle/cartographer/session-start.ts +33 -0
  317. package/src/runtime/lifecycle/cartographer/track-enrichment.ts +45 -0
  318. package/src/runtime/lifecycle/cartographer/write-plugin-map.ts +43 -0
  319. package/src/runtime/lifecycle/cartographer/write-tree.ts +44 -0
  320. package/src/runtime/lifecycle/changelog-research.ts +43 -0
  321. package/src/runtime/lifecycle/check-file-size.ts +61 -0
  322. package/src/runtime/lifecycle/codex-resync/fingerprint.ts +63 -0
  323. package/src/runtime/lifecycle/codex-resync/lock.ts +46 -0
  324. package/src/runtime/lifecycle/codex-resync/managed-destination.ts +39 -0
  325. package/src/runtime/lifecycle/codex-resync/materialize-agents.ts +57 -0
  326. package/src/runtime/lifecycle/codex-resync/plugin-files.ts +65 -0
  327. package/src/runtime/lifecycle/codex-resync/plugin-roots.ts +62 -0
  328. package/src/runtime/lifecycle/codex-resync/resync.ts +45 -0
  329. package/src/runtime/lifecycle/codex-resync/symlink-commands.ts +25 -0
  330. package/src/runtime/lifecycle/dispatch.ts +109 -0
  331. package/src/runtime/lifecycle/failure-lesson.ts +55 -0
  332. package/src/runtime/lifecycle/index.ts +34 -0
  333. package/src/runtime/lifecycle/inject-rules.ts +50 -0
  334. package/src/runtime/lifecycle/instructions-loaded.ts +22 -0
  335. package/src/runtime/lifecycle/kimi-rules-native.ts +44 -0
  336. package/src/runtime/lifecycle/lessons/dispatch.ts +88 -0
  337. package/src/runtime/lifecycle/lessons/reminder.ts +89 -0
  338. package/src/runtime/lifecycle/lessons/state.ts +23 -0
  339. package/src/runtime/lifecycle/memory/agent-lesson.ts +35 -0
  340. package/src/runtime/lifecycle/memory/capture-error.ts +41 -0
  341. package/src/runtime/lifecycle/memory/client.ts +65 -0
  342. package/src/runtime/lifecycle/memory/dispatch.ts +42 -0
  343. package/src/runtime/lifecycle/memory/recall.ts +30 -0
  344. package/src/runtime/lifecycle/memory/salience.ts +31 -0
  345. package/src/runtime/lifecycle/memory/state.ts +49 -0
  346. package/src/runtime/lifecycle/memory/track-ops.ts +15 -0
  347. package/src/runtime/lifecycle/post-compact.ts +38 -0
  348. package/src/runtime/lifecycle/post-edit-ts.ts +49 -0
  349. package/src/runtime/lifecycle/post-tracking.ts +40 -0
  350. package/src/runtime/lifecycle/pre-compact.ts +38 -0
  351. package/src/runtime/lifecycle/rules-root.ts +99 -0
  352. package/src/runtime/lifecycle/security/check-skill.ts +63 -0
  353. package/src/runtime/lifecycle/security/scan-patterns.ts +53 -0
  354. package/src/runtime/lifecycle/security/scan.ts +76 -0
  355. package/src/runtime/lifecycle/security/skill-state.ts +47 -0
  356. package/src/runtime/lifecycle/security/track-mcp.ts +29 -0
  357. package/src/runtime/lifecycle/security/track-skill-read.ts +31 -0
  358. package/src/runtime/lifecycle/seo/post-tool-use.ts +45 -0
  359. package/src/runtime/lifecycle/session-end.ts +17 -0
  360. package/src/runtime/lifecycle/session-start.ts +57 -0
  361. package/src/runtime/lifecycle/snapshot/board.ts +27 -0
  362. package/src/runtime/lifecycle/snapshot/format.ts +60 -0
  363. package/src/runtime/lifecycle/snapshot/git.ts +71 -0
  364. package/src/runtime/lifecycle/snapshot/index.ts +52 -0
  365. package/src/runtime/lifecycle/snapshot/version.ts +34 -0
  366. package/src/runtime/lifecycle/solid-detect.ts +56 -0
  367. package/src/runtime/lifecycle/stop-core.ts +35 -0
  368. package/src/runtime/lifecycle/subagent-cache.ts +61 -0
  369. package/src/runtime/lifecycle/task-completed.ts +94 -0
  370. package/src/runtime/lifecycle/teammate-idle-check.ts +63 -0
  371. package/src/runtime/lifecycle/teammate-idle.ts +28 -0
  372. package/src/runtime/lifecycle/tool-failure.ts +24 -0
  373. package/src/runtime/lifecycle/track-changes.ts +61 -0
  374. package/src/runtime/lifecycle/validate-solid.ts +70 -0
  375. package/src/runtime/lifecycle/validate-tailwind.ts +80 -0
  376. package/src/runtime/lifecycle-bridge.ts +74 -0
  377. package/src/runtime/mcp-key.ts +44 -0
  378. package/src/runtime/mcp-tool-name.ts +92 -0
  379. package/src/runtime/mcp.ts +91 -0
  380. package/src/runtime/modular.ts +77 -0
  381. package/src/runtime/normalize.ts +107 -0
  382. package/src/runtime/notices.ts +91 -0
  383. package/src/runtime/notification-sound.ts +49 -0
  384. package/src/runtime/notifications.ts +77 -0
  385. package/src/runtime/paths.ts +59 -0
  386. package/src/runtime/post-fanout.ts +41 -0
  387. package/src/runtime/post-outcome.ts +61 -0
  388. package/src/runtime/pre-allow.ts +97 -0
  389. package/src/runtime/precommit.ts +60 -0
  390. package/src/runtime/prompt-text.ts +38 -0
  391. package/src/runtime/receipt-capture.ts +34 -0
  392. package/src/runtime/record.ts +26 -0
  393. package/src/runtime/respond.ts +92 -0
  394. package/src/runtime/solid-file-size-gate.ts +58 -0
  395. package/src/runtime/solid-pre.ts +53 -0
  396. package/src/runtime/storage.ts +9 -0
  397. package/src/statusline/colors.ts +31 -0
  398. package/src/statusline/constants.ts +17 -0
  399. package/src/statusline/formatters.ts +54 -0
  400. package/src/statusline/index.ts +4 -0
  401. package/src/statusline/progress-bar.ts +46 -0
  402. package/src/tracking/codex-post-failure.ts +73 -0
  403. package/src/tracking/index.ts +2 -0
  404. package/src/tracking/integrity.ts +99 -0
  405. package/src/tracking/one-shot-dedup.ts +34 -0
  406. package/src/tracking/one-shot-failure.ts +42 -0
  407. package/src/tracking/one-shot-store.ts +95 -0
  408. package/src/tracking/one-shot.ts +93 -0
  409. package/src/tracking/receipts.ts +95 -0
  410. package/src/tracking/session-state.ts +94 -0
  411. package/src/tracking/store.ts +96 -0
  412. package/src/tracking/track-compact.ts +100 -0
  413. package/src/tracking/track-diff.ts +50 -0
  414. package/src/tracking/track-journal.ts +97 -0
  415. package/src/tracking/track-lock-sync.ts +83 -0
  416. package/src/tracking/track-lock.ts +67 -0
  417. package/src/util/compact-json.ts +23 -0
  418. package/src/util/index.ts +2 -0
  419. package/src/util/json-io.ts +47 -0
  420. package/src/util/project-root.ts +41 -0
  421. package/src/util/runtime-io.ts +85 -0
  422. package/src/util/semver.ts +30 -0
@@ -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
+ }
@@ -0,0 +1,32 @@
1
+ import { detectFramework } from "./detect-framework";
2
+
3
+ /**
4
+ * File-size gate scope — parity `enforce-file-size.py` CODE_EXT (both pre- and
5
+ * post-tool-use variants; deliberately excludes `.css`, matching the product
6
+ * decision already applied to `runtime/gate-helpers.ts::isApexScoped` for the
7
+ * sibling `require-apex-agents.py`). NOT `util/project-root.ts::isCodeFile`, a
8
+ * broader general-purpose predicate whose other two callers (`cli/run.ts`,
9
+ * `lifecycle/lessons/dispatch.ts`) are unrelated to this Python source.
10
+ */
11
+ const FILE_SIZE_CODE_EXT = /\.(ts|tsx|js|jsx|py|go|rs|java|php|cpp|c|rb|swift|kt|dart|vue|svelte|astro)$/;
12
+
13
+ /** True when `filePath` is in scope for the SOLID file-size gate (see {@link FILE_SIZE_CODE_EXT}). */
14
+ export function isFileSizeScoped(filePath: string): boolean {
15
+ return FILE_SIZE_CODE_EXT.test(filePath);
16
+ }
17
+
18
+ /**
19
+ * Resolve the SOLID_REF framework key for a file — now project-real via
20
+ * {@link detectFramework} (nearest-manifest caps ∩ file signal), NOT the old
21
+ * extension-only default that mislabeled every `.ts` as react (the "twin bug" of
22
+ * the require-solid-read pipeline; it only escaped when `next.config` sat in the
23
+ * SAME dir). A backend `.ts` in a react project — or any `.ts` in this generic
24
+ * Bun repo — now resolves to "generic". Content is unavailable at this call site
25
+ * (evaluate.ts:59 passes only the path), so `.tsx`/`.jsx` still resolve via caps
26
+ * while `.ts`/`.js` lacking a content signal fall to "generic". Return values
27
+ * stay in the existing union; fail-open to "generic".
28
+ * @param filePath - Absolute path of the file being written/edited.
29
+ */
30
+ export function resolveSolidRefFramework(filePath: string): string {
31
+ return detectFramework(filePath, "");
32
+ }
@@ -0,0 +1,99 @@
1
+ import { basename } from "node:path";
2
+ import { resolveMaxLines, splitTarget } from "../config/limits";
3
+
4
+ /**
5
+ * Fixed marketplace plugins root — parity with Python `enforce-file-size.py`'s
6
+ * literal, unexpanded `~/...` string. Exported: reused by
7
+ * `policy/apex.ts::solidReadGate` for its "no reference matched" deny message.
8
+ */
9
+ export const PLUGINS_DIR = "~/.claude/plugins/marketplaces/fusengine-plugins/plugins";
10
+
11
+ /**
12
+ * Skill-dir fragment per framework — parity with Python
13
+ * `enforce-file-size.py::get_solid_ref()` (falls back to `generic/`). Exported:
14
+ * reused by `policy/apex.ts::solidReadGate` (see {@link PLUGINS_DIR}).
15
+ */
16
+ export const SOLID_REF: Record<string, string> = {
17
+ react: "react-expert/skills/solid-react/",
18
+ nextjs: "nextjs-expert/skills/solid-nextjs/",
19
+ "tanstack-start": "tanstack-start-expert/skills/solid-tanstack-start/",
20
+ laravel: "laravel-expert/skills/solid-php/",
21
+ swift: "swift-apple-expert/skills/solid-swift/",
22
+ go: "solid/skills/solid-go/",
23
+ rust: "solid/skills/solid-rust/",
24
+ };
25
+
26
+ /** Verdict from {@link evaluateFileSize}. */
27
+ export interface FileSizeVerdict {
28
+ ok: boolean;
29
+ lines: number;
30
+ max: number;
31
+ message: string | null;
32
+ }
33
+
34
+ /**
35
+ * Count physical lines — parity with the Python `enforce-file-size.py`
36
+ * (`sum(1 for _ in f)`): every line counts (blanks and comments included), and a
37
+ * single trailing newline does not add a phantom line. The SOLID ceiling is
38
+ * measured on raw file length, not substantive code, to match the upstream plugin.
39
+ */
40
+ export function countLines(content: string): number {
41
+ if (content === "") return 0;
42
+ return content.split("\n").length - (content.endsWith("\n") ? 1 : 0);
43
+ }
44
+
45
+ /**
46
+ * Count non-empty, non-comment lines — parity with the Python `count_code_lines`
47
+ * shared by the framework-specific SOLID validators: `_shared/scripts/validate_solid_common.py`
48
+ * (imported by `nextjs-expert/scripts/validate-nextjs-solid.py` and
49
+ * `swift-apple-expert/scripts/validate-swift-solid.py`), duplicated verbatim in
50
+ * `react-expert/scripts/validate-react-solid.py` / `laravel-expert/scripts/validate-laravel-solid.py`.
51
+ * Strips blank lines and lines starting with `//` or `*` — a SINGLE fixed rule
52
+ * for all 4 callers (the Python `comment` param defaults to, and every real
53
+ * call site leaves it at, `"//"` — never per-language despite covering
54
+ * ts/tsx/js/jsx, php and swift).
55
+ *
56
+ * Deliberately distinct from two other "code-only" counters already in this
57
+ * repo, neither of which is a faithful substitute here:
58
+ * - `countLoc` (`runtime/lifecycle/check-file-size.ts`): a genuinely
59
+ * per-language table (PHP additionally strips `#`, Python strips
60
+ * `#`/`"""`/`'''`) — ported from the unrelated `solid/scripts/check-file-size.py`.
61
+ * - `countCodeLines` (`runtime/lifecycle/aipilot/solid-compliance.ts`): also
62
+ * strips `#` — ported from `ai-pilot/scripts/check-solid-compliance.py`.
63
+ * Reusing either would silently strip PHP `#`/Python-style comments that the
64
+ * real react/nextjs/laravel/swift validators do NOT strip.
65
+ * @param content - The file content to measure.
66
+ */
67
+ export function countFrameworkCodeLines(content: string): number {
68
+ let count = 0;
69
+ for (const raw of content.split("\n")) {
70
+ const line = raw.trim();
71
+ if (!line || line.startsWith("//") || line.startsWith("*")) continue;
72
+ count++;
73
+ }
74
+ return count;
75
+ }
76
+
77
+ /**
78
+ * Evaluate a file's line count against the SOLID limit.
79
+ * @param lines - the file's line count
80
+ * @param max - the limit (defaults to `resolveMaxLines()`)
81
+ */
82
+ export function evaluateFileSize(
83
+ lines: number,
84
+ max: number = resolveMaxLines(),
85
+ filePath = "",
86
+ framework = "generic",
87
+ displayLines: number = lines,
88
+ ): FileSizeVerdict {
89
+ if (lines <= max) return { ok: true, lines, max, message: null };
90
+ const split = splitTarget(max);
91
+ const fname = filePath ? basename(filePath) : "file";
92
+ const ref = SOLID_REF[framework] ?? "generic/";
93
+ return {
94
+ ok: false,
95
+ lines,
96
+ max,
97
+ message: `BLOCKED: '${fname}' has ${displayLines} lines (max: ${max}). TO SPLIT: 1) Read SOLID rules: ${PLUGINS_DIR}/${ref} 2) Create new module files (<${split} lines each) 3) Use Write to replace '${fname}' with <${max} lines version.`,
98
+ };
99
+ }
@@ -0,0 +1,52 @@
1
+ /** Build/dep paths excluded from React/Next.js SOLID gating — segment-anchored (parity `r"/(node_modules|dist|build|\.next)/"`), NOT a bare substring, so `distance.ts`/`rebuild/` stay gated. */
2
+ const JS_EXCLUDE_RE: RegExp = /(^|\/)(node_modules|dist|build|\.next)(\/|$)/;
3
+ /** Vendored dependency paths excluded from Laravel/PHP SOLID gating. */
4
+ const PHP_EXCLUDE_RE: RegExp = /\/vendor\//;
5
+ /** Derived/build artifact paths excluded from Swift SOLID gating. */
6
+ const SWIFT_EXCLUDE_RE: RegExp = /(\.build|DerivedData|Pods)/;
7
+
8
+ /**
9
+ * Whether a JS/TS (React/Next.js) file path is an excluded build artifact.
10
+ * Matches the Python validators' early-return guard to avoid false positives.
11
+ * @param filePath - absolute path of the file under validation
12
+ */
13
+ export function isExcludedJsPath(filePath: string): boolean {
14
+ return JS_EXCLUDE_RE.test(filePath);
15
+ }
16
+
17
+ /**
18
+ * Whether a PHP (Laravel) file path is a vendored dependency to skip.
19
+ * @param filePath - absolute path of the file under validation
20
+ */
21
+ export function isExcludedPhpPath(filePath: string): boolean {
22
+ return PHP_EXCLUDE_RE.test(filePath);
23
+ }
24
+
25
+ /**
26
+ * Whether a Swift file path is a derived/build artifact to skip.
27
+ * @param filePath - absolute path of the file under validation
28
+ */
29
+ export function isExcludedSwiftPath(filePath: string): boolean {
30
+ return SWIFT_EXCLUDE_RE.test(filePath);
31
+ }
32
+
33
+ /**
34
+ * This harness's own gate-detection source files. They hold the React/Next
35
+ * detection literals as regex source text (client-directive string, hook/
36
+ * event names, store/query/route signals) — scanning them as JS/TS would
37
+ * self-match the very heuristics they define. Exempt by exact filename or by
38
+ * the conventions/ detector directory, not by broad directory, so a genuine
39
+ * SOLID violation elsewhere in src/policy/ stays gated by the
40
+ * framework-agnostic line-size check in policy/evaluate.ts, untouched.
41
+ */
42
+ const SELF_GATE_EXCLUDE_RE: RegExp = /(^|\/)src\/policy\/(framework-solid-gates(-systems)?\.ts|framework-solid-extended\.ts|detect-framework\.ts|conventions\/[^/]+\.ts|guards\/interface-separation-ext\.ts)$/;
43
+
44
+ /**
45
+ * Whether a JS/TS file path is this harness's own gate-detection source —
46
+ * exempt from the pattern heuristics (not the line-size ceiling) to avoid
47
+ * self-blocking on regex literals that look like the code they detect.
48
+ * @param filePath - absolute path of the file under validation
49
+ */
50
+ export function isSelfGateSourcePath(filePath: string): boolean {
51
+ return SELF_GATE_EXCLUDE_RE.test(filePath);
52
+ }