@geraldmaron/construct 1.4.1 → 1.5.0

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 (556) hide show
  1. package/.env.example +36 -0
  2. package/README.md +41 -7
  3. package/bin/construct +1029 -65
  4. package/bin/construct-postinstall.mjs +27 -2
  5. package/config/tag-vocabulary.json +264 -0
  6. package/examples/distribution/sources/deck-one-pager.md +1 -1
  7. package/lib/acp/server.mjs +21 -7
  8. package/lib/adapters-sync.mjs +2 -1
  9. package/lib/audit-trail.mjs +185 -2
  10. package/lib/beads/auto-close.mjs +2 -1
  11. package/lib/beads/drift.mjs +2 -1
  12. package/lib/bridges/copilot-proxy.mjs +20 -5
  13. package/lib/certification/skill-inventory.mjs +10 -1
  14. package/lib/cli/approvals.mjs +147 -0
  15. package/lib/cli-commands.mjs +105 -14
  16. package/lib/comment-lint.mjs +127 -8
  17. package/lib/config/schema.mjs +7 -30
  18. package/lib/config/source-target-registry.mjs +70 -0
  19. package/lib/config/source-targets.mjs +179 -205
  20. package/lib/contracts/coverage.mjs +77 -0
  21. package/lib/contracts/validate.mjs +102 -13
  22. package/lib/contracts/violation-log.mjs +50 -4
  23. package/lib/db/migrate.mjs +69 -0
  24. package/lib/db/migrations/001_orchestration_runs.sql +9 -0
  25. package/lib/db/migrations/002_queue_provider.sql +50 -0
  26. package/lib/db/migrations/003_worker_registry.sql +17 -0
  27. package/lib/db/migrations/004_trace_events.sql +16 -0
  28. package/lib/db/migrations/005_shared_memory.sql +15 -0
  29. package/lib/db/migrations/006_orchestration_runs_tenant.sql +5 -0
  30. package/lib/decisions/enforced-baseline.json +0 -2
  31. package/lib/decisions/registry.mjs +3 -2
  32. package/lib/deployment/parity-contract.mjs +1 -1
  33. package/lib/deployment-mode.mjs +78 -2
  34. package/lib/diagram-export.mjs +10 -1
  35. package/lib/distill.mjs +5 -2
  36. package/lib/docs-verify.mjs +2 -1
  37. package/lib/doctor/cli.mjs +1 -0
  38. package/lib/doctor/command-on-path.mjs +24 -0
  39. package/lib/doctor/diagnosis.mjs +109 -0
  40. package/lib/doctor/embedding-health.mjs +50 -0
  41. package/lib/doctor/engine-health.mjs +93 -0
  42. package/lib/doctor/graph-validate.mjs +47 -0
  43. package/lib/doctor/index.mjs +23 -4
  44. package/lib/doctor/sidecar-providers.mjs +56 -0
  45. package/lib/doctor/watchers/consistency.mjs +93 -1
  46. package/lib/doctor/watchers/cx-budget.mjs +1 -1
  47. package/lib/doctor/watchers/graph-staleness.mjs +1 -1
  48. package/lib/doctor/watchers/mcp-protocol.mjs +17 -0
  49. package/lib/doctor/watchers/oracle-liveness.mjs +92 -0
  50. package/lib/doctor/watchers/orchestration-runs.mjs +108 -0
  51. package/lib/doctor/watchers/provider-breaker.mjs +78 -0
  52. package/lib/document-export.mjs +52 -27
  53. package/lib/document-extract/docling-client.mjs +9 -5
  54. package/lib/document-extract/docling-sidecar.py +30 -0
  55. package/lib/document-extract.mjs +1 -1
  56. package/lib/document-ingest.mjs +9 -0
  57. package/lib/embed/approval-queue.mjs +184 -88
  58. package/lib/embed/authority-guard.mjs +65 -2
  59. package/lib/embed/capability-jobs.mjs +365 -0
  60. package/lib/embed/capability-lifecycle.mjs +219 -0
  61. package/lib/embed/capability-loader.mjs +303 -0
  62. package/lib/embed/capability-runtime.mjs +58 -0
  63. package/lib/embed/cli.mjs +185 -8
  64. package/lib/embed/config.mjs +4 -0
  65. package/lib/embed/daemon.mjs +125 -6
  66. package/lib/embed/demand-fetch.mjs +190 -54
  67. package/lib/embed/inbox.mjs +12 -10
  68. package/lib/embed/presets/ops-triage.mjs +236 -0
  69. package/lib/embed/presets/pm-feedback.mjs +341 -0
  70. package/lib/embed/presets/tpm.mjs +401 -0
  71. package/lib/embed/providers/jira.mjs +72 -1
  72. package/lib/embed/providers/registry.mjs +140 -33
  73. package/lib/embed/worker.mjs +5 -0
  74. package/lib/embedded-contract/capability.mjs +4 -0
  75. package/lib/embedded-contract/execution.mjs +1 -1
  76. package/lib/embedded-contract/model-resolve.mjs +53 -3
  77. package/lib/embedded-contract/workflow-defs.mjs +48 -73
  78. package/lib/embedded-contract/workflow-invoke.mjs +144 -4
  79. package/lib/embedded-contract/workflows/architecture-review.manifest.json +15 -0
  80. package/lib/embedded-contract/workflows/data-structure.manifest.json +14 -0
  81. package/lib/embedded-contract/workflows/evidence-ingest.manifest.json +14 -0
  82. package/lib/embedded-contract/workflows/memo-draft.manifest.json +14 -0
  83. package/lib/embedded-contract/workflows/operations-triage.manifest.json +18 -0
  84. package/lib/embedded-contract/workflows/operations.manifest.json +18 -0
  85. package/lib/embedded-contract/workflows/pm-feedback.manifest.json +18 -0
  86. package/lib/embedded-contract/workflows/prd-draft.manifest.json +15 -0
  87. package/lib/embedded-contract/workflows/proposal-review.manifest.json +15 -0
  88. package/lib/embedded-contract/workflows/research-synthesis.manifest.json +14 -0
  89. package/lib/embedded-contract/workflows/risk-review.manifest.json +15 -0
  90. package/lib/embedded-contract/workflows/structure-notes.manifest.json +14 -0
  91. package/lib/embedded-contract/workflows/transcript-process.manifest.json +14 -0
  92. package/lib/embedded-contract/workflows/triage.manifest.json +14 -0
  93. package/lib/env-config.mjs +60 -11
  94. package/lib/export-validate.mjs +34 -2
  95. package/lib/extensions/index.mjs +27 -0
  96. package/lib/extensions/loader.mjs +120 -0
  97. package/lib/extensions/manifest-schema.mjs +141 -0
  98. package/lib/extensions/manifests/anthropic.manifest.json +12 -0
  99. package/lib/extensions/manifests/atlassian-confluence.manifest.json +12 -0
  100. package/lib/extensions/manifests/atlassian-jira.manifest.json +53 -0
  101. package/lib/extensions/manifests/directory.manifest.json +12 -0
  102. package/lib/extensions/manifests/docling.manifest.json +37 -0
  103. package/lib/extensions/manifests/echo.manifest.json +8 -0
  104. package/lib/extensions/manifests/feedback.manifest.json +12 -0
  105. package/lib/extensions/manifests/github-copilot.manifest.json +12 -0
  106. package/lib/extensions/manifests/github.manifest.json +52 -0
  107. package/lib/extensions/manifests/linear.manifest.json +50 -0
  108. package/lib/extensions/manifests/local.manifest.json +12 -0
  109. package/lib/extensions/manifests/ollama.manifest.json +12 -0
  110. package/lib/extensions/manifests/openai.manifest.json +12 -0
  111. package/lib/extensions/manifests/openrouter-anthropic.manifest.json +12 -0
  112. package/lib/extensions/manifests/openrouter-deepseek.manifest.json +12 -0
  113. package/lib/extensions/manifests/openrouter-google.manifest.json +12 -0
  114. package/lib/extensions/manifests/openrouter-llama.manifest.json +12 -0
  115. package/lib/extensions/manifests/openrouter-qwen.manifest.json +12 -0
  116. package/lib/extensions/manifests/openrouter.manifest.json +12 -0
  117. package/lib/extensions/manifests/postgres.manifest.json +12 -0
  118. package/lib/extensions/manifests/salesforce.manifest.json +12 -0
  119. package/lib/extensions/manifests/slack.manifest.json +58 -0
  120. package/lib/extensions/manifests/whisper.manifest.json +36 -0
  121. package/lib/extensions/validate.mjs +175 -0
  122. package/lib/features.mjs +13 -9
  123. package/lib/flows/checkpoint.mjs +184 -0
  124. package/lib/flows/constants.mjs +27 -0
  125. package/lib/flows/define.mjs +146 -0
  126. package/lib/flows/engine.mjs +249 -0
  127. package/lib/flows/errors.mjs +19 -0
  128. package/lib/flows/index.mjs +27 -0
  129. package/lib/flows/joins.mjs +18 -0
  130. package/lib/flows/schema.mjs +90 -0
  131. package/lib/flows/state.mjs +31 -0
  132. package/lib/frameworks/loader.mjs +99 -0
  133. package/lib/frameworks/schema.mjs +157 -0
  134. package/lib/graph/build-from-corpus.mjs +67 -0
  135. package/lib/graph/build-from-embed.mjs +82 -0
  136. package/lib/graph/build-from-registry.mjs +164 -3
  137. package/lib/graph/cli.mjs +456 -12
  138. package/lib/graph/gap-queries.mjs +156 -0
  139. package/lib/graph/gaps.mjs +41 -0
  140. package/lib/graph/impacted.mjs +129 -0
  141. package/lib/graph/runtime-evidence.mjs +177 -0
  142. package/lib/graph/security-coverage.mjs +113 -0
  143. package/lib/graph/staleness.mjs +241 -10
  144. package/lib/graph/store.mjs +24 -7
  145. package/lib/graph/validate.mjs +161 -0
  146. package/lib/headhunt.mjs +9 -8
  147. package/lib/health-check.mjs +15 -7
  148. package/lib/hook-health.mjs +1 -1
  149. package/lib/hooks/agent-tracker.mjs +10 -4
  150. package/lib/hooks/edit-guard.mjs +3 -3
  151. package/lib/hooks/graph-impact-advisory.mjs +2 -2
  152. package/lib/hooks/guard-bash.mjs +41 -15
  153. package/lib/hooks/mcp-health-check.mjs +11 -4
  154. package/lib/hooks/model-fallback.mjs +50 -6
  155. package/lib/hooks/orchestration-dispatch-guard.mjs +14 -3
  156. package/lib/hooks/pre-compact.mjs +181 -173
  157. package/lib/hooks/rule-verifier.mjs +2 -1
  158. package/lib/hooks/session-reflect.mjs +2 -1
  159. package/lib/hooks/session-start.mjs +3 -3
  160. package/lib/host/readiness.mjs +109 -0
  161. package/lib/host-capabilities.mjs +13 -2
  162. package/lib/host-disposition.mjs +29 -8
  163. package/lib/identity.mjs +92 -0
  164. package/lib/ingest/degraded-extract.mjs +96 -0
  165. package/lib/ingest/docling-remote.mjs +2 -1
  166. package/lib/ingest/provider-extract.mjs +7 -19
  167. package/lib/ingest/sidecar-providers.mjs +196 -0
  168. package/lib/ingest-tooling.mjs +11 -5
  169. package/lib/init-unified.mjs +55 -38
  170. package/lib/init-update.mjs +2 -1
  171. package/lib/install/legacy-global-cleanup.mjs +25 -0
  172. package/lib/install/stage-project.mjs +41 -4
  173. package/lib/intake/daemon.mjs +99 -8
  174. package/lib/intake/git-queue.mjs +110 -38
  175. package/lib/intake/prepare.mjs +2 -0
  176. package/lib/intake/queue-registry.mjs +50 -0
  177. package/lib/intake/queue.mjs +133 -17
  178. package/lib/intake/session-prelude.mjs +57 -8
  179. package/lib/integrations/intake-integrations.mjs +61 -111
  180. package/lib/intent-classifier.mjs +43 -5
  181. package/lib/libreoffice-export.mjs +8 -8
  182. package/lib/logging/rotate.mjs +5 -4
  183. package/lib/mcp/broker.mjs +332 -17
  184. package/lib/mcp/denied-store.mjs +95 -0
  185. package/lib/mcp/destructive-approval.mjs +57 -0
  186. package/lib/mcp/destructive-gate.mjs +30 -0
  187. package/lib/mcp/dispatch-envelope.mjs +199 -0
  188. package/lib/mcp/memory-bridge.mjs +2 -1
  189. package/lib/mcp/server.mjs +185 -1268
  190. package/lib/mcp/tool-definitions-memory.mjs +376 -0
  191. package/lib/mcp/tool-definitions-project.mjs +271 -0
  192. package/lib/mcp/tool-definitions-skills.mjs +311 -0
  193. package/lib/mcp/tool-definitions-workflow.mjs +398 -0
  194. package/lib/mcp/tool-definitions.mjs +25 -0
  195. package/lib/mcp/tool-rate-limit.mjs +47 -0
  196. package/lib/mcp/tool-registry.mjs +107 -0
  197. package/lib/mcp/tool-safety.mjs +95 -0
  198. package/lib/mcp/tool-surface-parity.mjs +60 -0
  199. package/lib/mcp/tools/orchestration-delegation-next.tool.mjs +68 -0
  200. package/lib/mcp/tools/orchestration-run.mjs +197 -19
  201. package/lib/mcp/tools/orchestration-task-result.tool.mjs +77 -0
  202. package/lib/mcp/tools/project.mjs +25 -8
  203. package/lib/mcp/tools/provider-write.mjs +187 -0
  204. package/lib/mcp/tools/scope.mjs +0 -3
  205. package/lib/mcp/tools/skills.mjs +1 -1
  206. package/lib/mcp/tools/storage.mjs +2 -2
  207. package/lib/mcp/tools/web-search-governance.mjs +96 -0
  208. package/lib/mcp/tools/web-search.mjs +5 -76
  209. package/lib/mcp/transport/auth.mjs +238 -0
  210. package/lib/mcp/transport/http.mjs +136 -0
  211. package/lib/mcp/transport/mode.mjs +31 -0
  212. package/lib/mcp/transport/stdio.mjs +22 -0
  213. package/lib/mcp-catalog.json +4 -4
  214. package/lib/mcp-manager.mjs +34 -23
  215. package/lib/mcp-platform-config.mjs +53 -20
  216. package/lib/mode-capabilities.mjs +109 -0
  217. package/lib/model-router.mjs +42 -9
  218. package/lib/models/catalog.mjs +40 -1
  219. package/lib/net-guard.mjs +247 -0
  220. package/lib/observation-store.mjs +6 -2
  221. package/lib/ollama/provision-context.mjs +2 -1
  222. package/lib/opencode-config.mjs +22 -3
  223. package/lib/opencode-runtime-plugin.mjs +120 -2
  224. package/lib/oracle/actions.mjs +204 -10
  225. package/lib/oracle/cli.mjs +24 -3
  226. package/lib/oracle/daemon-entry.mjs +6 -0
  227. package/lib/oracle/dispatch.mjs +2 -1
  228. package/lib/oracle/execute.mjs +2 -2
  229. package/lib/oracle/gaps.mjs +3 -3
  230. package/lib/oracle/heartbeat.mjs +53 -0
  231. package/lib/oracle/read-model.mjs +69 -13
  232. package/lib/oracle/reconcile.mjs +3 -46
  233. package/lib/oracle/routing.mjs +37 -31
  234. package/lib/oracle/synthesize.mjs +1 -1
  235. package/lib/orchestration/classification.mjs +434 -0
  236. package/lib/orchestration/delegation-flow.mjs +132 -0
  237. package/lib/orchestration/flow-selection.mjs +418 -0
  238. package/lib/orchestration/gates.mjs +244 -0
  239. package/lib/orchestration/host-sampling.mjs +121 -0
  240. package/lib/orchestration/policy-constants.mjs +48 -0
  241. package/lib/orchestration/provider-outcome.mjs +197 -0
  242. package/lib/orchestration/readiness.mjs +114 -13
  243. package/lib/orchestration/run-store-postgres.mjs +32 -26
  244. package/lib/orchestration/run-store-sqlite.mjs +8 -14
  245. package/lib/orchestration/run-store.mjs +25 -12
  246. package/lib/orchestration/runtime.mjs +508 -62
  247. package/lib/orchestration/store.mjs +87 -12
  248. package/lib/orchestration/trace-store.mjs +125 -0
  249. package/lib/orchestration/web-capability.mjs +60 -0
  250. package/lib/orchestration/worker-runtime.mjs +162 -0
  251. package/lib/orchestration/worker.mjs +763 -80
  252. package/lib/orchestration-policy.mjs +67 -1111
  253. package/lib/output-quality.mjs +61 -2
  254. package/lib/packs/cli.mjs +144 -0
  255. package/lib/packs/core-pack.mjs +112 -0
  256. package/lib/packs/enablement.mjs +187 -0
  257. package/lib/packs/index.mjs +23 -0
  258. package/lib/packs/loader.mjs +176 -0
  259. package/lib/packs/manifest-schema.mjs +58 -0
  260. package/lib/packs/prompts.mjs +120 -0
  261. package/lib/packs/validate.mjs +208 -0
  262. package/lib/path-policy.mjs +56 -0
  263. package/lib/plugin-registry.mjs +4 -5
  264. package/lib/policy/audit-gate.mjs +42 -0
  265. package/lib/policy/consumption-budget.mjs +149 -0
  266. package/lib/policy/engine.mjs +81 -6
  267. package/lib/policy/role-authority.mjs +89 -0
  268. package/lib/prompt-composer.js +19 -3
  269. package/lib/providers/contract/adapters/confluence/governed-write.mjs +215 -0
  270. package/lib/providers/contract/adapters/confluence/manifest.json +17 -0
  271. package/lib/providers/contract/adapters/confluence/transport.mjs +135 -0
  272. package/lib/providers/contract/adapters/github/governed-write.mjs +121 -0
  273. package/lib/providers/contract/adapters/github/index.mjs +38 -3
  274. package/lib/providers/contract/adapters/jira/adf.mjs +151 -0
  275. package/lib/providers/contract/adapters/jira/createmeta.mjs +143 -0
  276. package/lib/providers/contract/adapters/jira/governed-write.mjs +182 -0
  277. package/lib/providers/contract/adapters/jira/manifest.json +17 -0
  278. package/lib/providers/contract/adapters/jira/transport.mjs +119 -0
  279. package/lib/providers/contract.mjs +207 -0
  280. package/lib/providers/credential-bootstrap.mjs +7 -4
  281. package/lib/providers/credential-sources.mjs +14 -2
  282. package/lib/providers/directory/index.mjs +261 -0
  283. package/lib/providers/feedback/index.mjs +392 -0
  284. package/lib/providers/filter-audit.mjs +68 -0
  285. package/lib/providers/filter-schema.mjs +54 -0
  286. package/lib/providers/github/index.mjs +31 -0
  287. package/lib/providers/instance-config.mjs +215 -0
  288. package/lib/providers/op-locate.mjs +96 -0
  289. package/lib/providers/op-run.mjs +64 -9
  290. package/lib/providers/registry.mjs +26 -3
  291. package/lib/providers/secret-audit-wiring.mjs +41 -18
  292. package/lib/providers/secret-resolver.mjs +263 -31
  293. package/lib/publish-template.mjs +6 -3
  294. package/lib/publish-tooling.mjs +4 -0
  295. package/lib/publish.mjs +32 -4
  296. package/lib/queue/pg-queue.mjs +395 -0
  297. package/lib/reflect.mjs +2 -1
  298. package/lib/registry/assemble.mjs +28 -2
  299. package/lib/registry/catalog.mjs +6 -0
  300. package/lib/registry/consolidation.mjs +8 -1
  301. package/lib/registry/custom-scaffold.mjs +200 -0
  302. package/lib/registry/custom-schema.mjs +137 -0
  303. package/lib/registry/loader.mjs +15 -4
  304. package/lib/registry/manifests/format-engines.default.json +15 -0
  305. package/lib/registry/manifests/surface-map.default.json +46 -0
  306. package/lib/registry/retired-paths.mjs +1 -0
  307. package/lib/registry/surface-map.mjs +100 -50
  308. package/lib/registry/validate.mjs +3 -3
  309. package/lib/render-visual-check.mjs +240 -0
  310. package/lib/resources/budget.mjs +40 -17
  311. package/lib/roles/flavor-bindings.mjs +36 -14
  312. package/lib/roles/gateway.mjs +15 -8
  313. package/lib/roots.mjs +107 -0
  314. package/lib/runtime/uv-bootstrap.mjs +32 -6
  315. package/lib/runtime/whisper-bootstrap.mjs +11 -3
  316. package/lib/runtime-env.mjs +5 -2
  317. package/lib/sandbox.mjs +1 -1
  318. package/lib/scheduler/index.mjs +8 -20
  319. package/lib/schema-infer.mjs +31 -2
  320. package/lib/scopes/lifecycle.mjs +29 -25
  321. package/lib/scopes/loader.mjs +49 -12
  322. package/lib/scopes/teams.mjs +1 -1
  323. package/lib/security/ingest-boundary.mjs +80 -0
  324. package/lib/security/recall-wrapper.mjs +99 -0
  325. package/lib/security/trust.mjs +132 -0
  326. package/lib/service-manager.mjs +93 -24
  327. package/lib/setup.mjs +41 -23
  328. package/lib/skills-apply.mjs +4 -2
  329. package/lib/specialists/postconditions.mjs +2 -2
  330. package/lib/state-root.mjs +147 -0
  331. package/lib/status.mjs +597 -6
  332. package/lib/storage/admin.mjs +84 -7
  333. package/lib/storage/backend-registry.mjs +73 -0
  334. package/lib/storage/backend.mjs +63 -9
  335. package/lib/storage/embeddings-openai.mjs +12 -2
  336. package/lib/storage/hybrid-query.mjs +11 -3
  337. package/lib/storage/retrieval-hardening.mjs +384 -0
  338. package/lib/storage/shared-memory.mjs +158 -0
  339. package/lib/storage/vector-client.mjs +69 -2
  340. package/lib/team/health.mjs +72 -0
  341. package/lib/telemetry/backends/local.mjs +9 -3
  342. package/lib/telemetry/client.mjs +3 -7
  343. package/lib/template-registry.mjs +10 -12
  344. package/lib/tenant/context.mjs +82 -0
  345. package/lib/tenant/isolation.mjs +129 -0
  346. package/lib/test-corpus-inventory.mjs +103 -2
  347. package/lib/uninstall/uninstall.mjs +78 -12
  348. package/lib/validator.mjs +4 -6
  349. package/lib/worker/entrypoint.mjs +2 -1
  350. package/lib/worker/run.mjs +2 -2
  351. package/lib/worker/trace.mjs +5 -11
  352. package/lib/workflow-state.mjs +52 -8
  353. package/lib/workflows/liveness.mjs +203 -0
  354. package/lib/workflows/loader.mjs +177 -0
  355. package/lib/workflows/manifest-schema.mjs +71 -0
  356. package/lib/workflows/surface-parity.mjs +118 -0
  357. package/lib/workflows/validate.mjs +127 -0
  358. package/lib/writes/control-plane.mjs +140 -0
  359. package/lib/writes/envelope.mjs +206 -0
  360. package/lib/writes/sent-log.mjs +86 -0
  361. package/lib/writes/write-intent.mjs +109 -0
  362. package/package.json +20 -7
  363. package/personas/construct.md +12 -3
  364. package/registry/agent-manifest.json +117 -0
  365. package/registry/capabilities.json +1987 -0
  366. package/rules/common/comments.md +1 -0
  367. package/schemas/brand-voice.schema.json +24 -0
  368. package/schemas/capability-registry.schema.json +72 -0
  369. package/schemas/certification-run.schema.json +130 -0
  370. package/schemas/demo-recording.schema.json +46 -0
  371. package/schemas/eval-dataset.schema.json +79 -0
  372. package/schemas/execution-capability-profile.schema.json +46 -0
  373. package/schemas/execution-policy.schema.json +114 -0
  374. package/schemas/improvement-proposal.schema.json +65 -0
  375. package/schemas/mcp-tool-output.schema.json +61 -0
  376. package/schemas/platform-capabilities.schema.json +83 -0
  377. package/schemas/project-config.schema.json +215 -0
  378. package/schemas/project-demo.schema.json +60 -0
  379. package/schemas/provider-behavior-matrix.schema.json +91 -0
  380. package/schemas/scope.schema.json +197 -0
  381. package/schemas/specialist-trace.schema.json +107 -0
  382. package/schemas/team.schema.json +99 -0
  383. package/schemas/unified-registry.schema.json +546 -0
  384. package/scripts/sync-specialists.mjs +336 -106
  385. package/skills/docs/adr-workflow.md +1 -1
  386. package/skills/docs/codebase-research-workflow.md +3 -3
  387. package/skills/docs/prd-workflow.md +5 -6
  388. package/skills/docs/runbook-workflow.md +2 -2
  389. package/skills/docs/user-research-workflow.md +3 -3
  390. package/skills/operating/fleet-health-routing.md +77 -0
  391. package/skills/roles/ai-engineer.md +1 -1
  392. package/skills/roles/architect.md +0 -1
  393. package/skills/roles/business-strategist.md +1 -1
  394. package/skills/roles/data-analyst.telemetry.md +1 -1
  395. package/skills/roles/data-engineer.md +1 -1
  396. package/skills/roles/data-engineer.pipeline.md +1 -1
  397. package/skills/roles/data-engineer.vector-retrieval.md +1 -2
  398. package/skills/roles/data-engineer.warehouse.md +1 -1
  399. package/skills/roles/designer.accessibility.md +1 -1
  400. package/skills/roles/designer.md +0 -1
  401. package/skills/roles/devil-advocate.md +1 -1
  402. package/skills/roles/docs-keeper.md +1 -1
  403. package/skills/roles/evaluator.md +1 -1
  404. package/skills/roles/explorer.md +1 -1
  405. package/skills/roles/platform-engineer.md +1 -1
  406. package/skills/roles/qa.ai-eval.md +1 -2
  407. package/skills/roles/qa.api-contract.md +0 -1
  408. package/skills/roles/qa.data-pipeline.md +0 -1
  409. package/skills/roles/qa.md +0 -1
  410. package/skills/roles/qa.web-ui.md +0 -1
  411. package/skills/roles/release-manager.md +1 -1
  412. package/skills/roles/researcher.md +0 -2
  413. package/skills/roles/reviewer.md +0 -3
  414. package/skills/roles/security.ai.md +1 -1
  415. package/skills/roles/security.legal-compliance.md +1 -1
  416. package/skills/roles/security.md +0 -1
  417. package/skills/roles/security.privacy.md +0 -1
  418. package/skills/roles/security.supply-chain.md +1 -1
  419. package/skills/roles/sre.md +1 -1
  420. package/skills/roles/test-automation.md +1 -1
  421. package/skills/roles/trace-reviewer.md +1 -1
  422. package/skills/roles/ux-researcher.md +1 -1
  423. package/specialists/artifact-manifest.json +36 -36
  424. package/specialists/org/contracts/accessibility-to-qa.json +11 -3
  425. package/specialists/org/contracts/any-to-business-strategist.json +19 -7
  426. package/specialists/org/contracts/any-to-debugger.json +7 -1
  427. package/specialists/org/contracts/any-to-designer.json +7 -1
  428. package/specialists/org/contracts/any-to-docs-keeper.json +18 -4
  429. package/specialists/org/contracts/any-to-explorer.json +13 -4
  430. package/specialists/org/contracts/any-to-sre-incident.json +6 -2
  431. package/specialists/org/contracts/any-to-trace-reviewer.json +16 -4
  432. package/specialists/org/contracts/architect-to-ai-engineer.json +6 -2
  433. package/specialists/org/contracts/architect-to-data-engineer.json +17 -5
  434. package/specialists/org/contracts/architect-to-devil-advocate.json +11 -3
  435. package/specialists/org/contracts/architect-to-engineer.json +20 -4
  436. package/specialists/org/contracts/architect-to-evaluator.json +15 -5
  437. package/specialists/org/contracts/architect-to-legal-compliance.json +15 -5
  438. package/specialists/org/contracts/architect-to-operations.json +17 -4
  439. package/specialists/org/contracts/architect-to-platform-engineer.json +20 -4
  440. package/specialists/org/contracts/construct-to-orchestrator.json +10 -2
  441. package/specialists/org/contracts/data-analyst-to-product-manager.json +11 -2
  442. package/specialists/org/contracts/engineer-to-qa.json +20 -4
  443. package/specialists/org/contracts/engineer-to-reviewer.json +29 -5
  444. package/specialists/org/contracts/explorer-to-engineer.json +11 -3
  445. package/specialists/org/contracts/legal-compliance-to-release-manager.json +12 -4
  446. package/specialists/org/contracts/operations-to-user.json +53 -0
  447. package/specialists/org/contracts/operations-triage-output.json +53 -0
  448. package/specialists/org/contracts/pm-requirements-candidates.json +53 -0
  449. package/specialists/org/contracts/product-manager-to-architect.json +30 -10
  450. package/specialists/org/contracts/product-manager-to-data-analyst.json +13 -3
  451. package/specialists/org/contracts/product-manager-to-ux-researcher.json +15 -5
  452. package/specialists/org/contracts/qa-to-release-manager.json +11 -3
  453. package/specialists/org/contracts/researcher-to-architect.json +10 -2
  454. package/specialists/org/contracts/researcher-to-product-manager.json +16 -5
  455. package/specialists/org/contracts/reviewer-to-security.json +17 -3
  456. package/specialists/org/contracts/test-automation-to-engineer.json +11 -3
  457. package/specialists/org/contracts/trace-reviewer-to-sre.json +12 -4
  458. package/specialists/org/contracts/user-to-construct.json +11 -4
  459. package/specialists/org/frameworks/cx-architect-constraint-option-failure.md +66 -0
  460. package/specialists/org/frameworks/cx-engineer-feasibility-blast-radius.md +66 -0
  461. package/specialists/org/frameworks/cx-ops-dependency-sequencing.md +74 -0
  462. package/specialists/org/frameworks/cx-pm-value-tradeoff.md +66 -0
  463. package/specialists/org/frameworks/cx-qa-risk-based-coverage.md +69 -0
  464. package/specialists/org/groups/engineering-group.json +2 -6
  465. package/specialists/org/groups/governance-group.json +2 -3
  466. package/specialists/org/groups/operations-group.json +5 -8
  467. package/specialists/org/groups/product-group.json +4 -8
  468. package/specialists/org/groups/quality-group.json +3 -7
  469. package/specialists/org/groups/strategy-group.json +6 -9
  470. package/specialists/org/models.json.example +8 -0
  471. package/specialists/org/scopes/creative.json +6 -1
  472. package/specialists/org/scopes/operations.json +7 -1
  473. package/specialists/org/scopes/research.json +6 -1
  474. package/specialists/org/scopes/rnd.json +7 -1
  475. package/specialists/org/specialists/cx-architect.json +27 -8
  476. package/specialists/org/specialists/cx-designer.json +22 -8
  477. package/specialists/org/specialists/cx-engineer.json +71 -7
  478. package/specialists/org/specialists/cx-operations.json +89 -15
  479. package/specialists/org/specialists/cx-orchestrator.json +16 -4
  480. package/specialists/org/specialists/cx-product-manager.json +28 -9
  481. package/specialists/org/specialists/cx-qa.json +16 -6
  482. package/specialists/org/specialists/cx-researcher.json +41 -7
  483. package/specialists/org/specialists/cx-reviewer.json +61 -11
  484. package/specialists/org/specialists/cx-security.json +30 -10
  485. package/specialists/org/teams/design-team.json +3 -4
  486. package/specialists/org/teams/engineering-team.json +3 -10
  487. package/specialists/org/teams/governance-team.json +3 -5
  488. package/specialists/org/teams/operations-team.json +6 -12
  489. package/specialists/org/teams/product-management-team.json +2 -3
  490. package/specialists/org/teams/quality-team.json +4 -12
  491. package/specialists/org/teams/research-team.json +3 -3
  492. package/specialists/org/teams/strategy-team.json +7 -14
  493. package/specialists/prompts/cx-architect.md +20 -1
  494. package/specialists/prompts/cx-data-analyst.md +1 -1
  495. package/specialists/prompts/cx-designer.md +12 -4
  496. package/specialists/prompts/cx-engineer.md +15 -1
  497. package/specialists/prompts/cx-operations.md +14 -2
  498. package/specialists/prompts/cx-orchestrator.md +9 -5
  499. package/specialists/prompts/cx-product-manager.md +5 -1
  500. package/specialists/prompts/cx-qa.md +6 -2
  501. package/specialists/prompts/cx-researcher.md +27 -31
  502. package/specialists/prompts/cx-reviewer.md +19 -1
  503. package/specialists/prompts/cx-security.md +5 -1
  504. package/templates/distribution/construct-brand.typ +123 -59
  505. package/templates/distribution/construct-decision.typ +6 -3
  506. package/templates/distribution/construct-pdf.typ +11 -4
  507. package/templates/distribution/construct-prd.typ +6 -3
  508. package/templates/distribution/construct-research.typ +7 -4
  509. package/vendor/pandoc-ext/README.md +3 -0
  510. package/vendor/pandoc-ext/diagram.lua +687 -0
  511. package/lib/providers/contract/adapters/git/index.mjs +0 -115
  512. package/lib/providers/contract/adapters/slack/index.mjs +0 -175
  513. package/specialists/org/contracts/business-strategist-to-product-manager.json +0 -39
  514. package/specialists/org/contracts/construct-to-rd-lead.json +0 -53
  515. package/specialists/org/contracts/data-engineer-to-platform-engineer.json +0 -36
  516. package/specialists/org/contracts/designer-to-accessibility.json +0 -36
  517. package/specialists/org/contracts/platform-engineer-to-engineer.json +0 -31
  518. package/specialists/org/contracts/qa-to-test-automation.json +0 -42
  519. package/specialists/org/contracts/rd-lead-to-architect.json +0 -38
  520. package/specialists/org/contracts/sre-to-release-manager.json +0 -36
  521. package/specialists/org/specialists/cx-accessibility.json +0 -69
  522. package/specialists/org/specialists/cx-ai-engineer.json +0 -75
  523. package/specialists/org/specialists/cx-business-strategist.json +0 -72
  524. package/specialists/org/specialists/cx-data-engineer.json +0 -71
  525. package/specialists/org/specialists/cx-devil-advocate.json +0 -71
  526. package/specialists/org/specialists/cx-docs-keeper.json +0 -82
  527. package/specialists/org/specialists/cx-evaluator.json +0 -69
  528. package/specialists/org/specialists/cx-explorer.json +0 -69
  529. package/specialists/org/specialists/cx-legal-compliance.json +0 -74
  530. package/specialists/org/specialists/cx-oracle.json +0 -46
  531. package/specialists/org/specialists/cx-platform-engineer.json +0 -80
  532. package/specialists/org/specialists/cx-rd-lead.json +0 -73
  533. package/specialists/org/specialists/cx-release-manager.json +0 -77
  534. package/specialists/org/specialists/cx-sre.json +0 -94
  535. package/specialists/org/specialists/cx-test-automation.json +0 -69
  536. package/specialists/org/specialists/cx-trace-reviewer.json +0 -69
  537. package/specialists/org/specialists/cx-ux-researcher.json +0 -68
  538. package/specialists/org/teams/accessibility-team.json +0 -39
  539. package/specialists/org/teams/ux-research-team.json +0 -39
  540. package/specialists/prompts/cx-accessibility.md +0 -55
  541. package/specialists/prompts/cx-ai-engineer.md +0 -124
  542. package/specialists/prompts/cx-business-strategist.md +0 -58
  543. package/specialists/prompts/cx-data-engineer.md +0 -55
  544. package/specialists/prompts/cx-devil-advocate.md +0 -59
  545. package/specialists/prompts/cx-docs-keeper.md +0 -164
  546. package/specialists/prompts/cx-evaluator.md +0 -49
  547. package/specialists/prompts/cx-explorer.md +0 -71
  548. package/specialists/prompts/cx-legal-compliance.md +0 -58
  549. package/specialists/prompts/cx-oracle.md +0 -98
  550. package/specialists/prompts/cx-platform-engineer.md +0 -97
  551. package/specialists/prompts/cx-rd-lead.md +0 -59
  552. package/specialists/prompts/cx-release-manager.md +0 -54
  553. package/specialists/prompts/cx-sre.md +0 -111
  554. package/specialists/prompts/cx-test-automation.md +0 -55
  555. package/specialists/prompts/cx-trace-reviewer.md +0 -101
  556. package/specialists/prompts/cx-ux-researcher.md +0 -53
