@geraldmaron/construct 1.0.23 → 1.0.24

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 (52) hide show
  1. package/README.md +0 -2
  2. package/bin/construct +15 -215
  3. package/lib/embed/inbox.mjs +6 -3
  4. package/lib/embed/recommendation-store.mjs +7 -289
  5. package/lib/embed/reconcile.mjs +2 -2
  6. package/lib/hooks/config-protection.mjs +4 -4
  7. package/lib/hooks/session-reflect.mjs +5 -1
  8. package/lib/intake/git-queue.mjs +195 -0
  9. package/lib/intake/queue.mjs +9 -16
  10. package/lib/mcp/tools/storage.mjs +2 -3
  11. package/lib/mcp-catalog.json +3 -3
  12. package/lib/mcp-manager.mjs +59 -3
  13. package/lib/observation-store.mjs +38 -166
  14. package/lib/orchestration/runtime.mjs +3 -2
  15. package/lib/reconcile/index.mjs +0 -2
  16. package/lib/service-manager.mjs +38 -256
  17. package/lib/setup.mjs +26 -426
  18. package/lib/status.mjs +3 -6
  19. package/lib/storage/admin.mjs +48 -325
  20. package/lib/storage/backend.mjs +10 -57
  21. package/lib/storage/hybrid-query.mjs +15 -196
  22. package/lib/storage/sync.mjs +36 -177
  23. package/lib/storage/vector-client.mjs +256 -235
  24. package/lib/strategy-store.mjs +35 -286
  25. package/lib/worker/entrypoint.mjs +6 -14
  26. package/package.json +6 -5
  27. package/platforms/claude/settings.template.json +0 -7
  28. package/scripts/sync-specialists.mjs +46 -12
  29. package/specialists/prompts/cx-qa.md +1 -1
  30. package/specialists/registry.json +0 -8
  31. package/templates/docs/construct_guide.md +1 -1
  32. package/db/schema/001_init.sql +0 -40
  33. package/db/schema/002_pgvector.sql +0 -182
  34. package/db/schema/003_intake.sql +0 -47
  35. package/db/schema/003_observation_reconciliation.sql +0 -14
  36. package/db/schema/004_recommendations.sql +0 -46
  37. package/db/schema/005_strategy.sql +0 -21
  38. package/db/schema/006_graph.sql +0 -24
  39. package/db/schema/007_tags.sql +0 -30
  40. package/db/schema/008_skill_usage.sql +0 -24
  41. package/db/schema/009_scheduler.sql +0 -14
  42. package/db/schema/010_cx_scores.sql +0 -51
  43. package/lib/intake/postgres-queue.mjs +0 -240
  44. package/lib/reconcile/postgres-namespace.mjs +0 -102
  45. package/lib/services/local-postgres.mjs +0 -15
  46. package/lib/storage/backup.mjs +0 -347
  47. package/lib/storage/migrations.mjs +0 -187
  48. package/lib/storage/postgres-backup.mjs +0 -124
  49. package/lib/storage/sql-store.mjs +0 -45
  50. package/lib/storage/store-version.mjs +0 -115
  51. package/lib/storage/unified-storage.mjs +0 -550
  52. package/lib/storage/vector-store.mjs +0 -100
package/lib/setup.mjs CHANGED
@@ -2,15 +2,18 @@
2
2
  /**
3
3
  * lib/setup.mjs — Machine-scoped first-run setup wizard for Construct.
4
4
  *
5
- * Installs cm/cass, configures managed defaults, starts local Postgres
6
- * (consent-driven), pre-warms the embedding model, wires MCP integrations,
7
- * and writes ~/.construct/config.env. Invoked by `construct install` and
8
- * by lib/install/first-invocation.mjs when a TTY user is missing resources.
5
+ * Installs cm/cass, configures managed defaults, pre-warms the embedding model,
6
+ * wires MCP integrations, and writes ~/.construct/config.env.
7
+ * Invoked by `construct install` and by lib/install/first-invocation.mjs
8
+ * when a TTY user is missing resources.
9
9
  *
10
10
  * Project-scoped scaffolding (`.cx/`, AGENTS.md, plan.md, adapters) lives
11
11
  * in lib/init-unified.mjs — invoked by `construct init`. These two flows
12
12
  * are intentionally separate: install runs once per machine, init runs
13
13
  * once per repo.
14
+ *
15
+ * Construct now uses embedded LanceDB and Git-backed state, removing the
16
+ * dependency on local Docker-managed Postgres.
14
17
  */
15
18
 
16
19
  import fs from 'node:fs';
@@ -26,51 +29,34 @@ import {
26
29
  getDeploymentMode,
27
30
  } from './deployment-mode.mjs';
28
31
  import { getCanonicalOpenCodeConfigPath, readOpenCodeConfig, writeOpenCodeConfig } from './opencode-config.mjs';
29
- import { syncFileStateToSql } from './storage/sync.mjs';
30
- import { runMigrations } from './storage/migrations.mjs';
31
- import { createSqlClient, closeSqlClient } from './storage/backend.mjs';
32
32
  import { getEmbeddingModelInfo, warmupEmbeddingModel } from './storage/embeddings-engine.mjs';
33
- import { restoreConstructDb } from './storage/postgres-backup.mjs';
34
33
  import { buildPressureGuardValues, installPressureGuardLaunchAgent, loadPressureGuardLaunchAgent } from './runtime-pressure.mjs';
