@geraldmaron/construct 1.4.2 → 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 (519) hide show
  1. package/.env.example +36 -0
  2. package/README.md +41 -7
  3. package/bin/construct +1027 -64
  4. package/examples/distribution/sources/deck-one-pager.md +1 -1
  5. package/lib/acp/server.mjs +21 -7
  6. package/lib/adapters-sync.mjs +2 -1
  7. package/lib/audit-trail.mjs +185 -2
  8. package/lib/beads/auto-close.mjs +2 -1
  9. package/lib/beads/drift.mjs +2 -1
  10. package/lib/bridges/copilot-proxy.mjs +20 -5
  11. package/lib/certification/skill-inventory.mjs +10 -1
  12. package/lib/cli/approvals.mjs +147 -0
  13. package/lib/cli-commands.mjs +105 -14
  14. package/lib/comment-lint.mjs +127 -8
  15. package/lib/config/schema.mjs +6 -29
  16. package/lib/config/source-target-registry.mjs +70 -0
  17. package/lib/config/source-targets.mjs +179 -205
  18. package/lib/contracts/coverage.mjs +77 -0
  19. package/lib/contracts/validate.mjs +102 -13
  20. package/lib/contracts/violation-log.mjs +50 -4
  21. package/lib/db/migrate.mjs +69 -0
  22. package/lib/db/migrations/001_orchestration_runs.sql +9 -0
  23. package/lib/db/migrations/002_queue_provider.sql +50 -0
  24. package/lib/db/migrations/003_worker_registry.sql +17 -0
  25. package/lib/db/migrations/004_trace_events.sql +16 -0
  26. package/lib/db/migrations/005_shared_memory.sql +15 -0
  27. package/lib/db/migrations/006_orchestration_runs_tenant.sql +5 -0
  28. package/lib/decisions/enforced-baseline.json +0 -2
  29. package/lib/decisions/registry.mjs +3 -2
  30. package/lib/deployment/parity-contract.mjs +1 -1
  31. package/lib/deployment-mode.mjs +78 -2
  32. package/lib/diagram-export.mjs +10 -1
  33. package/lib/distill.mjs +5 -2
  34. package/lib/docs-verify.mjs +2 -1
  35. package/lib/doctor/cli.mjs +1 -0
  36. package/lib/doctor/command-on-path.mjs +24 -0
  37. package/lib/doctor/diagnosis.mjs +89 -0
  38. package/lib/doctor/embedding-health.mjs +50 -0
  39. package/lib/doctor/engine-health.mjs +93 -0
  40. package/lib/doctor/graph-validate.mjs +47 -0
  41. package/lib/doctor/index.mjs +18 -4
  42. package/lib/doctor/sidecar-providers.mjs +56 -0
  43. package/lib/doctor/watchers/consistency.mjs +93 -1
  44. package/lib/doctor/watchers/cx-budget.mjs +1 -1
  45. package/lib/doctor/watchers/graph-staleness.mjs +1 -1
  46. package/lib/doctor/watchers/mcp-protocol.mjs +17 -0
  47. package/lib/doctor/watchers/oracle-liveness.mjs +92 -0
  48. package/lib/doctor/watchers/orchestration-runs.mjs +108 -0
  49. package/lib/doctor/watchers/provider-breaker.mjs +78 -0
  50. package/lib/document-export.mjs +52 -27
  51. package/lib/document-extract/docling-client.mjs +9 -5
  52. package/lib/document-extract/docling-sidecar.py +30 -0
  53. package/lib/document-extract.mjs +1 -1
  54. package/lib/document-ingest.mjs +9 -0
  55. package/lib/embed/approval-queue.mjs +184 -88
  56. package/lib/embed/authority-guard.mjs +65 -2
  57. package/lib/embed/capability-jobs.mjs +365 -0
  58. package/lib/embed/capability-lifecycle.mjs +219 -0
  59. package/lib/embed/capability-loader.mjs +303 -0
  60. package/lib/embed/capability-runtime.mjs +58 -0
  61. package/lib/embed/cli.mjs +185 -8
  62. package/lib/embed/config.mjs +4 -0
  63. package/lib/embed/daemon.mjs +125 -6
  64. package/lib/embed/demand-fetch.mjs +190 -54
  65. package/lib/embed/inbox.mjs +12 -10
  66. package/lib/embed/presets/ops-triage.mjs +236 -0
  67. package/lib/embed/presets/pm-feedback.mjs +341 -0
  68. package/lib/embed/presets/tpm.mjs +401 -0
  69. package/lib/embed/providers/jira.mjs +72 -1
  70. package/lib/embed/providers/registry.mjs +140 -33
  71. package/lib/embedded-contract/capability.mjs +4 -0
  72. package/lib/embedded-contract/execution.mjs +1 -1
  73. package/lib/embedded-contract/model-resolve.mjs +53 -3
  74. package/lib/embedded-contract/workflow-defs.mjs +48 -73
  75. package/lib/embedded-contract/workflow-invoke.mjs +144 -4
  76. package/lib/embedded-contract/workflows/architecture-review.manifest.json +15 -0
  77. package/lib/embedded-contract/workflows/data-structure.manifest.json +14 -0
  78. package/lib/embedded-contract/workflows/evidence-ingest.manifest.json +14 -0
  79. package/lib/embedded-contract/workflows/memo-draft.manifest.json +14 -0
  80. package/lib/embedded-contract/workflows/operations-triage.manifest.json +18 -0
  81. package/lib/embedded-contract/workflows/operations.manifest.json +18 -0
  82. package/lib/embedded-contract/workflows/pm-feedback.manifest.json +18 -0
  83. package/lib/embedded-contract/workflows/prd-draft.manifest.json +15 -0
  84. package/lib/embedded-contract/workflows/proposal-review.manifest.json +15 -0
  85. package/lib/embedded-contract/workflows/research-synthesis.manifest.json +14 -0
  86. package/lib/embedded-contract/workflows/risk-review.manifest.json +15 -0
  87. package/lib/embedded-contract/workflows/structure-notes.manifest.json +14 -0
  88. package/lib/embedded-contract/workflows/transcript-process.manifest.json +14 -0
  89. package/lib/embedded-contract/workflows/triage.manifest.json +14 -0
  90. package/lib/env-config.mjs +50 -9
  91. package/lib/extensions/index.mjs +27 -0
  92. package/lib/extensions/loader.mjs +120 -0
  93. package/lib/extensions/manifest-schema.mjs +141 -0
  94. package/lib/extensions/manifests/anthropic.manifest.json +12 -0
  95. package/lib/extensions/manifests/atlassian-confluence.manifest.json +12 -0
  96. package/lib/extensions/manifests/atlassian-jira.manifest.json +53 -0
  97. package/lib/extensions/manifests/directory.manifest.json +12 -0
  98. package/lib/extensions/manifests/docling.manifest.json +37 -0
  99. package/lib/extensions/manifests/echo.manifest.json +8 -0
  100. package/lib/extensions/manifests/feedback.manifest.json +12 -0
  101. package/lib/extensions/manifests/github-copilot.manifest.json +12 -0
  102. package/lib/extensions/manifests/github.manifest.json +52 -0
  103. package/lib/extensions/manifests/linear.manifest.json +50 -0
  104. package/lib/extensions/manifests/local.manifest.json +12 -0
  105. package/lib/extensions/manifests/ollama.manifest.json +12 -0
  106. package/lib/extensions/manifests/openai.manifest.json +12 -0
  107. package/lib/extensions/manifests/openrouter-anthropic.manifest.json +12 -0
  108. package/lib/extensions/manifests/openrouter-deepseek.manifest.json +12 -0
  109. package/lib/extensions/manifests/openrouter-google.manifest.json +12 -0
  110. package/lib/extensions/manifests/openrouter-llama.manifest.json +12 -0
  111. package/lib/extensions/manifests/openrouter-qwen.manifest.json +12 -0
  112. package/lib/extensions/manifests/openrouter.manifest.json +12 -0
  113. package/lib/extensions/manifests/postgres.manifest.json +12 -0
  114. package/lib/extensions/manifests/salesforce.manifest.json +12 -0
  115. package/lib/extensions/manifests/slack.manifest.json +58 -0
  116. package/lib/extensions/manifests/whisper.manifest.json +36 -0
  117. package/lib/extensions/validate.mjs +175 -0
  118. package/lib/features.mjs +13 -9
  119. package/lib/flows/checkpoint.mjs +184 -0
  120. package/lib/flows/constants.mjs +27 -0
  121. package/lib/flows/define.mjs +146 -0
  122. package/lib/flows/engine.mjs +249 -0
  123. package/lib/flows/errors.mjs +19 -0
  124. package/lib/flows/index.mjs +27 -0
  125. package/lib/flows/joins.mjs +18 -0
  126. package/lib/flows/schema.mjs +90 -0
  127. package/lib/flows/state.mjs +31 -0
  128. package/lib/frameworks/loader.mjs +99 -0
  129. package/lib/frameworks/schema.mjs +157 -0
  130. package/lib/graph/build-from-corpus.mjs +67 -0
  131. package/lib/graph/build-from-embed.mjs +82 -0
  132. package/lib/graph/build-from-registry.mjs +164 -3
  133. package/lib/graph/cli.mjs +456 -12
  134. package/lib/graph/gap-queries.mjs +156 -0
  135. package/lib/graph/gaps.mjs +41 -0
  136. package/lib/graph/impacted.mjs +129 -0
  137. package/lib/graph/runtime-evidence.mjs +177 -0
  138. package/lib/graph/security-coverage.mjs +113 -0
  139. package/lib/graph/staleness.mjs +241 -10
  140. package/lib/graph/store.mjs +24 -7
  141. package/lib/graph/validate.mjs +161 -0
  142. package/lib/headhunt.mjs +9 -8
  143. package/lib/health-check.mjs +15 -7
  144. package/lib/hook-health.mjs +1 -1
  145. package/lib/hooks/agent-tracker.mjs +10 -4
  146. package/lib/hooks/edit-guard.mjs +3 -3
  147. package/lib/hooks/graph-impact-advisory.mjs +2 -2
  148. package/lib/hooks/guard-bash.mjs +41 -15
  149. package/lib/hooks/mcp-health-check.mjs +11 -4
  150. package/lib/hooks/model-fallback.mjs +50 -6
  151. package/lib/hooks/orchestration-dispatch-guard.mjs +14 -3
  152. package/lib/hooks/pre-compact.mjs +181 -173
  153. package/lib/hooks/rule-verifier.mjs +2 -1
  154. package/lib/hooks/session-reflect.mjs +2 -1
  155. package/lib/hooks/session-start.mjs +3 -3
  156. package/lib/host-capabilities.mjs +13 -2
  157. package/lib/host-disposition.mjs +19 -7
  158. package/lib/identity.mjs +92 -0
  159. package/lib/ingest/degraded-extract.mjs +96 -0
  160. package/lib/ingest/docling-remote.mjs +2 -1
  161. package/lib/ingest/provider-extract.mjs +7 -19
  162. package/lib/ingest/sidecar-providers.mjs +196 -0
  163. package/lib/ingest-tooling.mjs +11 -5
  164. package/lib/init-unified.mjs +55 -38
  165. package/lib/init-update.mjs +2 -1
  166. package/lib/install/legacy-global-cleanup.mjs +25 -0
  167. package/lib/intake/daemon.mjs +99 -8
  168. package/lib/intake/git-queue.mjs +110 -38
  169. package/lib/intake/queue-registry.mjs +50 -0
  170. package/lib/intake/queue.mjs +133 -17
  171. package/lib/intake/session-prelude.mjs +57 -8
  172. package/lib/integrations/intake-integrations.mjs +61 -111
  173. package/lib/intent-classifier.mjs +43 -5
  174. package/lib/libreoffice-export.mjs +8 -8
  175. package/lib/logging/rotate.mjs +5 -4
  176. package/lib/mcp/broker.mjs +332 -17
  177. package/lib/mcp/denied-store.mjs +95 -0
  178. package/lib/mcp/destructive-gate.mjs +30 -0
  179. package/lib/mcp/dispatch-envelope.mjs +199 -0
  180. package/lib/mcp/memory-bridge.mjs +2 -1
  181. package/lib/mcp/server.mjs +154 -1411
  182. package/lib/mcp/tool-definitions-memory.mjs +376 -0
  183. package/lib/mcp/tool-definitions-project.mjs +271 -0
  184. package/lib/mcp/tool-definitions-skills.mjs +311 -0
  185. package/lib/mcp/tool-definitions-workflow.mjs +398 -0
  186. package/lib/mcp/tool-definitions.mjs +25 -0
  187. package/lib/mcp/tool-registry.mjs +107 -0
  188. package/lib/mcp/tool-safety.mjs +1 -0
  189. package/lib/mcp/tools/orchestration-delegation-next.tool.mjs +68 -0
  190. package/lib/mcp/tools/orchestration-run.mjs +165 -25
  191. package/lib/mcp/tools/orchestration-task-result.tool.mjs +77 -0
  192. package/lib/mcp/tools/provider-write.mjs +187 -0
  193. package/lib/mcp/tools/scope.mjs +0 -3
  194. package/lib/mcp/tools/skills.mjs +1 -1
  195. package/lib/mcp/tools/storage.mjs +0 -8
  196. package/lib/mcp/transport/auth.mjs +238 -0
  197. package/lib/mcp/transport/http.mjs +136 -0
  198. package/lib/mcp/transport/mode.mjs +31 -0
  199. package/lib/mcp/transport/stdio.mjs +22 -0
  200. package/lib/mcp-catalog.json +4 -4
  201. package/lib/mcp-manager.mjs +34 -23
  202. package/lib/mcp-platform-config.mjs +31 -7
  203. package/lib/mode-capabilities.mjs +109 -0
  204. package/lib/model-router.mjs +42 -9
  205. package/lib/models/catalog.mjs +40 -1
  206. package/lib/net-guard.mjs +247 -0
  207. package/lib/observation-store.mjs +6 -2
  208. package/lib/ollama/provision-context.mjs +2 -1
  209. package/lib/opencode-config.mjs +22 -3
  210. package/lib/opencode-runtime-plugin.mjs +120 -2
  211. package/lib/oracle/actions.mjs +204 -10
  212. package/lib/oracle/cli.mjs +24 -3
  213. package/lib/oracle/dispatch.mjs +2 -1
  214. package/lib/oracle/execute.mjs +2 -2
  215. package/lib/oracle/gaps.mjs +3 -3
  216. package/lib/oracle/heartbeat.mjs +53 -0
  217. package/lib/oracle/read-model.mjs +69 -13
  218. package/lib/oracle/reconcile.mjs +3 -46
  219. package/lib/oracle/routing.mjs +37 -31
  220. package/lib/oracle/synthesize.mjs +1 -1
  221. package/lib/orchestration/classification.mjs +434 -0
  222. package/lib/orchestration/delegation-flow.mjs +132 -0
  223. package/lib/orchestration/flow-selection.mjs +418 -0
  224. package/lib/orchestration/gates.mjs +244 -0
  225. package/lib/orchestration/host-sampling.mjs +121 -0
  226. package/lib/orchestration/policy-constants.mjs +48 -0
  227. package/lib/orchestration/provider-outcome.mjs +197 -0
  228. package/lib/orchestration/readiness.mjs +114 -13
  229. package/lib/orchestration/run-store-postgres.mjs +32 -26
  230. package/lib/orchestration/run-store-sqlite.mjs +8 -14
  231. package/lib/orchestration/run-store.mjs +25 -12
  232. package/lib/orchestration/runtime.mjs +446 -39
  233. package/lib/orchestration/store.mjs +87 -12
  234. package/lib/orchestration/trace-store.mjs +125 -0
  235. package/lib/orchestration/web-capability.mjs +3 -2
  236. package/lib/orchestration/worker-runtime.mjs +162 -0
  237. package/lib/orchestration/worker.mjs +528 -89
  238. package/lib/orchestration-policy.mjs +67 -1111
  239. package/lib/packs/cli.mjs +144 -0
  240. package/lib/packs/core-pack.mjs +112 -0
  241. package/lib/packs/enablement.mjs +187 -0
  242. package/lib/packs/index.mjs +23 -0
  243. package/lib/packs/loader.mjs +176 -0
  244. package/lib/packs/manifest-schema.mjs +58 -0
  245. package/lib/packs/prompts.mjs +120 -0
  246. package/lib/packs/validate.mjs +208 -0
  247. package/lib/plugin-registry.mjs +4 -5
  248. package/lib/policy/audit-gate.mjs +42 -0
  249. package/lib/policy/consumption-budget.mjs +149 -0
  250. package/lib/policy/engine.mjs +81 -6
  251. package/lib/policy/role-authority.mjs +89 -0
  252. package/lib/prompt-composer.js +19 -3
  253. package/lib/providers/contract/adapters/confluence/governed-write.mjs +215 -0
  254. package/lib/providers/contract/adapters/confluence/manifest.json +17 -0
  255. package/lib/providers/contract/adapters/confluence/transport.mjs +135 -0
  256. package/lib/providers/contract/adapters/github/governed-write.mjs +121 -0
  257. package/lib/providers/contract/adapters/github/index.mjs +38 -3
  258. package/lib/providers/contract/adapters/jira/adf.mjs +151 -0
  259. package/lib/providers/contract/adapters/jira/createmeta.mjs +143 -0
  260. package/lib/providers/contract/adapters/jira/governed-write.mjs +182 -0
  261. package/lib/providers/contract/adapters/jira/manifest.json +17 -0
  262. package/lib/providers/contract/adapters/jira/transport.mjs +119 -0
  263. package/lib/providers/contract.mjs +207 -0
  264. package/lib/providers/credential-bootstrap.mjs +7 -4
  265. package/lib/providers/credential-sources.mjs +14 -2
  266. package/lib/providers/directory/index.mjs +261 -0
  267. package/lib/providers/feedback/index.mjs +392 -0
  268. package/lib/providers/filter-audit.mjs +68 -0
  269. package/lib/providers/filter-schema.mjs +54 -0
  270. package/lib/providers/github/index.mjs +31 -0
  271. package/lib/providers/instance-config.mjs +215 -0
  272. package/lib/providers/op-locate.mjs +96 -0
  273. package/lib/providers/op-run.mjs +64 -9
  274. package/lib/providers/registry.mjs +26 -3
  275. package/lib/providers/secret-audit-wiring.mjs +6 -0
  276. package/lib/providers/secret-resolver.mjs +240 -23
  277. package/lib/publish-template.mjs +6 -3
  278. package/lib/publish-tooling.mjs +4 -0
  279. package/lib/publish.mjs +32 -4
  280. package/lib/queue/pg-queue.mjs +395 -0
  281. package/lib/reflect.mjs +2 -1
  282. package/lib/registry/assemble.mjs +28 -2
  283. package/lib/registry/consolidation.mjs +8 -1
  284. package/lib/registry/custom-scaffold.mjs +200 -0
  285. package/lib/registry/custom-schema.mjs +137 -0
  286. package/lib/registry/loader.mjs +8 -3
  287. package/lib/registry/manifests/format-engines.default.json +15 -0
  288. package/lib/registry/manifests/surface-map.default.json +46 -0
  289. package/lib/registry/retired-paths.mjs +1 -0
  290. package/lib/registry/surface-map.mjs +100 -50
  291. package/lib/render-visual-check.mjs +240 -0
  292. package/lib/resources/budget.mjs +40 -17
  293. package/lib/roles/flavor-bindings.mjs +36 -14
  294. package/lib/roles/gateway.mjs +15 -8
  295. package/lib/roots.mjs +107 -0
  296. package/lib/runtime/uv-bootstrap.mjs +32 -6
  297. package/lib/runtime/whisper-bootstrap.mjs +11 -3
  298. package/lib/runtime-env.mjs +5 -2
  299. package/lib/scheduler/index.mjs +8 -20
  300. package/lib/schema-infer.mjs +31 -2
  301. package/lib/scopes/lifecycle.mjs +29 -25
  302. package/lib/scopes/loader.mjs +49 -12
  303. package/lib/scopes/teams.mjs +1 -1
  304. package/lib/security/ingest-boundary.mjs +80 -0
  305. package/lib/security/recall-wrapper.mjs +99 -0
  306. package/lib/security/trust.mjs +132 -0
  307. package/lib/service-manager.mjs +38 -19
  308. package/lib/setup.mjs +41 -23
  309. package/lib/skills-apply.mjs +4 -2
  310. package/lib/specialists/postconditions.mjs +2 -2
  311. package/lib/state-root.mjs +147 -0
  312. package/lib/status.mjs +597 -6
  313. package/lib/storage/admin.mjs +77 -5
  314. package/lib/storage/backend-registry.mjs +73 -0
  315. package/lib/storage/backend.mjs +63 -9
  316. package/lib/storage/embeddings-openai.mjs +12 -2
  317. package/lib/storage/hybrid-query.mjs +11 -3
  318. package/lib/storage/retrieval-hardening.mjs +384 -0
  319. package/lib/storage/shared-memory.mjs +158 -0
  320. package/lib/storage/vector-client.mjs +69 -2
  321. package/lib/team/health.mjs +72 -0
  322. package/lib/telemetry/backends/local.mjs +9 -3
  323. package/lib/telemetry/client.mjs +3 -7
  324. package/lib/template-registry.mjs +10 -12
  325. package/lib/tenant/context.mjs +82 -0
  326. package/lib/tenant/isolation.mjs +129 -0
  327. package/lib/test-corpus-inventory.mjs +103 -2
  328. package/lib/uninstall/uninstall.mjs +78 -12
  329. package/lib/validator.mjs +4 -6
  330. package/lib/worker/entrypoint.mjs +2 -1
  331. package/lib/worker/run.mjs +2 -2
  332. package/lib/worker/trace.mjs +5 -11
  333. package/lib/workflow-state.mjs +52 -8
  334. package/lib/workflows/liveness.mjs +203 -0
  335. package/lib/workflows/loader.mjs +177 -0
  336. package/lib/workflows/manifest-schema.mjs +71 -0
  337. package/lib/workflows/surface-parity.mjs +118 -0
  338. package/lib/workflows/validate.mjs +127 -0
  339. package/lib/writes/control-plane.mjs +140 -0
  340. package/lib/writes/envelope.mjs +206 -0
  341. package/lib/writes/sent-log.mjs +86 -0
  342. package/lib/writes/write-intent.mjs +109 -0
  343. package/package.json +16 -8
  344. package/personas/construct.md +12 -3
  345. package/registry/capabilities.json +143 -36
  346. package/rules/common/comments.md +1 -0
  347. package/schemas/project-config.schema.json +0 -12
  348. package/schemas/unified-registry.schema.json +2 -4
  349. package/scripts/sync-specialists.mjs +284 -81
  350. package/skills/docs/adr-workflow.md +1 -1
  351. package/skills/docs/codebase-research-workflow.md +3 -3
  352. package/skills/docs/prd-workflow.md +5 -6
  353. package/skills/docs/runbook-workflow.md +2 -2
  354. package/skills/docs/user-research-workflow.md +3 -3
  355. package/skills/operating/fleet-health-routing.md +77 -0
  356. package/skills/roles/ai-engineer.md +1 -1
  357. package/skills/roles/architect.md +0 -1
  358. package/skills/roles/business-strategist.md +1 -1
  359. package/skills/roles/data-analyst.telemetry.md +1 -1
  360. package/skills/roles/data-engineer.md +1 -1
  361. package/skills/roles/data-engineer.pipeline.md +1 -1
  362. package/skills/roles/data-engineer.vector-retrieval.md +1 -2
  363. package/skills/roles/data-engineer.warehouse.md +1 -1
  364. package/skills/roles/designer.accessibility.md +1 -1
  365. package/skills/roles/designer.md +0 -1
  366. package/skills/roles/devil-advocate.md +1 -1
  367. package/skills/roles/docs-keeper.md +1 -1
  368. package/skills/roles/evaluator.md +1 -1
  369. package/skills/roles/explorer.md +1 -1
  370. package/skills/roles/platform-engineer.md +1 -1
  371. package/skills/roles/qa.ai-eval.md +1 -2
  372. package/skills/roles/qa.api-contract.md +0 -1
  373. package/skills/roles/qa.data-pipeline.md +0 -1
  374. package/skills/roles/qa.md +0 -1
  375. package/skills/roles/qa.web-ui.md +0 -1
  376. package/skills/roles/release-manager.md +1 -1
  377. package/skills/roles/researcher.md +0 -2
  378. package/skills/roles/reviewer.md +0 -3
  379. package/skills/roles/security.ai.md +1 -1
  380. package/skills/roles/security.legal-compliance.md +1 -1
  381. package/skills/roles/security.md +0 -1
  382. package/skills/roles/security.privacy.md +0 -1
  383. package/skills/roles/security.supply-chain.md +1 -1
  384. package/skills/roles/sre.md +1 -1
  385. package/skills/roles/test-automation.md +1 -1
  386. package/skills/roles/trace-reviewer.md +1 -1
  387. package/skills/roles/ux-researcher.md +1 -1
  388. package/specialists/artifact-manifest.json +36 -36
  389. package/specialists/org/contracts/accessibility-to-qa.json +11 -3
  390. package/specialists/org/contracts/any-to-business-strategist.json +19 -7
  391. package/specialists/org/contracts/any-to-debugger.json +7 -1
  392. package/specialists/org/contracts/any-to-designer.json +7 -1
  393. package/specialists/org/contracts/any-to-docs-keeper.json +18 -4
  394. package/specialists/org/contracts/any-to-explorer.json +13 -4
  395. package/specialists/org/contracts/any-to-sre-incident.json +6 -2
  396. package/specialists/org/contracts/any-to-trace-reviewer.json +16 -4
  397. package/specialists/org/contracts/architect-to-ai-engineer.json +6 -2
  398. package/specialists/org/contracts/architect-to-data-engineer.json +17 -5
  399. package/specialists/org/contracts/architect-to-devil-advocate.json +11 -3
  400. package/specialists/org/contracts/architect-to-engineer.json +20 -4
  401. package/specialists/org/contracts/architect-to-evaluator.json +15 -5
  402. package/specialists/org/contracts/architect-to-legal-compliance.json +15 -5
  403. package/specialists/org/contracts/architect-to-operations.json +17 -4
  404. package/specialists/org/contracts/architect-to-platform-engineer.json +20 -4
  405. package/specialists/org/contracts/construct-to-orchestrator.json +10 -2
  406. package/specialists/org/contracts/data-analyst-to-product-manager.json +11 -2
  407. package/specialists/org/contracts/engineer-to-qa.json +20 -4
  408. package/specialists/org/contracts/engineer-to-reviewer.json +29 -5
  409. package/specialists/org/contracts/explorer-to-engineer.json +11 -3
  410. package/specialists/org/contracts/legal-compliance-to-release-manager.json +12 -4
  411. package/specialists/org/contracts/operations-to-user.json +53 -0
  412. package/specialists/org/contracts/operations-triage-output.json +53 -0
  413. package/specialists/org/contracts/pm-requirements-candidates.json +53 -0
  414. package/specialists/org/contracts/product-manager-to-architect.json +30 -10
  415. package/specialists/org/contracts/product-manager-to-data-analyst.json +13 -3
  416. package/specialists/org/contracts/product-manager-to-ux-researcher.json +15 -5
  417. package/specialists/org/contracts/qa-to-release-manager.json +11 -3
  418. package/specialists/org/contracts/researcher-to-architect.json +10 -2
  419. package/specialists/org/contracts/researcher-to-product-manager.json +16 -5
  420. package/specialists/org/contracts/reviewer-to-security.json +17 -3
  421. package/specialists/org/contracts/test-automation-to-engineer.json +11 -3
  422. package/specialists/org/contracts/trace-reviewer-to-sre.json +12 -4
  423. package/specialists/org/contracts/user-to-construct.json +11 -4
  424. package/specialists/org/frameworks/cx-architect-constraint-option-failure.md +66 -0
  425. package/specialists/org/frameworks/cx-engineer-feasibility-blast-radius.md +66 -0
  426. package/specialists/org/frameworks/cx-ops-dependency-sequencing.md +74 -0
  427. package/specialists/org/frameworks/cx-pm-value-tradeoff.md +66 -0
  428. package/specialists/org/frameworks/cx-qa-risk-based-coverage.md +69 -0
  429. package/specialists/org/groups/engineering-group.json +2 -6
  430. package/specialists/org/groups/governance-group.json +2 -3
  431. package/specialists/org/groups/operations-group.json +5 -8
  432. package/specialists/org/groups/product-group.json +4 -8
  433. package/specialists/org/groups/quality-group.json +3 -7
  434. package/specialists/org/groups/strategy-group.json +6 -9
  435. package/specialists/org/models.json.example +8 -0
  436. package/specialists/org/scopes/creative.json +6 -1
  437. package/specialists/org/scopes/operations.json +7 -1
  438. package/specialists/org/scopes/research.json +6 -1
  439. package/specialists/org/scopes/rnd.json +7 -1
  440. package/specialists/org/specialists/cx-architect.json +27 -8
  441. package/specialists/org/specialists/cx-designer.json +22 -8
  442. package/specialists/org/specialists/cx-engineer.json +71 -7
  443. package/specialists/org/specialists/cx-operations.json +89 -15
  444. package/specialists/org/specialists/cx-orchestrator.json +16 -4
  445. package/specialists/org/specialists/cx-product-manager.json +28 -9
  446. package/specialists/org/specialists/cx-qa.json +16 -6
  447. package/specialists/org/specialists/cx-researcher.json +40 -7
  448. package/specialists/org/specialists/cx-reviewer.json +61 -11
  449. package/specialists/org/specialists/cx-security.json +30 -10
  450. package/specialists/org/teams/design-team.json +3 -4
  451. package/specialists/org/teams/engineering-team.json +3 -10
  452. package/specialists/org/teams/governance-team.json +3 -5
  453. package/specialists/org/teams/operations-team.json +6 -12
  454. package/specialists/org/teams/product-management-team.json +2 -3
  455. package/specialists/org/teams/quality-team.json +4 -12
  456. package/specialists/org/teams/research-team.json +3 -3
  457. package/specialists/org/teams/strategy-team.json +7 -14
  458. package/specialists/prompts/cx-architect.md +20 -1
  459. package/specialists/prompts/cx-data-analyst.md +1 -1
  460. package/specialists/prompts/cx-designer.md +12 -4
  461. package/specialists/prompts/cx-engineer.md +15 -1
  462. package/specialists/prompts/cx-operations.md +14 -2
  463. package/specialists/prompts/cx-orchestrator.md +9 -5
  464. package/specialists/prompts/cx-product-manager.md +5 -1
  465. package/specialists/prompts/cx-qa.md +6 -2
  466. package/specialists/prompts/cx-researcher.md +25 -30
  467. package/specialists/prompts/cx-reviewer.md +19 -1
  468. package/specialists/prompts/cx-security.md +5 -1
  469. package/templates/distribution/construct-brand.typ +123 -59
  470. package/templates/distribution/construct-decision.typ +6 -3
  471. package/templates/distribution/construct-pdf.typ +11 -4
  472. package/templates/distribution/construct-prd.typ +6 -3
  473. package/templates/distribution/construct-research.typ +7 -4
  474. package/lib/providers/contract/adapters/git/index.mjs +0 -115
  475. package/lib/providers/contract/adapters/slack/index.mjs +0 -175
  476. package/specialists/org/contracts/business-strategist-to-product-manager.json +0 -39
  477. package/specialists/org/contracts/construct-to-rd-lead.json +0 -53
  478. package/specialists/org/contracts/data-engineer-to-platform-engineer.json +0 -36
  479. package/specialists/org/contracts/designer-to-accessibility.json +0 -36
  480. package/specialists/org/contracts/platform-engineer-to-engineer.json +0 -31
  481. package/specialists/org/contracts/qa-to-test-automation.json +0 -42
  482. package/specialists/org/contracts/rd-lead-to-architect.json +0 -38
  483. package/specialists/org/contracts/sre-to-release-manager.json +0 -36
  484. package/specialists/org/specialists/cx-accessibility.json +0 -69
  485. package/specialists/org/specialists/cx-ai-engineer.json +0 -75
  486. package/specialists/org/specialists/cx-business-strategist.json +0 -72
  487. package/specialists/org/specialists/cx-data-engineer.json +0 -71
  488. package/specialists/org/specialists/cx-devil-advocate.json +0 -71
  489. package/specialists/org/specialists/cx-docs-keeper.json +0 -82
  490. package/specialists/org/specialists/cx-evaluator.json +0 -69
  491. package/specialists/org/specialists/cx-explorer.json +0 -69
  492. package/specialists/org/specialists/cx-legal-compliance.json +0 -74
  493. package/specialists/org/specialists/cx-oracle.json +0 -46
  494. package/specialists/org/specialists/cx-platform-engineer.json +0 -80
  495. package/specialists/org/specialists/cx-rd-lead.json +0 -73
  496. package/specialists/org/specialists/cx-release-manager.json +0 -77
  497. package/specialists/org/specialists/cx-sre.json +0 -94
  498. package/specialists/org/specialists/cx-test-automation.json +0 -69
  499. package/specialists/org/specialists/cx-trace-reviewer.json +0 -69
  500. package/specialists/org/specialists/cx-ux-researcher.json +0 -68
  501. package/specialists/org/teams/accessibility-team.json +0 -39
  502. package/specialists/org/teams/ux-research-team.json +0 -39
  503. package/specialists/prompts/cx-accessibility.md +0 -55
  504. package/specialists/prompts/cx-ai-engineer.md +0 -124
  505. package/specialists/prompts/cx-business-strategist.md +0 -58
  506. package/specialists/prompts/cx-data-engineer.md +0 -55
  507. package/specialists/prompts/cx-devil-advocate.md +0 -59
  508. package/specialists/prompts/cx-docs-keeper.md +0 -164
  509. package/specialists/prompts/cx-evaluator.md +0 -49
  510. package/specialists/prompts/cx-explorer.md +0 -71
  511. package/specialists/prompts/cx-legal-compliance.md +0 -58
  512. package/specialists/prompts/cx-oracle.md +0 -98
  513. package/specialists/prompts/cx-platform-engineer.md +0 -97
  514. package/specialists/prompts/cx-rd-lead.md +0 -59
  515. package/specialists/prompts/cx-release-manager.md +0 -54
  516. package/specialists/prompts/cx-sre.md +0 -111
  517. package/specialists/prompts/cx-test-automation.md +0 -55
  518. package/specialists/prompts/cx-trace-reviewer.md +0 -101
  519. package/specialists/prompts/cx-ux-researcher.md +0 -53
