@geraldmaron/construct 1.2.0 → 1.2.2

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 (214) hide show
  1. package/README.md +8 -7
  2. package/bin/construct +206 -62
  3. package/commands/plan/decide.md +1 -1
  4. package/commands/remember/runbook.md +1 -1
  5. package/examples/README.md +1 -1
  6. package/examples/distribution/README.md +1 -1
  7. package/examples/distribution/sources/adr.md +2 -2
  8. package/examples/distribution/sources/deck-one-pager.md +1 -1
  9. package/examples/distribution/sources/prd-platform.md +1 -1
  10. package/examples/distribution/sources/research-brief.md +2 -2
  11. package/examples/distribution/sources/rfc-platform.md +1 -1
  12. package/examples/distribution/sources/runbook.md +2 -2
  13. package/examples/distribution/sources/strategy.md +1 -1
  14. package/lib/agent-instructions/inject.mjs +1 -1
  15. package/lib/artifact-manifest.mjs +150 -4
  16. package/lib/artifact-type-from-path.mjs +9 -5
  17. package/lib/artifact-workflow.mjs +252 -0
  18. package/lib/audit-specialists.mjs +1 -1
  19. package/lib/audit-trail.mjs +3 -3
  20. package/lib/auto-docs.mjs +13 -11
  21. package/lib/bootstrap/built-ins.mjs +3 -3
  22. package/lib/boundary.mjs +3 -2
  23. package/lib/brand-prose.mjs +1 -1
  24. package/lib/cache-governor.js +4 -4
  25. package/lib/certification/document-io-fixtures.mjs +1 -1
  26. package/lib/chat/config.mjs +2 -2
  27. package/lib/chat/desktop-binary.mjs +5 -3
  28. package/lib/chat/present.mjs +1 -0
  29. package/lib/cli-commands.mjs +23 -6
  30. package/lib/cli-service-inventory.mjs +42 -0
  31. package/lib/comment-lint.mjs +9 -5
  32. package/lib/config/alias.mjs +3 -2
  33. package/lib/config/intake-policy.mjs +139 -0
  34. package/lib/config/project-config.mjs +106 -7
  35. package/lib/config/schema.mjs +45 -6
  36. package/lib/config/source-targets.mjs +311 -0
  37. package/lib/config/xdg.mjs +63 -0
  38. package/lib/contracts/violation-log.mjs +2 -2
  39. package/lib/cost-ledger.mjs +4 -3
  40. package/lib/cost.mjs +3 -2
  41. package/lib/decisions/registry.mjs +3 -3
  42. package/lib/distill.mjs +2 -2
  43. package/lib/docs-verify.mjs +2 -0
  44. package/lib/doctor/audit.mjs +4 -3
  45. package/lib/doctor/cli.mjs +0 -1
  46. package/lib/doctor/index.mjs +4 -4
  47. package/lib/doctor/report.mjs +3 -3
  48. package/lib/doctor/source-checkout.mjs +16 -0
  49. package/lib/doctor/watchers/bd-watch.mjs +5 -5
  50. package/lib/doctor/watchers/cost.mjs +4 -4
  51. package/lib/doctor/watchers/disk.mjs +4 -3
  52. package/lib/doctor/watchers/service-health.mjs +5 -5
  53. package/lib/document-export.mjs +39 -17
  54. package/lib/document-extract.mjs +3 -3
  55. package/lib/document-ingest.mjs +13 -3
  56. package/lib/efficiency.mjs +2 -1
  57. package/lib/embed/approval-queue.mjs +3 -2
  58. package/lib/embed/auto-sources.mjs +44 -0
  59. package/lib/embed/cli.mjs +9 -6
  60. package/lib/embed/config.mjs +6 -5
  61. package/lib/embed/daemon.mjs +6 -5
  62. package/lib/embed/demand-fetch.mjs +50 -19
  63. package/lib/embed/inbox-live-watcher.mjs +1 -1
  64. package/lib/embed/inbox.mjs +52 -41
  65. package/lib/embed/intake-metrics.mjs +3 -2
  66. package/lib/embed/reconcile.mjs +1 -1
  67. package/lib/embed/semantic.mjs +3 -2
  68. package/lib/embed/supervision.mjs +4 -2
  69. package/lib/embed/worker.mjs +2 -1
  70. package/lib/embedded-contract/index.mjs +11 -0
  71. package/lib/engine/registry.mjs +3 -3
  72. package/lib/env-config.mjs +34 -12
  73. package/lib/export-branding.mjs +32 -0
  74. package/lib/features.mjs +4 -3
  75. package/lib/health-check.mjs +7 -6
  76. package/lib/hook-health.mjs +5 -4
  77. package/lib/hooks/_lib/log.mjs +4 -3
  78. package/lib/hooks/_lib/output-mode.mjs +2 -1
  79. package/lib/hooks/agent-tracker.mjs +3 -4
  80. package/lib/hooks/audit-reads.mjs +2 -2
  81. package/lib/hooks/audit-trail.mjs +2 -2
  82. package/lib/hooks/bash-output-logger.mjs +3 -3
  83. package/lib/hooks/brand-prose-lint.mjs +1 -1
  84. package/lib/hooks/ci-status-check.mjs +3 -2
  85. package/lib/hooks/context-watch.mjs +2 -2
  86. package/lib/hooks/context-window-recovery.mjs +5 -5
  87. package/lib/hooks/dep-audit.mjs +2 -2
  88. package/lib/hooks/doc-coupling-check.mjs +3 -2
  89. package/lib/hooks/edit-accumulator.mjs +3 -2
  90. package/lib/hooks/edit-guard.mjs +3 -3
  91. package/lib/hooks/guard-bash.mjs +2 -2
  92. package/lib/hooks/model-fallback.mjs +2 -1
  93. package/lib/hooks/policy-engine.mjs +4 -3
  94. package/lib/hooks/pre-compact.mjs +7 -7
  95. package/lib/hooks/readme-age-check.mjs +2 -2
  96. package/lib/hooks/session-optimize.mjs +4 -3
  97. package/lib/hooks/session-start.mjs +20 -16
  98. package/lib/hooks/stop-notify.mjs +9 -8
  99. package/lib/hooks/stop-typecheck.mjs +4 -3
  100. package/lib/hooks/test-watch.mjs +2 -2
  101. package/lib/host-disposition.mjs +1 -1
  102. package/lib/ingest/provider-extract.mjs +2 -1
  103. package/lib/init/detect-existing-structure.mjs +2 -2
  104. package/lib/init/doc-lanes.mjs +3 -10
  105. package/lib/init-docs.mjs +12 -54
  106. package/lib/init-unified.mjs +47 -92
  107. package/lib/init.mjs +7 -5
  108. package/lib/install/desktop-binary-download.mjs +5 -2
  109. package/lib/intake/constants.mjs +29 -0
  110. package/lib/intake/daemon.mjs +31 -8
  111. package/lib/intake/intake-config.mjs +32 -112
  112. package/lib/integrations/intake-integrations.mjs +8 -7
  113. package/lib/knowledge/rag.mjs +2 -2
  114. package/lib/knowledge/search.mjs +15 -15
  115. package/lib/maintenance/cleanup.mjs +14 -10
  116. package/lib/mcp/server.mjs +20 -2
  117. package/lib/mcp/tools/embedded-contract.mjs +14 -0
  118. package/lib/mcp/tools/project.mjs +2 -1
  119. package/lib/model-cheapest-provider.mjs +4 -3
  120. package/lib/model-pricing.mjs +3 -2
  121. package/lib/model-router.mjs +4 -3
  122. package/lib/models/catalog.mjs +2 -1
  123. package/lib/models/execution-capability-profile.mjs +2 -1
  124. package/lib/models/provider-poll.mjs +2 -1
  125. package/lib/ollama-manager.mjs +0 -1
  126. package/lib/op-log.mjs +2 -1
  127. package/lib/opencode-runtime-plugin.mjs +3 -2
  128. package/lib/oracle/artifact-gate.mjs +3 -0
  129. package/lib/oracle/cli.mjs +2 -1
  130. package/lib/oracle/execute.mjs +3 -2
  131. package/lib/oracle/index.mjs +2 -1
  132. package/lib/oracle/read-model.mjs +3 -2
  133. package/lib/performance/generate.mjs +4 -3
  134. package/lib/platforms/capabilities.mjs +1 -1
  135. package/lib/playwright-demo.mjs +2 -0
  136. package/lib/plugin-registry.mjs +2 -1
  137. package/lib/profiles/lifecycle.mjs +3 -3
  138. package/lib/project-profile.mjs +2 -1
  139. package/lib/project-root.mjs +9 -7
  140. package/lib/provider-capabilities.js +4 -3
  141. package/lib/providers/auth-manager.mjs +2 -1
  142. package/lib/providers/copilot-auth.mjs +3 -2
  143. package/lib/providers/creds.mjs +3 -2
  144. package/lib/providers/registry.mjs +3 -3
  145. package/lib/providers/secret-resolver.mjs +3 -2
  146. package/lib/read-tracker-store.mjs +2 -1
  147. package/lib/reconcile/mcp-entry-reconcile.mjs +2 -2
  148. package/lib/reflect.mjs +2 -1
  149. package/lib/registry/generate-docs.mjs +2 -2
  150. package/lib/roles/approval-surface.mjs +5 -4
  151. package/lib/roles/event-bus.mjs +0 -1
  152. package/lib/roles/gateway.mjs +6 -2
  153. package/lib/rules-delivery.mjs +1 -1
  154. package/lib/runtime-pressure.mjs +5 -3
  155. package/lib/sandbox.mjs +3 -2
  156. package/lib/scheduler/solo.mjs +6 -4
  157. package/lib/server/auth.mjs +4 -3
  158. package/lib/server/index.mjs +53 -26
  159. package/lib/server/insights.mjs +5 -4
  160. package/lib/server/rate-limit.mjs +5 -3
  161. package/lib/server/webhook.mjs +5 -3
  162. package/lib/service-manager.mjs +5 -3
  163. package/lib/setup.mjs +9 -8
  164. package/lib/status.mjs +6 -5
  165. package/lib/storage/embeddings-local.mjs +3 -2
  166. package/lib/storage/sync.mjs +2 -2
  167. package/lib/telemetry/client.mjs +0 -1
  168. package/lib/telemetry/intent-verifications.mjs +6 -6
  169. package/lib/telemetry/model-pricing-catalog.mjs +4 -3
  170. package/lib/telemetry/rule-calls.mjs +3 -3
  171. package/lib/telemetry/skill-calls.mjs +3 -3
  172. package/lib/template-registry.mjs +1 -0
  173. package/lib/templates/visual-requirements.mjs +1 -1
  174. package/lib/test-corpus-inventory.mjs +1 -1
  175. package/lib/uninstall/uninstall.mjs +12 -10
  176. package/package.json +3 -3
  177. package/platforms/claude/settings.template.json +43 -43
  178. package/rules/common/no-fabrication.md +1 -1
  179. package/rules/common/release-gates.md +1 -1
  180. package/rules/common/research.md +1 -1
  181. package/rules/common/review-before-change.md +1 -1
  182. package/scripts/sync-specialists.mjs +11 -8
  183. package/skills/docs/adr-workflow.md +3 -3
  184. package/skills/docs/init-docs.md +9 -9
  185. package/skills/docs/prd-workflow.md +5 -5
  186. package/skills/docs/product-intelligence-review.md +1 -1
  187. package/skills/docs/product-intelligence-workflow.md +2 -2
  188. package/skills/docs/product-signal-workflow.md +1 -1
  189. package/skills/docs/runbook-workflow.md +4 -4
  190. package/skills/exploration/unknown-codebase-onboarding.md +1 -1
  191. package/skills/operating/orchestration-reference.md +1 -1
  192. package/skills/routing.md +3 -3
  193. package/specialists/artifact-manifest.json +11 -2
  194. package/specialists/artifact-manifest.schema.json +31 -1
  195. package/specialists/prompts/cx-architect.md +1 -1
  196. package/specialists/prompts/cx-docs-keeper.md +1 -1
  197. package/specialists/prompts/cx-researcher.md +1 -1
  198. package/specialists/prompts/cx-sre.md +1 -1
  199. package/specialists/role-manifests.json +6 -6
  200. package/templates/distribution/construct-reference.docx +0 -0
  201. package/templates/docs/README.md +125 -0
  202. package/templates/docs/construct_guide.md +3 -4
  203. package/templates/docs/persona-artifact.md +1 -1
  204. package/templates/docs/prds/README.md +25 -0
  205. package/templates/docs/prds/templates/_template.md +206 -0
  206. package/templates/docs/prds/templates/meta-prd.template.md +177 -0
  207. package/templates/docs/prds/templates/prd-business.template.md +61 -0
  208. package/templates/docs/prds/templates/prd-platform.template.md +81 -0
  209. package/templates/docs/prds/templates/prfaq.template.md +38 -0
  210. package/templates/docs/rfcs/README.md +22 -0
  211. package/templates/docs/rfcs/templates/_template.md +58 -0
  212. package/templates/docs/rfcs/templates/rfc-platform.template.md +72 -0
  213. package/templates/homebrew/construct.rb +1 -1
  214. package/templates/workflows/new-feature.yml +8 -8
