@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
@@ -15,6 +15,7 @@ import path from 'node:path';
15
15
  import { spawnSync } from 'node:child_process';
16
16
  import { readCostLog, summarizeCostData } from '../cost.mjs';
17
17
  import { getRebrand } from '../profiles/rebrand.mjs';
18
+ import { configDir, doctorRoot } from '../config/xdg.mjs';
18
19
 
19
20
  const TELEMETRY_TIMEOUT_MS = 3500;
20
21
 
@@ -82,7 +83,7 @@ async function fetchTelemetrySummary(env, { limit = 100 } = {}) {
82
83
  // Fallback: read from local .cx/traces/ when Telemetry is unreachable or not configured.
83
84
  // The daemon always writes heartbeat, lifecycle, and probe events to local JSONL files.
84
85
  try {
85
- const traceDir = path.join(os.homedir(), '.cx', 'traces');
86
+ const traceDir = path.join(doctorRoot(), 'traces');
86
87
  if (!fs.existsSync(traceDir)) return { state: 'empty', message: 'No local traces. Start the daemon with `construct dev`.' };
87
88
  const files = fs.readdirSync(traceDir).filter(f => f.endsWith('.jsonl')).sort().reverse();
88
89
  if (!files.length) return { state: 'empty', message: 'No local trace files.' };
@@ -412,7 +413,7 @@ function resolveModelCredential(primaryKey, altKey) {
412
413
  } catch { /* not available */ }
413
414
  }
414
415
 
415
- const paths = [path.join(homeDir, '.construct', 'config.env'), path.join(homeDir, '.env')];
416
+ const paths = [path.join(configDir(homeDir), 'config.env'), path.join(homeDir, '.env')];
416
417
  for (const p of paths) {
417
418
  try {
418
419
  if (!fs.existsSync(p)) continue;
@@ -619,7 +620,7 @@ async function summarizeTelemetryLatency(env, { days = 7, limit = 50 } = {}) {
619
620
 
620
621
  function summarizeRetrievalEval(homeDir = os.homedir()) {
621
622
  try {
622
- const cachePath = path.join(homeDir, '.cx', 'evals', 'retrieval-latest.json');
623
+ const cachePath = path.join(doctorRoot(homeDir), 'evals', 'retrieval-latest.json');
623
624
  if (!fs.existsSync(cachePath)) {
624
625
  return {
625
626
  state: 'empty',
@@ -630,7 +631,7 @@ function summarizeRetrievalEval(homeDir = os.homedir()) {
630
631
  const r = cached.report?.summary || cached.report || {};
631
632
  let history = [];
632
633
  try {
633
- const historyPath = path.join(homeDir, '.cx', 'evals', 'retrieval-history.jsonl');
634
+ const historyPath = path.join(doctorRoot(homeDir), 'evals', 'retrieval-history.jsonl');
634
635
  if (fs.existsSync(historyPath)) {
635
636
  const lines = fs.readFileSync(historyPath, 'utf8').split('\n').filter(Boolean);
636
637
  history = lines.slice(-30).map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
@@ -38,6 +38,7 @@ import { existsSync, readFileSync } from 'fs';
38
38
  import { join } from 'path';
39
39
  import { fileURLToPath } from 'url';
40
40
  import { homedir as osHomedir } from 'os';
41
+ import { configDir } from '../config/xdg.mjs';
41
42
 
42
43
  const __dirname = fileURLToPath(new URL('.', import.meta.url));
43
44
  const ROOT_DIR = join(__dirname, '..', '..');
@@ -282,7 +283,7 @@ async function postSlackMessage(responseUrl, text, botToken) {
282
283
  * Load config.env values as an object.
283
284
  */
284
285
  function loadConfigEnv() {
285
- const envFile = join(HOME, '.construct', 'config.env');
286
+ const envFile = join(configDir(HOME), 'config.env');
286
287
  if (!existsSync(envFile)) return {};
287
288
  const lines = readFileSync(envFile, 'utf8').split('\n');
288
289
  const out = {};
@@ -20,13 +20,15 @@ import { resolveTraceBackend, telemetryProviderLabel } from './telemetry/client.
20
20
  import { memoryPort as derivedMemoryPort } from './home-namespace.mjs';
21
21
  import { readHeartbeat } from './daemons/contract.mjs';
22
22
  import { heartbeatPath } from './oracle/index.mjs';
23
+ import { stateDir } from './config/xdg.mjs';
24
+ import { doctorRoot } from './config/xdg.mjs';
23
25
 
24
26
  const DASHBOARD_STATE_FILE = 'dashboard.json';
25
27
 
26
28
  const INSTALL_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
27
29
 
28
30
  function runtimeStateDir(homeDir = os.homedir()) {
29
- return path.join(homeDir, '.construct', 'runtime');
31
+ return path.join(stateDir(homeDir), 'runtime');
30
32
  }
31
33
 
32
34
  function dashboardStatePath(homeDir = os.homedir()) {
@@ -84,7 +86,7 @@ export function buildRuntimeRecoverySummary({ rootDir, homeDir = os.homedir(), r
84
86
  latestHandoff: relativeOrNull(rootDir, latestHandoffPath(rootDir)),
85
87
  beads: fs.existsSync(path.join(rootDir, '.beads', 'metadata.json')) ? '.beads/metadata.json' : null,
86
88
  userConfig: fs.existsSync(getUserEnvPath(homeDir)) ? getUserEnvPath(homeDir) : null,
87
- userDataRoot: env.CX_DATA_DIR || path.join(homeDir, '.cx'),
89
+ userDataRoot: env.CX_DATA_DIR || doctorRoot(homeDir),
88
90
  };
89
91
 
90
92
  return {
@@ -215,7 +217,7 @@ export function stopDashboard(homeDir = os.homedir()) {
215
217
  }
216
218
 
217
219
  function doctorStatePath(homeDir) {
218
- return path.join(homeDir, '.construct', 'doctor.json');
220
+ return path.join(stateDir(homeDir), 'doctor.json');
219
221
  }
220
222
 
221
223
  export function readDoctorState(homeDir = os.homedir()) {
package/lib/setup.mjs CHANGED
@@ -3,8 +3,8 @@
3
3
  * lib/setup.mjs — Machine-scoped first-run setup wizard for Construct.
4
4
  *
5
5
  * Installs cm/cass, configures managed defaults, pre-warms the embedding model,
6
- * wires MCP integrations, and writes ~/.construct/config.env.
7
- * Invoked by `construct install` and by lib/install/first-invocation.mjs
6
+ * wires MCP integrations, and writes the XDG user config (~/.config/construct/config.env).
7
+ * Invoked by `construct install` and by lib/install/first-invocation.mjs
8
8
  * when a TTY user is missing resources.
9
9
  *
10
10
  * Project-scoped scaffolding (`.cx/`, AGENTS.md, plan.md, adapters) lives
@@ -22,6 +22,8 @@ import path from 'node:path';
22
22
  import { spawn, spawnSync } from 'node:child_process';
23
23
 
24
24
  import { ensureUserConfigDir, getUserEnvPath, writeEnvValues } from './env-config.mjs';
25
+ import { migrateLegacyModelConfig } from './config/legacy-config-migration.mjs';
26
+ import { configDir, stateDir, doctorRoot } from './config/xdg.mjs';
25
27
  import { DEFAULT_WORKSPACE_PATH, WORKSPACE_DOCS_LANES } from './embed/config.mjs';
26
28
  import {
27
29
  DEFAULT_DEPLOYMENT_MODE,
@@ -64,7 +66,7 @@ Flags:
64
66
  before setup; does not uninstall a global construct CLI
65
67
 
66
68
  What --scope=user does:
67
- - creates ~/.construct/config.env
69
+ - creates ~/.config/construct/config.env
68
70
  - ensures OpenCode config exists
69
71
  - configures managed defaults for local vector retrieval (LanceDB)
70
72
  - checks required runtime tools and installs cm and cass when available
@@ -97,7 +99,7 @@ function findCommand(command) {
97
99
 
98
100
  export function defaultVectorIndexPath(homeDir = HOME) {
99
101
  // LanceDB storage path
100
- return path.join(homeDir, '.construct', 'vector', 'lancedb');
102
+ return path.join(stateDir(homeDir), 'vector', 'lancedb');
101
103
  }
102
104
 
103
105
  export async function buildManagedSetupValues({ homeDir = HOME, env = process.env } = {}) {
@@ -266,7 +268,7 @@ function ensureOpenCodeConfig() {
266
268
  export { ensureGitHooksPath } from './git-hooks-path.mjs';
267
269
 
268
270
  export function ensureLibSymlink({ homeDir = HOME, rootDir = ROOT_DIR } = {}) {
269
- const target = path.join(homeDir, '.construct', 'lib');
271
+ const target = path.join(configDir(homeDir), 'lib');
270
272
  const source = path.join(rootDir, 'lib');
271
273
  fs.mkdirSync(path.dirname(target), { recursive: true });
272
274
  let stat = null;
@@ -348,12 +350,12 @@ function printInstallPlan({ scope, homeDir, withDocling, noLaunchAgent, cleanupL
348
350
  console.log('\nWould write:');
349
351
  console.log(` · ${getUserEnvPath(homeDir)} (user config, managed defaults)`);
350
352
  console.log(` · ${getCanonicalOpenCodeConfigPath(homeDir)} (OpenCode config)`);
351
- console.log(` · ${path.join(homeDir, '.construct', 'lib')} → ${path.join(ROOT_DIR, 'lib')} (hook lib symlink)`);
352
- console.log(` · ${path.join(homeDir, '.construct', 'workspace')} (workspace docs scaffold)`);
353
+ console.log(` · ${path.join(configDir(homeDir), 'lib')} → ${path.join(ROOT_DIR, 'lib')} (hook lib symlink)`);
354
+ console.log(` · ${path.join(stateDir(homeDir), 'workspace')} (workspace docs scaffold)`);
353
355
  console.log(` · ${path.dirname(defaultVectorIndexPath(homeDir))} (LanceDB vector store dir)`);
354
356
  console.log('\nWould check / install runtime tooling:');
355
357
  console.log(' · cm (memory CLI) and cass (session search) when available on PATH');
356
- console.log(' · construct-chat desktop binary (from GitHub releases → ~/.construct/bin/)');
358
+ console.log(` · construct-chat desktop binary (from GitHub releases → ${path.join(stateDir(homeDir), 'bin')})`);
357
359
  console.log('\nWould reach the network / external services:');
358
360
  console.log(' · embedding model warmup (loads from local cache; offline-safe, no download)');
359
361
  if (withDocling) console.log(' · docling Python venv provisioning via uv (~10 min)');
@@ -407,6 +409,14 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
407
409
  return;
408
410
  }
409
411
 
412
+ // Forward-migrate model tier overrides stranded in the pre-XDG legacy config
413
+ // before any legacy-global cleanup deletes ~/.construct, so an upgrade keeps
414
+ // the user's tier selection without a manual copy.
415
+ const modelMigration = migrateLegacyModelConfig({ homeDir });
416
+ if (modelMigration.performed) {
417
+ console.log(`Migrated ${Object.keys(modelMigration.migrated).join(', ')} from ${modelMigration.legacyPath} → ${modelMigration.xdgPath}`);
418
+ }
419
+
410
420
  if (cleanupLegacyGlobal) {
411
421
  runLegacyGlobalCleanup({ homeDir, dryRun: false });
412
422
  if (scope === 'project') {
@@ -587,7 +597,7 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
587
597
  runConstruct(['doctor']);
588
598
 
589
599
  const setupTs = new Date().toISOString().slice(0, 19).replace(/[:.]/g, '-');
590
- const setupLogPath = path.join(HOME, '.cx', `setup-${setupTs}.log`);
600
+ const setupLogPath = path.join(doctorRoot(), `setup-${setupTs}.log`);
591
601
  try {
592
602
  fs.mkdirSync(path.dirname(setupLogPath), { recursive: true });
593
603
  const logLines = [
@@ -605,7 +615,7 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
605
615
  console.log(' Traces: local JSONL (.cx/traces)');
606
616
  console.log(` Vector: LanceDB (embedded)`);
607
617
  console.log(` ${managedValues.CONSTRUCT_LANCEDB_PATH}`);
608
- console.log(' Credentials are saved to ~/.construct/config.env for later reference.');
618
+ console.log(` Credentials are saved to ${getUserEnvPath(homeDir)} for later reference.`);
609
619
 
610
620
  console.log('\nNext steps:');
611
621
  console.log(' construct provider add github # Connect GitHub repository data');
@@ -621,7 +631,7 @@ function ensureUserConfig(homeDir = HOME) {
621
631
  }
622
632
 
623
633
  export function ensureWorkspace(homeDir = HOME) {
624
- const wsPath = path.join(homeDir, '.construct', 'workspace');
634
+ const wsPath = path.join(stateDir(homeDir), 'workspace');
625
635
  const docsPath = path.join(wsPath, 'docs');
626
636
  for (const lane of WORKSPACE_DOCS_LANES) {
627
637
  fs.mkdirSync(path.join(docsPath, lane), { recursive: true });
package/lib/status.mjs CHANGED
@@ -22,6 +22,7 @@ import { readCostLog, summarizeCostData, normalizeCostEntry } from './cost.mjs';
22
22
  import { readEfficiencyLog, summarizeEfficiencyData } from './efficiency.mjs';
23
23
  import { triggerAutoBackfillIfSparse } from './telemetry/backfill.mjs';
24
24
  import { resolveTraceBackend, telemetryProviderLabel } from './telemetry/client.mjs';
25
+ import { doctorRoot } from './config/xdg.mjs';
25
26
  const TOTAL_BYTES_WARNING_THRESHOLD = 750_000;
26
27
 
27
28
  function readJSON(path) {
@@ -35,7 +36,7 @@ function readJSON(path) {
35
36
 
36
37
 
37
38
  function readSessionEfficiency(homeDir) {
38
- const stats = readJSON(join(homeDir, '.cx', 'session-efficiency.json'));
39
+ const stats = readJSON(join(doctorRoot(homeDir), 'session-efficiency.json'));
39
40
  if (!stats) return null;
40
41
 
41
42
  const readCount = Number(stats.readCount || 0);
@@ -85,7 +86,7 @@ function readSessionEfficiency(homeDir) {
85
86
  }
86
87
 
87
88
  function readTelemetryRichness(homeDir) {
88
- const stats = readJSON(join(homeDir, '.cx', 'session-telemetry.json'));
89
+ const stats = readJSON(join(doctorRoot(homeDir), 'session-telemetry.json'));
89
90
  if (!stats) return null;
90
91
 
91
92
  const total = Number(stats.total || 0);
@@ -134,7 +135,7 @@ export function readSessionUsage(homeDir) {
134
135
  totalCostUsd: 0,
135
136
  summary: 'No token usage recorded yet',
136
137
  lastInteraction: null,
137
- source: join(homeDir, '.cx', 'session-cost.jsonl'),
138
+ source: join(doctorRoot(homeDir), 'session-cost.jsonl'),
138
139
  lastUpdatedAt: null,
139
140
  };
140
141
  }
@@ -180,7 +181,7 @@ export function readSessionUsage(homeDir) {
180
181
  totalCostUsd: data.totalCostUsd,
181
182
  summary: `${data.interactions} interaction${data.interactions === 1 ? '' : 's'} · ${data.providerTotalTokens.toLocaleString()} provider · ${billed.toLocaleString()} billed (${cacheReadPct}% cache read · ${cacheWritePct}% cache write · ${freshPct}% fresh)`,
182
183
  lastInteraction,
183
- source: join(homeDir, '.cx', 'session-cost.jsonl'),
184
+ source: join(doctorRoot(homeDir), 'session-cost.jsonl'),
184
185
  lastUpdatedAt: lastInteraction.timestamp,
185
186
  };
186
187
  }
@@ -577,7 +578,7 @@ function buildSelfCheck() {
577
578
 
578
579
  // Embed daemon state
579
580
  try {
580
- const stateFile = join(homedir(), '.cx', 'runtime', 'embed-daemon.json');
581
+ const stateFile = join(doctorRoot(), 'runtime', 'embed-daemon.json');
581
582
  if (existsSync(stateFile)) {
582
583
  const state = JSON.parse(readFileSync(stateFile, 'utf8'));
583
584
  if (state.status === 'running' && state.pid) {
@@ -10,9 +10,10 @@
10
10
  * Model: Xenova/all-MiniLM-L6-v2 (384 dimensions, quantized)
11
11
  */
12
12
  import { join } from 'node:path';
13
- import { homedir } from 'node:os';
14
13
  import { mkdirSync, existsSync } from 'node:fs';
15
14
 
15
+ import { cacheDir as xdgCacheDir } from '../config/xdg.mjs';
16
+
16
17
  let modelPromise = null;
17
18
  let extractor = null;
18
19
 
@@ -23,7 +24,7 @@ async function getExtractor(cacheDir) {
23
24
  if (extractor) return extractor;
24
25
  if (modelPromise) return modelPromise;
25
26
 
26
- const cachePath = cacheDir || join(homedir(), '.construct', 'cache', 'embeddings');
27
+ const cachePath = cacheDir || join(xdgCacheDir(), 'embeddings');
27
28
  if (!existsSync(cachePath)) {
28
29
  mkdirSync(cachePath, { recursive: true });
29
30
  }
@@ -40,7 +40,7 @@ function toDocumentRows(rootDir, snapshot, project = 'construct') {
40
40
  title: 'Architecture docs',
41
41
  summary: body.slice(0, 240),
42
42
  body,
43
- source_path: 'docs/concepts/architecture.md',
43
+ source_path: 'docs/guides/concepts/architecture.md',
44
44
  tags: ['architecture', 'docs'],
45
45
  content_hash: hashContent(body),
46
46
  });
@@ -64,7 +64,7 @@ function toDocumentRows(rootDir, snapshot, project = 'construct') {
64
64
  for (const doc of snapshot.productIntelDocs ?? []) {
65
65
  const body = doc.body;
66
66
  const rel = doc.path;
67
- const kind = rel.startsWith('docs/prd/')
67
+ const kind = rel.startsWith('docs/specs/prd/')
68
68
  ? 'prd'
69
69
  : rel.startsWith('docs/meta-prd/')
70
70
  ? 'meta-prd'
@@ -5,7 +5,6 @@
5
5
  * CONSTRUCT_TRACE_BACKEND and never throws into callers.
6
6
  */
7
7
  import { existsSync, mkdirSync, appendFileSync } from 'node:fs';
8
- import { homedir } from 'node:os';
9
8
  import { join } from 'node:path';
10
9
  import { randomUUID } from 'node:crypto';
11
10
 
@@ -13,18 +13,18 @@
13
13
  */
14
14
 
15
15
  import fs from 'node:fs';
16
- import os from 'node:os';
17
16
  import path from 'node:path';
18
17
  import { appendBounded } from '../logging/rotate.mjs';
19
18
  import { resolveProjectScopedPath } from '../project-root.mjs';
19
+ import { doctorRoot } from '../config/xdg.mjs';
20
20
 
21
21
  // intent-verifications are PROJECT-SCOPED — a verification belongs to a
22
- // specific routing decision in a specific project. DEFAULT_LOG_PATH stays
23
- // at the legacy user-scope location for back-compat with readers that
24
- // imported the constant; the writer below resolves per-call so each
25
- // invocation lands in the project's .cx/ when one is detected.
22
+ // specific routing decision in a specific project. DEFAULT_LOG_PATH resolves
23
+ // to the global doctor root for back-compat with readers that imported the
24
+ // constant; the writer below resolves per-call so each invocation lands in
25
+ // the project's .cx/ when one is detected.
26
26
 
27
- export const DEFAULT_LOG_PATH = path.join(os.homedir(), '.cx', 'intent-verifications.jsonl');
27
+ export const DEFAULT_LOG_PATH = path.join(doctorRoot(), 'intent-verifications.jsonl');
28
28
 
29
29
  /**
30
30
  * Fire-and-forget append of one verification event.
@@ -15,13 +15,14 @@
15
15
  */
16
16
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
17
17
  import { join } from "path";
18
- import { homedir } from "os";
18
+
19
+ import { doctorRoot } from "../config/xdg.mjs";
19
20
 
20
21
  const OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
21
22
  const LITELLM_PRICING_URL =
22
23
  "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json";
23
24
  const PRICING_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
24
- const PRICING_CACHE_PATH = join(homedir(), ".cx", "pricing-cache.json");
25
+ const PRICING_CACHE_PATH = join(doctorRoot(), "pricing-cache.json");
25
26
 
26
27
  export function describePricingCatalogFreshness() {
27
28
  try {
@@ -133,7 +134,7 @@ async function fetchLiteLLMPricing(fetchImpl = globalThis.fetch) {
133
134
  });
134
135
  }
135
136
  try {
136
- mkdirSync(join(homedir(), ".cx"), { recursive: true });
137
+ mkdirSync(doctorRoot(), { recursive: true });
137
138
  writeFileSync(PRICING_CACHE_PATH, JSON.stringify({ fetchedAt: Date.now(), models }));
138
139
  } catch { /* non-critical */ }
139
140
  return models;
@@ -1,16 +1,16 @@
1
1
  /**
2
2
  * lib/telemetry/rule-calls.mjs — record when a rule path is referenced at runtime.
3
3
  *
4
- * Mirrors hook-calls telemetry: append-only JSONL at ~/.cx/rule-calls.jsonl.
4
+ * Mirrors hook-calls telemetry: append-only JSONL at <doctorRoot>/rule-calls.jsonl.
5
5
  * Consumed by `construct rules usage` for consolidation decisions.
6
6
  */
7
7
 
8
8
  import fs from 'node:fs';
9
- import os from 'node:os';
10
9
  import path from 'node:path';
11
10
  import { appendBounded } from '../logging/rotate.mjs';
11
+ import { doctorRoot } from '../config/xdg.mjs';
12
12
 
13
- export const DEFAULT_LOG_PATH = path.join(os.homedir(), '.cx', 'rule-calls.jsonl');
13
+ export const DEFAULT_LOG_PATH = path.join(doctorRoot(), 'rule-calls.jsonl');
14
14
 
15
15
  /**
16
16
  * @param {object} event
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * lib/telemetry/skill-calls.mjs — measure which skill files actually get loaded.
3
3
  *
4
- * Appends one JSON line per skill load to ~/.cx/skill-calls.jsonl so
4
+ * Appends one JSON line per skill load to <doctorRoot>/skill-calls.jsonl so
5
5
  * `construct skills usage/orphans/hot/correlate-quality` can answer:
6
6
  * which skills have no load events in a 30-day window (pruning candidates),
7
7
  * which skills are loaded from many agents (hot paths), and which skills
@@ -15,17 +15,17 @@
15
15
  */
16
16
 
17
17
  import fs from 'node:fs';
18
- import os from 'node:os';
19
18
  import path from 'node:path';
20
19
  import { appendBounded } from '../logging/rotate.mjs';
21
20
  import { resolveProjectScope } from '../project-root.mjs';
21
+ import { doctorRoot } from '../config/xdg.mjs';
22
22
 
23
23
  // Skill calls are CROSS-PROJECT telemetry — a single log lets you see which
24
24
  // skills load hot across every project the user has run Construct in. Stays
25
25
  // at user scope; each entry is tagged with `projectId` so a reader can
26
26
  // attribute usage to a specific project without losing the global view.
27
27
 
28
- export const DEFAULT_LOG_PATH = path.join(os.homedir(), '.cx', 'skill-calls.jsonl');
28
+ export const DEFAULT_LOG_PATH = path.join(doctorRoot(), 'skill-calls.jsonl');
29
29
 
30
30
  /**
31
31
  * Fire-and-forget log of a skill load event.
@@ -70,4 +70,5 @@ export const TEMPLATE_OWNERSHIP_EXEMPTIONS = new Set([
70
70
  'rfc-platform',
71
71
  'prd-platform',
72
72
  'prd-business',
73
+ 'README',
73
74
  ]);
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Loads structure and visual requirements from specialists/artifact-manifest.json
5
5
  * (single source of truth) and merges legacy types not yet in the manifest.
6
- * Paired with docs/concepts/doc-visual-matrix.md; pinned by template tests.
6
+ * Paired with docs/guides/concepts/doc-visual-matrix.md; pinned by template tests.
7
7
  */
8
8
 
9
9
  import { validateArtifactPostconditions } from '../contracts/validate.mjs';
@@ -173,7 +173,7 @@ export function computeReleaseCriticalGaps(ledger) {
173
173
  for (const row of SHIPPED_MATRIX_CAPABILITIES) {
174
174
  if (ledgerCoversHint(allLedgerIds, row.ledgerHints)) continue;
175
175
  gaps.push({
176
- source: 'docs/audit/capability-matrix.md',
176
+ source: 'docs/operations/audit/capability-matrix.md',
177
177
  capability: row.id,
178
178
  status: 'shipped',
179
179
  reason: 'no ledger entry matches shipped capability hints',
@@ -24,7 +24,7 @@
24
24
  * --dry-run print the plan, change nothing
25
25
  * --yes non-interactive; remove auto-risk items, skip ask-risk items
26
26
  * --yes --all non-interactive; remove both auto- and ask-risk items
27
- * --keep-state only remove project-scope launcher + adapters; preserve .cx/, ~/.construct, Postgres
27
+ * --keep-state only remove project-scope launcher + adapters; preserve .cx/, the XDG user dirs, Postgres
28
28
  * --with-images also remove the shared pgvector Docker image (off by default; ADR-0027)
29
29
  * --scope=project|machine|all limit which categories are evaluated (default: all)
30
30
  */
@@ -37,6 +37,7 @@ import { fileURLToPath } from 'node:url';
37
37
 
38
38
  import { multiSelect } from '../tty-prompts.mjs';
39
39
  import { getUserConfigDir, getUserEnvPath } from '../env-config.mjs';
40
+ import { stateDir, cacheDir, doctorRoot } from '../config/xdg.mjs';
40
41
  import { removeTomlTables, tomlString } from '../codex-config.mjs';
41
42
  import { postgresContainerName, LEGACY_PG_CONTAINER } from '../home-namespace.mjs';
42
43
 
@@ -84,12 +85,13 @@ function buildCategories({ cwd, home, withImages = false }) {
84
85
  const projectPlanMd = path.join(cwd, 'plan.md');
85
86
  const userHome = home;
86
87
  const userConstructDir = getUserConfigDir(userHome);
88
+ const userStateDir = stateDir(userHome);
87
89
  const userConfigEnv = getUserEnvPath(userHome);
88
- const userEmbedCache = path.join(userConstructDir, 'cache', 'embeddings');
89
- const userVectorIndex = path.join(userConstructDir, 'vector');
90
- const userWorkspace = path.join(userConstructDir, 'workspace');
90
+ const userEmbedCache = path.join(cacheDir(userHome), 'embeddings');
91
+ const userVectorIndex = path.join(userStateDir, 'vector');
92
+ const userWorkspace = path.join(userStateDir, 'workspace');
91
93
  const userPgComposeDir = path.join(userConstructDir, 'services', 'postgres');
92
- const userCxDir = path.join(userHome, '.cx');
94
+ const userCxDir = doctorRoot(userHome);
93
95
  const launchAgentPlist = path.join(userHome, 'Library', 'LaunchAgents', `${PRESSURE_GUARD_LABEL}.plist`);
94
96
  const claudeSettings = path.join(userHome, '.claude', 'settings.json');
95
97
  const opencodeConfig = path.join(userHome, '.config', 'opencode', 'opencode.json');
@@ -172,7 +174,7 @@ function buildCategories({ cwd, home, withImages = false }) {
172
174
  id: 'machine-workspace',
173
175
  scope: 'machine',
174
176
  risk: 'auto',
175
- label: '~/.construct/workspace and ~/.construct/vector',
177
+ label: `${rel(userHome, userWorkspace)} and ${rel(userHome, userVectorIndex)}`,
176
178
  detect: () => existsAny(userWorkspace) || existsAny(userVectorIndex),
177
179
  describe: () => 'Removes the per-user working dir and fallback JSON vector index. Both are regenerated on next use.',
178
180
  execute: () => {
@@ -186,7 +188,7 @@ function buildCategories({ cwd, home, withImages = false }) {
186
188
  id: 'machine-cx',
187
189
  scope: 'machine',
188
190
  risk: 'auto',
189
- label: '~/.cx (daemon state, logs)',
191
+ label: `${rel(userHome, userCxDir)} (daemon state, logs)`,
190
192
  detect: () => existsAny(userCxDir),
191
193
  describe: () => `Removes ${rel(userHome, userCxDir)}. Daemon PIDs, telemetry, setup logs; all regenerated on next setup.`,
192
194
  execute: () => removePath(userCxDir),
@@ -195,7 +197,7 @@ function buildCategories({ cwd, home, withImages = false }) {
195
197
  id: 'machine-embedding-cache',
196
198
  scope: 'machine',
197
199
  risk: 'ask',
198
- label: '~/.construct/cache/embeddings (~22 MB ONNX model)',
200
+ label: `${rel(userHome, userEmbedCache)} (~22 MB ONNX model)`,
199
201
  detect: () => existsAny(userEmbedCache),
200
202
  describe: () => 'Removes the cached ONNX embedding model. Skip if you plan to reinstall soon — re-downloading takes a minute.',
201
203
  execute: () => removePath(userEmbedCache),
@@ -204,7 +206,7 @@ function buildCategories({ cwd, home, withImages = false }) {
204
206
  id: 'machine-config-env',
205
207
  scope: 'machine',
206
208
  risk: 'ask',
207
- label: '~/.construct/config.env (API keys + consent flags)',
209
+ label: `${rel(userHome, userConfigEnv)} (API keys + consent flags)`,
208
210
  detect: () => existsAny(userConfigEnv),
209
211
  describe: () => 'Removes saved API keys (Anthropic/OpenAI/telemetry/etc.) and bootstrap consent. Skip if you intend to reinstall and reuse the keys.',
210
212
  execute: () => removePath(userConfigEnv),
@@ -231,7 +233,7 @@ function buildCategories({ cwd, home, withImages = false }) {
231
233
  id: 'machine-postgres-compose',
232
234
  scope: 'machine',
233
235
  risk: 'auto',
234
- label: '~/.construct/services/postgres/ (compose file)',
236
+ label: `${rel(userHome, userPgComposeDir)} (compose file)`,
235
237
  detect: () => existsAny(userPgComposeDir),
236
238
  describe: () => 'Removes the local docker-compose.yml. Run after removing the container.',
237
239
  execute: () => removePath(userPgComposeDir),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geraldmaron/construct",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "type": "module",
5
5
  "packageManager": "npm@11.5.1",
6
6
  "description": "Construct — agent orchestration layer for OpenCode, Claude Code, and other coding surfaces",
@@ -17,6 +17,7 @@
17
17
  "bin/construct",
18
18
  "bin/construct-postinstall.mjs",
19
19
  "lib/**",
20
+ "apps/chat/engine/**",
20
21
  "specialists/**",
21
22
  "examples/**",
22
23
  "personas/**",
@@ -52,7 +53,7 @@
52
53
  "scripts": {
53
54
  "mcp": "node lib/mcp/server.mjs",
54
55
  "test": "node scripts/run-tests.mjs",
55
- "test:functional": "node --test --test-timeout=120000 --test-concurrency=1 tests/functional/*.functional.test.mjs",
56
+ "test:functional": "XDG_CONFIG_HOME= XDG_STATE_HOME= XDG_CACHE_HOME= node --test --test-timeout=120000 --test-concurrency=1 tests/functional/*.functional.test.mjs",
56
57
  "test:unit": "node scripts/run-tests.mjs --exclude=tests/functional",
57
58
  "lint:js": "eslint bin/construct \"lib/**/*.mjs\" \"scripts/**/*.mjs\" \"tests/**/*.mjs\"",
58
59
  "coverage": "c8 --reporter=text-summary --reporter=lcov --src=lib --src=bin node scripts/run-tests.mjs --exclude=tests/functional",
@@ -80,7 +81,7 @@
80
81
  "release:check": "node ./bin/construct doctor && npm test && node ./bin/construct docs:update --check && node ./bin/construct docs:site --check && node ./bin/construct registry:generate-docs --check && node ./bin/construct docs:verify --strict && node scripts/alignment/census.mjs --ratchet && node ./bin/construct dashboard:sync --check && node ./bin/construct lint:comments && node ./bin/construct certify gate && node scripts/lint-commits-pr.mjs",
81
82
  "release:gate": "node --test tests/functional/release-gate.functional.test.mjs",
82
83
  "audit:published": "node scripts/audit-published-artifact.mjs",
83
- "version": "node scripts/sync-construct-version.mjs && git add .construct/version",
84
+ "version": "node scripts/sync-construct-version.mjs && git add -f .construct/version",
84
85
  "release:preflight": "node scripts/pre-release-check.mjs",
85
86
  "release:preflight:no-auth": "node scripts/pre-release-check.mjs --skip-auth",
86
87
  "npm:publish": "npm run release:check && npm publish --access public",