@@ -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
+ }
@@ -1,9 +1,19 @@
1
1
  /**
2
2
  * lib/runtime/uv-bootstrap.mjs — idempotent uv + docling venv provisioner.
3
3
  *
4
- * On first use: downloads uv via the official Astral installer, creates
5
- * `<rootDir>/.cx/runtime/docling/.venv`, installs the pinned docling release.
6
- * Subsequent calls are no-ops if the venv is already present.
4
+ * One venv per machine, not one per project (construct-rf26.16; ADR-0066
5
+ * called this out as the docling venv's terminal shape). On first use:
6
+ * downloads uv via the official Astral installer, creates
7
+ * `~/.construct/runtime/docling/.venv` (or `$CX_TOOLKIT_DIR/runtime/docling/.venv`)
8
+ * via `resolveSharedRuntimeDir`, installs the pinned docling release.
9
+ * Subsequent calls from any project on this machine are no-ops once the
10
+ * venv is present — there is no project key anywhere in this path.
11
+ *
12
+ * Version pin is the lockfile: `.install-marker.json`'s `doclingVersion`
13
+ * field is compared against DOCLING_PIN on every call. A mismatch (a
14
+ * construct upgrade that bumps the pin) is not silent drift — it clears the
15
+ * stale venv directory and re-provisions from scratch under `force`
16
+ * semantics, rather than layering a new install over an old one.
7
17
  *
8
18
  * Best-practice notes (2026-06):
9
19
  * - uv (Astral) is the consensus Python tooling, ~10× faster than pip, single
@@ -16,19 +26,25 @@
16
26
  * Returns the path to the venv's python binary so callers can spawn the
17
27
  * sidecar without relying on PATH state.
18
28
  */
