@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
@@ -18,6 +18,7 @@ import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
18
18
  import { join } from 'node:path';
19
19
  import { homedir } from 'node:os';
20
20
  import { spawnSync } from 'node:child_process';
21
+ import { configDir } from '../config/xdg.mjs';
21
22
 
22
23
  // ── Multi-method auth helpers ────────────────────────────────────────────
23
24
 
@@ -268,8 +269,8 @@ export async function createJiraTicket(packet, { host, email, token, project, is
268
269
  const resolvedToken = token || auth.token || process.env.JIRA_API_TOKEN;
269
270
  const resolvedProject = project || auth.project || process.env.JIRA_PROJECT;
270
271
 
271
- if (!resolvedHost) return { ok: false, externalUrl: null, externalId: null, error: 'JIRA_HOST not set (e.g. https://your-domain.atlassian.net). Check ~/.construct/config.env or shell rc.' };
272
- if (!resolvedEmail || !resolvedToken) return { ok: false, externalUrl: null, externalId: null, error: 'JIRA_USER and JIRA_API_TOKEN required. Check ~/.construct/config.env, ~/.env, or 1Password.' };
272
+ if (!resolvedHost) return { ok: false, externalUrl: null, externalId: null, error: 'JIRA_HOST not set (e.g. https://your-domain.atlassian.net). Check ~/.config/construct/config.env or shell rc.' };
273
+ if (!resolvedEmail || !resolvedToken) return { ok: false, externalUrl: null, externalId: null, error: 'JIRA_USER and JIRA_API_TOKEN required. Check ~/.config/construct/config.env, ~/.env, or 1Password.' };
273
274
  if (!resolvedProject) return { ok: false, externalUrl: null, externalId: null, error: 'JIRA_PROJECT not set (e.g. PROJ)' };
274
275
 
275
276
  const t = packet?.triage || {};
@@ -385,8 +386,8 @@ export async function publishArtifactToConfluence(artifact, { host, email, token
385
386
  const resolvedToken = token || auth.token || process.env.CONFLUENCE_API_TOKEN;
386
387
  const resolvedSpace = space || auth.space || process.env.CONFLUENCE_SPACE;
387
388
 
388
- if (!resolvedHost) return { ok: false, externalUrl: null, externalId: null, error: 'CONFLUENCE_HOST not set (e.g. https://your-domain.atlassian.net/wiki). Check ~/.construct/config.env or shell rc.' };
389
- if (!resolvedEmail || !resolvedToken) return { ok: false, externalUrl: null, externalId: null, error: 'CONFLUENCE_USER and CONFLUENCE_API_TOKEN required. Check ~/.construct/config.env, ~/.env, or 1Password.' };
389
+ if (!resolvedHost) return { ok: false, externalUrl: null, externalId: null, error: 'CONFLUENCE_HOST not set (e.g. https://your-domain.atlassian.net/wiki). Check ~/.config/construct/config.env or shell rc.' };
390
+ if (!resolvedEmail || !resolvedToken) return { ok: false, externalUrl: null, externalId: null, error: 'CONFLUENCE_USER and CONFLUENCE_API_TOKEN required. Check ~/.config/construct/config.env, ~/.env, or 1Password.' };
390
391
  if (!resolvedSpace) return { ok: false, externalUrl: null, externalId: null, error: 'CONFLUENCE_SPACE not set (e.g. PROD)' };
391
392
 
392
393
  const prefix = { prd: 'PRD', adr: 'ADR', rfc: 'RFC' }[artifact.type] || artifact.type.toUpperCase();
@@ -608,7 +609,7 @@ function resolveCredentialSync(varName, homeDir) {
608
609
  }
609
610
  } catch { /* skip */ }
610
611
  try {
611
- const cfgPath = join(homeDir, '.construct', 'config.env');
612
+ const cfgPath = join(configDir(homeDir), 'config.env');
612
613
  if (existsSync(cfgPath)) {
613
614
  const content = readFileSync(cfgPath, 'utf8');
614
615
  const m = content.match(new RegExp(`^${varName}=(.+)$`, 'm'));
@@ -703,9 +704,9 @@ function resolveCredential(varName, homeDir) {
703
704
  }
704
705
  } catch { /* skip */ }
705
706
 
706
- // ~/.construct/config.env
707
+ // config.env in the XDG config dir
707
708
  try {
708
- const cfgPath = join(homeDir, '.construct', 'config.env');
709
+ const cfgPath = join(configDir(homeDir), 'config.env');
709
710
  if (existsSync(cfgPath)) {
710
711
  const content = readFileSync(cfgPath, 'utf8');
711
712
  const m = content.match(new RegExp(`^${varName}=(.+)$`, 'm'));
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * Sources indexed:
9
9
  * - Observations (.cx/observations/)
10
- * - Artifacts (docs/adr/, docs/prd/, docs/rfc/)
10
+ * - Artifacts (docs/decisions/adr/, docs/specs/prd/, docs/decisions/rfc/)
11
11
  * - Snapshots (.cx/snapshot.md + any configured output paths)
12
12
  * - Approval queue (.cx/approval-queue.jsonl)
13
13
  *
@@ -102,7 +102,7 @@ function loadMarkdownChunks(dir, source) {
102
102
  */
103
103
  function loadArtifactChunks(rootDir) {
104
104
  const chunks = [];
105
- for (const subdir of ['docs/adr', 'docs/prd', 'docs/rfc', 'docs/concepts/architecture.md']) {
105
+ for (const subdir of ['docs/adr', 'docs/prd', 'docs/rfc', 'docs/guides/concepts/architecture.md']) {
106
106
  const full = path.resolve(rootDir, subdir);
107
107
  if (subdir.endsWith('.md')) {
108
108
  if (!fs.existsSync(full)) continue;
@@ -6,11 +6,11 @@
6
6
  * `knowledge_search` MCP tool — no daemon, no network, no external deps.
7
7
  *
8
8
  * Sources searched (in priority order):
9
- * 1. docs/concepts/*.md — architecture, agents, gates, durable state
10
- * 2. docs/start/*.mdx — install + first task
9
+ * 1. docs/guides/concepts/*.md — architecture, agents, gates, durable state
10
+ * 2. docs/guides/start/*.mdx — install + first task
11
11
  * 3. docs/README.md — docs index + contract
12
12
  * 4. .cx/knowledge/ — operator-written internal docs
13
- * 5. Any *.md in docs/cookbook/ — task-oriented recipes
13
+ * 5. Any *.md in docs/guides/cookbook/ — task-oriented recipes
14
14
  * 6. <projectRoot>/.cx/knowledge/** — the cwd project's knowledge tree,
15
15
  * including `external/research/` written by `construct knowledge add`,
16
16
  * so foreign-project queries surface project content alongside bundled docs.
@@ -47,17 +47,17 @@ function buildSourceList(repoRoot = REPO_ROOT, projectRoot = null) {
47
47
  const sources = [];
48
48
 
49
49
  const priority = [
50
- 'docs/concepts/architecture.md',
51
- 'docs/concepts/agents-and-personas.mdx',
52
- 'docs/concepts/gates-and-enforcement.md',
53
- 'docs/concepts/beads-and-state.md',
54
- 'docs/concepts/prompt-surfaces.md',
55
- 'docs/concepts/knowledge-layout.md',
56
- 'docs/concepts/deployment-model.md',
57
- 'docs/concepts/embedding-boundary.md',
50
+ 'docs/guides/concepts/architecture.md',
51
+ 'docs/guides/concepts/agents-and-personas.mdx',
52
+ 'docs/guides/concepts/gates-and-enforcement.md',
53
+ 'docs/guides/concepts/beads-and-state.md',
54
+ 'docs/guides/concepts/prompt-surfaces.md',
55
+ 'docs/guides/concepts/knowledge-layout.md',
56
+ 'docs/guides/concepts/deployment-model.md',
57
+ 'docs/guides/concepts/embedding-boundary.md',
58
58
  'docs/README.md',
59
- 'docs/start/install.mdx',
60
- 'docs/start/first-task.mdx',
59
+ 'docs/guides/start/install.mdx',
60
+ 'docs/guides/start/first-task.mdx',
61
61
  ];
62
62
 
63
63
  for (const rel of priority) {
@@ -66,12 +66,12 @@ function buildSourceList(repoRoot = REPO_ROOT, projectRoot = null) {
66
66
  }
67
67
 
68
68
  // Cookbook recipes (task-oriented how-tos)
69
- const cookbookDir = join(repoRoot, 'docs', 'cookbook');
69
+ const cookbookDir = join(repoRoot, 'docs', 'guides', 'cookbook');
70
70
  if (existsSync(cookbookDir)) {
71
71
  for (const file of readdirSync(cookbookDir)) {
72
72
  if (file.endsWith('.md') || file.endsWith('.mdx')) {
73
73
  const full = join(cookbookDir, file);
74
- sources.push({ path: full, rel: `docs/cookbook/${file}`, priority: 2 });
74
+ sources.push({ path: full, rel: `docs/guides/cookbook/${file}`, priority: 2 });
75
75
  }
76
76
  }
77
77
  }
@@ -10,8 +10,9 @@
10
10
  * Triggered from two entry points:
11
11
  * - `construct cleanup` — manual sweep, optionally --dry-run
12
12
  * - bin/construct startup check — automatic when the installed version
13
- * differs from ~/.construct/.cleanup-stamp,
14
- * so an upgrade reclaims disk without
13
+ * differs from the XDG state-dir
14
+ * .cleanup-stamp, so an upgrade reclaims
15
+ * disk without
15
16
  * requiring a manual command.
16
17
  *
17
18
  * Scope is deliberately narrow: only artifacts whose retention is owned by
@@ -24,6 +25,9 @@ import fs from 'node:fs';
24
25
  import os from 'node:os';
25
26
  import path from 'node:path';
26
27
 
28
+ import { stateDir, cacheDir } from '../config/xdg.mjs';
29
+ import { doctorRoot } from '../config/xdg.mjs';
30
+
27
31
  const HARD_BUDGET_MS = 5000;
28
32
 
29
33
  // Size caps in bytes. Each entry: [filename relative to ~/.cx/, maxBytes].
@@ -92,7 +96,7 @@ function rotationConfig(env = process.env) {
92
96
  * were oversized before the cap existed.
93
97
  */
94
98
  export function cleanupEmbedLog({ homeDir = os.homedir(), env = process.env, dryRun = false } = {}) {
95
- const dir = path.join(homeDir, '.cx', 'runtime');
99
+ const dir = path.join(doctorRoot(homeDir), 'runtime');
96
100
  const baseLog = path.join(dir, 'embed-daemon.log');
97
101
  const { maxBytes, keep } = rotationConfig(env);
98
102
 
@@ -149,7 +153,7 @@ export function cleanupEmbedLog({ homeDir = os.homedir(), env = process.env, dry
149
153
  * recent records survive.
150
154
  */
151
155
  export function cleanupJsonlLogs({ homeDir = os.homedir(), dryRun = false } = {}) {
152
- const dir = path.join(homeDir, '.cx');
156
+ const dir = doctorRoot(homeDir);
153
157
  const summary = { truncated: [], freedBytes: 0 };
154
158
  if (!fs.existsSync(dir)) return summary;
155
159
 
@@ -168,11 +172,11 @@ export function cleanupJsonlLogs({ homeDir = os.homedir(), dryRun = false } = {}
168
172
  }
169
173
 
170
174
  /**
171
- * Age out ~/.construct/cache/ entries older than `maxAgeDays`. Cache hits are
175
+ * Age out XDG cache-dir entries older than `maxAgeDays`. Cache hits are
172
176
  * idempotent and re-fetched on demand, so pruning is safe.
173
177
  */
174
178
  export function cleanupCacheDir({ homeDir = os.homedir(), maxAgeDays = 30, dryRun = false } = {}) {
175
- const dir = path.join(homeDir, '.construct', 'cache');
179
+ const dir = cacheDir(homeDir);
176
180
  const summary = { removed: [], freedBytes: 0 };
177
181
  if (!fs.existsSync(dir)) return summary;
178
182
 
@@ -249,18 +253,18 @@ export function formatBytes(bytes) {
249
253
  // ---------------------------------------------------------------------------
250
254
  // Version stamp — drives automatic cleanup on upgrade.
251
255
  //
252
- // ~/.construct/.cleanup-stamp records the last version that ran cleanup.
253
- // On every CLI invocation, bin/construct compares the current package
256
+ // The .cleanup-stamp in the XDG state dir records the last version that ran
257
+ // cleanup. On every CLI invocation, bin/construct compares the current package
254
258
  // version to the stamp. A mismatch triggers a single cleanup pass and
255
259
  // writes the new version into the stamp. The first ever invocation also
256
260
  // triggers cleanup (no stamp present) — this is what unblocks existing
257
261
  // installs that already have 34 GB logs sitting around.
258
262
  // ---------------------------------------------------------------------------
259
263
 
260
- const STAMP_RELATIVE = path.join('.construct', '.cleanup-stamp');
264
+ const STAMP_FILENAME = '.cleanup-stamp';
261
265
 
262
266
  export function stampPath(homeDir = os.homedir()) {
263
- return path.join(homeDir, STAMP_RELATIVE);
267
+ return path.join(stateDir(homeDir), STAMP_FILENAME);
264
268
  }
265
269
 
266
270
  export function readVersionStamp({ homeDir = os.homedir() } = {}) {
@@ -859,7 +859,7 @@ const ALL_TOOL_DEFS = [
859
859
  },
860
860
  {
861
861
  name: 'profile_create',
862
- description: 'Scaffold a draft org profile under `.cx/profiles/draft-<id>/` (requirements.md + profile.json + persona stubs + department charters). Writes durable state — requires `confirm=true`. For curated catalog work, follow `docs/concepts/profile-lifecycle.md` after creation.',
862
+ description: 'Scaffold a draft org profile under `.cx/profiles/draft-<id>/` (requirements.md + profile.json + persona stubs + department charters). Writes durable state — requires `confirm=true`. For curated catalog work, follow `docs/guides/concepts/profile-lifecycle.md` after creation.',
863
863
  inputSchema: {
864
864
  type: 'object',
865
865
  required: ['confirm', 'id'],
@@ -13,6 +13,7 @@ import { inspectContextState } from '../../context-state.mjs';
13
13
  import { loadWorkflow, summarizeWorkflow, inspectWorkflowHealth } from '../../workflow-state.mjs';
14
14
  import { readCurrentModels, resolveExecutionContractModelMetadata, selectModelTierForWorkCategory } from '../../model-router.mjs';
15
15
  import { buildPublicHealthSurface } from '../../status.mjs';
16
+ import { doctorRoot } from '../../config/xdg.mjs';
16
17
 
17
18
  export function exec(cmd, cwd) {
18
19
  return execSync(cmd, { stdio: 'pipe', timeout: 10000, cwd }).toString().trim();
@@ -60,7 +61,7 @@ function isBinary(buffer) {
60
61
  }
61
62
 
62
63
  export function agentHealth(args) {
63
- const reviewDir = join(homedir(), '.cx', 'performance-reviews');
64
+ const reviewDir = join(doctorRoot(), 'performance-reviews');
64
65
 
65
66
  if (!existsSync(reviewDir)) {
66
67
  return { error: "No performance reviews found. Run 'construct review' to generate one." };
@@ -21,10 +21,11 @@ import os from 'node:os';
21
21
  import path from 'node:path';
22
22
  import { getProviderModelCatalog, PROVIDER_FAMILY_TIERS } from './model-router.mjs';
23
23
  import { getPricingForModels, formatPricingLabel } from './model-pricing.mjs';
24
+ import { configDir } from './config/xdg.mjs';
24
25
 
25
26
  const CHEAPEST_PREF_KEY = 'CHEAPEST_PROVIDER_ENABLED';
26
27
  const CHEAPEST_CHECKED_KEY = 'CHEAPEST_PROVIDER_CHECKED';
27
- const ENV_PATH = path.join(os.homedir(), '.construct', 'config.env');
28
+ const ENV_PATH = path.join(configDir(), 'config.env');
28
29
 
29
30
  /**
30
31
  * Resolve the cheapest configured provider for a given tier.
@@ -32,7 +33,7 @@ const ENV_PATH = path.join(os.homedir(), '.construct', 'config.env');
32
33
  * @param {string} tier - One of "reasoning", "standard", "fast"
33
34
  * @param {object} [opts]
34
35
  * @param {object} [opts.env] - Environment object (default: process.env)
35
- * @param {string} [opts.envPath] - Path to config.env (default: ~/.construct/config.env)
36
+ * @param {string} [opts.envPath] - Path to config.env (default: config.env in the XDG config dir)
36
37
  * @param {Function} [opts.getPricingForModels] - Injectable pricing fetcher
37
38
  * @returns {Promise<{
38
39
  * providerId: string|null,
@@ -130,7 +131,7 @@ export async function rankConfiguredProvidersByCost(tier, opts = {}) {
130
131
  /**
131
132
  * Check if the user has opted into cheapest-provider selection.
132
133
  *
133
- * @param {string} [envPath] - Path to config.env (default: ~/.construct/config.env)
134
+ * @param {string} [envPath] - Path to config.env (default: config.env in the XDG config dir)
134
135
  * @param {object} [opts.env] - Environment override
135
136
  * @returns {boolean}
136
137
  */
@@ -16,10 +16,11 @@
16
16
  */
17
17
 
18
18
  import fs from 'node:fs';
19
- import os from 'node:os';
20
19
  import path from 'node:path';
21
20
 
22
- const DEFAULT_CACHE_FILE = path.join(os.homedir(), '.cx', 'model-pricing.json');
21
+ import { doctorRoot } from './config/xdg.mjs';
22
+
23
+ const DEFAULT_CACHE_FILE = path.join(doctorRoot(), 'model-pricing.json');
23
24
  const CACHE_TTL_MS = 5 * 60 * 1000;
24
25
  const OPENROUTER_ENDPOINT = 'https://openrouter.ai/api/v1/models';
25
26
 
@@ -41,6 +41,7 @@ import {
41
41
  loadModelsCatalogContext,
42
42
  mergeLiveModelsIntoProviders,
43
43
  } from "./models/catalog.mjs";
44
+ import { configDir } from "./config/xdg.mjs";
44
45
 
45
46
  function uniqueStrings(values = []) {
46
47
  return [...new Set(values.filter((value) => typeof value === "string" && value.trim()))];
@@ -380,7 +381,7 @@ function hasCopilotCredential() {
380
381
  const home = process.env.HOME || process.env.USERPROFILE || '';
381
382
  if (!home) return false;
382
383
  const candidates = [
383
- path.join(home, '.construct', 'auth', 'github-copilot.json'),
384
+ path.join(configDir(home), 'auth', 'github-copilot.json'),
384
385
  path.join(home, '.config', 'github-copilot', 'apps.json'),
385
386
  path.join(home, '.config', 'github-copilot', 'hosts.json'),
386
387
  ];
@@ -421,7 +422,7 @@ function isProviderConfigured(familyId, env, { allowAmbient = true } = {}) {
421
422
  } catch { /* not available */ }
422
423
  }
423
424
 
424
- // Any credential source counts — env, ~/.construct/config.env, ~/.env, the
425
+ // Any credential source counts — env, the XDG config dir config.env, ~/.env, the
425
426
  // project .env, and shell rc exports — and a stored 1Password op:// reference
426
427
  // counts as configured because the resolver reads it lazily at call time.
427
428
  if (hasAnySecret(varNames, { env, allowAmbient })) return true;
@@ -569,7 +570,7 @@ function availabilityNotice(rejected) {
569
570
  if (rejected.reason === 'provider_not_configured') {
570
571
  const family = rejected.provider || '';
571
572
  if (family.startsWith('openrouter')) {
572
- return `Saved ${label} — OpenRouter not configured. Set OPENROUTER_API_KEY in ~/.construct/config.env or run \`construct creds set openrouter\`.`;
573
+ return `Saved ${label} — OpenRouter not configured. Set OPENROUTER_API_KEY in ~/.config/construct/config.env or run \`construct creds set openrouter\`.`;
573
574
  }
574
575
  return `Saved ${label} — provider not configured.`;
575
576
  }
@@ -12,6 +12,7 @@ import os from 'node:os';
12
12
  import { pollFreeModels } from '../model-free-selector.mjs';
13
13
  import { resolveFirstSecret } from '../providers/secret-resolver.mjs';
14
14
  import { loadProjectConfig } from '../config/project-config.mjs';
15
+ import { doctorRoot } from '../config/xdg.mjs';
15
16
 
16
17
  export const MODEL_VISIBILITY_MODES = ['all_configured', 'tier_defaults', 'explicit'];
17
18
 
@@ -36,7 +37,7 @@ function uniqueStrings(values = []) {
36
37
  }
37
38
 
38
39
  function cachePath(homeDir = os.homedir()) {
39
- return path.join(homeDir, '.cx', CACHE_FILENAME);
40
+ return path.join(doctorRoot(homeDir), CACHE_FILENAME);
40
41
  }
41
42
 
42
43
  export function resolveModelsConfig(projectConfig = {}) {
@@ -32,6 +32,7 @@ import { getModelVerdict } from '../ollama/capability-store.mjs';
32
32
  import { isLocalModel } from '../mcp/tool-budget.mjs';
33
33
  import { transportForProviderGroup, classifyCapabilityClass } from './behavior-matrix.mjs';
34
34
  import { resolveCapabilityTier, resolveModelOperatingProfile } from '../model-router.mjs';
35
+ import { doctorRoot } from '../config/xdg.mjs';
35
36
 
36
37
  export const EXECUTION_PROFILE_SCHEMA_VERSION = 1;
37
38
 
@@ -81,7 +82,7 @@ function hasVendorAdapter(modelId) {
81
82
 
82
83
  function loadOverridesFile(homeDir) {
83
84
  try {
84
- const raw = JSON.parse(readFileSync(join(homeDir, '.cx', OVERRIDES_FILENAME), 'utf8'));
85
+ const raw = JSON.parse(readFileSync(join(doctorRoot(homeDir), OVERRIDES_FILENAME), 'utf8'));
85
86
  if (raw && typeof raw === 'object' && raw.models && typeof raw.models === 'object') return raw.models;
86
87
  } catch { /* absent or malformed — no overrides */ }
87
88
  return {};
@@ -18,6 +18,7 @@ import { resolveFirstSecret } from '../providers/secret-resolver.mjs';
18
18
  import { getProviderModelCatalog } from '../model-router.mjs';
19
19
  import { listInstalledOllamaModels, toOllamaNativeModelId } from '../ollama/installed-models.mjs';
20
20
  import { getPricingForModels } from '../model-pricing.mjs';
21
+ import { doctorRoot } from '../config/xdg.mjs';
21
22
 
22
23
  const FETCH_TIMEOUT_MS = 10_000;
23
24
  const CACHE_TTL_MS = 10 * 60 * 1000;
@@ -37,7 +38,7 @@ const PROVIDER_LABELS = {
37
38
  const PROVIDER_ORDER = ['anthropic', 'openai', 'github-copilot', 'openrouter', 'ollama', 'local'];
38
39
 
39
40
  function cachePath(homeDir = os.homedir()) {
40
- return path.join(homeDir, '.cx', CACHE_FILENAME);
41
+ return path.join(doctorRoot(homeDir), CACHE_FILENAME);
41
42
  }
42
43
 
43
44
  async function fetchJson(url, { headers = {}, signal } = {}) {
@@ -8,7 +8,6 @@
8
8
 
9
9
  import { execFileSync } from 'node:child_process';
10
10
  import { join } from 'node:path';
11
- import { homedir } from 'node:os';
12
11
 
13
12
  // argv-array form for every shell-out: no shell interpolation, model names and
14
13
  // JSON bodies are passed as discrete arguments / stdin so user-supplied values
package/lib/op-log.mjs CHANGED
@@ -22,12 +22,13 @@ import path from 'node:path';
22
22
  import os from 'node:os';
23
23
 
24
24
  import { makeLogger, newRequestId } from './logger.mjs';
25
+ import { doctorRoot } from './config/xdg.mjs';
25
26
 
26
27
  const NOOP_LOGGER = Object.freeze({ debug() {}, info() {}, warn() {}, error() {} });
27
28
 
28
29
  export function startOpLog(op, { homeDir = os.homedir(), env = process.env, now = new Date() } = {}) {
29
30
  const id = newRequestId();
30
- const dir = path.join(homeDir, '.cx');
31
+ const dir = doctorRoot(homeDir);
31
32
  const stamp = now.toISOString().replace(/[:.]/g, '-');
32
33
  const logPath = path.join(dir, `${op}-${stamp}.log`);
33
34
 
@@ -28,6 +28,7 @@ import {
28
28
  import { syncModelPricing, refreshPricingCatalog } from "./telemetry/model-pricing-catalog.mjs";
29
29
  import { estimateUsageCost } from "./telemetry/model-pricing-catalog.mjs";
30
30
  import { buildSessionPrelude } from "./intake/session-prelude.mjs";
31
+ import { doctorRoot } from './config/xdg.mjs';
31
32
 
32
33
  const EFFICIENCY_SESSION_IDLE_RESET_MS = 2 * 60 * 60 * 1000;
33
34
  const EFFICIENCY_REPEATED_READ_THRESHOLD = 5;
@@ -94,7 +95,7 @@ const COOLDOWN_MS = 10 * 60 * 1000;
94
95
 
95
96
  function getStatePath(env = process.env) {
96
97
  const home = env.HOME || homedir();
97
- return join(home, ".cx", "construct-opencode-fallback.json");
98
+ return join(doctorRoot(home), "construct-opencode-fallback.json");
98
99
  }
99
100
 
100
101
  function flatten(value) {
@@ -520,7 +521,7 @@ export function buildRuntimeTracePayload(event, { env = process.env } = {}) {
520
521
 
521
522
  function efficiencyStorePath(env = process.env) {
522
523
  const home = env.HOME || homedir();
523
- return join(home, ".cx", "session-efficiency.json");
524
+ return join(doctorRoot(home), "session-efficiency.json");
524
525
  }
525
526
 
526
527
  function loadEfficiencyStats(nowIso, env = process.env) {
@@ -50,8 +50,11 @@ function collectBypassArtifacts(projectDir) {
50
50
 
51
51
  function collectReviewerGaps(projectDir) {
52
52
  const roots = [
53
+ path.join(projectDir, 'docs', 'specs', 'prd'),
53
54
  path.join(projectDir, 'docs', 'prd'),
55
+ path.join(projectDir, 'docs', 'decisions', 'adr'),
54
56
  path.join(projectDir, 'docs', 'adr'),
57
+ path.join(projectDir, 'docs', 'decisions', 'rfc'),
55
58
  path.join(projectDir, 'docs', 'rfc'),
56
59
  ];
57
60
  const gaps = [];
@@ -20,11 +20,12 @@ import {
20
20
  import { runOracleTick, listPending, approvePending, defaultRootDir } from './actions.mjs';
21
21
  import { collectOracleGaps, formatOracleGapsReport } from './gaps.mjs';
22
22
  import { reconcileOracleHygieneBeads } from './reconcile.mjs';
23
+ import { doctorRoot } from '../config/xdg.mjs';
23
24
 
24
25
  const MODULE_FILE = fileURLToPath(import.meta.url);
25
26
 
26
27
  function runtimeDir(homeDir) {
27
- return path.join(homeDir, '.cx', 'runtime', 'oracle');
28
+ return path.join(doctorRoot(homeDir), 'runtime', 'oracle');
28
29
  }
29
30
 
30
31
  async function cmdStart(args, { rootDir, projectDir, homeDir }) {
@@ -16,9 +16,10 @@ import { recordAndMaybeInvoke } from '../roles/gateway.mjs';
16
16
  import { validateCapabilityRegistry } from '../registry/validate.mjs';
17
17
  import { writeRoutingArtifact } from './dispatch.mjs';
18
18
  import { routeAction } from './routing.mjs';
19
+ import { doctorRoot } from '../config/xdg.mjs';
19
20
 
20
21
  function rolePendingPath(homeDir = homedir()) {
21
- const root = process.env.CONSTRUCT_ROLES_ROOT || path.join(homeDir, '.cx');
22
+ const root = process.env.CONSTRUCT_ROLES_ROOT || doctorRoot(homeDir);
22
23
  return path.join(root, 'role-pending.jsonl');
23
24
  }
24
25
 
@@ -40,7 +41,7 @@ async function dispatchSpecialist({ action, projectDir, homeDir, personaId, even
40
41
  });
41
42
 
42
43
  const prevRolesRoot = process.env.CONSTRUCT_ROLES_ROOT;
43
- process.env.CONSTRUCT_ROLES_ROOT = path.join(homeDir, '.cx');
44
+ process.env.CONSTRUCT_ROLES_ROOT = doctorRoot(homeDir);
44
45
 
45
46
  const event = {
46
47
  project: projectDir,
@@ -13,11 +13,12 @@ import { homedir } from 'node:os';
13
13
  import { createDaemon } from '../daemons/contract.mjs';
14
14
  import { memoryCapMbFor } from '../resources/process-budget.mjs';
15
15
  import { runOracleTick, defaultRootDir } from './actions.mjs';
16
+ import { doctorRoot } from '../config/xdg.mjs';
16
17
 
17
18
  export const KILLSWITCH_ENV = 'CONSTRUCT_ORACLE';
18
19
 
19
20
  function runtimeDir(homeDir = homedir()) {
20
- return path.join(homeDir, '.cx', 'runtime', 'oracle');
21
+ return path.join(doctorRoot(homeDir), 'runtime', 'oracle');
21
22
  }
22
23
 
23
24
  export function heartbeatPath(homeDir = homedir()) {
@@ -21,6 +21,7 @@ import { collectArtifactGateSignals } from './artifact-gate.mjs';
21
21
  import { loadGraph, nodesByType, dependentsOf } from '../graph/store.mjs';
22
22
  import { hashFiles } from '../graph/build-from-registry.mjs';
23
23
  import { readViolationSupersedeCutoff } from '../contracts/violation-log.mjs';
24
+ import { doctorRoot } from '../config/xdg.mjs';
24
25
 
25
26
  const RECENT_MS = 24 * 60 * 60 * 1000;
26
27
  const DOCTOR_LIMIT = 50;
@@ -128,7 +129,7 @@ function collectContractViolations(projectDir) {
128
129
  }
129
130
 
130
131
  function collectDoctorLog(homeDir) {
131
- const file = path.join(homeDir, '.cx', 'doctor-log.jsonl');
132
+ const file = path.join(doctorRoot(homeDir), 'doctor-log.jsonl');
132
133
  const since = Date.now() - RECENT_MS;
133
134
  const recent = readRecentJsonl(file, { since, limit: DOCTOR_LIMIT });
134
135
  return {
@@ -205,7 +206,7 @@ function collectBeadsOpenCount(projectDir) {
205
206
  }
206
207
 
207
208
  function collectHookFailures(homeDir) {
208
- const file = path.join(homeDir, '.cx', 'doctor-log.jsonl');
209
+ const file = path.join(doctorRoot(homeDir), 'doctor-log.jsonl');
209
210
  const since = Date.now() - RECENT_MS;
210
211
  const recent = readRecentJsonl(file, { since, limit: DOCTOR_LIMIT });
211
212
  const failures = recent.filter((r) =>
@@ -24,11 +24,12 @@
24
24
  */
25
25
  import fs from 'node:fs';
26
26
  import path from 'node:path';
27
- import os from 'node:os';
27
+
28
+ import { doctorRoot } from '../config/xdg.mjs';
28
29
 
29
30
  const WINDOW_DAYS = 7;
30
- const REVIEW_DIR = path.join(os.homedir(), '.cx', 'performance-reviews');
31
- const COST_LEDGER = path.join(os.homedir(), '.cx', 'session-cost.jsonl');
31
+ const REVIEW_DIR = path.join(doctorRoot(), 'performance-reviews');
32
+ const COST_LEDGER = path.join(doctorRoot(), 'session-cost.jsonl');
32
33
 
33
34
  function quantile(values, q) {
34
35
  if (!values.length) return null;
@@ -6,7 +6,7 @@
6
6
  * local-model provisioning, instructions-only). The host enumeration, the
7
7
  * displayName-to-id map, the hasNativeSubagents matrix, and the global hook /
8
8
  * MCP allowlists all derive from this one registry so sync and init read host
9
- * capabilities as data. See docs/adr/0033-platform-capability-registry.md.
9
+ * capabilities as data. See docs/decisions/adr/0033-platform-capability-registry.md.
10
10
  *
11
11
  * Hand-rolled validation (no AJV/zod) so this loads dependency-free on the
12
12
  * bootstrap paths that run before npm install — same constraint as
@@ -8,6 +8,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from
8
8
  import { homedir } from "node:os";
9
9
  import { dirname, extname, join, resolve } from "node:path";
10
10
  import { fileURLToPath } from "node:url";
11
+ import { configDir } from "./config/xdg.mjs";
11
12
 
12
13
  const __dirname = dirname(fileURLToPath(import.meta.url));
13
14
  export const MANIFEST_VERSION = 1;
@@ -123,7 +124,7 @@ export function resolvePluginDirs({ cwd = process.cwd(), homeDir = homedir(), en
123
124
  return [...new Set([
124
125
  join(cwd, ".cx", "plugins"),
125
126
  join(cwd, ".construct", "plugins"),
126
- join(homeDir, ".construct", "plugins"),
127
+ join(configDir(homeDir), "plugins"),
127
128
  ...extra,
128
129
  ].map((dir) => resolve(dir)))];
129
130
  }
@@ -41,7 +41,7 @@ function archiveDir(cwd, id) {
41
41
 
42
42
  const REQUIREMENTS_BRIEF = (id, displayName) => `# Profile requirements: ${displayName} (${id})
43
43
 
44
- This brief is a discovery and design contract for a new Construct profile. Methodology lives in \`docs/concepts/persona-research.md\` and lifecycle in \`docs/concepts/profile-lifecycle.md\`. Treat each section as a task to dispatch to the named specialist. A draft profile is not ready to promote until every section has evidence backing it. The aim is research-grounded, not opinion-grounded.
44
+ This brief is a discovery and design contract for a new Construct profile. Methodology lives in \`docs/guides/concepts/persona-research.md\` and lifecycle in \`docs/guides/concepts/profile-lifecycle.md\`. Treat each section as a task to dispatch to the named specialist. A draft profile is not ready to promote until every section has evidence backing it. The aim is research-grounded, not opinion-grounded.
45
45
 
46
46
  Specialists used here are the existing Construct cx-* personas (already in the registry). They are dispatched the standard way; this doc just names which questions each one owns.
47
47
 
@@ -145,7 +145,7 @@ const DRAFT_PROFILE_TEMPLATE = (id, displayName) => ({
145
145
  function personaTemplate(displayName) {
146
146
  return `# ${displayName}
147
147
 
148
- > Persona research artifact. Methodology: \`docs/concepts/persona-research.md\`. Fill from evidence; opinion is rejected at review.
148
+ > Persona research artifact. Methodology: \`docs/guides/concepts/persona-research.md\`. Fill from evidence; opinion is rejected at review.
149
149
 
150
150
  ## Goals
151
151
  - <What does success look like for this role?>
@@ -178,7 +178,7 @@ function personaTemplate(displayName) {
178
178
  function departmentTemplate(id, displayName) {
179
179
  return `# ${displayName} (${id})
180
180
 
181
- > Department charter. One paragraph: what this department owns, what it does not own, who it hands off to. Frame from organizational-design research (\`docs/concepts/persona-research.md\` § departmental structure).
181
+ > Department charter. One paragraph: what this department owns, what it does not own, who it hands off to. Frame from organizational-design research (\`docs/guides/concepts/persona-research.md\` § departmental structure).
182
182
 
183
183
  ## Charter
184
184
 
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync } from 'node:fs';
16
16
  import { join, resolve } from 'node:path';
17
+ import { configDir } from './config/xdg.mjs';
17
18
 
18
19
  // Detection rules. Each rule checks a filesystem signal and emits one or more
19
20
  // tags. Keep rules tight — false positives cascade into bad filter decisions.
@@ -430,7 +431,7 @@ export function enumerateInstalledSkills(homeDir = (process.env.HOME || process.
430
431
  // Codex
431
432
  join(homeDir, '.codex', 'skills'),
432
433
  // Construct native
433
- join(homeDir, '.construct', 'skills'),
434
+ join(configDir(homeDir), 'skills'),
434
435
  ];
435
436
  for (const dir of candidates) {
436
437
  try {