@geraldmaron/construct 1.2.0 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/README.md +8 -7
  2. package/bin/construct +206 -62
  3. package/commands/plan/decide.md +1 -1
  4. package/commands/remember/runbook.md +1 -1
  5. package/examples/README.md +1 -1
  6. package/examples/distribution/README.md +1 -1
  7. package/examples/distribution/sources/adr.md +2 -2
  8. package/examples/distribution/sources/deck-one-pager.md +1 -1
  9. package/examples/distribution/sources/prd-platform.md +1 -1
  10. package/examples/distribution/sources/research-brief.md +2 -2
  11. package/examples/distribution/sources/rfc-platform.md +1 -1
  12. package/examples/distribution/sources/runbook.md +2 -2
  13. package/examples/distribution/sources/strategy.md +1 -1
  14. package/lib/agent-instructions/inject.mjs +1 -1
  15. package/lib/artifact-manifest.mjs +150 -4
  16. package/lib/artifact-type-from-path.mjs +9 -5
  17. package/lib/artifact-workflow.mjs +252 -0
  18. package/lib/audit-specialists.mjs +1 -1
  19. package/lib/audit-trail.mjs +3 -3
  20. package/lib/auto-docs.mjs +13 -11
  21. package/lib/bootstrap/built-ins.mjs +3 -3
  22. package/lib/boundary.mjs +3 -2
  23. package/lib/brand-prose.mjs +1 -1
  24. package/lib/cache-governor.js +4 -4
  25. package/lib/certification/document-io-fixtures.mjs +1 -1
  26. package/lib/chat/config.mjs +2 -2
  27. package/lib/chat/desktop-binary.mjs +5 -3
  28. package/lib/chat/present.mjs +1 -0
  29. package/lib/cli-commands.mjs +23 -6
  30. package/lib/cli-service-inventory.mjs +42 -0
  31. package/lib/comment-lint.mjs +9 -5
  32. package/lib/config/alias.mjs +3 -2
  33. package/lib/config/intake-policy.mjs +139 -0
  34. package/lib/config/project-config.mjs +106 -7
  35. package/lib/config/schema.mjs +45 -6
  36. package/lib/config/source-targets.mjs +311 -0
  37. package/lib/config/xdg.mjs +63 -0
  38. package/lib/contracts/violation-log.mjs +2 -2
  39. package/lib/cost-ledger.mjs +4 -3
  40. package/lib/cost.mjs +3 -2
  41. package/lib/decisions/registry.mjs +3 -3
  42. package/lib/distill.mjs +2 -2
  43. package/lib/docs-verify.mjs +2 -0
  44. package/lib/doctor/audit.mjs +4 -3
  45. package/lib/doctor/cli.mjs +0 -1
  46. package/lib/doctor/index.mjs +4 -4
  47. package/lib/doctor/report.mjs +3 -3
  48. package/lib/doctor/source-checkout.mjs +16 -0
  49. package/lib/doctor/watchers/bd-watch.mjs +5 -5
  50. package/lib/doctor/watchers/cost.mjs +4 -4
  51. package/lib/doctor/watchers/disk.mjs +4 -3
  52. package/lib/doctor/watchers/service-health.mjs +5 -5
  53. package/lib/document-export.mjs +39 -17
  54. package/lib/document-extract.mjs +3 -3
  55. package/lib/document-ingest.mjs +13 -3
  56. package/lib/efficiency.mjs +2 -1
  57. package/lib/embed/approval-queue.mjs +3 -2
  58. package/lib/embed/auto-sources.mjs +44 -0
  59. package/lib/embed/cli.mjs +9 -6
  60. package/lib/embed/config.mjs +6 -5
  61. package/lib/embed/daemon.mjs +6 -5
  62. package/lib/embed/demand-fetch.mjs +50 -19
  63. package/lib/embed/inbox-live-watcher.mjs +1 -1
  64. package/lib/embed/inbox.mjs +52 -41
  65. package/lib/embed/intake-metrics.mjs +3 -2
  66. package/lib/embed/reconcile.mjs +1 -1
  67. package/lib/embed/semantic.mjs +3 -2
  68. package/lib/embed/supervision.mjs +4 -2
  69. package/lib/embed/worker.mjs +2 -1
  70. package/lib/embedded-contract/index.mjs +11 -0
  71. package/lib/engine/registry.mjs +3 -3
  72. package/lib/env-config.mjs +34 -12
  73. package/lib/export-branding.mjs +32 -0
  74. package/lib/features.mjs +4 -3
  75. package/lib/health-check.mjs +7 -6
  76. package/lib/hook-health.mjs +5 -4
  77. package/lib/hooks/_lib/log.mjs +4 -3
  78. package/lib/hooks/_lib/output-mode.mjs +2 -1
  79. package/lib/hooks/agent-tracker.mjs +3 -4
  80. package/lib/hooks/audit-reads.mjs +2 -2
  81. package/lib/hooks/audit-trail.mjs +2 -2
  82. package/lib/hooks/bash-output-logger.mjs +3 -3
  83. package/lib/hooks/brand-prose-lint.mjs +1 -1
  84. package/lib/hooks/ci-status-check.mjs +3 -2
  85. package/lib/hooks/context-watch.mjs +2 -2
  86. package/lib/hooks/context-window-recovery.mjs +5 -5
  87. package/lib/hooks/dep-audit.mjs +2 -2
  88. package/lib/hooks/doc-coupling-check.mjs +3 -2
  89. package/lib/hooks/edit-accumulator.mjs +3 -2
  90. package/lib/hooks/edit-guard.mjs +3 -3
  91. package/lib/hooks/guard-bash.mjs +2 -2
  92. package/lib/hooks/model-fallback.mjs +2 -1
  93. package/lib/hooks/policy-engine.mjs +4 -3
  94. package/lib/hooks/pre-compact.mjs +7 -7
  95. package/lib/hooks/readme-age-check.mjs +2 -2
  96. package/lib/hooks/session-optimize.mjs +4 -3
  97. package/lib/hooks/session-start.mjs +20 -16
  98. package/lib/hooks/stop-notify.mjs +9 -8
  99. package/lib/hooks/stop-typecheck.mjs +4 -3
  100. package/lib/hooks/test-watch.mjs +2 -2
  101. package/lib/host-disposition.mjs +1 -1
  102. package/lib/ingest/provider-extract.mjs +2 -1
  103. package/lib/init/detect-existing-structure.mjs +2 -2
  104. package/lib/init/doc-lanes.mjs +3 -10
  105. package/lib/init-docs.mjs +12 -54
  106. package/lib/init-unified.mjs +47 -92
  107. package/lib/init.mjs +7 -5
  108. package/lib/install/desktop-binary-download.mjs +5 -2
  109. package/lib/intake/constants.mjs +29 -0
  110. package/lib/intake/daemon.mjs +31 -8
  111. package/lib/intake/intake-config.mjs +32 -112
  112. package/lib/integrations/intake-integrations.mjs +8 -7
  113. package/lib/knowledge/rag.mjs +2 -2
  114. package/lib/knowledge/search.mjs +15 -15
  115. package/lib/maintenance/cleanup.mjs +14 -10
  116. package/lib/mcp/server.mjs +20 -2
  117. package/lib/mcp/tools/embedded-contract.mjs +14 -0
  118. package/lib/mcp/tools/project.mjs +2 -1
  119. package/lib/model-cheapest-provider.mjs +4 -3
  120. package/lib/model-pricing.mjs +3 -2
  121. package/lib/model-router.mjs +4 -3
  122. package/lib/models/catalog.mjs +2 -1
  123. package/lib/models/execution-capability-profile.mjs +2 -1
  124. package/lib/models/provider-poll.mjs +2 -1
  125. package/lib/ollama-manager.mjs +0 -1
  126. package/lib/op-log.mjs +2 -1
  127. package/lib/opencode-runtime-plugin.mjs +3 -2
  128. package/lib/oracle/artifact-gate.mjs +3 -0
  129. package/lib/oracle/cli.mjs +2 -1
  130. package/lib/oracle/execute.mjs +3 -2
  131. package/lib/oracle/index.mjs +2 -1
  132. package/lib/oracle/read-model.mjs +3 -2
  133. package/lib/performance/generate.mjs +4 -3
  134. package/lib/platforms/capabilities.mjs +1 -1
  135. package/lib/playwright-demo.mjs +2 -0
  136. package/lib/plugin-registry.mjs +2 -1
  137. package/lib/profiles/lifecycle.mjs +3 -3
  138. package/lib/project-profile.mjs +2 -1
  139. package/lib/project-root.mjs +9 -7
  140. package/lib/provider-capabilities.js +4 -3
  141. package/lib/providers/auth-manager.mjs +2 -1
  142. package/lib/providers/copilot-auth.mjs +3 -2
  143. package/lib/providers/creds.mjs +3 -2
  144. package/lib/providers/registry.mjs +3 -3
  145. package/lib/providers/secret-resolver.mjs +3 -2
  146. package/lib/read-tracker-store.mjs +2 -1
  147. package/lib/reconcile/mcp-entry-reconcile.mjs +2 -2
  148. package/lib/reflect.mjs +2 -1
  149. package/lib/registry/generate-docs.mjs +2 -2
  150. package/lib/roles/approval-surface.mjs +5 -4
  151. package/lib/roles/event-bus.mjs +0 -1
  152. package/lib/roles/gateway.mjs +6 -2
  153. package/lib/rules-delivery.mjs +1 -1
  154. package/lib/runtime-pressure.mjs +5 -3
  155. package/lib/sandbox.mjs +3 -2
  156. package/lib/scheduler/solo.mjs +6 -4
  157. package/lib/server/auth.mjs +4 -3
  158. package/lib/server/index.mjs +53 -26
  159. package/lib/server/insights.mjs +5 -4
  160. package/lib/server/rate-limit.mjs +5 -3
  161. package/lib/server/webhook.mjs +5 -3
  162. package/lib/service-manager.mjs +5 -3
  163. package/lib/setup.mjs +9 -8
  164. package/lib/status.mjs +6 -5
  165. package/lib/storage/embeddings-local.mjs +3 -2
  166. package/lib/storage/sync.mjs +2 -2
  167. package/lib/telemetry/client.mjs +0 -1
  168. package/lib/telemetry/intent-verifications.mjs +6 -6
  169. package/lib/telemetry/model-pricing-catalog.mjs +4 -3
  170. package/lib/telemetry/rule-calls.mjs +3 -3
  171. package/lib/telemetry/skill-calls.mjs +3 -3
  172. package/lib/template-registry.mjs +1 -0
  173. package/lib/templates/visual-requirements.mjs +1 -1
  174. package/lib/test-corpus-inventory.mjs +1 -1
  175. package/lib/uninstall/uninstall.mjs +12 -10
  176. package/package.json +3 -3
  177. package/platforms/claude/settings.template.json +43 -43
  178. package/rules/common/no-fabrication.md +1 -1
  179. package/rules/common/release-gates.md +1 -1
  180. package/rules/common/research.md +1 -1
  181. package/rules/common/review-before-change.md +1 -1
  182. package/scripts/sync-specialists.mjs +11 -8
  183. package/skills/docs/adr-workflow.md +3 -3
  184. package/skills/docs/init-docs.md +9 -9
  185. package/skills/docs/prd-workflow.md +5 -5
  186. package/skills/docs/product-intelligence-review.md +1 -1
  187. package/skills/docs/product-intelligence-workflow.md +2 -2
  188. package/skills/docs/product-signal-workflow.md +1 -1
  189. package/skills/docs/runbook-workflow.md +4 -4
  190. package/skills/exploration/unknown-codebase-onboarding.md +1 -1
  191. package/skills/operating/orchestration-reference.md +1 -1
  192. package/skills/routing.md +3 -3
  193. package/specialists/artifact-manifest.json +11 -2
  194. package/specialists/artifact-manifest.schema.json +31 -1
  195. package/specialists/prompts/cx-architect.md +1 -1
  196. package/specialists/prompts/cx-docs-keeper.md +1 -1
  197. package/specialists/prompts/cx-researcher.md +1 -1
  198. package/specialists/prompts/cx-sre.md +1 -1
  199. package/specialists/role-manifests.json +6 -6
  200. package/templates/distribution/construct-reference.docx +0 -0
  201. package/templates/docs/README.md +125 -0
  202. package/templates/docs/construct_guide.md +3 -4
  203. package/templates/docs/persona-artifact.md +1 -1
  204. package/templates/docs/prds/README.md +25 -0
  205. package/templates/docs/prds/templates/_template.md +206 -0
  206. package/templates/docs/prds/templates/meta-prd.template.md +177 -0
  207. package/templates/docs/prds/templates/prd-business.template.md +61 -0
  208. package/templates/docs/prds/templates/prd-platform.template.md +81 -0
  209. package/templates/docs/prds/templates/prfaq.template.md +38 -0
  210. package/templates/docs/rfcs/README.md +22 -0
  211. package/templates/docs/rfcs/templates/_template.md +58 -0
  212. package/templates/docs/rfcs/templates/rfc-platform.template.md +72 -0
  213. package/templates/homebrew/construct.rb +1 -1
  214. package/templates/workflows/new-feature.yml +8 -8
