@geraldmaron/construct 1.2.1 → 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 (198) hide show
  1. package/README.md +6 -6
  2. package/bin/construct +41 -67
  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-type-from-path.mjs +9 -5
  16. package/lib/audit-specialists.mjs +1 -1
  17. package/lib/audit-trail.mjs +3 -3
  18. package/lib/auto-docs.mjs +10 -10
  19. package/lib/bootstrap/built-ins.mjs +3 -3
  20. package/lib/boundary.mjs +3 -2
  21. package/lib/brand-prose.mjs +1 -1
  22. package/lib/cache-governor.js +4 -4
  23. package/lib/certification/document-io-fixtures.mjs +1 -1
  24. package/lib/chat/config.mjs +2 -2
  25. package/lib/chat/desktop-binary.mjs +5 -3
  26. package/lib/cli-commands.mjs +3 -3
  27. package/lib/cli-service-inventory.mjs +1 -1
  28. package/lib/comment-lint.mjs +9 -5
  29. package/lib/config/alias.mjs +3 -2
  30. package/lib/config/intake-policy.mjs +8 -78
  31. package/lib/config/project-config.mjs +95 -5
  32. package/lib/config/schema.mjs +9 -19
  33. package/lib/config/xdg.mjs +63 -0
  34. package/lib/contracts/violation-log.mjs +2 -2
  35. package/lib/cost-ledger.mjs +4 -3
  36. package/lib/cost.mjs +3 -2
  37. package/lib/decisions/registry.mjs +3 -3
  38. package/lib/distill.mjs +2 -2
  39. package/lib/docs-verify.mjs +2 -0
  40. package/lib/doctor/audit.mjs +4 -3
  41. package/lib/doctor/cli.mjs +0 -1
  42. package/lib/doctor/index.mjs +4 -4
  43. package/lib/doctor/report.mjs +3 -3
  44. package/lib/doctor/watchers/bd-watch.mjs +5 -5
  45. package/lib/doctor/watchers/cost.mjs +4 -4
  46. package/lib/doctor/watchers/disk.mjs +4 -3
  47. package/lib/doctor/watchers/service-health.mjs +5 -5
  48. package/lib/document-export.mjs +1 -1
  49. package/lib/document-extract.mjs +3 -3
  50. package/lib/document-ingest.mjs +13 -3
  51. package/lib/efficiency.mjs +2 -1
  52. package/lib/embed/approval-queue.mjs +3 -2
  53. package/lib/embed/cli.mjs +7 -5
  54. package/lib/embed/config.mjs +6 -5
  55. package/lib/embed/daemon.mjs +6 -5
  56. package/lib/embed/inbox-live-watcher.mjs +1 -1
  57. package/lib/embed/inbox.mjs +52 -37
  58. package/lib/embed/intake-metrics.mjs +3 -2
  59. package/lib/embed/reconcile.mjs +1 -1
  60. package/lib/embed/semantic.mjs +3 -2
  61. package/lib/embed/supervision.mjs +4 -2
  62. package/lib/engine/registry.mjs +3 -3
  63. package/lib/env-config.mjs +34 -12
  64. package/lib/features.mjs +4 -3
  65. package/lib/health-check.mjs +7 -6
  66. package/lib/hook-health.mjs +5 -4
  67. package/lib/hooks/_lib/log.mjs +4 -3
  68. package/lib/hooks/_lib/output-mode.mjs +2 -1
  69. package/lib/hooks/agent-tracker.mjs +3 -4
  70. package/lib/hooks/audit-reads.mjs +2 -2
  71. package/lib/hooks/audit-trail.mjs +2 -2
  72. package/lib/hooks/bash-output-logger.mjs +3 -3
  73. package/lib/hooks/brand-prose-lint.mjs +1 -1
  74. package/lib/hooks/ci-status-check.mjs +3 -2
  75. package/lib/hooks/context-watch.mjs +2 -2
  76. package/lib/hooks/context-window-recovery.mjs +5 -5
  77. package/lib/hooks/dep-audit.mjs +2 -2
  78. package/lib/hooks/doc-coupling-check.mjs +3 -2
  79. package/lib/hooks/edit-accumulator.mjs +3 -2
  80. package/lib/hooks/edit-guard.mjs +3 -3
  81. package/lib/hooks/guard-bash.mjs +2 -2
  82. package/lib/hooks/model-fallback.mjs +2 -1
  83. package/lib/hooks/policy-engine.mjs +4 -3
  84. package/lib/hooks/pre-compact.mjs +7 -7
  85. package/lib/hooks/readme-age-check.mjs +2 -2
  86. package/lib/hooks/session-optimize.mjs +4 -3
  87. package/lib/hooks/session-start.mjs +6 -5
  88. package/lib/hooks/stop-notify.mjs +9 -8
  89. package/lib/hooks/stop-typecheck.mjs +4 -3
  90. package/lib/hooks/test-watch.mjs +2 -2
  91. package/lib/host-disposition.mjs +1 -1
  92. package/lib/ingest/provider-extract.mjs +2 -1
  93. package/lib/init/detect-existing-structure.mjs +2 -2
  94. package/lib/init/doc-lanes.mjs +3 -10
  95. package/lib/init-docs.mjs +12 -54
  96. package/lib/init-unified.mjs +35 -81
  97. package/lib/init.mjs +7 -5
  98. package/lib/install/desktop-binary-download.mjs +5 -2
  99. package/lib/intake/daemon.mjs +31 -8
  100. package/lib/intake/intake-config.mjs +5 -32
  101. package/lib/integrations/intake-integrations.mjs +8 -7
  102. package/lib/knowledge/rag.mjs +2 -2
  103. package/lib/knowledge/search.mjs +15 -15
  104. package/lib/maintenance/cleanup.mjs +14 -10
  105. package/lib/mcp/server.mjs +1 -1
  106. package/lib/mcp/tools/project.mjs +2 -1
  107. package/lib/model-cheapest-provider.mjs +4 -3
  108. package/lib/model-pricing.mjs +3 -2
  109. package/lib/model-router.mjs +4 -3
  110. package/lib/models/catalog.mjs +2 -1
  111. package/lib/models/execution-capability-profile.mjs +2 -1
  112. package/lib/models/provider-poll.mjs +2 -1
  113. package/lib/ollama-manager.mjs +0 -1
  114. package/lib/op-log.mjs +2 -1
  115. package/lib/opencode-runtime-plugin.mjs +3 -2
  116. package/lib/oracle/artifact-gate.mjs +3 -0
  117. package/lib/oracle/cli.mjs +2 -1
  118. package/lib/oracle/execute.mjs +3 -2
  119. package/lib/oracle/index.mjs +2 -1
  120. package/lib/oracle/read-model.mjs +3 -2
  121. package/lib/performance/generate.mjs +4 -3
  122. package/lib/platforms/capabilities.mjs +1 -1
  123. package/lib/plugin-registry.mjs +2 -1
  124. package/lib/profiles/lifecycle.mjs +3 -3
  125. package/lib/project-profile.mjs +2 -1
  126. package/lib/project-root.mjs +9 -7
  127. package/lib/provider-capabilities.js +4 -3
  128. package/lib/providers/auth-manager.mjs +2 -1
  129. package/lib/providers/copilot-auth.mjs +3 -2
  130. package/lib/providers/creds.mjs +3 -2
  131. package/lib/providers/registry.mjs +3 -3
  132. package/lib/providers/secret-resolver.mjs +3 -2
  133. package/lib/read-tracker-store.mjs +2 -1
  134. package/lib/reconcile/mcp-entry-reconcile.mjs +2 -2
  135. package/lib/reflect.mjs +2 -1
  136. package/lib/registry/generate-docs.mjs +2 -2
  137. package/lib/roles/approval-surface.mjs +5 -4
  138. package/lib/roles/event-bus.mjs +0 -1
  139. package/lib/roles/gateway.mjs +6 -2
  140. package/lib/rules-delivery.mjs +1 -1
  141. package/lib/runtime-pressure.mjs +5 -3
  142. package/lib/sandbox.mjs +3 -2
  143. package/lib/scheduler/solo.mjs +6 -4
  144. package/lib/server/auth.mjs +4 -3
  145. package/lib/server/index.mjs +24 -26
  146. package/lib/server/insights.mjs +5 -4
  147. package/lib/server/webhook.mjs +2 -1
  148. package/lib/service-manager.mjs +5 -3
  149. package/lib/setup.mjs +9 -8
  150. package/lib/status.mjs +6 -5
  151. package/lib/storage/embeddings-local.mjs +3 -2
  152. package/lib/storage/sync.mjs +2 -2
  153. package/lib/telemetry/client.mjs +0 -1
  154. package/lib/telemetry/intent-verifications.mjs +6 -6
  155. package/lib/telemetry/model-pricing-catalog.mjs +4 -3
  156. package/lib/telemetry/rule-calls.mjs +3 -3
  157. package/lib/telemetry/skill-calls.mjs +3 -3
  158. package/lib/template-registry.mjs +1 -0
  159. package/lib/templates/visual-requirements.mjs +1 -1
  160. package/lib/test-corpus-inventory.mjs +1 -1
  161. package/lib/uninstall/uninstall.mjs +12 -10
  162. package/package.json +3 -3
  163. package/platforms/claude/settings.template.json +43 -43
  164. package/rules/common/no-fabrication.md +1 -1
  165. package/rules/common/release-gates.md +1 -1
  166. package/rules/common/research.md +1 -1
  167. package/rules/common/review-before-change.md +1 -1
  168. package/scripts/sync-specialists.mjs +11 -8
  169. package/skills/docs/adr-workflow.md +3 -3
  170. package/skills/docs/init-docs.md +9 -9
  171. package/skills/docs/prd-workflow.md +5 -5
  172. package/skills/docs/product-intelligence-review.md +1 -1
  173. package/skills/docs/product-intelligence-workflow.md +2 -2
  174. package/skills/docs/product-signal-workflow.md +1 -1
  175. package/skills/docs/runbook-workflow.md +4 -4
  176. package/skills/exploration/unknown-codebase-onboarding.md +1 -1
  177. package/skills/operating/orchestration-reference.md +1 -1
  178. package/skills/routing.md +3 -3
  179. package/specialists/prompts/cx-architect.md +1 -1
  180. package/specialists/prompts/cx-docs-keeper.md +1 -1
  181. package/specialists/prompts/cx-researcher.md +1 -1
  182. package/specialists/prompts/cx-sre.md +1 -1
  183. package/specialists/role-manifests.json +6 -6
  184. package/templates/docs/README.md +125 -0
  185. package/templates/docs/construct_guide.md +3 -4
  186. package/templates/docs/persona-artifact.md +1 -1
  187. package/templates/docs/prds/README.md +25 -0
  188. package/templates/docs/prds/templates/_template.md +206 -0
  189. package/templates/docs/prds/templates/meta-prd.template.md +177 -0
  190. package/templates/docs/prds/templates/prd-business.template.md +61 -0
  191. package/templates/docs/prds/templates/prd-platform.template.md +81 -0
  192. package/templates/docs/prds/templates/prfaq.template.md +38 -0
  193. package/templates/docs/rfcs/README.md +22 -0
  194. package/templates/docs/rfcs/templates/_template.md +58 -0
  195. package/templates/docs/rfcs/templates/rfc-platform.template.md +72 -0
  196. package/templates/homebrew/construct.rb +1 -1
  197. package/templates/workflows/new-feature.yml +8 -8
  198. package/lib/intake/legacy-paths.mjs +0 -5
