@geraldmaron/construct 1.0.2 → 1.0.4

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 (97) hide show
  1. package/.env.example +1 -1
  2. package/README.md +4 -4
  3. package/agents/prompts/cx-ai-engineer.md +6 -26
  4. package/agents/prompts/cx-architect.md +1 -0
  5. package/agents/prompts/cx-business-strategist.md +2 -0
  6. package/agents/prompts/cx-data-analyst.md +6 -26
  7. package/agents/prompts/cx-docs-keeper.md +1 -31
  8. package/agents/prompts/cx-explorer.md +1 -0
  9. package/agents/prompts/cx-orchestrator.md +40 -112
  10. package/agents/prompts/cx-platform-engineer.md +2 -22
  11. package/agents/prompts/cx-product-manager.md +2 -1
  12. package/agents/prompts/cx-qa.md +0 -20
  13. package/agents/prompts/cx-rd-lead.md +2 -0
  14. package/agents/prompts/cx-researcher.md +77 -31
  15. package/agents/prompts/cx-security.md +11 -49
  16. package/agents/prompts/cx-sre.md +9 -43
  17. package/agents/prompts/cx-ux-researcher.md +1 -0
  18. package/agents/role-manifests.json +4 -4
  19. package/bin/construct +72 -11
  20. package/bin/construct-postinstall.mjs +1 -1
  21. package/db/schema/004_recommendations.sql +46 -0
  22. package/db/schema/005_strategy.sql +21 -0
  23. package/lib/auto-docs.mjs +1 -2
  24. package/lib/beads-automation.mjs +16 -7
  25. package/lib/bootstrap/resources.mjs +2 -2
  26. package/lib/cli-commands.mjs +8 -2
  27. package/lib/document-ingest.mjs +6 -5
  28. package/lib/embed/cli.mjs +16 -3
  29. package/lib/embed/conflict-detection.mjs +26 -9
  30. package/lib/embed/customer-profiles.mjs +38 -18
  31. package/lib/embed/daemon.mjs +59 -50
  32. package/lib/embed/inbox.mjs +30 -0
  33. package/lib/embed/recommendation-store.mjs +214 -15
  34. package/lib/embed/workspaces.mjs +53 -18
  35. package/lib/evaluator-optimizer.mjs +0 -2
  36. package/lib/features.mjs +11 -0
  37. package/lib/gates-audit.mjs +3 -3
  38. package/lib/health-check.mjs +3 -5
  39. package/lib/hooks/pre-compact.mjs +3 -0
  40. package/lib/hooks/read-tracker.mjs +10 -101
  41. package/lib/host-capabilities.mjs +90 -1
  42. package/lib/init-unified.mjs +119 -3
  43. package/lib/init-update.mjs +246 -131
  44. package/lib/install/first-invocation.mjs +4 -4
  45. package/lib/intake/queue.mjs +1 -1
  46. package/lib/integrations/intake-integrations.mjs +4 -5
  47. package/lib/intent-classifier.mjs +1 -0
  48. package/lib/knowledge/layout.mjs +10 -0
  49. package/lib/knowledge/rag.mjs +16 -0
  50. package/lib/mcp/tools/telemetry.mjs +30 -78
  51. package/lib/model-cheapest-provider.mjs +231 -0
  52. package/lib/model-router.mjs +68 -9
  53. package/lib/ollama-manager.mjs +1 -1
  54. package/lib/opencode-telemetry.mjs +4 -5
  55. package/lib/orchestration-policy.mjs +9 -0
  56. package/lib/parity.mjs +124 -21
  57. package/lib/project-profile.mjs +1 -1
  58. package/lib/prompt-composer.js +106 -29
  59. package/lib/read-tracker-store.mjs +149 -0
  60. package/lib/roles/catalog.mjs +133 -0
  61. package/lib/roles/preference.mjs +74 -0
  62. package/lib/server/index.mjs +109 -47
  63. package/lib/server/insights.mjs +1 -1
  64. package/lib/server/telemetry-login.mjs +17 -25
  65. package/lib/service-manager.mjs +32 -24
  66. package/lib/services/local-postgres.mjs +15 -0
  67. package/lib/services/telemetry-backend.mjs +2 -3
  68. package/lib/setup-prompts.mjs +2 -2
  69. package/lib/setup.mjs +55 -46
  70. package/lib/status.mjs +56 -8
  71. package/lib/storage/backend.mjs +12 -2
  72. package/lib/storage/postgres-backup.mjs +1 -1
  73. package/lib/strategy-store.mjs +371 -0
  74. package/lib/telemetry/backends/local.mjs +6 -4
  75. package/lib/telemetry/client.mjs +185 -0
  76. package/lib/telemetry/ingest.mjs +13 -5
  77. package/lib/telemetry/team-rollup.mjs +9 -2
  78. package/lib/uninstall/uninstall.mjs +1 -1
  79. package/lib/worker/trace.mjs +17 -27
  80. package/package.json +5 -2
  81. package/rules/common/research.md +44 -12
  82. package/skills/docs/backlog-proposal-workflow.md +2 -2
  83. package/skills/docs/customer-profile-workflow.md +1 -1
  84. package/skills/docs/evidence-ingest-workflow.md +5 -5
  85. package/skills/docs/prfaq-workflow.md +1 -1
  86. package/skills/docs/product-intelligence-review.md +1 -1
  87. package/skills/docs/product-intelligence-workflow.md +3 -3
  88. package/skills/docs/product-signal-workflow.md +48 -18
  89. package/skills/docs/research-workflow.md +26 -14
  90. package/skills/docs/strategy-workflow.md +36 -0
  91. package/skills/roles/data-analyst.product-intelligence.md +1 -1
  92. package/skills/roles/researcher.md +28 -15
  93. package/skills/routing.md +8 -1
  94. package/templates/docs/construct_guide.md +2 -2
  95. package/templates/docs/research-brief.md +63 -9
  96. package/templates/docs/strategy.md +36 -0
  97. package/templates/homebrew/construct.rb +7 -7