@@ -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
@@ -80,6 +80,8 @@ export function transcodeWebmToMp4(src, dest) {
80
80
  const result = spawnSync(ffmpeg, [
81
81
  '-y', '-i', src,
82
82
  '-c:v', 'libx264',
83
+ '-crf', '18',
84
+ '-preset', 'medium',
83
85
  '-pix_fmt', 'yuv420p',
84
86
  '-movflags', '+faststart',
85
87
  dest,
@@ -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 {
@@ -4,12 +4,12 @@
4
4
  * Writers that own per-project state (contract violations, audit reads, agent
5
5
  * dispatches, intent verifications) call `resolveProjectScope()` to decide
6
6
  * which `<project>/.cx/<file>.jsonl` to write to. When no project is
7
- * detected, the call returns `null` and writers fall back to the legacy
8
- * user-scope path (`~/.cx/<file>.jsonl`) so existing standalone hook
7
+ * detected, the call returns `null` and writers fall back to the global
8
+ * user-scope path (`<doctorRoot>/<file>.jsonl`) so existing standalone hook
9
9
  * invocations don't lose data.
10
10
  *
11
11
  * Writers that own cross-project telemetry (skill calls, role-pending,
12
- * session cost) keep using `~/.cx/` but tag each entry with the
12
+ * session cost) keep using the global doctor root but tag each entry with the
13
13
  * `projectId` from `resolveProjectId()` so a reader can attribute the
14
14
  * entry to a specific project later.
15
15
  *
@@ -24,6 +24,8 @@ import path from 'node:path';
24
24
  import os from 'node:os';
25
25
  import { createHash } from 'node:crypto';
26
26
 
27
+ import { doctorRoot } from './config/xdg.mjs';
28
+
27
29
  const HOME = os.homedir();
28
30
  const MARKERS = ['.cx', '.construct'];
29
31
  const cache = new Map(); // cwd → { projectRoot, projectId, scope } | null
@@ -84,13 +86,13 @@ export function resolveProjectScope(cwd = process.cwd()) {
84
86
 
85
87
  /**
86
88
  * For project-scoped writers: returns the `<project>/.cx/<basename>` path
87
- * when cwd is inside a Construct project, otherwise the legacy
88
- * `~/.cx/<basename>` path. Callers pass just the file basename; this helper
89
- * resolves the directory side. ensureDir=true creates the parent dir.
89
+ * when cwd is inside a Construct project, otherwise the global
90
+ * `<doctorRoot>/<basename>` path. Callers pass just the file basename; this
91
+ * helper resolves the directory side. ensureDir=true creates the parent dir.
90
92
  */
91
93
  export function resolveProjectScopedPath(basename, { cwd, ensureDir = true } = {}) {
92
94
  const scope = resolveProjectScope(cwd ?? process.cwd());
93
- const dir = scope ? scope.cxDir : path.join(HOME, '.cx');
95
+ const dir = scope ? scope.cxDir : doctorRoot();
94
96
  if (ensureDir && !fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
95
97
  return path.join(dir, basename);
96
98
  }
@@ -18,9 +18,10 @@
18
18
  */
19
19
  import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'node:fs';
20
20
  import { join } from 'node:path';
21
- import { homedir } from 'node:os';
22
21
 
23
- const CAPABILITY_CACHE_PATH = join(homedir(), '.cx', 'provider-capabilities.json');
22
+ import { doctorRoot } from './config/xdg.mjs';
23
+
24
+ const CAPABILITY_CACHE_PATH = join(doctorRoot(), 'provider-capabilities.json');
24
25
  const CAPABILITY_CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24h
25
26
 
26
27
  const ADAPTERS = {
@@ -110,7 +111,7 @@ function readCapabilityCache() {
110
111
 
111
112
  function writeCapabilityCache(capabilities) {
112
113
  try {
113
- mkdirSync(join(homedir(), '.cx'), { recursive: true });
114
+ mkdirSync(doctorRoot(), { recursive: true });
114
115
  writeFileSync(CAPABILITY_CACHE_PATH, JSON.stringify({
115
116
  fetchedAt: Date.now(),
116
117
  capabilities,
@@ -11,10 +11,11 @@
11
11
  import fs from 'node:fs';
12
12
  import path from 'node:path';
13
13
  import { homedir } from 'node:os';
14
+ import { configDir } from '../config/xdg.mjs';
14
15
 
15
16
  function authDir() {
16
17
  const home = process.env.HOME || homedir();
17
- const dir = path.join(home, '.construct', 'auth');
18
+ const dir = path.join(configDir(home), 'auth');
18
19
  try { fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); } catch { /* ignore */ }
19
20
  return dir;
20
21
  }
@@ -11,7 +11,7 @@
11
11
  * its token is rejected by the exchange endpoint.
12
12
  *
13
13
  * Credentials persist to two places: Construct's own store
14
- * (~/.construct/auth/github-copilot.json) and the de-facto standard
14
+ * (github-copilot.json under the XDG config dir auth/) and the de-facto standard
15
15
  * ~/.config/github-copilot/apps.json that other tools read and write, so a login
16
16
  * here is reusable elsewhere and an existing login is reused here. The access
17
17
  * token is refreshed from the refresh token when it expires; the session token
@@ -22,6 +22,7 @@
22
22
  import fs from 'node:fs';
23
23
  import path from 'node:path';
24
24
  import os from 'node:os';
25
+ import { configDir } from '../config/xdg.mjs';
25
26
 
26
27
  const CLIENT_ID = 'Iv1.b507a08c87ecfe98';
27
28
  const DEVICE_CODE_URL = 'https://github.com/login/device/code';
@@ -53,7 +54,7 @@ function homeDir() {
53
54
  }
54
55
 
55
56
  function constructStorePath() {
56
- return path.join(homeDir(), '.construct', 'auth', 'github-copilot.json');
57
+ return path.join(configDir(homeDir()), 'auth', 'github-copilot.json');
57
58
  }
58
59
 
59
60
  function appsStorePath() {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * lib/providers/creds.mjs — credential management for provider integrations.
3
3
  *
4
- * Reads and writes provider credentials stored in `~/.construct/config.env`.
4
+ * Reads and writes provider credentials stored in the user config.env (XDG config dir).
5
5
  * Each provider block is delimited by comment markers so the file can be
6
6
  * parsed without a full ini/dotenv library and can also be sourced by a shell.
7
7
  *
@@ -24,11 +24,12 @@
24
24
  import fs from 'node:fs';
25
25
  import os from 'node:os';
26
26
  import path from 'node:path';
27
+ import { configDir } from '../config/xdg.mjs';
27
28
 
28
29
  const CONFIG_FILE = 'config.env';
29
30
 
30
31
  function constructDir() {
31
- return path.join(os.homedir(), '.construct');
32
+ return configDir();
32
33
  }
33
34
 
34
35
  export function credsFilePath() {
@@ -4,7 +4,7 @@
4
4
  * Resolution order (last-write wins):
5
5
  *
6
6
  * 1. Built-in factories under `lib/providers/<id>/index.mjs` (loaded lazily).
7
- * 2. Operator overrides at `~/.construct/providers.json`:
7
+ * 2. Operator overrides at `providers.json` in the XDG config dir:
8
8
  * { "providers": [{ "id": "...", "package": "...", "options": {} }] }
9
9
  * 3. Project overrides at `<projectRoot>/.cx/providers.json` (same shape).
10
10
  *
@@ -21,11 +21,11 @@
21
21
  */
22
22
 
23
23
  import { readFileSync, existsSync } from 'node:fs';
24
- import { homedir } from 'node:os';
25
24
  import { join, isAbsolute, resolve, dirname } from 'node:path';
26
25
  import { fileURLToPath, pathToFileURL } from 'node:url';
27
26
  import { assertProviderContract } from './contract.mjs';
28
27
  import { getBreaker } from './circuit-breaker.mjs';
28
+ import { configDir } from '../config/xdg.mjs';
29
29
 
30
30
  const HERE = dirname(fileURLToPath(import.meta.url));
31
31
 
@@ -92,7 +92,7 @@ async function instantiate(mod, options) {
92
92
 
93
93
  function gatherOverrides(rootDir) {
94
94
  const out = [];
95
- const userConfig = readJsonIfExists(join(homedir(), '.construct', 'providers.json'));
95
+ const userConfig = readJsonIfExists(join(configDir(), 'providers.json'));
96
96
  if (userConfig?.providers) out.push(...userConfig.providers);
97
97
  const projectConfig = readJsonIfExists(join(rootDir, '.cx', 'providers.json'));
98
98
  if (projectConfig?.providers) out.push(...projectConfig.providers);
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Resolves a canonical environment variable (for example ANTHROPIC_API_KEY) by
5
5
  * walking the same sources the rest of Construct trusts, in priority order:
6
- * process env, ~/.construct/config.env, ~/.env, the project .env, then shell rc
6
+ * process env, the XDG config dir config.env, ~/.env, the project .env, then shell rc
7
7
  * files (.zshrc/.bashrc/.bash_profile/.profile). When the value carries a
8
8
  * 1Password reference — a bare `op://vault/item/field` or the shell form
9
9
  * `$(op read 'op://...')` — it is resolved through the `op` CLI and cached for
@@ -22,6 +22,7 @@ import path from 'node:path';
22
22
  import os from 'node:os';
23
23
  import { spawnSync } from 'node:child_process';
24
24
  import { discoverAlternateRawForVar } from './credential-sources.mjs';
25
+ import { configDir } from '../config/xdg.mjs';
25
26
 
26
27
  // The 1Password desktop integration prompts for biometric unlock on the first
27
28
  // read after the vault locks, and that interactive round-trip routinely takes
@@ -111,7 +112,7 @@ function readShellRcVar(varName, home) {
111
112
  function rawCandidate(varName, { env, cwd, home }) {
112
113
  const direct = env?.[varName];
113
114
  if (typeof direct === 'string' && direct.length > 0) return direct;
114
- const files = [path.join(home, '.construct', 'config.env'), path.join(home, '.env'), path.join(cwd, '.env')];
115
+ const files = [path.join(configDir(home), 'config.env'), path.join(home, '.env'), path.join(cwd, '.env')];
115
116
  for (const file of files) {
116
117
  const value = readDotenvVar(file, varName);
117
118
  if (value) return value;
@@ -9,6 +9,7 @@
9
9
  import { appendFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
10
10
  import { join } from 'node:path';
11
11
  import { homedir } from 'node:os';
12
+ import { doctorRoot } from './config/xdg.mjs';
12
13
 
13
14
  const SESSION_IDLE_RESET_MS = 2 * 60 * 60 * 1000;
14
15
  const REPEATED_READ_WARNING_THRESHOLD = 5;
@@ -20,7 +21,7 @@ const EFFICIENCY_LARGE_READ_LIMIT = 400;
20
21
 
21
22
  export function readTrackerPaths(env = process.env) {
22
23
  const home = env.CX_HOME_OVERRIDE || env.HOME || homedir();
23
- const cxDir = join(home, '.cx');
24
+ const cxDir = doctorRoot(home);
24
25
  return {
25
26
  cxDir,
26
27
  efficiencyStore: join(cxDir, 'session-efficiency.json'),
@@ -21,7 +21,7 @@ import fs from 'node:fs';
21
21
  import path from 'node:path';
22
22
  import { fileURLToPath } from 'node:url';
23
23
 
24
- import { homeDir } from '../paths.mjs';
24
+ import { homeDir, constructDir } from '../paths.mjs';
25
25
  import {
26
26
  getCodexConfigPath,
27
27
  buildCodexMcpEntry,
@@ -34,7 +34,7 @@ function escapeRegExp(value) {
34
34
  }
35
35
 
36
36
  function registryMcpServers() {
37
- const installed = path.join(homeDir(), '.construct', 'specialists', 'registry.json');
37
+ const installed = path.join(constructDir(), 'specialists', 'registry.json');
38
38
  const bundled = fileURLToPath(new URL('../../specialists/registry.json', import.meta.url));
39
39
  for (const src of [installed, bundled]) {
40
40
  try {
package/lib/reflect.mjs CHANGED
@@ -13,9 +13,10 @@ import { loadConstructEnv } from './env-config.mjs';
13
13
  import { KNOWLEDGE_ROOT, KNOWLEDGE_SUBDIRS, inferKnowledgeTarget } from './knowledge/layout.mjs';
14
14
  import { extractSessionObservation } from './reflect/extractor.mjs';
15
15
  import { readContextState, contextSummaryLine } from './context-state.mjs';
16
+ import { configDir } from './config/xdg.mjs';
16
17
 
17
18
  const HOME = process.env.HOME || process.env.USERPROFILE;
18
- const USER_ENV_PATH = join(HOME, '.construct', 'config.env');
19
+ const USER_ENV_PATH = join(configDir(HOME), 'config.env');
19
20
 
20
21
  /**
21
22
  * Main reflect command handler.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * lib/registry/generate-docs.mjs — generate reference docs from registry/capabilities.json.
3
3
  *
4
- * Writes docs/reference/capabilities.md so the human-readable capability matrix
4
+ * Writes docs/guides/reference/capabilities.md so the human-readable capability matrix
5
5
  * cannot drift from the machine-readable registry.
6
6
  */
7
7
 
@@ -62,7 +62,7 @@ export function generateCapabilitiesDoc({ rootDir = REPO_ROOT, write = true } =
62
62
  }
63
63
 
64
64
  const content = `${lines.join('\n')}\n`;
65
- const out = path.join(rootDir, 'docs', 'reference', 'capabilities.md');
65
+ const out = path.join(rootDir, 'docs', 'guides', 'reference', 'capabilities.md');
66
66
  if (!write) return { out, content, drift: fs.existsSync(out) ? fs.readFileSync(out, 'utf8') !== content : true };
67
67
  fs.mkdirSync(path.dirname(out), { recursive: true });
68
68
  fs.writeFileSync(out, content);