@geraldmaron/construct 1.2.1 → 1.2.3

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 (208) hide show
  1. package/README.md +6 -6
  2. package/apps/chat/engine/ai-sdk-agent.mjs +183 -0
  3. package/apps/chat/engine/loop-driver.mjs +211 -0
  4. package/apps/chat/engine/models.mjs +122 -0
  5. package/apps/chat/engine/provider-adapters.mjs +171 -0
  6. package/apps/chat/engine/tools/permission.mjs +54 -0
  7. package/apps/chat/engine/tools/primitives.mjs +180 -0
  8. package/apps/chat/engine/tools/registry.mjs +122 -0
  9. package/apps/chat/engine/turn-controls.mjs +70 -0
  10. package/bin/construct +93 -79
  11. package/commands/plan/decide.md +1 -1
  12. package/commands/remember/runbook.md +1 -1
  13. package/examples/README.md +1 -1
  14. package/examples/distribution/README.md +1 -1
  15. package/examples/distribution/sources/adr.md +2 -2
  16. package/examples/distribution/sources/deck-one-pager.md +1 -1
  17. package/examples/distribution/sources/prd-platform.md +1 -1
  18. package/examples/distribution/sources/research-brief.md +2 -2
  19. package/examples/distribution/sources/rfc-platform.md +1 -1
  20. package/examples/distribution/sources/runbook.md +2 -2
  21. package/examples/distribution/sources/strategy.md +1 -1
  22. package/lib/agent-instructions/inject.mjs +1 -1
  23. package/lib/artifact-type-from-path.mjs +9 -5
  24. package/lib/audit-specialists.mjs +1 -1
  25. package/lib/audit-trail.mjs +3 -3
  26. package/lib/auto-docs.mjs +10 -10
  27. package/lib/bootstrap/built-ins.mjs +3 -3
  28. package/lib/boundary.mjs +3 -2
  29. package/lib/brand-prose.mjs +1 -1
  30. package/lib/cache-governor.js +4 -4
  31. package/lib/certification/document-io-fixtures.mjs +1 -1
  32. package/lib/chat/config.mjs +2 -2
  33. package/lib/chat/desktop-binary.mjs +5 -3
  34. package/lib/cli-commands.mjs +4 -6
  35. package/lib/cli-service-inventory.mjs +1 -1
  36. package/lib/comment-lint.mjs +9 -5
  37. package/lib/config/alias.mjs +3 -2
  38. package/lib/config/intake-policy.mjs +8 -78
  39. package/lib/config/legacy-config-migration.mjs +59 -0
  40. package/lib/config/project-config.mjs +95 -5
  41. package/lib/config/schema.mjs +9 -19
  42. package/lib/config/xdg.mjs +63 -0
  43. package/lib/contracts/violation-log.mjs +2 -2
  44. package/lib/cost-ledger.mjs +4 -3
  45. package/lib/cost.mjs +3 -2
  46. package/lib/decisions/registry.mjs +3 -3
  47. package/lib/distill.mjs +2 -2
  48. package/lib/docs-verify.mjs +2 -0
  49. package/lib/doctor/audit.mjs +4 -3
  50. package/lib/doctor/cli.mjs +0 -1
  51. package/lib/doctor/index.mjs +4 -4
  52. package/lib/doctor/report.mjs +3 -3
  53. package/lib/doctor/watchers/bd-watch.mjs +5 -5
  54. package/lib/doctor/watchers/cost.mjs +4 -4
  55. package/lib/doctor/watchers/disk.mjs +4 -3
  56. package/lib/doctor/watchers/service-health.mjs +5 -5
  57. package/lib/document-export.mjs +1 -1
  58. package/lib/document-extract.mjs +3 -3
  59. package/lib/document-ingest.mjs +13 -3
  60. package/lib/efficiency.mjs +2 -1
  61. package/lib/embed/approval-queue.mjs +3 -2
  62. package/lib/embed/cli.mjs +7 -5
  63. package/lib/embed/config.mjs +6 -5
  64. package/lib/embed/daemon.mjs +6 -5
  65. package/lib/embed/inbox-live-watcher.mjs +1 -1
  66. package/lib/embed/inbox.mjs +52 -37
  67. package/lib/embed/intake-metrics.mjs +3 -2
  68. package/lib/embed/reconcile.mjs +1 -1
  69. package/lib/embed/semantic.mjs +3 -2
  70. package/lib/embed/supervision.mjs +4 -2
  71. package/lib/engine/registry.mjs +3 -3
  72. package/lib/env-config.mjs +34 -12
  73. package/lib/features.mjs +4 -3
  74. package/lib/health-check.mjs +7 -6
  75. package/lib/hook-health.mjs +5 -4
  76. package/lib/hooks/_lib/log.mjs +4 -3
  77. package/lib/hooks/_lib/output-mode.mjs +2 -1
  78. package/lib/hooks/agent-tracker.mjs +3 -4
  79. package/lib/hooks/audit-reads.mjs +2 -2
  80. package/lib/hooks/audit-trail.mjs +2 -2
  81. package/lib/hooks/bash-output-logger.mjs +3 -3
  82. package/lib/hooks/brand-prose-lint.mjs +1 -1
  83. package/lib/hooks/ci-status-check.mjs +3 -2
  84. package/lib/hooks/context-watch.mjs +2 -2
  85. package/lib/hooks/context-window-recovery.mjs +5 -5
  86. package/lib/hooks/dep-audit.mjs +2 -2
  87. package/lib/hooks/doc-coupling-check.mjs +3 -2
  88. package/lib/hooks/edit-accumulator.mjs +3 -2
  89. package/lib/hooks/edit-guard.mjs +3 -3
  90. package/lib/hooks/guard-bash.mjs +2 -2
  91. package/lib/hooks/model-fallback.mjs +2 -1
  92. package/lib/hooks/policy-engine.mjs +4 -3
  93. package/lib/hooks/pre-compact.mjs +7 -7
  94. package/lib/hooks/readme-age-check.mjs +2 -2
  95. package/lib/hooks/session-optimize.mjs +4 -3
  96. package/lib/hooks/session-start.mjs +6 -5
  97. package/lib/hooks/stop-notify.mjs +9 -8
  98. package/lib/hooks/stop-typecheck.mjs +4 -3
  99. package/lib/hooks/test-watch.mjs +2 -2
  100. package/lib/host-disposition.mjs +1 -1
  101. package/lib/ingest/provider-extract.mjs +2 -1
  102. package/lib/init/detect-existing-structure.mjs +2 -2
  103. package/lib/init/doc-lanes.mjs +3 -10
  104. package/lib/init-docs.mjs +12 -54
  105. package/lib/init-unified.mjs +35 -81
  106. package/lib/init.mjs +7 -5
  107. package/lib/install/desktop-binary-download.mjs +5 -2
  108. package/lib/intake/daemon.mjs +31 -8
  109. package/lib/intake/intake-config.mjs +5 -32
  110. package/lib/integrations/intake-integrations.mjs +8 -7
  111. package/lib/knowledge/rag.mjs +2 -2
  112. package/lib/knowledge/search.mjs +15 -15
  113. package/lib/maintenance/cleanup.mjs +14 -10
  114. package/lib/mcp/server.mjs +1 -1
  115. package/lib/mcp/tools/project.mjs +2 -1
  116. package/lib/model-cheapest-provider.mjs +4 -3
  117. package/lib/model-pricing.mjs +3 -2
  118. package/lib/model-router.mjs +4 -3
  119. package/lib/models/catalog.mjs +2 -1
  120. package/lib/models/execution-capability-profile.mjs +2 -1
  121. package/lib/models/provider-poll.mjs +2 -1
  122. package/lib/ollama-manager.mjs +0 -1
  123. package/lib/op-log.mjs +2 -1
  124. package/lib/opencode-runtime-plugin.mjs +3 -2
  125. package/lib/oracle/artifact-gate.mjs +3 -0
  126. package/lib/oracle/cli.mjs +2 -1
  127. package/lib/oracle/execute.mjs +3 -2
  128. package/lib/oracle/index.mjs +2 -1
  129. package/lib/oracle/read-model.mjs +3 -2
  130. package/lib/performance/generate.mjs +4 -3
  131. package/lib/platforms/capabilities.mjs +1 -1
  132. package/lib/plugin-registry.mjs +2 -1
  133. package/lib/profiles/lifecycle.mjs +3 -3
  134. package/lib/project-profile.mjs +2 -1
  135. package/lib/project-root.mjs +9 -7
  136. package/lib/provider-capabilities.js +4 -3
  137. package/lib/providers/auth-manager.mjs +2 -1
  138. package/lib/providers/copilot-auth.mjs +3 -2
  139. package/lib/providers/creds.mjs +3 -2
  140. package/lib/providers/registry.mjs +3 -3
  141. package/lib/providers/secret-resolver.mjs +3 -2
  142. package/lib/read-tracker-store.mjs +2 -1
  143. package/lib/reconcile/mcp-entry-reconcile.mjs +2 -2
  144. package/lib/reflect.mjs +2 -1
  145. package/lib/registry/generate-docs.mjs +2 -2
  146. package/lib/roles/approval-surface.mjs +5 -4
  147. package/lib/roles/event-bus.mjs +0 -1
  148. package/lib/roles/gateway.mjs +6 -2
  149. package/lib/rules-delivery.mjs +1 -1
  150. package/lib/runtime-env.mjs +1 -1
  151. package/lib/runtime-pressure.mjs +5 -3
  152. package/lib/sandbox.mjs +3 -2
  153. package/lib/scheduler/solo.mjs +6 -4
  154. package/lib/server/auth.mjs +4 -3
  155. package/lib/server/index.mjs +24 -26
  156. package/lib/server/insights.mjs +5 -4
  157. package/lib/server/webhook.mjs +2 -1
  158. package/lib/service-manager.mjs +5 -3
  159. package/lib/setup.mjs +21 -11
  160. package/lib/status.mjs +6 -5
  161. package/lib/storage/embeddings-local.mjs +3 -2
  162. package/lib/storage/sync.mjs +2 -2
  163. package/lib/telemetry/client.mjs +0 -1
  164. package/lib/telemetry/intent-verifications.mjs +6 -6
  165. package/lib/telemetry/model-pricing-catalog.mjs +4 -3
  166. package/lib/telemetry/rule-calls.mjs +3 -3
  167. package/lib/telemetry/skill-calls.mjs +3 -3
  168. package/lib/template-registry.mjs +1 -0
  169. package/lib/templates/visual-requirements.mjs +1 -1
  170. package/lib/test-corpus-inventory.mjs +1 -1
  171. package/lib/uninstall/uninstall.mjs +12 -10
  172. package/package.json +4 -3
  173. package/platforms/claude/settings.template.json +43 -43
  174. package/rules/common/no-fabrication.md +1 -1
  175. package/rules/common/release-gates.md +1 -1
  176. package/rules/common/research.md +1 -1
  177. package/rules/common/review-before-change.md +1 -1
  178. package/scripts/sync-specialists.mjs +11 -8
  179. package/skills/docs/adr-workflow.md +3 -3
  180. package/skills/docs/init-docs.md +9 -9
  181. package/skills/docs/prd-workflow.md +5 -5
  182. package/skills/docs/product-intelligence-review.md +1 -1
  183. package/skills/docs/product-intelligence-workflow.md +2 -2
  184. package/skills/docs/product-signal-workflow.md +1 -1
  185. package/skills/docs/runbook-workflow.md +4 -4
  186. package/skills/exploration/unknown-codebase-onboarding.md +1 -1
  187. package/skills/operating/orchestration-reference.md +1 -1
  188. package/skills/routing.md +3 -3
  189. package/specialists/prompts/cx-architect.md +1 -1
  190. package/specialists/prompts/cx-docs-keeper.md +1 -1
  191. package/specialists/prompts/cx-researcher.md +1 -1
  192. package/specialists/prompts/cx-sre.md +1 -1
  193. package/specialists/role-manifests.json +6 -6
  194. package/templates/docs/README.md +125 -0
  195. package/templates/docs/construct_guide.md +3 -4
  196. package/templates/docs/persona-artifact.md +1 -1
  197. package/templates/docs/prds/README.md +25 -0
  198. package/templates/docs/prds/templates/_template.md +206 -0
  199. package/templates/docs/prds/templates/meta-prd.template.md +177 -0
  200. package/templates/docs/prds/templates/prd-business.template.md +61 -0
  201. package/templates/docs/prds/templates/prd-platform.template.md +81 -0
  202. package/templates/docs/prds/templates/prfaq.template.md +38 -0
  203. package/templates/docs/rfcs/README.md +22 -0
  204. package/templates/docs/rfcs/templates/_template.md +58 -0
  205. package/templates/docs/rfcs/templates/rfc-platform.template.md +72 -0
  206. package/templates/homebrew/construct.rb +1 -1
  207. package/templates/workflows/new-feature.yml +8 -8
  208. package/lib/intake/legacy-paths.mjs +0 -5
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');
@@ -592,7 +593,7 @@ async function cmdDoctor() {
592
593
  // minute at 50MB, but a freshly-upgraded install with a 34GB legacy
593
594
  // log shouldn't wait 60s. --fix-embed-log forces rotation now.
594
595
 
595
- const embedLogPath = path.join(userHome, '.cx', 'runtime', 'embed-daemon.log');
596
+ const embedLogPath = path.join(doctorRoot(userHome), 'runtime', 'embed-daemon.log');
596
597
  if (fs.existsSync(embedLogPath)) {
597
598
  const sizeMb = fs.statSync(embedLogPath).size / 1024 / 1024;
598
599
  const FORCE_THRESHOLD_MB = Number(process.env.CONSTRUCT_DOCTOR_EMBED_LOG_FORCE_MB) || 500;
@@ -627,11 +628,11 @@ async function cmdDoctor() {
627
628
  'intent-verifications.jsonl',
628
629
  ];
629
630
  const legacyState = projectScopedNames
630
- .map((name) => ({ name, path: path.join(userHome, '.cx', name) }))
631
+ .map((name) => ({ name, path: path.join(doctorRoot(userHome), name) }))
631
632
  .filter((p) => fs.existsSync(p.path) && fs.statSync(p.path).size > 0);
632
633
 
633
634
  if (legacyState.length > 0 && wantsFixMigrateState) {
634
- const archiveDir = path.join(userHome, '.cx', 'legacy');
635
+ const archiveDir = path.join(doctorRoot(userHome), 'legacy');
635
636
  fs.mkdirSync(archiveDir, { recursive: true });
636
637
  const ts = new Date().toISOString().replace(/[:.]/g, '-');
637
638
  const moved = [];
@@ -652,6 +653,21 @@ async function cmdDoctor() {
652
653
  );
653
654
  }
654
655
 
656
+ // Mirror model tier overrides stranded in the pre-XDG legacy config forward
657
+ // before resolving tiers, so an install upgraded across the XDG move reports
658
+ // configured without a manual copy. Applied to process.env too so the check
659
+ // below sees the just-migrated values in the same run.
660
+ try {
661
+ const { migrateLegacyModelConfig } = await import('../lib/config/legacy-config-migration.mjs');
662
+ const migration = migrateLegacyModelConfig({ homeDir: HOME });
663
+ if (migration.performed) {
664
+ Object.assign(process.env, migration.migrated);
665
+ println(` fix: migrated ${Object.keys(migration.migrated).join(', ')} from ${migration.legacyPath} → ${migration.xdgPath}`);
666
+ }
667
+ } catch (err) {
668
+ add(`Models — legacy config migration failed: ${err.message}`, false, true);
669
+ }
670
+
655
671
  // Tier model selection. Construct ships with no default — at least
656
672
  // one tier must be configured (registry.json primary OR CX_MODEL_*
657
673
  // env override) before any LLM-backed workflow can run.
@@ -739,13 +755,13 @@ async function cmdDoctor() {
739
755
  const vscodeSettingsOk = vscodeSettingsPaths.some((candidate) => fs.existsSync(candidate));
740
756
  if (vscodeSettingsOk) add('VS Code settings file', true, true);
741
757
  }
742
- add('User config ready', fs.existsSync(getUserEnvPath(HOME)) || fs.existsSync(path.join(HOME, '.construct')), true);
758
+ add('User config ready', fs.existsSync(getUserEnvPath(HOME)) || fs.existsSync(configDir(HOME)), true);
743
759
  add('skills/ directory', fs.existsSync(path.join(ROOT_DIR, 'skills')));
744
760
  add('rules/common/', fs.existsSync(path.join(ROOT_DIR, 'rules', 'common')));
745
761
  add('specialists/prompts/ directory', fs.existsSync(path.join(ROOT_DIR, 'specialists', 'prompts')));
746
762
  add('Hybrid storage env keys documented', fs.existsSync(path.join(ROOT_DIR, '.env.example')));
747
- fs.mkdirSync(path.join(HOME, '.cx', 'performance-reviews'), { recursive: true });
748
- add('~/.cx/performance-reviews/ ready', true);
763
+ fs.mkdirSync(path.join(doctorRoot(), 'performance-reviews'), { recursive: true });
764
+ add('performance-reviews/ ready', true);
749
765
 
750
766
  // construct.config.json — optional today (config is permitted to be absent;
751
767
  // env vars + defaults still work). The check enforces that when the file
@@ -765,33 +781,29 @@ async function cmdDoctor() {
765
781
 
766
782
  // Claude Code Stop-hook installation check. The settings template
767
783
  // wires several Stop hooks (policy-engine / stop-typecheck /
768
- // stop-notify / readme-age-check) that reference
769
- // $HOME/.construct/lib/hooks/*. If that path doesn't resolve, every
770
- // Stop hook fails silently — including stop-notify, which is what
771
- // appends session cost data. Result: the dashboard stops updating
772
- // and the user sees stale data. Surface loudly so the fix is one
773
- // command away.
784
+ // stop-notify / readme-age-check) whose commands sync resolves to the
785
+ // hook lib symlink. If those scripts don't resolve, every Stop hook
786
+ // fails silently — including stop-notify, which is what appends session
787
+ // cost data. Result: the dashboard stops updating and the user sees stale
788
+ // data. Surface loudly so the fix is one command away.
774
789
  try {
775
790
  const settingsPath = path.join(HOME, '.claude', 'settings.json');
776
791
  if (fs.existsSync(settingsPath)) {
777
792
  const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
778
793
  const stopHooks = settings?.hooks?.Stop || [];
779
- const installRoot = path.join(HOME, '.construct', 'lib', 'hooks');
780
- const installed = fs.existsSync(installRoot);
794
+ const installRoot = path.join(configDir(HOME), 'lib', 'hooks');
781
795
  const referenced = [];
782
796
  for (const group of stopHooks) {
783
797
  for (const h of (group?.hooks || [])) {
784
798
  const cmd = String(h?.command || '');
785
- const m = cmd.match(/\.construct\/lib\/hooks\/([a-zA-Z0-9_-]+\.mjs)/);
799
+ const m = cmd.match(/(?:^|["\s])(\/[^"\s]*\/lib\/hooks\/[a-zA-Z0-9_-]+\.mjs)/);
786
800
  if (m) referenced.push(m[1]);
787
801
  }
788
802
  }
789
- const missing = installed
790
- ? referenced.filter((f) => !fs.existsSync(path.join(installRoot, f)))
791
- : referenced;
803
+ const missing = referenced.filter((scriptPath) => !fs.existsSync(scriptPath));
792
804
  if (referenced.length > 0 && missing.length > 0) {
793
805
  add(
794
- `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`,
806
+ `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$/, '')}).`,
795
807
  false,
796
808
  false,
797
809
  );
@@ -1073,7 +1085,7 @@ async function cmdDoctor() {
1073
1085
  }
1074
1086
 
1075
1087
  try {
1076
- const failuresPath = path.join(HOME, '.cx', 'hook-failures.jsonl');
1088
+ const failuresPath = path.join(doctorRoot(), 'hook-failures.jsonl');
1077
1089
  if (fs.existsSync(failuresPath)) {
1078
1090
  const lines = fs.readFileSync(failuresPath, 'utf8').trim().split('\n').filter(Boolean);
1079
1091
  const cutoff = Date.now() - 24 * 60 * 60 * 1000;
@@ -1519,7 +1531,7 @@ async function cmdMigrate(args) {
1519
1531
  }
1520
1532
 
1521
1533
  async function cmdReview(args) {
1522
- fs.mkdirSync(path.join(HOME, '.cx', 'performance-reviews'), { recursive: true });
1534
+ fs.mkdirSync(path.join(doctorRoot(), 'performance-reviews'), { recursive: true });
1523
1535
  const sub = args[0];
1524
1536
 
1525
1537
  if (!sub || sub === 'run') {
@@ -2018,7 +2030,7 @@ async function _cmdPricing(args) {
2018
2030
  const f = describePricingCatalogFreshness();
2019
2031
  if (!f.present) { info(f.message); return; }
2020
2032
  try {
2021
- const cached = JSON.parse(fs.readFileSync(path.join(HOME, '.cx', 'pricing-cache.json'), 'utf8'));
2033
+ const cached = JSON.parse(fs.readFileSync(path.join(doctorRoot(), 'pricing-cache.json'), 'utf8'));
2022
2034
  const divergences = checkPricingDivergence(cached.models || []);
2023
2035
  if (divergences.length === 0) { info('No pricing divergences (>25%) between LiteLLM and the static fallback.'); return; }
2024
2036
  println(`⚠ ${divergences.length} divergence(s):`);
@@ -2116,15 +2128,13 @@ async function printIntakeHelp() {
2116
2128
  println(' construct intake metrics Show pipeline volume, velocity, and processing stats.');
2117
2129
  println(' construct intake integrate <id> <system> Push intake to external system (github, jira, confluence).');
2118
2130
  println(' construct intake integrate <id> <system> --create-issue Create an issue from the intake packet.');
2119
- println(' construct intake config Show current watch dirs + scan depth.');
2131
+ println(' construct intake config Show the inbox/ drop zone + scan depth.');
2120
2132
  println(' construct intake config set --depth=N [--add-dir=PATH] [--remove-dir=PATH]');
2133
+ println(' Update intake config. --depth caps at the hard limit.');
2121
2134
  println(' construct intake extraction-warnings [--since=7d] [--kind=table] [--limit=50]');
2122
2135
  println(' Summarize ~/.cx/intake/extraction-warnings.jsonl (dropped content log).');
2123
2136
  println(' construct intake needs-asr list List audio/video files awaiting ASR transcription.');
2124
2137
  println(' construct intake needs-asr show <id> Show a specific ASR-pending packet.');
2125
- println(' Update intake config. --depth caps at the hard limit.');
2126
- println(' construct intake config set --include-project-inbox=on|off');
2127
- println(' construct intake config set --include-docs-intake=on|off');
2128
2138
  }
2129
2139
 
2130
2140
  function parseKvFlags(args, keys) {
@@ -2618,7 +2628,6 @@ async function cmdIntakeReroute(args, cwd) {
2618
2628
 
2619
2629
  async function cmdIntakeExtractionWarnings(args) {
2620
2630
  const { createReadStream, existsSync } = await import('node:fs');
2621
- const { homedir } = await import('node:os');
2622
2631
  const { join } = await import('node:path');
2623
2632
  const { createInterface } = await import('node:readline');
2624
2633
 
@@ -2636,7 +2645,7 @@ async function cmdIntakeExtractionWarnings(args) {
2636
2645
  }
2637
2646
  }
2638
2647
 
2639
- const logPath = join(homedir(), '.cx', 'intake', 'extraction-warnings.jsonl');
2648
+ const logPath = join(doctorRoot(), 'intake', 'extraction-warnings.jsonl');
2640
2649
  if (!existsSync(logPath)) {
2641
2650
  println('No extraction warnings recorded yet.');
2642
2651
  return;
@@ -2687,11 +2696,10 @@ async function cmdIntakeExtractionWarnings(args) {
2687
2696
 
2688
2697
  async function cmdIntakeNeedsAsr(args) {
2689
2698
  const { existsSync, readdirSync, readFileSync } = await import('node:fs');
2690
- const { homedir } = await import('node:os');
2691
2699
  const { join } = await import('node:path');
2692
2700
 
2693
2701
  const sub = args[0] || 'list';
2694
- const asrDir = join(homedir(), '.cx', 'intake', 'needs-asr');
2702
+ const asrDir = join(doctorRoot(), 'intake', 'needs-asr');
2695
2703
 
2696
2704
  if (sub === 'list') {
2697
2705
  if (!existsSync(asrDir)) { println('No audio/video files awaiting ASR.'); return; }
@@ -2856,33 +2864,23 @@ async function cmdIntakeConfig(args) {
2856
2864
  const cwd = process.cwd();
2857
2865
 
2858
2866
  const action = args[0];
2859
- if (action === 'migrate') {
2860
- const { migrateLegacyIntakeConfig } = await import('../lib/intake/intake-config.mjs');
2861
- const result = migrateLegacyIntakeConfig(cwd);
2862
- if (!result.migrated) {
2863
- info(result.reason ?? 'nothing to migrate');
2864
- return;
2865
- }
2866
- info('Migrated .cx/intake-config.json → construct.config.json intakePolicy');
2867
- println(JSON.stringify(result.policy, null, 2));
2868
- return;
2869
- }
2867
+
2868
+ // Single-zone model (ADR-0045 §C): the canonical `inbox/` is always watched.
2869
+ // Only maxDepth and additionalDirs are configurable.
2870
2870
 
2871
2871
  if (!action || action === 'show') {
2872
2872
  const cfg = loadIntakeConfig(cwd, process.env);
2873
2873
  const desc = describeIntakeDepth(cfg.maxDepth);
2874
2874
  println(`Intake config (root: ${cwd})`);
2875
2875
  println('');
2876
- println(` includeProjectInbox: ${cfg.includeProjectInbox ? 'on' : 'off'} (.cx/inbox)`);
2877
- println(` includeRootInbox: ${(cfg.includeRootInbox ?? cfg.includeArchetypeInbox) ? 'on' : 'off'} (inbox/)`);
2878
- println(` includeDocsIntake: ${cfg.includeDocsIntake ? 'on' : 'off'} (docs/intake)`);
2876
+ println(' drop zone: inbox/ (always watched)');
2879
2877
  println(` maxDepth: ${cfg.maxDepth} — ${desc.label}`);
2880
2878
  println(` ${desc.detail}`);
2881
2879
  println(` hardMaxDepth: ${INTAKE_HARD_MAX_DEPTH}`);
2882
2880
  if (cfg.parentDirs.length === 0) {
2883
- println(' parentDirs: (none — only the built-in zones are watched)');
2881
+ println(' additionalDirs: (none — only inbox/ is watched)');
2884
2882
  } else {
2885
- println(' parentDirs:');
2883
+ println(' additionalDirs:');
2886
2884
  for (const dir of cfg.parentDirs) println(` - ${dir}`);
2887
2885
  }
2888
2886
  println('');
@@ -2903,15 +2901,6 @@ async function cmdIntakeConfig(args) {
2903
2901
  const depthFlag = tail.find((a) => a.startsWith('--depth='));
2904
2902
  if (depthFlag) patch.maxDepth = Number(depthFlag.split('=')[1]);
2905
2903
 
2906
- const projectFlag = tail.find((a) => a.startsWith('--include-project-inbox='));
2907
- if (projectFlag) patch.includeProjectInbox = /^(on|true|1|yes)$/i.test(projectFlag.split('=')[1] || '');
2908
-
2909
- const docsFlag = tail.find((a) => a.startsWith('--include-docs-intake='));
2910
- if (docsFlag) patch.includeDocsIntake = /^(on|true|1|yes)$/i.test(docsFlag.split('=')[1] || '');
2911
-
2912
- const rootFlag = tail.find((a) => a.startsWith('--include-root-inbox='));
2913
- if (rootFlag) patch.includeRootInbox = /^(on|true|1|yes)$/i.test(rootFlag.split('=')[1] || '');
2914
-
2915
2904
  const addFlags = tail.filter((a) => a.startsWith('--add-dir='));
2916
2905
  const removeFlags = tail.filter((a) => a.startsWith('--remove-dir='));
2917
2906
 
@@ -2924,7 +2913,7 @@ async function cmdIntakeConfig(args) {
2924
2913
  }
2925
2914
 
2926
2915
  if (Object.keys(patch).length === 0) {
2927
- errorln('Nothing to change. Pass --depth=N, --add-dir=PATH, --remove-dir=PATH, --include-project-inbox=on|off, or --include-docs-intake=on|off.');
2916
+ errorln('Nothing to change. Pass --depth=N, --add-dir=PATH, or --remove-dir=PATH.');
2928
2917
  process.exit(1);
2929
2918
  }
2930
2919
 
@@ -3799,7 +3788,15 @@ async function cmdOrchestrate(args) {
3799
3788
  }
3800
3789
 
3801
3790
  async function cmdModels(args) {
3802
- if (args[0] === 'resolve') {
3791
+ const envPath = getUserEnvPath(HOME);
3792
+ const sub = args[0] && !args[0].startsWith('-') ? args[0] : null;
3793
+ const KNOWN_SUBCOMMANDS = new Set(['list', 'set', 'free', 'reset', 'resolve']);
3794
+ if (sub && !KNOWN_SUBCOMMANDS.has(sub)) {
3795
+ errorln(`Unknown models subcommand: ${sub}. Run \`construct models --help\` for the supported set.`);
3796
+ process.exit(1);
3797
+ }
3798
+
3799
+ if (sub === 'resolve') {
3803
3800
  const flag = (name) => { const i = args.indexOf(name); return i !== -1 ? args[i + 1] : undefined; };
3804
3801
  const { resolveEmbeddedModel } = await import('../lib/embedded-contract/model-resolve.mjs');
3805
3802
  const { wrapContractResult } = await import('../lib/embedded-contract/envelope.mjs');
@@ -3816,21 +3813,37 @@ async function cmdModels(args) {
3816
3813
  println(JSON.stringify(envelope, null, 2));
3817
3814
  return;
3818
3815
  }
3816
+ // Legacy flag forms stay as compatibility aliases for the documented
3817
+ // subcommands. Each emits a one-line deprecation notice on stderr so stdout
3818
+ // stays clean while operators migrate to `models set|reset|free`.
3819
3819
  const tier = args.find((arg) => arg.startsWith('--tier='))?.split('=')[1] ?? '';
3820
- const setModel = args.find((arg) => arg.startsWith('--set='))?.split('=')[1] ?? '';
3821
- const envPath = getUserEnvPath(HOME);
3822
- if (args.includes('--reset')) {
3820
+ const modelFlag = args.find((arg) => arg.startsWith('--model='))?.split('=')[1];
3821
+ const legacySetFlag = args.find((arg) => arg.startsWith('--set='))?.split('=')[1];
3822
+ const setModel = modelFlag ?? legacySetFlag ?? '';
3823
+
3824
+ if (sub === 'reset' || args.includes('--reset')) {
3825
+ if (sub !== 'reset') errorln('Note: `construct models --reset` is a deprecated alias for `construct models reset`.');
3823
3826
  resetEnv(envPath);
3824
- println('Removed CX_MODEL_* overrides from ~/.construct/config.env.');
3827
+ println(`Removed CX_MODEL_* overrides from ${envPath}.`);
3825
3828
  await cmdSync([]);
3826
3829
  return;
3827
3830
  }
3828
- if (tier && setModel) {
3831
+ if (sub === 'set' || (sub === null && tier && setModel)) {
3832
+ if (!tier || !setModel) {
3833
+ errorln('Usage: construct models set --tier=<reasoning|standard|fast> --model=<provider/model-id>');
3834
+ process.exit(1);
3835
+ }
3836
+ if (modelFlag === undefined && legacySetFlag !== undefined) {
3837
+ errorln('Note: `--set=<model>` is a deprecated alias for `--model=<model>`.');
3838
+ }
3839
+ if (sub === null) {
3840
+ errorln('Note: `construct models --tier=… --set=…` is a deprecated alias for `construct models set --tier=… --model=…`.');
3841
+ }
3829
3842
  const registry = JSON.parse(fs.readFileSync(path.join(ROOT_DIR, 'specialists', 'registry.json'), 'utf8'));
3830
3843
  const preferFree = args.includes('--prefer-free');
3831
3844
  const preferFreeSameFamily = args.includes('--prefer-free-same-family');
3832
3845
  const inferred = setModelWithTierInference(envPath, tier, setModel, registry.models ?? {}, { preferFree, preferFreeSameFamily });
3833
- println(`Set ${tier} -> ${setModel} in ~/.construct/config.env`);
3846
+ println(`Set ${tier} -> ${setModel} in ${envPath}`);
3834
3847
  if (preferFreeSameFamily) println('Prefer-free-same-family mode: enabled');
3835
3848
  if (preferFree) println('Prefer-free mode: enabled');
3836
3849
  println(`Resolved tier set:`);
@@ -3840,7 +3853,8 @@ async function cmdModels(args) {
3840
3853
  await cmdSync([]);
3841
3854
  return;
3842
3855
  }
3843
- if (args.includes('--poll')) {
3856
+ if (sub === 'free' || args.includes('--poll')) {
3857
+ if (sub !== 'free') errorln('Note: `construct models --poll` is a deprecated alias for `construct models free`.');
3844
3858
  const apiKey = process.env.OPENROUTER_API_KEY || readOpenRouterApiKeyFromOpenCodeConfig();
3845
3859
  if (!apiKey) {
3846
3860
  errorln('Error: no OpenRouter API key found in OPENROUTER_API_KEY or OpenCode config.');
@@ -3867,7 +3881,7 @@ async function cmdModels(args) {
3867
3881
  if (id) selections[currentTier] = id;
3868
3882
  }
3869
3883
  applyToEnv(envPath, selections);
3870
- println('Written to ~/.construct/config.env. Running construct sync...');
3884
+ println(`Written to ${envPath}. Running construct sync...`);
3871
3885
  await cmdSync([]);
3872
3886
  return;
3873
3887
  }
@@ -3904,12 +3918,12 @@ async function cmdModels(args) {
3904
3918
  for (const [tier, model] of Object.entries(selections)) {
3905
3919
  println(` ${tier.padEnd(11)} ${model}`);
3906
3920
  }
3907
- println('Written to ~/.construct/config.env. Running construct sync...');
3921
+ println(`Written to ${envPath}. Running construct sync...`);
3908
3922
  setCheapestProviderPreference(envPath, true);
3909
3923
  await cmdSync([]);
3910
3924
  return;
3911
3925
  }
3912
- if (args.includes('--list') || args[0] === 'list') {
3926
+ if (args.includes('--list') || sub === 'list') {
3913
3927
  const { getProviderModelCatalog } = await import('../lib/model-router.mjs');
3914
3928
  const { listChatModels } = await import('../apps/chat/engine/models.mjs');
3915
3929
  const catalog = getProviderModelCatalog({ env: process.env, cwd: process.cwd() });
@@ -3939,7 +3953,7 @@ async function cmdModels(args) {
3939
3953
  println('No tier has a model selected. Construct ships with no default.');
3940
3954
  println('Pick one of:');
3941
3955
  println(' • Dashboard → Models page (single source of truth)');
3942
- println(' • construct models --set=<provider/model-id> --tier=reasoning|standard|fast');
3956
+ println(' • construct models set --tier=<reasoning|standard|fast> --model=<provider/model-id>');
3943
3957
  println(' • construct models --apply Poll OpenRouter free catalog and seed all tiers');
3944
3958
  }
3945
3959
  }
@@ -4034,7 +4048,7 @@ async function cmdPluginEngine(args) {
4034
4048
  const [action = 'list', ...rest] = args;
4035
4049
  const isGlobal = rest.includes('--global');
4036
4050
  const targetPath = isGlobal
4037
- ? path.join(HOME, '.construct', 'plugins.json')
4051
+ ? path.join(configDir(HOME), 'plugins.json')
4038
4052
  : path.join(process.cwd(), '.cx', 'plugins.json');
4039
4053
 
4040
4054
  function readPluginsJson() {
@@ -4241,7 +4255,7 @@ async function cmdEvals(args) {
4241
4255
  const report = await evaluateFixture(fixturePath);
4242
4256
 
4243
4257
  try {
4244
- const cacheDir = path.join(HOME, '.cx', 'evals');
4258
+ const cacheDir = path.join(doctorRoot(), 'evals');
4245
4259
  fs.mkdirSync(cacheDir, { recursive: true });
4246
4260
  const ts = new Date().toISOString();
4247
4261
  fs.writeFileSync(
@@ -4438,7 +4452,7 @@ async function cmdDocsCheck(args) {
4438
4452
  println('Commands with no linked how-to in docs/README.md:');
4439
4453
  for (const name of result.uncovered) println(` ✗ construct ${name}`);
4440
4454
  println('');
4441
- println(`Add cookbook recipes under docs/cookbook/ and link them from the cookbook index.`);
4455
+ println(`Add cookbook recipes under docs/guides/cookbook/ and link them from the cookbook index.`);
4442
4456
  process.exitCode = 1;
4443
4457
  }
4444
4458
  }
@@ -4449,7 +4463,7 @@ async function cmdDocsSite(args = []) {
4449
4463
  if (check) {
4450
4464
  const { drift } = checkFumadocsReferenceDrift({ rootDir: ROOT_DIR });
4451
4465
  if (drift.length === 0) {
4452
- ok('docs/reference/ is up to date.');
4466
+ ok('docs/guides/reference/ is up to date.');
4453
4467
  return;
4454
4468
  }
4455
4469
  println('');
@@ -4460,9 +4474,9 @@ async function cmdDocsSite(args = []) {
4460
4474
  }
4461
4475
  const { written } = buildFumadocsReference({ rootDir: ROOT_DIR });
4462
4476
  if (written.length === 0) {
4463
- ok('docs/reference/ already up to date.');
4477
+ ok('docs/guides/reference/ already up to date.');
4464
4478
  } else {
4465
- ok(`docs/reference/ regenerated (${written.length} file${written.length === 1 ? '' : 's'}).`);
4479
+ ok(`docs/guides/reference/ regenerated (${written.length} file${written.length === 1 ? '' : 's'}).`);
4466
4480
  }
4467
4481
  }
4468
4482
 
@@ -4961,7 +4975,7 @@ async function cmdProvider(args) {
4961
4975
  if (!id) { errorln(`Usage: construct provider plugins ${action} <id> [<package>]`); process.exit(1); }
4962
4976
  const isGlobal = args.includes('--global');
4963
4977
  const targetPath = isGlobal
4964
- ? path.join(HOME, '.construct', 'providers.json')
4978
+ ? path.join(configDir(HOME), 'providers.json')
4965
4979
  : path.join(process.cwd(), '.cx', 'providers.json');
4966
4980
  let data = { providers: [] };
4967
4981
  try { data = JSON.parse(fs.readFileSync(targetPath, 'utf8')); } catch { /* fresh */ }
@@ -5486,7 +5500,7 @@ async function cmdTelemetryQuery(args) {
5486
5500
  println(' errors [--agent=<name>] [--since=24h] error span summary');
5487
5501
  println(' trace <traceId> full span tree');
5488
5502
  println('');
5489
- println('For Langfuse/Honeycomb/Grafana Tempo/Datadog, see docs/concepts/observability.md');
5503
+ println('For Langfuse/Honeycomb/Grafana Tempo/Datadog, see docs/guides/concepts/observability.md');
5490
5504
  }
5491
5505
 
5492
5506
  async function cmdHook(args) {
@@ -6553,7 +6567,7 @@ const handlers = new Map([
6553
6567
 
6554
6568
  println('');
6555
6569
  println(`Scaffolding a new profile draft. Three questions, then a preview.`);
6556
- println(`Background: docs/concepts/persona-research.md and docs/concepts/profile-lifecycle.md`);
6570
+ println(`Background: docs/guides/concepts/persona-research.md and docs/guides/concepts/profile-lifecycle.md`);
6557
6571
  println('');
6558
6572
  if (!displayName) displayName = await ask('Display name?', id);
6559
6573
  if (seedDepartments.length === 0) {
@@ -6612,7 +6626,7 @@ const handlers = new Map([
6612
6626
  println('');
6613
6627
  println(`✓ draft created at ${pathMod.relative(process.cwd(), result.dir)}`);
6614
6628
  println('');
6615
- println(`Next steps (in order, per docs/concepts/profile-lifecycle.md):`);
6629
+ println(`Next steps (in order, per docs/guides/concepts/profile-lifecycle.md):`);
6616
6630
  println(` 1. Discover (cx-ux-researcher): fill personas/<role>.md from interviews + primary sources.`);
6617
6631
  println(` 2. Frame (cx-product-manager): fill departments/<dept>.md charters and intake taxonomy.`);
6618
6632
  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).
@@ -51,7 +51,7 @@ tokens -> dash
51
51
 
52
52
  ## Decision
53
53
 
54
- Adopt `lib/brand-tokens.mjs` as the single source of truth for visual brand primitives. Document naming, voice, tone, and profile rebrand in `docs/reference/branding.md`. Enforce retired-font drift with `scripts/audit/03d-brand.mjs` in the audit ratchet.
54
+ Adopt `lib/brand-tokens.mjs` as the single source of truth for visual brand primitives. Document naming, voice, tone, and profile rebrand in `docs/guides/reference/branding.md`. Enforce retired-font drift with `scripts/audit/03d-brand.mjs` in the audit ratchet.
55
55
 
56
56
  ## Rationale
57
57
 
@@ -79,6 +79,6 @@ Two-way door for documentation and audit scope; one-way for shipped PDFs already
79
79
 
80
80
  ## References
81
81
 
82
- - `docs/reference/branding.md`
82
+ - `docs/guides/reference/branding.md`
83
83
  - `lib/brand-tokens.mjs`
84
84
  - https://fonts.google.com/specimen/Space+Grotesk (accessed 2026-06-22)
@@ -60,6 +60,6 @@ One token set across all distributable formats:
60
60
 
61
61
  - Export: `construct export … --to=pptx`
62
62
  - Regenerate: `npm run examples:deck`
63
- - Matrix: [Document I/O reference](/reference/document-io)
63
+ - Matrix: [Document I/O reference](/guides/reference/document-io)
64
64
 
65
65
  Review outputs in `.tmp/distribution-examples/`.
@@ -157,5 +157,5 @@ https://langchain-ai.github.io/langgraph/ (accessed 2026-06-19).
157
157
 
158
158
  - Construct artifact manifest: `specialists/artifact-manifest.json`
159
159
  - PRD workflow skill: `skills/docs/prd-workflow.md`
160
- - Publish cookbook: `docs/cookbook/diagram-and-demo.md`
160
+ - Publish cookbook: `docs/guides/cookbook/diagram-and-demo.md`
161
161
  - Golden D2 sources: `tests/fixtures/publish/diagrams/`
@@ -32,7 +32,7 @@ Searched arXiv and vendor docs (2024–2026), reviewed three internal ADRs on in
32
32
  |---|---|---|---|---|---|---|---|
33
33
  | Multi-agent survey | secondary | B | 3 | 2023-08-01 | https://arxiv.org/abs/2308.08155 | yes | Patterns for specialist routing |
34
34
  | LangGraph overview | primary | A | 4 | 2025-01-15 | https://langchain-ai.github.io/langgraph/ | yes | Stateful agent graphs |
35
- | Construct architecture | primary | A | 5 | 2026-06-01 | docs/concepts/architecture.mdx | yes | Internal contract |
35
+ | Construct architecture | primary | A | 5 | 2026-06-01 | docs/guides/concepts/architecture.mdx | yes | Internal contract |
36
36
 
37
37
  ## Findings
38
38
 
@@ -77,4 +77,4 @@ Ship distribution examples (`npm run examples:distribution`) so evaluators can c
77
77
  ## References
78
78
 
79
79
  - https://arxiv.org/abs/2308.08155 (accessed 2026-06-22)
80
- - `docs/cookbook/diagram-and-demo.md`
80
+ - `docs/guides/cookbook/diagram-and-demo.md`
@@ -78,5 +78,5 @@ sequenceDiagram
78
78
 
79
79
  ## References
80
80
 
81
- - `docs/reference/branding.md`
81
+ - `docs/guides/reference/branding.md`
82
82
  - `scripts/generate-deck-examples.mjs` (prior art)
@@ -99,5 +99,5 @@ Preserve `construct tools detect --json` output and the failing pandoc/typst std
99
99
 
100
100
  ## References
101
101
 
102
- - `docs/reference/document-io.md`
103
- - `docs/cookbook/diagram-and-demo.md`
102
+ - `docs/guides/reference/document-io.md`
103
+ - `docs/guides/cookbook/diagram-and-demo.md`
@@ -26,7 +26,7 @@ Host-native agents optimize for single-session chat. Enterprise platform teams n
26
26
  ## Goals
27
27
 
28
28
  1. **Prove value in five minutes** — `npm run examples:distribution` produces a browsable gallery with figures.
29
- 2. **One brand contract** — `docs/reference/branding.md` + `03d-brand` audit ratchet.
29
+ 2. **One brand contract** — `docs/guides/reference/branding.md` + `03d-brand` audit ratchet.
30
30
  3. **Profile-aware language** — intake queue labels follow active profile rebrand in CLI, session prelude, and dashboard API.
31
31
 
32
32
  ## Non-goals
@@ -71,7 +71,7 @@ export function buildConstructIntegrationBody({ hasBeadsBlock = false, variant =
71
71
  '- **Durable state** lives in `.cx/` (context, knowledge, intake, traces) and Beads.',
72
72
  ' If services are down, resume from `plan.md`, `.cx/context.md`, the latest',
73
73
  ' `.cx/handoffs/` file, Beads, and git.',
74
- '- **Signals**: drop a file into `.cx/inbox/`; `construct intake` classifies and routes it.',
74
+ '- **Signals**: drop a file into `inbox/`; `construct intake` classifies and routes it.',
75
75
  '- **Specialists** (architect, reviewer, security, …) are dispatched by Construct — you',
76
76
  ' do not call them directly.',
77
77
  tracker,
@@ -41,12 +41,16 @@ function inferRfcSubtype(rel, base) {
41
41
  return 'rfc';
42
42
  }
43
43
 
44
+ // Match both this repo's bucketed layout (docs/specs/prd, docs/decisions/adr, …)
45
+ // and the init-lane layout that `construct init` scaffolds downstream (docs/prd,
46
+ // docs/adr, …), so artifact-type inference works in either project shape.
47
+
44
48
  const PREFIX_RULES = [
45
- [/^docs\/prd\//, inferPrdSubtype],
46
- [/^docs\/adr\//, () => 'adr'],
47
- [/^docs\/rfc\//, inferRfcSubtype],
48
- [/^docs\/research\//, () => 'research-brief'],
49
- [/^docs\/runbooks?\//, () => 'runbook'],
49
+ [/^docs\/(?:specs\/)?prd\//, inferPrdSubtype],
50
+ [/^docs\/(?:decisions\/)?adr\//, () => 'adr'],
51
+ [/^docs\/(?:decisions\/)?rfc\//, inferRfcSubtype],
52
+ [/^docs\/(?:notes\/)?research\//, () => 'research-brief'],
53
+ [/^docs\/(?:operations\/)?runbooks?\//, () => 'runbook'],
50
54
  [/^\.cx\/research\//, () => 'research-brief'],
51
55
  ];
52
56
 
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Merges live registry, contracts, skills, prompts, and audit-enrichments into
5
5
  * one machine-readable matrix. Powers `construct audit specialists` and the
6
- * human-readable docs/concepts/specialist-skill-audit.md companion.
6
+ * human-readable docs/guides/concepts/specialist-skill-audit.md companion.
7
7
  */
8
8
 
9
9
  import fs from 'node:fs';
@@ -16,17 +16,17 @@
16
16
  * Corrupted links cannot be re-derived, so a `chain_reset` boundary line seals
17
17
  * the damaged prefix as immutable legacy and re-bases the live chain;
18
18
  * verifyChain validates only from the last boundary forward. See
19
- * docs/concepts/observability.md.
19
+ * docs/guides/concepts/observability.md.
20
20
  */
21
21
  import { readFileSync, existsSync, statSync, mkdirSync } from 'node:fs';
22
22
  import { createHash } from 'node:crypto';
23
23
  import { join, dirname } from 'node:path';
24
- import { homedir } from 'node:os';
25
24
 
26
25
  import { appendBounded, readLastLineAcrossSegments } from './logging/rotate.mjs';
27
26
  import { withFileLockSync } from './storage/file-lock.mjs';
27
+ import { doctorRoot } from './config/xdg.mjs';
28
28
 
29
- const AUDIT_FILE = join(homedir(), '.cx', 'audit-trail.jsonl');
29
+ const AUDIT_FILE = join(doctorRoot(), 'audit-trail.jsonl');
30
30
 
31
31
  function sha256(input) {
32
32
  return createHash('sha256').update(input).digest('hex');