@geraldmaron/construct 1.4.2 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (519) hide show
  1. package/.env.example +36 -0
  2. package/README.md +41 -7
  3. package/bin/construct +1027 -64
  4. package/examples/distribution/sources/deck-one-pager.md +1 -1
  5. package/lib/acp/server.mjs +21 -7
  6. package/lib/adapters-sync.mjs +2 -1
  7. package/lib/audit-trail.mjs +185 -2
  8. package/lib/beads/auto-close.mjs +2 -1
  9. package/lib/beads/drift.mjs +2 -1
  10. package/lib/bridges/copilot-proxy.mjs +20 -5
  11. package/lib/certification/skill-inventory.mjs +10 -1
  12. package/lib/cli/approvals.mjs +147 -0
  13. package/lib/cli-commands.mjs +105 -14
  14. package/lib/comment-lint.mjs +127 -8
  15. package/lib/config/schema.mjs +6 -29
  16. package/lib/config/source-target-registry.mjs +70 -0
  17. package/lib/config/source-targets.mjs +179 -205
  18. package/lib/contracts/coverage.mjs +77 -0
  19. package/lib/contracts/validate.mjs +102 -13
  20. package/lib/contracts/violation-log.mjs +50 -4
  21. package/lib/db/migrate.mjs +69 -0
  22. package/lib/db/migrations/001_orchestration_runs.sql +9 -0
  23. package/lib/db/migrations/002_queue_provider.sql +50 -0
  24. package/lib/db/migrations/003_worker_registry.sql +17 -0
  25. package/lib/db/migrations/004_trace_events.sql +16 -0
  26. package/lib/db/migrations/005_shared_memory.sql +15 -0
  27. package/lib/db/migrations/006_orchestration_runs_tenant.sql +5 -0
  28. package/lib/decisions/enforced-baseline.json +0 -2
  29. package/lib/decisions/registry.mjs +3 -2
  30. package/lib/deployment/parity-contract.mjs +1 -1
  31. package/lib/deployment-mode.mjs +78 -2
  32. package/lib/diagram-export.mjs +10 -1
  33. package/lib/distill.mjs +5 -2
  34. package/lib/docs-verify.mjs +2 -1
  35. package/lib/doctor/cli.mjs +1 -0
  36. package/lib/doctor/command-on-path.mjs +24 -0
  37. package/lib/doctor/diagnosis.mjs +89 -0
  38. package/lib/doctor/embedding-health.mjs +50 -0
  39. package/lib/doctor/engine-health.mjs +93 -0
  40. package/lib/doctor/graph-validate.mjs +47 -0
  41. package/lib/doctor/index.mjs +18 -4
  42. package/lib/doctor/sidecar-providers.mjs +56 -0
  43. package/lib/doctor/watchers/consistency.mjs +93 -1
  44. package/lib/doctor/watchers/cx-budget.mjs +1 -1
  45. package/lib/doctor/watchers/graph-staleness.mjs +1 -1
  46. package/lib/doctor/watchers/mcp-protocol.mjs +17 -0
  47. package/lib/doctor/watchers/oracle-liveness.mjs +92 -0
  48. package/lib/doctor/watchers/orchestration-runs.mjs +108 -0
  49. package/lib/doctor/watchers/provider-breaker.mjs +78 -0
  50. package/lib/document-export.mjs +52 -27
  51. package/lib/document-extract/docling-client.mjs +9 -5
  52. package/lib/document-extract/docling-sidecar.py +30 -0
  53. package/lib/document-extract.mjs +1 -1
  54. package/lib/document-ingest.mjs +9 -0
  55. package/lib/embed/approval-queue.mjs +184 -88
  56. package/lib/embed/authority-guard.mjs +65 -2
  57. package/lib/embed/capability-jobs.mjs +365 -0
  58. package/lib/embed/capability-lifecycle.mjs +219 -0
  59. package/lib/embed/capability-loader.mjs +303 -0
  60. package/lib/embed/capability-runtime.mjs +58 -0
  61. package/lib/embed/cli.mjs +185 -8
  62. package/lib/embed/config.mjs +4 -0
  63. package/lib/embed/daemon.mjs +125 -6
  64. package/lib/embed/demand-fetch.mjs +190 -54
  65. package/lib/embed/inbox.mjs +12 -10
  66. package/lib/embed/presets/ops-triage.mjs +236 -0
  67. package/lib/embed/presets/pm-feedback.mjs +341 -0
  68. package/lib/embed/presets/tpm.mjs +401 -0
  69. package/lib/embed/providers/jira.mjs +72 -1
  70. package/lib/embed/providers/registry.mjs +140 -33
  71. package/lib/embedded-contract/capability.mjs +4 -0
  72. package/lib/embedded-contract/execution.mjs +1 -1
  73. package/lib/embedded-contract/model-resolve.mjs +53 -3
  74. package/lib/embedded-contract/workflow-defs.mjs +48 -73
  75. package/lib/embedded-contract/workflow-invoke.mjs +144 -4
  76. package/lib/embedded-contract/workflows/architecture-review.manifest.json +15 -0
  77. package/lib/embedded-contract/workflows/data-structure.manifest.json +14 -0
  78. package/lib/embedded-contract/workflows/evidence-ingest.manifest.json +14 -0
  79. package/lib/embedded-contract/workflows/memo-draft.manifest.json +14 -0
  80. package/lib/embedded-contract/workflows/operations-triage.manifest.json +18 -0
  81. package/lib/embedded-contract/workflows/operations.manifest.json +18 -0
  82. package/lib/embedded-contract/workflows/pm-feedback.manifest.json +18 -0
  83. package/lib/embedded-contract/workflows/prd-draft.manifest.json +15 -0
  84. package/lib/embedded-contract/workflows/proposal-review.manifest.json +15 -0
  85. package/lib/embedded-contract/workflows/research-synthesis.manifest.json +14 -0
  86. package/lib/embedded-contract/workflows/risk-review.manifest.json +15 -0
  87. package/lib/embedded-contract/workflows/structure-notes.manifest.json +14 -0
  88. package/lib/embedded-contract/workflows/transcript-process.manifest.json +14 -0
  89. package/lib/embedded-contract/workflows/triage.manifest.json +14 -0
  90. package/lib/env-config.mjs +50 -9
  91. package/lib/extensions/index.mjs +27 -0
  92. package/lib/extensions/loader.mjs +120 -0
  93. package/lib/extensions/manifest-schema.mjs +141 -0
  94. package/lib/extensions/manifests/anthropic.manifest.json +12 -0
  95. package/lib/extensions/manifests/atlassian-confluence.manifest.json +12 -0
  96. package/lib/extensions/manifests/atlassian-jira.manifest.json +53 -0
  97. package/lib/extensions/manifests/directory.manifest.json +12 -0
  98. package/lib/extensions/manifests/docling.manifest.json +37 -0
  99. package/lib/extensions/manifests/echo.manifest.json +8 -0
  100. package/lib/extensions/manifests/feedback.manifest.json +12 -0
  101. package/lib/extensions/manifests/github-copilot.manifest.json +12 -0
  102. package/lib/extensions/manifests/github.manifest.json +52 -0
  103. package/lib/extensions/manifests/linear.manifest.json +50 -0
  104. package/lib/extensions/manifests/local.manifest.json +12 -0
  105. package/lib/extensions/manifests/ollama.manifest.json +12 -0
  106. package/lib/extensions/manifests/openai.manifest.json +12 -0
  107. package/lib/extensions/manifests/openrouter-anthropic.manifest.json +12 -0
  108. package/lib/extensions/manifests/openrouter-deepseek.manifest.json +12 -0
  109. package/lib/extensions/manifests/openrouter-google.manifest.json +12 -0
  110. package/lib/extensions/manifests/openrouter-llama.manifest.json +12 -0
  111. package/lib/extensions/manifests/openrouter-qwen.manifest.json +12 -0
  112. package/lib/extensions/manifests/openrouter.manifest.json +12 -0
  113. package/lib/extensions/manifests/postgres.manifest.json +12 -0
  114. package/lib/extensions/manifests/salesforce.manifest.json +12 -0
  115. package/lib/extensions/manifests/slack.manifest.json +58 -0
  116. package/lib/extensions/manifests/whisper.manifest.json +36 -0
  117. package/lib/extensions/validate.mjs +175 -0
  118. package/lib/features.mjs +13 -9
  119. package/lib/flows/checkpoint.mjs +184 -0
  120. package/lib/flows/constants.mjs +27 -0
  121. package/lib/flows/define.mjs +146 -0
  122. package/lib/flows/engine.mjs +249 -0
  123. package/lib/flows/errors.mjs +19 -0
  124. package/lib/flows/index.mjs +27 -0
  125. package/lib/flows/joins.mjs +18 -0
  126. package/lib/flows/schema.mjs +90 -0
  127. package/lib/flows/state.mjs +31 -0
  128. package/lib/frameworks/loader.mjs +99 -0
  129. package/lib/frameworks/schema.mjs +157 -0
  130. package/lib/graph/build-from-corpus.mjs +67 -0
  131. package/lib/graph/build-from-embed.mjs +82 -0
  132. package/lib/graph/build-from-registry.mjs +164 -3
  133. package/lib/graph/cli.mjs +456 -12
  134. package/lib/graph/gap-queries.mjs +156 -0
  135. package/lib/graph/gaps.mjs +41 -0
  136. package/lib/graph/impacted.mjs +129 -0
  137. package/lib/graph/runtime-evidence.mjs +177 -0
  138. package/lib/graph/security-coverage.mjs +113 -0
  139. package/lib/graph/staleness.mjs +241 -10
  140. package/lib/graph/store.mjs +24 -7
  141. package/lib/graph/validate.mjs +161 -0
  142. package/lib/headhunt.mjs +9 -8
  143. package/lib/health-check.mjs +15 -7
  144. package/lib/hook-health.mjs +1 -1
  145. package/lib/hooks/agent-tracker.mjs +10 -4
  146. package/lib/hooks/edit-guard.mjs +3 -3
  147. package/lib/hooks/graph-impact-advisory.mjs +2 -2
  148. package/lib/hooks/guard-bash.mjs +41 -15
  149. package/lib/hooks/mcp-health-check.mjs +11 -4
  150. package/lib/hooks/model-fallback.mjs +50 -6
  151. package/lib/hooks/orchestration-dispatch-guard.mjs +14 -3
  152. package/lib/hooks/pre-compact.mjs +181 -173
  153. package/lib/hooks/rule-verifier.mjs +2 -1
  154. package/lib/hooks/session-reflect.mjs +2 -1
  155. package/lib/hooks/session-start.mjs +3 -3
  156. package/lib/host-capabilities.mjs +13 -2
  157. package/lib/host-disposition.mjs +19 -7
  158. package/lib/identity.mjs +92 -0
  159. package/lib/ingest/degraded-extract.mjs +96 -0
  160. package/lib/ingest/docling-remote.mjs +2 -1
  161. package/lib/ingest/provider-extract.mjs +7 -19
  162. package/lib/ingest/sidecar-providers.mjs +196 -0
  163. package/lib/ingest-tooling.mjs +11 -5
  164. package/lib/init-unified.mjs +57 -45
  165. package/lib/init-update.mjs +2 -1
  166. package/lib/install/legacy-global-cleanup.mjs +25 -0
  167. package/lib/intake/daemon.mjs +99 -8
  168. package/lib/intake/git-queue.mjs +110 -38
  169. package/lib/intake/queue-registry.mjs +50 -0
  170. package/lib/intake/queue.mjs +133 -17
  171. package/lib/intake/session-prelude.mjs +57 -8
  172. package/lib/integrations/intake-integrations.mjs +61 -111
  173. package/lib/intent-classifier.mjs +43 -5
  174. package/lib/libreoffice-export.mjs +8 -8
  175. package/lib/logging/rotate.mjs +5 -4
  176. package/lib/mcp/broker.mjs +332 -17
  177. package/lib/mcp/denied-store.mjs +95 -0
  178. package/lib/mcp/destructive-gate.mjs +30 -0
  179. package/lib/mcp/dispatch-envelope.mjs +199 -0
  180. package/lib/mcp/memory-bridge.mjs +2 -1
  181. package/lib/mcp/server.mjs +154 -1411
  182. package/lib/mcp/tool-definitions-memory.mjs +376 -0
  183. package/lib/mcp/tool-definitions-project.mjs +271 -0
  184. package/lib/mcp/tool-definitions-skills.mjs +311 -0
  185. package/lib/mcp/tool-definitions-workflow.mjs +398 -0
  186. package/lib/mcp/tool-definitions.mjs +25 -0
  187. package/lib/mcp/tool-registry.mjs +107 -0
  188. package/lib/mcp/tool-safety.mjs +1 -0
  189. package/lib/mcp/tools/orchestration-delegation-next.tool.mjs +68 -0
  190. package/lib/mcp/tools/orchestration-run.mjs +165 -25
  191. package/lib/mcp/tools/orchestration-task-result.tool.mjs +77 -0
  192. package/lib/mcp/tools/provider-write.mjs +187 -0
  193. package/lib/mcp/tools/scope.mjs +0 -3
  194. package/lib/mcp/tools/skills.mjs +1 -1
  195. package/lib/mcp/tools/storage.mjs +0 -8
  196. package/lib/mcp/transport/auth.mjs +238 -0
  197. package/lib/mcp/transport/http.mjs +136 -0
  198. package/lib/mcp/transport/mode.mjs +31 -0
  199. package/lib/mcp/transport/stdio.mjs +22 -0
  200. package/lib/mcp-catalog.json +4 -4
  201. package/lib/mcp-manager.mjs +34 -23
  202. package/lib/mcp-platform-config.mjs +31 -7
  203. package/lib/mode-capabilities.mjs +109 -0
  204. package/lib/model-router.mjs +42 -9
  205. package/lib/models/catalog.mjs +40 -1
  206. package/lib/net-guard.mjs +247 -0
  207. package/lib/observation-store.mjs +6 -2
  208. package/lib/ollama/provision-context.mjs +2 -1
  209. package/lib/opencode-config.mjs +22 -3
  210. package/lib/opencode-runtime-plugin.mjs +120 -2
  211. package/lib/oracle/actions.mjs +204 -10
  212. package/lib/oracle/cli.mjs +24 -3
  213. package/lib/oracle/dispatch.mjs +2 -1
  214. package/lib/oracle/execute.mjs +2 -2
  215. package/lib/oracle/gaps.mjs +3 -3
  216. package/lib/oracle/heartbeat.mjs +53 -0
  217. package/lib/oracle/read-model.mjs +69 -13
  218. package/lib/oracle/reconcile.mjs +3 -46
  219. package/lib/oracle/routing.mjs +37 -31
  220. package/lib/oracle/synthesize.mjs +1 -1
  221. package/lib/orchestration/classification.mjs +434 -0
  222. package/lib/orchestration/delegation-flow.mjs +132 -0
  223. package/lib/orchestration/flow-selection.mjs +418 -0
  224. package/lib/orchestration/gates.mjs +244 -0
  225. package/lib/orchestration/host-sampling.mjs +121 -0
  226. package/lib/orchestration/policy-constants.mjs +48 -0
  227. package/lib/orchestration/provider-outcome.mjs +197 -0
  228. package/lib/orchestration/readiness.mjs +114 -13
  229. package/lib/orchestration/run-store-postgres.mjs +32 -26
  230. package/lib/orchestration/run-store-sqlite.mjs +8 -14
  231. package/lib/orchestration/run-store.mjs +25 -12
  232. package/lib/orchestration/runtime.mjs +446 -39
  233. package/lib/orchestration/store.mjs +87 -12
  234. package/lib/orchestration/trace-store.mjs +125 -0
  235. package/lib/orchestration/web-capability.mjs +3 -2
  236. package/lib/orchestration/worker-runtime.mjs +162 -0
  237. package/lib/orchestration/worker.mjs +528 -89
  238. package/lib/orchestration-policy.mjs +67 -1111
  239. package/lib/packs/cli.mjs +144 -0
  240. package/lib/packs/core-pack.mjs +112 -0
  241. package/lib/packs/enablement.mjs +187 -0
  242. package/lib/packs/index.mjs +23 -0
  243. package/lib/packs/loader.mjs +176 -0
  244. package/lib/packs/manifest-schema.mjs +58 -0
  245. package/lib/packs/prompts.mjs +120 -0
  246. package/lib/packs/validate.mjs +208 -0
  247. package/lib/plugin-registry.mjs +4 -5
  248. package/lib/policy/audit-gate.mjs +42 -0
  249. package/lib/policy/consumption-budget.mjs +149 -0
  250. package/lib/policy/engine.mjs +81 -6
  251. package/lib/policy/role-authority.mjs +89 -0
  252. package/lib/prompt-composer.js +19 -3
  253. package/lib/providers/contract/adapters/confluence/governed-write.mjs +215 -0
  254. package/lib/providers/contract/adapters/confluence/manifest.json +17 -0
  255. package/lib/providers/contract/adapters/confluence/transport.mjs +135 -0
  256. package/lib/providers/contract/adapters/github/governed-write.mjs +121 -0
  257. package/lib/providers/contract/adapters/github/index.mjs +38 -3
  258. package/lib/providers/contract/adapters/jira/adf.mjs +151 -0
  259. package/lib/providers/contract/adapters/jira/createmeta.mjs +143 -0
  260. package/lib/providers/contract/adapters/jira/governed-write.mjs +182 -0
  261. package/lib/providers/contract/adapters/jira/manifest.json +17 -0
  262. package/lib/providers/contract/adapters/jira/transport.mjs +119 -0
  263. package/lib/providers/contract.mjs +207 -0
  264. package/lib/providers/credential-bootstrap.mjs +7 -4
  265. package/lib/providers/credential-sources.mjs +14 -2
  266. package/lib/providers/directory/index.mjs +273 -0
  267. package/lib/providers/feedback/index.mjs +392 -0
  268. package/lib/providers/filter-audit.mjs +68 -0
  269. package/lib/providers/filter-schema.mjs +54 -0
  270. package/lib/providers/github/index.mjs +31 -0
  271. package/lib/providers/instance-config.mjs +215 -0
  272. package/lib/providers/op-locate.mjs +96 -0
  273. package/lib/providers/op-run.mjs +64 -9
  274. package/lib/providers/registry.mjs +26 -3
  275. package/lib/providers/secret-audit-wiring.mjs +6 -0
  276. package/lib/providers/secret-resolver.mjs +240 -23
  277. package/lib/publish-template.mjs +6 -3
  278. package/lib/publish-tooling.mjs +4 -0
  279. package/lib/publish.mjs +32 -4
  280. package/lib/queue/pg-queue.mjs +395 -0
  281. package/lib/reflect.mjs +2 -1
  282. package/lib/registry/assemble.mjs +28 -2
  283. package/lib/registry/consolidation.mjs +8 -1
  284. package/lib/registry/custom-scaffold.mjs +200 -0
  285. package/lib/registry/custom-schema.mjs +137 -0
  286. package/lib/registry/loader.mjs +8 -3
  287. package/lib/registry/manifests/format-engines.default.json +15 -0
  288. package/lib/registry/manifests/surface-map.default.json +46 -0
  289. package/lib/registry/retired-paths.mjs +1 -0
  290. package/lib/registry/surface-map.mjs +100 -50
  291. package/lib/render-visual-check.mjs +240 -0
  292. package/lib/resources/budget.mjs +40 -17
  293. package/lib/roles/flavor-bindings.mjs +36 -14
  294. package/lib/roles/gateway.mjs +15 -8
  295. package/lib/roots.mjs +107 -0
  296. package/lib/runtime/uv-bootstrap.mjs +32 -6
  297. package/lib/runtime/whisper-bootstrap.mjs +11 -3
  298. package/lib/runtime-env.mjs +5 -2
  299. package/lib/scheduler/index.mjs +8 -20
  300. package/lib/schema-infer.mjs +31 -2
  301. package/lib/scopes/lifecycle.mjs +29 -25
  302. package/lib/scopes/loader.mjs +49 -12
  303. package/lib/scopes/teams.mjs +1 -1
  304. package/lib/security/ingest-boundary.mjs +80 -0
  305. package/lib/security/recall-wrapper.mjs +99 -0
  306. package/lib/security/trust.mjs +132 -0
  307. package/lib/service-manager.mjs +38 -19
  308. package/lib/setup.mjs +41 -23
  309. package/lib/skills-apply.mjs +4 -2
  310. package/lib/specialists/postconditions.mjs +2 -2
  311. package/lib/state-root.mjs +147 -0
  312. package/lib/status.mjs +597 -6
  313. package/lib/storage/admin.mjs +77 -5
  314. package/lib/storage/backend-registry.mjs +73 -0
  315. package/lib/storage/backend.mjs +63 -9
  316. package/lib/storage/embeddings-openai.mjs +12 -2
  317. package/lib/storage/hybrid-query.mjs +11 -3
  318. package/lib/storage/retrieval-hardening.mjs +384 -0
  319. package/lib/storage/shared-memory.mjs +158 -0
  320. package/lib/storage/vector-client.mjs +69 -2
  321. package/lib/team/health.mjs +72 -0
  322. package/lib/telemetry/backends/local.mjs +9 -3
  323. package/lib/telemetry/client.mjs +3 -7
  324. package/lib/template-registry.mjs +10 -12
  325. package/lib/tenant/context.mjs +82 -0
  326. package/lib/tenant/isolation.mjs +129 -0
  327. package/lib/test-corpus-inventory.mjs +104 -2
  328. package/lib/uninstall/uninstall.mjs +78 -12
  329. package/lib/validator.mjs +4 -6
  330. package/lib/worker/entrypoint.mjs +2 -1
  331. package/lib/worker/run.mjs +2 -2
  332. package/lib/worker/trace.mjs +5 -11
  333. package/lib/workflow-state.mjs +52 -8
  334. package/lib/workflows/liveness.mjs +203 -0
  335. package/lib/workflows/loader.mjs +177 -0
  336. package/lib/workflows/manifest-schema.mjs +71 -0
  337. package/lib/workflows/surface-parity.mjs +118 -0
  338. package/lib/workflows/validate.mjs +127 -0
  339. package/lib/writes/control-plane.mjs +140 -0
  340. package/lib/writes/envelope.mjs +206 -0
  341. package/lib/writes/sent-log.mjs +86 -0
  342. package/lib/writes/write-intent.mjs +109 -0
  343. package/package.json +16 -8
  344. package/personas/construct.md +12 -3
  345. package/registry/capabilities.json +143 -36
  346. package/rules/common/comments.md +1 -0
  347. package/schemas/project-config.schema.json +0 -12
  348. package/schemas/unified-registry.schema.json +2 -4
  349. package/scripts/sync-specialists.mjs +284 -81
  350. package/skills/docs/adr-workflow.md +1 -1
  351. package/skills/docs/codebase-research-workflow.md +3 -3
  352. package/skills/docs/prd-workflow.md +5 -6
  353. package/skills/docs/runbook-workflow.md +2 -2
  354. package/skills/docs/user-research-workflow.md +3 -3
  355. package/skills/operating/fleet-health-routing.md +77 -0
  356. package/skills/roles/ai-engineer.md +1 -1
  357. package/skills/roles/architect.md +0 -1
  358. package/skills/roles/business-strategist.md +1 -1
  359. package/skills/roles/data-analyst.telemetry.md +1 -1
  360. package/skills/roles/data-engineer.md +1 -1
  361. package/skills/roles/data-engineer.pipeline.md +1 -1
  362. package/skills/roles/data-engineer.vector-retrieval.md +1 -2
  363. package/skills/roles/data-engineer.warehouse.md +1 -1
  364. package/skills/roles/designer.accessibility.md +1 -1
  365. package/skills/roles/designer.md +0 -1
  366. package/skills/roles/devil-advocate.md +1 -1
  367. package/skills/roles/docs-keeper.md +1 -1
  368. package/skills/roles/evaluator.md +1 -1
  369. package/skills/roles/explorer.md +1 -1
  370. package/skills/roles/platform-engineer.md +1 -1
  371. package/skills/roles/qa.ai-eval.md +1 -2
  372. package/skills/roles/qa.api-contract.md +0 -1
  373. package/skills/roles/qa.data-pipeline.md +0 -1
  374. package/skills/roles/qa.md +0 -1
  375. package/skills/roles/qa.web-ui.md +0 -1
  376. package/skills/roles/release-manager.md +1 -1
  377. package/skills/roles/researcher.md +0 -2
  378. package/skills/roles/reviewer.md +0 -3
  379. package/skills/roles/security.ai.md +1 -1
  380. package/skills/roles/security.legal-compliance.md +1 -1
  381. package/skills/roles/security.md +0 -1
  382. package/skills/roles/security.privacy.md +0 -1
  383. package/skills/roles/security.supply-chain.md +1 -1
  384. package/skills/roles/sre.md +1 -1
  385. package/skills/roles/test-automation.md +1 -1
  386. package/skills/roles/trace-reviewer.md +1 -1
  387. package/skills/roles/ux-researcher.md +1 -1
  388. package/specialists/artifact-manifest.json +36 -36
  389. package/specialists/org/contracts/accessibility-to-qa.json +11 -3
  390. package/specialists/org/contracts/any-to-business-strategist.json +19 -7
  391. package/specialists/org/contracts/any-to-debugger.json +7 -1
  392. package/specialists/org/contracts/any-to-designer.json +7 -1
  393. package/specialists/org/contracts/any-to-docs-keeper.json +18 -4
  394. package/specialists/org/contracts/any-to-explorer.json +13 -4
  395. package/specialists/org/contracts/any-to-sre-incident.json +6 -2
  396. package/specialists/org/contracts/any-to-trace-reviewer.json +16 -4
  397. package/specialists/org/contracts/architect-to-ai-engineer.json +6 -2
  398. package/specialists/org/contracts/architect-to-data-engineer.json +17 -5
  399. package/specialists/org/contracts/architect-to-devil-advocate.json +11 -3
  400. package/specialists/org/contracts/architect-to-engineer.json +20 -4
  401. package/specialists/org/contracts/architect-to-evaluator.json +15 -5
  402. package/specialists/org/contracts/architect-to-legal-compliance.json +15 -5
  403. package/specialists/org/contracts/architect-to-operations.json +17 -4
  404. package/specialists/org/contracts/architect-to-platform-engineer.json +20 -4
  405. package/specialists/org/contracts/construct-to-orchestrator.json +10 -2
  406. package/specialists/org/contracts/data-analyst-to-product-manager.json +11 -2
  407. package/specialists/org/contracts/engineer-to-qa.json +20 -4
  408. package/specialists/org/contracts/engineer-to-reviewer.json +29 -5
  409. package/specialists/org/contracts/explorer-to-engineer.json +11 -3
  410. package/specialists/org/contracts/legal-compliance-to-release-manager.json +12 -4
  411. package/specialists/org/contracts/operations-to-user.json +53 -0
  412. package/specialists/org/contracts/operations-triage-output.json +53 -0
  413. package/specialists/org/contracts/pm-requirements-candidates.json +53 -0
  414. package/specialists/org/contracts/product-manager-to-architect.json +30 -10
  415. package/specialists/org/contracts/product-manager-to-data-analyst.json +13 -3
  416. package/specialists/org/contracts/product-manager-to-ux-researcher.json +15 -5
  417. package/specialists/org/contracts/qa-to-release-manager.json +11 -3
  418. package/specialists/org/contracts/researcher-to-architect.json +10 -2
  419. package/specialists/org/contracts/researcher-to-product-manager.json +16 -5
  420. package/specialists/org/contracts/reviewer-to-security.json +17 -3
  421. package/specialists/org/contracts/test-automation-to-engineer.json +11 -3
  422. package/specialists/org/contracts/trace-reviewer-to-sre.json +12 -4
  423. package/specialists/org/contracts/user-to-construct.json +11 -4
  424. package/specialists/org/frameworks/cx-architect-constraint-option-failure.md +66 -0
  425. package/specialists/org/frameworks/cx-engineer-feasibility-blast-radius.md +66 -0
  426. package/specialists/org/frameworks/cx-ops-dependency-sequencing.md +74 -0
  427. package/specialists/org/frameworks/cx-pm-value-tradeoff.md +66 -0
  428. package/specialists/org/frameworks/cx-qa-risk-based-coverage.md +69 -0
  429. package/specialists/org/groups/engineering-group.json +2 -6
  430. package/specialists/org/groups/governance-group.json +2 -3
  431. package/specialists/org/groups/operations-group.json +5 -8
  432. package/specialists/org/groups/product-group.json +4 -8
  433. package/specialists/org/groups/quality-group.json +3 -7
  434. package/specialists/org/groups/strategy-group.json +6 -9
  435. package/specialists/org/models.json.example +8 -0
  436. package/specialists/org/scopes/creative.json +6 -1
  437. package/specialists/org/scopes/operations.json +7 -1
  438. package/specialists/org/scopes/research.json +6 -1
  439. package/specialists/org/scopes/rnd.json +7 -1
  440. package/specialists/org/specialists/cx-architect.json +27 -8
  441. package/specialists/org/specialists/cx-designer.json +22 -8
  442. package/specialists/org/specialists/cx-engineer.json +71 -7
  443. package/specialists/org/specialists/cx-operations.json +89 -15
  444. package/specialists/org/specialists/cx-orchestrator.json +16 -4
  445. package/specialists/org/specialists/cx-product-manager.json +28 -9
  446. package/specialists/org/specialists/cx-qa.json +16 -6
  447. package/specialists/org/specialists/cx-researcher.json +40 -7
  448. package/specialists/org/specialists/cx-reviewer.json +61 -11
  449. package/specialists/org/specialists/cx-security.json +30 -10
  450. package/specialists/org/teams/design-team.json +3 -4
  451. package/specialists/org/teams/engineering-team.json +3 -10
  452. package/specialists/org/teams/governance-team.json +3 -5
  453. package/specialists/org/teams/operations-team.json +6 -12
  454. package/specialists/org/teams/product-management-team.json +2 -3
  455. package/specialists/org/teams/quality-team.json +4 -12
  456. package/specialists/org/teams/research-team.json +3 -3
  457. package/specialists/org/teams/strategy-team.json +7 -14
  458. package/specialists/prompts/cx-architect.md +20 -1
  459. package/specialists/prompts/cx-data-analyst.md +1 -1
  460. package/specialists/prompts/cx-designer.md +12 -4
  461. package/specialists/prompts/cx-engineer.md +15 -1
  462. package/specialists/prompts/cx-operations.md +14 -2
  463. package/specialists/prompts/cx-orchestrator.md +9 -5
  464. package/specialists/prompts/cx-product-manager.md +5 -1
  465. package/specialists/prompts/cx-qa.md +6 -2
  466. package/specialists/prompts/cx-researcher.md +25 -30
  467. package/specialists/prompts/cx-reviewer.md +19 -1
  468. package/specialists/prompts/cx-security.md +5 -1
  469. package/templates/distribution/construct-brand.typ +123 -59
  470. package/templates/distribution/construct-decision.typ +6 -3
  471. package/templates/distribution/construct-pdf.typ +11 -4
  472. package/templates/distribution/construct-prd.typ +6 -3
  473. package/templates/distribution/construct-research.typ +7 -4
  474. package/lib/providers/contract/adapters/git/index.mjs +0 -115
  475. package/lib/providers/contract/adapters/slack/index.mjs +0 -175
  476. package/specialists/org/contracts/business-strategist-to-product-manager.json +0 -39
  477. package/specialists/org/contracts/construct-to-rd-lead.json +0 -53
  478. package/specialists/org/contracts/data-engineer-to-platform-engineer.json +0 -36
  479. package/specialists/org/contracts/designer-to-accessibility.json +0 -36
  480. package/specialists/org/contracts/platform-engineer-to-engineer.json +0 -31
  481. package/specialists/org/contracts/qa-to-test-automation.json +0 -42
  482. package/specialists/org/contracts/rd-lead-to-architect.json +0 -38
  483. package/specialists/org/contracts/sre-to-release-manager.json +0 -36
  484. package/specialists/org/specialists/cx-accessibility.json +0 -69
  485. package/specialists/org/specialists/cx-ai-engineer.json +0 -75
  486. package/specialists/org/specialists/cx-business-strategist.json +0 -72
  487. package/specialists/org/specialists/cx-data-engineer.json +0 -71
  488. package/specialists/org/specialists/cx-devil-advocate.json +0 -71
  489. package/specialists/org/specialists/cx-docs-keeper.json +0 -82
  490. package/specialists/org/specialists/cx-evaluator.json +0 -69
  491. package/specialists/org/specialists/cx-explorer.json +0 -69
  492. package/specialists/org/specialists/cx-legal-compliance.json +0 -74
  493. package/specialists/org/specialists/cx-oracle.json +0 -46
  494. package/specialists/org/specialists/cx-platform-engineer.json +0 -80
  495. package/specialists/org/specialists/cx-rd-lead.json +0 -73
  496. package/specialists/org/specialists/cx-release-manager.json +0 -77
  497. package/specialists/org/specialists/cx-sre.json +0 -94
  498. package/specialists/org/specialists/cx-test-automation.json +0 -69
  499. package/specialists/org/specialists/cx-trace-reviewer.json +0 -69
  500. package/specialists/org/specialists/cx-ux-researcher.json +0 -68
  501. package/specialists/org/teams/accessibility-team.json +0 -39
  502. package/specialists/org/teams/ux-research-team.json +0 -39
  503. package/specialists/prompts/cx-accessibility.md +0 -55
  504. package/specialists/prompts/cx-ai-engineer.md +0 -124
  505. package/specialists/prompts/cx-business-strategist.md +0 -58
  506. package/specialists/prompts/cx-data-engineer.md +0 -55
  507. package/specialists/prompts/cx-devil-advocate.md +0 -59
  508. package/specialists/prompts/cx-docs-keeper.md +0 -164
  509. package/specialists/prompts/cx-evaluator.md +0 -49
  510. package/specialists/prompts/cx-explorer.md +0 -71
  511. package/specialists/prompts/cx-legal-compliance.md +0 -58
  512. package/specialists/prompts/cx-oracle.md +0 -98
  513. package/specialists/prompts/cx-platform-engineer.md +0 -97
  514. package/specialists/prompts/cx-rd-lead.md +0 -59
  515. package/specialists/prompts/cx-release-manager.md +0 -54
  516. package/specialists/prompts/cx-sre.md +0 -111
  517. package/specialists/prompts/cx-test-automation.md +0 -55
  518. package/specialists/prompts/cx-trace-reviewer.md +0 -101
  519. package/specialists/prompts/cx-ux-researcher.md +0 -53
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;
@@ -521,6 +600,17 @@ async function cmdDoctor() {
521
600
  add('specialists/org exists', fs.existsSync(path.join(ROOT_DIR, 'specialists', 'org')));
522
601
  add('sync-specialists.mjs exists', fs.existsSync(path.join(ROOT_DIR, 'scripts', 'sync-specialists.mjs')));
523
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
+
524
614
  // Project .gitignore must cover .cx/. Auto-fix via --fix-gitignore.
525
615
  // Only checks when cwd looks like a Construct project (has .cx/ or
526
616
  // .construct/); non-project cwds get no warning.
@@ -659,6 +749,31 @@ async function cmdDoctor() {
659
749
  );
660
750
  }