35
34
  import { consentToInstall } from './setup-prompts.mjs';
36
- import { postgresPort, postgresContainerName } from './home-namespace.mjs';
37
35
 
38
36
  const ROOT_DIR = path.resolve(import.meta.dirname, '..');
39
37
  const HOME = os.homedir();
40
- const LOCAL_POSTGRES_USER = 'construct';
41
- const LOCAL_POSTGRES_PASSWORD = 'construct';
42
- const LOCAL_POSTGRES_DB = 'construct';
43
-
44
- // The local DB URL's port is derived per home (construct-lb7b), so build it at
45
- // the call site from the active homeDir rather than a single module-level const.
46
-
47
- function localDatabaseUrl(env = process.env, home = HOME) {
48
- return `postgresql://${LOCAL_POSTGRES_USER}:${LOCAL_POSTGRES_PASSWORD}@127.0.0.1:${postgresPort(env, home)}/${LOCAL_POSTGRES_DB}`;
49
- }
50
38
 
51
39
  function printHelp() {
52
40
  console.log(`Construct install — machine setup (once per machine)
53
41
 
54
42
  Usage:
55
- construct install [--scope=project|user|both] [--yes] [--no-docker] [--no-launch-agent] [--reconfigure]
43
+ construct install [--scope=project|user|both] [--yes] [--no-launch-agent] [--reconfigure]
56
44
 
57
45
  Flags:
58
46
  --scope=<s> project | user | both (default: project — see ADR-0029)
59
47
  project: no-op + scope guidance; Construct's project artifacts
60
48
  are written by \`construct init\` inside a repo.
61
49
  user: write machine-scope state (~/.construct/, MCP configs,
62
- local Postgres, ~/.claude/* via consent-gated sync).
50
+ ~/.claude/* via consent-gated sync).
63
51
  both: print project guidance, then run user-scope install.
64
52
  --yes accept detected defaults without prompting
65
- --no-docker skip local Postgres / Docker service setup
66
53
  --no-launch-agent skip macOS LaunchAgent background service registration
67
54
  --reconfigure re-prompt for service consent, ignoring cached answers
68
55
 
69
56
  What --scope=user does:
70
57
  - creates ~/.construct/config.env
71
58
  - ensures OpenCode config exists
72
- - configures managed defaults for local vector retrieval
73
- - starts local Postgres with Docker when available
59
+ - configures managed defaults for local vector retrieval (LanceDB)
74
60
  - checks required runtime tools and installs cm and cass when available
75
61
  - wires Memory, GitHub, and telemetry configuration
76
62
  - runs construct sync --global (which writes ~/.claude/* per consent)
@@ -100,23 +86,20 @@ function findCommand(command) {
100
86
  }
101
87
 
102
88
  export function defaultVectorIndexPath(homeDir = HOME) {
103
- return path.join(homeDir, '.construct', 'vector', 'index.json');
89
+ // LanceDB storage path
90
+ return path.join(homeDir, '.construct', 'vector', 'lancedb');
104
91
  }
105
92
 
106
- export async function buildManagedSetupValues({ homeDir = HOME, env = process.env, databaseUrl = '' } = {}) {
93
+ export async function buildManagedSetupValues({ homeDir = HOME, env = process.env } = {}) {
107
94
  const modelInfo = await getEmbeddingModelInfo({ env });
108
95
 
109
96
  const values = {
110
97
  CONSTRUCT_TRACE_BACKEND: env.CONSTRUCT_TRACE_BACKEND || 'local',
111
- CONSTRUCT_VECTOR_INDEX_PATH: env.CONSTRUCT_VECTOR_INDEX_PATH || defaultVectorIndexPath(homeDir),
98
+ CONSTRUCT_LANCEDB_PATH: env.CONSTRUCT_LANCEDB_PATH || defaultVectorIndexPath(homeDir),
112
99
  CONSTRUCT_VECTOR_MODEL: env.CONSTRUCT_VECTOR_MODEL || modelInfo.model,
113
100
  ...buildPressureGuardValues({ env }),
114
101
  };
115
- // Deployment mode is persisted in construct.config.json — setup never writes
116
- // CONSTRUCT_DEPLOYMENT_MODE to ~/.construct/config.env. The env var is
117
- // reserved for ephemeral runtime overrides (CI/scripts via `export …`).
118
- // Forwarding it here pinned the env value and silently masked any JSON
119
- // setter that ran later.
102
+
120
103
  if (env[DEPLOYMENT_MODE_ENV_KEY] && getDeploymentMode(env) !== DEFAULT_DEPLOYMENT_MODE) {
121
104
  values[DEPLOYMENT_MODE_ENV_KEY] = getDeploymentMode(env);
122
105
  }
@@ -125,53 +108,10 @@ export async function buildManagedSetupValues({ homeDir = HOME, env = process.en
125
108
  if (env.CONSTRUCT_TELEMETRY_PUBLIC_KEY) values.CONSTRUCT_TELEMETRY_PUBLIC_KEY = env.CONSTRUCT_TELEMETRY_PUBLIC_KEY;
126
109
  if (env.CONSTRUCT_TELEMETRY_SECRET_KEY) values.CONSTRUCT_TELEMETRY_SECRET_KEY = env.CONSTRUCT_TELEMETRY_SECRET_KEY;
127
110
 
128
- const resolvedDatabaseUrl = databaseUrl || env.DATABASE_URL || '';
129
- if (resolvedDatabaseUrl) values.DATABASE_URL = resolvedDatabaseUrl;
130
- if (env.CONSTRUCT_VECTOR_URL) values.CONSTRUCT_VECTOR_URL = env.CONSTRUCT_VECTOR_URL;
111
+ if (env.DATABASE_URL) values.DATABASE_URL = env.DATABASE_URL;
131
112
  return values;
132
113
  }
133
114
 
134
- export function localPostgresComposePath(homeDir = HOME) {
135
- return path.join(homeDir, '.construct', 'services', 'postgres', 'docker-compose.yml');
136
- }
137
-
138
- export function writeLocalPostgresCompose(homeDir = HOME) {
139
- const composePath = localPostgresComposePath(homeDir);
140
- fs.mkdirSync(path.dirname(composePath), { recursive: true });
141
-
142
- // Container, port, and volume are derived per home so isolated HOMEs do not
143
- // collide (construct-lb7b). The runtime (service-manager) derives the same
144
- // values from the same home, so they match.
145
-
146
- const container = postgresContainerName(process.env, homeDir);
147
- const port = postgresPort(process.env, homeDir);
148
- const volume = `${container}-data`;
149
- const content = `services:
150
- postgres:
151
- image: pgvector/pgvector:pg16
152
- container_name: ${container}
153
- restart: unless-stopped
154
- environment:
155
- POSTGRES_USER: ${LOCAL_POSTGRES_USER}
156
- POSTGRES_PASSWORD: ${LOCAL_POSTGRES_PASSWORD}
157
- POSTGRES_DB: ${LOCAL_POSTGRES_DB}
158
- ports:
159
- - "127.0.0.1:${port}:5432"
160
- volumes:
161
- - ${volume}:/var/lib/postgresql/data
162
- healthcheck:
163
- test: ["CMD-SHELL", "pg_isready -U ${LOCAL_POSTGRES_USER} -d ${LOCAL_POSTGRES_DB}"]
164
- interval: 5s
165
- timeout: 5s
166
- retries: 20
167
-
168
- volumes:
169
- ${volume}:
170
- `;
171
- fs.writeFileSync(composePath, content, 'utf8');
172
- return composePath;
173
- }
174
-
175
115
  function runQuiet(command, args, { env = process.env, spawn = spawnSync } = {}) {
176
116
  const result = spawn(command, args, {
177
117
  env,
@@ -180,26 +120,6 @@ function runQuiet(command, args, { env = process.env, spawn = spawnSync } = {})
180
120
  return result;
181
121
  }
182
122
 
183
- async function runAsyncQuiet(command, args, { env = process.env, spawn = spawn } = {}) {
184
- return new Promise((resolve) => {
185
- const timeout = setTimeout(() => {
186
- resolve({ status: -1, signal: 'SIGTERM' }); // treat timeout as error
187
- }, 5000);
188
- const child = spawn(command, args, {
189
- env,
190
- stdio: ['ignore', 'pipe', 'pipe'],
191
- });
192
- let stdout = '';
193
- let stderr = '';
194
- child.stdout.on('data', (data) => { stdout += data.toString(); });
195
- child.stderr.on('data', (data) => { stderr += data.toString(); });
196
- child.on('close', (code, signal) => {
197
- clearTimeout(timeout);
198
- resolve({ status: code || 0, signal, stdout, stderr });
199
- });
200
- });
201
- }
202
-
203
123
  export function commandExists(command, { env = process.env, spawn = spawnSync } = {}) {
204
124
  const checker = process.platform === 'win32' ? 'where' : 'which';
205
125
  return runQuiet(checker, [command], { env, spawn }).status === 0;
@@ -233,8 +153,6 @@ export function ensureCmInstalled({ env = process.env, spawn = spawnSync } = {})
233
153
  };
234
154
  }
235
155
 
236
- // Version 0.3.0 has a known issue where the FTS-rebuild loop can OOM on large corpora and fail to converge.
237
- // Version 0.4.0 resolves this. Below this version, we fail closed to avoid the problem.
238
156
  const CASS_MIN_VERSION = '0.4.0';
239
157
 
240
158
  function parseSemver(s) {
@@ -268,7 +186,7 @@ export function ensureCassInstalled({ env = process.env, spawn = spawnSync } = {
268
186
  }
269
187
  return {
270
188
  status: 'outdated',
271
- message: `cass ${version || 'unknown'} is older than required ${CASS_MIN_VERSION}. Older versions hit a frankensqlite FTS-rebuild OOM loop on large session corpora (upstream #168, #186, #110, #155).`,
189
+ message: `cass ${version || 'unknown'} is older than required ${CASS_MIN_VERSION}.`,
272
190
  installCommand: 'brew upgrade dicklesworthstone/tap/cass || brew install dicklesworthstone/tap/cass',
273
191
  };
274
192
  }
@@ -306,121 +224,11 @@ export function ensureCassInstalled({ env = process.env, spawn = spawnSync } = {
306
224
  };
307
225
  }
308
226
 
309
- /**
310
- * Platform-aware install hint shown when Docker isn't on PATH. Mirrors
311
- * what `supabase start` / `prisma init` print: don't auto-install, but
312
- * point at the canonical option for the user's OS so they have a one-line
313
- * command to copy.
314
- */
315
- export function dockerInstallHint(platform = process.platform) {
316
- if (platform === 'darwin') {
317
- return 'Install Docker Desktop (https://www.docker.com/products/docker-desktop) or a lighter alternative like OrbStack (`brew install orbstack`) or Colima (`brew install colima && colima start`).';
318
- }
319
- if (platform === 'win32') {
320
- return 'Install Docker Desktop for Windows (https://www.docker.com/products/docker-desktop).';
321
- }
322
- return 'Install Docker Engine (https://docs.docker.com/engine/install/) — your distribution likely has a `docker` package (apt/dnf/pacman).';
323
- }
324
-
325
- export function detectDockerCompose({ env = process.env, spawn = spawnSync } = {}) {
326
- // Use a timeout wrapper so we don't block forever if Docker daemon is hung/missing
327
- const timeoutWrap = (fn) => {
328
- let timedOut = false;
329
- const timer = setTimeout(() => { timedOut = true; }, 5000);
330
- try {
331
- const result = fn();
332
- clearTimeout(timer);
333
- return timedOut ? null : result;
334
- } catch {
335
- clearTimeout(timer);
336
- return null;
337
- }
338
- };
339
-
340
- const docker = timeoutWrap(() => runQuiet('docker', ['info'], { env, spawn }));
341
- if (!docker || docker.status !== 0) return null;
342
- const compose = timeoutWrap(() => runQuiet('docker', ['compose', 'version'], { env, spawn }));
343
- if (compose && compose.status === 0) return { command: 'docker', argsPrefix: ['compose'] };
344
- const dockerCompose = timeoutWrap(() => runQuiet('docker-compose', ['version'], { env, spawn }));
345
- if (dockerCompose && dockerCompose.status === 0) return { command: 'docker-compose', argsPrefix: [] };
346
- return null;
347
- }
348
-
349
- // Duplicated from service-manager.spawnDetached to avoid the import cycle
350
- // setup ↔ service-manager (service-manager already imports detectDockerCompose
351
- // from setup, so the reverse path can't exist).
352
-
353
- function spawnDetachedForSetup(command, args, homeDir, logFile, options = {}) {
354
- const logDir = path.join(homeDir, '.construct', 'runtime');
355
- fs.mkdirSync(logDir, { recursive: true });
356
- const logPath = path.join(logDir, logFile);
357
- const fd = fs.openSync(logPath, 'a');
358
- const child = spawn(command, args, {
359
- detached: true,
360
- stdio: ['ignore', fd, fd],
361
- cwd: options.cwd,
362
- env: options.env,
363
- });
364
- child.unref();
365
- return { child, logPath };
366
- }
367
-
368
- export function startManagedPostgres({ homeDir = HOME, env = process.env, spawn = spawnSync } = {}) {
369
- const composeRunner = detectDockerCompose({ env, spawn });
370
- if (!composeRunner) {
371
- return {
372
- status: 'skipped',
373
- databaseUrl: env.DATABASE_URL || '',
374
- message: 'Docker is not available; using existing DATABASE_URL if configured.',
375
- };
376
- }
377
-
378
- const composePath = writeLocalPostgresCompose(homeDir);
379
- const result = runQuiet(
380
- composeRunner.command,
381
- [...composeRunner.argsPrefix, '-f', composePath, 'up', '-d', 'postgres'],
382
- { env, spawn },
383
- );
384
-
385
- if (result.status !== 0) {
386
- return {
387
- status: 'degraded',
388
- databaseUrl: env.DATABASE_URL || '',
389
- composePath,
390
- message: (result.stderr || result.stdout || 'Docker compose failed').trim(),
391
- };
392
- }
393
-
394
- return {
395
- status: 'ok',
396
- databaseUrl: localDatabaseUrl(env, homeDir),
397
- composePath,
398
- message: 'Managed local Postgres is running.',
399
- };
400
- }
401
-
402
- async function waitForSqlReady(env, { attempts = 20, delayMs = 500 } = {}) {
403
- for (let attempt = 0; attempt < attempts; attempt += 1) {
404
- const client = createSqlClient(env);
405
- try {
406
- await client`select 1 as ok`;
407
- await closeSqlClient(client);
408
- return true;
409
- } catch {
410
- await closeSqlClient(client).catch(() => {});
411
- await new Promise((resolve) => setTimeout(resolve, delayMs));
412
- }
413
- }
414
- return false;
415
- }
416
-
417
227
  function warnIfGlobalCommandIsUnavailable() {
418
228
  const globalConstruct = findCommand('construct');
419
229
  if (!globalConstruct) {
420
230
  console.log('\nInstall warning: `construct` is not on PATH yet.');
421
231
  console.log(' From this checkout, run: npm install -g .');
422
- console.log(' Without cloning, run: npm install -g github:geraldmaron/construct');
423
- console.log(' Do not use `npm install -g construct`; that npm name belongs to another project.');
424
232
  return;
425
233
  }
426
234
 
@@ -431,7 +239,6 @@ function warnIfGlobalCommandIsUnavailable() {
431
239
  if (version.status !== 0 || !/^construct v\d+\./.test(version.stdout.trim())) {
432
240
  console.log(`\nInstall warning: PATH resolves \`construct\` to ${globalConstruct}, but it does not look like this CLI.`);
433
241
  console.log(' Reinstall from this checkout with: npm install -g .');
434
- console.log(' Or install from GitHub with: npm install -g github:geraldmaron/construct');
435
242
  }
436
243
  }
437
244
 
@@ -446,25 +253,8 @@ function ensureOpenCodeConfig() {
446
253
  return getCanonicalOpenCodeConfigPath();
447
254
  }
448
255
 
449
- // Wire core.hooksPath to .beads/hooks so the tracked hook scripts (ECC
450
- // secret-scan, Construct policy gates, BEADS dispatcher) actually fire on
451
- // git commit/push. Without this, .beads/hooks/* are inert — present in the
452
- // repo but never executed by git. Idempotent: leaves a user-set value alone
453
- // rather than clobbering it.
454
-
455
- // Project-scoped git-hooks wiring lives in its own module and is owned by
456
- // `construct init`. Re-exported here for importers that reference it via
457
- // setup.mjs (e.g. tests/setup-git-hooks-path.test.mjs). `construct install`
458
- // must never mutate the cwd repo (ADR-0027 §3), so the install flow leaves
459
- // hooks wiring to init.
460
-
461
256
  export { ensureGitHooksPath } from './git-hooks-path.mjs';
462
257
 
463
- // $HOME/.construct/lib/hooks/* is the resolution path for Claude Code
464
- // Stop-hook commands. Absent symlink → every Stop hook fails silently,
465
- // including stop-notify (dashboard cost feed). Idempotent: matching
466
- // symlink left alone, stale symlink replaced, real dir refused.
467
-
468
258
  export function ensureLibSymlink({ homeDir = HOME, rootDir = ROOT_DIR } = {}) {
469
259
  const target = path.join(homeDir, '.construct', 'lib');
470
260
  const source = path.join(rootDir, 'lib');
@@ -485,15 +275,10 @@ export function ensureLibSymlink({ homeDir = HOME, rootDir = ROOT_DIR } = {}) {
485
275
  return {
486
276
  status: 'conflict',
487
277
  target,
488
- message: `${target} exists and is not a symlink — leaving alone. Move or remove it, then re-run \`construct install\` to wire the hooks.`,
278
+ message: `${target} exists and is not a symlink — leaving alone.`,
489
279
  };
490
280
  }
491
281
 
492
- // ADR-0029: install is explicitly scoped. Default is `project`, which writes
493
- // nothing and prints scope guidance — silent global writes are the failure mode
494
- // that ADR retires. `user` runs the machine-state body below; `both` prints the
495
- // project guidance and then runs user-scope.
496
-
497
282
  const VALID_SCOPES = new Set(['project', 'user', 'both']);
498
283
  const DEFAULT_SCOPE = 'project';
499
284
 
@@ -516,13 +301,11 @@ function printProjectScopeGuidance() {
516
301
  console.log(' construct install --scope=user');
517
302
  console.log('To do both project (guidance) and user-scope install:');
518
303
  console.log(' construct install --scope=both');
519
- console.log('\nSee ADR-0029 (docs/adr/0029-install-scopes-and-hook-budgets.md) for the contract.');
520
304
  }
521
305
 
522
306
  export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME } = {}) {
523
307
  const argSet = new Set(args);
524
308
  const isYes = argSet.has('--yes');
525
- const skipDocker = argSet.has('--no-docker');
526
309
  const reconfigure = argSet.has('--reconfigure');
527
310
 
528
311
  if (argSet.has('--help') || argSet.has('-h')) {
@@ -530,10 +313,7 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
530
313
  return;
531
314
  }
532
315
 
533
- // Reject unknown flags loudly so a typo (e.g. --reconfig) fails fast with the
534
- // help text instead of silently running defaults.
535
-
536
- const KNOWN_FLAGS = new Set(['--yes', '--no-docker', '--no-launch-agent', '--reconfigure', '--help', '-h']);
316
+ const KNOWN_FLAGS = new Set(['--yes', '--no-launch-agent', '--reconfigure', '--help', '-h']);
537
317
  const unknownFlags = args.filter((a) => {
538
318
  if (!a.startsWith('-')) return false;
539
319
  if (a.startsWith('--scope=') || a === '--scope') return false;
@@ -575,68 +355,25 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
575
355
  console.log(`OpenCode config: ${opencodePath}`);
576
356
  if (libLink.status === 'created' || libLink.status === 'replaced') {
577
357
  console.log(`Hook lib link: ${libLink.target} → ${libLink.source} (${libLink.status})`);
578
- } else if (libLink.status === 'kept') {
579
- console.log(`Hook lib link: ${libLink.target} already in place`);
580
- } else if (libLink.status === 'conflict') {
581
- console.log(`Hook lib link: ${libLink.message}`);
582
358
  }
583
359
  warnIfGlobalCommandIsUnavailable();
584
360
 
585
361
  const cmInstall = ensureCmInstalled({ env: process.env });
586
- if (cmInstall.status === 'installed') {
587
- console.log('Memory CLI: installed cm via Homebrew');
588
- } else if (cmInstall.status === 'available') {
589
- console.log('Memory CLI: cm available');
362
+ if (cmInstall.status === 'installed' || cmInstall.status === 'available') {
363
+ console.log(`Memory CLI: ${cmInstall.message}`);
590
364
  } else {
591
365
  console.log(`Memory CLI: ${cmInstall.message}`);
592
366
  if (cmInstall.installCommand) console.log(` Install with: ${cmInstall.installCommand}`);
593
367
  }
594
368
 
595
369
  const cassInstall = ensureCassInstalled({ env: process.env });
596
- if (cassInstall.status === 'installed') {
370
+ if (cassInstall.status === 'installed' || cassInstall.status === 'available') {
597
371
  console.log(`Session search: ${cassInstall.message}`);
598
- } else if (cassInstall.status === 'available') {
599
- console.log('Session search: cass available');
600
372
  } else {
601
373
  console.log(`Session search: ${cassInstall.message}`);
602
374
  if (cassInstall.installCommand) console.log(` Install with: ${cassInstall.installCommand}`);
603
375
  }
604
376
 
605
- // Local Postgres — consent-driven. Interactive default-yes; --yes accepts
606
- // without prompting. Skipped when --no-docker, when DATABASE_URL is already
607
- // set (caller has an external DB), or when user declines.
608
-
609
- const dockerRunner = detectDockerCompose();
610
- const dockerAvailable = !skipDocker && Boolean(dockerRunner);
611
- let serviceResult = {
612
- status: 'skipped',
613
- databaseUrl: process.env.DATABASE_URL || '',
614
- message: skipDocker ? 'Docker service setup skipped by flag.' : 'Docker not detected — skipping local Postgres.',
615
- };
616
-
617
- if (dockerAvailable) {
618
- const pgConsent = await consentToInstall({
619
- name: 'postgres',
620
- isYes,
621
- force: reconfigure,
622
- alreadyConfigured: Boolean(process.env.DATABASE_URL),
623
- alreadyConfiguredNote: 'DATABASE_URL already set — using external database.',
624
- envPath,
625
- });
626
- if (pgConsent.decision) {
627
- writeLocalPostgresCompose(homeDir);
628
- serviceResult = startManagedPostgres({ homeDir, env: process.env });
629
- console.log(`Postgres: ${serviceResult.status === 'ok' ? 'started locally' : serviceResult.message}`);
630
- } else {
631
- console.log(`Postgres: skipped (${pgConsent.note})`);
632
- }
633
- } else if (skipDocker) {
634
- console.log('Postgres: skipped (--no-docker)');
635
- } else {
636
- console.log('Postgres: skipped (Docker not detected — local Postgres unavailable)');
637
- console.log(` ${dockerInstallHint()}`);
638
- }
639
-
640
377
  const telemetryResult = process.env.CONSTRUCT_TELEMETRY_URL
641
378
  ? { status: 'configured', note: `remote export configured (${process.env.CONSTRUCT_TELEMETRY_URL})` }
642
379
  : { status: 'local', note: 'local JSONL traces in .cx/traces; remote export optional' };
@@ -644,13 +381,6 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
644
381
 
645
382
  fs.mkdirSync(path.dirname(defaultVectorIndexPath(homeDir)), { recursive: true });
646
383
 
647
- // Pre-warm the embedding model so the first agent query doesn't stall on
648
- // a one-time ONNX download. Same pattern as `supabase start` pre-pulling
649
- // images and `playwright install` pre-fetching browsers — make the wait
650
- // visible during setup, not during user-facing work. Best-effort: a
651
- // download failure here degrades gracefully via the engine's hash fallback
652
- // and is non-fatal to setup.
653
-
654
384
  try {
655
385
  const warm = await warmupEmbeddingModel({ env: process.env });
656
386
  if (warm.degraded) {
@@ -662,12 +392,7 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
662
392
  console.log(`Embeddings: warmup skipped (${err?.message || 'unknown error'}) — model will load on first use`);
663
393
  }
664
394
 
665
- // Cheapest provider selection opt-out by default. When user consents,
666
- // evaluate all configured providers, pick the lowest-cost model per tier,
667
- // and write to config.env. On subsequent runs the preference is persisted
668
- // so the prompt is skipped.
669
-
670
- const { isCheapestProviderEnabled, selectCheapestForAllTiers, setCheapestProviderPreference, formatCheapestProviderMessage } =
395
+ const { isCheapestProviderEnabled, selectCheapestForAllTiers, setCheapestProviderPreference } =
671
396
  await import('./model-cheapest-provider.mjs');
672
397
  const cheapestAlreadyEnabled = isCheapestProviderEnabled(envPath, { env: process.env });
673
398
  if (!cheapestAlreadyEnabled) {
@@ -694,29 +419,22 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
694
419
  const label = selections[tier]?.providerLabel || '';
695
420
  console.log(` ${tier.padEnd(11)} ${model} (${label})`);
696
421
  }
697
- } else {
698
- console.log('\nCheapest provider: no configured providers found — nothing to apply.');
699
422
  }
700
423
  setCheapestProviderPreference(envPath, true);
701
424
  } catch (err) {
702
425
  console.log(`Cheapest provider: skipped (${err?.message || 'unknown error'})`);
703
426
  }
704
- } else {
705
- console.log(`Cheapest provider: skipped (${cheapestConsent.note})`);
706
427
  }
707
- } else {
708
- console.log('Cheapest provider: already enabled — skipping prompt.');
709
428
  }
710
429
 
711
- // Ensure workspace directory with docs lanes exists
712
430
  ensureWorkspace(homeDir);
713
431
 
714
432
  const managedValues = await buildManagedSetupValues({
715
433
  homeDir,
716
434
  env: process.env,
717
- databaseUrl: serviceResult.databaseUrl,
718
435
  });
719
436
  writeEnvValues(envPath, managedValues);
437
+
720
438
  let pressureGuardAgent = null;
721
439
  let pressureGuardLoad = null;
722
440
  if (process.platform === 'darwin' && !argSet.has('--no-launch-agent')) {
@@ -737,110 +455,18 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
737
455
  nodePath: process.execPath,
738
456
  });
739
457
  pressureGuardLoad = loadPressureGuardLaunchAgent({ plistPath: pressureGuardAgent.plistPath });
740
- } else {
741
- console.log(`LaunchAgent: skipped (${laConsent.note})`);
742
- }
743
- } else if (argSet.has('--no-launch-agent')) {
744
- console.log('LaunchAgent: skipped (--no-launch-agent)');
745
- }
746
-
747
- if (isYes) {
748
- console.log('\nManaged setup:');
749
- console.log(` Deployment mode: ${getDeploymentMode(process.env) || DEFAULT_DEPLOYMENT_MODE} (set in construct.config.json — runtime env override available via ${DEPLOYMENT_MODE_ENV_KEY})`);
750
- console.log(` Vector index: ${managedValues.CONSTRUCT_VECTOR_INDEX_PATH}`);
751
- console.log(` Vector model: ${managedValues.CONSTRUCT_VECTOR_MODEL}`);
752
- console.log(` Trace backend: ${managedValues.CONSTRUCT_TRACE_BACKEND}`);
753
- if (managedValues.CONSTRUCT_TELEMETRY_URL) console.log(` Trace URL: ${managedValues.CONSTRUCT_TELEMETRY_URL}`);
754
- console.log(` Pressure guard: every ${managedValues.CONSTRUCT_PRESSURE_GUARD_INTERVAL_SECONDS}s, swap threshold ${managedValues.CONSTRUCT_PRESSURE_GUARD_SWAP_GB} GiB`);
755
- if (serviceResult.status === 'ok') {
756
- console.log(` Postgres: ${serviceResult.databaseUrl}`);
757
- console.log(` Compose file: ${serviceResult.composePath}`);
758
- } else {
759
- console.log(` Postgres: ${serviceResult.message}`);
760
- }
761
- if (pressureGuardAgent?.plistPath) {
762
- console.log(` LaunchAgent: ${pressureGuardAgent.plistPath}`);
763
- console.log(` LaunchAgent load: ${pressureGuardLoad?.loaded ? 'active' : pressureGuardLoad?.reason || 'pending manual load'}`);
764
- }
765
- }
766
-
767
- // Schema init runs whenever postgres is reachable — pgvector tables must exist before any
768
- // retrieval call. Idempotent via construct_schema_migrations hash tracking.
769
-
770
- if (serviceResult.status === 'ok') {
771
- try {
772
- await waitForSqlReady({ ...process.env, ...managedValues });
773
- const readyClient = createSqlClient({ ...process.env, ...managedValues });
774
- if (readyClient) {
775
- try {
776
- const { applied } = await runMigrations(readyClient);
777
- if (applied.length) console.log(`Postgres schema: applied ${applied.length} migration(s) — ${applied.join(', ')}`);
778
- else console.log('Postgres schema: up to date');
779
- } finally {
780
- await closeSqlClient(readyClient).catch(() => {});
781
- }
782
- }
783
- } catch (error) {
784
- console.log(`Postgres schema init failed: ${error?.message || 'unknown error'}`);
785
- }
786
- }
787
-
788
- if (isYes && serviceResult.status === 'ok') {
789
- const sqlClient = createSqlClient({ ...process.env, ...managedValues });
790
- if (sqlClient) {
791
- try {
792
- const restore = restoreConstructDb({ homeDir });
793
- if (restore.status === 'restored') {
794
- console.log(`Construct DB restored from stash: ${restore.stashPath}`);
795
- } else if (restore.status === 'no-stash') {
796
- console.log('No stash found — starting with empty construct DB.');
797
- } else {
798
- console.log(`Construct DB restore: ${restore.status}`);
799
- }
800
-
801
- // Index Construct's own package docs (rootDir, machine-scoped) into the
802
- // shared hybrid store. Seeding a downstream project's corpus is project
803
- // state and belongs to `construct init` / `construct ingest`, never to
804
- // machine install (ADR-0027 §3): install reads no cwd.
805
-
806
- const syncResult = await syncFileStateToSql(rootDir, { env: { ...process.env, ...managedValues }, project: 'construct' });
807
- console.log(`Hybrid storage sync: ${syncResult.status}`);
808
- if (syncResult.embeddingModel) console.log(`Embedding model: ${syncResult.embeddingModel}`);
809
- } catch (error) {
810
- console.log(`Hybrid storage sync failed: ${error?.message || 'unknown error'}`);
811
- } finally {
812
- await closeSqlClient(sqlClient).catch(() => {});
813
- }
814
458
  }
815
459
  }
