@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
@@ -1,13 +1,16 @@
1
1
  /**
2
2
  * lib/env-config.mjs — Read and write Construct environment configuration files.
3
3
  *
4
- * Provides helpers to load ~/.cx/env, merge with process.env, and persist
5
- * key/value pairs back to disk. Backs setup, model-router, and the MCP
6
- * server when they resolve API keys and model overrides without leaking secrets.
4
+ * Provides helpers to load the user config.env (XDG config dir) and a project
5
+ * .env, merge them with process.env, and persist key/value pairs back to disk.
6
+ * Project .env wins over user config.env, which wins over shell exports. Backs
7
+ * setup, model-router, and the MCP server when they resolve API keys and model
8
+ * overrides without leaking secrets.
7
9
  */
8
10
  import fs from 'node:fs';
9
11
  import os from 'node:os';
10
12
  import path from 'node:path';
13
+ import { configDir } from './config/xdg.mjs';
11
14
  import { API_KEY_CREDENTIALS } from './providers/credential-catalog.mjs';
12
15
  import { extractOpRef } from './providers/secret-resolver.mjs';
13
16
 
@@ -26,7 +29,7 @@ export function parseEnvFile(filePath) {
26
29
  }
27
30
 
28
31
  export function getUserConfigDir(homeDir = os.homedir()) {
29
- return path.join(homeDir, '.construct');
32
+ return configDir(homeDir);
30
33
  }
31
34
 