661
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
+
662
777
  // Mirror model tier overrides stranded in the pre-XDG legacy config forward
663
778
  // before resolving tiers, so an install upgraded across the XDG move reports
664
779
  // configured without a manual copy. Applied to process.env too so the check
@@ -688,7 +803,7 @@ async function cmdDoctor() {
688
803
  try {
689
804
  const { resolveModelTiers } = await import('../lib/model-registry.mjs');
690
805
  const resolved = resolveModelTiers({ env: process.env });
691
- if (resolved.complete && !resolved.errors) {
806
+ if (resolved.complete) {
692
807
  add('Models — all tiers configured', true);
693
808
  } else if (resolved.configured === 0) {
694
809
  add('Models — no tier configured (run `construct models --apply`)', false, true);
@@ -702,8 +817,79 @@ async function cmdDoctor() {
702
817
  add(`Models — check failed: ${err.message}`, false, true);
703
818
  }
704
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
+
705
891
  add('Node.js 20+ (recommended)', Number.parseInt(process.versions.node.split('.')[0], 10) >= 20);
706
- add('npm available', true);
892
+ add('npm available', resolveCommandOnPath('npm').status === 0);
707
893
  const npmDevdir = process.env.npm_config_devdir ?? process.env.NPM_CONFIG_DEVDIR;
708
894
  if (npmDevdir) {
709
895
  add(
@@ -712,7 +898,7 @@ async function cmdDoctor() {
712
898
  true,
713
899
  );
714
900
  }
715
- const constructOnPath = spawnSync('zsh', ['-lc', 'command -v construct'], { encoding: 'utf8', env: process.env });
901
+ const constructOnPath = resolveCommandOnPath('construct');
716
902
  const constructPath = constructOnPath.status === 0 ? constructOnPath.stdout.trim() : '';
717
903
  const constructVersion = constructPath
718
904
  ? spawnSync(constructPath, ['version'], { encoding: 'utf8', env: process.env })
@@ -763,16 +949,53 @@ async function cmdDoctor() {
763
949
  // Absence is not a doctor finding — VS Code adapter support hasn't landed
764
950
  // yet, so "create one or wait for support" isn't actionable. The line is
765
951
  // suppressed entirely when missing; doctor surfaces facts, not roadmap.
766
- const vscodeSettingsOk = vscodeSettingsPaths.some((candidate) => fs.existsSync(candidate));
952
+ const vscodeSettingsPath = vscodeSettingsPaths.find((candidate) => fs.existsSync(candidate)) ?? null;
953
+ const vscodeSettingsOk = Boolean(vscodeSettingsPath);
767
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
+ }
768
979
  }
769
980
  add('User config ready', fs.existsSync(getUserEnvPath(HOME)) || fs.existsSync(configDir(HOME)), true);
770
981
  add('skills/ directory', fs.existsSync(path.join(ROOT_DIR, 'skills')));
771
982
  add('rules/common/', fs.existsSync(path.join(ROOT_DIR, 'rules', 'common')));
772
983
  add('specialists/prompts/ directory', fs.existsSync(path.join(ROOT_DIR, 'specialists', 'prompts')));
773
984
  add('Hybrid storage env keys documented', fs.existsSync(path.join(ROOT_DIR, '.env.example')));
774
- fs.mkdirSync(path.join(doctorRoot(), 'performance-reviews'), { recursive: true });
775
- 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
+ }
776
999
 
777
1000
  // construct.config.json — optional today (config is permitted to be absent;
778
1001
  // env vars + defaults still work). The check enforces that when the file
@@ -780,7 +1003,9 @@ async function cmdDoctor() {
780
1003
  try {
781
1004
  const projectCfg = loadProjectConfig(process.cwd(), process.env);
782
1005
  if (projectCfg.source === 'invalid') {
783
- 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);
784
1009
  } else if (projectCfg.source === 'file') {
785
1010
  add(`construct.config.json valid`, true, true);
786
1011
  } else {
@@ -879,7 +1104,7 @@ async function cmdDoctor() {
879
1104
  return !fs.existsSync(path.join(ROOT_DIR, s.promptFile));
880
1105
  });
881
1106
  add('All specialist promptFiles resolve', brokenPromptFiles.length === 0);
882
- 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'];
883
1108
  const registryNames = new Set(Object.values(registry.specialists ?? {}).map((s) => `cx-${s.name}`));
884
1109
  const classifierOrphans = headhuntClassifierAgents.filter((n) => !registryNames.has(n));
885
1110
  add('Headhunt classifier agents exist in registry', classifierOrphans.length === 0);
@@ -913,10 +1138,33 @@ async function cmdDoctor() {
913
1138
  const researchLintResults = lintResearchRepo({ rootDir: process.cwd() });
914
1139
  add('Research artifacts meet minimum evidence structure', researchLintResults.every((entry) => entry.errors.length === 0), true);
915
1140
 
916
- const { VectorClient } = await import('../lib/storage/vector-client.mjs');
917
- const vectorClient = new VectorClient({ env: process.env });
918
- const vectorOk = await vectorClient.isHealthy();
919
- 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
+ }
920
1168
 
921
1169
  const { regenerateDocs } = await import('../lib/auto-docs.mjs');
922
1170
  const { changed: stale } = await regenerateDocs({ rootDir: ROOT_DIR, check: true });
@@ -949,6 +1197,34 @@ async function cmdDoctor() {
949
1197
  add(label, wired);
950
1198
  }
951
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
+
952
1228
  const pluginRegistry = loadPluginRegistry({ cwd: process.cwd(), homeDir: HOME, rootDir: ROOT_DIR, env: process.env });
953
1229
  add('Plugin manifests valid', pluginRegistry.valid);
954
1230
 
@@ -1121,16 +1397,17 @@ async function cmdDoctor() {
1121
1397
  add('No recent hook failures (24h)', false, true);
1122
1398
  }
1123
1399
 
1124
- // 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.
1125
1403
  const { getAllContracts } = await import('../lib/specialist-contracts.mjs');
1126
1404
  const contracts = getAllContracts();
1127
1405
  add('Agent contracts loaded', contracts.length > 0);
1128
1406
  if (contracts.length > 0) {
1129
- const missingTerminals = contracts.filter((c) =>
1130
- c.consumer && !c.consumer.startsWith('cx-') && c.consumer !== 'construct' && c.consumer !== '*'
1131
- ? false : false,
1407
+ const brokenContracts = contracts.filter((c) =>
1408
+ !c.id || !c.producer || !c.consumer || (!c.postconditions && !c.output),
1132
1409
  );
1133
- add('Agent contract schema intact', missingTerminals.length === 0);
1410
+ add('Agent contract schema intact', brokenContracts.length === 0);
1134
1411
  }
1135
1412
 
1136
1413
  // Skills profile — if present, check staleness against current tags
@@ -1155,6 +1432,67 @@ async function cmdDoctor() {
1155
1432
  add('Registry integrity check', false);
1156
1433
  }
1157
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
+
1158
1496
  // Reconciliation drift (ADR-0027). Any task reporting needsRepair is surfaced
1159
1497
  // with its summary and the exact `construct sync --reconcile=<id>` command.
1160
1498
  // Auto tasks that still show drift (e.g. a run with --no-reconcile) and all
@@ -1347,7 +1685,7 @@ async function cmdDev(args = []) {
1347
1685
  process.exit(1);
1348
1686
  }
1349
1687
 
1350
- const { results, recovery } = await startServices({ rootDir: ROOT_DIR, homeDir: HOME, selected });
1688
+ const { results, recovery } = await startServices({ rootDir: PROJECT_ROOT, homeDir: HOME, selected });
1351
1689
  opLog.event('services', { results: results.map((r) => ({ name: r.name, status: r.status })) });
1352
1690
  for (const svc of results) {
1353
1691
  const label = svc.url ? `${svc.name} → ${svc.url}` : svc.name;
@@ -1388,14 +1726,14 @@ async function cmdDev(args = []) {
1388
1726
  const { existsSync } = await import('node:fs');
1389
1727
  const { join } = await import('node:path');
1390
1728
 
1391
- const embedConfigExists = existsSync(join(ROOT_DIR, 'embed.yaml'));
1392
- const config = loadProjectConfig(ROOT_DIR);
1729
+ const embedConfigExists = existsSync(join(PROJECT_ROOT, 'embed.yaml'));
1730
+ const config = loadProjectConfig(PROJECT_ROOT);
1393
1731
  const autoEmbed = config.config?.autoEmbed;
1394
-
1732
+
1395
1733
  if (embedConfigExists && autoEmbed) {
1396
1734
  try {
1397
1735
  const { runEmbedCli } = await import('../lib/embed/cli.mjs');
1398
- await runEmbedCli(['start'], { rootDir: ROOT_DIR });
1736
+ await runEmbedCli(['start'], { rootDir: PROJECT_ROOT });
1399
1737
  opLog.event('embed', { status: 'started' });
1400
1738
  } catch (err) {
1401
1739
  warn(`embed start failed: ${err.message} — check ~/.cx/runtime/embed-daemon.log`);
@@ -1748,18 +2086,38 @@ async function cmdPrune(args) {
1748
2086
  const dryRun = args.includes('--dry-run');
1749
2087
  const { planPrune, executePrune, measureUsage } = await import('../lib/resources/budget.mjs');
1750
2088
  const actions = planPrune(process.cwd(), process.env);
1751
- if (actions.length === 0) { info('Nothing to prune. .cx/ is within all configured budgets.'); return; }
1752
- const byCat = {};
1753
- let totalBytes = 0;
1754
- for (const a of actions) {
1755
- byCat[a.category] = (byCat[a.category] || 0) + 1;
1756
- totalBytes += a.bytes || 0;
1757
- }
1758
- println(`Plan: prune ${actions.length} file(s), free ${Math.round(totalBytes / 1024)}KB`);
1759
- 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; }
1760
2120
  if (dryRun) { info('--dry-run: no files removed.'); return; }
1761
- const result = executePrune(actions);
1762
- info(`Pruned ${result.removed.length} file(s), freed ${Math.round(result.bytesFreed / 1024)}KB.`);
1763
2121
  const after = measureUsage(process.cwd(), process.env);
1764
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)}%).`);
1765
2123
  }
@@ -2223,7 +2581,7 @@ async function cmdIntake(args) {
2223
2581
  const queue = createIntakeQueue(cwd, process.env);
2224
2582
 
2225
2583
  if (sub === 'list') {
2226
- const pending = queue.listPending();
2584
+ const pending = await queue.listPending();
2227
2585
  const rows = pending.map((p) => {
2228
2586
  const t = p.triage || {};
2229
2587
  return [p.id, t.intakeType || 'unknown', t.rdStage || 'unknown', t.primaryOwner || '—', t.recommendedAction || '—'];
@@ -2235,7 +2593,7 @@ async function cmdIntake(args) {
2235
2593
  const id = args[1];
2236
2594
  if (sub === 'show') {
2237
2595
  if (!id) { errorln('Usage: construct intake show <id>'); process.exit(1); }
2238
- const entry = queue.read(id);
2596
+ const entry = await queue.read(id);
2239
2597
  if (!entry) { errorln(`No ${rebrand.signalNoun} with id: ${id}`); process.exit(1); }
2240
2598
  const t = entry.triage || {};
2241
2599
  println(`${rebrand.signalNoun.charAt(0).toUpperCase() + rebrand.signalNoun.slice(1)} ${entry.id}`);
@@ -2349,7 +2707,7 @@ async function cmdIntake(args) {
2349
2707
 
2350
2708
  let stamped = null;
2351
2709
  if (outputPath) {
2352
- const packet = queue.read(id);
2710
+ const packet = await queue.read(id);
2353
2711
  if (!packet) {
2354
2712
  errorln(`No intake packet found: ${id}`);
2355
2713
  process.exit(1);
@@ -2375,7 +2733,7 @@ async function cmdIntake(args) {
2375
2733
  || (outputPath ? `output: ${outputPath}` : '');
2376
2734
 
2377
2735
  try {
2378
- const r = queue.markProcessed(id, { processedBy: 'construct-intake-cli', notes });
2736
+ const r = await queue.markProcessed(id, { processedBy: 'construct-intake-cli', notes });
2379
2737
  if (stamped) {
2380
2738
  info(`Marked ${r.id} processed. Stamped ${outputPath} with intake_id=${id} (confidence=${stamped.intake_confidence ?? 'n/a'}).`);
2381
2739
  } else if (noOutputOutcome) {
@@ -2394,7 +2752,7 @@ async function cmdIntake(args) {
2394
2752
  if (!id) { errorln('Usage: construct intake skip <id> [--reason=…]'); process.exit(1); }
2395
2753
  const flags = parseKvFlags(args.slice(2), ['reason']);
2396
2754
  try {
2397
- 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 || '' });
2398
2756
  info(`Marked ${r.id} skipped.`);
2399
2757
  } catch (err) {
2400
2758
  errorln(err.message || 'mark skipped failed');
@@ -2406,7 +2764,7 @@ async function cmdIntake(args) {
2406
2764
  if (sub === 'reopen') {
2407
2765
  if (!id) { errorln('Usage: construct intake reopen <id>'); process.exit(1); }
2408
2766
  try {
2409
- const r = queue.reopen(id);
2767
+ const r = await queue.reopen(id);
2410
2768
  info(`Reopened ${r.id} (from ${r.from}).`);
2411
2769
  } catch (err) {
2412
2770
  errorln(err.message || 'reopen failed');
@@ -3095,7 +3453,7 @@ async function cmdIntakeIntegrate(id, args, cwd) {
3095
3453
 
3096
3454
  const { createIntakeQueue } = await import('../lib/intake/queue.mjs');
3097
3455
  const queue = createIntakeQueue(cwd, process.env);
3098
- const entry = queue.read(id);
3456
+ const entry = await queue.read(id);
3099
3457
  if (!entry) { errorln(`No intake packet with id: ${id}`); process.exit(1); }
3100
3458
 
3101
3459
  let result;
@@ -3471,7 +3829,7 @@ async function cmdGraph(args) {
3471
3829
  if (!intakeId) { errorln('Usage: construct graph from-intake <intake-id>'); process.exit(1); }
3472
3830
  const { createIntakeQueue } = await import('../lib/intake/queue.mjs');
3473
3831
  const queue = createIntakeQueue(cwd, process.env);
3474
- const entry = queue.read(intakeId);
3832
+ const entry = await queue.read(intakeId);
3475
3833
  if (!entry) { errorln(`No intake packet: ${intakeId}`); process.exit(1); }
3476
3834
  const graph = generateTaskGraphFromTriage({
3477
3835
  triage: entry.triage,
@@ -3509,6 +3867,76 @@ async function cmdGraph(args) {
3509
3867
  process.exit(1);
3510
3868
  }
3511
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
+
3512
3940
  async function cmdConfig(args) {
3513
3941
  const sub = args[0];
3514
3942
 
@@ -3695,6 +4123,7 @@ async function cmdOrchestrate(args) {
3695
4123
  const timeoutMs = Number(eqFlag('--timeout-ms') || 8000);
3696
4124
  let observedTools = splitCsv(eqFlag('--observed-tools'));
3697
4125
  let reachableTools = splitCsv(eqFlag('--reachable-tools'));
4126
+ const callerSuppliedObservations = Boolean(observedTools || reachableTools);
3698
4127
  let probeError = null;
3699
4128
 
3700
4129
  if (!rest.includes('--no-probe') && !observedTools && !reachableTools) {
@@ -3719,21 +4148,52 @@ async function cmdOrchestrate(args) {
3719
4148
  reachableTools,
3720
4149
  requiredTools: splitCsv(eqFlag('--required-tools')),
3721
4150
  clientContractVersion: eqFlag('--client-contract-version'),
3722
- observationScope: eqFlag('--observation-scope') || (observedTools || reachableTools ? 'host-session' : 'local-config'),
4151
+ observationScope: eqFlag('--observation-scope') || (callerSuppliedObservations ? 'host-session' : (observedTools || reachableTools ? 'local-probe' : 'local-config')),
3723
4152
  probeError,
3724
4153
  authRequired: rest.includes('--auth-required'),
3725
4154
  }, { env: process.env, cwd: process.cwd() });
3726
4155
  let eventResult = null;
3727
4156
  try { eventResult = recordOrchestrationReadinessEvent(readiness, { homeDir: HOME }); } catch { /* telemetry must not break preflight */ }
3728
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
+
3729
4178
  if (wantsJson) {
3730
- println(JSON.stringify({ ...readiness, eventPath: eventResult?.path ?? null }, null, 2));
4179
+ println(JSON.stringify({ ...readiness, eventPath: eventResult?.path ?? null, deepProbes }, null, 2));
3731
4180
  process.exit(readiness.attached ? 0 : 1);
3732
4181
  }
3733
4182
 
3734
4183
  println(summarizeOrchestrationReadiness(readiness));
3735
4184
  println(`Reason: ${readiness.reasonCode}`);
3736
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
+ }
3737
4197
  println(`Diagnostic id: ${readiness.diagnosticBundle?.diagnosticId}`);
3738
4198
  process.exit(readiness.attached ? 0 : 1);
3739
4199
  }
@@ -3751,6 +4211,10 @@ async function cmdOrchestrate(args) {
3751
4211
  if (sub === 'run') {
3752
4212
  const text = flag('--text') || positional.join(' ').trim();
3753
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
+ }
3754
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) }) });
3755
4219
  const env = await res.json();
3756
4220
  if (!res.ok) { errorln(`Daemon error (${res.status}): ${env.error || 'unknown'}`); process.exit(1); }
@@ -3759,6 +4223,7 @@ async function cmdOrchestrate(args) {
3759
4223
  return;
3760
4224
  }
3761
4225
  if (sub === 'status') {
4226
+ const { shapeRun } = await import('../lib/mcp/tools/orchestration-run.mjs');
3762
4227
  const runId = positional[0];
3763
4228
  const path = runId ? `/api/orchestration/runs/${encodeURIComponent(runId)}` : '/api/orchestration/runs';
3764
4229
  const res = await fetch(`${base}${path}`, { headers });
@@ -3766,10 +4231,16 @@ async function cmdOrchestrate(args) {
3766
4231
  if (!res.ok) { errorln(`Daemon error (${res.status}): ${env.error || 'unknown'}`); process.exit(1); }
3767
4232
  if (wantsJson) { println(JSON.stringify(env.data, null, 2)); return; }
3768
4233
  if (runId) {
3769
- 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}` : ''}`);
3770
4238
  for (const t of env.data.tasks || []) println(` ${t.id} ${t.role} — ${t.status}${t.output ? ` (${t.output.length} chars)` : ''}`);
3771
4239
  } else {
3772
- 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
+ }
3773
4244
  }
3774
4245
  return;
3775
4246
  }
@@ -3781,9 +4252,21 @@ async function cmdOrchestrate(args) {
3781
4252
 
3782
4253
  if (sub === 'run') {
3783
4254
  const { runOrchestration, planRun, hostAdapterMetadata } = await import('../lib/orchestration/runtime.mjs');
4255
+ const { shapeRun } = await import('../lib/mcp/tools/orchestration-run.mjs');
3784
4256
  const positional = rest.filter((a, i) => !a.startsWith('--') && !(i > 0 && rest[i - 1].startsWith('--') && !['--no-construct', '--no-execute', '--json'].includes(rest[i - 1])));
3785
4257
  const text = flag('--text') || positional.join(' ').trim();
3786
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
+ }
3787
4270
  const request = {
3788
4271
  request: text,
3789
4272
  workflowType: flag('--workflow-type'),
@@ -3792,14 +4275,25 @@ async function cmdOrchestrate(args) {
3792
4275
  host: flag('--host'),
3793
4276
  hostModel: flag('--host-model'),
3794
4277
  hostProvider: flag('--host-provider'),
3795
- workerBackend: flag('--worker-backend'),
4278
+ workerBackend: cliWorkerBackend,
3796
4279
  fileCount: Number(flag('--file-count') || 0),
3797
4280
  moduleCount: Number(flag('--module-count') || 0),
3798
4281
  };
3799
- 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);
3800
4288
  if (wantsJson) { println(JSON.stringify(hostAdapterMetadata(run), null, 2)); return; }
3801
- println(`Run ${run.runId} — ${run.status} · executionMode=${run.execution.executionMode} · backend=${run.workerBackend} · hostRole=${run.hostRole}`);
3802
- 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}`);
3803
4297
  if (run.tasks.length) println(` tasks: ${run.tasks.map((t) => `${t.role}(${t.status})`).join(' → ')}`);
3804
4298
  else println(' tasks: none (no Construct-owned specialist sequence for this execution mode)');
3805
4299
  for (const t of run.tasks) {
@@ -3810,12 +4304,14 @@ async function cmdOrchestrate(args) {
3810
4304
 
3811
4305
  if (sub === 'status') {
3812
4306
  const { getRun, getRuns, hostAdapterMetadata } = await import('../lib/orchestration/runtime.mjs');
4307
+ const { shapeRun } = await import('../lib/mcp/tools/orchestration-run.mjs');
3813
4308
  const runId = rest.find((a) => !a.startsWith('--'));
3814
4309
  if (runId) {
3815
4310
  const run = await getRun(process.cwd(), runId);
3816
4311
  if (!run) { errorln(`Run not found: ${runId}`); process.exit(1); }
3817
4312
  if (wantsJson) { println(JSON.stringify(hostAdapterMetadata(run), null, 2)); return; }
3818
- 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}` : ''}`);
3819
4315
  for (const t of run.tasks) {
3820
4316
  println(` ${t.id} ${t.role} — ${t.status}`);
3821
4317
  if (t.reasoning) println(` reasoning: ${t.reasoning.length > 300 ? `${t.reasoning.slice(0, 300)}…` : t.reasoning}`);
@@ -3825,7 +4321,10 @@ async function cmdOrchestrate(args) {
3825
4321
  const runs = await getRuns(process.cwd());
3826
4322
  if (wantsJson) { println(JSON.stringify(runs, null, 2)); return; }
3827
4323
  if (!runs.length) { println('No orchestration runs recorded.'); return; }
3828
- 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
+ }
3829
4328
  return;
3830
4329
  }
3831
4330
 
@@ -3835,6 +4334,85 @@ async function cmdOrchestrate(args) {
3835
4334
  println(' preflight [--host H] [--json] [--no-probe] [--observed-tools=a,b] [--reachable-tools=a,b]');
3836
4335
  }
3837
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
+
3838
4416
  async function cmdModels(args) {
3839
4417
  const envPath = getUserEnvPath(HOME);
3840
4418
  const sub = args[0] && !args[0].startsWith('-') ? args[0] : null;
@@ -4903,6 +5481,10 @@ async function cmdSpecialist(args) {
4903
5481
  const sub = args[0];
4904
5482
  if (sub === 'lint') return cmdLintPrompts();
4905
5483
 
5484
+ if (sub === 'create' && args.includes('--custom')) {
5485
+ return cmdSpecialistCreateCustom(args.slice(1));
5486
+ }
5487
+
4906
5488
  if (sub === 'create') {
4907
5489
  const role = args[1];
4908
5490
  if (!role) { errorln('Usage: construct specialist create <role>'); process.exit(1); }
@@ -4939,6 +5521,91 @@ async function cmdSpecialist(args) {
4939
5521
  process.exit(1);
4940
5522
  }
4941
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
+
4942
5609
  async function cmdBackup(args) {
4943
5610
  // Postgres-era backups were removed with the SQL backend (lib/storage/backup.mjs).
4944
5611
  // State is now Git-backed (.cx/) plus the local LanceDB index under .cx/lancedb.
@@ -4947,6 +5614,14 @@ async function cmdBackup(args) {
4947
5614
  process.exit(1);
4948
5615
  }
4949
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
+
4950
5625
  async function cmdProvider(args) {
4951
5626
  const sub = args[0];
4952
5627
 
@@ -4971,6 +5646,164 @@ async function cmdProvider(args) {
4971
5646
  return;
4972
5647
  }
4973
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
+
4974
5807
  if (sub === 'info') {
4975
5808
  const id = args[1];
4976
5809
  if (!id) { errorln('Usage: construct provider info <id>'); process.exit(1); }
@@ -5052,7 +5885,105 @@ async function cmdProvider(args) {
5052
5885
  return;
5053
5886
  }
5054
5887
 
5055
- 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`);
5056
5987
  process.exit(1);
5057
5988
  }
5058
5989
 
@@ -5211,9 +6142,9 @@ async function cmdCreds(args) {
5211
6142
  try {
5212
6143
  const { getProviderModelCatalog } = await import('../lib/model-router.mjs');
5213
6144
  const { providers } = getProviderModelCatalog({ env: process.env });
5214
- 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');
5215
6146
  for (const p of providers) {
5216
- println(` ${p.configured ? '✓' : '·'} ${p.id}`);
6147
+ println(` ${p.configured ? '✓ configured (unverified ref)' : '· not configured'} ${p.id}`);
5217
6148
  }
5218
6149
  println(`\n Set keys as values or op:// refs in ${getUserEnvPath(HOME)}; run \`construct creds login copilot\` for GitHub Copilot.`);
5219
6150
  } catch { /* readiness view is informational */ }
@@ -5609,15 +6540,16 @@ const handlers = new Map([
5609
6540
  ['dev', cmdDev],
5610
6541
  ['stop', cmdStop],
5611
6542
  ['status', cmdStatus],
6543
+ ['workers', cmdWorkers],
5612
6544
  ['install', cmdInstall],
5613
6545
  ['config', cmdConfig],
6546
+ ['approvals', cmdApprovals],
5614
6547
  ['sources', cmdSources],
5615
6548
  ['intake', cmdIntake],
5616
6549
  ['recommendations', cmdRecommendations],
5617
6550
  ['integrations', cmdIntegrations],
5618
6551
  ['customer', cmdCustomer],
5619
6552
  ['workspace', cmdWorkspace],
5620
- ['graph', cmdGraph],
5621
6553
  ['uninstall', cmdUninstall],
5622
6554
  ['update', cmdUpdate],
5623
6555
  ['upgrade', cmdUpgrade],
@@ -5892,12 +6824,13 @@ const handlers = new Map([
5892
6824
  const sub = args[0];
5893
6825
  if (sub === 'review') return runTeamReviewCli(args.slice(1));
5894
6826
  if (sub === 'templates') { listTeamTemplates(); return; }
6827
+ if (sub === 'create') return cmdTeamCreateCustom(args.slice(1));
5895
6828
  if (sub === 'list' || sub === 'show') {
5896
6829
  const { runTeamList, runTeamShow } = await import('../lib/registry/cli.mjs');
5897
6830
  if (sub === 'list') return runTeamList(args.slice(1), { rootDir: ROOT_DIR, println, errorln });
5898
6831
  return runTeamShow(args.slice(1), { rootDir: ROOT_DIR, println, errorln });
5899
6832
  }
5900
- errorln(`Unknown team subcommand: ${sub}. Available: list, show, review, templates`);
6833
+ errorln(`Unknown team subcommand: ${sub}. Available: list, show, create, review, templates`);
5901
6834
  process.exit(1);
5902
6835
  }],
5903
6836
  ['role', async (args) => {
@@ -6128,6 +7061,9 @@ const handlers = new Map([
6128
7061
  ['telemetry', async (args) => {
6129
7062
  return cmdTelemetryQuery(args);
6130
7063
  }],
7064
+ ['db', async (args) => {
7065
+ return cmdDb(args);
7066
+ }],
6131
7067
  ['beads', async (args) => {
6132
7068
  const sub = args[0];
6133
7069
  const subArgs = args.slice(1);
@@ -6356,15 +7292,42 @@ const handlers = new Map([
6356
7292
  if (typeof code === 'number' && code !== 0) process.exit(code);
6357
7293
  }],
6358
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
+ }
6359
7299
  const { runGraphCli } = await import('../lib/graph/cli.mjs');
6360
7300
  const code = runGraphCli(args, { rootDir: ROOT_DIR, projectDir: process.cwd() });
6361
7301
  if (typeof code === 'number' && code !== 0) process.exit(code);
6362
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)],
6363
7321
  ['impact', async (args) => {
6364
7322
  const { runImpactCli } = await import('../lib/graph/impact-cli.mjs');
6365
7323
  const code = await runImpactCli(args, { rootDir: ROOT_DIR, projectDir: process.cwd() });
6366
7324
  if (typeof code === 'number' && code !== 0) process.exit(code);
6367
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
+ }],
6368
7331
  ['reflect', async (args) => {
6369
7332
  const { runReflectCli } = await import('../lib/reflect.mjs');
6370
7333
  return runReflectCli(args);
@@ -6628,7 +7591,7 @@ const handlers = new Map([
6628
7591
 
6629
7592
  println('');
6630
7593
  println(`Scaffolding a new scope draft. Three questions, then a preview.`);
6631
- 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`);
6632
7595
  println('');
6633
7596
  if (!displayName) displayName = await ask('Display name?', id);
6634
7597
  if (seedDepartments.length === 0) {
@@ -6687,11 +7650,11 @@ const handlers = new Map([
6687
7650
  println('');
6688
7651
  println(`✓ draft created at ${pathMod.relative(process.cwd(), result.dir)}`);
6689
7652
  println('');
6690
- println(`Next steps (in order, per docs/guides/concepts/scope-lifecycle.md):`);
6691
- 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.`);
6692
7655
  println(` 2. Frame (cx-product-manager): fill departments/<dept>.md charters and intake taxonomy.`);
6693
7656
  println(` 3. Architect (cx-architect): reconcile role reuse vs new; populate departments[] in scope.json.`);
6694
- 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.`);
6695
7658
  println('');
6696
7659
  println(`Inspect the brief: open ${pathMod.relative(process.cwd(), result.briefPath)}`);
6697
7660
  return;