816
460
 
817
461
  if (isYes) {
818
462
  runConstruct(['mcp', 'add', 'memory', '--auto'], { optional: true });
819
463
  runConstruct(['mcp', 'add', 'github', '--auto'], { optional: true });
820
- } else {
821
- console.log('\nManaged defaults written:');
822
- console.log(` Deployment mode: ${getDeploymentMode(process.env) || DEFAULT_DEPLOYMENT_MODE} (set in construct.config.json — runtime env override available via ${DEPLOYMENT_MODE_ENV_KEY})`);
823
- console.log(` Vector index: ${managedValues.CONSTRUCT_VECTOR_INDEX_PATH}`);
824
- console.log(` Trace backend: ${managedValues.CONSTRUCT_TRACE_BACKEND}${managedValues.CONSTRUCT_TELEMETRY_URL ? ` (${managedValues.CONSTRUCT_TELEMETRY_URL})` : ''}`);
825
- console.log(` Pressure guard: swap ${managedValues.CONSTRUCT_PRESSURE_GUARD_SWAP_GB} GiB, opencode max ${managedValues.CONSTRUCT_PRESSURE_GUARD_MAX_OPENCODE}`);
826
- console.log('\nFor unattended setup, including local Postgres when Docker is running:');
827
- console.log(' construct install --yes');
828
464
  }
