@geraldmaron/construct 1.0.21 → 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 (150) hide show
  1. package/README.md +23 -7
  2. package/bin/construct +206 -228
  3. package/bin/construct-postinstall.mjs +25 -15
  4. package/lib/acp/server.mjs +113 -0
  5. package/lib/agent-instructions/inject.mjs +94 -0
  6. package/lib/auto-docs.mjs +10 -2
  7. package/lib/cli-commands.mjs +43 -15
  8. package/lib/comment-lint.mjs +115 -0
  9. package/lib/completions.mjs +7 -1
  10. package/lib/config/schema.mjs +4 -0
  11. package/lib/decisions/enforced-baseline.json +2 -0
  12. package/lib/docs-verify.mjs +15 -18
  13. package/lib/doctor/cli.mjs +8 -1
  14. package/lib/document-export.mjs +124 -0
  15. package/lib/embed/daemon.mjs +1 -1
  16. package/lib/embed/inbox.mjs +31 -10
  17. package/lib/embed/recommendation-store.mjs +7 -289
  18. package/lib/embed/reconcile.mjs +2 -2
  19. package/lib/features.mjs +11 -11
  20. package/lib/git-hooks-path.mjs +61 -0
  21. package/lib/home-namespace.mjs +60 -0
  22. package/lib/hooks/ci-status-check.mjs +62 -40
  23. package/lib/hooks/config-protection.mjs +4 -4
  24. package/lib/hooks/orchestration-dispatch-guard.mjs +153 -0
  25. package/lib/hooks/pre-push-gate.mjs +15 -6
  26. package/lib/hooks/session-reflect.mjs +5 -1
  27. package/lib/hooks/stop-notify.mjs +32 -17
  28. package/lib/hooks/stop-typecheck.mjs +7 -2
  29. package/lib/host-capabilities.mjs +24 -8
  30. package/lib/host-disposition.mjs +76 -0
  31. package/lib/ingest/provider-extract.mjs +1 -1
  32. package/lib/init-docs.mjs +1 -1
  33. package/lib/init-unified.mjs +320 -219
  34. package/lib/init-update.mjs +4 -84
  35. package/lib/init.mjs +9 -25
  36. package/lib/install/stage-project.mjs +8 -2
  37. package/lib/intake/git-queue.mjs +195 -0
  38. package/lib/intake/queue.mjs +9 -16
  39. package/lib/intent-classifier.mjs +1 -1
  40. package/lib/knowledge/search.mjs +52 -3
  41. package/lib/mcp/server.mjs +57 -14
  42. package/lib/mcp/tools/memory.mjs +2 -2
  43. package/lib/mcp/tools/orchestration-run.mjs +125 -0
  44. package/lib/mcp/tools/storage.mjs +2 -3
  45. package/lib/mcp-catalog.json +3 -3
  46. package/lib/mcp-manager.mjs +59 -3
  47. package/lib/model-registry.mjs +40 -33
  48. package/lib/observation-store.mjs +39 -163
  49. package/lib/opencode-config.mjs +1 -1
  50. package/lib/orchestration/events.mjs +66 -0
  51. package/lib/orchestration/runtime.mjs +72 -10
  52. package/lib/orchestration/worker.mjs +69 -21
  53. package/lib/orchestration-policy.mjs +27 -3
  54. package/lib/parity.mjs +46 -24
  55. package/lib/policy/unified-gates.mjs +96 -0
  56. package/lib/project-init-shared.mjs +0 -173
  57. package/lib/reconcile/adapter-prune.mjs +105 -0
  58. package/lib/reconcile/agent-instructions-rewrap.mjs +98 -0
  59. package/lib/reconcile/gitignore-coverage.mjs +88 -0
  60. package/lib/reconcile/index.mjs +169 -0
  61. package/lib/reconcile/legacy-doctrine-strip.mjs +139 -0
  62. package/lib/reconcile/legacy-guide-decommit.mjs +67 -0
  63. package/lib/reconcile/legacy-skills-cleanup.mjs +200 -0
  64. package/lib/reconcile/mcp-entry-reconcile.mjs +142 -0
  65. package/lib/runtime/uv-bootstrap.mjs +27 -3
  66. package/lib/schema-infer.mjs +16 -2
  67. package/lib/server/csrf.mjs +14 -2
  68. package/lib/server/index.mjs +95 -0
  69. package/lib/service-manager.mjs +59 -252
  70. package/lib/setup-prompts.mjs +2 -1
  71. package/lib/setup.mjs +104 -480
  72. package/lib/status.mjs +3 -6
  73. package/lib/storage/admin.mjs +48 -325
  74. package/lib/storage/backend.mjs +10 -43
  75. package/lib/storage/hybrid-query.mjs +15 -196
  76. package/lib/storage/sync.mjs +36 -177
  77. package/lib/storage/vector-client.mjs +256 -235
  78. package/lib/strategy-store.mjs +35 -286
  79. package/lib/template-registry.mjs +73 -0
  80. package/lib/term-format.mjs +75 -0
  81. package/lib/uninstall/uninstall.mjs +180 -7
  82. package/lib/worker/entrypoint.mjs +6 -14
  83. package/package.json +7 -6
  84. package/personas/construct.md +7 -8
  85. package/platforms/claude/settings.template.json +30 -11
  86. package/platforms/opencode/config.template.json +2 -2
  87. package/rules/common/neurodivergent-output.md +66 -0
  88. package/rules/common/tool-invisibility.md +37 -0
  89. package/scripts/sync-specialists.mjs +427 -107
  90. package/skills/operating/orchestration-reference.md +2 -16
  91. package/specialists/policy-inventory.json +14 -0
  92. package/specialists/prompts/cx-accessibility.md +2 -6
  93. package/specialists/prompts/cx-ai-engineer.md +0 -4
  94. package/specialists/prompts/cx-architect.md +3 -5
  95. package/specialists/prompts/cx-business-strategist.md +0 -5
  96. package/specialists/prompts/cx-data-analyst.md +0 -4
  97. package/specialists/prompts/cx-data-engineer.md +0 -4
  98. package/specialists/prompts/cx-debugger.md +2 -6
  99. package/specialists/prompts/cx-designer.md +0 -8
  100. package/specialists/prompts/cx-devil-advocate.md +2 -2
  101. package/specialists/prompts/cx-docs-keeper.md +0 -13
  102. package/specialists/prompts/cx-engineer.md +0 -13
  103. package/specialists/prompts/cx-evaluator.md +2 -2
  104. package/specialists/prompts/cx-explorer.md +4 -5
  105. package/specialists/prompts/cx-legal-compliance.md +4 -5
  106. package/specialists/prompts/cx-operations.md +0 -5
  107. package/specialists/prompts/cx-orchestrator.md +0 -4
  108. package/specialists/prompts/cx-platform-engineer.md +0 -8
  109. package/specialists/prompts/cx-product-manager.md +0 -8
  110. package/specialists/prompts/cx-qa.md +3 -12
  111. package/specialists/prompts/cx-rd-lead.md +0 -5
  112. package/specialists/prompts/cx-release-manager.md +0 -8
  113. package/specialists/prompts/cx-researcher.md +5 -29
  114. package/specialists/prompts/cx-reviewer.md +2 -6
  115. package/specialists/prompts/cx-security.md +2 -11
  116. package/specialists/prompts/cx-sre.md +0 -15
  117. package/specialists/prompts/cx-test-automation.md +0 -4
  118. package/specialists/prompts/cx-trace-reviewer.md +2 -2
  119. package/specialists/prompts/cx-ux-researcher.md +0 -4
  120. package/specialists/registry.json +28 -36
  121. package/templates/distribution/run.mjs +36 -7
  122. package/templates/docs/accessibility-audit.md +56 -0
  123. package/templates/docs/architecture-review.md +59 -0
  124. package/templates/docs/code-review-report.md +46 -0
  125. package/templates/docs/construct_guide.md +15 -15
  126. package/templates/docs/debug-investigation.md +53 -0
  127. package/templates/docs/qa-report.md +48 -0
  128. package/templates/docs/security-audit-report.md +48 -0
  129. package/templates/docs/task-packet.md +49 -0
  130. package/templates/docs/verdict.md +40 -0
  131. package/db/schema/001_init.sql +0 -40
  132. package/db/schema/002_pgvector.sql +0 -182
  133. package/db/schema/003_intake.sql +0 -47
  134. package/db/schema/003_observation_reconciliation.sql +0 -14
  135. package/db/schema/004_recommendations.sql +0 -46
  136. package/db/schema/005_strategy.sql +0 -21
  137. package/db/schema/006_graph.sql +0 -24
  138. package/db/schema/007_tags.sql +0 -30
  139. package/db/schema/008_skill_usage.sql +0 -24
  140. package/db/schema/009_scheduler.sql +0 -14
  141. package/db/schema/010_cx_scores.sql +0 -51
  142. package/lib/intake/postgres-queue.mjs +0 -240
  143. package/lib/server/static/index.html +0 -1
  144. package/lib/services/local-postgres.mjs +0 -15
  145. package/lib/storage/backup.mjs +0 -347
  146. package/lib/storage/migrations.mjs +0 -187
  147. package/lib/storage/postgres-backup.mjs +0 -124
  148. package/lib/storage/sql-store.mjs +0 -45
  149. package/lib/storage/store-version.mjs +0 -115
  150. package/lib/storage/vector-store.mjs +0 -100
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * lib/service-manager.mjs — start, stop, and describe Construct runtime services.
3
3
  *