@@ -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);
@@ -17,11 +17,11 @@
17
17
 
18
18
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
19
19
  import { execSync } from 'node:child_process';
20
- import { homedir } from 'node:os';
21
20
  import { join } from 'node:path';
22
21
  import { emitRoleEvent } from '../roles/hook-emit.mjs';
22
+ import { doctorRoot } from '../config/xdg.mjs';
23
23
 
24
- const STATE_PATH = join(homedir(), '.cx', 'readme-age-state.json');
24
+ const STATE_PATH = join(doctorRoot(), 'readme-age-state.json');
25
25
  const STALE_DAYS = 90;
26
26
  const SUPPRESS_DAYS = 7;
27
27
 
@@ -14,18 +14,19 @@
14
14
  */
15
15
  import { readFileSync, existsSync, writeFileSync, mkdirSync, readdirSync, statSync } from 'fs';
16
16
  import { join, dirname } from 'path';
17
- import { homedir } from 'os';
18
17
  import { spawn } from 'child_process';
19
18
  import { fileURLToPath } from 'url';
20
19
 
20
+ import { doctorRoot } from '../config/xdg.mjs';
21
+
21
22
  const __dirname = dirname(fileURLToPath(import.meta.url));
22
23
  const CONSTRUCT_BIN = join(__dirname, '..', '..', 'bin', 'construct');