829
465
 
830
- // Install is machine setup: the global front-door agent must land on every
831
- // user-scope surface (opencode/claude/codex/copilot) or a fresh machine fails
832
- // cross-surface parity. Both `sync` and `sync --global` are kept to preserve
833
- // every write across the project-detected and forced-global paths; in a
834
- // non-project install cwd both resolve to the same global branch and would
835
- // print the "Synced … to global scope" summary twice. The first call runs
836
- // --quiet so the canonical summary prints exactly once from `sync --global`,
837
- // with no change to what either writes.
838
-
839
466
  runConstruct(['sync', '--quiet']);
840
467
  runConstruct(['sync', '--global']);
841
468
  runConstruct(['doctor']);
842
469
 
843
- // ── Summary panel ────────────────────────────────────────────────────────
844
470
  const setupTs = new Date().toISOString().slice(0, 19).replace(/[:.]/g, '-');
845
471
  const setupLogPath = path.join(HOME, '.cx', `setup-${setupTs}.log`);
846
472
  try {
@@ -850,42 +476,21 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
850
476
  `Config: ${getUserEnvPath(HOME)}`,
851
477
  `OpenCode: ${getCanonicalOpenCodeConfigPath(HOME)}`,
852
478
  ];
853
- if (managedValues?.DATABASE_URL) {
854
- logLines.push(`Database: ${managedValues.DATABASE_URL.replace(/:\/\/[^@]+@/, '://<credentials>@')}`);
855
- }
856
479
  fs.writeFileSync(setupLogPath, logLines.join('\n') + '\n');