19
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
29
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
20
30
  import { spawn, spawnSync } from 'node:child_process';
21
31
  import path from 'node:path';
22
32
  import os from 'node:os';
23
33
 
34
+ import { resolveSharedRuntimeDir } from '../state-root.mjs';
35
+
24
36
  const DOCLING_PIN = '2.45.0';
25
37
  const UV_INSTALL_URL = 'https://astral.sh/uv/install.sh';
26
38
  const UV_TIMEOUT_MS = 120_000;
27
39
  const VENV_TIMEOUT_MS = 240_000;
28
40
  const INSTALL_TIMEOUT_MS = 600_000;
29
41
 
30
- export function defaultRuntimeDir(cwd = process.cwd()) {
31
- return path.join(cwd, '.cx', 'runtime', 'docling');
42
+ // ensureDir:false shared by the provisioner (ensureDoclingVenv, which
43
+ // already mkdirs before it writes) and the read-only detector
44
+ // (describeDoclingRuntime); a detect-only call must not conjure the dir.
45
+
46
+ export function defaultRuntimeDir() {
47
+ return resolveSharedRuntimeDir('docling', { ensureDir: false });
32
48
  }
33
49
 
34
50
  function which(bin) {
@@ -127,6 +143,16 @@ export async function ensureDoclingVenv({ runtimeDir = defaultRuntimeDir(), forc
127
143
  return { pythonBin: existingPython, venvDir, runtimeDir, fresh: false };
128
144
  }
129
145
 
146
+ // A version-pin mismatch (existingMarker present but doclingVersion !== DOCLING_PIN)
147
+ // is a controlled re-provision, not silent drift: clear the stale venv outright
148
+ // rather than trust `uv venv` to reconcile an existing directory left by a
149
+ // different docling/Python pin.
150
+
151
+ if (existingMarker && existingMarker.doclingVersion !== DOCLING_PIN) {
152
+ progress(`Pinned docling version changed (${existingMarker.doclingVersion} → ${DOCLING_PIN}) — re-provisioning.`);
153
+ try { rmSync(venvDir, { recursive: true, force: true }); } catch { /* best effort */ }
154
+ }
155
+
130
156
  progress('Provisioning the docling document extractor (first run). This downloads a Python runtime and ML dependencies and can take several minutes; later runs are instant.');
131
157
  const uv = await ensureUv(runtimeDir);
132
158
 
@@ -12,8 +12,9 @@
12
12
  *
13
13
  * Model files are downloaded directly from Hugging Face the first time the
14
14
  * default `base.en` (or env-overridden) model is requested. Models cache at
15
- * <runtimeDir>/models/. base.en is ~150MB; smaller than the small.en or
16
- * medium.en model that callers can opt into via CONSTRUCT_WHISPER_MODEL.
15
+ * <stateRoot>/runtime/whisper/models/ (ADR-0066: machine-scoped, not
16
+ * project-local). base.en is ~150MB; smaller than the small.en or medium.en
17
+ * model that callers can opt into via CONSTRUCT_WHISPER_MODEL.
17
18
  *
18
19
  * No bundled distribution: whisper.cpp upstream does not publish stable
19
20
  * pre-built CLI binaries across platforms, and building from source via
@@ -25,12 +26,19 @@ import { pipeline } from 'node:stream/promises';
25
26
  import path from 'node:path';
26
27
  import os from 'node:os';
27
28
 
29
+ import { resolveStateDir } from '../state-root.mjs';
30
+
28
31
  const HF_MODEL_BASE_URL = 'https://huggingface.co/ggerganov/whisper.cpp/resolve/main';
29
32
  const DEFAULT_MODEL = process.env.CONSTRUCT_WHISPER_MODEL || 'base.en';
30
33
  const KNOWN_MODELS = new Set(['tiny', 'tiny.en', 'base', 'base.en', 'small', 'small.en', 'medium', 'medium.en', 'large-v3', 'large-v3-turbo']);
31
34
 
35
+ // ensureDir:false — shared by the provisioner (ensureWhisperRuntime, which
36
+ // already mkdirs before it writes) and the read-only detector
37
+ // (describeWhisperRuntime/locateWhisperBinary); a detect-only call must not
38
+ // conjure the dir.
39
+
32
40
  export function defaultRuntimeDir(cwd = process.cwd()) {
33
- return path.join(cwd, '.cx', 'runtime', 'whisper');
41
+ return resolveStateDir(cwd, 'runtime', 'whisper', { ensureDir: false });
34
42
  }
35
43
 
36
44
  function which(bin) {
@@ -1,8 +1,11 @@
1
1
  /**
2
2
  * lib/runtime-env.mjs — Construct runtime environment with credential bootstrap.
3
3
  *
4
- * prepareConstructEnv merges ~/.config/construct/config.env into the process environment.
5
- * Credential auto-link from 1Password runs only when autoLink:true (setup-credentials).
4
+ * prepareConstructEnv merges the user's ~/.config/construct/config.env and,
5
+ * when the caller passes `rootDir` (the resolved project root, not the
6
+ * toolkit install checkout), the project .env into the process environment —
7
+ * project .env wins. Credential auto-link from 1Password runs only when
8
+ * autoLink:true (setup-credentials).
6
9
  */
7
10
 
8
11
  import { loadConstructEnv } from './env-config.mjs';
@@ -8,11 +8,15 @@
8
8
  *
9
9
  * Jobs registered here: 'tag-candidate-mining', 'skill-usage-rollup', 'doc-hygiene-scan'
10
10
  *
11
- * Deployment mode is determined by getDeploymentMode(env), which inspects env vars.
11
+ * Deployment mode is resolved through the canonical lib/deployment-mode.mjs
12
+ * getDeploymentMode (CONSTRUCT_DEPLOYMENT_MODE env, then project config, then solo),
13
+ * so the scheduler agrees with the broker, session prelude, and status surfaces.
12
14
  * Registration is in-memory: no persistence across process restarts beyond the
13
15
  * native trigger files written by solo.mjs or the DB rows in 009_scheduler.sql.
14
16
  */
15
17
 
18
+ import { getDeploymentMode } from '../deployment-mode.mjs';
19
+
16
20
  // ---------------------------------------------------------------------------
17
21
  // In-memory job registry
18
22
  // ---------------------------------------------------------------------------
@@ -57,22 +61,6 @@ export function listJobs() {
57
61
  }));
58
62
  }
59
63
 
60
- /**
61
- * Derives the deployment mode from environment variables.
62
- * Returns 'enterprise' | 'team' | 'solo'.
63
- *
64
- * CONSTRUCT_DEPLOYMENT=enterprise or CONSTRUCT_ENTERPRISE=1 => enterprise
65
- * CONSTRUCT_DEPLOYMENT=team or CONSTRUCT_TEAM=1 => team
66
- * Otherwise => solo
67
- */
68
- export function getDeploymentMode(env = process.env) {
69
- const explicit = env.CONSTRUCT_DEPLOYMENT;
70
- if (explicit === 'enterprise' || env.CONSTRUCT_ENTERPRISE === '1') return 'enterprise';
71
- if (explicit === 'team' || env.CONSTRUCT_TEAM === '1') return 'team';
72
- if (explicit === 'solo') return 'solo';
73
- return 'solo';
74
- }
75
-
76
64
  // ---------------------------------------------------------------------------
77
65
  // Built-in job registrations
78
66
  // ---------------------------------------------------------------------------
@@ -104,8 +92,8 @@ registerJob({
104
92
  const DOC_HYGIENE_SOLO_CRON = '0 2 * * *';
105
93
  const DOC_HYGIENE_TEAM_CRON = '0 * * * *';
106
94
 
107
- export function resolveDocHygieneSchedule(env = process.env) {
108
- const mode = getDeploymentMode(env);
95
+ export function resolveDocHygieneSchedule(env = process.env, { cwd } = {}) {
96
+ const mode = getDeploymentMode(env, { cwd });
109
97
  const isTeamish = mode === 'team' || mode === 'enterprise';
110
98
  return {
111
99
  mode: isTeamish ? 'team' : 'solo',
@@ -121,7 +109,7 @@ registerJob({
121
109
  mode: docHygiene.mode,
122
110
  handler: async ({ cwd, env }) => {
123
111
  const { findHygieneCandidates } = await import('../hygiene/scan.mjs');
124
- const { limit } = resolveDocHygieneSchedule(env);
112
+ const { limit } = resolveDocHygieneSchedule(env, { cwd });
125
113
  const candidates = findHygieneCandidates({ cwd, limit });
126
114
  return {
127
115
  status: 'ok',
@@ -25,6 +25,7 @@ import { extractDocumentTextNodeNative, extractDocumentTextAsync } from './docum
25
25
  import { readCurrentModels, readOpenRouterApiKeyFromOpenCodeConfig } from './model-router.mjs';
26
26
  import { getUserEnvPath } from './env-config.mjs';
27
27
  import { pollFreeModels, topForTier } from './model-free-selector.mjs';
28
+ import { extractOpRef, resolveOpRef, resolveSecret } from './providers/secret-resolver.mjs';
28
29
  import { execSync } from 'node:child_process';
29
30
 
30
31
  // ---------------------------------------------------------------------------
@@ -187,9 +188,37 @@ async function fetchWithTimeout(url, options) {
187
188
  }
188
189
  }
189
190
 
191
+ function resolveSecretOrNull(varName, { env = process.env, allowAmbient = true } = {}) {
192
+ try {
193
+ return resolveSecret(varName, { env, allowAmbient });
194
+ } catch {
195
+ return null;
196
+ }
197
+ }
198
+
199
+ function materializeConfiguredKey(rawValue) {
200
+ if (typeof rawValue !== 'string' || rawValue.length === 0) return null;
201
+ const ref = extractOpRef(rawValue);
202
+ try {
203
+ return ref ? resolveOpRef(ref) : rawValue;
204
+ } catch {
205
+ return null;
206
+ }
207
+ }
208
+
209
+ function resolveOpenRouterApiKey() {
210
+ const envKey = resolveSecretOrNull('OPENROUTER_API_KEY');
211
+ if (envKey) return envKey;
212
+ try {
213
+ return materializeConfiguredKey(readOpenRouterApiKeyFromOpenCodeConfig());
214
+ } catch {
215
+ return null;
216
+ }
217
+ }
218
+
190
219
  async function callModel(userContent, { traceId } = {}) {
191
- const orKey = process.env.OPENROUTER_API_KEY || readOpenRouterApiKeyFromOpenCodeConfig();
192
- const anthropicKey = process.env.ANTHROPIC_API_KEY;
220
+ const orKey = resolveOpenRouterApiKey();
221
+ const anthropicKey = resolveSecretOrNull('ANTHROPIC_API_KEY');
193
222
 
194
223
  if (!orKey && !anthropicKey) {
195
224
  throw new Error('No API key found. Set ANTHROPIC_API_KEY or OPENROUTER_API_KEY to use schema inference.');