@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
package/bin/construct CHANGED
@@ -3,7 +3,10 @@
3
3
  * bin/construct — construct CLI entrypoint.
4
4
  *
5
5
  * Parses the command string, dispatches to the appropriate handler, and
6
- * loads environment config from ~/.construct/.env. This is the only
6
+ * loads environment config from the resolved project root's .env (nearest
7
+ * .cx/ or package.json ancestor of cwd, per lib/roots.mjs) and the user
8
+ * config.env ($XDG_CONFIG_HOME/construct/config.env), with project .env
9
+ * winning per lib/env-config.mjs's tier order. This is the only
7
10
  * public-facing binary; all commands are exported from lib/.
8
11
  */
9
12
 
@@ -79,12 +82,29 @@ import { syncFileStateToSql } from '../lib/storage/sync.mjs';
79
82
  import { deleteIngestedArtifacts, getStorageStatus, inferProjectName, resetStorage } from '../lib/storage/admin.mjs';
80
83
  import { runPressureRelease } from '../lib/runtime-pressure.mjs';
81
84
  import { loadRegistry, listSpecialists } from '../lib/registry/loader.mjs';
85
+ import { resolveProjectRoot, resolveInstallRoot } from '../lib/roots.mjs';
82
86
 
83
- const ROOT_DIR = path.resolve(import.meta.dirname, '..');
87
+ // See lib/roots.mjs's resolveInstallRoot for why: under a Bun-compiled
88
+ // binary, import.meta.dirname resolves to the virtual /$bunfs/root rather
89
+ // than a real path, so every sibling data-dir read below would throw ENOENT.
90
+
91
+ const ROOT_DIR = resolveInstallRoot(path.resolve(import.meta.dirname, '..'));
92
+ const PROJECT_ROOT = resolveProjectRoot(process.cwd());
84
93
  const HOME = os.homedir();
85
94
  enableSecretAuditTrail();
86
- ensureConstructCredentials({ env: process.env, cwd: ROOT_DIR, home: HOME });
87
- const ENV = loadConstructEnv({ rootDir: ROOT_DIR, homeDir: HOME, env: process.env });
95
+
96
+ // The project .env tier binds to PROJECT_ROOT (the user's repo), not
97
+ // ROOT_DIR (the toolkit install checkout) — ROOT_DIR stays reserved for
98
+ // locating the toolkit's own assets (registry, skills, hooks) below.
99
+
100
+ ensureConstructCredentials({ env: process.env, cwd: PROJECT_ROOT, home: HOME });
101
+
102
+ // The CLI only fills keys missing from process.env, so a conflicting shell
103
+ // export wins here — the opposite of the MCP server's file-wins merge.
104
+ // shadowWinner: 'shell' keeps the warning honest about which value the CLI
105
+ // actually applies (construct-6y6w.3 / construct-xj96.12).
106
+
107
+ const ENV = loadConstructEnv({ rootDir: PROJECT_ROOT, homeDir: HOME, env: process.env, shadowWinner: 'shell' });
88
108
  for (const [key, value] of Object.entries(ENV)) {
89
109
  if (!(key in process.env)) process.env[key] = value;
90
110
  }
@@ -247,6 +267,12 @@ async function cmdRulesUsage(args = []) {
247
267
  process.exit(1);
248
268
  }
249
269
 
270
+ async function cmdApprovals(args = []) {
271
+ const { runApprovalsCli } = await import('../lib/cli/approvals.mjs');
272
+ const code = await runApprovalsCli(args, { rootDir: ROOT_DIR, homeDir: HOME, env: process.env, println, errorln });
273
+ if (code) process.exit(code);
274
+ }
275
+
250
276
  async function cmdStatus() {
251
277
  const jsonOutput = restArgsCache.includes('--json');
252
278
  const status = await buildStatus({ rootDir: ROOT_DIR, cwd: process.cwd(), homeDir: HOME, env: process.env });
@@ -295,6 +321,45 @@ async function cmdStatus() {
295
321
  } catch { /* best-effort surface; never block status */ }
296
322
  }
297
323
 
324
+ async function cmdWorkers(args = []) {
325
+ const sub = args.find((a) => !a.startsWith('--')) || 'list';
326
+ const wantsJson = args.includes('--json');
327
+ if (sub !== 'list') {
328
+ errorln('Usage: construct workers <list> [--json]');
329
+ process.exit(1);
330
+ }
331
+
332
+ const { createSqlClient, closeSqlClient } = await import('../lib/storage/backend.mjs');
333
+ const { WorkerRegistry } = await import('../lib/orchestration/worker-runtime.mjs');
334
+ const { resolveIntakeTenantId, INTAKE_PROJECT_ENV_KEY } = await import('../lib/intake/queue.mjs');
335
+ const sql = createSqlClient(process.env);
336
+ const project = process.env[INTAKE_PROJECT_ENV_KEY] || path.basename(path.resolve(process.cwd())) || 'construct';
337
+ const tenantId = resolveIntakeTenantId(process.env);
338
+
339
+ if (!sql) {
340
+ const payload = { status: 'unavailable', reason: 'postgres-unavailable', workers: [] };
341
+ if (wantsJson) { println(JSON.stringify(payload, null, 2)); return; }
342
+ println('Workers unavailable: DATABASE_URL or CONSTRUCT_DATABASE_URL is not configured.');
343
+ return;
344
+ }
345
+
346
+ try {
347
+ const registry = new WorkerRegistry({ sql, project, tenantId });
348
+ const workers = await registry.list();
349
+ const payload = { status: 'available', project, tenantId, workers };
350
+ if (wantsJson) { println(JSON.stringify(payload, null, 2)); return; }
351
+ if (!workers.length) {
352
+ println(`No workers registered for ${project}/${tenantId}.`);
353
+ return;
354
+ }
355
+ for (const worker of workers) {
356
+ println(`${worker.workerId} ${worker.status} heartbeat=${worker.heartbeatAt || 'never'} ttl=${worker.ttlSeconds}s`);
357
+ }
358
+ } finally {
359
+ await closeSqlClient(sql);
360
+ }
361
+ }
362
+
298
363
  async function cmdSync(args) {
299
364
  const { startOpLog } = await import('../lib/op-log.mjs');
300
365
  const opLog = startOpLog('sync', { homeDir: HOME });
@@ -345,6 +410,20 @@ async function cmdSync(args) {
345
410
  }
346
411
  }
347
412
 