857
480
  } catch { /* best effort */ }
858
481
 
859
482
  console.log('\n────────────────────────────────────');
860
483
  console.log('Setup complete.');
861
484
 
862
- // Local-service summary — surfaces running local services. Same pattern
863
- // as `supabase start`, which prints all local URLs + keys after spin-up.
864
-
865
- const hasLocalPostgres = serviceResult?.status === 'ok' && serviceResult?.databaseUrl?.includes('127.0.0.1');
866
- const vectorBackend = hasLocalPostgres
867
- ? { label: 'Postgres + pgvector', detail: `${serviceResult.databaseUrl} (384d embeddings, ${managedValues.CONSTRUCT_VECTOR_MODEL})` }
868
- : { label: 'JSON fallback', detail: `${managedValues.CONSTRUCT_VECTOR_INDEX_PATH} (${managedValues.CONSTRUCT_VECTOR_MODEL})` };
869
-
870
485
  console.log('\nLocal services:');
871
486
  console.log(' Traces: local JSONL (.cx/traces)');
872
- if (process.env.CONSTRUCT_TELEMETRY_URL) console.log(` Telemetry: remote export (${process.env.CONSTRUCT_TELEMETRY_URL})`);
873
- else console.log(' Telemetry: remote export not configured');
874
- if (hasLocalPostgres) {
875
- console.log(` Postgres: ${serviceResult.databaseUrl}`);
876
- } else if (process.env.DATABASE_URL) {
877
- console.log(` Postgres: external (${process.env.DATABASE_URL.replace(/:\/\/[^@]+@/, '://<credentials>@')})`);
878
- } else {
879
- console.log(` Postgres: not running — vector retrieval will use JSON fallback`);
880
- }
881
- console.log(` Vector: ${vectorBackend.label}`);
882
- console.log(` ${vectorBackend.detail}`);
487
+ console.log(` Vector: LanceDB (embedded)`);
488
+ console.log(` ${managedValues.CONSTRUCT_LANCEDB_PATH}`);
883
489
  console.log(' Credentials are saved to ~/.construct/config.env for later reference.');