32
35
  export function getUserEnvPath(homeDir = os.homedir()) {
@@ -109,31 +112,50 @@ function preferInjectedCredential(envValue, fileValue) {
109
112
  export function loadConstructEnv({ rootDir, homeDir = os.homedir(), env = process.env, warn = true } = {}) {
110
113
  const rootEnv = rootDir ? parseEnvFile(path.join(rootDir, '.env')) : {};
111
114
  const userEnv = parseEnvFile(getUserEnvPath(homeDir));
112
- const fileEnv = { ...rootEnv, ...userEnv };
115
+
116
+ // Most-specific config wins: project .env overrides user config.env, which
117
+ // overrides shell exports. A repo's local .env is closer to the work than a
118
+ // machine-wide default, so it takes the file-tier precedence.
119
+ const fileEnv = { ...userEnv, ...rootEnv };
120
+ const userEnvPath = getUserEnvPath(homeDir);
113
121
 
114
122
  if (warn) {
123
+ // Every key that both files define to a different value is a real shadow:
124
+ // project .env silently wins, so surface it rather than gating on a
125
+ // hardcoded credential subset.
126
+ for (const key of Object.keys(rootEnv)) {
127
+ if (!(key in userEnv)) continue;
128
+ if (rootEnv[key] === userEnv[key]) continue;
129
+ if (preferInjectedCredential(rootEnv[key], userEnv[key])) continue;
130
+ process.stderr.write(
131
+ `[construct] WARNING: ${key} is set in both project .env (${rootEnv[key].slice(0, 6)}…) ` +
132
+ `and ${userEnvPath} (${userEnv[key].slice(0, 6)}…). ` +
133
+ `Project .env wins. To silence: remove the key from one file.\n`,
134
+ );
135
+ }
136
+
115
137
  for (const key of Object.keys(env)) {
116
138
  if (ENV_SHADOW_IGNORE.has(key)) continue;
117
139
  if (!ENV_SHADOW_WARN.has(key)) continue;
118
140
  // Only warn when process.env has a non-empty competing value.
119
141
  // An empty-string env var (e.g. Claude for Desktop clears ANTHROPIC_API_KEY
120
- // so its internal auth is used instead) is not a real conflict — config.env wins
121
- // silently and no user action is needed.
142
+ // so its internal auth is used instead) is not a real conflict — the file
143
+ // value wins silently and no user action is needed.
122
144
  if (!env[key]) continue;
123
145
  if (key in fileEnv && fileEnv[key] !== env[key]) {
124
146
  if (preferInjectedCredential(env[key], fileEnv[key])) continue;
125
147
  process.stderr.write(
126
148
  `[construct] WARNING: process.env.${key} (${env[key].slice(0, 6)}…) ` +
127
- `differs from config.env value (${fileEnv[key].slice(0, 6)}…). ` +
128
- `config.env will be used. To silence: unset the shell variable or ` +
129
- `update ~/.construct/config.env to match.\n`,
149
+ `differs from the config file value (${fileEnv[key].slice(0, 6)}…). ` +
150
+ `The config file will be used. To silence: unset the shell variable or ` +
151
+ `update the config file to match.\n`,
130
152
  );
131
153
  }
132
154
  }
133
155
  }
134
156
 
135
- // process.env is the base; config.env (file) values override shell values
136
- // so that credentials set in the dashboard always win over stale shell exports.
157
+ // process.env is the base; .env-file values override shell values so that
158
+ // credentials set in the dashboard always win over stale shell exports.
137
159
  // Materialized credentials from `op run` (or an explicit export) win over stored
138
160
  // op:// references so one 1Password auth per invocation is enough.
139
161
  const merged = { ...env, ...fileEnv };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * lib/export-branding.mjs — one honest branding contract for distribution exports.
3
+ */
4
+
5
+ import { BRAND_CAPABLE_FORMATS, PLAIN_OUTPUT_FORMATS } from './artifact-manifest.mjs';
6
+
7
+ export const EXPORT_BRANDING_CAPABILITIES = Object.freeze({
8
+ pdf: { capable: true, mechanism: 'typst-template', asset: 'construct-pdf.typ' },
9
+ html: { capable: true, mechanism: 'pandoc-template', asset: 'construct-web.html' },
10
+ deck: { capable: true, mechanism: 'pandoc-template', asset: 'construct-deck.html' },
11
+ pptx: { capable: true, mechanism: 'native-pptx', asset: 'pptxgenjs' },
12
+ docx: { capable: true, mechanism: 'reference-doc', asset: 'construct-reference.docx' },
13
+ doc: { capable: true, mechanism: 'docx-reference-via-libreoffice', asset: 'construct-reference.docx' },
14
+ rtf: { capable: true, mechanism: 'pandoc-writer', asset: null },
15
+ odt: { capable: true, mechanism: 'pandoc-writer', asset: null },
16
+ epub: { capable: true, mechanism: 'pandoc-writer', asset: null },
17
+ tex: { capable: true, mechanism: 'pandoc-writer', asset: null },
18
+ txt: { capable: false, mechanism: 'plain-source', asset: null },
19
+ md: { capable: false, mechanism: 'plain-source', asset: null },
20
+ mdx: { capable: false, mechanism: 'plain-source', asset: null },
21
+ });
22
+
23
+ export function resolveExportBranding(format, requested = 'construct') {
24
+ const capability = EXPORT_BRANDING_CAPABILITIES[format];
25
+ if (!capability) return { requested, applied: 'none', capable: false, reason: 'unsupported format' };
26
+ if (!BRAND_CAPABLE_FORMATS.includes(format) || PLAIN_OUTPUT_FORMATS.includes(format)) {
27
+ return { requested, applied: 'none', capable: false, reason: 'format has no styling surface' };
28
+ }
29
+ if (requested === 'plain') return { requested, applied: 'plain', capable: true, mechanism: capability.mechanism, reason: 'explicit opt-out' };
30
+ return { requested: 'construct', applied: 'construct', capable: true, mechanism: capability.mechanism, asset: capability.asset, reason: 'default policy' };
31
+ }
32
+
package/lib/features.mjs CHANGED
@@ -9,13 +9,14 @@ import { readFileSync, existsSync, readdirSync } from 'fs';
9
9
  import { homedir } from 'os';
10
10
  import { join } from 'path';
11
11
  import { loadPluginRegistry } from './plugin-registry.mjs';
12
+ import { configDir } from './config/xdg.mjs';
12
13
 
13
14
  function getHomeDir(overrides = {}) {
14
15
  return overrides.homeDir ?? homedir();
15
16
  }
16
17
 
17
18
  function getFeaturesFile(overrides = {}) {
18
- return overrides.featuresFile ?? join(getHomeDir(overrides), '.construct', 'features.json');
19
+ return overrides.featuresFile ?? join(configDir(getHomeDir(overrides)), 'features.json');
19
20
  }
20
21
 
21
22
  function getClaudeSettingsPath(overrides = {}) {
@@ -145,10 +146,10 @@ function getProjectMcpPaths(overrides = {}) {
145
146
  * Load Claude.ai server-side managed MCP IDs.
146
147
  * These are integrations connected through Claude.ai's web interface
147
148
  * (e.g., Notion, Google Drive) that aren't discoverable from local config.
148
- * Users declare them via ~/.construct/claude-ai-mcps.json: { "mcps": ["notion", "slack"] }
149
+ * Users declare them via claude-ai-mcps.json in the XDG config dir: { "mcps": ["notion", "slack"] }
149
150
  */
150
151
  function getClaudeAiManagedMcps(overrides = {}) {
151
- const p = overrides.claudeAiMcpsPath ?? join(getHomeDir(overrides), '.construct', 'claude-ai-mcps.json');
152
+ const p = overrides.claudeAiMcpsPath ?? join(configDir(getHomeDir(overrides)), 'claude-ai-mcps.json');
152
153
  if (!existsSync(p)) return [];
153
154
  try {
154
155
  const data = JSON.parse(readFileSync(p, 'utf8'));
@@ -11,6 +11,7 @@ import os from 'node:os';
11
11
  import path from 'node:path';
12
12
  import { existsSync, readFileSync } from 'node:fs';
13
13
  import { extractOpRef, hasAnySecret, hasSecret } from './providers/secret-resolver.mjs';
14
+ import { configDir } from './config/xdg.mjs';
14
15
 
15
16
  const isWindows = os.platform() === 'win32';
16
17
 
@@ -110,8 +111,8 @@ export async function checkPrerequisites(options = {}) {
110
111
  });
111
112
 
112
113
  // Check user config
113
- const hasUserConfig = existsSync(path.join(homeDir || os.homedir(), '.construct', 'config.env')) ||
114
- existsSync(path.join(homeDir || os.homedir(), '.construct'));
114
+ const hasUserConfig = existsSync(path.join(configDir(homeDir || os.homedir()), 'config.env')) ||
115
+ existsSync(configDir(homeDir || os.homedir()));
115
116
  checks.push({
116
117
  name: 'User config',
117
118
  required: false,
@@ -176,7 +177,7 @@ function readCredentialRaw(varName, homeDir) {
176
177
  for (const envPath of [
177
178
  path.join(process.cwd(), '.env'),
178
179
  path.join(homeDir, '.env'),
179
- path.join(homeDir, '.construct', 'config.env'),
180
+ path.join(configDir(homeDir), 'config.env'),
180
181
  ]) {
181
182
  if (!existsSync(envPath)) continue;
182
183
  try {
@@ -222,7 +223,7 @@ export function describeCredentialPresence(varName, { homeDir = os.homedir(), cw
222
223
  for (const envPath of [
223
224
  path.join(cwd, '.env'),
224
225
  path.join(homeDir, '.env'),
225
- path.join(homeDir, '.construct', 'config.env'),
226
+ path.join(configDir(homeDir), 'config.env'),
226
227
  ]) {
227
228
  if (!existsSync(envPath)) continue;
228
229
  try {
@@ -295,7 +296,7 @@ const CREDENTIAL_VARS = [
295
296
  ];
296
297
 
297
298
  /**
298
- * Link discovered credential references into ~/.construct/config.env without
299
+ * Link discovered credential references into the XDG config dir config.env without
299
300
  * invoking `op read`. Plain values and op:// refs are persisted as found.
300
301
  * @param {Object} options
301
302
  * @param {string} options.homeDir
@@ -342,7 +343,7 @@ export function detectCredentials(options = {}) {
342
343
  // GitHub Copilot: a device-flow credential (Construct's own store or the shared
343
344
  // github-copilot store) is the usable signal; the VS Code extension and the
344
345
  // copilot CLI are softer fallbacks.
345
- const copilotAuthStore = existsSync(path.join(homeDir, '.construct', 'auth', 'github-copilot.json')) ||
346
+ const copilotAuthStore = existsSync(path.join(configDir(homeDir), 'auth', 'github-copilot.json')) ||
346
347
  existsSync(path.join(homeDir, '.config', 'github-copilot', 'apps.json')) ||
347
348
  existsSync(path.join(homeDir, '.config', 'github-copilot', 'hosts.json'));
348
349
  const copilotDir = path.join(homeDir, '.vscode', 'extensions');
@@ -15,8 +15,9 @@ import fs from 'node:fs';
15
15
  import path from 'node:path';
16
16
  import { spawnSync } from 'node:child_process';
17
17
  import { homedir } from 'node:os';
18
+ import { configDir, doctorRoot } from './config/xdg.mjs';
18
19
 
19
- const CX_DIR = path.join(homedir(), '.cx');
20
+ const CX_DIR = doctorRoot();
20
21
  const HOOK_HEALTH_DIR = path.join(CX_DIR, 'hook-health');
21
22
  const HEALTH_CHECK_INTERVAL_MS = 60 * 1000; // 1 minute
22
23
 
@@ -266,12 +267,12 @@ async function checkSingleHook(hookId, config, hookPath) {
266
267
  }
267
268
 
268
269
  function findHookPath(hookName) {
269
- // Check standard locations
270
+ // The user hook symlink lives in the XDG config dir; the project-local
271
+ // .construct/lib and the npm-global install are fallbacks.
270
272
  const possiblePaths = [
271
- path.join(homedir(), '.construct', 'lib', 'hooks', `${hookName}.mjs`),
273
+ path.join(configDir(), 'lib', 'hooks', `${hookName}.mjs`),
272
274
  path.join(process.cwd(), '.construct', 'lib', 'hooks', `${hookName}.mjs`),
273
275
  path.join(homedir(), '.npm-global', 'lib', 'node_modules', '@geraldmaron/construct', 'lib', 'hooks', `${hookName}.mjs`),
274
- // Try to find from PATH
275
276
  ];
276
277
 
277
278
  for (const p of possiblePaths) {
@@ -5,7 +5,7 @@
5
5
  * a regression in the hook harness or a malformed payload does not block the
6
6
  * user. The trade-off is silent failure: until now, hook regressions were
7
7
  * invisible. This helper writes a single JSONL entry per failure to
8
- * ~/.cx/hook-failures.jsonl so `construct doctor` can surface the top
8
+ * <doctorRoot>/hook-failures.jsonl so `construct doctor` can surface the top
9
9
  * failing hooks without changing the fail-open behavior of any individual
10
10
  * hook.
11
11
  *
@@ -21,9 +21,10 @@
21
21
 
22
22
  import { appendFileSync, statSync, renameSync, mkdirSync } from 'node:fs';
23
23
  import { join } from 'node:path';
24
- import { homedir } from 'node:os';
25
24
 
26
- const LOG_DIR = join(homedir(), '.cx');
25
+ import { doctorRoot } from '../../config/xdg.mjs';
26
+
27
+ const LOG_DIR = doctorRoot();
27
28
  const LOG_PATH = join(LOG_DIR, 'hook-failures.jsonl');
28
29
  const LOG_ROTATED_PATH = join(LOG_DIR, 'hook-failures.1.jsonl');
29
30
  const LOG_MAX_BYTES = 1_048_576; // 1 MiB
@@ -29,6 +29,7 @@ import { homedir } from 'node:os';
29
29
 
30
30
  import { resolveSetting, loadProjectConfig } from '../../config/project-config.mjs';
31
31
  import { HOOK_OUTPUT_MODES } from '../../config/schema.mjs';
32
+ import { doctorRoot } from '../../config/xdg.mjs';
32
33
 
33
34
  export { HOOK_OUTPUT_MODES };
34
35
 
@@ -93,7 +94,7 @@ export function writeHookContext({ payload, mode, homeDir = homedir(), stdout =
93
94
  if (mode === 'stdout') { stdout.write(payload); return 'stdout'; }
94
95
  if (mode === 'stderr') { stderr.write(payload); return 'stderr'; }
95
96
  try {
96
- const dir = join(homeDir, '.cx');
97
+ const dir = doctorRoot(homeDir);
97
98
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
98
99
  writeFileSync(join(dir, 'session-start-last.log'), payload);
99
100
  } catch { /* best effort — never block the session on a debug-log write */ }
@@ -24,10 +24,10 @@
24
24
  */
25
25
  import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
26
26
  import { join } from 'node:path';
27
- import { homedir } from 'node:os';
28
27
  import { appendBounded } from '../logging/rotate.mjs';
29
28
  import { resolveProjectScopedPath } from '../project-root.mjs';
30
29
  import { emit as emitRoleEvent } from '../roles/event-bus.mjs';
30
+ import { doctorRoot } from '../config/xdg.mjs';
31
31
  import { logHookFailure } from './_lib/log.mjs';
32
32
 
33
33
  let input = {};
@@ -74,8 +74,7 @@ if (errorIndicators.some(ind => lowerResult.includes(ind))) {
74
74
 
75
75
  // Record agent invocation
76
76
  try {
77
- const home = homedir();
78
- const cxDir = join(home, '.cx');
77
+ const cxDir = doctorRoot();
79
78
  mkdirSync(cxDir, { recursive: true });
80
79
 
81
80
  // Update last-agent files for coordination (shared + per-agent)
@@ -188,7 +187,7 @@ try {
188
187
  // role-pending stays at user scope (a handoff might span projects),
189
188
  // but each entry gets a projectId tag so a reader can attribute it.
190
189
 
191
- const pendingPath = join(homedir(), '.cx', 'role-pending.jsonl');
190
+ const pendingPath = join(doctorRoot(), 'role-pending.jsonl');
192
191
  const { resolveProjectScope } = await import('../project-root.mjs');
193
192
  const scope = resolveProjectScope();
194
193
  const bdMatch = /\b(construct-[a-z0-9]+)\b/i.exec(resultText) || [];
@@ -33,13 +33,13 @@
33
33
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
34
34
  import { createHash } from 'node:crypto';
35
35
  import { join, resolve, dirname } from 'node:path';
36
- import { homedir } from 'node:os';
37
36
  import { logHookFailure } from './_lib/log.mjs';
38
37
  import { appendBounded, readLastLineAcrossSegments } from '../logging/rotate.mjs';
39
38
  import { resolveProjectScopedPath } from '../project-root.mjs';
40
39
  import { flushReadTrackerDeltas, recordReadDelta } from '../read-tracker-store.mjs';
40
+ import { doctorRoot } from '../config/xdg.mjs';
41
41
 
42
- const CX_DIR = join(homedir(), '.cx');
42
+ const CX_DIR = doctorRoot();
43
43
  // audit-reads.jsonl is PROJECT-SCOPED — reads happen in a specific project,
44
44
  // not across the user's whole machine. resolveProjectScopedPath returns
45
45
  // <project>/.cx/audit-reads.jsonl when the hook fires from inside a
@@ -36,12 +36,12 @@
36
36
  import { readFileSync, existsSync } from 'node:fs';
37
37
  import { createHash } from 'node:crypto';
38
38
  import { join, resolve } from 'node:path';
39
- import { homedir } from 'node:os';
40
39
  import { appendAuditRecord } from '../audit-trail.mjs';
41
40
  import { resolveProjectScopedPath } from '../project-root.mjs';
42
41
  import { logHookFailure } from './_lib/log.mjs';
42
+ import { doctorRoot } from '../config/xdg.mjs';
43
43
 
44
- const CX_DIR = join(homedir(), '.cx');
44
+ const CX_DIR = doctorRoot();
45
45
  const AUDIT_FILE = resolveProjectScopedPath('audit-trail.jsonl', { ensureDir: false });
46
46
  const LAST_AGENT = join(CX_DIR, 'last-agent.json');
47
47
  const MUTATING_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit', 'Bash']);
@@ -21,12 +21,12 @@
21
21
  */
22
22
  import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
23
23
  import { join } from 'node:path';
24
- import { homedir } from 'node:os';
25
24
  import { appendBounded } from '../logging/rotate.mjs';
25
+ import { doctorRoot } from '../config/xdg.mjs';
26
26
 
27
27
  const SIZE_THRESHOLD_CHARS = 4000;
28
- const LOG_DIR = join(homedir(), '.cx', 'bash-logs');
29
- const WARN_FLAGS = join(homedir(), '.cx', 'warn-flags.txt');
28
+ const LOG_DIR = join(doctorRoot(), 'bash-logs');
29
+ const WARN_FLAGS = join(doctorRoot(), 'warn-flags.txt');
30
30
 
31
31
  let input = {};
32
32
  try { input = JSON.parse(readFileSync(0, 'utf8')); } catch { process.exit(0); }
@@ -33,6 +33,6 @@ if (!result.violations.length) process.exit(0);
33
33
  const output = formatBrandViolations([result]);
34
34
  process.stderr.write(`${output}\n`);
35
35
  process.stderr.write(
36
- '\nBrand policy blocked this edit (marketing voice, retired fonts, or Construct/cli naming). See docs/reference/branding.md and docs/STYLE.md.\n',
36
+ '\nBrand policy blocked this edit (marketing voice, retired fonts, or Construct/cli naming). See docs/guides/reference/branding.md and docs/STYLE.md.\n',
37
37
  );
38
38
  process.exit(2);
@@ -27,11 +27,12 @@
27
27
 
28
28
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
29
29
  import { execSync, spawn } from 'node:child_process';
30
- import { homedir } from 'node:os';
31
30
  import { join, dirname, resolve } from 'node:path';
32
31
  import { fileURLToPath } from 'node:url';
33
32
 
34
- const CACHE_PATH = join(homedir(), '.cx', 'ci-status-cache.json');
33
+ import { doctorRoot } from '../config/xdg.mjs';
34
+
35
+ const CACHE_PATH = join(doctorRoot(), 'ci-status-cache.json');
35
36
  const CACHE_TTL_MS = 60_000;
36
37
  const HERE = dirname(fileURLToPath(import.meta.url));
37
38
  const REFRESHER = resolve(HERE, '..', '..', 'scripts', 'refresh-ci-status.mjs');
@@ -33,10 +33,10 @@
33
33
  */
34
34
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
35
35
  import { join } from 'node:path';
36
- import { homedir } from 'node:os';
37
36
  import { resolveContextWindow } from '../telemetry/model-pricing-catalog.mjs';
37
+ import { doctorRoot } from '../config/xdg.mjs';
38
38
 
39
- const CX_DIR = join(homedir(), '.cx');
39
+ const CX_DIR = doctorRoot();
40
40
  const COST_LOG = join(CX_DIR, 'session-cost.jsonl');
41
41
  const EFFICIENCY = join(CX_DIR, 'session-efficiency.json');
42
42
  const STATE = join(CX_DIR, 'context-watch-state.json');
@@ -12,9 +12,9 @@
12
12
  * @exits 0 = pass
13
13
  */
14
14
  import { readFileSync, writeFileSync, mkdirSync } from 'fs';
15
- import { homedir } from 'os';
16
15
  import { join } from 'path';
17
16
  import { readContextState, writeContextState } from '../context-state.mjs';
17
+ import { doctorRoot } from '../config/xdg.mjs';
18
18
 
19
19
  const CONTEXT_LIMIT_PATTERNS = [
20
20
  'context window', 'maximum context', 'too long', 'prompt is too long',
@@ -23,7 +23,7 @@ const CONTEXT_LIMIT_PATTERNS = [
23
23
  ];
24
24
 
25
25
  const COOLDOWN_MS = 10 * 60 * 1000;
26
- const STATE_PATH = join(homedir(), '.cx', 'context-recovery.json');
26
+ const STATE_PATH = join(doctorRoot(), 'context-recovery.json');
27
27
 
28
28
  let input = {};
29
29
  try { input = JSON.parse(readFileSync(0, 'utf8')); } catch { process.exit(0); }
@@ -42,7 +42,7 @@ try {
42
42
  if (now - (state.lastTriggeredAt || 0) < COOLDOWN_MS) process.exit(0);
43
43
  } catch { /* first run */ }
44
44
 
45
- const cxDir = join(homedir(), '.cx');
45
+ const cxDir = doctorRoot();
46
46
  try { mkdirSync(cxDir, { recursive: true }); } catch { /* exists */ }
47
47
  try { writeFileSync(STATE_PATH, JSON.stringify({ lastTriggeredAt: now })); } catch { /* best effort */ }
48
48
 
@@ -50,7 +50,7 @@ const cwd = input?.cwd || process.cwd();
50
50
 
51
51
  let existingContext = '';
52
52
  try {
53
- const state = readContextState(cwd) || readContextState(homedir());
53
+ const state = readContextState(cwd) || readContextState(doctorRoot());
54
54
  if (state?.markdown) existingContext = String(state.markdown).slice(0, 800);
55
55
  else if (state?.context) existingContext = String(state.context).slice(0, 800);
56
56
  } catch { /* none */ }
@@ -66,7 +66,7 @@ const recoveryContext = [
66
66
  const projectCxDir = join(cwd, '.cx');
67
67
  try { mkdirSync(projectCxDir, { recursive: true }); } catch { /* exists */ }
68
68
  try { writeContextState(cwd, { source: 'context-window-recovery', recoveryContext, format: 'json' }, { markdown: recoveryContext }); } catch { /* best effort */ }
69
- try { writeContextState(homedir(), { source: 'context-window-recovery', recoveryContext, format: 'json' }, { markdown: recoveryContext }); } catch { /* best effort */ }
69
+ try { writeContextState(doctorRoot(), { source: 'context-window-recovery', recoveryContext, format: 'json' }, { markdown: recoveryContext }); } catch { /* best effort */ }
70
70
 
71
71
  process.stdout.write([
72
72
  ``,
@@ -15,10 +15,10 @@
15
15
  */
16
16
  import { readFileSync, existsSync } from 'node:fs';
17
17
  import { execSync } from 'node:child_process';
18
- import { homedir } from 'node:os';
19
18
  import { join, dirname, basename } from 'node:path';
20
19
  import { appendBounded } from '../logging/rotate.mjs';
21
20
  import { logHookFailure } from './_lib/log.mjs';
21
+ import { doctorRoot } from '../config/xdg.mjs';
22
22
 
23
23
  const filePath = process.env.TOOL_INPUT_FILE_PATH || '';
24
24
  if (!filePath) process.exit(0);
@@ -31,7 +31,7 @@ if (!MANIFESTS.has(file)) process.exit(0);
31
31
  if (/node_modules|\/dist\/|\/build\/|\/\.git\//.test(filePath)) process.exit(0);
32
32
 
33
33
  const projectDir = dirname(filePath);
34
- const warnFlagsPath = join(homedir(), '.cx', 'warn-flags.txt');
34
+ const warnFlagsPath = join(doctorRoot(), 'warn-flags.txt');
35
35
 
36
36
  function appendWarn(msg) {
37
37
  try { appendBounded('bash-warn-flags', warnFlagsPath, msg + '\n'); }
@@ -19,10 +19,11 @@
19
19
  */
20
20
 
21
21
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
22
- import { homedir } from 'node:os';
23
22
  import { join, dirname } from 'node:path';
24
23
 
25
- const STATE_PATH = join(homedir(), '.cx', 'doc-coupling.json');
24
+ import { doctorRoot } from '../config/xdg.mjs';
25
+
26
+ const STATE_PATH = join(doctorRoot(), 'doc-coupling.json');
26
27
  const CODE_RE = /^(lib|bin|src|app)\/.*\.(?:m?js|jsx?|tsx?|cjs)$/;
27
28
  const DOC_RE = /^(?:CHANGELOG\.md|docs\/.+\.md|\.cx\/context\.(?:md|json))$/;
28
29
  const THRESHOLDS = [3, 5, 10];
@@ -20,6 +20,7 @@ import { homedir } from 'node:os';
20
20
  import path from 'node:path';
21
21
  import { appendBounded } from '../logging/rotate.mjs';
22
22
  import { logHookFailure } from './_lib/log.mjs';
23
+ import { doctorRoot } from '../config/xdg.mjs';
23
24
 
24
25
  const TS_EXTS = new Set(['.ts','.tsx','.js','.mjs','.jsx','.cjs']);
25
26
  const filePath = process.env.TOOL_INPUT_FILE_PATH || '';
@@ -28,7 +29,7 @@ if (!filePath) process.exit(0);
28
29
 
29
30
  const ext = path.extname(filePath);
30
31
  const home = homedir();
31
- const countPath = path.join(home, '.cx', 'files-changed-count.txt');
32
+ const countPath = path.join(doctorRoot(home), 'files-changed-count.txt');
32
33
 
33
34
  // Always increment the total files-changed counter for notify
34
35
  try {
@@ -41,7 +42,7 @@ try {
41
42
  // Only accumulate TS/JS files for typecheck
42
43
  if (!TS_EXTS.has(ext)) process.exit(0);
43
44
 
44
- const tcPath = path.join(home, '.cx', 'pending-typecheck.txt');
45
+ const tcPath = path.join(doctorRoot(home), 'pending-typecheck.txt');
45
46
  try {
46
47
  const existing = existsSync(tcPath) ? readFileSync(tcPath, 'utf8').split('\n').filter(Boolean) : [];
47
48
  if (!existing.includes(filePath)) {
@@ -14,10 +14,10 @@
14
14
  import { readFileSync, existsSync } from 'fs';
15
15
  import { createHash } from 'crypto';
16
16
  import { join, resolve } from 'path';
17
- import { homedir } from 'os';
18
17
  import { logHookFailure } from './_lib/log.mjs';
18
+ import { doctorRoot } from '../config/xdg.mjs';
19
19
 
20
- const HASH_STORE = join(homedir(), '.cx', 'file-hashes.json');
20
+ const HASH_STORE = join(doctorRoot(), 'file-hashes.json');
21
21
 
22
22
  function storedHash(absPath) {
23
23
  try {
@@ -48,7 +48,7 @@ function absOf(p) { return p.startsWith('/') ? p : resolve(cwd, p); }
48
48
 
49
49
  if ((toolName === 'Edit' || toolName === 'Write' || toolName === 'MultiEdit') && process.env.CONSTRUCT_ROLES !== 'off') {
50
50
  try {
51
- const lastAgentPath = join(homedir(), '.cx', 'last-agent.json');
51
+ const lastAgentPath = join(doctorRoot(), 'last-agent.json');
52
52
  if (existsSync(lastAgentPath)) {
53
53
  const last = JSON.parse(readFileSync(lastAgentPath, 'utf8'));
54
54
  const lastTs = last?.ts ? Date.parse(last.ts) : 0;
@@ -13,9 +13,9 @@
13
13
  */
14
14
  import { createInterface } from 'readline';
15
15
  import { readFileSync, existsSync } from 'fs';
16
- import { homedir } from 'os';
17
16
  import { join } from 'path';
18
17
  import { logHookFailure } from './_lib/log.mjs';
18
+ import { doctorRoot } from '../config/xdg.mjs';
19
19
 
20
20
  const BLOCK_PATTERNS = [
21
21
  {
@@ -99,7 +99,7 @@ for (const { pattern, reason } of WARN_PATTERNS) {
99
99
 
100
100
  if (process.env.CONSTRUCT_ROLES !== 'off') {
101
101
  try {
102
- const cxDir = join(homedir(), '.cx');
102
+ const cxDir = doctorRoot();
103
103
  const id = String(process.env.CONSTRUCT_AGENT_ID || '').replace(/^cx-/, '');
104
104
  let agentData = null;
105
105
 
@@ -32,8 +32,9 @@ import {
32
32
  writeProviderCooldown,
33
33
  } from '../model-router.mjs';
34
34
  import { readHookInput } from './_lib/input.mjs';
35
+ import { doctorRoot } from '../config/xdg.mjs';
35
36
 
36
- const cooldownPath = join(homedir(), '.cx', 'provider-cooldowns.json');
37
+ const cooldownPath = join(doctorRoot(), 'provider-cooldowns.json');
37
38
  const envPath = join(process.cwd(), '.env');
38
39
 
39
40
  const input = readHookInput();
@@ -24,6 +24,7 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
24
24
  import { execSync } from 'node:child_process';
25
25
  import { join } from 'node:path';
26
26
  import { homedir } from 'node:os';
27
+ import { doctorRoot } from '../config/xdg.mjs';
27
28
 
28
29
  let input = {};
29
30
  try { input = JSON.parse(readFileSync(0, 'utf8')) || {}; } catch { process.exit(0); }
@@ -55,7 +56,7 @@ if (hookEvent === 'PreToolUse') {
55
56
  const READ_TOOLS = new Set(['Read', 'Grep', 'Glob', 'LS', 'NotebookRead']);
56
57
  const BLOCKED_TOOLS = new Set(['Write', 'Edit', 'MultiEdit', 'NotebookEdit', 'TodoWrite']);
57
58
 
58
- const stateDir = join(home, '.cx');
59
+ const stateDir = doctorRoot(home);
59
60
  const statePath = join(stateDir, 'bootstrap-state.json');
60
61
  const sessionId = input.session_id || input.sessionId || 'default';
61
62
 
@@ -147,7 +148,7 @@ if (hookEvent === 'Stop') {
147
148
 
148
149
  const branch = safeExec('git branch --show-current');
149
150
  const isFeatureBranch = branch && branch !== 'main' && branch !== 'dev' && branch !== 'master';
150
- const filesChangedPath = join(home, '.cx', 'files-changed-count.txt');
151
+ const filesChangedPath = join(doctorRoot(home), 'files-changed-count.txt');
151
152
  let filesChanged = 0;
152
153
  try { filesChanged = parseInt(readFileSync(filesChangedPath, 'utf8').trim() || '0', 10); } catch {}
153
154
 
@@ -188,7 +189,7 @@ if (hookEvent === 'Stop') {
188
189
  }
189
190
  }
190
191
 
191
- const driveSessionPath = join(home, '.cx', 'drive-session.json');
192
+ const driveSessionPath = join(doctorRoot(home), 'drive-session.json');
192
193
  if (existsSync(driveSessionPath)) {
193
194
  let ds = {};
194
195
  try { ds = JSON.parse(readFileSync(driveSessionPath, 'utf8')); } catch {}
@@ -17,10 +17,10 @@
17
17
  * @exits 0 = pass
18
18
  */
19
19
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
20
- import { homedir } from 'os';
21
20
  import { join } from 'path';
22
21
  import { writeContextState, contextMarkdownPath, contextJsonPath } from '../context-state.mjs';
23
22
  import { flushReadTrackerDeltas } from '../read-tracker-store.mjs';
23
+ import { doctorRoot } from '../config/xdg.mjs';
24
24
 
25
25
  // Section headings that are auto-generated by this hook. Any other heading in
26
26
  // the existing context.md is treated as manually authored and preserved.
@@ -100,8 +100,8 @@ const transcriptPath = input?.transcript_path || '';
100
100
  const filesChanged = [];
101
101
  const decisions = [];
102
102
  const pendingTodos = [];
103
- const warnFlagsPath = join(homedir(), '.cx', 'warn-flags.txt');
104
- const efficiencyPath = join(homedir(), '.cx', 'session-efficiency.json');
103
+ const warnFlagsPath = join(doctorRoot(), 'warn-flags.txt');
104
+ const efficiencyPath = join(doctorRoot(), 'session-efficiency.json');
105
105
  let lastSummary = '';
106
106
 
107
107
  if (transcriptPath && existsSync(transcriptPath)) {
@@ -275,12 +275,12 @@ if (decisions.length) {
275
275
  try { writeFileSync(join(decisionsDir, `${slug}.md`), decisionDoc); } catch { /* best effort */ }
276
276
  }
277
277
 
278
- const globalCxDir = join(homedir(), '.cx');
278
+ const globalCxDir = join(doctorRoot(), '.cx');
279
279
  try { mkdirSync(globalCxDir, { recursive: true }); } catch { /* exists */ }
280
- const globalMdPath = contextMarkdownPath(homedir());
280
+ const globalMdPath = contextMarkdownPath(doctorRoot());
281
281
  const globalManualSections = extractManualSections(globalMdPath);
282
282
  const globalFinalContent = content + (globalManualSections ? '\n' + globalManualSections : '');
283
- const globalMergedJson = mergeExistingJson(contextJsonPath(homedir()), contextJson);
284
- try { writeContextState(homedir(), { ...globalMergedJson, contextSummary: lastSummary }, { markdown: globalFinalContent }); } catch { /* best effort */ }
283
+ const globalMergedJson = mergeExistingJson(contextJsonPath(doctorRoot()), contextJson);
284
+ try { writeContextState(doctorRoot(), { ...globalMergedJson, contextSummary: lastSummary }, { markdown: globalFinalContent }); } catch { /* best effort */ }
285
285
 
286
286
  process.exit(0);