@@ -0,0 +1,240 @@
1
+ /**
2
+ * lib/render-visual-check.mjs — rendered-artifact visual gate for the test:visual script.
3
+ *
4
+ * Exports a fixture through the real document-export engines (Pandoc/Typst for PDF, pptxgenjs for
5
+ * PPTX, Pandoc for DOCX) and asserts what a source-level check cannot: (1) for pdf/pptx — the same
6
+ * RENDER_REQUIRED_FORMATS output-quality.mjs already treats as render-required — every rasterized
7
+ * page carries real ink, not a blank/near-blank canvas from a broken template or missing font,
8
+ * decoded straight from the produced PNG's own IDAT stream with zlib inflate plus PNG filter
9
+ * reconstruction (no image-decoding dependency needed); (2) runOutputQuality's text-extraction-
10
+ * parity and on-disk reference-integrity checks pass against the produced file for every format,
11
+ * reusing the same gate publish.mjs enforces; (3) a renderer required for the requested format that
12
+ * is absent is reported as a typed, CI-visible failure reason, never a silent skip, because a
13
+ * visual claim that rests on a tool that was never invoked is not a visual claim. docx has no
14
+ * raster path in render-pipeline.mjs (only pdf/html/pptx/mermaid/d2), so it is validated via export
15
+ * plus text/reference checks only, matching the existing render-required contract.
16
+ */
17
+
18
+ import fs from 'node:fs';
19
+ import os from 'node:os';
20
+ import path from 'node:path';
21
+ import { spawnSync } from 'node:child_process';
22
+ import zlib from 'node:zlib';
23
+
24
+ import { exportMarkdown, whichBin } from './document-export.mjs';
25
+ import { runOutputQuality } from './output-quality.mjs';
26
+
27
+ // Below this ink ratio a rasterized page is treated as blank: a faithful export of a
28
+ // multi-paragraph fixture always covers more of the canvas than antialiasing noise or a stray
29
+ // hairline would produce.
30
+
31
+ const MIN_INK_RATIO = 0.01;
32
+
33
+ // Formats output-quality.mjs already treats as render-required (RENDER_REQUIRED_FORMATS); only
34
+ // these get rasterized and blank-page checked. docx has no renderer in render-pipeline.mjs.
35
+
36
+ const RASTER_FORMATS = new Set(['pdf', 'pptx']);
37
+
38
+ function readPngChunks(pngPath) {
39
+ const buf = fs.readFileSync(pngPath);
40
+ const sig = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
41
+ if (!buf.subarray(0, 8).equals(sig)) throw new Error(`${pngPath}: not a PNG (bad signature)`);
42
+
43
+ let offset = 8;
44
+ let header = null;
45
+ const idatParts = [];
46
+ while (offset < buf.length) {
47
+ const len = buf.readUInt32BE(offset);
48
+ const type = buf.toString('ascii', offset + 4, offset + 8);
49
+ const data = buf.subarray(offset + 8, offset + 8 + len);
50
+ if (type === 'IHDR') {
51
+ header = {
52
+ width: data.readUInt32BE(0),
53
+ height: data.readUInt32BE(4),
54
+ bitDepth: data.readUInt8(8),
55
+ colorType: data.readUInt8(9),
56
+ };
57
+ }
58
+ if (type === 'IDAT') idatParts.push(data);
59
+ offset += 8 + len + 4;
60
+ if (type === 'IEND') break;
61
+ }
62
+ if (!header) throw new Error(`${pngPath}: missing IHDR chunk`);
63
+ if (header.bitDepth !== 8) throw new Error(`${pngPath}: unsupported bit depth ${header.bitDepth} (need 8)`);
64
+ const channels = { 0: 1, 2: 3, 4: 2, 6: 4 }[header.colorType];
65
+ if (!channels) throw new Error(`${pngPath}: unsupported color type ${header.colorType}`);
66
+ return { ...header, channels, raw: zlib.inflateSync(Buffer.concat(idatParts)) };
67
+ }
68
+
69
+ function paeth(a, b, c) {
70
+ const p = a + b - c;
71
+ const pa = Math.abs(p - a);
72
+ const pb = Math.abs(p - b);
73
+ const pc = Math.abs(p - c);
74
+ if (pa <= pb && pa <= pc) return a;
75
+ if (pb <= pc) return b;
76
+ return c;
77
+ }
78
+
79
+ // Reverses the per-scanline PNG filter (none/sub/up/average/paeth) so pixel values are real
80
+ // sample bytes rather than filter-encoded deltas.
81
+
82
+ function unfilterPng({ width, height, channels, raw }) {
83
+ const stride = width * channels;
84
+ const bpp = channels;
85
+ const out = Buffer.alloc(height * stride);
86
+ let pos = 0;
87
+ for (let y = 0; y < height; y++) {
88
+ const filter = raw[pos];
89
+ pos += 1;
90
+ const rowStart = y * stride;
91
+ for (let x = 0; x < stride; x++) {
92
+ const val = raw[pos + x];
93
+ const a = x >= bpp ? out[rowStart + x - bpp] : 0;
94
+ const b = y > 0 ? out[rowStart - stride + x] : 0;
95
+ const c = x >= bpp && y > 0 ? out[rowStart - stride + x - bpp] : 0;
96
+ let recon;
97
+ if (filter === 0) recon = val;
98
+ else if (filter === 1) recon = val + a;
99
+ else if (filter === 2) recon = val + b;
100
+ else if (filter === 3) recon = val + Math.floor((a + b) / 2);
101
+ else if (filter === 4) recon = val + paeth(a, b, c);
102
+ else throw new Error(`unsupported PNG filter type ${filter} at row ${y}`);
103
+ out[rowStart + x] = recon & 0xff;
104
+ }
105
+ pos += stride;
106
+ }
107
+ return out;
108
+ }
109
+
110
+ // Fraction of pixels that are not near-white (allows antialiasing/JPEG-adjacent noise near 255).
111
+
112
+ export function inkRatio(pngPath) {
113
+ const decoded = readPngChunks(pngPath);
114
+ const pixels = unfilterPng(decoded);
115
+ const { channels } = decoded;
116
+ let inked = 0;
117
+ let total = 0;
118
+ for (let i = 0; i < pixels.length; i += channels) {
119
+ total += 1;
120
+ const r = pixels[i];
121
+ const g = channels >= 2 ? pixels[i + 1] : r;
122
+ const b = channels >= 3 ? pixels[i + 2] : r;
123
+ if (r < 245 || g < 245 || b < 245) inked += 1;
124
+ }
125
+ return total === 0 ? 0 : inked / total;
126
+ }
127
+
128
+ function rasterizePdf(pdfPath, outDir) {
129
+ const result = spawnSync('pdftoppm', ['-png', '-r', '72', pdfPath, path.join(outDir, 'page')], { encoding: 'utf8' });
130
+ if (result.status !== 0) throw new Error(`pdftoppm failed: ${(result.stderr || '').trim()}`);
131
+ return fs.readdirSync(outDir)
132
+ .filter((f) => f.startsWith('page') && f.endsWith('.png'))
133
+ .sort()
134
+ .map((f) => path.join(outDir, f));
135
+ }
136
+
137
+ // pptx has no direct rasterizer; render-pipeline.mjs's own pptx path goes soffice -> pdf ->
138
+ // pdftoppm, so this mirrors that exact chain rather than inventing a second one.
139
+
140
+ function rasterizePptx(pptxPath, outDir, env) {
141
+ const soffice = whichBin('soffice', env) || whichBin('libreoffice', env);
142
+ if (!soffice) return { ok: false, reason: 'missing-dependency', message: 'soffice/libreoffice not on PATH' };
143
+ const conv = spawnSync(soffice, ['--headless', '--convert-to', 'pdf', '--outdir', outDir, pptxPath], { encoding: 'utf8' });
144
+ if (conv.status !== 0) return { ok: false, reason: 'unavailable-renderer', message: (conv.stderr || '').trim() || 'soffice convert failed' };
145
+ const pdf = path.join(outDir, `${path.parse(pptxPath).name}.pdf`);
146
+ if (!fs.existsSync(pdf)) return { ok: false, reason: 'unavailable-renderer', message: 'soffice produced no pdf' };
147
+ return { ok: true, images: rasterizePdf(pdf, outDir) };
148
+ }
149
+
150
+ const REQUIRED_ENGINES = {
151
+ pdf: ['pandoc', 'typst', 'pdftoppm'],
152
+ pptx: ['pandoc', 'soffice', 'pdftoppm'],
153
+ docx: ['pandoc'],
154
+ };
155
+
156
+ export function requiredEngineStatus(format, env = process.env) {
157
+ const names = REQUIRED_ENGINES[format] || [];
158
+ const missing = names.filter((name) => {
159
+ if (name === 'soffice') return !whichBin('soffice', env) && !whichBin('libreoffice', env);
160
+ return !whichBin(name, env);
161
+ });
162
+ return { format, required: names, missing, present: missing.length === 0 };
163
+ }
164
+
165
+ // Runs the full rendered-artifact gate for one format: export via the real engine, rasterize when
166
+ // the format is render-required, assert non-blank pages, and run the shared output-quality checks
167
+ // (text parity + references) against the produced file. Returns a typed failure (never throws)
168
+ // when a required engine is absent, so the caller can fail the process loudly instead of skipping.
169
+
170
+ export function checkRenderedArtifact({ fixturePath, format, workDir, env = process.env } = {}) {
171
+ const engineStatus = requiredEngineStatus(format, env);
172
+ if (!engineStatus.present) {
173
+ const message = `test:visual requires real engines for ${format} but ${engineStatus.missing.join(', ')} ${engineStatus.missing.length > 1 ? 'are' : 'is'} not on PATH. Install: ${engineStatus.missing.join(', ')}. This is a hard failure: a rendered-artifact claim cannot rest on a tool that never ran.`;
174
+ return { ok: false, format, skipped: false, reason: 'missing-dependency', message, failures: [message] };
175
+ }
176
+
177
+ const dir = workDir || fs.mkdtempSync(path.join(os.tmpdir(), `cx-visual-${format}-`));
178
+ const outputPath = path.join(dir, `artifact.${format}`);
179
+ const sourceMarkdown = fs.readFileSync(fixturePath, 'utf8');
180
+
181
+ const exported = exportMarkdown({ inputPath: fixturePath, outputPath, format, env });
182
+ if (!exported.ok) {
183
+ const message = `export failed for ${format}: ${exported.message}`;
184
+ return { ok: false, format, skipped: false, reason: 'export-failed', message, failures: [message] };
185
+ }
186
+
187
+ let images = [];
188
+ if (RASTER_FORMATS.has(format)) {
189
+ if (format === 'pdf') {
190
+ images = rasterizePdf(outputPath, dir);
191
+ } else {
192
+ const raster = rasterizePptx(outputPath, dir, env);
193
+ if (!raster.ok) {
194
+ const message = `rasterize failed for ${format}: ${raster.message}`;
195
+ return { ok: false, format, skipped: false, reason: raster.reason, message, failures: [message] };
196
+ }
197
+ images = raster.images;
198
+ }
199
+ if (images.length === 0) {
200
+ const message = `${format} export produced zero rasterizable pages`;
201
+ return { ok: false, format, skipped: false, reason: 'no-pages', message, failures: [message] };
202
+ }
203
+ }
204
+
205
+ const blankPages = [];
206
+ const ratios = [];
207
+ for (const image of images) {
208
+ const ratio = inkRatio(image);
209
+ ratios.push(ratio);
210
+ if (ratio < MIN_INK_RATIO) blankPages.push({ image, ratio });
211
+ }
212
+
213
+ const quality = runOutputQuality({
214
+ exportPath: outputPath,
215
+ format,
216
+ sourceMarkdown,
217
+ baseDir: path.dirname(fixturePath),
218
+ gateLevel: 'render-smoke',
219
+ env,
220
+ });
221
+
222
+ const failures = [];
223
+ if (blankPages.length > 0) {
224
+ failures.push(`${blankPages.length}/${images.length} rendered page(s) are blank (ink ratio < ${MIN_INK_RATIO}): ${blankPages.map((p) => `${path.basename(p.image)}=${p.ratio.toFixed(4)}`).join(', ')}`);
225
+ }
226
+ for (const f of quality.failures) failures.push(f);
227
+
228
+ return {
229
+ ok: failures.length === 0,
230
+ format,
231
+ skipped: false,
232
+ pageCount: images.length,
233
+ inkRatios: ratios,
234
+ quality,
235
+ failures,
236
+ message: failures.length === 0
237
+ ? `${format}: ${images.length ? `${images.length} page(s) rendered, all non-blank; ` : ''}text parity and references verified`
238
+ : failures.join('; '),
239
+ };
240
+ }
@@ -1,11 +1,12 @@
1
1
  /**
2
- * lib/resources/budget.mjs — disk-budget enforcement for .cx/ assets.
2
+ * lib/resources/budget.mjs — disk-budget enforcement for Construct's
3
+ * generated project state.
3
4
  *
4
5
  * Construct generates data continuously: traces, intake archives, task
5
6
  * graphs, worker logs, session records, backups. None of it has a
6
- * natural ceiling, so left unchecked the .cx/ tree grows until it
7
- * threatens the operator's disk. The budget primitive caps each
8
- * category against a config-defined ceiling.
7
+ * natural ceiling, so left unchecked the tree grows until it threatens
8
+ * the operator's disk. The budget primitive caps each category against
9
+ * a config-defined ceiling.
9
10
  *
10
11
  * Two enforcement modes:
11
12
  * - **Hard-reject** (traces, worker logs): observability is
@@ -17,6 +18,13 @@
17
18
  * load-bearing — never reject; emit a warning the operator can
18
19
  * see in the doctor surface, and pruning gets aggressive.
19
20
  *
21
+ * Category locations (ADR-0066): traces and worker-logs are the two
22
+ * hard-reject, highest-volume categories, so they resolve through the
23
+ * machine-scoped state root (`resolveStateDir`) rather than the
24
+ * project's own `.cx/`. The remaining soft-warn categories are smaller
25
+ * and still project-local. "Total .cx/ cap" therefore really means
26
+ * "total generated-state cap" and sums bytes across both locations.
27
+ *
20
28
  * Defaults live in DEFAULT_PROJECT_CONFIG.resources. The operator
21
29
  * raises them in construct.config.json or via the dashboard. Doctor
22
30
  * surfaces usage vs cap at 80% (warn) and 100% (fail).
@@ -24,13 +32,17 @@
24
32
  import fs from 'node:fs';
25
33
  import path from 'node:path';
26
34
  import { loadProjectConfig } from '../config/project-config.mjs';
35
+ import { resolveStateDir } from '../state-root.mjs';
27
36
 
28
37
  export const HARD_REJECT_CATEGORIES = new Set(['traces', 'worker-logs']);
29
38
  export const SOFT_WARN_CATEGORIES = new Set(['intake-archive', 'task-graphs', 'sessions', 'backups', 'handoffs']);
30
39
 
31
- const CATEGORY_PATHS = {
32
- traces: '.cx/traces',
33
- 'worker-logs': '.cx/runtime/worker',
40
+ const STATE_ROOT_CATEGORY_SEGMENTS = {
41
+ traces: ['traces'],
42
+ 'worker-logs': ['runtime', 'worker'],
43
+ };
44
+
45
+ const PROJECT_LOCAL_CATEGORY_PATHS = {
34
46
  'intake-archive': '.cx/intake/processed',
35
47
  'intake-skipped': '.cx/intake/skipped',
36
48
  'task-graphs': '.cx/task-graphs',
@@ -39,6 +51,19 @@ const CATEGORY_PATHS = {
39
51
  handoffs: '.cx/handoffs',
40
52
  };
41
53
 
54
+ const CATEGORY_NAMES = [...Object.keys(STATE_ROOT_CATEGORY_SEGMENTS), ...Object.keys(PROJECT_LOCAL_CATEGORY_PATHS)];
55
+
56
+ // ensureDir:false — this module only measures/prunes usage, it never itself
57
+ // writes a trace or worker-log file, so a measurement call must not conjure
58
+ // the directory it is merely checking.
59
+
60
+ function categoryDir(projectRoot, name) {
61
+ const stateSegments = STATE_ROOT_CATEGORY_SEGMENTS[name];
62
+ if (stateSegments) return resolveStateDir(projectRoot, ...stateSegments, { ensureDir: false });
63
+ const rel = PROJECT_LOCAL_CATEGORY_PATHS[name];
64
+ return rel ? path.join(projectRoot, rel) : null;
65
+ }
66
+
42
67
  function loadResourceBudgets(projectRoot, env) {
43
68
  const { config } = loadProjectConfig(projectRoot, env);
44
69
  return config?.resources ?? {};
@@ -67,13 +92,13 @@ function walkDirSize(dir) {
67
92
  export function measureUsage(projectRoot, env = process.env) {
68
93
  const budgets = loadResourceBudgets(projectRoot, env);
69
94
  const result = { categories: {}, totalCxBytes: 0, totalCxCap: (budgets?.disk?.totalCxMaxMb ?? 2000) * 1024 * 1024 };
70
- for (const [name, rel] of Object.entries(CATEGORY_PATHS)) {
71
- const full = path.join(projectRoot, rel);
95
+ for (const name of CATEGORY_NAMES) {
96
+ const full = categoryDir(projectRoot, name);
72
97
  const { bytes, files } = walkDirSize(full);
73
98
  result.categories[name] = {
74
99
  bytes,
75
100
  files,
76
- path: rel,
101
+ path: full,
77
102
  enforcement: HARD_REJECT_CATEGORIES.has(name) ? 'hard-reject' : 'soft-warn',
78
103
  };
79
104
  result.totalCxBytes += bytes;
@@ -93,7 +118,7 @@ export function warnBudgetOnce(message, env = process.env) {
93
118
  }
94
119
 
95
120
  export function reserveOrReject(projectRoot, category, sizeBytes, env = process.env) {
96
- if (!CATEGORY_PATHS[category]) {
121
+ if (!CATEGORY_NAMES.includes(category)) {
97
122
  return { ok: true, source: 'unknown-category' };
98
123
  }
99
124
  const usage = measureUsage(projectRoot, env);
@@ -123,10 +148,8 @@ export function reserveOrReject(projectRoot, category, sizeBytes, env = process.
123
148
  function listHardRejectFiles(projectRoot) {
124
149
  const files = [];
125
150
  for (const cat of HARD_REJECT_CATEGORIES) {
126
- const rel = CATEGORY_PATHS[cat];
127
- if (!rel) continue;
128
- const dir = path.join(projectRoot, rel);
129
- if (!fs.existsSync(dir)) continue;
151
+ const dir = categoryDir(projectRoot, cat);
152
+ if (!dir || !fs.existsSync(dir)) continue;
130
153
  const walk = (current) => {
131
154
  for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
132
155
  const full = path.join(current, entry.name);
@@ -199,7 +222,7 @@ export function planPrune(projectRoot, env = process.env, opts = {}) {
199
222
  const actions = [];
200
223
 
201
224
  const tracesMaxDays = disk.tracesMaxDays ?? 30;
202
- const tracesDir = path.join(projectRoot, '.cx/traces');
225
+ const tracesDir = categoryDir(projectRoot, 'traces');
203
226
  if (fs.existsSync(tracesDir)) {
204
227
  const cutoff = now - tracesMaxDays * 24 * 60 * 60 * 1000;
205
228
  for (const f of fs.readdirSync(tracesDir)) {
@@ -258,7 +281,7 @@ export function planPrune(projectRoot, env = process.env, opts = {}) {
258
281
  }
259
282
  }
260
283
 
261
- const workerDir = path.join(projectRoot, '.cx/runtime/worker');
284
+ const workerDir = categoryDir(projectRoot, 'worker-logs');
262
285
  const workerMaxMb = disk.workerLogsMaxMb ?? 100;
263
286
  const workerMaxDays = disk.workerLogsMaxDays ?? 14;
264
287
  if (fs.existsSync(workerDir)) {
@@ -7,33 +7,39 @@
7
7
  *
8
8
  * baseOnly: classifier value "core" loads the base role file (no .flavor suffix).
9
9
  * See ADR-0047 for when to split specialists vs add flavors.
10
+ *
11
+ * construct-rf26.11 folded 15 of the 29 legacy specialists into 11 surviving
12
+ * worker anchors as skill bundles rather than separate personas. rolePrefix
13
+ * and classifierKey are unchanged (the skills/roles/*.md overlay files and
14
+ * the classifier heuristics that detect them are untouched); only
15
+ * specialistId is repointed to the anchor that now loads that overlay.
10
16
  */