package/README.md CHANGED
@@ -28,7 +28,7 @@ Bootstrap local services (once per machine, opt-in to machine-scope writes):
28
28
  construct install --scope=user --yes
29
29
  ```
30
30
 
31
- `construct install` defaults to `--scope=project`, which writes nothing and prints scope guidance — see the [footprint contract](#footprint-contract) below or [ADR 0029](docs/adr/0029-install-scopes-and-hook-budgets.md). Use `--scope=user` for machine setup, `--scope=both` for both.
31
+ `construct install` defaults to `--scope=project`, which writes nothing and prints scope guidance — see the [footprint contract](#footprint-contract) below or [ADR 0029](docs/decisions/adr/0029-install-scopes-and-hook-budgets.md). Use `--scope=user` for machine setup, `--scope=both` for both.
32
32
 
33
33
  Initialize a project:
34
34
 
@@ -87,7 +87,7 @@ Pick or change modes with `construct config mode [solo|team|enterprise]`. [Deplo
87
87
 
88
88
  ## Intake
89
89
 
90
- Anything dropped into `.cx/inbox/` (a bug report, a customer comment, a competitor PDF, a postmortem draft) is classified by the active profile's intake taxonomy. The default `rnd` profile uses bug, user-signal, experiment, architecture, incident, security, requirement, research, ops, eval-finding, launch-asset, legal-compliance. The `operations` profile uses request, incident, ops, security, docs. The `creative` profile uses brief, content-request, asset, experiment, report. The `research` profile uses question, study, synthesis, report.
90
+ Anything dropped into `inbox/` (a bug report, a customer comment, a competitor PDF, a postmortem draft) is classified by the active profile's intake taxonomy. The default `rnd` profile uses bug, user-signal, experiment, architecture, incident, security, requirement, research, ops, eval-finding, launch-asset, legal-compliance. The `operations` profile uses request, incident, ops, security, docs. The `creative` profile uses brief, content-request, asset, experiment, report. The `research` profile uses question, study, synthesis, report.
91
91
 
92
92
  Each signal gets a primary owner and a recommended handoff chain. Inspect with `construct intake list` and `construct intake show <id>`. Generate a task graph with `construct graph from-intake <id>`. The classifier runs in the daemon and is deterministic. The agent in your editor does the actual analysis. [Intake and triage](https://geraldmaron.github.io/construct/concepts/intake-and-triage).
93
93
 
@@ -111,11 +111,11 @@ Construct's writes are scoped and disclosed up front. The default `construct ins
111
111
  | Machine | `construct install --scope=user` | `~/.construct/config.env`, `~/.construct/lib` (symlink), `~/.construct/services/`, `~/Library/LaunchAgents/` (macOS), MCP entries in `~/.config/opencode/opencode.json` and `~/.codex/config.toml`, marker block in `~/.claude/CLAUDE.md`, hook injection in `~/.claude/settings.json` (last two require interactive consent or `--yes`) |
