@geraldmaron/construct 1.0.23 → 1.1.0
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.
- package/README.md +0 -2
- package/bin/construct +189 -207
- package/lib/agent-instructions/inject.mjs +25 -4
- package/lib/audit-rules.mjs +127 -0
- package/lib/audit-skills.mjs +43 -1
- package/lib/beads-client.mjs +9 -0
- package/lib/beads-optimistic.mjs +23 -71
- package/lib/bridges/copilot-proxy.mjs +116 -0
- package/lib/cli-commands.mjs +5 -1
- package/lib/comment-lint.mjs +1 -1
- package/lib/config/schema.mjs +1 -1
- package/lib/document-extract/docling-client.mjs +16 -6
- package/lib/document-extract/docling-sidecar.py +32 -2
- package/lib/document-extract.mjs +37 -10
- package/lib/document-ingest.mjs +90 -5
- package/lib/embed/inbox.mjs +6 -3
- package/lib/embed/recommendation-store.mjs +7 -289
- package/lib/embed/reconcile.mjs +2 -2
- package/lib/embed/roadmap.mjs +16 -1
- package/lib/engine/consolidate.mjs +160 -3
- package/lib/engine/contradiction-judge.mjs +71 -0
- package/lib/engine/contradiction.mjs +74 -0
- package/lib/hooks/config-protection.mjs +4 -4
- package/lib/hooks/session-reflect.mjs +5 -1
- package/lib/host-capabilities.mjs +30 -0
- package/lib/ingest/docling-remote.mjs +90 -0
- package/lib/ingest/strategy.mjs +1 -1
- package/lib/init-unified.mjs +9 -13
- package/lib/intake/git-queue.mjs +195 -0
- package/lib/intake/queue.mjs +9 -16
- package/lib/logging/rotate.mjs +18 -0
- package/lib/mcp/server.mjs +124 -12
- package/lib/mcp/tool-budget.mjs +53 -0
- package/lib/mcp/tools/storage.mjs +2 -3
- package/lib/mcp-catalog.json +4 -4
- package/lib/mcp-manager.mjs +59 -3
- package/lib/observation-store.mjs +38 -166
- package/lib/ollama/capability-store.mjs +78 -0
- package/lib/ollama/provision-context.mjs +344 -0
- package/lib/opencode-config.mjs +148 -0
- package/lib/opencode-telemetry.mjs +7 -0
- package/lib/orchestration/runtime.mjs +3 -2
- package/lib/orchestration-policy.mjs +41 -6
- package/lib/platforms/capabilities.mjs +100 -0
- package/lib/prompt-composer.js +12 -8
- package/lib/reconcile/agent-instructions-rewrap.mjs +8 -4
- package/lib/reconcile/index.mjs +0 -2
- package/lib/reflect/extractor.mjs +14 -1
- package/lib/reflect/salience.mjs +65 -0
- package/lib/rules-delivery.mjs +122 -0
- package/lib/runtime/uv-bootstrap.mjs +32 -17
- package/lib/service-manager.mjs +79 -259
- package/lib/setup.mjs +44 -425
- package/lib/specialists/prompt-schema.mjs +162 -0
- package/lib/specialists/scaffold.mjs +109 -0
- package/lib/status.mjs +3 -6
- package/lib/storage/admin.mjs +48 -325
- package/lib/storage/backend.mjs +10 -57
- package/lib/storage/embeddings-engine.mjs +19 -5
- package/lib/storage/hybrid-query.mjs +15 -196
- package/lib/storage/sync.mjs +36 -177
- package/lib/storage/vector-client.mjs +256 -235
- package/lib/strategy-store.mjs +35 -286
- package/lib/telemetry/beads-fallback.mjs +40 -0
- package/lib/telemetry/hook-calls.mjs +138 -0
- package/lib/worker/entrypoint.mjs +6 -14
- package/package.json +6 -5
- package/personas/construct.md +1 -1
- package/platforms/capabilities.json +76 -0
- package/platforms/claude/settings.template.json +0 -7
- package/platforms/opencode/sync-config.mjs +121 -25
- package/rules/common/neurodivergent-output.md +1 -1
- package/rules/web/coding-style.md +8 -0
- package/rules/web/design-quality.md +8 -0
- package/rules/web/hooks.md +8 -0
- package/rules/web/patterns.md +8 -0
- package/rules/web/performance.md +8 -0
- package/rules/web/security.md +8 -0
- package/rules/web/testing.md +8 -0
- package/scripts/sync-specialists.mjs +174 -40
- package/specialists/prompts/cx-architect.md +20 -0
- package/specialists/prompts/cx-qa.md +1 -1
- package/specialists/prompts/cx-test-automation.md +12 -0
- package/specialists/registry.json +0 -8
- package/templates/docs/construct_guide.md +1 -1
- package/db/schema/001_init.sql +0 -40
- package/db/schema/002_pgvector.sql +0 -182
- package/db/schema/003_intake.sql +0 -47
- package/db/schema/003_observation_reconciliation.sql +0 -14
- package/db/schema/004_recommendations.sql +0 -46
- package/db/schema/005_strategy.sql +0 -21
- package/db/schema/006_graph.sql +0 -24
- package/db/schema/007_tags.sql +0 -30
- package/db/schema/008_skill_usage.sql +0 -24
- package/db/schema/009_scheduler.sql +0 -14
- package/db/schema/010_cx_scores.sql +0 -51
- package/lib/intake/postgres-queue.mjs +0 -240
- package/lib/reconcile/postgres-namespace.mjs +0 -102
- package/lib/services/local-postgres.mjs +0 -15
- package/lib/storage/backup.mjs +0 -347
- package/lib/storage/migrations.mjs +0 -187
- package/lib/storage/postgres-backup.mjs +0 -124
- package/lib/storage/sql-store.mjs +0 -45
- package/lib/storage/store-version.mjs +0 -115
- package/lib/storage/unified-storage.mjs +0 -550
- package/lib/storage/vector-store.mjs +0 -100
package/lib/service-manager.mjs
CHANGED
|
@@ -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,
|
|
5
|
-
*
|
|
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';
|
|
@@ -14,37 +14,14 @@ import { fileURLToPath } from 'node:url';
|
|
|
14
14
|
|
|
15
15
|
import { findAvailablePort } from './host-capabilities.mjs';
|
|
16
16
|
import { getUserEnvPath, loadConstructEnv, parseEnvFile, writeEnvValues } from './env-config.mjs';
|
|
17
|
-
import { detectDockerCompose } from './setup.mjs';
|
|
18
|
-
import { stashConstructDb, restoreConstructDb } from './storage/postgres-backup.mjs';
|
|
19
17
|
import { runPressureRelease } from './runtime-pressure.mjs';
|
|
20
|
-
import {
|
|
21
|
-
pruneStashDir,
|
|
22
|
-
verifyTelemetryKeys,
|
|
23
|
-
isRemoteTelemetry,
|
|
24
|
-
} from './services/local-postgres.mjs';
|
|
25
18
|
import { resolveTraceBackend, telemetryProviderLabel } from './telemetry/client.mjs';
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
const CONSTRUCT_PG_COMPOSE_DIR = 'services/postgres';
|
|
29
|
-
const CONSTRUCT_PG_CONTAINER = postgresContainerName();
|
|
30
|
-
const CONSTRUCT_PG_PORT = postgresPort();
|
|
31
|
-
const CONSTRUCT_PG_HEALTH_RETRIES = 12;
|
|
32
|
-
const CONSTRUCT_PG_HEALTH_INTERVAL_MS = 2000;
|
|
19
|
+
import { memoryPort as derivedMemoryPort } from './home-namespace.mjs';
|
|
33
20
|
|
|
34
21
|
const DASHBOARD_STATE_FILE = 'dashboard.json';
|
|
35
22
|
|
|
36
|
-
// The caller's `rootDir` is the project (for compose/context/traces), but the
|
|
37
|
-
// dashboard and doctor entrypoints ship with the install, not the project. They
|
|
38
|
-
// must resolve against the install root — derived from this module's own
|
|
39
|
-
// location — or `construct init` spawns `<project>/lib/server/index.mjs` and the
|
|
40
|
-
// process dies with MODULE_NOT_FOUND while still advertising the URL.
|
|
41
|
-
|
|
42
23
|
const INSTALL_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
43
24
|
|
|
44
|
-
// pruneStashDir and verifyTelemetryKeys now live in lib/services/telemetry-backend.mjs.
|
|
45
|
-
// Re-exported below at module bottom for tests that already import from here.
|
|
46
|
-
|
|
47
|
-
|
|
48
25
|
function runtimeStateDir(homeDir = os.homedir()) {
|
|
49
26
|
return path.join(homeDir, '.construct', 'runtime');
|
|
50
27
|
}
|
|
@@ -118,17 +95,8 @@ export function buildRuntimeRecoverySummary({ rootDir, homeDir = os.homedir(), r
|
|
|
118
95
|
}
|
|
119
96
|
|
|
120
97
|
export function isManagedConstructPostgresUrl(databaseUrl = '') {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const parsed = new URL(databaseUrl);
|
|
124
|
-
const host = parsed.hostname;
|
|
125
|
-
return (
|
|
126
|
-
parsed.port === String(CONSTRUCT_PG_PORT) &&
|
|
127
|
-
['127.0.0.1', 'localhost', '::1'].includes(host)
|
|
128
|
-
) || host === CONSTRUCT_PG_CONTAINER;
|
|
129
|
-
} catch {
|
|
130
|
-
return databaseUrl.includes(`:${CONSTRUCT_PG_PORT}/`);
|
|
131
|
-
}
|
|
98
|
+
// Construct no longer manages local Postgres containers.
|
|
99
|
+
return false;
|
|
132
100
|
}
|
|
133
101
|
|
|
134
102
|
function processExists(pid) {
|
|
@@ -191,10 +159,15 @@ async function isMemoryRunning(port) {
|
|
|
191
159
|
return probeRuntimePort(port);
|
|
192
160
|
}
|
|
193
161
|
|
|
194
|
-
async function
|
|
162
|
+
async function isBridgeRunning(port) {
|
|
195
163
|
return probeRuntimeHttp(`http://127.0.0.1:${port}/`, { timeoutMs: 1000 });
|
|
196
164
|
}
|
|
197
165
|
|
|
166
|
+
async function isCopilotBridgeRunning(port) {
|
|
167
|
+
// Check if the port is reachable.
|
|
168
|
+
return probeRuntimePort(port);
|
|
169
|
+
}
|
|
170
|
+
|
|
198
171
|
function spawnDetached(command, args, homeDir, logFile, options = {}) {
|
|
199
172
|
const logPath = path.join(runtimeStateDir(homeDir), logFile);
|
|
200
173
|
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
@@ -296,11 +269,6 @@ export async function startDashboard({ rootDir, homeDir = os.homedir(), preferre
|
|
|
296
269
|
});
|
|
297
270
|
child.unref();
|
|
298
271
|
|
|
299
|
-
// Confirm the process actually bound the port before persisting state or
|
|
300
|
-
// advertising the URL. Without this the caller reports a dead PID/URL when the
|
|
301
|
-
// server crashes on boot — the readiness gate turns a silent failure into an
|
|
302
|
-
// honest, actionable one (the log tail names the cause).
|
|
303
|
-
|
|
304
272
|
let ready = false;
|
|
305
273
|
for (let i = 0; i < 30; i += 1) {
|
|
306
274
|
if (await probeRuntimePort(port)) { ready = true; break; }
|
|
@@ -324,14 +292,10 @@ export async function startDashboard({ rootDir, homeDir = os.homedir(), preferre
|
|
|
324
292
|
return { started: true, reused: false, ...state };
|
|
325
293
|
}
|
|
326
294
|
|
|
327
|
-
/**
|
|
328
|
-
* Resolve runtime ports. Re-uses ports from config.env when the service is
|
|
329
|
-
* already listening there (avoids port drift on repeated `construct up`).
|
|
330
|
-
*/
|
|
331
295
|
export async function getRuntimePorts(homeDir = os.homedir(), {
|
|
332
296
|
dashboardProbeFn = isDashboardRunning,
|
|
333
297
|
memoryProbeFn = isMemoryRunning,
|
|
334
|
-
openCodeProbeFn =
|
|
298
|
+
openCodeProbeFn = isBridgeRunning,
|
|
335
299
|
findAvailablePortFn = findAvailablePort,
|
|
336
300
|
} = {}) {
|
|
337
301
|
const dashboard = readDashboardState(homeDir);
|
|
@@ -349,6 +313,7 @@ export async function getRuntimePorts(homeDir = os.homedir(), {
|
|
|
349
313
|
dashboard: dashboard?.port ?? await resolvePort('DASHBOARD_PORT', 4242, dashboardProbeFn),
|
|
350
314
|
memory: await resolvePort('MEMORY_PORT', derivedMemoryPort(), memoryProbeFn),
|
|
351
315
|
bridge: await resolvePort('BRIDGE_PORT', 5173, openCodeProbeFn),
|
|
316
|
+
copilotBridge: await resolvePort('COPILOT_BRIDGE_PORT', 5174, probeRuntimePort),
|
|
352
317
|
};
|
|
353
318
|
}
|
|
354
319
|
|
|
@@ -363,122 +328,18 @@ export async function describeRuntimeSupport() {
|
|
|
363
328
|
}
|
|
364
329
|
return {
|
|
365
330
|
tmux: commandExists('tmux'),
|
|
366
|
-
docker: commandExists('docker'),
|
|
367
331
|
cm: commandExists('cm'),
|
|
368
332
|
opencode: commandExists('opencode'),
|
|
333
|
+
gh: commandExists('gh'),
|
|
369
334
|
};
|
|
370
335
|
}
|
|
371
336
|
|
|
372
|
-
// ── Construct Postgres management ──────────────────────────────────────────
|
|
373
|
-
|
|
374
|
-
function constructPgComposePath(rootDir) {
|
|
375
|
-
return path.join(rootDir, CONSTRUCT_PG_COMPOSE_DIR, 'docker-compose.yml');
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function isConstructPostgresRunning(spawnSyncFn = spawnSync) {
|
|
379
|
-
const result = spawnSyncFn('docker', ['inspect', '--format', '{{.State.Running}}', CONSTRUCT_PG_CONTAINER], {
|
|
380
|
-
encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
381
|
-
});
|
|
382
|
-
return result.stdout?.trim() === 'true';
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
function isConstructPostgresHealthy(spawnSyncFn = spawnSync) {
|
|
386
|
-
const result = spawnSyncFn('docker', ['exec', CONSTRUCT_PG_CONTAINER, 'pg_isready', '-U', 'construct'], {
|
|
387
|
-
encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
388
|
-
});
|
|
389
|
-
return result.status === 0;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
async function waitForConstructPostgresHealthy({
|
|
393
|
-
spawnSyncFn = spawnSync,
|
|
394
|
-
maxRetries = CONSTRUCT_PG_HEALTH_RETRIES,
|
|
395
|
-
intervalMs = CONSTRUCT_PG_HEALTH_INTERVAL_MS,
|
|
396
|
-
} = {}) {
|
|
397
|
-
for (let i = 0; i < maxRetries; i++) {
|
|
398
|
-
if (isConstructPostgresHealthy(spawnSyncFn)) return true;
|
|
399
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
400
|
-
}
|
|
401
|
-
return false;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
function startConstructPostgres({ rootDir, homeDir = os.homedir(), spawnSyncFn = spawnSync, detectDockerComposeFn = detectDockerCompose } = {}) {
|
|
405
|
-
const composeRunner = detectDockerComposeFn({ autoStart: true });
|
|
406
|
-
if (!composeRunner) return { status: 'unavailable', note: 'Docker not available' };
|
|
407
|
-
|
|
408
|
-
const composeFile = constructPgComposePath(rootDir);
|
|
409
|
-
if (!fs.existsSync(composeFile)) return { status: 'unavailable', note: 'Postgres compose file not found — run construct init first' };
|
|
410
|
-
|
|
411
|
-
const args = [...composeRunner.argsPrefix, '-p', CONSTRUCT_PG_CONTAINER, '-f', composeFile, 'up', '-d'];
|
|
412
|
-
const r = spawnSyncFn(composeRunner.command, args, { stdio: 'pipe', encoding: 'utf8' });
|
|
413
|
-
if (r.status === 0) return { status: 'started' };
|
|
414
|
-
return { status: 'error', note: (r.stderr || '').trim().split('\n')[0] || 'compose up failed' };
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
function stopConstructPostgres({ rootDir, homeDir = os.homedir(), spawnSyncFn = spawnSync, detectDockerComposeFn = detectDockerCompose } = {}) {
|
|
418
|
-
const composeRunner = detectDockerComposeFn();
|
|
419
|
-
if (!composeRunner) return { status: 'skipped', note: 'Docker not available' };
|
|
420
|
-
|
|
421
|
-
const composeFile = constructPgComposePath(rootDir);
|
|
422
|
-
if (!fs.existsSync(composeFile)) return { status: 'skipped', note: 'no compose file' };
|
|
423
|
-
|
|
424
|
-
const args = [...composeRunner.argsPrefix, '-p', CONSTRUCT_PG_CONTAINER, '-f', composeFile, 'down'];
|
|
425
|
-
const r = spawnSyncFn(composeRunner.command, args, { stdio: 'pipe', encoding: 'utf8' });
|
|
426
|
-
if (r.status === 0) return { status: 'stopped' };
|
|
427
|
-
return { status: 'error', note: (r.stderr || '').trim().split('\n')[0] || 'compose down failed' };
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
function checkPgvectorEnabled(spawnSyncFn = spawnSync) {
|
|
431
|
-
const result = spawnSyncFn('docker', [
|
|
432
|
-
'exec', CONSTRUCT_PG_CONTAINER, 'psql', '-U', 'construct', '-d', 'construct',
|
|
433
|
-
'-tAc', "SELECT EXISTS(SELECT 1 FROM pg_extension WHERE extname = 'vector')",
|
|
434
|
-
], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
435
|
-
return result.stdout?.trim() === 't';
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
/**
|
|
439
|
-
* On macOS, when the Docker CLI is installed but the daemon is down,
|
|
440
|
-
* launch Docker Desktop and poll `docker info` up to `timeoutMs` for
|
|
441
|
-
* the daemon to come up. Opt-out via `CONSTRUCT_DISABLE_DOCKER_AUTOSTART=1`
|
|
442
|
-
* so users who keep Docker off aren't forced into it. No-ops on Linux
|
|
443
|
-
* (systemd needs sudo) and Windows (different launcher).
|
|
444
|
-
*/
|
|
445
|
-
async function tryStartDockerDaemon({
|
|
446
|
-
env = process.env,
|
|
447
|
-
spawnSyncFn = spawnSync,
|
|
448
|
-
timeoutMs = 60_000,
|
|
449
|
-
pollMs = 1500,
|
|
450
|
-
} = {}) {
|
|
451
|
-
if (env.CONSTRUCT_DISABLE_DOCKER_AUTOSTART === '1') return false;
|
|
452
|
-
if (process.platform !== 'darwin') return false;
|
|
453
|
-
|
|
454
|
-
// CLI must be present; without it, nothing to start.
|
|
455
|
-
if (spawnSyncFn('command -v docker', { stdio: 'ignore', shell: true }).status !== 0) return false;
|
|
456
|
-
|
|
457
|
-
// If the daemon is already up, no work needed.
|
|
458
|
-
if (spawnSyncFn('docker', ['info'], { stdio: 'ignore' }).status === 0) return true;
|
|
459
|
-
|
|
460
|
-
// Best-effort launch. `open -a Docker` returns immediately; the daemon
|
|
461
|
-
// takes 5-30s to come fully up so we poll.
|
|
462
|
-
spawnSyncFn('open', ['-a', 'Docker'], { stdio: 'ignore' });
|
|
463
|
-
|
|
464
|
-
const deadline = Date.now() + timeoutMs;
|
|
465
|
-
while (Date.now() < deadline) {
|
|
466
|
-
await new Promise((r) => setTimeout(r, pollMs));
|
|
467
|
-
if (spawnSyncFn('docker', ['info'], { stdio: 'ignore' }).status === 0) return true;
|
|
468
|
-
}
|
|
469
|
-
return false;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
// The optional services a user can pick from with `construct dev --select` /
|
|
473
|
-
// `--only=`. Pressure Guard (cleanup) and Doctor (the L0 supervisor daemon)
|
|
474
|
-
// are infrastructure, always on — they are intentionally not selectable.
|
|
475
|
-
|
|
476
337
|
export const SELECTABLE_SERVICES = Object.freeze([
|
|
477
|
-
{ key: 'postgres', label: 'Postgres', description: 'Managed pgvector container (or external DATABASE_URL).' },
|
|
478
338
|
{ key: 'dashboard', label: 'Dashboard', description: 'Local operations dashboard on http://127.0.0.1:4242.' },
|
|
479
339
|
{ key: 'telemetry', label: 'Telemetry', description: 'Trace export / local JSONL traces.' },
|
|
480
340
|
{ key: 'memory', label: 'Memory (cm)', description: 'Persistent memory service (cm).' },
|
|
481
341
|
{ key: 'opencode', label: 'OpenCode', description: 'OpenCode bridge server.' },
|
|
342
|
+
{ key: 'copilot-bridge', label: 'Copilot Bridge', description: 'Host-native Copilot bridge proxy (requires gh auth).' },
|
|
482
343
|
]);
|
|
483
344
|
|
|
484
345
|
export async function startServices({
|
|
@@ -488,38 +349,31 @@ export async function startServices({
|
|
|
488
349
|
describeRuntimeSupportFn = describeRuntimeSupport,
|
|
489
350
|
getRuntimePortsFn = getRuntimePorts,
|
|
490
351
|
startDashboardFn = startDashboard,
|
|
491
|
-
detectDockerComposeFn = detectDockerCompose,
|
|
492
352
|
loadConstructEnvFn = loadConstructEnv,
|
|
493
353
|
spawnDetachedFn = spawnDetached,
|
|
494
|
-
verifyTelemetryKeysFn = verifyTelemetryKeys,
|
|
495
354
|
memoryProbeFn = isMemoryRunning,
|
|
496
|
-
openCodeProbeFn =
|
|
355
|
+
openCodeProbeFn = isBridgeRunning,
|
|
497
356
|
runPressureReleaseFn = runPressureRelease,
|
|
498
|
-
tryStartDockerDaemonFn = tryStartDockerDaemon,
|
|
499
357
|
} = {}) {
|
|
500
358
|
const support = await describeRuntimeSupportFn();
|
|
501
359
|
const ports = await getRuntimePortsFn(homeDir);
|
|
502
360
|
const envPath = getUserEnvPath(homeDir);
|
|
503
361
|
|
|
504
|
-
// A null selection means "start everything" (the default). A Set restricts
|
|
505
|
-
// startup to the chosen optional services; always-on infrastructure ignores it.
|
|
506
|
-
|
|
507
362
|
const wants = (key) => selected === null || selected.has(key);
|
|
508
363
|
|
|
509
|
-
// On macOS, auto-launch Docker Desktop when the CLI is present but the
|
|
510
|
-
// daemon is down. Polls up to 60s so downstream Postgres startup sees a working
|
|
511
|
-
// compose runner instead of "Docker not available".
|
|
512
|
-
await tryStartDockerDaemonFn({ env: { ...process.env, HOME: homeDir }, timeoutMs: 5000 });
|
|
513
|
-
|
|
514
364
|
writeEnvValues(envPath, {
|
|
515
365
|
DASHBOARD_PORT: String(ports.dashboard),
|
|
516
366
|
MEMORY_PORT: String(ports.memory),
|
|
517
367
|
BRIDGE_PORT: String(ports.bridge),
|
|
368
|
+
COPILOT_BRIDGE_PORT: String(ports.copilotBridge),
|
|
518
369
|
});
|
|
519
370
|
|
|
520
|
-
// Construct Postgres — start if DATABASE_URL points to managed container
|
|
521
371
|
const liveEnv = loadConstructEnvFn({ rootDir, homeDir });
|
|
522
372
|
const results = [];
|
|
373
|
+
|
|
374
|
+
// Set Ollama persistence for local inference performance
|
|
375
|
+
liveEnv.OLLAMA_KEEP_ALIVE = "-1";
|
|
376
|
+
|
|
523
377
|
const pressureReport = runPressureReleaseFn({ env: { ...liveEnv, HOME: homeDir } });
|
|
524
378
|
if (pressureReport?.killed?.length) {
|
|
525
379
|
results.push({
|
|
@@ -529,40 +383,6 @@ export async function startServices({
|
|
|
529
383
|
note: `terminated ${pressureReport.killed.length} stale process(es)`,
|
|
530
384
|
});
|
|
531
385
|
}
|
|
532
|
-
const databaseUrl = liveEnv.DATABASE_URL || '';
|
|
533
|
-
const usesManagedPostgres = isManagedConstructPostgresUrl(databaseUrl);
|
|
534
|
-
|
|
535
|
-
if (wants('postgres') && usesManagedPostgres) {
|
|
536
|
-
if (!isConstructPostgresRunning()) {
|
|
537
|
-
const pgStart = startConstructPostgres({ rootDir, homeDir, spawnSyncFn: spawnSync, detectDockerComposeFn: detectDockerComposeFn });
|
|
538
|
-
if (pgStart.status === 'started') {
|
|
539
|
-
const healthy = await waitForConstructPostgresHealthy();
|
|
540
|
-
if (healthy) {
|
|
541
|
-
const pgvector = checkPgvectorEnabled();
|
|
542
|
-
results.push({
|
|
543
|
-
name: 'Postgres',
|
|
544
|
-
url: `postgresql://127.0.0.1:${CONSTRUCT_PG_PORT}/construct`,
|
|
545
|
-
status: pgvector ? 'started' : 'degraded',
|
|
546
|
-
note: pgvector ? 'pgvector enabled' : 'pgvector not installed — semantic search unavailable',
|
|
547
|
-
});
|
|
548
|
-
} else {
|
|
549
|
-
results.push({ name: 'Postgres', status: 'degraded', note: 'container started but health check timed out' });
|
|
550
|
-
}
|
|
551
|
-
} else {
|
|
552
|
-
results.push({ name: 'Postgres', status: 'error', note: pgStart.note });
|
|
553
|
-
}
|
|
554
|
-
} else {
|
|
555
|
-
const pgvector = checkPgvectorEnabled();
|
|
556
|
-
results.push({
|
|
557
|
-
name: 'Postgres',
|
|
558
|
-
url: `postgresql://127.0.0.1:${CONSTRUCT_PG_PORT}/construct`,
|
|
559
|
-
status: 'reused',
|
|
560
|
-
note: pgvector ? 'pgvector enabled' : 'pgvector not installed',
|
|
561
|
-
});
|
|
562
|
-
}
|
|
563
|
-
} else if (wants('postgres') && databaseUrl) {
|
|
564
|
-
results.push({ name: 'Postgres', url: databaseUrl, status: 'configured', note: 'external database' });
|
|
565
|
-
}
|
|
566
386
|
|
|
567
387
|
// Dashboard
|
|
568
388
|
if (wants('dashboard')) {
|
|
@@ -575,38 +395,38 @@ export async function startServices({
|
|
|
575
395
|
const telemetryUrl = liveEnv.CONSTRUCT_TELEMETRY_URL ?? '';
|
|
576
396
|
const traceBackend = resolveTraceBackend(liveEnv);
|
|
577
397
|
if (wants('telemetry')) {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
398
|
+
if (traceBackend === 'local') {
|
|
399
|
+
results.push({
|
|
400
|
+
name: 'Telemetry',
|
|
401
|
+
url: path.join(rootDir, '.cx', 'traces'),
|
|
402
|
+
status: 'configured',
|
|
403
|
+
note: 'local JSONL traces; remote export not configured',
|
|
404
|
+
});
|
|
405
|
+
} else if (traceBackend === 'none') {
|
|
406
|
+
results.push({
|
|
407
|
+
name: 'Telemetry',
|
|
408
|
+
url: path.join(rootDir, '.cx', 'traces'),
|
|
409
|
+
status: 'configured',
|
|
410
|
+
note: 'remote export disabled; local JSONL traces preserved',
|
|
411
|
+
});
|
|
412
|
+
} else if (traceBackend === 'otel') {
|
|
413
|
+
const endpoint = liveEnv.CONSTRUCT_OTEL_EXPORTER_OTLP_ENDPOINT || '';
|
|
414
|
+
results.push({
|
|
415
|
+
name: 'Telemetry',
|
|
416
|
+
url: endpoint || undefined,
|
|
417
|
+
status: endpoint ? 'configured' : 'unavailable',
|
|
418
|
+
note: endpoint ? `OTLP export (${telemetryProviderLabel(liveEnv)})` : 'CONSTRUCT_OTEL_EXPORTER_OTLP_ENDPOINT not set',
|
|
419
|
+
});
|
|
420
|
+
} else if (telemetryUrl && (traceBackend === 'langfuse' || traceBackend === 'http')) {
|
|
421
|
+
results.push({
|
|
422
|
+
name: 'Telemetry',
|
|
423
|
+
url: telemetryUrl,
|
|
424
|
+
status: 'configured',
|
|
425
|
+
note: `${telemetryProviderLabel(liveEnv)} export — ${telemetryUrl}`,
|
|
426
|
+
});
|
|
427
|
+
} else {
|
|
428
|
+
results.push({ name: 'Telemetry', status: 'unavailable', note: 'remote export requested but endpoint is not configured' });
|
|
429
|
+
}
|
|
610
430
|
}
|
|
611
431
|
|
|
612
432
|
// Memory (cm)
|
|
@@ -631,6 +451,27 @@ export async function startServices({
|
|
|
631
451
|
}
|
|
632
452
|
}
|
|
633
453
|
|
|
454
|
+
// Copilot Bridge
|
|
455
|
+
if (wants('copilot-bridge') && support.gh) {
|
|
456
|
+
const { detectActiveSessions } = await import('./host-capabilities.mjs');
|
|
457
|
+
const sessions = detectActiveSessions();
|
|
458
|
+
if (sessions.includes('github-copilot')) {
|
|
459
|
+
if (await probeRuntimePort(ports.copilotBridge)) {
|
|
460
|
+
results.push({ name: 'Copilot Bridge', url: `http://127.0.0.1:${ports.copilotBridge}`, status: 'reused' });
|
|
461
|
+
} else {
|
|
462
|
+
const proxyPath = path.join(INSTALL_ROOT, 'lib', 'bridges', 'copilot-proxy.mjs');
|
|
463
|
+
if (fs.existsSync(proxyPath)) {
|
|
464
|
+
spawnDetachedFn('node', [proxyPath, '--port', String(ports.copilotBridge)], homeDir, 'copilot-bridge.log');
|
|
465
|
+
results.push({ name: 'Copilot Bridge', url: `http://127.0.0.1:${ports.copilotBridge}`, status: 'started' });
|
|
466
|
+
} else {
|
|
467
|
+
results.push({ name: 'Copilot Bridge', status: 'unavailable', note: 'copilot-proxy binary missing in lib/bridges/' });
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
} else {
|
|
471
|
+
results.push({ name: 'Copilot Bridge', status: 'unavailable', note: 'gh auth status shows no active GitHub session' });
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
634
475
|
const doctor = startDoctor({ rootDir, homeDir });
|
|
635
476
|
if (doctor.started) {
|
|
636
477
|
results.push({ name: 'Doctor', status: 'started', note: `L0 daemon · logs: ${doctor.logPath}` });
|
|
@@ -650,9 +491,6 @@ export async function startServices({
|
|
|
650
491
|
};
|
|
651
492
|
}
|
|
652
493
|
|
|
653
|
-
// Surface service probe failures into the role-framework event bus so cx-sre
|
|
654
|
-
// can be invoked when a critical service is down. Non-blocking, best effort.
|
|
655
|
-
|
|
656
494
|
async function emitServiceFailures(results) {
|
|
657
495
|
if (process.env.CONSTRUCT_ROLES === 'off') return;
|
|
658
496
|
const failed = results.filter((r) => r.status === 'error');
|
|
@@ -670,17 +508,9 @@ async function emitServiceFailures(results) {
|
|
|
670
508
|
} catch { /* best effort */ }
|
|
671
509
|
}
|
|
672
510
|
|
|
673
|
-
// Exported for testing only
|
|
674
|
-
export { verifyTelemetryKeys as _verifyTelemetryKeys, pruneStashDir as _pruneStashDir };
|
|
675
|
-
|
|
676
|
-
/**
|
|
677
|
-
* Kill every process listening on a given TCP port (best-effort, non-fatal).
|
|
678
|
-
* Returns true if at least one PID was killed.
|
|
679
|
-
*/
|
|
680
511
|
function killPortOwners(port, spawnSyncFn = spawnSync) {
|
|
681
512
|
if (!port || !Number.isInteger(port) || port <= 0) return false;
|
|
682
513
|
try {
|
|
683
|
-
// lsof works on macOS and Linux; -t returns just PIDs
|
|
684
514
|
const result = spawnSyncFn('lsof', ['-t', `-i:${port}`], { encoding: 'utf8' });
|
|
685
515
|
const pids = (result.stdout || '').trim().split(/\s+/).filter(Boolean).map(Number).filter((n) => n > 0);
|
|
686
516
|
if (pids.length === 0) return false;
|
|
@@ -695,21 +525,10 @@ function killPortOwners(port, spawnSyncFn = spawnSync) {
|
|
|
695
525
|
|
|
696
526
|
export async function stopServices({
|
|
697
527
|
homeDir = os.homedir(),
|
|
698
|
-
rootDir,
|
|
699
528
|
spawnSyncFn = spawnSync,
|
|
700
|
-
detectDockerComposeFn = detectDockerCompose,
|
|
701
529
|
} = {}) {
|
|
702
530
|
const results = [];
|
|
703
531
|
|
|
704
|
-
// Stash construct Postgres data before any container is stopped so data
|
|
705
|
-
// survives Docker restarts and machine reboots.
|
|
706
|
-
const constructDbStash = stashConstructDb({ homeDir, spawnSyncFn });
|
|
707
|
-
|
|
708
|
-
// Construct Postgres
|
|
709
|
-
const pgStop = stopConstructPostgres({ rootDir, homeDir, spawnSyncFn, detectDockerComposeFn });
|
|
710
|
-
results.push({ name: 'Postgres', status: pgStop.status });
|
|
711
|
-
|
|
712
|
-
// Doctor (L0 daemon)
|
|
713
532
|
const doctor = stopDoctor(homeDir);
|
|
714
533
|
if (doctor.stopped) {
|
|
715
534
|
results.push({ name: 'Doctor', status: 'stopped', note: `pid ${doctor.pid}` });
|
|
@@ -717,7 +536,6 @@ export async function stopServices({
|
|
|
717
536
|
results.push({ name: 'Doctor', status: 'cleaned', note: 'stale state file removed' });
|
|
718
537
|
}
|
|
719
538
|
|
|
720
|
-
// Dashboard
|
|
721
539
|
const dashboard = stopDashboard(homeDir);
|
|
722
540
|
if (dashboard.stopped) {
|
|
723
541
|
results.push({ name: 'Dashboard', status: 'stopped', note: `pid ${dashboard.pid}, port ${dashboard.port}` });
|
|
@@ -727,18 +545,20 @@ export async function stopServices({
|
|
|
727
545
|
results.push({ name: 'Dashboard', status: 'not-running' });
|
|
728
546
|
}
|
|
729
547
|
|
|
730
|
-
// Memory (cm) — find port from config.env or fall back to default
|
|
731
548
|
const envPath = getUserEnvPath(homeDir);
|
|
732
549
|
const envValues = parseEnvFile(envPath);
|
|
733
550
|
const memoryPort = Number(envValues.MEMORY_PORT) || derivedMemoryPort();
|
|
734
551
|
const cmKilled = killPortOwners(memoryPort, spawnSyncFn);
|
|
735
552
|
results.push({ name: 'Memory (cm)', status: cmKilled ? 'stopped' : 'not-running' });
|
|
736
553
|
|
|
737
|
-
// OpenCode — find port from config.env or fall back to default
|
|
738
554
|
const bridgePort = Number(envValues.BRIDGE_PORT) || 5173;
|
|
739
555
|
const openCodeKilled = killPortOwners(bridgePort, spawnSyncFn);
|
|
740
556
|
results.push({ name: 'OpenCode', status: openCodeKilled ? 'stopped' : 'not-running' });
|
|
741
557
|
|
|
558
|
+
const copilotBridgePort = Number(envValues.COPILOT_BRIDGE_PORT) || 5174;
|
|
559
|
+
const copilotBridgeKilled = killPortOwners(copilotBridgePort, spawnSyncFn);
|
|
560
|
+
results.push({ name: 'Copilot Bridge', status: copilotBridgeKilled ? 'stopped' : 'not-running' });
|
|
561
|
+
|
|
742
562
|
const stopped = results.filter((r) => r.status === 'stopped' || r.status === 'cleaned').map((r) => r.name);
|
|
743
|
-
return { stopped, results
|
|
563
|
+
return { stopped, results };
|
|
744
564
|
}
|