11
17
 
12
18
  export const SPECIALIST_FLAVOR_BINDINGS = {
13
19
  engineer: { classifierKey: 'engineer', rolePrefix: 'engineer', specialistId: 'cx-engineer' },
14
- 'ai-engineer': { classifierKey: 'aiEngineer', rolePrefix: 'ai-engineer', specialistId: 'cx-ai-engineer', baseOnly: true },
15
- 'platform-engineer': { classifierKey: 'platformEngineer', rolePrefix: 'platform-engineer', specialistId: 'cx-platform-engineer', baseOnly: true },
16
- 'data-engineer': { classifierKey: 'dataEngineer', rolePrefix: 'data-engineer', specialistId: 'cx-data-engineer' },
20
+ 'ai-engineer': { classifierKey: 'aiEngineer', rolePrefix: 'ai-engineer', specialistId: 'cx-engineer', baseOnly: true },
21
+ 'platform-engineer': { classifierKey: 'platformEngineer', rolePrefix: 'platform-engineer', specialistId: 'cx-engineer', baseOnly: true },
22
+ 'data-engineer': { classifierKey: 'dataEngineer', rolePrefix: 'data-engineer', specialistId: 'cx-engineer' },
17
23
  architect: { classifierKey: 'architect', rolePrefix: 'architect', specialistId: 'cx-architect' },
18
24
  'product-manager': { classifierKey: 'productManager', rolePrefix: 'product-manager', specialistId: 'cx-product-manager' },
19
- 'business-strategist': { classifierKey: 'businessStrategist', rolePrefix: 'business-strategist', specialistId: 'cx-business-strategist', baseOnly: true },
25
+ 'business-strategist': { classifierKey: 'businessStrategist', rolePrefix: 'business-strategist', specialistId: 'cx-product-manager', baseOnly: true },
20
26
  qa: { classifierKey: 'qa', rolePrefix: 'qa', specialistId: 'cx-qa' },
21
- 'test-automation': { classifierKey: 'testAutomation', rolePrefix: 'test-automation', specialistId: 'cx-test-automation', baseOnly: true },
27
+ 'test-automation': { classifierKey: 'testAutomation', rolePrefix: 'test-automation', specialistId: 'cx-qa', baseOnly: true },
22
28
  security: { classifierKey: 'security', rolePrefix: 'security', specialistId: 'cx-security' },
23
29
  'data-analyst': { classifierKey: 'dataAnalyst', rolePrefix: 'data-analyst', specialistId: 'cx-data-analyst' },
24
- sre: { classifierKey: 'sre', rolePrefix: 'sre', specialistId: 'cx-sre', baseOnly: true },
30
+ sre: { classifierKey: 'sre', rolePrefix: 'sre', specialistId: 'cx-operations', baseOnly: true },
25
31
  operations: { classifierKey: 'operations', rolePrefix: 'operations', specialistId: 'cx-operations', baseOnly: true },
26
- 'release-manager': { classifierKey: 'releaseManager', rolePrefix: 'release-manager', specialistId: 'cx-release-manager', baseOnly: true },
27
- 'docs-keeper': { classifierKey: 'docsKeeper', rolePrefix: 'docs-keeper', specialistId: 'cx-docs-keeper', baseOnly: true },
32
+ 'release-manager': { classifierKey: 'releaseManager', rolePrefix: 'release-manager', specialistId: 'cx-operations', baseOnly: true },
33
+ 'docs-keeper': { classifierKey: 'docsKeeper', rolePrefix: 'docs-keeper', specialistId: 'cx-operations', baseOnly: true },
28
34
  reviewer: { classifierKey: 'reviewer', rolePrefix: 'reviewer', specialistId: 'cx-reviewer' },
29
- 'devil-advocate': { classifierKey: 'devilAdvocate', rolePrefix: 'devil-advocate', specialistId: 'cx-devil-advocate', baseOnly: true },
30
- evaluator: { classifierKey: 'evaluator', rolePrefix: 'evaluator', specialistId: 'cx-evaluator', baseOnly: true },
31
- 'trace-reviewer': { classifierKey: 'traceReviewer', rolePrefix: 'trace-reviewer', specialistId: 'cx-trace-reviewer', baseOnly: true },
35
+ 'devil-advocate': { classifierKey: 'devilAdvocate', rolePrefix: 'devil-advocate', specialistId: 'cx-reviewer', baseOnly: true },
36
+ evaluator: { classifierKey: 'evaluator', rolePrefix: 'evaluator', specialistId: 'cx-reviewer', baseOnly: true },
37
+ 'trace-reviewer': { classifierKey: 'traceReviewer', rolePrefix: 'trace-reviewer', specialistId: 'cx-reviewer', baseOnly: true },
32
38
  researcher: { classifierKey: 'researcher', rolePrefix: 'researcher', specialistId: 'cx-researcher' },
33
- 'ux-researcher': { classifierKey: 'uxResearcher', rolePrefix: 'ux-researcher', specialistId: 'cx-ux-researcher', baseOnly: true },
34
- explorer: { classifierKey: 'explorer', rolePrefix: 'explorer', specialistId: 'cx-explorer', baseOnly: true },
39
+ 'ux-researcher': { classifierKey: 'uxResearcher', rolePrefix: 'ux-researcher', specialistId: 'cx-researcher', baseOnly: true },
40
+ explorer: { classifierKey: 'explorer', rolePrefix: 'explorer', specialistId: 'cx-researcher', baseOnly: true },
35
41
  designer: { classifierKey: 'designer', rolePrefix: 'designer', specialistId: 'cx-designer' },
36
- accessibility: { classifierKey: 'accessibility', rolePrefix: 'designer.accessibility', specialistId: 'cx-accessibility', baseOnly: true },
42
+ accessibility: { classifierKey: 'accessibility', rolePrefix: 'designer.accessibility', specialistId: 'cx-designer', baseOnly: true },
37
43
  debugger: { classifierKey: 'debugger', rolePrefix: 'debugger', specialistId: 'cx-debugger', baseOnly: true },
38
44
  };