413
+ // Advisory only: workflow-defs.mjs (LMCP-D2) can only drift from the
414
+ // manifests via a source regression, which release:check's --check run
415
+ // treats as a hard failure. Sync surfaces it early without blocking.
416
+
417
+ try {
418
+ const driftStatus = runNodeScript(path.join(ROOT_DIR, 'scripts', 'check-workflow-defs-drift.mjs'), ['--check'], {}, { exitOnError: false });
419
+ if (driftStatus !== 0) {
420
+ errorln('workflow-defs.mjs drift detected — run: npm run workflow-defs:check');
421
+ }
422
+ opLog.event('workflow-defs-check', { status: driftStatus });
423
+ } catch (err) {
424
+ opLog.event('workflow-defs-check-error', { message: err.message });
425
+ }
426
+
348
427
  if (args.includes('--no-docs')) {
349
428
  opLog.close('ok', { docs: 'skipped' });
350
429
  return;
@@ -514,12 +593,24 @@ async function cmdDoctor() {
514
593
  const wantsFixMigrateState = wantsFixAll || args.includes('--fix-migrate-state');
515
594
 
516
595
  const checks = [];
517
- const add = (label, pass, optional = false) => checks.push({ label, pass, optional });
596
+ const { DOCTOR_STATES } = await import('../lib/doctor/diagnosis.mjs');
597
+ const add = (label, pass, optional = false, failureState = pass ? DOCTOR_STATES.healthy : null) => checks.push({ label, pass, optional, failureState });
518
598
  const { isConstructSourceCheckout } = await import('../lib/doctor/source-checkout.mjs');
519
599
  const isSourceCheckout = isConstructSourceCheckout(ROOT_DIR);
520
600
  add('specialists/org exists', fs.existsSync(path.join(ROOT_DIR, 'specialists', 'org')));
521
601
  add('sync-specialists.mjs exists', fs.existsSync(path.join(ROOT_DIR, 'scripts', 'sync-specialists.mjs')));
522
602
 
603
+ // Extension manifests check
604
+ try {
605
+ const extManifestsDir = path.join(ROOT_DIR, 'lib', 'extensions', 'manifests');
606
+ const extManifestFiles = fs.existsSync(extManifestsDir)
607
+ ? fs.readdirSync(extManifestsDir).filter((f) => f.endsWith('.manifest.json'))
608
+ : [];
609
+ add(`Extension manifests: ${extManifestFiles.length} loaded`, extManifestFiles.length > 0);
610
+ } catch {
611
+ add('Extension manifests: check failed', false);
612
+ }
613
+
523
614
  // Project .gitignore must cover .cx/. Auto-fix via --fix-gitignore.
524
615
  // Only checks when cwd looks like a Construct project (has .cx/ or
525
616
  // .construct/); non-project cwds get no warning.
@@ -658,6 +749,31 @@ async function cmdDoctor() {
658
749
  );
659
750
  }
660
751
 
752
+ // Legacy in-project heavy state (ADR-0066): runtime/traces/lancedb now
753
+ // resolve through the machine-scoped state root, so a project that still
754
+ // carries these directories is holding pre-refit state at the old
755
+ // project-local path. No auto-fix — the ADR calls for manual deletion, not
756
+ // a migration shim, since the durable copy already lives at the state root.
757
+
758
+ if (isProject) {
759
+ const legacyHeavyDirs = [
760
+ { rel: path.join('.cx', 'runtime'), label: '.cx/runtime' },
761
+ { rel: path.join('.cx', 'traces'), label: '.cx/traces' },
762
+ { rel: path.join('.cx', 'lancedb'), label: '.cx/lancedb' },
763
+ ]
764
+ .map((d) => ({ ...d, full: path.join(cwd, d.rel) }))
765
+ .filter((d) => fs.existsSync(d.full) && fs.readdirSync(d.full).length > 0);
766
+
767
+ add(
768
+ legacyHeavyDirs.length === 0
769
+ ? 'No legacy in-project heavy state (runtime/traces/lancedb)'
770
+ : `Legacy in-project heavy state found: ${legacyHeavyDirs.map((d) => d.label).join(', ')} — `
771
+ + 'durable state now lives at ~/.construct/projects/<key>/ (ADR-0066); delete the old directories manually to reclaim disk',
772
+ true,
773
+ legacyHeavyDirs.length !== 0,
774
+ );
775
+ }
776
+
661
777
  // Mirror model tier overrides stranded in the pre-XDG legacy config forward
662
778
  // before resolving tiers, so an install upgraded across the XDG move reports
663
779
  // configured without a manual copy. Applied to process.env too so the check
@@ -687,7 +803,7 @@ async function cmdDoctor() {
687
803
  try {
688
804
  const { resolveModelTiers } = await import('../lib/model-registry.mjs');
689
805
  const resolved = resolveModelTiers({ env: process.env });
690
- if (resolved.complete && !resolved.errors) {
806
+ if (resolved.complete) {
691
807
  add('Models — all tiers configured', true);
692
808
  } else if (resolved.configured === 0) {
693
809
  add('Models — no tier configured (run `construct models --apply`)', false, true);
@@ -701,8 +817,79 @@ async function cmdDoctor() {
701
817
  add(`Models — check failed: ${err.message}`, false, true);
702
818
  }
703
819
 
820
+ // Worker-backend and web-capability visibility (construct-1yhp.2,
821
+ // construct-1yhp.8): reuse buildOrchestrationReadiness — the same
822
+ // resolution orchestration_run itself calls — so doctor can never
823
+ // advertise a backend/web mode that diverges from what a run would
824
+ // actually get. Both lines are advisory only; the resolved values never
825
+ // fail the gate.
826
+ try {
827
+ const { buildOrchestrationReadiness } = await import('../lib/orchestration/readiness.mjs');
828
+ const orchReadiness = buildOrchestrationReadiness({}, { env: process.env, cwd });
829
+ const backendNote = orchReadiness.workerBackend === 'inline'
830
+ ? 'inline (prepare-only — no specialist LLM reasoning runs; set orchestration.workerBackend=provider for real execution)'
831
+ : orchReadiness.workerBackend;
832
+ add(`Worker backend: ${backendNote}`, true, true);
833
+
834
+ const webNote = orchReadiness.webMode === 'unavailable'
835
+ ? 'unavailable (set WEB_SEARCH_URL for governed mode, or CONSTRUCT_ORCHESTRATION_WEB_DELEGATE=1 to delegate to a tool-capable host)'
836
+ : orchReadiness.webMode;
837
+ add(`Web capability: ${webNote}`, true, true);
838
+ } catch (err) {
839
+ add(`Worker backend / web capability check failed: ${err.message}`, false, true);
840
+ }
841
+
842
+ // Provider worker reliability settings (construct-5wkl): the effective
843
+ // per-call timeout and retry policy are resolved the same way the worker
844
+ // itself resolves them (resolveNonNegativeSetting over CONSTRUCT_PROVIDER_*),
845
+ // so this can never drift from what a real provider-backed run applies, and
846
+ // a test-scale or otherwise unintended override is never invisible. Advisory only.
847
+ try {
848
+ const { resolveNonNegativeSetting } = await import('../lib/env-config.mjs');
849
+ const { PROVIDER_TIMEOUT_DEFAULT_MS } = await import('../lib/orchestration/worker.mjs');
850
+ const timeoutMs = resolveNonNegativeSetting(process.env, 'CONSTRUCT_PROVIDER_TIMEOUT_MS', PROVIDER_TIMEOUT_DEFAULT_MS);
851
+ const maxAttempts = resolveNonNegativeSetting(process.env, 'CONSTRUCT_PROVIDER_MAX_ATTEMPTS', 3);
852
+ const retryBaseMs = resolveNonNegativeSetting(process.env, 'CONSTRUCT_PROVIDER_RETRY_BASE_MS', 250);
853
+ add(`Provider timeout: ${timeoutMs}ms · retry attempts: ${maxAttempts} (base backoff ${retryBaseMs}ms)`, true, true);
854
+ } catch (err) {
855
+ add(`Provider reliability settings check failed: ${err.message}`, false, true);
856
+ }
857
+
858
+
859
+ // Opt-in deep credential check (construct-1yhp.5). Default doctor stays
860
+ // presence-first (construct-trxz: a default-on op:// probe would fight the
861
+ // prompt-storm mitigation) — `--probe-providers` performs the real network
862
+ // auth probe only for providers already presence-configured.
863
+ if (args.includes('--probe-providers')) {
864
+ try {
865
+ const { getProviderModelCatalog } = await import('../lib/model-router.mjs');
866
+ const { probeProviderConnection } = await import('../lib/providers/connection-probe.mjs');
867
+ const { providers } = getProviderModelCatalog({ env: process.env });
868
+ const probeable = providers.filter((p) => p.configured && ['openrouter', 'anthropic', 'openai'].includes(p.id));
869
+ if (probeable.length === 0) {
870
+ add('Provider auth probe — no presence-configured openrouter/anthropic/openai provider to probe', true, true);
871
+ }
872
+ for (const p of probeable) {
873
+ const result = await probeProviderConnection(p.id, { env: process.env, cwd: process.cwd() });
874
+ if (result.ok) {
875
+ add(`Provider auth probe — ${p.id}: auth-verified (SERVING)`, true, true);
876
+ } else if (result.code === 'auth_error') {
877
+ add(`Provider auth probe — ${p.id}: ${result.status || '401/403'} (NOT_SERVING) — rotate or re-authenticate the key`, false, true);
878
+ } else if (result.code === 'missing_key') {
879
+ add(`Provider auth probe — ${p.id}: credential did not materialize (op:// ref unresolved) — check the ref or run \`op signin\``, false, true);
880
+ } else {
881
+ add(`Provider auth probe — ${p.id}: ${result.code}${result.status ? ` (${result.status})` : ''} (unverified)`, false, true);
882
+ }
883
+ }
884
+ } catch (err) {
885
+ add(`Provider auth probe failed: ${err.message}`, false, true);
886
+ }
887
+ }
888
+
889
+ const { resolveCommandOnPath } = await import('../lib/doctor/command-on-path.mjs');
890
+
704
891
  add('Node.js 20+ (recommended)', Number.parseInt(process.versions.node.split('.')[0], 10) >= 20);
705
- add('npm available', true);
892
+ add('npm available', resolveCommandOnPath('npm').status === 0);
706
893
  const npmDevdir = process.env.npm_config_devdir ?? process.env.NPM_CONFIG_DEVDIR;
707
894
  if (npmDevdir) {
708
895
  add(
@@ -711,7 +898,7 @@ async function cmdDoctor() {
711
898
  true,
712
899
  );
713
900
  }
714
- const constructOnPath = spawnSync('zsh', ['-lc', 'command -v construct'], { encoding: 'utf8', env: process.env });
901
+ const constructOnPath = resolveCommandOnPath('construct');
715
902
  const constructPath = constructOnPath.status === 0 ? constructOnPath.stdout.trim() : '';
716
903
  const constructVersion = constructPath
717
904
  ? spawnSync(constructPath, ['version'], { encoding: 'utf8', env: process.env })
@@ -762,16 +949,53 @@ async function cmdDoctor() {
762
949
  // Absence is not a doctor finding — VS Code adapter support hasn't landed
763
950
  // yet, so "create one or wait for support" isn't actionable. The line is
764
951
  // suppressed entirely when missing; doctor surfaces facts, not roadmap.
765
- const vscodeSettingsOk = vscodeSettingsPaths.some((candidate) => fs.existsSync(candidate));
952
+ const vscodeSettingsPath = vscodeSettingsPaths.find((candidate) => fs.existsSync(candidate)) ?? null;
953
+ const vscodeSettingsOk = Boolean(vscodeSettingsPath);
766
954
  if (vscodeSettingsOk) add('VS Code settings file', true, true);
955
+
956
+ // classifyHostReadiness distinguishes the discrete host-config states
957
+ // (missing_config/stale_path/jsonc_unpatched/wrong_key/disabled/healthy)
958
+ // instead of collapsing readiness to settings.json presence above — only
959
+ // meaningful once VS Code has a settings file or a project mcp.json to read.
960
+ const vscodeProjectMcpPath = path.join(process.cwd(), '.vscode', 'mcp.json');
961
+ const vscodeMcpPath = fs.existsSync(vscodeProjectMcpPath) ? vscodeProjectMcpPath : null;
962
+ if (vscodeSettingsPath || vscodeMcpPath) {
963
+ const { classifyHostReadiness, HOST_READINESS_NEXT_STEPS } = await import('../lib/host/readiness.mjs');
964
+ const reasonCode = classifyHostReadiness({
965
+ host: 'vscode',
966
+ settingsPath: vscodeSettingsPath,
967
+ mcpPath: vscodeMcpPath,
968
+ root: ROOT_DIR,
969
+ });
970
+ const nextStep = HOST_READINESS_NEXT_STEPS[reasonCode];
971
+ add(
972
+ reasonCode === 'healthy'
973
+ ? 'VS Code MCP host readiness: healthy'
974
+ : `VS Code MCP host readiness: ${reasonCode} — ${nextStep}`,
975
+ reasonCode === 'healthy',
976
+ true,
977
+ );
978
+ }
767
979
  }
768
980
  add('User config ready', fs.existsSync(getUserEnvPath(HOME)) || fs.existsSync(configDir(HOME)), true);
769
981
  add('skills/ directory', fs.existsSync(path.join(ROOT_DIR, 'skills')));
770
982
  add('rules/common/', fs.existsSync(path.join(ROOT_DIR, 'rules', 'common')));
771
983
  add('specialists/prompts/ directory', fs.existsSync(path.join(ROOT_DIR, 'specialists', 'prompts')));
772
984
  add('Hybrid storage env keys documented', fs.existsSync(path.join(ROOT_DIR, '.env.example')));
773
- fs.mkdirSync(path.join(doctorRoot(), 'performance-reviews'), { recursive: true });
774
- add('performance-reviews/ ready', true);
985
+ const perfReviewsDir = path.join(doctorRoot(), 'performance-reviews');
986
+ fs.mkdirSync(perfReviewsDir, { recursive: true });
987
+ add('performance-reviews/ ready', fs.existsSync(perfReviewsDir));
988
+
989
+ // Living-graph truth: doctor runs `graph validate` so workflow/manifest drift
990
+ // surfaces as a health check, not only via the standalone command (LMCP-C7).
991
+
992
+ try {
993
+ const { checkGraphValidateForDoctor } = await import('../lib/doctor/graph-validate.mjs');
994
+ const graphCheck = checkGraphValidateForDoctor({ rootDir: process.cwd() });
995
+ add(graphCheck.label, graphCheck.ok, graphCheck.warning);
996
+ } catch (err) {
997
+ add(`Graph validate check failed: ${err.message}`, false, true);
998
+ }
775
999
 
776
1000
  // construct.config.json — optional today (config is permitted to be absent;
777
1001
  // env vars + defaults still work). The check enforces that when the file
@@ -779,7 +1003,9 @@ async function cmdDoctor() {
779
1003
  try {
780
1004
  const projectCfg = loadProjectConfig(process.cwd(), process.env);
781
1005
  if (projectCfg.source === 'invalid') {
782
- add(`construct.config.json valid (${projectCfg.errors.length} error${projectCfg.errors.length === 1 ? '' : 's'} — run \`construct config validate\`)`, false, false);
1006
+ const detail = projectCfg.errors[0] || 'unknown error';
1007
+ const more = projectCfg.errors.length > 1 ? ` (+${projectCfg.errors.length - 1} more)` : '';
1008
+ add(`construct.config.json invalid: ${detail}${more} — defaults applied; run \`construct config validate\``, false, false);
783
1009
  } else if (projectCfg.source === 'file') {
784
1010
  add(`construct.config.json valid`, true, true);
785
1011
  } else {
@@ -878,7 +1104,7 @@ async function cmdDoctor() {
878
1104
  return !fs.existsSync(path.join(ROOT_DIR, s.promptFile));
879
1105
  });
880
1106
  add('All specialist promptFiles resolve', brokenPromptFiles.length === 0);
881
- const headhuntClassifierAgents = ['cx-docs-keeper', 'cx-researcher', 'cx-architect', 'cx-security', 'cx-engineer'];
1107
+ const headhuntClassifierAgents = ['cx-operations', 'cx-researcher', 'cx-architect', 'cx-security', 'cx-engineer'];
882
1108
  const registryNames = new Set(Object.values(registry.specialists ?? {}).map((s) => `cx-${s.name}`));
883
1109
  const classifierOrphans = headhuntClassifierAgents.filter((n) => !registryNames.has(n));
884
1110
  add('Headhunt classifier agents exist in registry', classifierOrphans.length === 0);
@@ -912,10 +1138,33 @@ async function cmdDoctor() {
912
1138
  const researchLintResults = lintResearchRepo({ rootDir: process.cwd() });
913
1139
  add('Research artifacts meet minimum evidence structure', researchLintResults.every((entry) => entry.errors.length === 0), true);
914
1140
 
915
- const { VectorClient } = await import('../lib/storage/vector-client.mjs');
916
- const vectorClient = new VectorClient({ env: process.env });
917
- const vectorOk = await vectorClient.isHealthy();
918
- add('Vector backend (LanceDB) reachable', vectorOk);
1141
+ // isHealthy() connects to LanceDB, which creates the index directory on
1142
+ // first touch checking existence first keeps doctor from provisioning
1143
+ // the vector store on a project that has never run semantic search
1144
+ // (construct-rf26.17: lazy provisioning, not "on every project touch").
1145
+ const { resolveStateDir } = await import('../lib/state-root.mjs');
1146
+ const lancedbPath = process.env.CONSTRUCT_LANCEDB_PATH || resolveStateDir(process.cwd(), 'lancedb', { ensureDir: false });
1147
+ if (fs.existsSync(lancedbPath)) {
1148
+ const { VectorClient } = await import('../lib/storage/vector-client.mjs');
1149
+ const vectorClient = new VectorClient({ env: process.env });
1150
+ const vectorOk = await vectorClient.isHealthy();
1151
+ add('Vector backend (LanceDB) reachable', vectorOk);
1152
+
1153
+ const { purgeExpiredData } = await import('../lib/storage/admin.mjs');
1154
+ const purge = await purgeExpiredData(process.cwd(), { env: process.env });
1155
+ if (purge.status === 'ok') {
1156
+ const sizeMb = (purge.sizeBytes / 1024 / 1024).toFixed(1);
1157
+ const oldest = purge.oldestRetainedAt ? new Date(purge.oldestRetainedAt).toISOString().slice(0, 10) : 'n/a';
1158
+ add(
1159
+ `Observations (vector index): ${sizeMb}MB, ${purge.remainingCount} row(s), oldest ${oldest}` +
1160
+ (purge.evictedCount > 0 ? ` — evicted ${purge.evictedCount} past ${purge.maxAgeDays}d/${purge.maxRows} rows` : ''),
1161
+ true,
1162
+ true,
1163
+ );
1164
+ }
1165
+ } else {
1166
+ add('Vector backend (LanceDB): not yet provisioned (lazy — first semantic-search use creates it)', true, true);
1167
+ }
919
1168
 
920
1169
  const { regenerateDocs } = await import('../lib/auto-docs.mjs');
921
1170
  const { changed: stale } = await regenerateDocs({ rootDir: ROOT_DIR, check: true });
@@ -948,6 +1197,34 @@ async function cmdDoctor() {
948
1197
  add(label, wired);
949
1198
  }
950
1199
 
1200
+ // Half-initialized Beads: .claude/settings.json references bd but .beads/ is
1201
+ // absent or has no database file. A project set up before D3 (fail-closed Beads)
1202
+ // fired the hooks without a tracker, so bd exits non-zero with no database to read.
1203
+
1204
+ try {
1205
+ const projectSettingsPath = path.join(process.cwd(), '.claude', 'settings.json');
1206
+ let hasHooksRef = false;
1207
+ if (fs.existsSync(projectSettingsPath)) {
1208
+ hasHooksRef = /\bbd\b/.test(fs.readFileSync(projectSettingsPath, 'utf8'));
1209
+ }
1210
+ if (hasHooksRef) {
1211
+ const beadsDir = path.join(process.cwd(), '.beads');
1212
+ const beadsHasDb = fs.existsSync(beadsDir) && (
1213
+ fs.existsSync(path.join(beadsDir, 'issues.jsonl')) ||
1214
+ fs.existsSync(path.join(beadsDir, 'metadata.json'))
1215
+ );
1216
+ add(
1217
+ beadsHasDb
1218
+ ? 'Beads tracker initialized'
1219
+ : 'Beads tracker not initialized (hooks reference bd but .beads/ missing — run: bd init)',
1220
+ beadsHasDb,
1221
+ false,
1222
+ );
1223
+ }
1224
+ } catch (err) {
1225
+ add(`Beads half-staged check failed: ${err.message}`, false, true);
1226
+ }
1227
+
951
1228
  const pluginRegistry = loadPluginRegistry({ cwd: process.cwd(), homeDir: HOME, rootDir: ROOT_DIR, env: process.env });
952
1229
  add('Plugin manifests valid', pluginRegistry.valid);
953
1230
 
@@ -1120,16 +1397,17 @@ async function cmdDoctor() {
1120
1397
  add('No recent hook failures (24h)', false, true);
1121
1398
  }
1122
1399
 
1123
- // Agent contracts integrity
1400
+ // Agent contracts integrity. A contract is broken if it lacks id,
1401
+ // producer, or consumer, or declares neither postconditions nor output —
1402
+ // mirrors the predicate in tests/doctor/contract-validation.test.mjs.
1124
1403
  const { getAllContracts } = await import('../lib/specialist-contracts.mjs');
1125
1404
  const contracts = getAllContracts();
1126
1405
  add('Agent contracts loaded', contracts.length > 0);
1127
1406
  if (contracts.length > 0) {
1128
- const missingTerminals = contracts.filter((c) =>
1129
- c.consumer && !c.consumer.startsWith('cx-') && c.consumer !== 'construct' && c.consumer !== '*'
1130
- ? false : false,
1407
+ const brokenContracts = contracts.filter((c) =>
1408
+ !c.id || !c.producer || !c.consumer || (!c.postconditions && !c.output),
1131
1409
  );
1132
- add('Agent contract schema intact', missingTerminals.length === 0);
1410
+ add('Agent contract schema intact', brokenContracts.length === 0);
1133
1411
  }
1134
1412
 
1135
1413
  // Skills profile — if present, check staleness against current tags
@@ -1154,6 +1432,67 @@ async function cmdDoctor() {
1154
1432
  add('Registry integrity check', false);
1155
1433
  }
1156
1434
 
1435
+ // Deployment-mode capability check: surfaces stub/not-implemented capabilities
1436
+ // for team/enterprise modes so users know which advertised features are unbuilt.
1437
+ // Solo mode is fully-implemented; this check is a no-op there.
1438
+ //
1439
+ // Enterprise mode additionally applies ADR-0057's status contract: a
1440
+ // FAIL-CLOSED capability (tenant-isolation, isolated-workers) is architecturally
1441
+ // non-trivial and "must not be a warning; it must be a hard error" surfaced
1442
+ // "at startup (during construct doctor)". A LATER capability (rbac,
1443
+ // signed-mcp-allowlists) has no runtime effect and must not read as a doctor
1444
+ // failure at all — it is reported informationally only.
1445
+
1446
+ try {
1447
+ const { getModeCapabilityStatus, getUnsupportedCapabilities, getCapabilities, categorizeEnterpriseCapability } =
1448
+ await import('../lib/mode-capabilities.mjs');
1449
+ const currentMode = getDeploymentMode(process.env);
1450
+ const capStatus = getModeCapabilityStatus(currentMode);
1451
+ if (currentMode === 'enterprise') {
1452
+ const allCaps = getCapabilities('enterprise');
1453
+ const failClosed = allCaps.filter((cap) => categorizeEnterpriseCapability(cap) === 'fail-closed');
1454
+ const later = allCaps.filter((cap) => categorizeEnterpriseCapability(cap) === 'later');
1455
+ if (failClosed.length > 0) {
1456
+ add(
1457
+ `Mode capabilities (enterprise): ${failClosed.length} fail-closed, not yet implemented (${failClosed.map((c) => c.label).join(', ')}) — ADR-0057 refuses these hard, not just a warning`,
1458
+ false,
1459
+ false,
1460
+ );
1461
+ } else {
1462
+ add('Mode capabilities (enterprise): no fail-closed gaps', true, true);
1463
+ }
1464
+ if (later.length > 0) {
1465
+ add(`Mode capabilities (enterprise): ${later.length} deferred to a later wave, no runtime effect (${later.map((c) => c.label).join(', ')})`, true, true);
1466
+ }
1467
+ } else if (capStatus !== 'fully-implemented') {
1468
+ const unsupported = getUnsupportedCapabilities(currentMode);
1469
+ add(
1470
+ `Mode capabilities (${currentMode}): ${unsupported.length} stub/not-implemented (${unsupported.map(c => c.label).join(', ')})`,
1471
+ false,
1472
+ true,
1473
+ );
1474
+ } else {
1475
+ add(`Mode capabilities (${currentMode})`, true, true);
1476
+ }
1477
+ } catch (err) {
1478
+ add(`Mode capabilities check failed: ${err.message}`, false, true);
1479
+ }
1480
+
1481
+ try {
1482
+ const currentMode = getDeploymentMode(process.env);
1483
+ if (currentMode === 'team' || currentMode === 'enterprise') {
1484
+ const { summarizeTeamHealth } = await import('../lib/team/health.mjs');
1485
+ const teamHealth = await summarizeTeamHealth({ rootDir: process.cwd(), env: process.env });
1486
+ add(`Team queue/worker health: ${teamHealth.summary}`, teamHealth.status !== 'unavailable');
1487
+ if (teamHealth.status !== 'unavailable') {
1488
+ add(`Team queue DLQ depth: ${teamHealth.deadLetter}`, teamHealth.deadLetter === 0, teamHealth.deadLetter > 0);
1489
+ add(`Team stale workers: ${teamHealth.staleWorkers}`, teamHealth.staleWorkers === 0, teamHealth.staleWorkers > 0);
1490
+ }
1491
+ }
1492
+ } catch (err) {
1493
+ add(`Team health check failed: ${err.message}`, false);
1494
+ }
1495
+
1157
1496
  // Reconciliation drift (ADR-0027). Any task reporting needsRepair is surfaced
1158
1497
  // with its summary and the exact `construct sync --reconcile=<id>` command.
1159
1498
  // Auto tasks that still show drift (e.g. a run with --no-reconcile) and all
@@ -1346,7 +1685,7 @@ async function cmdDev(args = []) {
1346
1685
  process.exit(1);
1347
1686
  }
1348
1687
 
1349
- const { results, recovery } = await startServices({ rootDir: ROOT_DIR, homeDir: HOME, selected });
1688
+ const { results, recovery } = await startServices({ rootDir: PROJECT_ROOT, homeDir: HOME, selected });
1350
1689
  opLog.event('services', { results: results.map((r) => ({ name: r.name, status: r.status })) });
1351
1690
  for (const svc of results) {
1352
1691
  const label = svc.url ? `${svc.name} → ${svc.url}` : svc.name;
@@ -1387,14 +1726,14 @@ async function cmdDev(args = []) {
1387
1726
  const { existsSync } = await import('node:fs');
1388
1727
  const { join } = await import('node:path');
1389
1728
 
1390
- const embedConfigExists = existsSync(join(ROOT_DIR, 'embed.yaml'));
1391
- const config = loadProjectConfig(ROOT_DIR);
1729
+ const embedConfigExists = existsSync(join(PROJECT_ROOT, 'embed.yaml'));
1730
+ const config = loadProjectConfig(PROJECT_ROOT);
1392
1731
  const autoEmbed = config.config?.autoEmbed;
1393
-
1732
+
1394
1733
  if (embedConfigExists && autoEmbed) {
1395
1734
  try {
1396
1735
  const { runEmbedCli } = await import('../lib/embed/cli.mjs');
1397
- await runEmbedCli(['start'], { rootDir: ROOT_DIR });
1736
+ await runEmbedCli(['start'], { rootDir: PROJECT_ROOT });
1398
1737
  opLog.event('embed', { status: 'started' });
1399
1738
  } catch (err) {
1400
1739
  warn(`embed start failed: ${err.message} — check ~/.cx/runtime/embed-daemon.log`);
@@ -1747,18 +2086,38 @@ async function cmdPrune(args) {
1747
2086
  const dryRun = args.includes('--dry-run');
1748
2087
  const { planPrune, executePrune, measureUsage } = await import('../lib/resources/budget.mjs');
1749
2088
  const actions = planPrune(process.cwd(), process.env);
1750
- if (actions.length === 0) { info('Nothing to prune. .cx/ is within all configured budgets.'); return; }
1751
- const byCat = {};
1752
- let totalBytes = 0;
1753
- for (const a of actions) {
1754
- byCat[a.category] = (byCat[a.category] || 0) + 1;
1755
- totalBytes += a.bytes || 0;
1756
- }
1757
- println(`Plan: prune ${actions.length} file(s), free ${Math.round(totalBytes / 1024)}KB`);
1758
- for (const [cat, count] of Object.entries(byCat)) println(` ${cat}: ${count} file(s)`);
2089
+ let pruned = false;
2090
+ if (actions.length > 0) {
2091
+ pruned = true;
2092
+ const byCat = {};
2093
+ let totalBytes = 0;
2094
+ for (const a of actions) {
2095
+ byCat[a.category] = (byCat[a.category] || 0) + 1;
2096
+ totalBytes += a.bytes || 0;
2097
+ }
2098
+ println(`Plan: prune ${actions.length} file(s), free ${Math.round(totalBytes / 1024)}KB`);
2099
+ for (const [cat, count] of Object.entries(byCat)) println(` ${cat}: ${count} file(s)`);
2100
+ if (!dryRun) {
2101
+ const result = executePrune(actions);
2102
+ info(`Pruned ${result.removed.length} file(s), freed ${Math.round(result.bytesFreed / 1024)}KB.`);
2103
+ }
2104
+ }
2105
+
2106
+ // Vector-indexed observations live in LanceDB, not flat files, so they
2107
+ // fall outside planPrune/executePrune's file-list model — purgeExpiredData
2108
+ // is a no-op when the index was never provisioned, matching the lazy
2109
+ // vector-index contract (construct-rf26.17).
2110
+ if (!dryRun) {
2111
+ const { purgeExpiredData } = await import('../lib/storage/admin.mjs');
2112
+ const purge = await purgeExpiredData(process.cwd(), { env: process.env });
2113
+ if (purge.status === 'ok' && purge.evictedCount > 0) {
2114
+ pruned = true;
2115
+ info(`Evicted ${purge.evictedCount} observation(s) past ${purge.maxAgeDays}d/${purge.maxRows}-row cap (${purge.remainingCount} retained).`);
2116
+ }
2117
+ }
2118
+
2119
+ if (!pruned) { info('Nothing to prune. .cx/ is within all configured budgets.'); return; }
1759
2120
  if (dryRun) { info('--dry-run: no files removed.'); return; }
1760
- const result = executePrune(actions);
1761
- info(`Pruned ${result.removed.length} file(s), freed ${Math.round(result.bytesFreed / 1024)}KB.`);
1762
2121
  const after = measureUsage(process.cwd(), process.env);
1763
2122
  println(`.cx/ now at ${Math.round(after.totalCxBytes / 1024 / 1024)}MB / ${Math.round(after.totalCxCap / 1024 / 1024)}MB cap (${Math.round(after.totalCxUsageRatio * 100)}%).`);
1764
2123
  }
@@ -2222,7 +2581,7 @@ async function cmdIntake(args) {
2222
2581
  const queue = createIntakeQueue(cwd, process.env);
2223
2582
 
2224
2583
  if (sub === 'list') {
2225
- const pending = queue.listPending();
2584
+ const pending = await queue.listPending();
2226
2585
  const rows = pending.map((p) => {
2227
2586
  const t = p.triage || {};
2228
2587
  return [p.id, t.intakeType || 'unknown', t.rdStage || 'unknown', t.primaryOwner || '—', t.recommendedAction || '—'];
@@ -2234,7 +2593,7 @@ async function cmdIntake(args) {
2234
2593
  const id = args[1];
2235
2594
  if (sub === 'show') {
2236
2595
  if (!id) { errorln('Usage: construct intake show <id>'); process.exit(1); }
2237
- const entry = queue.read(id);
2596
+ const entry = await queue.read(id);
2238
2597
  if (!entry) { errorln(`No ${rebrand.signalNoun} with id: ${id}`); process.exit(1); }
2239
2598
  const t = entry.triage || {};
2240
2599
  println(`${rebrand.signalNoun.charAt(0).toUpperCase() + rebrand.signalNoun.slice(1)} ${entry.id}`);
@@ -2348,7 +2707,7 @@ async function cmdIntake(args) {
2348
2707
 
2349
2708
  let stamped = null;
2350
2709
  if (outputPath) {
2351
- const packet = queue.read(id);
2710
+ const packet = await queue.read(id);
2352
2711
  if (!packet) {
2353
2712
  errorln(`No intake packet found: ${id}`);
2354
2713
  process.exit(1);
@@ -2374,7 +2733,7 @@ async function cmdIntake(args) {
2374
2733
  || (outputPath ? `output: ${outputPath}` : '');
2375
2734
 
2376
2735
  try {
2377
- const r = queue.markProcessed(id, { processedBy: 'construct-intake-cli', notes });
2736
+ const r = await queue.markProcessed(id, { processedBy: 'construct-intake-cli', notes });
2378
2737
  if (stamped) {
2379
2738
  info(`Marked ${r.id} processed. Stamped ${outputPath} with intake_id=${id} (confidence=${stamped.intake_confidence ?? 'n/a'}).`);
2380
2739
  } else if (noOutputOutcome) {
@@ -2393,7 +2752,7 @@ async function cmdIntake(args) {
2393
2752
  if (!id) { errorln('Usage: construct intake skip <id> [--reason=…]'); process.exit(1); }
2394
2753
  const flags = parseKvFlags(args.slice(2), ['reason']);
2395
2754
  try {
2396
- const r = queue.markSkipped(id, { skippedBy: 'construct-intake-cli', reason: flags.reason || '' });
2755
+ const r = await queue.markSkipped(id, { skippedBy: 'construct-intake-cli', reason: flags.reason || '' });
2397
2756
  info(`Marked ${r.id} skipped.`);
2398
2757
  } catch (err) {
2399
2758
  errorln(err.message || 'mark skipped failed');
@@ -2405,7 +2764,7 @@ async function cmdIntake(args) {
2405
2764
  if (sub === 'reopen') {
2406
2765
  if (!id) { errorln('Usage: construct intake reopen <id>'); process.exit(1); }
2407
2766
  try {
2408
- const r = queue.reopen(id);
2767
+ const r = await queue.reopen(id);
2409
2768
  info(`Reopened ${r.id} (from ${r.from}).`);
2410
2769
  } catch (err) {
2411
2770
  errorln(err.message || 'reopen failed');
@@ -3094,7 +3453,7 @@ async function cmdIntakeIntegrate(id, args, cwd) {
3094
3453
 
3095
3454
  const { createIntakeQueue } = await import('../lib/intake/queue.mjs');
3096
3455
  const queue = createIntakeQueue(cwd, process.env);
3097
- const entry = queue.read(id);
3456
+ const entry = await queue.read(id);
3098
3457
  if (!entry) { errorln(`No intake packet with id: ${id}`); process.exit(1); }
3099
3458
 
3100
3459
  let result;
@@ -3470,7 +3829,7 @@ async function cmdGraph(args) {
3470
3829
  if (!intakeId) { errorln('Usage: construct graph from-intake <intake-id>'); process.exit(1); }
3471
3830
  const { createIntakeQueue } = await import('../lib/intake/queue.mjs');
3472
3831
  const queue = createIntakeQueue(cwd, process.env);
3473
- const entry = queue.read(intakeId);
3832
+ const entry = await queue.read(intakeId);
3474
3833
  if (!entry) { errorln(`No intake packet: ${intakeId}`); process.exit(1); }
3475
3834
  const graph = generateTaskGraphFromTriage({
3476
3835
  triage: entry.triage,
@@ -3508,6 +3867,76 @@ async function cmdGraph(args) {
3508
3867
  process.exit(1);
3509
3868
  }
3510
3869
 
3870
+ function printFlowHelp() {
3871
+ println('Construct flow — resume a checkpointed flow-engine run (ADR-0067).');
3872
+ println('');
3873
+ println('Usage:');
3874
+ println(' construct flow resume <run-id> --flow=<path> [--state=<json>]');
3875
+ println(' Resume <run-id> from its last checkpoint (or start it if none');
3876
+ println(' exists yet) and drive it to completion. <path> loads a JS');
3877
+ println(' flow module (default export from defineFlow). --state supplies');
3878
+ println(' the initial state as a JSON string, used only when starting fresh.');
3879
+ println(' construct flow status <run-id> Print a checkpoint\'s persisted status without driving it.');
3880
+ }
3881
+
3882
+ async function cmdFlow(args) {
3883
+ const sub = args[0];
3884
+ if (!sub || sub === '--help' || sub === '-h') { printFlowHelp(); return; }
3885
+
3886
+ const { loadCheckpoint, runCheckpointed } = await import('../lib/flows/checkpoint.mjs');
3887
+ const cwd = process.cwd();
3888
+
3889
+ if (sub === 'status') {
3890
+ const runId = args[1];
3891
+ if (!runId) { errorln('Usage: construct flow status <run-id>'); process.exit(1); }
3892
+ const checkpoint = loadCheckpoint(cwd, runId);
3893
+ if (!checkpoint) { errorln(`No checkpoint for run "${runId}".`); process.exit(1); }
3894
+ println(`Run: ${checkpoint.runId}`);
3895
+ println(`Flow: ${checkpoint.flowId ?? '(unnamed)'}`);
3896
+ println(`Updated: ${checkpoint.updatedAt}`);
3897
+ println(`Status: ${checkpoint.run.status}`);
3898
+ println(`Completed steps: ${checkpoint.run.completed.join(', ') || '(none)'}`);
3899
+ println(`Frontier: ${checkpoint.run.frontier.join(', ') || '(empty)'}`);
3900
+ return;
3901
+ }
3902
+
3903
+ if (sub === 'resume') {
3904
+ const runId = args[1];
3905
+ const flags = parseKvFlags(args.slice(2), ['flow', 'state']);
3906
+ if (!runId || !flags.flow) { errorln('Usage: construct flow resume <run-id> --flow=<path> [--state=<json>]'); process.exit(1); }
3907
+ const { loadFlow } = await import('../lib/flows/define.mjs');
3908
+ let flow;
3909
+ try {
3910
+ flow = await loadFlow(path.resolve(cwd, flags.flow));
3911
+ } catch (err) {
3912
+ errorln(`construct flow resume: failed to load flow "${flags.flow}": ${err.message}`);
3913
+ process.exit(1);
3914
+ }
3915
+ let initialState = {};
3916
+ if (flags.state) {
3917
+ try {
3918
+ initialState = JSON.parse(flags.state);
3919
+ } catch (err) {
3920
+ errorln(`construct flow resume: --state is not valid JSON: ${err.message}`);
3921
+ process.exit(1);
3922
+ }
3923
+ }
3924
+ const existed = Boolean(loadCheckpoint(cwd, runId));
3925
+ const run = await runCheckpointed(cwd, runId, flow, initialState);
3926
+ info(`${existed ? 'Resumed' : 'Started'} run ${runId} (flow "${flow.id ?? '(unnamed)'}")`);
3927
+ println(`Status: ${run.status}`);
3928
+ println(`Steps completed (cumulative): ${run.history.length}`);
3929
+ if (run.status === 'error' || run.status === 'budget-exhausted') {
3930
+ println(`Error: ${JSON.stringify(run.error)}`);
3931
+ process.exit(1);
3932
+ }
3933
+ return;
3934
+ }
3935
+
3936
+ errorln(`Unknown flow subcommand: ${sub}. Available: resume, status`);
3937
+ process.exit(1);
3938
+ }
3939
+
3511
3940
  async function cmdConfig(args) {
3512
3941
  const sub = args[0];
3513
3942
 
@@ -3694,6 +4123,7 @@ async function cmdOrchestrate(args) {
3694
4123
  const timeoutMs = Number(eqFlag('--timeout-ms') || 8000);
3695
4124
  let observedTools = splitCsv(eqFlag('--observed-tools'));
3696
4125
  let reachableTools = splitCsv(eqFlag('--reachable-tools'));
4126
+ const callerSuppliedObservations = Boolean(observedTools || reachableTools);
3697
4127
  let probeError = null;
3698
4128
 
3699
4129
  if (!rest.includes('--no-probe') && !observedTools && !reachableTools) {
@@ -3718,21 +4148,52 @@ async function cmdOrchestrate(args) {
3718
4148
  reachableTools,
3719
4149
  requiredTools: splitCsv(eqFlag('--required-tools')),
3720
4150
  clientContractVersion: eqFlag('--client-contract-version'),
3721
- observationScope: eqFlag('--observation-scope') || (observedTools || reachableTools ? 'host-session' : 'local-config'),
4151
+ observationScope: eqFlag('--observation-scope') || (callerSuppliedObservations ? 'host-session' : (observedTools || reachableTools ? 'local-probe' : 'local-config')),
3722
4152
  probeError,
3723
4153
  authRequired: rest.includes('--auth-required'),
3724
4154
  }, { env: process.env, cwd: process.cwd() });
3725
4155
  let eventResult = null;
3726
4156
  try { eventResult = recordOrchestrationReadinessEvent(readiness, { homeDir: HOME }); } catch { /* telemetry must not break preflight */ }
3727
4157
 
4158
+ // Opt-in deep credential check (construct-1yhp.5). Default preflight stays
4159
+ // presence-first — `--deep` performs the real network auth probe, only for
4160
+ // providers the presence-based catalog already marks configured.
4161
+ let deepProbes = null;
4162
+ if (rest.includes('--deep')) {
4163
+ try {
4164
+ const { getProviderModelCatalog } = await import('../lib/model-router.mjs');
4165
+ const { probeProviderConnection } = await import('../lib/providers/connection-probe.mjs');
4166
+ const { providers } = getProviderModelCatalog({ env: process.env });
4167
+ const probeable = providers.filter((p) => p.configured && ['openrouter', 'anthropic', 'openai'].includes(p.id));
4168
+ deepProbes = [];
4169
+ for (const p of probeable) {
4170
+ const result = await probeProviderConnection(p.id, { env: process.env, cwd: process.cwd() });
4171
+ deepProbes.push({ provider: p.id, ...result });
4172
+ }
4173
+ } catch (err) {
4174
+ deepProbes = { error: err.message };
4175
+ }
4176
+ }
4177
+
3728
4178
  if (wantsJson) {
3729
- println(JSON.stringify({ ...readiness, eventPath: eventResult?.path ?? null }, null, 2));
4179
+ println(JSON.stringify({ ...readiness, eventPath: eventResult?.path ?? null, deepProbes }, null, 2));
3730
4180
  process.exit(readiness.attached ? 0 : 1);
3731
4181
  }
3732
4182
 
3733
4183
  println(summarizeOrchestrationReadiness(readiness));
3734
4184
  println(`Reason: ${readiness.reasonCode}`);
3735
4185
  println(`Next: ${readiness.nextStep}`);
4186
+ const modelLine = ['reasoning', 'standard', 'fast']
4187
+ .map((tier) => `${tier}=${readiness.modelResolved?.[tier] ? 'resolved' : 'unresolved'}`)
4188
+ .join(' ');
4189
+ println(`Execution: workerBackend=${readiness.workerBackend} webMode=${readiness.webMode} credentialMaterializable=${readiness.credentialMaterializable} (${modelLine})`);
4190
+ if (Array.isArray(deepProbes)) {
4191
+ if (deepProbes.length === 0) println('Deep provider probe: no presence-configured openrouter/anthropic/openai provider to probe.');
4192
+ for (const probe of deepProbes) {
4193
+ if (probe.ok) println(`Deep provider probe — ${probe.provider}: auth-verified (SERVING)`);
4194
+ else println(`Deep provider probe — ${probe.provider}: ${probe.code}${probe.status ? ` (${probe.status})` : ''} (unverified/failing)`);
4195
+ }
4196
+ }
3736
4197
  println(`Diagnostic id: ${readiness.diagnosticBundle?.diagnosticId}`);
3737
4198
  process.exit(readiness.attached ? 0 : 1);
3738
4199
  }
@@ -3750,6 +4211,10 @@ async function cmdOrchestrate(args) {
3750
4211
  if (sub === 'run') {
3751
4212
  const text = flag('--text') || positional.join(' ').trim();
3752
4213
  if (!text) { errorln('Usage: construct orchestrate run "<request>" --remote [--strategy S] [--worker-backend provider] [--json]'); process.exit(1); }
4214
+ if (flag('--worker-backend') === 'host') {
4215
+ errorln('--worker-backend=host requires an attached MCP host session to execute the materialized prompts and submit results back — the CLI (including --remote) has no such session. Use --worker-backend=inline or --worker-backend=provider, or invoke orchestration_run through an MCP host instead.');
4216
+ process.exit(1);
4217
+ }
3753
4218
  const res = await fetch(`${base}/api/orchestration/runs`, { method: 'POST', headers, body: JSON.stringify({ request: text, workflowType: flag('--workflow-type'), requestedStrategy: flag('--strategy') || 'auto', host: flag('--host'), hostModel: flag('--host-model'), hostProvider: flag('--host-provider'), workerBackend: flag('--worker-backend'), fileCount: Number(flag('--file-count') || 0), moduleCount: Number(flag('--module-count') || 0) }) });
3754
4219
  const env = await res.json();
3755
4220
  if (!res.ok) { errorln(`Daemon error (${res.status}): ${env.error || 'unknown'}`); process.exit(1); }
@@ -3758,6 +4223,7 @@ async function cmdOrchestrate(args) {
3758
4223
  return;
3759
4224
  }
3760
4225
  if (sub === 'status') {
4226
+ const { shapeRun } = await import('../lib/mcp/tools/orchestration-run.mjs');
3761
4227
  const runId = positional[0];
3762
4228
  const path = runId ? `/api/orchestration/runs/${encodeURIComponent(runId)}` : '/api/orchestration/runs';
3763
4229
  const res = await fetch(`${base}${path}`, { headers });
@@ -3765,10 +4231,16 @@ async function cmdOrchestrate(args) {
3765
4231
  if (!res.ok) { errorln(`Daemon error (${res.status}): ${env.error || 'unknown'}`); process.exit(1); }
3766
4232
  if (wantsJson) { println(JSON.stringify(env.data, null, 2)); return; }
3767
4233
  if (runId) {
3768
- println(`Run ${env.data.runId} ${env.data.status} · executionMode=${env.data.execution?.executionMode}`);
4234
+ // AP5.4: a remote run payload is just as subject to the honest
4235
+ // terminal-state taxonomy as a local one — never echo its raw status.
4236
+ const shaped = shapeRun(env.data);
4237
+ println(`Run ${env.data.runId} — ${shaped.status} · executionMode=${shaped.executionMode}${shaped.degraded ? ` · degraded: ${shaped.degradationReason}` : ''}`);
3769
4238
  for (const t of env.data.tasks || []) println(` ${t.id} ${t.role} — ${t.status}${t.output ? ` (${t.output.length} chars)` : ''}`);
3770
4239
  } else {
3771
- for (const r of env.data.runs || []) println(` ${r.runId} — ${r.status} · ${r.executionMode || ''} · ${r.createdAt}`);
4240
+ for (const r of env.data.runs || []) {
4241
+ const shaped = shapeRun(r);
4242
+ println(` ${r.runId} — ${shaped.status}${shaped.degraded ? ' (degraded)' : ''} · ${shaped.executionMode || r.executionMode || ''} · ${r.createdAt}`);
4243
+ }
3772
4244
  }
3773
4245
  return;
3774
4246
  }
@@ -3780,9 +4252,21 @@ async function cmdOrchestrate(args) {
3780
4252
 
3781
4253
  if (sub === 'run') {
3782
4254
  const { runOrchestration, planRun, hostAdapterMetadata } = await import('../lib/orchestration/runtime.mjs');
4255
+ const { shapeRun } = await import('../lib/mcp/tools/orchestration-run.mjs');
3783
4256
  const positional = rest.filter((a, i) => !a.startsWith('--') && !(i > 0 && rest[i - 1].startsWith('--') && !['--no-construct', '--no-execute', '--json'].includes(rest[i - 1])));
3784
4257
  const text = flag('--text') || positional.join(' ').trim();
3785
4258
  if (!text) { errorln('Usage: construct orchestrate run "<request>" [--workflow-type T] [--strategy orchestrated|prompt-only|auto] [--host H] [--host-model M] [--no-construct] [--no-execute] [--json]'); process.exit(1); }
4259
+ const cliWorkerBackend = flag('--worker-backend');
4260
+ // The host worker backend materializes prompts for an attached MCP host
4261
+ // session to execute and submit back via orchestration_task_result — a
4262
+ // bare CLI invocation has no such session on the other end, so a run
4263
+ // planned this way would stand at 'awaiting-host' forever with nobody to
4264
+ // submit results. Fail loud here instead of producing that silent
4265
+ // abandoned run (LMCP host-execution).
4266
+ if (cliWorkerBackend === 'host') {
4267
+ errorln('--worker-backend=host requires an attached MCP host session to execute the materialized prompts and submit results back — the CLI has no such session. Use --worker-backend=inline (prepare only) or --worker-backend=provider (needs a provider key), or invoke orchestration_run through an MCP host instead.');
4268
+ process.exit(1);
4269
+ }
3786
4270
  const request = {
3787
4271
  request: text,
3788
4272
  workflowType: flag('--workflow-type'),
@@ -3791,14 +4275,25 @@ async function cmdOrchestrate(args) {
3791
4275
  host: flag('--host'),
3792
4276
  hostModel: flag('--host-model'),
3793
4277
  hostProvider: flag('--host-provider'),
3794
- workerBackend: flag('--worker-backend'),
4278
+ workerBackend: cliWorkerBackend,
3795
4279
  fileCount: Number(flag('--file-count') || 0),
3796
4280
  moduleCount: Number(flag('--module-count') || 0),
3797
4281
  };
3798
- const run = rest.includes('--no-execute') ? await planRun(request, {}) : await runOrchestration(request, {});
4282
+ // runOrchestration/executeRun resolve the worker backend from opts.workerBackend,
4283
+ // not from request — the flag must reach opts explicitly or an override is silently
4284
+ // dropped (construct-1xlz).
4285
+
4286
+ const runOpts = cliWorkerBackend ? { workerBackend: cliWorkerBackend } : {};
4287
+ const run = rest.includes('--no-execute') ? await planRun(request, runOpts) : await runOrchestration(request, runOpts);
3799
4288
  if (wantsJson) { println(JSON.stringify(hostAdapterMetadata(run), null, 2)); return; }
3800
- println(`Run ${run.runId} — ${run.status} · executionMode=${run.execution.executionMode} · backend=${run.workerBackend} · hostRole=${run.hostRole}`);
3801
- if (run.execution.degraded) println(` degraded: ${run.execution.degradationReason}`);
4289
+
4290
+ // AP5.1/AP5.4: the CLI must never print the raw persisted status — a
4291
+ // pre-taxonomy run record can still carry a bare 'completed' despite being
4292
+ // degraded or prepare-only, so route through the same honest read-model
4293
+ // (shapeRun) the MCP orchestration_status tool uses.
4294
+ const shaped = shapeRun(run);
4295
+ println(`Run ${run.runId} — ${shaped.status} · executionMode=${shaped.executionMode} · backend=${run.workerBackend} · hostRole=${run.hostRole}`);
4296
+ if (shaped.degraded) println(` degraded: ${shaped.degradationReason}`);
3802
4297
  if (run.tasks.length) println(` tasks: ${run.tasks.map((t) => `${t.role}(${t.status})`).join(' → ')}`);
3803
4298
  else println(' tasks: none (no Construct-owned specialist sequence for this execution mode)');
3804
4299
  for (const t of run.tasks) {
@@ -3809,12 +4304,14 @@ async function cmdOrchestrate(args) {
3809
4304
 
3810
4305
  if (sub === 'status') {
3811
4306
  const { getRun, getRuns, hostAdapterMetadata } = await import('../lib/orchestration/runtime.mjs');
4307
+ const { shapeRun } = await import('../lib/mcp/tools/orchestration-run.mjs');
3812
4308
  const runId = rest.find((a) => !a.startsWith('--'));
3813
4309
  if (runId) {
3814
4310
  const run = await getRun(process.cwd(), runId);
3815
4311
  if (!run) { errorln(`Run not found: ${runId}`); process.exit(1); }
3816
4312
  if (wantsJson) { println(JSON.stringify(hostAdapterMetadata(run), null, 2)); return; }
3817
- println(`Run ${run.runId} ${run.status} · executionMode=${run.execution.executionMode}`);
4313
+ const shaped = shapeRun(run);
4314
+ println(`Run ${run.runId} — ${shaped.status} · executionMode=${shaped.executionMode}${shaped.degraded ? ` · degraded: ${shaped.degradationReason}` : ''}`);
3818
4315
  for (const t of run.tasks) {
3819
4316
  println(` ${t.id} ${t.role} — ${t.status}`);
3820
4317
  if (t.reasoning) println(` reasoning: ${t.reasoning.length > 300 ? `${t.reasoning.slice(0, 300)}…` : t.reasoning}`);
@@ -3824,7 +4321,10 @@ async function cmdOrchestrate(args) {
3824
4321
  const runs = await getRuns(process.cwd());
3825
4322
  if (wantsJson) { println(JSON.stringify(runs, null, 2)); return; }
3826
4323
  if (!runs.length) { println('No orchestration runs recorded.'); return; }
3827
- for (const r of runs) println(` ${r.runId} — ${r.status} · ${r.executionMode || ''} · ${r.createdAt}`);
4324
+ for (const r of runs) {
4325
+ const shaped = shapeRun(r);
4326
+ println(` ${r.runId} — ${shaped.status}${shaped.degraded ? ' (degraded)' : ''} · ${shaped.executionMode || r.executionMode || ''} · ${r.createdAt}`);
4327
+ }
3828
4328
  return;
3829
4329
  }
3830
4330
 
@@ -3834,6 +4334,85 @@ async function cmdOrchestrate(args) {
3834
4334
  println(' preflight [--host H] [--json] [--no-probe] [--observed-tools=a,b] [--reachable-tools=a,b]');
3835
4335
  }
3836
4336
 
4337
+ async function cmdDb(args) {
4338
+ const sub = args[0] || 'status';
4339
+ const wantsJson = args.includes('--json');
4340
+ const sql = createSqlClient(process.env);
4341
+
4342
+ async function finish(code = 0) {
4343
+ await closeSqlClient(sql);
4344
+ if (code) process.exit(code);
4345
+ }
4346
+
4347
+ if (!sql) {
4348
+ const result = {
4349
+ status: 'unavailable',
4350
+ reason: 'sql-client-unavailable',
4351
+ message: 'Set DATABASE_URL or CONSTRUCT_DATABASE_URL and install optional dependency "postgres".',
4352
+ migrations: [],
4353
+ };
4354
+ if (wantsJson) println(JSON.stringify(result, null, 2));
4355
+ else {
4356
+ println('Database unavailable.');
4357
+ println(`Reason: ${result.message}`);
4358
+ }
4359
+ return finish(1);
4360
+ }
4361
+
4362
+ try {
4363
+ const { probeSqlClient } = await import('../lib/storage/backend.mjs');
4364
+ const { getMigrationStatus, applyMigrations } = await import('../lib/db/migrate.mjs');
4365
+ const probe = await probeSqlClient(sql);
4366
+ if (probe.status !== 'available') {
4367
+ const result = { status: probe.status, reason: 'postgres-unavailable', message: probe.message, migrations: [] };
4368
+ if (wantsJson) println(JSON.stringify(result, null, 2));
4369
+ else {
4370
+ println('Database unavailable.');
4371
+ println(`Reason: ${probe.message}`);
4372
+ }
4373
+ return finish(1);
4374
+ }
4375
+
4376
+ if (sub === 'status') {
4377
+ const migrations = await getMigrationStatus(sql);
4378
+ const result = {
4379
+ status: 'available',
4380
+ pending: migrations.filter((m) => !m.applied).map((m) => m.id),
4381
+ migrations: migrations.map((m) => ({ id: m.id, applied: m.applied })),
4382
+ };
4383
+ if (wantsJson) println(JSON.stringify(result, null, 2));
4384
+ else {
4385
+ println(`Database available. ${result.pending.length} pending migration${result.pending.length === 1 ? '' : 's'}.`);
4386
+ for (const migration of result.migrations) println(` ${migration.applied ? 'applied' : 'pending'} ${migration.id}`);
4387
+ }
4388
+ return finish(0);
4389
+ }
4390
+
4391
+ if (sub === 'migrate') {
4392
+ const result = await applyMigrations(sql);
4393
+ const output = {
4394
+ status: 'available',
4395
+ applied: result.applied,
4396
+ pending: result.status.filter((m) => !m.applied).map((m) => m.id),
4397
+ migrations: result.status.map((m) => ({ id: m.id, applied: m.applied })),
4398
+ };
4399
+ if (wantsJson) println(JSON.stringify(output, null, 2));
4400
+ else {
4401
+ println(`Applied ${output.applied.length} migration${output.applied.length === 1 ? '' : 's'}.`);
4402
+ for (const id of output.applied) println(` ${id}`);
4403
+ }
4404
+ return finish(0);
4405
+ }
4406
+
4407
+ errorln('Usage: construct db <status|migrate> [--json]');
4408
+ return finish(1);
4409
+ } catch (err) {
4410
+ await closeSqlClient(sql);
4411
+ errorln(err?.message || String(err));
4412
+ process.exit(1);
4413
+ }
4414
+ }
4415
+
3837
4416
  async function cmdModels(args) {
3838
4417
  const envPath = getUserEnvPath(HOME);
3839
4418
  const sub = args[0] && !args[0].startsWith('-') ? args[0] : null;
@@ -4902,6 +5481,10 @@ async function cmdSpecialist(args) {
4902
5481
  const sub = args[0];
4903
5482
  if (sub === 'lint') return cmdLintPrompts();
4904
5483
 
5484
+ if (sub === 'create' && args.includes('--custom')) {
5485
+ return cmdSpecialistCreateCustom(args.slice(1));
5486
+ }
5487
+
4905
5488
  if (sub === 'create') {
4906
5489
  const role = args[1];
4907
5490
  if (!role) { errorln('Usage: construct specialist create <role>'); process.exit(1); }
@@ -4938,6 +5521,91 @@ async function cmdSpecialist(args) {
4938
5521
  process.exit(1);
4939
5522
  }
4940
5523
 
5524
+ // `construct specialist create <id> --custom` scaffolds a user-authored specialist
5525
+ // (construct-rf26.13) — distinct from the maintainer-facing `specialist create <role>`
5526
+ // above, which only stubs a prompt file for the built-in roster. `--custom` writes a
5527
+ // full registry-shaped JSON record (role, skills, fence, modelTier, delegation) into
5528
+ // the project's `.cx/org/` layer by default, or `~/.construct/org/` with `--user`.
5529
+
5530
+ async function cmdSpecialistCreateCustom(args) {
5531
+ const id = args.find((a) => !a.startsWith('--'));
5532
+ const flag = (name) => args.find((a) => a.startsWith(`--${name}=`))?.slice(name.length + 3);
5533
+ const list = (name) => (flag(name) || '').split(',').map((s) => s.trim()).filter(Boolean);
5534
+ const usage = 'Usage: construct specialist create <id> --custom --role=<role> --team=<team-id> --description="…" --skills=<bundle/skill,…> --fence-paths=<glob,…> [--model-tier=fast|standard|reasoning] [--handoff=role,…] [--tools=Read,Grep,…] [--user] [--force] [--root=<dir>]';
5535
+
5536
+ const role = flag('role');
5537
+ const team = flag('team');
5538
+ const description = flag('description');
5539
+ const skills = list('skills');
5540
+ const fencePaths = list('fence-paths');
5541
+ const missing = [
5542
+ !id && 'id (positional)', !role && '--role', !team && '--team',
5543
+ !description && '--description', !skills.length && '--skills', !fencePaths.length && '--fence-paths',
5544
+ ].filter(Boolean);
5545
+ if (missing.length) {
5546
+ errorln(usage);
5547
+ errorln(`Missing required: ${missing.join(', ')}`);
5548
+ process.exit(1);
5549
+ }
5550
+
5551
+ const rootDir = flag('root') ? path.resolve(flag('root')) : ROOT_DIR;
5552
+ const scope = args.includes('--user') ? 'user' : 'project';
5553
+ const { createCustomSpecialist } = await import('../lib/registry/custom-scaffold.mjs');
5554
+ try {
5555
+ const { relPath, promptPath, scope: writtenScope } = createCustomSpecialist({
5556
+ rootDir, scope, id, role, description,
5557
+ modelTier: flag('model-tier') || 'standard',
5558
+ skills, fence: { allowedPaths: fencePaths },
5559
+ team, handoffCandidates: list('handoff'),
5560
+ claudeTools: flag('tools') || undefined,
5561
+ force: args.includes('--force'),
5562
+ });
5563
+ ok(`Created ${relPath} (${writtenScope} scope) and ${path.relative(rootDir, promptPath)}.`);
5564
+ println('Run `construct sync` to pick it up — it then resolves through orchestration the same way a built-in specialist does.');
5565
+ } catch (e) {
5566
+ errorln(e.message);
5567
+ process.exit(1);
5568
+ }
5569
+ }
5570
+
5571
+ async function cmdTeamCreateCustom(args) {
5572
+ const id = args.find((a) => !a.startsWith('--'));
5573
+ const flag = (name) => args.find((a) => a.startsWith(`--${name}=`))?.slice(name.length + 3);
5574
+ const list = (name) => (flag(name) || '').split(',').map((s) => s.trim()).filter(Boolean);
5575
+ const usage = 'Usage: construct team create <id> --owner=<role> --charter="…" [--roles=role,…] [--specialists=cx-a,…] [--decision-rights=a,…] [--forbidden=a,…] [--escalation=role,orchestrator] [--group=<group-id>] [--user] [--force] [--root=<dir>]';
5576
+
5577
+ const owner = flag('owner');
5578
+ const charter = flag('charter');
5579
+ const missing = [!id && 'id (positional)', !owner && '--owner', !charter && '--charter'].filter(Boolean);
5580
+ if (missing.length) {
5581
+ errorln(usage);
5582
+ errorln(`Missing required: ${missing.join(', ')}`);
5583
+ process.exit(1);
5584
+ }
5585
+
5586
+ const rootDir = flag('root') ? path.resolve(flag('root')) : ROOT_DIR;
5587
+ const scope = args.includes('--user') ? 'user' : 'project';
5588
+ const { createCustomTeam } = await import('../lib/registry/custom-scaffold.mjs');
5589
+ try {
5590
+ const { relPath, scope: writtenScope } = createCustomTeam({
5591
+ rootDir, scope, id, owner, charter,
5592
+ name: flag('name'),
5593
+ roles: list('roles'),
5594
+ specialists: list('specialists'),
5595
+ decisionRights: list('decision-rights'),
5596
+ forbiddenDecisions: list('forbidden'),
5597
+ escalationPath: list('escalation'),
5598
+ groupId: flag('group'),
5599
+ force: args.includes('--force'),
5600
+ });
5601
+ ok(`Created ${relPath} (${writtenScope} scope).`);
5602
+ println(`Run \`construct specialist create <id> --custom --role=${owner} --team=${id} …\` to give it an owner specialist, then \`construct sync\`.`);
5603
+ } catch (e) {
5604
+ errorln(e.message);
5605
+ process.exit(1);
5606
+ }
5607
+ }
5608
+
4941
5609
  async function cmdBackup(args) {
4942
5610
  // Postgres-era backups were removed with the SQL backend (lib/storage/backup.mjs).
4943
5611
  // State is now Git-backed (.cx/) plus the local LanceDB index under .cx/lancedb.
@@ -4946,6 +5614,14 @@ async function cmdBackup(args) {
4946
5614
  process.exit(1);
4947
5615
  }
4948
5616
 
5617
+ // `provider add|configure` persist to `.cx/providers/<id>.json`; every
5618
+ // success message names the path relative to cwd so output is copy-pasteable
5619
+ // and consistent whether the command ran from the project root or a subdir.
5620
+
5621
+ function instanceConfigRelPath(id) {
5622
+ return path.join('.cx', 'providers', `${id}.json`);
5623
+ }
5624
+
4949
5625
  async function cmdProvider(args) {
4950
5626
  const sub = args[0];
4951
5627
 
@@ -4970,6 +5646,164 @@ async function cmdProvider(args) {
4970
5646
  return;
4971
5647
  }
4972
5648
 
5649
+ if (sub === 'status') {
5650
+ const jsonOutput = args.includes('--json');
5651
+ const { getState } = await import('../lib/providers/circuit-breaker.mjs');
5652
+ const { describeProviders } = await import('../lib/providers/registry.mjs');
5653
+ const { readInstanceConfig } = await import('../lib/providers/instance-config.mjs');
5654
+ const desc = await describeProviders({ rootDir: process.cwd(), env: process.env });
5655
+
5656
+ // Breaker state is registered under `provider:<id>` (see registry.mjs
5657
+ // wrapWithBreaker); look up with that prefix or the row reads CLOSED for
5658
+ // every provider regardless of actual state.
5659
+ const rows = desc.summary.map((entry) => {
5660
+ const breaker = getState(`provider:${entry.id}`);
5661
+ const breakerState = breaker ? breaker.state : 'CLOSED';
5662
+ const degraded = !entry.health.ok || breakerState !== 'CLOSED';
5663
+ const instance = readInstanceConfig(process.cwd(), entry.id);
5664
+ const filter = instance?.config?.filter ?? null;
5665
+ return {
5666
+ id: entry.id,
5667
+ enabled: entry.health.ok,
5668
+ breaker: breakerState,
5669
+ failures: breaker ? breaker.failures : 0,
5670
+ degraded,
5671
+ source: entry.source,
5672
+ filter,
5673
+ };
5674
+ });
5675
+
5676
+ if (jsonOutput) {
5677
+ println(JSON.stringify({ providers: rows, errors: desc.errors }, null, 2));
5678
+ if (desc.errors.length) process.exit(1);
5679
+ return;
5680
+ }
5681
+
5682
+ println('Provider status');
5683
+ println('════════════════');
5684
+ println(` ${'ID'.padEnd(24)} ${'ENABLED'.padEnd(9)} ${'BREAKER'.padEnd(18)} DEGRADED`);
5685
+ for (const row of rows) {
5686
+ const breakerCol = row.failures > 0 ? `${row.breaker} (${row.failures})` : row.breaker;
5687
+ println(` ${row.id.padEnd(24)} ${(row.enabled ? 'yes' : 'no').padEnd(9)} ${breakerCol.padEnd(18)} ${row.degraded ? 'yes' : 'no'}`);
5688
+ if (row.filter) println(` filter: ${JSON.stringify(row.filter)}`);
5689
+ }
5690
+ if (desc.errors.length) {
5691
+ println('');
5692
+ errorln('Errors loading providers:');
5693
+ for (const e of desc.errors) errorln(` ✗ [${e.id}] ${e.error}`);
5694
+ process.exit(1);
5695
+ }
5696
+ return;
5697
+ }
5698
+
5699
+ if (sub === 'health') {
5700
+ const jsonOutput = args.includes('--json');
5701
+ const id = args[1] && !args[1].startsWith('--') ? args[1] : undefined;
5702
+ const { resolveProviders } = await import('../lib/providers/registry.mjs');
5703
+ const { providers, errors } = await resolveProviders({ rootDir: process.cwd(), env: process.env });
5704
+
5705
+ if (id && !providers[id]) {
5706
+ const loadError = errors.find((e) => e.id === id);
5707
+ if (jsonOutput) {
5708
+ println(JSON.stringify({ id, ok: false, error: loadError ? loadError.error : `unknown provider: ${id}` }, null, 2));
5709
+ } else {
5710
+ errorln(`Unknown provider: ${id}`);
5711
+ if (loadError) errorln(loadError.error);
5712
+ }
5713
+ process.exit(1);
5714
+ }
5715
+
5716
+ const targetIds = id ? [id] : Object.keys(providers);
5717
+ const results = await Promise.all(
5718
+ targetIds.map(async (pid) => {
5719
+ try {
5720
+ const health = await providers[pid].health({});
5721
+ return { id: pid, ok: !!health.ok, detail: health.detail || null };
5722
+ } catch (err) {
5723
+ return { id: pid, ok: false, detail: err.message };
5724
+ }
5725
+ })
5726
+ );
5727
+
5728
+ const anyFailed = results.some((r) => !r.ok) || errors.length > 0;
5729
+
5730
+ if (jsonOutput) {
5731
+ println(JSON.stringify({ results, loadErrors: errors }, null, 2));
5732
+ process.exit(anyFailed ? 1 : 0);
5733
+ }
5734
+
5735
+ println('Provider health');
5736
+ println('════════════════');
5737
+ for (const r of results) {
5738
+ println(` ${r.ok ? '✓' : '✗'} ${r.id.padEnd(24)} ${r.ok ? 'ok' : 'unhealthy'}`);
5739
+ if (r.detail) println(` ${r.detail}`);
5740
+ }
5741
+ if (errors.length) {
5742
+ println('');
5743
+ errorln('Errors loading providers:');
5744
+ for (const e of errors) errorln(` ✗ [${e.id}] ${e.error}`);
5745
+ }
5746
+ process.exit(anyFailed ? 1 : 0);
5747
+ }
5748
+
5749
+ if (sub === 'validate') {
5750
+ const jsonOutput = args.includes('--json');
5751
+ const target = args[1] && !args[1].startsWith('--') ? args[1] : undefined;
5752
+ if (!target) { errorln('Usage: construct provider validate <path|id>'); process.exit(1); }
5753
+
5754
+ const { validateManifest } = await import('../lib/extensions/validate.mjs');
5755
+ const { loadManifestsFromDir, mergeManifests, resolveManifestDirs } = await import('../lib/extensions/loader.mjs');
5756
+
5757
+ let manifest;
5758
+ let filePath;
5759
+ const looksLikePath = target.endsWith('.json') || target.includes('/') || target.startsWith('.');
5760
+
5761
+ if (looksLikePath) {
5762
+ filePath = path.isAbsolute(target) ? target : path.resolve(process.cwd(), target);
5763
+ if (!fs.existsSync(filePath)) {
5764
+ const result = { valid: false, errors: [`${filePath}: file not found`] };
5765
+ if (jsonOutput) { println(JSON.stringify(result, null, 2)); } else { errorln(result.errors[0]); }
5766
+ process.exit(1);
5767
+ }
5768
+ try {
5769
+ manifest = JSON.parse(fs.readFileSync(filePath, 'utf8'));
5770
+ } catch (err) {
5771
+ const result = { valid: false, errors: [`${filePath}: failed to parse JSON (${err.message})`] };
5772
+ if (jsonOutput) { println(JSON.stringify(result, null, 2)); } else { errorln(result.errors[0]); }
5773
+ process.exit(1);
5774
+ }
5775
+ } else {
5776
+ const dirs = resolveManifestDirs({ rootDir: process.cwd(), homeDir: HOME });
5777
+ const builtin = loadManifestsFromDir(dirs.builtin).manifests;
5778
+ const user = loadManifestsFromDir(dirs.user).manifests;
5779
+ const project = loadManifestsFromDir(dirs.project).manifests;
5780
+ const merged = mergeManifests(builtin, user, project);
5781
+ manifest = merged.find((m) => m.id === target);
5782
+ if (!manifest) {
5783
+ const result = { valid: false, errors: [`no manifest found for provider id '${target}'`] };
5784
+ if (jsonOutput) { println(JSON.stringify(result, null, 2)); } else { errorln(result.errors[0]); }
5785
+ process.exit(1);
5786
+ }
5787
+ filePath = manifest._filePath;
5788
+ }
5789
+
5790
+ const strict = args.includes('--strict');
5791
+ const result = validateManifest(manifest, { filePath, strict });
5792
+
5793
+ if (jsonOutput) {
5794
+ println(JSON.stringify(result, null, 2));
5795
+ process.exit(result.valid ? 0 : 1);
5796
+ }
5797
+
5798
+ if (result.valid) {
5799
+ println(`✓ ${filePath || target} is a valid manifest`);
5800
+ process.exit(0);
5801
+ }
5802
+ errorln(`✗ ${filePath || target} failed validation:`);
5803
+ for (const e of result.errors) errorln(` ${e}`);
5804
+ process.exit(1);
5805
+ }
5806
+
4973
5807
  if (sub === 'info') {
4974
5808
  const id = args[1];
4975
5809
  if (!id) { errorln('Usage: construct provider info <id>'); process.exit(1); }
@@ -5051,7 +5885,105 @@ async function cmdProvider(args) {
5051
5885
  return;
5052
5886
  }
5053
5887
 
5054
- errorln(`Unknown provider subcommand: ${sub}. Available: list, info, test, plugins, new`);
5888
+ if (sub === 'add') {
5889
+ const id = args[1] && !args[1].startsWith('--') ? args[1] : undefined;
5890
+ const jsonOutput = args.includes('--json');
5891
+ if (!id) { errorln('Usage: construct provider add <id>'); process.exit(1); }
5892
+
5893
+ const { resolveProviders } = await import('../lib/providers/registry.mjs');
5894
+ const { providers, errors } = await resolveProviders({ rootDir: process.cwd(), env: process.env });
5895
+ if (!providers[id]) {
5896
+ const loadError = errors.find((e) => e.id === id);
5897
+ const result = { ok: false, error: loadError ? loadError.error : `unknown provider: ${id}` };
5898
+ if (jsonOutput) { println(JSON.stringify(result, null, 2)); } else { errorln(`Unknown provider: ${id}`); if (loadError) errorln(loadError.error); }
5899
+ process.exit(1);
5900
+ }
5901
+
5902
+ const { readInstanceConfig, writeInstanceConfig, defaultsFromSchema } = await import('../lib/providers/instance-config.mjs');
5903
+ const existing = readInstanceConfig(process.cwd(), id);
5904
+ if (existing) {
5905
+ const result = { ok: false, error: `instance config already exists for '${id}'; use 'provider configure ${id}' to change it` };
5906
+ if (jsonOutput) { println(JSON.stringify(result, null, 2)); } else { errorln(result.error); }
5907
+ process.exit(1);
5908
+ }
5909
+
5910
+ const configSchema = providers[id].configSchema || {};
5911
+ const defaults = defaultsFromSchema(configSchema);
5912
+ const record = writeInstanceConfig(process.cwd(), id, defaults);
5913
+
5914
+ if (jsonOutput) {
5915
+ println(JSON.stringify({ ok: true, providerId: id, config: record.config, path: instanceConfigRelPath(id) }, null, 2));
5916
+ return;
5917
+ }
5918
+ println(`Added ${id} (${instanceConfigRelPath(id)})`);
5919
+ println(JSON.stringify(record.config, null, 2));
5920
+ return;
5921
+ }
5922
+
5923
+ if (sub === 'configure') {
5924
+ const id = args[1] && !args[1].startsWith('--') ? args[1] : undefined;
5925
+ const jsonOutput = args.includes('--json');
5926
+ if (!id) { errorln('Usage: construct provider configure <id> [--key value ...]'); process.exit(1); }
5927
+
5928
+ const { resolveProviders } = await import('../lib/providers/registry.mjs');
5929
+ const { providers, errors } = await resolveProviders({ rootDir: process.cwd(), env: process.env });
5930
+ if (!providers[id]) {
5931
+ const loadError = errors.find((e) => e.id === id);
5932
+ const result = { ok: false, error: loadError ? loadError.error : `unknown provider: ${id}` };
5933
+ if (jsonOutput) { println(JSON.stringify(result, null, 2)); } else { errorln(`Unknown provider: ${id}`); if (loadError) errorln(loadError.error); }
5934
+ process.exit(1);
5935
+ }
5936
+
5937
+ const {
5938
+ readInstanceConfig, writeInstanceConfig, defaultsFromSchema, applyOverrides, validateInstanceConfig,
5939
+ } = await import('../lib/providers/instance-config.mjs');
5940
+
5941
+ // `--json` is the one bare boolean flag this subcommand accepts; every
5942
+ // other `--foo` is a key.path expecting a value, so it is excluded from
5943
+ // the key/value scan below rather than mis-parsed as a dangling key.
5944
+ const BARE_FLAGS = new Set(['--json']);
5945
+
5946
+ const overrides = [];
5947
+ for (let i = 2; i < args.length; i++) {
5948
+ const a = args[i];
5949
+ if (!a.startsWith('--') || BARE_FLAGS.has(a)) continue;
5950
+ const keyPath = a.slice(2);
5951
+ const value = args[i + 1];
5952
+ if (value === undefined || value.startsWith('--')) {
5953
+ errorln(`Usage: construct provider configure <id> --<key.path> <value> (missing value for --${keyPath})`);
5954
+ process.exit(1);
5955
+ }
5956
+ overrides.push({ keyPath, value });
5957
+ i++;
5958
+ }
5959
+
5960
+ const configSchema = providers[id].configSchema || {};
5961
+ const existing = readInstanceConfig(process.cwd(), id);
5962
+ const base = existing ? existing.config : defaultsFromSchema(configSchema);
5963
+ const merged = applyOverrides(base, overrides);
5964
+
5965
+ const result = validateInstanceConfig(id, configSchema, merged);
5966
+ if (!result.valid) {
5967
+ if (jsonOutput) {
5968
+ println(JSON.stringify({ ok: false, providerId: id, errors: result.errors }, null, 2));
5969
+ } else {
5970
+ errorln(`✗ configure ${id} failed validation:`);
5971
+ for (const e of result.errors) errorln(` ${e}`);
5972
+ }
5973
+ process.exit(1);
5974
+ }
5975
+
5976
+ const record = writeInstanceConfig(process.cwd(), id, merged);
5977
+ if (jsonOutput) {
5978
+ println(JSON.stringify({ ok: true, providerId: id, config: record.config, path: instanceConfigRelPath(id) }, null, 2));
5979
+ return;
5980
+ }
5981
+ println(`✓ Configured ${id} (${instanceConfigRelPath(id)})`);
5982
+ println(JSON.stringify(record.config, null, 2));
5983
+ return;
5984
+ }
5985
+
5986
+ errorln(`Unknown provider subcommand: ${sub}. Available: list, status, health, validate, info, test, add, configure, plugins, new`);
5055
5987
  process.exit(1);
5056
5988
  }
5057
5989
 
@@ -5210,9 +6142,9 @@ async function cmdCreds(args) {
5210
6142
  try {
5211
6143
  const { getProviderModelCatalog } = await import('../lib/model-router.mjs');
5212
6144
  const { providers } = getProviderModelCatalog({ env: process.env });
5213
- println('\nLLM provider readiness (op:// references resolve at call time)\n');
6145
+ println('\nLLM provider readiness (presence-only — op:// references resolve at call time; run `construct creds test <provider>` to verify auth)\n');
5214
6146
  for (const p of providers) {
5215
- println(` ${p.configured ? '✓' : '·'} ${p.id}`);
6147
+ println(` ${p.configured ? '✓ configured (unverified ref)' : '· not configured'} ${p.id}`);
5216
6148
  }
5217
6149
  println(`\n Set keys as values or op:// refs in ${getUserEnvPath(HOME)}; run \`construct creds login copilot\` for GitHub Copilot.`);
5218
6150
  } catch { /* readiness view is informational */ }
@@ -5608,15 +6540,16 @@ const handlers = new Map([
5608
6540
  ['dev', cmdDev],
5609
6541
  ['stop', cmdStop],
5610
6542
  ['status', cmdStatus],
6543
+ ['workers', cmdWorkers],
5611
6544
  ['install', cmdInstall],
5612
6545
  ['config', cmdConfig],
6546
+ ['approvals', cmdApprovals],
5613
6547
  ['sources', cmdSources],
5614
6548
  ['intake', cmdIntake],
5615
6549
  ['recommendations', cmdRecommendations],
5616
6550
  ['integrations', cmdIntegrations],
5617
6551
  ['customer', cmdCustomer],
5618
6552
  ['workspace', cmdWorkspace],
5619
- ['graph', cmdGraph],
5620
6553
  ['uninstall', cmdUninstall],
5621
6554
  ['update', cmdUpdate],
5622
6555
  ['upgrade', cmdUpgrade],
@@ -5891,12 +6824,13 @@ const handlers = new Map([
5891
6824
  const sub = args[0];
5892
6825
  if (sub === 'review') return runTeamReviewCli(args.slice(1));
5893
6826
  if (sub === 'templates') { listTeamTemplates(); return; }
6827
+ if (sub === 'create') return cmdTeamCreateCustom(args.slice(1));
5894
6828
  if (sub === 'list' || sub === 'show') {
5895
6829
  const { runTeamList, runTeamShow } = await import('../lib/registry/cli.mjs');
5896
6830
  if (sub === 'list') return runTeamList(args.slice(1), { rootDir: ROOT_DIR, println, errorln });
5897
6831
  return runTeamShow(args.slice(1), { rootDir: ROOT_DIR, println, errorln });
5898
6832
  }
5899
- errorln(`Unknown team subcommand: ${sub}. Available: list, show, review, templates`);
6833
+ errorln(`Unknown team subcommand: ${sub}. Available: list, show, create, review, templates`);
5900
6834
  process.exit(1);
5901
6835
  }],
5902
6836
  ['role', async (args) => {
@@ -6127,6 +7061,9 @@ const handlers = new Map([
6127
7061
  ['telemetry', async (args) => {
6128
7062
  return cmdTelemetryQuery(args);
6129
7063
  }],
7064
+ ['db', async (args) => {
7065
+ return cmdDb(args);
7066
+ }],
6130
7067
  ['beads', async (args) => {
6131
7068
  const sub = args[0];
6132
7069
  const subArgs = args.slice(1);
@@ -6355,15 +7292,42 @@ const handlers = new Map([
6355
7292
  if (typeof code === 'number' && code !== 0) process.exit(code);
6356
7293
  }],
6357
7294
  ['matrix', async (args) => {
7295
+ if (!globalThis._matrixDeprecated) {
7296
+ process.stderr.write('[deprecation] `construct matrix` is deprecated; use `construct graph` instead\n');
7297
+ globalThis._matrixDeprecated = true;
7298
+ }
6358
7299
  const { runGraphCli } = await import('../lib/graph/cli.mjs');
6359
7300
  const code = runGraphCli(args, { rootDir: ROOT_DIR, projectDir: process.cwd() });
6360
7301
  if (typeof code === 'number' && code !== 0) process.exit(code);
6361
7302
  }],
7303
+ // `graph` carries two disjoint subcommand families: the living dependency
7304
+ // graph (build/stat/query/validate/impacted/missing-tests/missing-docs/
7305
+ // stale/dependencies/providers/surfaces/explain → lib/graph/cli.mjs) and the
7306
+ // intake task graph (list/show/from-intake/status/recommend → cmdGraph).
7307
+ ['graph', async (args) => {
7308
+ const DEPENDENCY_GRAPH_SUBCOMMANDS = new Set([
7309
+ 'build', 'stat', 'query', 'validate', 'impacted',
7310
+ 'missing-tests', 'missing-docs', 'stale', 'dependencies', 'providers', 'surfaces', 'explain', 'owasp',
7311
+ ]);
7312
+ if (DEPENDENCY_GRAPH_SUBCOMMANDS.has(args[0])) {
7313
+ const { runGraphCli } = await import('../lib/graph/cli.mjs');
7314
+ const code = runGraphCli(args, { rootDir: ROOT_DIR, projectDir: process.cwd() });
7315
+ if (typeof code === 'number' && code !== 0) process.exit(code);
7316
+ return;
7317
+ }
7318
+ return cmdGraph(args);
7319
+ }],
7320
+ ['flow', async (args) => cmdFlow(args)],
6362
7321
  ['impact', async (args) => {
6363
7322
  const { runImpactCli } = await import('../lib/graph/impact-cli.mjs');
6364
7323
  const code = await runImpactCli(args, { rootDir: ROOT_DIR, projectDir: process.cwd() });
6365
7324
  if (typeof code === 'number' && code !== 0) process.exit(code);
6366
7325
  }],
7326
+ ['pack', async (args) => {
7327
+ const { runPackCli } = await import('../lib/packs/cli.mjs');
7328
+ const code = runPackCli(args, { rootDir: process.cwd(), packageRoot: ROOT_DIR });
7329
+ if (typeof code === 'number' && code !== 0) process.exit(code);
7330
+ }],
6367
7331
  ['reflect', async (args) => {
6368
7332
  const { runReflectCli } = await import('../lib/reflect.mjs');
6369
7333
  return runReflectCli(args);
@@ -6627,7 +7591,7 @@ const handlers = new Map([
6627
7591
 
6628
7592
  println('');
6629
7593
  println(`Scaffolding a new scope draft. Three questions, then a preview.`);
6630
- println(`Background: docs/guides/concepts/persona-research.md and docs/guides/concepts/scope-lifecycle.md`);
7594
+ println(`Background: docs/guides/concepts/persona-research.md and docs/guides/concepts/profile-lifecycle.md`);
6631
7595
  println('');
6632
7596
  if (!displayName) displayName = await ask('Display name?', id);
6633
7597
  if (seedDepartments.length === 0) {
@@ -6686,11 +7650,11 @@ const handlers = new Map([
6686
7650
  println('');
6687
7651
  println(`✓ draft created at ${pathMod.relative(process.cwd(), result.dir)}`);
6688
7652
  println('');
6689
- println(`Next steps (in order, per docs/guides/concepts/scope-lifecycle.md):`);
6690
- println(` 1. Discover (cx-ux-researcher): fill personas/<role>.md from interviews + primary sources.`);
7653
+ println(`Next steps (in order, per docs/guides/concepts/profile-lifecycle.md):`);
7654
+ println(` 1. Discover (cx-researcher): fill personas/<role>.md from interviews + primary sources.`);
6691
7655
  println(` 2. Frame (cx-product-manager): fill departments/<dept>.md charters and intake taxonomy.`);
6692
7656
  println(` 3. Architect (cx-architect): reconcile role reuse vs new; populate departments[] in scope.json.`);
6693
- println(` 4. Validate (cx-evaluator): run persona-eval and classifier-eval against representative signals.`);
7657
+ println(` 4. Validate (cx-reviewer): run persona-eval and classifier-eval against representative signals.`);
6694
7658
  println('');
6695
7659
  println(`Inspect the brief: open ${pathMod.relative(process.cwd(), result.briefPath)}`);
6696
7660
  return;