@@ -7,63 +7,14 @@
7
7
  * @p95ms 10
8
8
  * @maxBlockingScope none (PostToolUse, non-blocking)
9
9
  */
10
- import { readFileSync, writeFileSync, mkdirSync, existsSync, appendFileSync } from 'fs';
10
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
11
11
  import { createHash } from 'crypto';
12
12
  import { join, resolve } from 'path';
13
13
  import { homedir } from 'os';
14
+ import { flushReadTrackerDeltas, recordReadDelta } from '../read-tracker-store.mjs';
14
15
 
15
16
  const CX_DIR = join(homedir(), '.cx');
16
17
  const HASH_STORE = join(CX_DIR, 'file-hashes.json');
17
- const EFFICIENCY_STORE = join(CX_DIR, 'session-efficiency.json');
18
- const WARN_FLAGS = join(CX_DIR, 'warn-flags.txt');
19
- const SESSION_IDLE_RESET_MS = 2 * 60 * 60 * 1000;
20
- const REPEATED_READ_WARNING_THRESHOLD = 5;
21
- const REPEATED_READ_TIER2_THRESHOLD = 8;
22
- const LARGE_READ_WARNING_THRESHOLD = 3;
23
- const TOTAL_BYTES_WARNING_THRESHOLD = 750_000;
24
-
25
- // Bound per-session `stats.files` so the read-tracking map cannot grow
26
- // without limit across long sessions. The hook is a fresh node process per
27
- // Read, so every invocation must read-mutate-write the full state — there
28
- // is no in-memory accumulator that would survive between calls. The LRU cap
29
- // drops the oldest entries by `lastReadAt`.
30
-
31
- const FILES_LRU_CAP = 200;
32
-
33
- function loadEfficiencyStats(nowIso) {
34
- const fresh = {
35
- sessionStartedAt: nowIso,
36
- lastUpdatedAt: nowIso,
37
- readCount: 0,
38
- uniqueFileCount: 0,
39
- repeatedReadCount: 0,
40
- largeReadCount: 0,
41
- totalBytesRead: 0,
42
- warnings: {},
43
- files: {},
44
- };
45
-
46
- try {
47
- const existing = JSON.parse(readFileSync(EFFICIENCY_STORE, 'utf8'));
48
- const lastUpdated = new Date(existing.lastUpdatedAt || 0).getTime();
49
- if (!lastUpdated || Date.now() - lastUpdated > SESSION_IDLE_RESET_MS) return fresh;
50
- return { ...fresh, ...existing, warnings: existing.warnings || {}, files: existing.files || {} };
51
- } catch {
52
- return fresh;
53
- }
54
- }
55
-
56
- function appendWarning(message) {
57
- try { appendFileSync(WARN_FLAGS, `${message}\n`); } catch { /* best effort */ }
58
- }
59
-
60
- function topRepeatedPath(files) {
61
- return Object.entries(files || {})
62
- .map(([filePath, value]) => ({ filePath, count: Number(value?.count || 0) }))
63
- .filter((entry) => entry.count > 1)
64
- .sort((a, b) => b.count - a.count || a.filePath.localeCompare(b.filePath))[0];
65
- }
66
-
67
18
  let input = {};