39
45
 
@@ -45,10 +51,26 @@ const BY_SPECIALIST = Object.fromEntries(
45
51
  Object.entries(SPECIALIST_FLAVOR_BINDINGS).map(([name, b]) => [name, b]),
46
52
  );
47
53
 
54
+ // Reverse index: since construct-rf26.11, several short-name keys above share
55
+ // a single specialistId (e.g. engineer/ai-engineer/platform-engineer/
56
+ // data-engineer all point at cx-engineer). bindingForSpecialist(shortName)
57
+ // only ever returns the entry whose OWN key matches the short name, so a
58
+ // caller resolving a flavor by specialistId (rather than by the historical
59
+ // short name) needs every candidate binding for that id, not just one.
60
+ const BY_SPECIALIST_ID = new Map();
61
+ for (const binding of Object.values(SPECIALIST_FLAVOR_BINDINGS)) {
62
+ if (!BY_SPECIALIST_ID.has(binding.specialistId)) BY_SPECIALIST_ID.set(binding.specialistId, []);
63
+ BY_SPECIALIST_ID.get(binding.specialistId).push(binding);
64
+ }
65
+
48
66
  export function bindingForSpecialist(shortName) {
49
67
  return BY_SPECIALIST[shortName] ?? null;
50
68
  }
51
69
 