112
112
  | Never touched | — | Shell rc files (`~/.bashrc`, `~/.zshrc`), npm global config, `git config --global` |
113
113
 
114
- Full table with file:line citations and the per-hook performance budget contract: [Architecture — Footprint contract](https://geraldmaron.github.io/construct/concepts/architecture#footprint-contract) and [ADR 0029](docs/adr/0029-install-scopes-and-hook-budgets.md).
114
+ Full table with file:line citations and the per-hook performance budget contract: [Architecture — Footprint contract](https://geraldmaron.github.io/construct/concepts/architecture#footprint-contract) and [ADR 0029](docs/decisions/adr/0029-install-scopes-and-hook-budgets.md).
115
115
 
116
116
  ## Learning loops
117
117
 
118
- Construct gets smarter on its own. Every session ends with an automatic capture: tools used, files touched, what the final reply said. That goes into `.cx/observations/` and is searchable from the next session. See [`docs/concepts/learning-loops.mdx`](./docs/concepts/learning-loops.mdx) for what's wired, what's coming, and how to turn pieces off.
118
+ Construct gets smarter on its own. Every session ends with an automatic capture: tools used, files touched, what the final reply said. That goes into `.cx/observations/` and is searchable from the next session. See [`docs/guides/concepts/learning-loops.mdx`](./docs/guides/concepts/learning-loops.mdx) for what's wired, what's coming, and how to turn pieces off.
119
119
 
120
120
  ## `.cx/` is local-only runtime state
121
121
 
@@ -148,7 +148,7 @@ The embed daemon writes its supervisor stdout log to `~/.cx/runtime/embed-daemon
148
148
 
149
149
  | Command | What it does |
150
150
  |---|---|
151
- | `construct artifact` | Validate typed document artifacts against the release gate |
151
+ | `construct artifact` | Plan or locally execute manifest-backed artifact workflows with execution provenance |
152
152
  | `construct ask` | One-shot ask against the active knowledge index |
153
153
  | `construct bootstrap` | Import seed observation corpus into local memory store for cold-start acceleration |
154
154
  | `construct customer` | Manage customer profiles for product intelligence |
@@ -226,7 +226,7 @@ The embed daemon writes its supervisor stdout log to `~/.cx/runtime/embed-daemon
226
226
  | `construct doc` | Verify or inspect auditability stamps on Construct-generated markdown files |
227
227
  | `construct docs:check` | Check for missing how-to guides (alias for `docs check`) |
228
228
  | `construct docs:reconcile` | Reconcile docs against the registry |
229
- | `construct docs:site` | Regenerate generated reference pages under docs/reference/ |
229
+ | `construct docs:site` | Regenerate generated reference pages under docs/guides/reference/ |
230
230
  | `construct docs:update` | Regenerate AUTO-managed doc regions (alias for `docs update`) |
231
231
  | `construct docs:verify` | Validate documentation quality (alias for `docs verify`) |
232
232
  | `construct impact` | Change-impact analysis — map changed files to affected tests, capabilities, and workflows |
@@ -257,6 +257,7 @@ The embed daemon writes its supervisor stdout log to `~/.cx/runtime/embed-daemon
257
257
  | `construct roles:set` | Activate a role contract |
258
258
  | `construct scheduler` | Manage scheduled background jobs (tag-mining, doc-hygiene, skill-rollup) |
259
259
  | `construct skills` | Skill relevance detection |
260
+ | `construct sources` | Manage typed integration source targets in construct.config.json |
260
261
  | `construct uninstall` | Remove Construct state |
261
262
  | `construct update` | Reinstall this checkout |
262
263
  | `construct upgrade` | Upgrade to latest npm version |
@@ -268,7 +269,7 @@ The embed daemon writes its supervisor stdout log to `~/.cx/runtime/embed-daemon
268
269
 
269
270
  - [`CONTRIBUTING.md`](./CONTRIBUTING.md). Branch workflow, gates, review expectations.
270
271
  - [`CHANGELOG.md`](./CHANGELOG.md). Release history.
271
- - [`docs/concepts/architecture.mdx`](./docs/concepts/architecture.mdx). Canonical architecture.
272
+ - [`docs/guides/concepts/architecture.mdx`](./docs/guides/concepts/architecture.mdx). Canonical architecture.
272
273
  - [`AGENTS.md`](./AGENTS.md). Agent operating contract.
273
274
 
274
275
  ## Project structure
package/bin/construct CHANGED
@@ -38,6 +38,7 @@ import {
38
38
  PROJECT_CONFIG_FILENAME,
39
39
  } from '../lib/config/project-config.mjs';
40
40
  import { validateProjectConfig } from '../lib/config/schema.mjs';
41
+ import { configDir, doctorRoot } from '../lib/config/xdg.mjs';
41
42
  import {
42
43
  DEPLOYMENT_MODES,
43
44
  DEPLOYMENT_MODE_ENV_KEY,
@@ -400,7 +401,7 @@ async function cmdDoctor() {
400
401
  }
401
402
 
402
403
  // .env files
403
- for (const envPath of [join(process.cwd(), '.env'), join(homedir(), '.env'), join(homedir(), '.construct', 'config.env')]) {
404
+ for (const envPath of [join(process.cwd(), '.env'), join(homedir(), '.env'), getUserEnvPath(homedir())]) {
404
405
  if (fs.existsSync(envPath)) {
405
406
  try {
406
407
  const content = fs.readFileSync(envPath, 'utf8');
@@ -500,15 +501,19 @@ async function cmdDoctor() {
500
501
 
501
502
  const checks = [];
502
503
  const add = (label, pass, optional = false) => checks.push({ label, pass, optional });
504
+ const { isConstructSourceCheckout } = await import('../lib/doctor/source-checkout.mjs');
505
+ const isSourceCheckout = isConstructSourceCheckout(ROOT_DIR);
503
506
  add('registry.json exists', fs.existsSync(path.join(ROOT_DIR, 'specialists', 'registry.json')));
504
507
  add('sync-specialists.mjs exists', fs.existsSync(path.join(ROOT_DIR, 'scripts', 'sync-specialists.mjs')));
505
508
 
506
- // Dashboard static build output. The Next.js app under apps/dashboard
507
- // emits a static export into lib/server/static/ via its postbuild script;
508
- // an empty static dir means lib/server/index.mjs will serve 404s.
509
+ // Dashboard static build output is absent from published npm installs.
509
510
 
510
- const dashboardIndex = path.join(ROOT_DIR, 'lib', 'server', 'static', 'index.html');
511
- add('Dashboard built (lib/server/static/index.html)', fs.existsSync(dashboardIndex));
511
+ if (isSourceCheckout) {
512
+ const dashboardIndex = path.join(ROOT_DIR, 'lib', 'server', 'static', 'index.html');
513
+ add('Dashboard built (lib/server/static/index.html)', fs.existsSync(dashboardIndex));
514
+ } else {
515
+ add('Dashboard built (skipped — published package)', true, true);
516
+ }
512
517
 
513
518
  // Project .gitignore must cover .cx/. Auto-fix via --fix-gitignore.
514
519
  // Only checks when cwd looks like a Construct project (has .cx/ or
@@ -588,7 +593,7 @@ async function cmdDoctor() {
588
593
  // minute at 50MB, but a freshly-upgraded install with a 34GB legacy
589
594
  // log shouldn't wait 60s. --fix-embed-log forces rotation now.
590
595
 
591
- const embedLogPath = path.join(userHome, '.cx', 'runtime', 'embed-daemon.log');
596
+ const embedLogPath = path.join(doctorRoot(userHome), 'runtime', 'embed-daemon.log');
592
597
  if (fs.existsSync(embedLogPath)) {
593
598
  const sizeMb = fs.statSync(embedLogPath).size / 1024 / 1024;
594
599
  const FORCE_THRESHOLD_MB = Number(process.env.CONSTRUCT_DOCTOR_EMBED_LOG_FORCE_MB) || 500;
@@ -623,11 +628,11 @@ async function cmdDoctor() {
623
628
  'intent-verifications.jsonl',
624
629
  ];
625
630
  const legacyState = projectScopedNames
626
- .map((name) => ({ name, path: path.join(userHome, '.cx', name) }))
631
+ .map((name) => ({ name, path: path.join(doctorRoot(userHome), name) }))
627
632
  .filter((p) => fs.existsSync(p.path) && fs.statSync(p.path).size > 0);
628
633
 
629
634
  if (legacyState.length > 0 && wantsFixMigrateState) {
630
- const archiveDir = path.join(userHome, '.cx', 'legacy');
635
+ const archiveDir = path.join(doctorRoot(userHome), 'legacy');
631
636
  fs.mkdirSync(archiveDir, { recursive: true });
632
637
  const ts = new Date().toISOString().replace(/[:.]/g, '-');
633
638
  const moved = [];
@@ -735,13 +740,13 @@ async function cmdDoctor() {
735
740
  const vscodeSettingsOk = vscodeSettingsPaths.some((candidate) => fs.existsSync(candidate));
736
741
  if (vscodeSettingsOk) add('VS Code settings file', true, true);
737
742
  }
738
- add('User config ready', fs.existsSync(getUserEnvPath(HOME)) || fs.existsSync(path.join(HOME, '.construct')), true);
743
+ add('User config ready', fs.existsSync(getUserEnvPath(HOME)) || fs.existsSync(configDir(HOME)), true);
739
744
  add('skills/ directory', fs.existsSync(path.join(ROOT_DIR, 'skills')));
740
745
  add('rules/common/', fs.existsSync(path.join(ROOT_DIR, 'rules', 'common')));
741
746
  add('specialists/prompts/ directory', fs.existsSync(path.join(ROOT_DIR, 'specialists', 'prompts')));
742
747
  add('Hybrid storage env keys documented', fs.existsSync(path.join(ROOT_DIR, '.env.example')));
743
- fs.mkdirSync(path.join(HOME, '.cx', 'performance-reviews'), { recursive: true });
744
- add('~/.cx/performance-reviews/ ready', true);
748
+ fs.mkdirSync(path.join(doctorRoot(), 'performance-reviews'), { recursive: true });
749
+ add('performance-reviews/ ready', true);
745
750
 
746
751
  // construct.config.json — optional today (config is permitted to be absent;
747
752
  // env vars + defaults still work). The check enforces that when the file
@@ -761,33 +766,29 @@ async function cmdDoctor() {
761
766
 
762
767
  // Claude Code Stop-hook installation check. The settings template
763
768
  // wires several Stop hooks (policy-engine / stop-typecheck /
764
- // stop-notify / readme-age-check) that reference
765
- // $HOME/.construct/lib/hooks/*. If that path doesn't resolve, every
766
- // Stop hook fails silently — including stop-notify, which is what
767
- // appends session cost data. Result: the dashboard stops updating
768
- // and the user sees stale data. Surface loudly so the fix is one
769
- // command away.
769
+ // stop-notify / readme-age-check) whose commands sync resolves to the
770
+ // hook lib symlink. If those scripts don't resolve, every Stop hook
771
+ // fails silently — including stop-notify, which is what appends session
772
+ // cost data. Result: the dashboard stops updating and the user sees stale
773
+ // data. Surface loudly so the fix is one command away.
770
774
  try {
771
775
  const settingsPath = path.join(HOME, '.claude', 'settings.json');
772
776
  if (fs.existsSync(settingsPath)) {
773
777
  const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
774
778
  const stopHooks = settings?.hooks?.Stop || [];
775
- const installRoot = path.join(HOME, '.construct', 'lib', 'hooks');
776
- const installed = fs.existsSync(installRoot);
779
+ const installRoot = path.join(configDir(HOME), 'lib', 'hooks');
777
780
  const referenced = [];
778
781
  for (const group of stopHooks) {
779
782
  for (const h of (group?.hooks || [])) {
780
783
  const cmd = String(h?.command || '');
781
- const m = cmd.match(/\.construct\/lib\/hooks\/([a-zA-Z0-9_-]+\.mjs)/);
784
+ const m = cmd.match(/(?:^|["\s])(\/[^"\s]*\/lib\/hooks\/[a-zA-Z0-9_-]+\.mjs)/);
782
785
  if (m) referenced.push(m[1]);
783
786
  }
784
787
  }
785
- const missing = installed
786
- ? referenced.filter((f) => !fs.existsSync(path.join(installRoot, f)))
787
- : referenced;
788
+ const missing = referenced.filter((scriptPath) => !fs.existsSync(scriptPath));
788
789
  if (referenced.length > 0 && missing.length > 0) {
789
790
  add(
790
- `Claude Stop hooks broken: ${missing.length} hook${missing.length === 1 ? '' : 's'} missing in ~/.construct/lib/hooks/. Symlink the install: ln -sfn ${ROOT_DIR}/lib ~/.construct/lib`,
791
+ `Claude Stop hooks broken: ${missing.length} hook${missing.length === 1 ? '' : 's'} do not resolve. Re-run \`construct install --scope=user\` (symlinks ${ROOT_DIR}/lib → ${installRoot.replace(/\/hooks$/, '')}).`,
791
792
  false,
792
793
  false,
793
794
  );
@@ -858,8 +859,21 @@ async function cmdDoctor() {
858
859
  add('Headhunt classifier agents exist in registry', classifierOrphans.length === 0);
859
860
  const skillAudit = auditSkills({ rootDir: ROOT_DIR, silent: true });
860
861
  add('No declared skills missing on disk', skillAudit.pass, false);
861
- const specialistAudit = auditSpecialists({ rootDir: ROOT_DIR, silent: true });
862
- add('Specialist/skill audit cross-checks', specialistAudit.pass, false);
862
+ if (isSourceCheckout) {
863
+ const specialistAudit = auditSpecialists({ rootDir: ROOT_DIR, silent: true });
864
+ add('Specialist/skill audit cross-checks', specialistAudit.pass, false);
865
+
866
+ // The catalog is the public-service source of truth; doctor joins it to runtime dispatch
867
+ // and the generated reference so a command that is unroutable or undocumented surfaces here.
868
+
869
+ const { buildCliServiceInventory } = await import('../lib/cli-service-inventory.mjs');
870
+ const cliDrift = buildCliServiceInventory({ rootDir: ROOT_DIR })
871
+ .filter((entry) => !entry.runnable || !entry.documented);
872
+ add('Public CLI dispatch/reference parity', cliDrift.length === 0, false);
873
+ } else {
874
+ add('Specialist/skill audit cross-checks (skipped — published package)', true, true);
875
+ add('Public CLI dispatch/reference parity (skipped — published package)', true, true);
876
+ }
863
877
  const { lintResearchRepo } = await import('../lib/research-lint.mjs');
864
878
  const researchLintResults = lintResearchRepo({ rootDir: process.cwd() });
865
879
  add('Research artifacts meet minimum evidence structure', researchLintResults.every((entry) => entry.errors.length === 0), true);
@@ -1056,7 +1070,7 @@ async function cmdDoctor() {
1056
1070
  }
1057
1071
 
1058
1072
  try {
1059
- const failuresPath = path.join(HOME, '.cx', 'hook-failures.jsonl');
1073
+ const failuresPath = path.join(doctorRoot(), 'hook-failures.jsonl');
1060
1074
  if (fs.existsSync(failuresPath)) {
1061
1075
  const lines = fs.readFileSync(failuresPath, 'utf8').trim().split('\n').filter(Boolean);
1062
1076
  const cutoff = Date.now() - 24 * 60 * 60 * 1000;
@@ -1502,7 +1516,7 @@ async function cmdMigrate(args) {
1502
1516
  }
1503
1517
 
1504
1518
  async function cmdReview(args) {
1505
- fs.mkdirSync(path.join(HOME, '.cx', 'performance-reviews'), { recursive: true });
1519
+ fs.mkdirSync(path.join(doctorRoot(), 'performance-reviews'), { recursive: true });
1506
1520
  const sub = args[0];
1507
1521
 
1508
1522
  if (!sub || sub === 'run') {
@@ -1561,17 +1575,18 @@ async function cmdExport(args) {
1561
1575
  const outFlag = args.find((a) => a.startsWith('--output='))?.split('=')[1];
1562
1576
  const detectOnly = args.includes('--detect');
1563
1577
  const figures = !args.includes('--no-figures');
1578
+ const branding = args.includes('--plain') || args.includes('--no-brand') ? 'plain' : 'construct';
1564
1579
 
1565
1580
  const { detect, exportMarkdown, EXPORT_FORMATS } = await import('../lib/document-export.mjs');
1566
1581
 
1567
1582
  if (detectOnly) {
1568
1583
  const format = toFlag || 'pdf';
1569
- println(JSON.stringify(detect(format), null, 2));
1584
+ println(JSON.stringify(detect(format, process.env, { branding }), null, 2));
1570
1585
  return;
1571
1586
  }
1572
1587
 
1573
1588
  if (!inputPath) {
1574
- errorln('Usage: construct export <markdown-file> --to=<pdf|docx|html> [--output=<path>] [--figures|--no-figures] [--detect]');
1589
+ errorln('Usage: construct export <markdown-file> --to=<pdf|docx|html> [--output=<path>] [--figures|--no-figures] [--plain] [--detect]');
1575
1590
  process.exit(1);
1576
1591
  }
1577
1592
  if (!toFlag) {
@@ -1586,6 +1601,7 @@ async function cmdExport(args) {
1586
1601
  outputPath: resolvedOut,
1587
1602
  format: toFlag,
1588
1603
  figures: toFlag === 'pdf' ? figures : false,
1604
+ branding,
1589
1605
  repoRoot: ROOT_DIR,
1590
1606
  });
1591
1607
  if (!result.ok) {
@@ -1999,7 +2015,7 @@ async function _cmdPricing(args) {
1999
2015
  const f = describePricingCatalogFreshness();
2000
2016
  if (!f.present) { info(f.message); return; }
2001
2017
  try {
2002
- const cached = JSON.parse(fs.readFileSync(path.join(HOME, '.cx', 'pricing-cache.json'), 'utf8'));
2018
+ const cached = JSON.parse(fs.readFileSync(path.join(doctorRoot(), 'pricing-cache.json'), 'utf8'));
2003
2019
  const divergences = checkPricingDivergence(cached.models || []);
2004
2020
  if (divergences.length === 0) { info('No pricing divergences (>25%) between LiteLLM and the static fallback.'); return; }
2005
2021
  println(`⚠ ${divergences.length} divergence(s):`);
@@ -2097,15 +2113,13 @@ async function printIntakeHelp() {
2097
2113
  println(' construct intake metrics Show pipeline volume, velocity, and processing stats.');
2098
2114
  println(' construct intake integrate <id> <system> Push intake to external system (github, jira, confluence).');
2099
2115
  println(' construct intake integrate <id> <system> --create-issue Create an issue from the intake packet.');
2100
- println(' construct intake config Show current watch dirs + scan depth.');
2116
+ println(' construct intake config Show the inbox/ drop zone + scan depth.');
2101
2117
  println(' construct intake config set --depth=N [--add-dir=PATH] [--remove-dir=PATH]');
2118
+ println(' Update intake config. --depth caps at the hard limit.');
2102
2119
  println(' construct intake extraction-warnings [--since=7d] [--kind=table] [--limit=50]');
2103
2120
  println(' Summarize ~/.cx/intake/extraction-warnings.jsonl (dropped content log).');
2104
2121
  println(' construct intake needs-asr list List audio/video files awaiting ASR transcription.');
2105
2122
  println(' construct intake needs-asr show <id> Show a specific ASR-pending packet.');
2106
- println(' Update intake config. --depth caps at the hard limit.');
2107
- println(' construct intake config set --include-project-inbox=on|off');
2108
- println(' construct intake config set --include-docs-intake=on|off');
2109
2123
  }
2110
2124
 
2111
2125
  function parseKvFlags(args, keys) {
@@ -2599,7 +2613,6 @@ async function cmdIntakeReroute(args, cwd) {
2599
2613
 
2600
2614
  async function cmdIntakeExtractionWarnings(args) {
2601
2615
  const { createReadStream, existsSync } = await import('node:fs');
2602
- const { homedir } = await import('node:os');
2603
2616
  const { join } = await import('node:path');
2604
2617
  const { createInterface } = await import('node:readline');
2605
2618
 
@@ -2617,7 +2630,7 @@ async function cmdIntakeExtractionWarnings(args) {
2617
2630
  }
2618
2631
  }
2619
2632
 
2620
- const logPath = join(homedir(), '.cx', 'intake', 'extraction-warnings.jsonl');
2633
+ const logPath = join(doctorRoot(), 'intake', 'extraction-warnings.jsonl');
2621
2634
  if (!existsSync(logPath)) {
2622
2635
  println('No extraction warnings recorded yet.');
2623
2636
  return;
@@ -2668,11 +2681,10 @@ async function cmdIntakeExtractionWarnings(args) {
2668
2681
 
2669
2682
  async function cmdIntakeNeedsAsr(args) {
2670
2683
  const { existsSync, readdirSync, readFileSync } = await import('node:fs');
2671
- const { homedir } = await import('node:os');
2672
2684
  const { join } = await import('node:path');
2673
2685
 
2674
2686
  const sub = args[0] || 'list';
2675
- const asrDir = join(homedir(), '.cx', 'intake', 'needs-asr');
2687
+ const asrDir = join(doctorRoot(), 'intake', 'needs-asr');
2676
2688
 
2677
2689
  if (sub === 'list') {
2678
2690
  if (!existsSync(asrDir)) { println('No audio/video files awaiting ASR.'); return; }
@@ -2837,20 +2849,23 @@ async function cmdIntakeConfig(args) {
2837
2849
  const cwd = process.cwd();
2838
2850
 
2839
2851
  const action = args[0];
2852
+
2853
+ // Single-zone model (ADR-0045 §C): the canonical `inbox/` is always watched.
2854
+ // Only maxDepth and additionalDirs are configurable.
2855
+
2840
2856
  if (!action || action === 'show') {
2841
2857
  const cfg = loadIntakeConfig(cwd, process.env);
2842
2858
  const desc = describeIntakeDepth(cfg.maxDepth);
2843
2859
  println(`Intake config (root: ${cwd})`);
2844
2860
  println('');
2845
- println(` includeProjectInbox: ${cfg.includeProjectInbox ? 'on' : 'off'} (.cx/inbox)`);
2846
- println(` includeDocsIntake: ${cfg.includeDocsIntake ? 'on' : 'off'} (docs/intake)`);
2861
+ println(' drop zone: inbox/ (always watched)');
2847
2862
  println(` maxDepth: ${cfg.maxDepth} — ${desc.label}`);
2848
2863
  println(` ${desc.detail}`);
2849
2864
  println(` hardMaxDepth: ${INTAKE_HARD_MAX_DEPTH}`);
2850
2865
  if (cfg.parentDirs.length === 0) {
2851
- println(' parentDirs: (none — only the built-in zones are watched)');
2866
+ println(' additionalDirs: (none — only inbox/ is watched)');
2852
2867
  } else {
2853
- println(' parentDirs:');
2868
+ println(' additionalDirs:');
2854
2869
  for (const dir of cfg.parentDirs) println(` - ${dir}`);
2855
2870
  }
2856
2871
  println('');
@@ -2871,12 +2886,6 @@ async function cmdIntakeConfig(args) {
2871
2886
  const depthFlag = tail.find((a) => a.startsWith('--depth='));
2872
2887
  if (depthFlag) patch.maxDepth = Number(depthFlag.split('=')[1]);
2873
2888
 
2874
- const projectFlag = tail.find((a) => a.startsWith('--include-project-inbox='));
2875
- if (projectFlag) patch.includeProjectInbox = /^(on|true|1|yes)$/i.test(projectFlag.split('=')[1] || '');
2876
-
2877
- const docsFlag = tail.find((a) => a.startsWith('--include-docs-intake='));
2878
- if (docsFlag) patch.includeDocsIntake = /^(on|true|1|yes)$/i.test(docsFlag.split('=')[1] || '');
2879
-
2880
2889
  const addFlags = tail.filter((a) => a.startsWith('--add-dir='));
2881
2890
  const removeFlags = tail.filter((a) => a.startsWith('--remove-dir='));
2882
2891
 
@@ -2889,7 +2898,7 @@ async function cmdIntakeConfig(args) {
2889
2898
  }
2890
2899
 
2891
2900
  if (Object.keys(patch).length === 0) {
2892
- errorln('Nothing to change. Pass --depth=N, --add-dir=PATH, --remove-dir=PATH, --include-project-inbox=on|off, or --include-docs-intake=on|off.');
2901
+ errorln('Nothing to change. Pass --depth=N, --add-dir=PATH, or --remove-dir=PATH.');
2893
2902
  process.exit(1);
2894
2903
  }
2895
2904
 
@@ -2903,6 +2912,123 @@ async function cmdIntakeConfig(args) {
2903
2912
  }
2904
2913
  }
2905
2914
 
2915
+ async function cmdSources(args) {
2916
+ const {
2917
+ validateSourceTargets,
2918
+ normalizeConfigTarget,
2919
+ resolveEffectiveSourceTargetsFromConfig,
2920
+ legacyEnvSourceTargets,
2921
+ SOURCE_PROVIDERS,
2922
+ } = await import('../lib/config/source-targets.mjs');
2923
+ const {
2924
+ loadProjectConfig,
2925
+ writeProjectConfig,
2926
+ findProjectConfigPath,
2927
+ PROJECT_CONFIG_FILENAME,
2928
+ } = await import('../lib/config/project-config.mjs');
2929
+
2930
+ const cwd = process.cwd();
2931
+ const action = args[0] ?? 'list';
2932
+
2933
+ if (action === 'list' || action === 'show') {
2934
+ const { config, path: cfgPath } = loadProjectConfig(cwd, process.env);
2935
+ const configTargets = (config.sources?.targets ?? []).map(normalizeConfigTarget);
2936
+ const envTargets = legacyEnvSourceTargets(process.env);
2937
+ const effective = resolveEffectiveSourceTargetsFromConfig(config, process.env);
2938
+ println(`Source targets (config: ${cfgPath ?? 'default'})`);
2939
+ println('');
2940
+ if (configTargets.length === 0) println(' (no targets in construct.config.json)');
2941
+ for (const t of configTargets) {
2942
+ println(` ${t.id} · ${t.provider} · ${JSON.stringify(t.selector)}`);
2943
+ }
2944
+ if (envTargets.length) {
2945
+ println('');
2946
+ println('Legacy env targets (merged at runtime):');
2947
+ for (const t of envTargets) {
2948
+ println(` ${t.id} · ${t.provenance} · ${t.provider} · ${JSON.stringify(t.selector)}`);
2949
+ }
2950
+ }
2951
+ println('');
2952
+ println(`Effective targets: ${effective.length}`);
2953
+ return;
2954
+ }
2955
+
2956
+ if (action === 'validate') {
2957
+ const loaded = loadProjectConfig(cwd, process.env);
2958
+ const targetErrors = validateSourceTargets(loaded.raw?.sources?.targets ?? loaded.config?.sources?.targets ?? []);
2959
+ const all = [...(loaded.errors ?? []), ...targetErrors];
2960
+ if (!all.length) {
2961
+ info(`${loaded.path ?? PROJECT_CONFIG_FILENAME}: source targets valid`);
2962
+ return;
2963
+ }
2964
+ errorln(`Source target validation failed (${all.length}):`);
2965
+ for (const e of all) errorln(` - ${e}`);
2966
+ process.exit(1);
2967
+ }
2968
+
2969
+ if (action === 'add') {
2970
+ const provider = args[1];
2971
+ const id = args[2];
2972
+ const selectorRaw = args[3];
2973
+ if (!provider || !id || !selectorRaw) {
2974
+ errorln('Usage: construct sources add <provider> <id> <selector-json>');
2975
+ process.exit(1);
2976
+ }
2977
+ if (!SOURCE_PROVIDERS.includes(provider)) {
2978
+ errorln(`Unknown provider: ${provider}. Expected: ${SOURCE_PROVIDERS.join(', ')}`);
2979
+ process.exit(1);
2980
+ }
2981
+ let selector;
2982
+ try { selector = JSON.parse(selectorRaw); } catch (err) {
2983
+ errorln(`Invalid selector JSON: ${err.message}`);
2984
+ process.exit(1);
2985
+ }
2986
+ const target = normalizeConfigTarget({ id, provider, selector });
2987
+ const targetErrors = validateSourceTargets([target]);
2988
+ if (targetErrors.length) {
2989
+ for (const e of targetErrors) errorln(` - ${e}`);
2990
+ process.exit(1);
2991
+ }
2992
+ const cfgPath = findProjectConfigPath(cwd) || path.join(cwd, PROJECT_CONFIG_FILENAME);
2993
+ const { config } = loadProjectConfig(cwd, process.env);
2994
+ const existing = config.sources?.targets ?? [];
2995
+ if (existing.some((t) => t.id === id)) {
2996
+ errorln(`Target id already exists: ${id}`);
2997
+ process.exit(1);
2998
+ }
2999
+ const next = {
3000
+ ...config,
3001
+ sources: { ...(config.sources ?? {}), targets: [...existing, target] },
3002
+ };
3003
+ writeProjectConfig(cfgPath, next);
3004
+ info(`added source target ${id}`);
3005
+ return;
3006
+ }
3007
+
3008
+ if (action === 'remove') {
3009
+ const id = args[1];
3010
+ if (!id) {
3011
+ errorln('Usage: construct sources remove <id>');
3012
+ process.exit(1);
3013
+ }
3014
+ const cfgPath = findProjectConfigPath(cwd) || path.join(cwd, PROJECT_CONFIG_FILENAME);
3015
+ const { config } = loadProjectConfig(cwd, process.env);
3016
+ const existing = config.sources?.targets ?? [];
3017
+ const nextTargets = existing.filter((t) => t.id !== id);
3018
+ if (nextTargets.length === existing.length) {
3019
+ errorln(`No target with id: ${id}`);
3020
+ process.exit(1);
3021
+ }
3022
+ const next = { ...config, sources: { ...(config.sources ?? {}), targets: nextTargets } };
3023
+ writeProjectConfig(cfgPath, next);
3024
+ info(`removed source target ${id}`);
3025
+ return;
3026
+ }
3027
+
3028
+ errorln(`Unknown sources action: ${action}. Available: list, add, remove, validate`);
3029
+ process.exit(1);
3030
+ }
3031
+
2906
3032
  async function cmdIntakeMetrics() {
2907
3033
  try {
2908
3034
  const { computeIntakeMetrics, pendingAge } = await import('../lib/embed/intake-metrics.mjs');
@@ -3882,7 +4008,7 @@ async function cmdPluginEngine(args) {
3882
4008
  const [action = 'list', ...rest] = args;
3883
4009
  const isGlobal = rest.includes('--global');
3884
4010
  const targetPath = isGlobal
3885
- ? path.join(HOME, '.construct', 'plugins.json')
4011
+ ? path.join(configDir(HOME), 'plugins.json')
3886
4012
  : path.join(process.cwd(), '.cx', 'plugins.json');
3887
4013
 
3888
4014
  function readPluginsJson() {
@@ -4089,7 +4215,7 @@ async function cmdEvals(args) {
4089
4215
  const report = await evaluateFixture(fixturePath);
4090
4216
 
4091
4217
  try {
4092
- const cacheDir = path.join(HOME, '.cx', 'evals');
4218
+ const cacheDir = path.join(doctorRoot(), 'evals');
4093
4219
  fs.mkdirSync(cacheDir, { recursive: true });
4094
4220
  const ts = new Date().toISOString();
4095
4221
  fs.writeFileSync(
@@ -4286,7 +4412,7 @@ async function cmdDocsCheck(args) {
4286
4412
  println('Commands with no linked how-to in docs/README.md:');
4287
4413
  for (const name of result.uncovered) println(` ✗ construct ${name}`);
4288
4414
  println('');
4289
- println(`Add cookbook recipes under docs/cookbook/ and link them from the cookbook index.`);
4415
+ println(`Add cookbook recipes under docs/guides/cookbook/ and link them from the cookbook index.`);
4290
4416
  process.exitCode = 1;
4291
4417
  }
4292
4418
  }
@@ -4297,7 +4423,7 @@ async function cmdDocsSite(args = []) {
4297
4423
  if (check) {
4298
4424
  const { drift } = checkFumadocsReferenceDrift({ rootDir: ROOT_DIR });
4299
4425
  if (drift.length === 0) {
4300
- ok('docs/reference/ is up to date.');
4426
+ ok('docs/guides/reference/ is up to date.');
4301
4427
  return;
4302
4428
  }
4303
4429
  println('');
@@ -4308,9 +4434,9 @@ async function cmdDocsSite(args = []) {
4308
4434
  }
4309
4435
  const { written } = buildFumadocsReference({ rootDir: ROOT_DIR });
4310
4436
  if (written.length === 0) {
4311
- ok('docs/reference/ already up to date.');
4437
+ ok('docs/guides/reference/ already up to date.');
4312
4438
  } else {
4313
- ok(`docs/reference/ regenerated (${written.length} file${written.length === 1 ? '' : 's'}).`);
4439
+ ok(`docs/guides/reference/ regenerated (${written.length} file${written.length === 1 ? '' : 's'}).`);
4314
4440
  }
4315
4441
  }
4316
4442
 
@@ -4809,7 +4935,7 @@ async function cmdProvider(args) {
4809
4935
  if (!id) { errorln(`Usage: construct provider plugins ${action} <id> [<package>]`); process.exit(1); }
4810
4936
  const isGlobal = args.includes('--global');
4811
4937
  const targetPath = isGlobal
4812
- ? path.join(HOME, '.construct', 'providers.json')
4938
+ ? path.join(configDir(HOME), 'providers.json')
4813
4939
  : path.join(process.cwd(), '.cx', 'providers.json');
4814
4940
  let data = { providers: [] };
4815
4941
  try { data = JSON.parse(fs.readFileSync(targetPath, 'utf8')); } catch { /* fresh */ }
@@ -5334,7 +5460,7 @@ async function cmdTelemetryQuery(args) {
5334
5460
  println(' errors [--agent=<name>] [--since=24h] error span summary');
5335
5461
  println(' trace <traceId> full span tree');
5336
5462
  println('');
5337
- println('For Langfuse/Honeycomb/Grafana Tempo/Datadog, see docs/concepts/observability.md');
5463
+ println('For Langfuse/Honeycomb/Grafana Tempo/Datadog, see docs/guides/concepts/observability.md');
5338
5464
  }
5339
5465
 
5340
5466
  async function cmdHook(args) {
@@ -5400,6 +5526,7 @@ const handlers = new Map([
5400
5526
  ['status', cmdStatus],
5401
5527
  ['install', cmdInstall],
5402
5528
  ['config', cmdConfig],
5529
+ ['sources', cmdSources],
5403
5530
  ['intake', cmdIntake],
5404
5531
  ['recommendations', cmdRecommendations],
5405
5532
  ['integrations', cmdIntegrations],
@@ -5648,7 +5775,24 @@ const handlers = new Map([
5648
5775
  ['artifact', async (args) => {
5649
5776
  const sub = args[0];
5650
5777
  if (sub === 'validate') return runArtifactValidateCli(args.slice(1));
5651
- errorln(`Unknown artifact subcommand: ${sub || '(none)'}. Available: validate`);
5778
+ if (sub === 'workflow') {
5779
+ const rest = args.slice(1);
5780
+ const request = rest.filter((arg) => !arg.startsWith('--')).join(' ');
5781
+ const read = (name) => rest.find((arg) => arg.startsWith(`${name}=`))?.slice(name.length + 1);
5782
+ const { runArtifactWorkflow } = await import('../lib/artifact-workflow.mjs');
5783
+ const result = runArtifactWorkflow({
5784
+ input: request,
5785
+ artifactType: read('--type'),
5786
+ filePath: read('--file'),
5787
+ format: read('--to'),
5788
+ outputPath: read('--output'),
5789
+ branding: rest.includes('--plain') ? 'plain' : undefined,
5790
+ approvalMode: rest.includes('--apply') ? 'allow-durable-write' : (read('--approval-mode') || 'proposal-only'),
5791
+ }, { cwd: process.cwd(), rootDir: ROOT_DIR });
5792
+ println(JSON.stringify(result, null, 2));
5793
+ return;
5794
+ }
5795
+ errorln(`Unknown artifact subcommand: ${sub || '(none)'}. Available: validate, workflow`);
5652
5796
  process.exit(1);
5653
5797
  }],
5654
5798
  ['doc', async (args) => {
@@ -6383,7 +6527,7 @@ const handlers = new Map([
6383
6527
 
6384
6528
  println('');
6385
6529
  println(`Scaffolding a new profile draft. Three questions, then a preview.`);
6386
- println(`Background: docs/concepts/persona-research.md and docs/concepts/profile-lifecycle.md`);
6530
+ println(`Background: docs/guides/concepts/persona-research.md and docs/guides/concepts/profile-lifecycle.md`);
6387
6531
  println('');
6388
6532
  if (!displayName) displayName = await ask('Display name?', id);
6389
6533
  if (seedDepartments.length === 0) {
@@ -6442,7 +6586,7 @@ const handlers = new Map([
6442
6586
  println('');
6443
6587
  println(`✓ draft created at ${pathMod.relative(process.cwd(), result.dir)}`);
6444
6588
  println('');
6445
- println(`Next steps (in order, per docs/concepts/profile-lifecycle.md):`);
6589
+ println(`Next steps (in order, per docs/guides/concepts/profile-lifecycle.md):`);
6446
6590
  println(` 1. Discover (cx-ux-researcher): fill personas/<role>.md from interviews + primary sources.`);
6447
6591
  println(` 2. Frame (cx-product-manager): fill departments/<dept>.md charters and intake taxonomy.`);
6448
6592
  println(` 3. Architect (cx-architect): reconcile role reuse vs new; populate departments[] in profile.json.`);
@@ -11,4 +11,4 @@ ADR format:
11
11
  - FILES AFFECTED: paths future contributors should inspect
12
12
  - FOLLOW-UP: docs, tests, migrations, or risks to track
13
13
 
14
- Save to `docs/adr/ADR-{NNN}-{slug}.md` and `.cx/decisions/` if those directories exist.
14
+ Save to `docs/decisions/adr/ADR-{NNN}-{slug}.md` and `.cx/decisions/` if those directories exist.
@@ -11,4 +11,4 @@ Structure:
11
11
  - ROLLBACK: how to undo if something goes wrong
12
12
  - ESCALATION: who to contact and when
13
13
 
14
- Save to `docs/runbooks/{service}-{operation}.md` if the directory exists.
14
+ Save to `docs/operations/runbooks/{service}-{operation}.md` if the directory exists.
@@ -78,7 +78,7 @@ Required internal coverage:
78
78
  - Name the expected behavior in repo terms: branch confirmation, approval boundary, routing, verification, blocker surfacing, anti-pattern avoidance.
79
79
  - For bad examples, show the failure plainly and explain why it violates the surface contract.
80
80
 
81
- See `docs/concepts/prompt-surfaces.md` for the canonical public-vs-internal taxonomy.
81
+ See `docs/guides/concepts/prompt-surfaces.md` for the canonical public-vs-internal taxonomy.
82
82
 
83
83
  ## Scope
84
84
 
@@ -39,4 +39,4 @@ Edit sources, then re-run the generator. Outputs land in `.tmp/distribution-exam
39
39
  node bin/construct export examples/distribution/sources/adr.md --to=pdf --figures
40
40
  ```
41
41
 
42
- See [branding.md](../../docs/reference/branding.md) and [diagram-and-demo.md](../../docs/cookbook/diagram-and-demo.md).
42
+ See [branding.md](../../docs/guides/reference/branding.md) and [diagram-and-demo.md](../../docs/guides/cookbook/diagram-and-demo.md).