4
- * Manages the dashboard process, local Postgres (Docker), memory server (cm),
5
- * and OpenCode. startServices() is the single entry point for `construct up` —
4
+ * Manages the dashboard process, memory server (cm), and OpenCode.
5
+ * startServices() is the single entry point for `construct up` —
6
6
  * spawns whatever is available and returns a result per service.
7
7
  */
8
8
  import { spawn, spawnSync } from 'node:child_process';
@@ -10,30 +10,17 @@ import fs from 'node:fs';
10
10
  import net from 'node:net';
11
11
  import os from 'node:os';
12
12
  import path from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
13
14
 
14
15
  import { findAvailablePort } from './host-capabilities.mjs';
15
16
  import { getUserEnvPath, loadConstructEnv, parseEnvFile, writeEnvValues } from './env-config.mjs';
16
- import { detectDockerCompose } from './setup.mjs';
17
- import { stashConstructDb, restoreConstructDb } from './storage/postgres-backup.mjs';
18
17
  import { runPressureRelease } from './runtime-pressure.mjs';
19
- import {
20
- pruneStashDir,
21
- verifyTelemetryKeys,
22
- isRemoteTelemetry,
23
- } from './services/local-postgres.mjs';
24
18
  import { resolveTraceBackend, telemetryProviderLabel } from './telemetry/client.mjs';