70
+ export function bindingsForSpecialistId(specialistId) {
71
+ return BY_SPECIALIST_ID.get(specialistId) ?? [];
72
+ }
73
+
52
74
  export function bindingForClassifier(classifierKey) {
53
75
  return BY_CLASSIFIER[classifierKey] ?? null;
54
76
  }
@@ -196,7 +196,14 @@ export function shouldEscalate(event, manifest, now = Date.now()) {
196
196
  return { escalate: false, reason: 'below-threshold', count: hits };
197
197
  }
198
198
 
199
- async function createBdIncident(event, routeResult) {
199
+ // The bd client is injectable so tests can intercept the escalation path
200
+ // without reaching the real, shared bd/dolt database (construct-y4iv) --
201
+ // CONSTRUCT_ROLES_ROOT isolates the dedup-fingerprint state but is also set
202
+ // legitimately in production (lib/oracle/execute.mjs), so it can't double as
203
+ // a "we're in a test" signal. Callers that don't inject a client get the real
204
+ // one, so production behavior (and every other existing caller) is unchanged.
205
+
206
+ async function createBdIncident(event, routeResult, { runBd: runBdImpl = runBd } = {}) {
200
207
  // Dedup: check if this fingerprint has already been raised as a bead
201
208
  if (fingerprintAlreadyRaised(event.fingerprint)) {
202
209
  return { success: true, skipped: true, reason: 'fingerprint-already-raised', fingerprint: event.fingerprint };
@@ -221,7 +228,7 @@ async function createBdIncident(event, routeResult) {
221
228
  null,
222
229
  2
223
230
  );
224
- const result = await runBd(
231
+ const result = await runBdImpl(
225
232
  ['create', title, '-t', 'bug', '-l', labels, '-d', body],
226
233
  { actor: `roles:${routeResult.personaId}`, silent: true, timeoutSeconds: 10, commandTimeoutSeconds: 30 }
227
234
  );
@@ -246,7 +253,7 @@ async function emitToastBestEffort(payload) {
246
253
  } catch { /* dashboard not loaded; toast is best-effort */ }
247
254
  }
248
255
 
249
- export async function tryEscalate(event) {
256
+ export async function tryEscalate(event, deps = {}) {
250
257
  const r = routeEvent(event);
251
258
  if (!r) return { escalated: false, event, reason: 'no-owner' };
252
259
 
@@ -267,7 +274,7 @@ export async function tryEscalate(event) {
267
274
  }
268
275
  } catch { /* cost ledger optional — never block on missing module */ }
269
276
 
270
- const bd = await createBdIncident(event, r);
277
+ const bd = await createBdIncident(event, r, deps);
271
278
  if (!bd.success) {
272
279
  return { escalated: false, event, reason: 'bd-create-failed', error: bd.error };
273
280
  }
@@ -299,12 +306,12 @@ export async function tryEscalate(event) {
299
306
  return { escalated: true, event, personaId: r.personaId, bdIssueId: bd.issueId, decision };
300
307
  }
301
308
 
302
- export async function recordAndMaybeInvoke(eventType, payload = {}) {
309
+ export async function recordAndMaybeInvoke(eventType, payload = {}, deps = {}) {
303
310
  if (process.env.CONSTRUCT_ROLES === 'off') {
304
311
  return { recorded: false, escalated: false, reason: 'global-off' };
305
312
  }
306
313
  const event = emit(eventType, payload);
307
- const r = await tryEscalate(event);
314
+ const r = await tryEscalate(event, deps);
308
315
  return { recorded: true, ...r };
309
316
  }
310
317
 
@@ -366,7 +373,7 @@ export function prunePending({ now = Date.now() } = {}) {
366
373
  // events.jsonl, runs each unescalated one through tryEscalate. Bounded by
367
374
  // `sinceMs` and `maxProcess` so a long-quiet machine can't trigger a flood.
368
375
 
369
- export async function processBacklog({ sinceMs = 60 * 60 * 1000, maxProcess = 10 } = {}) {
376
+ export async function processBacklog({ sinceMs = 60 * 60 * 1000, maxProcess = 10, deps = {} } = {}) {
370
377
  if (process.env.CONSTRUCT_ROLES === 'off') return { processed: 0, escalated: 0, reason: 'global-off' };
371
378
 
372
379
  const ep = _paths.eventsPath();
@@ -390,7 +397,7 @@ export async function processBacklog({ sinceMs = 60 * 60 * 1000, maxProcess = 10
390
397
  for (const e of candidates.reverse()) {
391
398
  if (processed >= maxProcess) break;
392
399
  processed++;
393
- const r = await tryEscalate(e);
400
+ const r = await tryEscalate(e, deps);
394
401
  if (r.escalated) escalated++;
395
402
  }
396
403
  return { processed, escalated };
package/lib/roots.mjs ADDED
@@ -0,0 +1,107 @@
1
+ /**
2
+ * lib/roots.mjs — canonical resolution of the three directory concepts in Construct.
3
+ *
4
+ * packageRoot — where the construct npm package is installed. Resolved from
5
+ * import.meta.url, stable across global install, npx, and local
6
+ * dev symlinks. Points to the directory that contains
7
+ * construct's own package.json. Locate bundled assets here:
8
+ * personas, skills, templates, schemas, hooks.
9
+ *
10
+ * projectRoot — the user's repo being operated on. Discovered at runtime by
11
+ * inspecting --project <dir> in argv, then walking up from cwd
12
+ * looking for .cx/ or package.json. Falls back to cwd when no
13
+ * marker is found. Services (oracle, embed, telemetry) that
14
+ * read or write to the user's repo must use this, not
15
+ * packageRoot.
16
+ *
17
+ * cwd — process.cwd(). Useful for relative-path resolution before
18
+ * projectRoot is determined. Callers should prefer
19
+ * projectRoot once it is resolved.
20
+ */
21
+
22
+ import fs from 'node:fs';
23
+ import path from 'node:path';
24
+ import { fileURLToPath } from 'node:url';
25
+
26
+ // Under `bun build --compile`, every module's import.meta.url/dirname resolves
27
+ // inside the bundle's virtual filesystem (`/$bunfs/root/...`) rather than a
28
+ // real on-disk path, so any install-root resolution derived from it collapses
29
+ // to a directory that does not exist and every sibling-data-dir read (skills/,
30
+ // specialists/, templates/, config/, registry/) throws ENOENT. process.execPath
31
+ // still reports the compiled binary's real on-disk location, so callers whose
32
+ // naive resolution lands inside /$bunfs fall back to the binary's own
33
+ // directory. This assumes the data directories ship next to the binary
34
+ // (true for the CI/dev smoke path — dist/<binary> sits one level below the
35
+ // checkout root, the same depth bin/construct and lib/roots.mjs assume); a
36
+ // fully standalone end-user install still needs its own asset story
37
+ // (embedded assets or a co-installed data tree), tracked separately.
38
+
39
+ const BUN_VIRTUAL_FS_PREFIX = '/$bunfs';
40
+
41
+ export function isBunCompiledVirtualPath(candidatePath) {
42
+ return typeof process.versions?.bun === 'string' && candidatePath.startsWith(BUN_VIRTUAL_FS_PREFIX);
43
+ }
44
+
45
+ export function resolveInstallRoot(naiveRoot, { execPath = process.execPath, upFromBinary = 1 } = {}) {
46
+ if (!isBunCompiledVirtualPath(naiveRoot)) return naiveRoot;
47
+ return path.resolve(path.dirname(execPath), ...Array(upFromBinary).fill('..'));
48
+ }
49
+
50
+ // Two dozen lib/*.mjs files use the standard Node "was I run directly"
51
+ // idiom (`import.meta.url === file://${process.argv[1]}`) so they double as
52
+ // standalone scripts. Under a Bun-compiled binary, every bundled module's
53
+ // import.meta.url collapses to the same virtual bunfs path as process.argv[1]
54
+ // (both point at the single compiled executable), so that comparison is true
55
+ // for every one of those files simultaneously — each runs its own top-level
56
+ // CLI logic as an unwanted side effect of merely being imported, and the
57
+ // first one whose logic throws (e.g. lib/headhunt.mjs's bare invocation
58
+ // requiring --for) takes the whole binary down before bin/construct's real
59
+ // dispatch ever executes. There is exactly one legitimate entry point inside
60
+ // a compiled binary (bin/construct itself), so this always resolves false
61
+ // under Bun-compile regardless of the module asking.
62
+
63
+ export function isMainModule(moduleUrl) {
64
+ if (typeof process.versions?.bun === 'string' && isBunCompiledVirtualPath(process.argv[1] || '')) {
65
+ return false;
66
+ }
67
+ return moduleUrl === `file://${process.argv[1]}`;
68
+ }
69
+
70
+ // lib/roots.mjs lives one level below the package root (<package>/lib/roots.mjs).
71
+
72
+ export const packageRoot = resolveInstallRoot(path.resolve(fileURLToPath(import.meta.url), '..', '..'));
73
+
74
+ // Walk upward from `start`, looking for .cx/ first (Construct project marker),
75
+ // then package.json (generic JS project root). Stops at filesystem root.
76
+
77
+ function walkUp(start) {
78
+ let dir = path.resolve(start);
79
+ while (true) {
80
+ if (fs.existsSync(path.join(dir, '.cx'))) return dir;
81
+ if (fs.existsSync(path.join(dir, 'package.json'))) return dir;
82
+ const parent = path.dirname(dir);
83
+ if (parent === dir) return null;
84
+ dir = parent;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Resolve the project root for the user's repo.
90
+ *
91
+ * Resolution order:
92
+ * 1. --project <dir> in process.argv (explicit override)
93
+ * 2. Walk upward from cwd looking for .cx/ or package.json
94
+ * 3. cwd itself as a last resort
95
+ *
96
+ * Pass a cwd argument when the caller controls the starting directory (tests,
97
+ * sub-process workers). Omit it in top-level CLI code to use process.cwd().
98
+ */
99
+ export function resolveProjectRoot(cwd = process.cwd()) {
100
+ const argv = process.argv;
101
+ const flagIndex = argv.indexOf('--project');
102
+ if (flagIndex !== -1 && argv[flagIndex + 1]) {
103
+ return path.resolve(argv[flagIndex + 1]);
104
+ }
105
+
106
+ return walkUp(cwd) ?? path.resolve(cwd);
107
+ }