884
490
 
885
491
  console.log('\nNext steps:');
886
492
  console.log(' construct provider add github # Connect GitHub repository data');
887
493
  console.log(' construct doctor # Verify all systems');
888
- console.log(' construct evals retrieval # Baseline retrieval quality');
889
494
  console.log(`\nSetup log: ${setupLogPath}`);
890
495
  }
891
496
 
@@ -896,17 +501,12 @@ function ensureUserConfig(homeDir = HOME) {
896
501
  return envPath;
897
502
  }
898
503
 
899
- /**
900
- * Create ~/.construct/workspace/ with the standard docs lane structure.
901
- * Fallback target for embed output that isn't repo-specific.
902
- */
903
504
  export function ensureWorkspace(homeDir = HOME) {
904
505
  const wsPath = path.join(homeDir, '.construct', 'workspace');
905
506
  const docsPath = path.join(wsPath, 'docs');
906
507
  for (const lane of WORKSPACE_DOCS_LANES) {
907
508
  fs.mkdirSync(path.join(docsPath, lane), { recursive: true });
908
509
  }
909
- // Ensure top-level workspace files exist
910
510
  const snapshotPath = path.join(wsPath, 'snapshot.md');
911
511
  if (!fs.existsSync(snapshotPath)) fs.writeFileSync(snapshotPath, '# Snapshot\n\nNo snapshot yet.\n');
912
512
  const roadmapPath = path.join(wsPath, 'roadmap.md');