25
-
26
- const CONSTRUCT_PG_COMPOSE_DIR = 'services/postgres';
27
- const CONSTRUCT_PG_CONTAINER = 'construct-postgres';
28
- const CONSTRUCT_PG_PORT = 54329;
29
- const CONSTRUCT_PG_HEALTH_RETRIES = 12;
30
- const CONSTRUCT_PG_HEALTH_INTERVAL_MS = 2000;
19
+ import { memoryPort as derivedMemoryPort } from './home-namespace.mjs';
31
20
 
32
21
  const DASHBOARD_STATE_FILE = 'dashboard.json';
33
22
 
34
- // pruneStashDir and verifyTelemetryKeys now live in lib/services/telemetry-backend.mjs.
35
- // Re-exported below at module bottom for tests that already import from here.
36
-
23
+ const INSTALL_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
37
24
 
38
25
  function runtimeStateDir(homeDir = os.homedir()) {
39
26
  return path.join(homeDir, '.construct', 'runtime');
@@ -108,17 +95,8 @@ export function buildRuntimeRecoverySummary({ rootDir, homeDir = os.homedir(), r
108
95
  }
109
96
 
110
97
  export function isManagedConstructPostgresUrl(databaseUrl = '') {
111
- if (!databaseUrl) return false;
112
- try {
113
- const parsed = new URL(databaseUrl);
114
- const host = parsed.hostname;
115
- return (
116
- parsed.port === String(CONSTRUCT_PG_PORT) &&
117
- ['127.0.0.1', 'localhost', '::1'].includes(host)
118
- ) || host === CONSTRUCT_PG_CONTAINER;
119
- } catch {
120
- return databaseUrl.includes(`:${CONSTRUCT_PG_PORT}/`);
121
- }
98
+ // Construct no longer manages local Postgres containers.
99
+ return false;
122
100
  }
123
101
 
124
102
  function processExists(pid) {
@@ -260,7 +238,7 @@ export function startDoctor({ rootDir, homeDir = os.homedir() } = {}) {
260
238
  }
261
239
  const existing = readDoctorState(homeDir);
262
240
  if (existing) return { started: false, reused: true, pid: existing.pid };
263
- const indexPath = path.join(rootDir, 'lib', 'doctor', 'index.mjs');
241
+ const indexPath = path.join(INSTALL_ROOT, 'lib', 'doctor', 'index.mjs');
264
242
  if (!fs.existsSync(indexPath)) return { started: false, reason: 'missing-binary' };
265
243
  const { logPath } = spawnDetached('node', [indexPath], homeDir, 'doctor.log', { cwd: rootDir, env: { ...process.env } });
266
244
  return { started: true, logPath };
@@ -279,13 +257,25 @@ export async function startDashboard({ rootDir, homeDir = os.homedir(), preferre
279
257
  fs.mkdirSync(path.dirname(outPath), { recursive: true });
280
258
  const out = fs.openSync(outPath, 'a');
281
259
  const err = fs.openSync(outPath, 'a');
282
- const child = spawn(process.execPath, [path.join(rootDir, 'lib', 'server', 'index.mjs')], {
260
+ const child = spawn(process.execPath, [path.join(INSTALL_ROOT, 'lib', 'server', 'index.mjs')], {
283
261
  detached: true,
284
262
  stdio: ['ignore', out, err],
285
263
  env: { ...process.env, PORT: String(port) },
286
264
  });
287
265
  child.unref();
288
266
 
267
+ let ready = false;
268
+ for (let i = 0; i < 30; i += 1) {
269
+ if (await probeRuntimePort(port)) { ready = true; break; }
270
+ await new Promise((resolve) => setTimeout(resolve, 200));
271
+ }
272
+
273
+ if (!ready) {
274
+ let logTail = '';
275
+ try { logTail = fs.readFileSync(outPath, 'utf8').split('\n').slice(-12).join('\n'); } catch { /* log unreadable */ }
276
+ return { started: false, failed: true, port, logPath: outPath, error: `dashboard did not bind 127.0.0.1:${port} within 6s`, logTail };
277
+ }
278
+
289
279
  const state = {
290
280
  pid: child.pid,
291
281
  port,
@@ -297,10 +287,6 @@ export async function startDashboard({ rootDir, homeDir = os.homedir(), preferre
297
287
  return { started: true, reused: false, ...state };
298
288
  }
299
289
 
300
- /**
301
- * Resolve runtime ports. Re-uses ports from config.env when the service is
302
- * already listening there (avoids port drift on repeated `construct up`).
303
- */
304
290
  export async function getRuntimePorts(homeDir = os.homedir(), {
305
291
  dashboardProbeFn = isDashboardRunning,
306
292
  memoryProbeFn = isMemoryRunning,
@@ -320,7 +306,7 @@ export async function getRuntimePorts(homeDir = os.homedir(), {
320
306
 
321
307
  return {
322
308
  dashboard: dashboard?.port ?? await resolvePort('DASHBOARD_PORT', 4242, dashboardProbeFn),
323
- memory: await resolvePort('MEMORY_PORT', 8765, memoryProbeFn),
309
+ memory: await resolvePort('MEMORY_PORT', derivedMemoryPort(), memoryProbeFn),
324
310
  bridge: await resolvePort('BRIDGE_PORT', 5173, openCodeProbeFn),
325
311
  };
326
312
  }
@@ -336,118 +322,12 @@ export async function describeRuntimeSupport() {
336
322
  }
337
323
  return {
338
324
  tmux: commandExists('tmux'),
339
- docker: commandExists('docker'),
340
325
  cm: commandExists('cm'),
341
326
  opencode: commandExists('opencode'),
342
327
  };
343
328
  }
344
329
 
345
- // ── Construct Postgres management ──────────────────────────────────────────
346
-
347
- function constructPgComposePath(rootDir) {
348
- return path.join(rootDir, CONSTRUCT_PG_COMPOSE_DIR, 'docker-compose.yml');
349
- }
350
-
351
- function isConstructPostgresRunning(spawnSyncFn = spawnSync) {
352
- const result = spawnSyncFn('docker', ['inspect', '--format', '{{.State.Running}}', CONSTRUCT_PG_CONTAINER], {
353
- encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
354
- });
355
- return result.stdout?.trim() === 'true';
356
- }
357
-
358
- function isConstructPostgresHealthy(spawnSyncFn = spawnSync) {
359
- const result = spawnSyncFn('docker', ['exec', CONSTRUCT_PG_CONTAINER, 'pg_isready', '-U', 'construct'], {
360
- encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
361
- });
362
- return result.status === 0;
363
- }
364
-
365
- async function waitForConstructPostgresHealthy({
366
- spawnSyncFn = spawnSync,
367
- maxRetries = CONSTRUCT_PG_HEALTH_RETRIES,
368
- intervalMs = CONSTRUCT_PG_HEALTH_INTERVAL_MS,
369
- } = {}) {
370
- for (let i = 0; i < maxRetries; i++) {
371
- if (isConstructPostgresHealthy(spawnSyncFn)) return true;
372
- await new Promise((r) => setTimeout(r, intervalMs));
373
- }
374
- return false;
375
- }
376
-
377
- function startConstructPostgres({ rootDir, homeDir = os.homedir(), spawnSyncFn = spawnSync, detectDockerComposeFn = detectDockerCompose } = {}) {
378
- const composeRunner = detectDockerComposeFn();
379
- if (!composeRunner) return { status: 'unavailable', note: 'Docker not available' };
380
-
381
- const composeFile = constructPgComposePath(rootDir);
382
- if (!fs.existsSync(composeFile)) return { status: 'unavailable', note: 'Postgres compose file not found — run construct init first' };
383
-
384
- const args = [...composeRunner.argsPrefix, '-p', 'construct-postgres', '-f', composeFile, 'up', '-d'];
385
- const r = spawnSyncFn(composeRunner.command, args, { stdio: 'pipe', encoding: 'utf8' });
386
- if (r.status === 0) return { status: 'started' };
387
- return { status: 'error', note: (r.stderr || '').trim().split('\n')[0] || 'compose up failed' };
388
- }
389
-
390
- function stopConstructPostgres({ rootDir, homeDir = os.homedir(), spawnSyncFn = spawnSync, detectDockerComposeFn = detectDockerCompose } = {}) {
391
- const composeRunner = detectDockerComposeFn();
392
- if (!composeRunner) return { status: 'skipped', note: 'Docker not available' };
393
-
394
- const composeFile = constructPgComposePath(rootDir);
395
- if (!fs.existsSync(composeFile)) return { status: 'skipped', note: 'no compose file' };
396
-
397
- const args = [...composeRunner.argsPrefix, '-p', 'construct-postgres', '-f', composeFile, 'down'];
398
- const r = spawnSyncFn(composeRunner.command, args, { stdio: 'pipe', encoding: 'utf8' });
399
- if (r.status === 0) return { status: 'stopped' };
400
- return { status: 'error', note: (r.stderr || '').trim().split('\n')[0] || 'compose down failed' };
401
- }
402
-
403
- function checkPgvectorEnabled(spawnSyncFn = spawnSync) {
404
- const result = spawnSyncFn('docker', [
405
- 'exec', CONSTRUCT_PG_CONTAINER, 'psql', '-U', 'construct', '-d', 'construct',
406
- '-tAc', "SELECT EXISTS(SELECT 1 FROM pg_extension WHERE extname = 'vector')",
407
- ], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
408
- return result.stdout?.trim() === 't';
409
- }
410
-
411
- /**
412
- * On macOS, when the Docker CLI is installed but the daemon is down,
413
- * launch Docker Desktop and poll `docker info` up to `timeoutMs` for
414
- * the daemon to come up. Opt-out via `CONSTRUCT_DISABLE_DOCKER_AUTOSTART=1`
415
- * so users who keep Docker off aren't forced into it. No-ops on Linux
416
- * (systemd needs sudo) and Windows (different launcher).
417
- */
418
- async function tryStartDockerDaemon({
419
- env = process.env,
420
- spawnSyncFn = spawnSync,
421
- timeoutMs = 60_000,
422
- pollMs = 1500,
423
- } = {}) {
424
- if (env.CONSTRUCT_DISABLE_DOCKER_AUTOSTART === '1') return false;
425
- if (process.platform !== 'darwin') return false;
426
-
427
- // CLI must be present; without it, nothing to start.
428
- if (spawnSyncFn('command -v docker', { stdio: 'ignore', shell: true }).status !== 0) return false;
429
-
430
- // If the daemon is already up, no work needed.
431
- if (spawnSyncFn('docker', ['info'], { stdio: 'ignore' }).status === 0) return true;
432
-
433
- // Best-effort launch. `open -a Docker` returns immediately; the daemon
434
- // takes 5-30s to come fully up so we poll.
435
- spawnSyncFn('open', ['-a', 'Docker'], { stdio: 'ignore' });
436
-
437
- const deadline = Date.now() + timeoutMs;
438
- while (Date.now() < deadline) {
439
- await new Promise((r) => setTimeout(r, pollMs));
440
- if (spawnSyncFn('docker', ['info'], { stdio: 'ignore' }).status === 0) return true;
441
- }
442
- return false;
443
- }
444
-
445
- // The optional services a user can pick from with `construct dev --select` /
446
- // `--only=`. Pressure Guard (cleanup) and Doctor (the L0 supervisor daemon)
447
- // are infrastructure, always on — they are intentionally not selectable.
448
-
449
330
  export const SELECTABLE_SERVICES = Object.freeze([
450
- { key: 'postgres', label: 'Postgres', description: 'Managed pgvector container (or external DATABASE_URL).' },
451
331
  { key: 'dashboard', label: 'Dashboard', description: 'Local operations dashboard on http://127.0.0.1:4242.' },
452
332
  { key: 'telemetry', label: 'Telemetry', description: 'Trace export / local JSONL traces.' },
453
333
  { key: 'memory', label: 'Memory (cm)', description: 'Persistent memory service (cm).' },
@@ -461,36 +341,24 @@ export async function startServices({
461
341
  describeRuntimeSupportFn = describeRuntimeSupport,
462
342
  getRuntimePortsFn = getRuntimePorts,
463
343
  startDashboardFn = startDashboard,
464
- detectDockerComposeFn = detectDockerCompose,
465
344
  loadConstructEnvFn = loadConstructEnv,
466
345
  spawnDetachedFn = spawnDetached,
467
- verifyTelemetryKeysFn = verifyTelemetryKeys,
468
346
  memoryProbeFn = isMemoryRunning,
469
347
  openCodeProbeFn = isOpenCodeRunning,
470
348
  runPressureReleaseFn = runPressureRelease,
471
- tryStartDockerDaemonFn = tryStartDockerDaemon,
472
349
  } = {}) {
473
350
  const support = await describeRuntimeSupportFn();
474
351
  const ports = await getRuntimePortsFn(homeDir);
475
352
  const envPath = getUserEnvPath(homeDir);
476
353
 
477
- // A null selection means "start everything" (the default). A Set restricts
478
- // startup to the chosen optional services; always-on infrastructure ignores it.
479
-
480
354
  const wants = (key) => selected === null || selected.has(key);
481
355
 
482
- // On macOS, auto-launch Docker Desktop when the CLI is present but the
483
- // daemon is down. Polls up to 60s so downstream Postgres startup sees a working
484
- // compose runner instead of "Docker not available".
485
- await tryStartDockerDaemonFn({ env: { ...process.env, HOME: homeDir }, timeoutMs: 5000 });
486
-
487
356
  writeEnvValues(envPath, {
488
357
  DASHBOARD_PORT: String(ports.dashboard),
489
358
  MEMORY_PORT: String(ports.memory),
490
359
  BRIDGE_PORT: String(ports.bridge),
491
360
  });
492
361
 
493
- // Construct Postgres — start if DATABASE_URL points to managed container
494
362
  const liveEnv = loadConstructEnvFn({ rootDir, homeDir });
495
363
  const results = [];
496
364
  const pressureReport = runPressureReleaseFn({ env: { ...liveEnv, HOME: homeDir } });
@@ -502,86 +370,50 @@ export async function startServices({
502
370
  note: `terminated ${pressureReport.killed.length} stale process(es)`,
503
371
  });
504
372
  }
505
- const databaseUrl = liveEnv.DATABASE_URL || '';
506
- const usesManagedPostgres = isManagedConstructPostgresUrl(databaseUrl);
507
-
508
- if (wants('postgres') && usesManagedPostgres) {
509
- if (!isConstructPostgresRunning()) {
510
- const pgStart = startConstructPostgres({ rootDir, homeDir, spawnSyncFn: spawnSync, detectDockerComposeFn: detectDockerComposeFn });
511
- if (pgStart.status === 'started') {
512
- const healthy = await waitForConstructPostgresHealthy();
513
- if (healthy) {
514
- const pgvector = checkPgvectorEnabled();
515
- results.push({
516
- name: 'Postgres',
517
- url: `postgresql://127.0.0.1:${CONSTRUCT_PG_PORT}/construct`,
518
- status: pgvector ? 'started' : 'degraded',
519
- note: pgvector ? 'pgvector enabled' : 'pgvector not installed — semantic search unavailable',
520
- });
521
- } else {
522
- results.push({ name: 'Postgres', status: 'degraded', note: 'container started but health check timed out' });
523
- }
524
- } else {
525
- results.push({ name: 'Postgres', status: 'error', note: pgStart.note });
526
- }
527
- } else {
528
- const pgvector = checkPgvectorEnabled();
529
- results.push({
530
- name: 'Postgres',
531
- url: `postgresql://127.0.0.1:${CONSTRUCT_PG_PORT}/construct`,
532
- status: 'reused',
533
- note: pgvector ? 'pgvector enabled' : 'pgvector not installed',
534
- });
535
- }
536
- } else if (wants('postgres') && databaseUrl) {
537
- results.push({ name: 'Postgres', url: databaseUrl, status: 'configured', note: 'external database' });
538
- }
539
373
 
540
374
  // Dashboard
541
375
  if (wants('dashboard')) {
542
376
  const dashboard = await startDashboardFn({ rootDir, homeDir, preferredPort: ports.dashboard });
543
- results.push({
544
- name: 'Dashboard',
545
- url: dashboard.url,
546
- status: dashboard.reused ? 'reused' : 'started',
547
- });
377
+ results.push(dashboard.failed
378
+ ? { name: 'Dashboard', status: 'failed', note: `${dashboard.error} — see ${dashboard.logPath}` }
379
+ : { name: 'Dashboard', url: dashboard.url, status: dashboard.reused ? 'reused' : 'started' });
548
380
  }
549
381
 
550
382
  const telemetryUrl = liveEnv.CONSTRUCT_TELEMETRY_URL ?? '';
551
383
  const traceBackend = resolveTraceBackend(liveEnv);
552
384
  if (wants('telemetry')) {
553
- if (traceBackend === 'local') {
554
- results.push({
555
- name: 'Telemetry',
556
- url: path.join(rootDir, '.cx', 'traces'),
557
- status: 'configured',
558
- note: 'local JSONL traces; remote export not configured',
559
- });
560
- } else if (traceBackend === 'none') {
561
- results.push({
562
- name: 'Telemetry',
563
- url: path.join(rootDir, '.cx', 'traces'),
564
- status: 'configured',
565
- note: 'remote export disabled; local JSONL traces preserved',
566
- });
567
- } else if (traceBackend === 'otel') {
568
- const endpoint = liveEnv.CONSTRUCT_OTEL_EXPORTER_OTLP_ENDPOINT || '';
569
- results.push({
570
- name: 'Telemetry',
571
- url: endpoint || undefined,
572
- status: endpoint ? 'configured' : 'unavailable',
573
- note: endpoint ? `OTLP export (${telemetryProviderLabel(liveEnv)})` : 'CONSTRUCT_OTEL_EXPORTER_OTLP_ENDPOINT not set',
574
- });
575
- } else if (telemetryUrl && (isRemoteTelemetry(telemetryUrl) || traceBackend === 'langfuse' || traceBackend === 'http')) {
576
- results.push({
577
- name: 'Telemetry',
578
- url: telemetryUrl,
579
- status: 'configured',
580
- note: `${telemetryProviderLabel(liveEnv)} export — ${telemetryUrl}`,
581
- });
582
- } else {
583
- results.push({ name: 'Telemetry', status: 'unavailable', note: 'remote export requested but endpoint is not configured' });
584
- }
385
+ if (traceBackend === 'local') {
386
+ results.push({
387
+ name: 'Telemetry',
388
+ url: path.join(rootDir, '.cx', 'traces'),
389
+ status: 'configured',
390
+ note: 'local JSONL traces; remote export not configured',
391
+ });
392
+ } else if (traceBackend === 'none') {
393
+ results.push({
394
+ name: 'Telemetry',
395
+ url: path.join(rootDir, '.cx', 'traces'),
396
+ status: 'configured',
397
+ note: 'remote export disabled; local JSONL traces preserved',
398
+ });
399
+ } else if (traceBackend === 'otel') {
400
+ const endpoint = liveEnv.CONSTRUCT_OTEL_EXPORTER_OTLP_ENDPOINT || '';
401
+ results.push({
402
+ name: 'Telemetry',
403
+ url: endpoint || undefined,
404
+ status: endpoint ? 'configured' : 'unavailable',
405
+ note: endpoint ? `OTLP export (${telemetryProviderLabel(liveEnv)})` : 'CONSTRUCT_OTEL_EXPORTER_OTLP_ENDPOINT not set',
406
+ });
407
+ } else if (telemetryUrl && (traceBackend === 'langfuse' || traceBackend === 'http')) {
408
+ results.push({
409
+ name: 'Telemetry',
410
+ url: telemetryUrl,
411
+ status: 'configured',
412
+ note: `${telemetryProviderLabel(liveEnv)} export — ${telemetryUrl}`,
413
+ });
414
+ } else {
415
+ results.push({ name: 'Telemetry', status: 'unavailable', note: 'remote export requested but endpoint is not configured' });
416
+ }
585
417
  }
586
418
 
587
419
  // Memory (cm)
@@ -625,9 +457,6 @@ export async function startServices({
625
457
  };
626
458
  }
627
459
 
628
- // Surface service probe failures into the role-framework event bus so cx-sre
629
- // can be invoked when a critical service is down. Non-blocking, best effort.
630
-
631
460
  async function emitServiceFailures(results) {
632
461
  if (process.env.CONSTRUCT_ROLES === 'off') return;
633
462
  const failed = results.filter((r) => r.status === 'error');
@@ -645,17 +474,9 @@ async function emitServiceFailures(results) {
645
474
  } catch { /* best effort */ }
646
475
  }
647
476
 
648
- // Exported for testing only
649
- export { verifyTelemetryKeys as _verifyTelemetryKeys, pruneStashDir as _pruneStashDir };
650
-
651
- /**
652
- * Kill every process listening on a given TCP port (best-effort, non-fatal).
653
- * Returns true if at least one PID was killed.
654
- */
655
477
  function killPortOwners(port, spawnSyncFn = spawnSync) {
656
478
  if (!port || !Number.isInteger(port) || port <= 0) return false;
657
479
  try {
658
- // lsof works on macOS and Linux; -t returns just PIDs
659
480
  const result = spawnSyncFn('lsof', ['-t', `-i:${port}`], { encoding: 'utf8' });
660
481
  const pids = (result.stdout || '').trim().split(/\s+/).filter(Boolean).map(Number).filter((n) => n > 0);
661
482
  if (pids.length === 0) return false;
@@ -670,21 +491,10 @@ function killPortOwners(port, spawnSyncFn = spawnSync) {
670
491
 
671
492
  export async function stopServices({
672
493
  homeDir = os.homedir(),
673
- rootDir,
674
494
  spawnSyncFn = spawnSync,
675
- detectDockerComposeFn = detectDockerCompose,
676
495
  } = {}) {
677
496
  const results = [];
678
497
 
679
- // Stash construct Postgres data before any container is stopped so data
680
- // survives Docker restarts and machine reboots.
681
- const constructDbStash = stashConstructDb({ homeDir, spawnSyncFn });
682
-
683
- // Construct Postgres
684
- const pgStop = stopConstructPostgres({ rootDir, homeDir, spawnSyncFn, detectDockerComposeFn });
685
- results.push({ name: 'Postgres', status: pgStop.status });
686
-
687
- // Doctor (L0 daemon)
688
498
  const doctor = stopDoctor(homeDir);
689
499
  if (doctor.stopped) {
690
500
  results.push({ name: 'Doctor', status: 'stopped', note: `pid ${doctor.pid}` });
@@ -692,7 +502,6 @@ export async function stopServices({
692
502
  results.push({ name: 'Doctor', status: 'cleaned', note: 'stale state file removed' });
693
503
  }
694
504
 
695
- // Dashboard
696
505
  const dashboard = stopDashboard(homeDir);
697
506
  if (dashboard.stopped) {
698
507
  results.push({ name: 'Dashboard', status: 'stopped', note: `pid ${dashboard.pid}, port ${dashboard.port}` });
@@ -702,18 +511,16 @@ export async function stopServices({
702
511
  results.push({ name: 'Dashboard', status: 'not-running' });
703
512
  }
704
513
 
705
- // Memory (cm) — find port from config.env or fall back to default
706
514
  const envPath = getUserEnvPath(homeDir);
707
515
  const envValues = parseEnvFile(envPath);
708
- const memoryPort = Number(envValues.MEMORY_PORT) || 8765;
516
+ const memoryPort = Number(envValues.MEMORY_PORT) || derivedMemoryPort();
709
517
  const cmKilled = killPortOwners(memoryPort, spawnSyncFn);
710
518
  results.push({ name: 'Memory (cm)', status: cmKilled ? 'stopped' : 'not-running' });
711
519
 
712
- // OpenCode — find port from config.env or fall back to default
713
520
  const bridgePort = Number(envValues.BRIDGE_PORT) || 5173;
714
521
  const openCodeKilled = killPortOwners(bridgePort, spawnSyncFn);
715
522
  results.push({ name: 'OpenCode', status: openCodeKilled ? 'stopped' : 'not-running' });
716
523
 
717
524
  const stopped = results.filter((r) => r.status === 'stopped' || r.status === 'cleaned').map((r) => r.name);
718
- return { stopped, results, constructDbStash };
525
+ return { stopped, results };
719
526
  }
@@ -29,6 +29,7 @@ export async function consentToInstall({
29
29
  alreadyConfigured = false,
30
30
  alreadyConfiguredNote = '',
31
31
  envPath,
32
+ question = '',
32
33
  defaultYes = true,
33
34
  readlineModule = readline,
34
35
  stdin = process.stdin,
@@ -64,7 +65,7 @@ export async function consentToInstall({
64
65
  }
65
66
 
66
67
  const answer = await promptYesNo({
67
- question: `Install local ${name} via Docker?`,
68
+ question: question || `Install local ${name} via Docker?`,
68
69
  defaultYes,
69
70
  readlineModule,
70
71
  stdin,