23
24
 
24
25
  const OPTIMIZATION_THRESHOLD = 0.7;
25
26
  const MIN_TRACES_PER_AGENT = 3;
26
27
  const OPTIMIZATION_DAYS = 7;
27
- const OPTIMIZATION_LOG_DIR = join(homedir(), '.cx', 'optimization-logs');
28
- const REVIEWS_DIR = join(homedir(), '.cx', 'performance-reviews');
28
+ const OPTIMIZATION_LOG_DIR = join(doctorRoot(), 'optimization-logs');
29
+ const REVIEWS_DIR = join(doctorRoot(), 'performance-reviews');
29
30
 
30
31
  mkdirSync(OPTIMIZATION_LOG_DIR, { recursive: true });
31
32
 
@@ -24,6 +24,7 @@ import { listObservations, searchObservations } from '../observation-store.mjs';
24
24
  import { countEntities } from '../entity-store.mjs';
25
25
  import { logHookFailure } from './_lib/log.mjs';
26
26
  import { resolveHookOutputMode, writeHookContext } from './_lib/output-mode.mjs';
27
+ import { doctorRoot } from '../config/xdg.mjs';
27
28
 
28
29
  const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
29
30
  const CONSTRUCT_BIN = resolve(MODULE_DIR, '..', '..', 'bin', 'construct');