68
19
  try { input = JSON.parse(readFileSync(0, 'utf8')); } catch { process.exit(0); }
69
20
 
@@ -81,7 +32,6 @@ try {
81
32
  const nowIso = new Date().toISOString();
82
33
  const requestedLimit = Number(input?.tool_input?.limit || 0);
83
34
  const effectiveLimit = requestedLimit > 0 ? requestedLimit : 2000;
84
- const isLargeRead = effectiveLimit > 400;
85
35
 
86
36
  mkdirSync(CX_DIR, { recursive: true });
87
37
  let store = {};
@@ -95,58 +45,17 @@ try {
95
45
  if (new Date(v.ts).getTime() < cutoff) delete store[k];
96
46
  }
97
47
 
98
- const stats = loadEfficiencyStats(nowIso);
99
- const existingFile = stats.files[absPath];
100
- stats.readCount += 1;
101
- stats.totalBytesRead += content.length;
102
- if (isLargeRead) stats.largeReadCount += 1;
103
- if (existingFile) {
104
- stats.repeatedReadCount += 1;
105
- } else {
106
- stats.uniqueFileCount += 1;
107
- }
108
- stats.files[absPath] = {
109
- count: (existingFile?.count || 0) + 1,
48
+ writeFileSync(HASH_STORE, JSON.stringify(store, null, 2));
49
+ recordReadDelta({
50
+ path: absPath,
110
51
  size: content.length,
111
- lastReadAt: nowIso,
112
- lastRequestedLimit: effectiveLimit,
113
- };
52
+ limit: effectiveLimit,
53
+ ts: nowIso,
54
+ }, process.env);
114
55
 
115
- // LRU cap: bound stats.files to FILES_LRU_CAP entries by lastReadAt so the
116
- // structure does not grow without limit across long sessions.
117
- const fileEntries = Object.entries(stats.files);
118
- if (fileEntries.length > FILES_LRU_CAP) {
119
- fileEntries.sort((a, b) => (a[1]?.lastReadAt || '').localeCompare(b[1]?.lastReadAt || ''));
120
- const drop = fileEntries.length - FILES_LRU_CAP;
121
- for (let i = 0; i < drop; i++) delete stats.files[fileEntries[i][0]];
56
+ if (process.env.CONSTRUCT_READ_TRACKER_FLUSH === '1') {
57
+ flushReadTrackerDeltas({ nowIso, env: process.env });
122
58
  }
123
-
124
- if (stats.repeatedReadCount >= REPEATED_READ_WARNING_THRESHOLD && !stats.warnings.repeatedReads) {
125
- const top = topRepeatedPath(stats.files);
126
- const topNote = top ? ` Top repeat: ${top.filePath} (${top.count}x).` : '';
127
- appendWarning(`Efficiency: ${stats.repeatedReadCount} repeated reads this session.${topNote} Use rg or construct distill before re-reading more files.`);
128
- stats.warnings.repeatedReads = nowIso;
129
-
130
- if (stats.repeatedReadCount >= REPEATED_READ_TIER2_THRESHOLD && !stats.warnings.repeatedReadsTier2) {
131
- appendWarning(`Efficiency: ${stats.repeatedReadCount} repeated reads this session — consider using construct distill or rg before re-reading entire files.`);
132
- stats.warnings.repeatedReadsTier2 = nowIso;
133
- }
134
- }
135
-
136
- if (stats.largeReadCount >= LARGE_READ_WARNING_THRESHOLD && !stats.warnings.largeReads) {
137
- appendWarning(`Efficiency: ${stats.largeReadCount} large reads this session — prefer rg/glob plus targeted reads under 400 lines.`);
138
- stats.warnings.largeReads = nowIso;
139
- }
140
-
141
- if (stats.totalBytesRead >= TOTAL_BYTES_WARNING_THRESHOLD && !stats.warnings.totalBytes) {
142
- appendWarning(`Efficiency: ${Math.round(stats.totalBytesRead / 1024)} KB read this session — consider distill/query-focused retrieval or compact context before continuing.`);
143
- stats.warnings.totalBytes = nowIso;
144
- }
145
-
146
- stats.lastUpdatedAt = nowIso;
147
-
148
- writeFileSync(HASH_STORE, JSON.stringify(store, null, 2));
149
- writeFileSync(EFFICIENCY_STORE, JSON.stringify(stats, null, 2));
150
59
  } catch { /* best effort */ }
151
60
 
152
61
  process.exit(0);
@@ -9,7 +9,10 @@
9
9
  */
10
10
  import { execFileSync, execSync } from "node:child_process";
11
11
  import { readOpenCodeConfig } from "./opencode-config.mjs";
12
+ import fs from "node:fs";
12
13
  import net from "node:net";
14
+ import os from "node:os";
15
+ import path from "node:path";
13
16
 
14
17
  const LOWEST_PORT = 0;
15
18
  const HIGHEST_PORT = 65535;
@@ -46,7 +49,8 @@ function commandVersion(command, args = ["--version"]) {
46
49
  try {
47
50
  return execFileSync(command, args, { stdio: ["ignore", "pipe", "ignore"], timeout: 5000 })
48
51
  .toString()
49
- .trim();
52
+ .trim()
53
+ .split(/\r?\n/, 1)[0];
50
54
  } catch {
51
55
  return null;
52
56
  }
@@ -71,6 +75,49 @@ function versionAtLeast(version, minimum) {
71
75
  return true;
72
76
  }
73
77
 
78
+ function detectVsCodeAvailability(homeDir = os.homedir()) {
79
+ const settingsCandidates = (() => {
80
+ const platform = os.platform();
81
+ if (platform === "darwin") {
82
+ return [
83
+ path.join(homeDir, "Library", "Application Support", "Code", "User", "settings.json"),
84
+ path.join(homeDir, "Library", "Application Support", "Code - Insiders", "User", "settings.json"),
85
+ ];
86
+ }
87
+ if (platform === "linux") {
88
+ return [
89
+ path.join(homeDir, ".config", "Code", "User", "settings.json"),
90
+ path.join(homeDir, ".config", "Code - Insiders", "User", "settings.json"),
91
+ ];
92
+ }
93
+ if (platform === "win32") {
94
+ const appData = process.env.APPDATA ?? path.join(homeDir, "AppData", "Roaming");
95
+ return [
96
+ path.join(appData, "Code", "User", "settings.json"),
97
+ path.join(appData, "Code - Insiders", "User", "settings.json"),
98
+ ];
99
+ }
100
+ return [];
101
+ })();
102
+
103
+ const version = commandVersion("code") || commandVersion("code-insiders");
104
+ const hasSettings = settingsCandidates.some((candidate) => fs.existsSync(candidate));
105
+ return { version, hasSettings };
106
+ }
107
+
108
+ function detectCursorAvailability(homeDir = os.homedir()) {
109
+ const version = commandVersion("cursor") || commandVersion("cursor-agent");
110
+ const hasConfig = fs.existsSync(path.join(homeDir, ".cursor", "mcp.json"));
111
+ return { version, hasConfig };
112
+ }
113
+
114
+ function detectCopilotAvailability(homeDir = os.homedir()) {
115
+ const promptsDir = path.join(homeDir, ".github", "prompts");
116
+ const instructionsPath = path.join(homeDir, ".github", "copilot-instructions.md");
117
+ const hasFiles = fs.existsSync(promptsDir) || fs.existsSync(instructionsPath);
118
+ return { hasFiles };
119
+ }
120
+
74
121
  export function detectHostCapabilities() {
75
122
  const claudeRaw = commandVersion("claude");
76
123
  const claudeVersion = parseClaudeVersion(claudeRaw);
@@ -78,6 +125,9 @@ export function detectHostCapabilities() {
78
125
  const tmuxRaw = commandVersion("tmux", ["-V"]);
79
126
  const opencodeRaw = commandVersion("opencode");
80
127
  const codexRaw = commandVersion("codex");
128
+ const vscode = detectVsCodeAvailability();
129
+ const cursor = detectCursorAvailability();
130
+ const copilot = detectCopilotAvailability();
81
131
 
82
132
  return [
83
133
  {
@@ -123,6 +173,45 @@ export function detectHostCapabilities() {
123
173
  "Native profile switching is not automatic; continue in-session when dispatch is unavailable.",
124
174
  ],
125
175
  },
176
+ {
177
+ host: "VS Code",
178
+ availability: vscode.version || vscode.hasSettings ? "installed" : "missing",
179
+ version: vscode.version,
180
+ orchestration: "copilot-mcp",
181
+ promptableWorkers: false,
182
+ sharedTaskRuntime: "editor-session",
183
+ lifecycleHooks: [],
184
+ notes: [
185
+ "Construct manages GitHub Copilot MCP registrations in user settings when a VS Code settings.json already exists.",
186
+ "VS Code is an editor surface, not Construct's primary multi-agent runtime.",
187
+ ],
188
+ },
189
+ {
190
+ host: "Cursor",
191
+ availability: cursor.version || cursor.hasConfig ? "installed" : "missing",
192
+ version: cursor.version,
193
+ orchestration: "mcp-only",
194
+ promptableWorkers: false,
195
+ sharedTaskRuntime: "editor-session",
196
+ lifecycleHooks: [],
197
+ notes: [
198
+ "Construct manages Cursor MCP registrations in ~/.cursor/mcp.json when that file exists.",
199
+ "Cursor agent profiles are not generated today; use Construct through MCP-backed tools and shared project state.",
200
+ ],
201
+ },
202
+ {
203
+ host: "Copilot",
204
+ availability: copilot.hasFiles ? "installed" : "missing",
205
+ version: null,
206
+ orchestration: "prompt-profiles",
207
+ promptableWorkers: false,
208
+ sharedTaskRuntime: "editor-session",
209
+ lifecycleHooks: [],
210
+ notes: [
211
+ "Construct writes reusable prompt profiles under ~/.github/prompts/ plus a managed block in ~/.github/copilot-instructions.md.",
212
+ "Copilot does not expose Construct-style subagent dispatch; use the generated profiles for focused passes.",
213
+ ],
214
+ },
126
215
  ];
127
216
  }
128
217
 
@@ -591,6 +591,108 @@ async function askDocumentationQuestions() {
591
591
  return { lanes: selectedLanes, withArchitecture, withReadme };
592
592
  }
593
593
 
594
+ // Intake collection — suggest directories to watch for context
595
+ function discoverProjectDirs(targetPath) {
596
+ const dirs = [];
597
+ let entries;
598
+ try { entries = fs.readdirSync(targetPath, { withFileTypes: true }); } catch { return dirs; }
599
+
600
+ for (const entry of entries) {
601
+ if (!entry.isDirectory()) continue;
602
+ const name = entry.name;
603
+ // Skip hidden dirs, node_modules, build artifacts
604
+ if (name.startsWith('.')) continue;
605
+ if (name === 'node_modules') continue;
606
+ if (name === 'dist' || name === 'build' || name === '.next' || name === '.out') continue;
607
+ dirs.push(name);
608
+ }
609
+ return dirs.sort();
610
+ }
611
+
612
+ const INTAKE_DIR_PRESETS = [
613
+ { value: 'src', label: 'src/', reason: 'Source code — most projects put code here' },
614
+ { value: 'lib', label: 'lib/', reason: 'Library code' },
615
+ { value: 'packages', label: 'packages/', reason: 'Monorepo packages' },
616
+ { value: 'services', label: 'services/', reason: 'Microservices or backend services' },
617
+ { value: 'apps', label: 'apps/', reason: 'Monorepo apps' },
618
+ { value: 'docs', label: 'docs/', reason: 'Documentation source' },
619
+ { value: 'tests', label: 'tests/', reason: 'Test files' },
620
+ { value: 'spec', label: 'spec/', reason: 'Specifications' },
621
+ { value: 'infra', label: 'infra/', reason: 'Infrastructure code (Terraform, etc.)' },
622
+ { value: 'config', label: 'config/', reason: 'Configuration files' },
623
+ { value: 'scripts', label: 'scripts/', reason: 'Build/ops scripts' },
624
+ { value: 'tools', label: 'tools/', reason: 'Internal tooling' },
625
+ ];
626
+
627
+ async function askIntakeCollection(targetPath, skipInteractive) {
628
+ if (skipInteractive) {
629
+ // Non-interactive: auto-detect and suggest based on what exists
630
+ const existingDirs = discoverProjectDirs(targetPath);
631
+ const selected = INTAKE_DIR_PRESETS
632
+ .filter(preset => existingDirs.includes(preset.value))
633
+ .map(p => p.value);
634
+
635
+ if (selected.length === 0) return null;
636
+
637
+ console.log('');
638
+ console.log('═══════════════════════════════════════════════════════════');
639
+ console.log(' INTAKE COLLECTION');
640
+ console.log('═══════════════════════════════════════════════════════════');
641
+ console.log('');
642
+ console.log(`Auto-detected ${selected.length} directory(ies) to watch for context:`);
643
+ for (const d of selected) console.log(` • ${d}/`);
644
+ console.log('');
645
+ console.log('The inbox watcher will scan these directories for new files');
646
+ console.log('and route them through R&D triage automatically.');
647
+ console.log('');
648
+
649
+ return { parentDirs: selected, maxDepth: 4 };
650
+ }
651
+
652
+ // Interactive mode
653
+ const existingDirs = discoverProjectDirs(targetPath);
654
+ const presetOptions = INTAKE_DIR_PRESETS.map(p => ({
655
+ label: p.label,
656
+ value: p.value,
657
+ checked: existingDirs.includes(p.value),
658
+ description: p.reason,
659
+ }));
660
+
661
+ console.log('');
662
+ console.log('═══════════════════════════════════════════════════════════');
663
+ console.log(' INTAKE COLLECTION');
664
+ console.log('═══════════════════════════════════════════════════════════');
665
+ console.log('');
666
+ console.log('Intake watches directories for new files and routes them');
667
+ console.log('through R&D triage. Select which directories to watch.');
668
+ console.log('(directories already found in your project are pre-checked)');
669
+ console.log('');
670
+
671
+ const selected = await multiSelect({
672
+ title: 'Directories to Watch',
673
+ instructions: 'Press Enter to confirm your selection',
674
+ options: presetOptions,
675
+ });
676
+
677
+ if (selected.length === 0) {
678
+ console.log('');
679
+ console.log('No directories selected. The inbox watcher will only scan');
680
+ console.log('.cx/inbox/ and docs/intake/ (built-in zones).');
681
+ console.log('');
682
+ console.log('You can always add more later with:');
683
+ console.log(' construct intake config set --add-dir=src');
684
+ console.log('');
685
+ return null;
686
+ }
687
+
688
+ console.log('');
689
+ console.log(`Watching ${selected.length} directory(ies):`);
690
+ for (const d of selected) console.log(` • ${d}/`);
691
+ console.log('');
692
+
693
+ return { parentDirs: selected, maxDepth: 4 };
694
+ }
695
+
594
696
  function buildProjectReadme(projectName) {
595
697
  return `# ${projectName}
596
698
 
@@ -824,6 +926,20 @@ async function main() {
824
926
  console.log('');
825
927
  }
826
928
 
929
+ // Intake collection — ask which directories to watch for context
930
+ console.log('[TRACE init:intake-ask]');
931
+
932
+ const intakeConfig = await askIntakeCollection(target, skipInteractive);
933
+ if (intakeConfig) {
934
+ const { saveIntakeConfig } = await import('./intake/intake-config.mjs');
935
+ try {
936
+ saveIntakeConfig(target, intakeConfig);
937
+ created.push('.cx/intake-config.json');
938
+ } catch (err) {
939
+ console.warn(`⚠️ Could not write intake config: ${err.message}`);
940
+ }
941
+ }
942
+
827
943
  // Ask about documentation system
828
944
  console.log('[TRACE init:docs-ask]');
829
945
 
@@ -895,7 +1011,7 @@ async function main() {
895
1011
  // can compare new intake against established PRDs/RFCs/ADRs from day
896
1012
  // one. Best-effort: skipped silently when Postgres + embedding model
897
1013
  // aren't ready yet (DATABASE_URL unset, no ONNX cache). User can re-run
898
- // `construct ingest` or `construct setup` later to seed manually.
1014
+ // `construct ingest` or `construct init` later to seed manually.
899
1015
 
900
1016
  try {
901
1017
  const { syncFileStateToSql } = await import('./storage/sync.mjs');
@@ -950,10 +1066,10 @@ async function main() {
950
1066
  if (verbose) console.log('\nStarting services...');
951
1067
  try {
952
1068
  const { startServices } = await import('./service-manager.mjs');
953
- const { os } = await import('node:os');
1069
+ const { homedir } = await import('node:os');
954
1070
  const { detectDockerCompose } = await import('./setup.mjs');
955
1071
 
956
- const homeDir = os.homedir();
1072
+ const homeDir = homedir();
957
1073
  const composeRunner = detectDockerCompose();
958
1074
 
959
1075
  const { results } = await startServices({