@@ -64,11 +65,11 @@ function fileIsFresh(path) {
64
65
  // stale context files inject only the digest line as fallback (Tier 2 → Tier 1
65
66
  // degradation), full body only when modified within the freshness window.
66
67
  const localCtx = join(cwd, '.cx', 'context.md');
67
- const globalCtx = join(homedir(), '.cx', 'context.md');
68
+ const globalCtx = join(doctorRoot(), '.cx', 'context.md');
68
69
  let context = '';
69
70
  let contextStale = false;
70
71
  const localState = readContextState(cwd);
71
- const globalState = localState ? null : readContextState(homedir());
72
+ const globalState = localState ? null : readContextState(doctorRoot());
72
73
  const stateContext = localState || globalState;
73
74
  const stateContextPath = localState ? localCtx : globalCtx;
74
75
  if (stateContext?.markdown) {
@@ -151,7 +152,7 @@ try {
151
152
 
152
153
  // Record memory injection stats for the Stop hook to persist into .cx/memory-stats.jsonl.
153
154
  try {
154
- const memStatsPath = join(homedir(), '.cx', 'session-memory-stats.json');
155
+ const memStatsPath = join(doctorRoot(), 'session-memory-stats.json');
155
156
  const injected = observationsNote ? (observationsNote.match(/^- /gm) || []).length : 0;
156
157
  writeFileSync(memStatsPath, JSON.stringify({
157
158
  project: projectName,
@@ -162,7 +163,7 @@ try {
162
163
  } catch { /* best effort */ }
163
164
 
164
165
  // Pending typecheck warning
165
- const tcPath = join(homedir(), '.cx', 'pending-typecheck.txt');
166
+ const tcPath = join(doctorRoot(), 'pending-typecheck.txt');
166
167
  let pendingNote = '';
167
168
  try {
168
169
  const pending = existsSync(tcPath) ? readFileSync(tcPath, 'utf8').split('\n').filter(Boolean) : [];
@@ -400,7 +401,7 @@ try {
400
401
  try {
401
402
  const sessionId = input?.session_id || input?.sessionId;
402
403
  if (sessionId) {
403
- const bootstrapPath = join(homedir(), ".cx", "bootstrap-state.json");
404
+ const bootstrapPath = join(doctorRoot(), "bootstrap-state.json");
404
405
  let state = {};
405
406
  try { state = JSON.parse(readFileSync(bootstrapPath, "utf8")) || {}; } catch {}
406
407
  const now = Date.now();
@@ -27,6 +27,7 @@ import { captureSessionArtifacts } from '../artifact-capture.mjs';
27
27
  import { appendSessionStats } from '../memory-stats.mjs';
28
28
  import { estimateUsageCost } from '../telemetry/model-pricing-catalog.mjs';
29
29
  import { flushReadTrackerDeltas } from '../read-tracker-store.mjs';
30
+ import { doctorRoot } from '../config/xdg.mjs';
30
31
 
31
32
  function loadTranscriptCheckpoints(checkpointPath) {
32
33
  try {
@@ -148,12 +149,12 @@ if (raw) process.stdout.write(raw);
148
149
  try { flushReadTrackerDeltas({ env: process.env }); } catch { /* non-critical */ }
149
150
 
150
151
  const home = homedir();
151
- const tsResultPath = join(home, '.cx', 'ts-result.txt');
152
- const warnFlagsPath = join(home, '.cx', 'warn-flags.txt');
153
- const countPath = join(home, '.cx', 'files-changed-count.txt');
154
- const costLogPath = join(home, '.cx', 'session-cost.jsonl');
155
- const lastAgentPath = join(home, '.cx', 'last-agent.json');
156
- const checkpointPath = join(home, '.cx', 'transcript-checkpoints.json');
152
+ const tsResultPath = join(doctorRoot(home), 'ts-result.txt');
153
+ const warnFlagsPath = join(doctorRoot(home), 'warn-flags.txt');
154
+ const countPath = join(doctorRoot(home), 'files-changed-count.txt');
155
+ const costLogPath = join(doctorRoot(home), 'session-cost.jsonl');
156
+ const lastAgentPath = join(doctorRoot(home), 'last-agent.json');
157
+ const checkpointPath = join(doctorRoot(home), 'transcript-checkpoints.json');
157
158
 
158
159
  // Consume-clear like warn-flags/files-changed below: a TS result is reported
159
160
  // once, not re-fired on every subsequent no-op Stop. stop-typecheck rewrites
@@ -304,7 +305,7 @@ try {
304
305
  }
305
306
  } catch { /* non-critical */ }
306
307
 
307
- const memStatsPath = join(home, '.cx', 'session-memory-stats.json');
308
+ const memStatsPath = join(doctorRoot(home), 'session-memory-stats.json');
308
309
  try {
309
310
  if (existsSync(memStatsPath)) {
310
311
  const memStats = JSON.parse(readFileSync(memStatsPath, 'utf8'));
@@ -323,7 +324,7 @@ try {
323
324
  // repeat Stop events within the window to avoid review-spam.
324
325
  try {
325
326
  const homeDir = process.env.HOME || homedir();
326
- const stampPath = join(homeDir, '.cx', 'performance-reviews', '.last-run');
327
+ const stampPath = join(doctorRoot(homeDir), 'performance-reviews', '.last-run');
327
328
  let lastRun = 0;
328
329
  try { lastRun = Number(readFileSync(stampPath, 'utf8')) || 0; } catch { /* fresh */ }
329
330
  if (Date.now() - lastRun > 24 * 60 * 60 * 1000) {
@@ -15,6 +15,7 @@ import { readFileSync, writeFileSync, existsSync } from 'fs';
15
15
  import { execSync } from 'child_process';
16
16
  import { homedir } from 'os';
17
17
  import { join, dirname } from 'path';
18
+ import { doctorRoot } from '../config/xdg.mjs';
18
19
 
19
20
  // Stop hooks must echo stdin to stdout
20
21
  let raw = '';
@@ -22,9 +23,9 @@ try { raw = readFileSync(0, 'utf8'); } catch { /* no stdin */ }
22
23
  if (raw) process.stdout.write(raw);
23
24
 
24
25
  const home = homedir();
25
- const tcPath = join(home, '.cx', 'pending-typecheck.txt');
26
- const tsResultPath = join(home, '.cx', 'ts-result.txt');
27
- const warnFlagsPath = join(home, '.cx', 'warn-flags.txt');
26
+ const tcPath = join(doctorRoot(home), 'pending-typecheck.txt');
27
+ const tsResultPath = join(doctorRoot(home), 'ts-result.txt');
28
+ const warnFlagsPath = join(doctorRoot(home), 'warn-flags.txt');
28
29
 
29
30
  // Read pending files
30
31
  const pending = existsSync(tcPath)
@@ -15,10 +15,10 @@
15
15
  */
16
16
 
17
17
  import { readFileSync, existsSync, writeFileSync } from 'node:fs';
18
- import { homedir } from 'node:os';
19
18
  import { join } from 'node:path';
20
19
  import { logHookFailure } from './_lib/log.mjs';
21
20
  import { emitRoleEvent } from '../roles/hook-emit.mjs';
21
+ import { doctorRoot } from '../config/xdg.mjs';
22
22
 
23
23
  let input = {};
24
24
  try { input = JSON.parse(readFileSync(0, 'utf8')); }
@@ -44,7 +44,7 @@ emitRoleEvent({
44
44
  context: { command, exitCode, failedNames },
45
45
  });
46
46
 
47
- const flakeTrackerPath = join(homedir(), '.cx', 'test-watch-state.json');
47
+ const flakeTrackerPath = join(doctorRoot(), 'test-watch-state.json');
48
48
  try {
49
49
  const state = existsSync(flakeTrackerPath) ? JSON.parse(readFileSync(flakeTrackerPath, 'utf8')) : {};
50
50
  const now = Date.now();
@@ -2,7 +2,7 @@
2
2
  * lib/host-disposition.mjs — single source of truth for the disposition of
3
3
  * every artifact Construct creates in a host project (ADR-0027 §1).
4
4
  *
5
- * Scope model (see docs/concepts/project-scopes.md):
5
+ * Scope model (see docs/guides/concepts/project-scopes.md):
6
6
  * templates/, docs/ — package/repo durable sources (committed)
7
7
  * .construct/ — per-project launcher (ignored)
8
8
  * .cx/ — per-project runtime: intake, oracle, observations, demos (ignored)
@@ -24,6 +24,7 @@
24
24
  import { readFileSync, existsSync } from 'node:fs';
25
25
  import { extname, join } from 'node:path';
26
26
  import { homedir } from 'node:os';
27
+ import { configDir } from '../config/xdg.mjs';
27
28
 
28
29
  import {
29
30
  AUDIO_VIDEO_EXTS,
@@ -75,7 +76,7 @@ function isAnthropic(provider, model) {
75
76
  function resolveKey(varName, env, allowAmbient) {
76
77
  if (env[varName] && typeof env[varName] === 'string' && env[varName].length > 0) return env[varName];
77
78
  if (!allowAmbient) return null;
78
- for (const file of [join(homedir(), '.construct', 'config.env'), join(homedir(), '.env')]) {
79
+ for (const file of [join(configDir(), 'config.env'), join(homedir(), '.env')]) {
79
80
  try {
80
81
  if (!existsSync(file)) continue;
81
82
  const m = readFileSync(file, 'utf8').match(new RegExp(`^${varName}=["']?(.+?)["']?$`, 'm'));
@@ -12,13 +12,13 @@
12
12
  *
13
13
  * Heuristics intentionally err on the side of "skip" — a directory is treated
14
14
  * as a real lane only when it carries at least one markdown file. An empty
15
- * docs/meetings/ that an earlier init scaffolded does NOT register; that lets
15
+ * docs/notes/meetings/ that an earlier init scaffolded does NOT register; that lets
16
16
  * re-running init be a no-op rather than a contradictory "skip what we just
17
17
  * created" message.
18
18
  *
19
19
  * Issue #97 motivates this: `construct init` on an existing project with
20
20
  * `internal/meetings/`, a custom `ingest` script, and root-level `templates/`
21
- * created `docs/meetings/`, `.cx/inbox/`, and per-lane `templates/` folders
21
+ * created `docs/notes/meetings/`, `inbox/`, and per-lane `templates/` folders
22
22
  * that conflicted with the existing workflow.
23
23
  */
24
24
 
@@ -32,12 +32,6 @@ export const DOC_LANES = {
32
32
  description: 'User-facing release notes and version history entries.',
33
33
  templates: ['changelog-entry.md'],
34
34
  },
35
- intake: {
36
- title: 'Intake',
37
- dir: 'intake',
38
- description: 'Intake batch records that explain what arrived, why it matters, and how it should be ingested.',
39
- templates: ['__intake-template__'],
40
- },
41
35
  memos: {
42
36
  title: 'Memos',
43
37
  dir: 'memos',
@@ -88,11 +82,11 @@ export const DOC_LANES = {
88
82
  },
89
83
  };
90
84
 
91
- export const LANE_ORDER = ['adrs', 'briefs', 'changelogs', 'intake', 'memos', 'meetings', 'notes', 'onboarding', 'postmortems', 'prds', 'rfcs', 'runbooks'];
85
+ export const LANE_ORDER = ['adrs', 'briefs', 'changelogs', 'memos', 'meetings', 'notes', 'onboarding', 'postmortems', 'prds', 'rfcs', 'runbooks'];
92
86
 
93
87
  export const DOC_PRESETS = {
94
- lean: ['adrs', 'intake', 'memos', 'meetings', 'notes', 'prds'],
95
- product: ['adrs', 'intake', 'memos', 'meetings', 'notes', 'prds', 'rfcs'],
88
+ lean: ['adrs', 'memos', 'meetings', 'notes', 'prds'],
89
+ product: ['adrs', 'memos', 'meetings', 'notes', 'prds', 'rfcs'],
96
90
  full: LANE_ORDER,
97
91
  };
98
92
 
@@ -107,7 +101,6 @@ export const LANE_ALIASES = {
107
101
  changelogs: 'changelogs',
108
102
  releases: 'changelogs',
109
103
  release: 'changelogs',
110
- intake: 'intake',
111
104
  memo: 'memos',
112
105
  memos: 'memos',
113
106
  meeting: 'meetings',
package/lib/init-docs.mjs CHANGED
@@ -3,8 +3,8 @@
3
3
  * lib/init-docs.mjs — stand up a documentation system for a project.
4
4
  *
5
5
  * Intentionally separate from `construct init`. Creates the docs surface only:
6
- * docs/README.md plus selected lane directories such as docs/adr/, docs/intake/,
7
- * docs/memos/, docs/notes/, docs/prds/, and docs/rfcs/ with starter templates
6
+ * docs/README.md plus selected lane directories such as docs/decisions/adr/, docs/intake/,
7
+ * docs/notes/memos/, docs/notes/, templates/docs/prds/, and templates/docs/rfcs/ with starter templates
8
8
  * copied into per-lane templates/ directories from Construct's template library.
9
9
  *
10
10
  * Usage:
@@ -290,15 +290,6 @@ function buildLaneReadme(laneKey) {
290
290
  const filename = index === 0 ? "_template.md" : templateName.replace(/\.md$/, ".template.md");
291
291
  return `- [${filename}](./templates/${filename})`;
292
292
  });
293
- const usageSection = laneKey === "intake"
294
- ? `
295
- ## Intake flow
296
-
297
- - Drop source files into either [\`.cx/inbox/\`](../../.cx/inbox/) or this lane's directory when you want Construct to ingest them.
298
- - Run \`construct ingest ./docs/intake --sync\` or \`construct ingest ./.cx/inbox --sync\` to convert supported files into retrieval-ready markdown, or let the embed daemon watch those drop zones automatically.
299
- - Durable ingested knowledge lands under \`.cx/knowledge/internal/\` by default, which is where Construct's learning and search paths already operate.
300
- `
301
- : "";
302
293
 
303
294
  return `<!--
304
295
  docs/${dirName}/README.md — lane guide for ${title}.
@@ -314,7 +305,6 @@ ${description}
314
305
  ## Starter templates
315
306
 
316
307
  ${templateLines.join("\n")}
317
- ${usageSection}
318
308
  `;
319
309
  }
320
310
 
@@ -432,39 +422,6 @@ Promote durable decisions into ADRs, memos, or PRDs when needed.
432
422
  `;
433
423
  }
434
424
 
435
- function buildIntakeTemplate() {
436
- return `<!--
437
- docs/intake/templates/_template.md — starter template for logging an intake batch.
438
-
439
- Use this when you want a durable record of what was dropped into intake and why.
440
- Raw source files belong in .cx/inbox/ until they are ingested.
441
- -->
442
-
443
- # Intake Batch: {title}
444
-
445
- - **Date**: {YYYY-MM-DD}
446
- - **Owner**: {name}
447
- - **Source**: {vendor, teammate, export, upload}
448
-
449
- ## What arrived
450
-
451
- <!-- Files, folders, or links dropped into .cx/inbox/. -->
452
-
453
- ## Why it matters
454
-
455
- <!-- Why Construct should ingest this material. -->
456
-
457
- ## Ingest plan
458
-
459
- - Run: \`construct ingest ./.cx/inbox --sync\`
460
- - Target: \`.cx/knowledge/internal/\` unless a different knowledge target is more appropriate
461
-
462
- ## Notes
463
-
464
- <!-- Caveats, access concerns, or cleanup notes. -->
465
- `;
466
- }
467
-
468
425
  function titleCase(value) {
469
426
  return value
470
427
  .split(/[-_\s]+/)
@@ -532,7 +489,6 @@ function copyLaneTemplates(laneKey) {
532
489
  const content =
533
490
  templateName === "__notes-template__" ? buildNotesTemplate()
534
491
  : templateName === '__meeting-notes-template__' ? buildMeetingNotesTemplate()
535
- : templateName === "__intake-template__" ? buildIntakeTemplate()
536
492
  : fs.readFileSync(path.join(TEMPLATE_DIR, templateName), "utf8");
537
493
  writeIfMissing(path.join(laneRoot, "templates", outputName), content);
538
494
  }
@@ -632,14 +588,16 @@ async function main() {
632
588
  if (withArchitecture) {
633
589
  writeIfMissing(path.join(docsDir, "architecture.md"), buildArchitectureDoc(projectName, allScaffoldedLaneKeys));
634
590
  }
635
- if (lanesToScaffold.includes('intake')) {
636
- const inboxDecision = shouldSkipProjectInbox(detection, { force: forceScaffold });
637
- if (inboxDecision.skip) {
638
- process.stdout.write(`[init:docs] skipping .cx/inbox/ ${inboxDecision.reason}. Run with --force to scaffold anyway.\n`);
639
- skipped.push('.cx/inbox/ (deferred to existing intake)');
640
- } else {
641
- fs.mkdirSync(path.join(target, '.cx', 'inbox'), { recursive: true });
642
- }
591
+ const inboxDecision = shouldSkipProjectInbox(detection, { force: forceScaffold });
592
+ if (inboxDecision.skip) {
593
+ process.stdout.write(`[init:docs] skipping inbox/ — ${inboxDecision.reason}. Run with --force to scaffold anyway.\n`);
594
+ skipped.push('inbox/ (deferred to existing intake)');
595
+ } else {
596
+ // Single canonical drop zone (ADR-0045 §C): the project-root inbox/, with
597
+ // a gitignored .staging/ for atomic rename-in handoff. Independent of any
598
+ // docs lane.
599
+
600
+ fs.mkdirSync(path.join(target, 'inbox', '.staging'), { recursive: true });
643
601
  }
644
602
 
645
603
  for (const { laneKey, reason } of deferredLanes) {