@geraldmaron/construct 1.0.20 → 1.0.23
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 +23 -5
- package/bin/construct +194 -16
- package/bin/construct-postinstall.mjs +25 -15
- package/lib/acp/server.mjs +113 -0
- package/lib/agent-instructions/inject.mjs +94 -0
- package/lib/auto-docs.mjs +10 -2
- package/lib/cli-commands.mjs +44 -16
- package/lib/comment-lint.mjs +115 -0
- package/lib/completions.mjs +7 -1
- package/lib/config/schema.mjs +22 -0
- package/lib/decisions/enforced-baseline.json +3 -0
- package/lib/docs-verify.mjs +15 -18
- package/lib/doctor/cli.mjs +8 -1
- package/lib/document-export.mjs +124 -0
- package/lib/document-ingest.mjs +13 -4
- package/lib/embed/daemon.mjs +1 -1
- package/lib/embed/inbox.mjs +25 -7
- package/lib/embedded-contract/triage.mjs +24 -2
- package/lib/embedded-contract/workflow-invoke.mjs +21 -0
- package/lib/features.mjs +11 -11
- package/lib/git-hooks-path.mjs +61 -0
- package/lib/home-namespace.mjs +60 -0
- package/lib/hooks/ci-status-check.mjs +62 -40
- package/lib/hooks/orchestration-dispatch-guard.mjs +153 -0
- package/lib/hooks/pre-push-gate.mjs +15 -6
- package/lib/hooks/stop-notify.mjs +32 -17
- package/lib/hooks/stop-typecheck.mjs +7 -2
- package/lib/host-capabilities.mjs +24 -8
- package/lib/host-disposition.mjs +76 -0
- package/lib/ingest/provider-extract.mjs +1 -1
- package/lib/ingest/strategy.mjs +35 -3
- package/lib/init-docs.mjs +1 -1
- package/lib/init-unified.mjs +320 -219
- package/lib/init-update.mjs +4 -84
- package/lib/init.mjs +9 -25
- package/lib/install/stage-project.mjs +8 -2
- package/lib/intent-classifier.mjs +1 -1
- package/lib/knowledge/search.mjs +52 -3
- package/lib/mcp/server.mjs +57 -14
- package/lib/mcp/tools/memory.mjs +2 -2
- package/lib/mcp/tools/orchestration-run.mjs +125 -0
- package/lib/model-registry.mjs +40 -33
- package/lib/observation-store.mjs +6 -2
- package/lib/opencode-config.mjs +1 -1
- package/lib/orchestration/events.mjs +66 -0
- package/lib/orchestration/run-store-postgres.mjs +85 -0
- package/lib/orchestration/run-store-sqlite.mjs +122 -0
- package/lib/orchestration/runtime.mjs +188 -50
- package/lib/orchestration/store.mjs +102 -0
- package/lib/orchestration/worker.mjs +215 -0
- package/lib/orchestration-policy.mjs +27 -3
- package/lib/parity.mjs +80 -26
- package/lib/policy/unified-gates.mjs +96 -0
- package/lib/project-init-shared.mjs +0 -173
- package/lib/reconcile/adapter-prune.mjs +105 -0
- package/lib/reconcile/agent-instructions-rewrap.mjs +98 -0
- package/lib/reconcile/gitignore-coverage.mjs +88 -0
- package/lib/reconcile/index.mjs +171 -0
- package/lib/reconcile/legacy-doctrine-strip.mjs +139 -0
- package/lib/reconcile/legacy-guide-decommit.mjs +67 -0
- package/lib/reconcile/legacy-skills-cleanup.mjs +200 -0
- package/lib/reconcile/mcp-entry-reconcile.mjs +142 -0
- package/lib/reconcile/postgres-namespace.mjs +102 -0
- package/lib/runtime/uv-bootstrap.mjs +27 -3
- package/lib/schema-infer.mjs +16 -2
- package/lib/server/csrf.mjs +14 -2
- package/lib/server/index.mjs +95 -0
- package/lib/service-manager.mjs +39 -14
- package/lib/setup-prompts.mjs +2 -1
- package/lib/setup.mjs +165 -141
- package/lib/storage/backend.mjs +14 -0
- package/lib/storage/unified-storage.mjs +550 -0
- package/lib/template-registry.mjs +73 -0
- package/lib/term-format.mjs +75 -0
- package/lib/uninstall/uninstall.mjs +180 -7
- package/package.json +2 -2
- package/personas/construct.md +7 -8
- package/platforms/claude/settings.template.json +30 -4
- package/platforms/opencode/config.template.json +2 -2
- package/rules/common/neurodivergent-output.md +66 -0
- package/rules/common/tool-invisibility.md +37 -0
- package/scripts/sync-specialists.mjs +381 -95
- package/skills/operating/orchestration-reference.md +2 -16
- package/specialists/policy-inventory.json +14 -0
- package/specialists/prompts/cx-accessibility.md +2 -6
- package/specialists/prompts/cx-ai-engineer.md +0 -4
- package/specialists/prompts/cx-architect.md +3 -5
- package/specialists/prompts/cx-business-strategist.md +0 -5
- package/specialists/prompts/cx-data-analyst.md +0 -4
- package/specialists/prompts/cx-data-engineer.md +0 -4
- package/specialists/prompts/cx-debugger.md +2 -6
- package/specialists/prompts/cx-designer.md +0 -8
- package/specialists/prompts/cx-devil-advocate.md +2 -2
- package/specialists/prompts/cx-docs-keeper.md +0 -13
- package/specialists/prompts/cx-engineer.md +0 -13
- package/specialists/prompts/cx-evaluator.md +2 -2
- package/specialists/prompts/cx-explorer.md +4 -5
- package/specialists/prompts/cx-legal-compliance.md +4 -5
- package/specialists/prompts/cx-operations.md +0 -5
- package/specialists/prompts/cx-orchestrator.md +0 -4
- package/specialists/prompts/cx-platform-engineer.md +0 -8
- package/specialists/prompts/cx-product-manager.md +0 -8
- package/specialists/prompts/cx-qa.md +2 -11
- package/specialists/prompts/cx-rd-lead.md +0 -5
- package/specialists/prompts/cx-release-manager.md +0 -8
- package/specialists/prompts/cx-researcher.md +5 -29
- package/specialists/prompts/cx-reviewer.md +2 -6
- package/specialists/prompts/cx-security.md +2 -11
- package/specialists/prompts/cx-sre.md +0 -15
- package/specialists/prompts/cx-test-automation.md +0 -4
- package/specialists/prompts/cx-trace-reviewer.md +2 -2
- package/specialists/prompts/cx-ux-researcher.md +0 -4
- package/specialists/registry.json +29 -29
- package/templates/distribution/run.mjs +36 -7
- package/templates/docs/accessibility-audit.md +56 -0
- package/templates/docs/architecture-review.md +59 -0
- package/templates/docs/code-review-report.md +46 -0
- package/templates/docs/construct_guide.md +14 -14
- package/templates/docs/debug-investigation.md +53 -0
- package/templates/docs/qa-report.md +48 -0
- package/templates/docs/security-audit-report.md +48 -0
- package/templates/docs/task-packet.md +49 -0
- package/templates/docs/verdict.md +40 -0
- package/lib/server/static/index.html +0 -1
package/lib/setup.mjs
CHANGED
|
@@ -27,41 +27,54 @@ import {
|
|
|
27
27
|
} from './deployment-mode.mjs';
|
|
28
28
|
import { getCanonicalOpenCodeConfigPath, readOpenCodeConfig, writeOpenCodeConfig } from './opencode-config.mjs';
|
|
29
29
|
import { syncFileStateToSql } from './storage/sync.mjs';
|
|
30
|
+
import { runMigrations } from './storage/migrations.mjs';
|
|
30
31
|
import { createSqlClient, closeSqlClient } from './storage/backend.mjs';
|
|
31
32
|
import { getEmbeddingModelInfo, warmupEmbeddingModel } from './storage/embeddings-engine.mjs';
|
|
32
33
|
import { restoreConstructDb } from './storage/postgres-backup.mjs';
|
|
33
34
|
import { buildPressureGuardValues, installPressureGuardLaunchAgent, loadPressureGuardLaunchAgent } from './runtime-pressure.mjs';
|
|
34
35
|
import { consentToInstall } from './setup-prompts.mjs';
|
|
36
|
+
import { postgresPort, postgresContainerName } from './home-namespace.mjs';
|
|
35
37
|
|
|
36
38
|
const ROOT_DIR = path.resolve(import.meta.dirname, '..');
|
|
37
39
|
const HOME = os.homedir();
|
|
38
|
-
const LOCAL_POSTGRES_PORT = '54329';
|
|
39
40
|
const LOCAL_POSTGRES_USER = 'construct';
|
|
40
41
|
const LOCAL_POSTGRES_PASSWORD = 'construct';
|
|
41
42
|
const LOCAL_POSTGRES_DB = 'construct';
|
|
42
|
-
|
|
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
|
+
}
|
|
43
50
|
|
|
44
51
|
function printHelp() {
|
|
45
52
|
console.log(`Construct install — machine setup (once per machine)
|
|
46
53
|
|
|
47
54
|
Usage:
|
|
48
|
-
construct install [--yes] [--no-docker] [--reconfigure]
|
|
55
|
+
construct install [--scope=project|user|both] [--yes] [--no-docker] [--no-launch-agent] [--reconfigure]
|
|
49
56
|
|
|
50
57
|
Flags:
|
|
51
|
-
--
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
58
|
+
--scope=<s> project | user | both (default: project — see ADR-0029)
|
|
59
|
+
project: no-op + scope guidance; Construct's project artifacts
|
|
60
|
+
are written by \`construct init\` inside a repo.
|
|
61
|
+
user: write machine-scope state (~/.construct/, MCP configs,
|
|
62
|
+
local Postgres, ~/.claude/* via consent-gated sync).
|
|
63
|
+
both: print project guidance, then run user-scope install.
|
|
64
|
+
--yes accept detected defaults without prompting
|
|
65
|
+
--no-docker skip local Postgres / Docker service setup
|
|
66
|
+
--no-launch-agent skip macOS LaunchAgent background service registration
|
|
67
|
+
--reconfigure re-prompt for service consent, ignoring cached answers
|
|
68
|
+
|
|
69
|
+
What --scope=user does:
|
|
56
70
|
- creates ~/.construct/config.env
|
|
57
71
|
- ensures OpenCode config exists
|
|
58
72
|
- configures managed defaults for local vector retrieval
|
|
59
73
|
- starts local Postgres with Docker when available
|
|
60
74
|
- checks required runtime tools and installs cm and cass when available
|
|
61
75
|
- wires Memory, GitHub, and telemetry configuration
|
|
62
|
-
- runs construct sync (which
|
|
76
|
+
- runs construct sync --global (which writes ~/.claude/* per consent)
|
|
63
77
|
- runs construct doctor
|
|
64
|
-
- detects the project tech stack and writes .cx/project-profile.json
|
|
65
78
|
|
|
66
79
|
For project setup (once per repo): construct init`);
|
|
67
80
|
}
|
|
@@ -125,19 +138,27 @@ export function localPostgresComposePath(homeDir = HOME) {
|
|
|
125
138
|
export function writeLocalPostgresCompose(homeDir = HOME) {
|
|
126
139
|
const composePath = localPostgresComposePath(homeDir);
|
|
127
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`;
|
|
128
149
|
const content = `services:
|
|
129
150
|
postgres:
|
|
130
151
|
image: pgvector/pgvector:pg16
|
|
131
|
-
container_name:
|
|
152
|
+
container_name: ${container}
|
|
132
153
|
restart: unless-stopped
|
|
133
154
|
environment:
|
|
134
155
|
POSTGRES_USER: ${LOCAL_POSTGRES_USER}
|
|
135
156
|
POSTGRES_PASSWORD: ${LOCAL_POSTGRES_PASSWORD}
|
|
136
157
|
POSTGRES_DB: ${LOCAL_POSTGRES_DB}
|
|
137
158
|
ports:
|
|
138
|
-
- "127.0.0.1:${
|
|
159
|
+
- "127.0.0.1:${port}:5432"
|
|
139
160
|
volumes:
|
|
140
|
-
-
|
|
161
|
+
- ${volume}:/var/lib/postgresql/data
|
|
141
162
|
healthcheck:
|
|
142
163
|
test: ["CMD-SHELL", "pg_isready -U ${LOCAL_POSTGRES_USER} -d ${LOCAL_POSTGRES_DB}"]
|
|
143
164
|
interval: 5s
|
|
@@ -145,7 +166,7 @@ export function writeLocalPostgresCompose(homeDir = HOME) {
|
|
|
145
166
|
retries: 20
|
|
146
167
|
|
|
147
168
|
volumes:
|
|
148
|
-
|
|
169
|
+
${volume}:
|
|
149
170
|
`;
|
|
150
171
|
fs.writeFileSync(composePath, content, 'utf8');
|
|
151
172
|
return composePath;
|
|
@@ -372,7 +393,7 @@ export function startManagedPostgres({ homeDir = HOME, env = process.env, spawn
|
|
|
372
393
|
|
|
373
394
|
return {
|
|
374
395
|
status: 'ok',
|
|
375
|
-
databaseUrl:
|
|
396
|
+
databaseUrl: localDatabaseUrl(env, homeDir),
|
|
376
397
|
composePath,
|
|
377
398
|
message: 'Managed local Postgres is running.',
|
|
378
399
|
};
|
|
@@ -431,50 +452,13 @@ function ensureOpenCodeConfig() {
|
|
|
431
452
|
// repo but never executed by git. Idempotent: leaves a user-set value alone
|
|
432
453
|
// rather than clobbering it.
|
|
433
454
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
return { status: 'skipped', reason: 'not a git working tree' };
|
|
442
|
-
}
|
|
443
|
-
const current = spawnSync('git', ['config', '--get', 'core.hooksPath'], { cwd, stdio: 'pipe', encoding: 'utf8' });
|
|
444
|
-
const currentValue = current.status === 0 ? (current.stdout || '').trim() : '';
|
|
445
|
-
const desired = '.beads/hooks';
|
|
446
|
-
if (currentValue === desired) {
|
|
447
|
-
return { status: 'ok', message: 'core.hooksPath already wired to .beads/hooks' };
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// Treat the git default (`.git/hooks` or an absolute path to it) as
|
|
451
|
-
// equivalent to unset. A user who has accepted the default has not made an
|
|
452
|
-
// active choice that justifies "leave alone" semantics; without this branch
|
|
453
|
-
// an install that happens to land while `.git/hooks` is already in play
|
|
454
|
-
// leaves policy gates inactive permanently.
|
|
455
|
-
const isGitDefault =
|
|
456
|
-
!currentValue ||
|
|
457
|
-
currentValue === '.git/hooks' ||
|
|
458
|
-
currentValue === path.join(cwd, '.git', 'hooks') ||
|
|
459
|
-
currentValue.replace(/\/+$/, '').endsWith(`${path.sep}.git${path.sep}hooks`);
|
|
460
|
-
if (!isGitDefault) {
|
|
461
|
-
return {
|
|
462
|
-
status: 'warning',
|
|
463
|
-
message: `core.hooksPath is set to '${currentValue}'. Leaving alone. Set to '.beads/hooks' to activate Construct policy gates.`,
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
|
-
const result = spawnSync('git', ['config', 'core.hooksPath', desired], { cwd, stdio: 'pipe', encoding: 'utf8' });
|
|
467
|
-
if (result.status !== 0) {
|
|
468
|
-
return {
|
|
469
|
-
status: 'error',
|
|
470
|
-
message: `failed to set core.hooksPath: ${(result.stderr || '').trim() || 'unknown error'}`,
|
|
471
|
-
};
|
|
472
|
-
}
|
|
473
|
-
return {
|
|
474
|
-
status: 'set',
|
|
475
|
-
message: `core.hooksPath set to .beads/hooks (activates pre-commit secret-scan + Construct policy gates + beads dispatcher)`,
|
|
476
|
-
};
|
|
477
|
-
}
|
|
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
|
+
export { ensureGitHooksPath } from './git-hooks-path.mjs';
|
|
478
462
|
|
|
479
463
|
// $HOME/.construct/lib/hooks/* is the resolution path for Claude Code
|
|
480
464
|
// Stop-hook commands. Absent symlink → every Stop hook fails silently,
|
|
@@ -505,6 +489,36 @@ export function ensureLibSymlink({ homeDir = HOME, rootDir = ROOT_DIR } = {}) {
|
|
|
505
489
|
};
|
|
506
490
|
}
|
|
507
491
|
|
|
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
|
+
const VALID_SCOPES = new Set(['project', 'user', 'both']);
|
|
498
|
+
const DEFAULT_SCOPE = 'project';
|
|
499
|
+
|
|
500
|
+
function parseScopeFlag(args) {
|
|
501
|
+
for (const a of args) {
|
|
502
|
+
if (a === '--scope') return { invalid: '--scope requires a value (project|user|both)' };
|
|
503
|
+
if (!a.startsWith('--scope=')) continue;
|
|
504
|
+
const v = a.slice('--scope='.length);
|
|
505
|
+
if (!VALID_SCOPES.has(v)) return { invalid: `--scope=${v} is not one of project|user|both` };
|
|
506
|
+
return { scope: v };
|
|
507
|
+
}
|
|
508
|
+
return { scope: DEFAULT_SCOPE };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function printProjectScopeGuidance() {
|
|
512
|
+
console.log('Construct install — scope: project (no-op)');
|
|
513
|
+
console.log('────────────────────────────────────────');
|
|
514
|
+
console.log('Project artifacts are written by `construct init` inside a repo.');
|
|
515
|
+
console.log('To install machine-scope state on this user account:');
|
|
516
|
+
console.log(' construct install --scope=user');
|
|
517
|
+
console.log('To do both project (guidance) and user-scope install:');
|
|
518
|
+
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
|
+
}
|
|
521
|
+
|
|
508
522
|
export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME } = {}) {
|
|
509
523
|
const argSet = new Set(args);
|
|
510
524
|
const isYes = argSet.has('--yes');
|
|
@@ -519,16 +533,39 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
|
|
|
519
533
|
// Reject unknown flags loudly so a typo (e.g. --reconfig) fails fast with the
|
|
520
534
|
// help text instead of silently running defaults.
|
|
521
535
|
|
|
522
|
-
const KNOWN_FLAGS = new Set(['--yes', '--no-docker', '--reconfigure', '--help', '-h']);
|
|
523
|
-
const unknownFlags = args.filter((a) =>
|
|
536
|
+
const KNOWN_FLAGS = new Set(['--yes', '--no-docker', '--no-launch-agent', '--reconfigure', '--help', '-h']);
|
|
537
|
+
const unknownFlags = args.filter((a) => {
|
|
538
|
+
if (!a.startsWith('-')) return false;
|
|
539
|
+
if (a.startsWith('--scope=') || a === '--scope') return false;
|
|
540
|
+
return !KNOWN_FLAGS.has(a);
|
|
541
|
+
});
|
|
524
542
|
if (unknownFlags.length) {
|
|
525
543
|
console.error(`Unknown flag(s): ${unknownFlags.join(', ')}`);
|
|
526
544
|
printHelp();
|
|
527
545
|
throw new Error(`Unknown setup flag(s): ${unknownFlags.join(', ')}`);
|
|
528
546
|
}
|
|
529
547
|
|
|
548
|
+
const scopeResult = parseScopeFlag(args);
|
|
549
|
+
if (scopeResult.invalid) {
|
|
550
|
+
console.error(scopeResult.invalid);
|
|
551
|
+
printHelp();
|
|
552
|
+
throw new Error(`Invalid setup scope: ${scopeResult.invalid}`);
|
|
553
|
+
}
|
|
554
|
+
const scope = scopeResult.scope;
|
|
555
|
+
|
|
556
|
+
if (scope === 'project') {
|
|
557
|
+
printProjectScopeGuidance();
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (scope === 'both') {
|
|
562
|
+
printProjectScopeGuidance();
|
|
563
|
+
console.log('');
|
|
564
|
+
}
|
|
565
|
+
|
|
530
566
|
console.log('Construct setup');
|
|
531
567
|
console.log('────────────────');
|
|
568
|
+
console.log(`Scope: ${scope}`);
|
|
532
569
|
|
|
533
570
|
const envPath = ensureUserConfig(homeDir);
|
|
534
571
|
const opencodePath = ensureOpenCodeConfig();
|
|
@@ -671,12 +708,6 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
|
|
|
671
708
|
console.log('Cheapest provider: already enabled — skipping prompt.');
|
|
672
709
|
}
|
|
673
710
|
|
|
674
|
-
const hooksResult = ensureGitHooksPath({ cwd: process.cwd() });
|
|
675
|
-
if (hooksResult.status === 'set') console.log(`Git hooks: ${hooksResult.message}`);
|
|
676
|
-
else if (hooksResult.status === 'ok') console.log('Git hooks: wired');
|
|
677
|
-
else if (hooksResult.status === 'warning') console.log(`Git hooks: ${hooksResult.message}`);
|
|
678
|
-
else if (hooksResult.status === 'error') console.log(`Git hooks: ${hooksResult.message}`);
|
|
679
|
-
|
|
680
711
|
// Ensure workspace directory with docs lanes exists
|
|
681
712
|
ensureWorkspace(homeDir);
|
|
682
713
|
|
|
@@ -688,14 +719,29 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
|
|
|
688
719
|
writeEnvValues(envPath, managedValues);
|
|
689
720
|
let pressureGuardAgent = null;
|
|
690
721
|
let pressureGuardLoad = null;
|
|
691
|
-
if (process.platform === 'darwin') {
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
722
|
+
if (process.platform === 'darwin' && !argSet.has('--no-launch-agent')) {
|
|
723
|
+
const laConsent = await consentToInstall({
|
|
724
|
+
name: 'launchagent',
|
|
725
|
+
question: 'Install background pressure-release service (macOS LaunchAgent)?',
|
|
726
|
+
isYes,
|
|
727
|
+
force: reconfigure,
|
|
728
|
+
alreadyConfigured: fs.existsSync(path.join(homeDir, 'Library', 'LaunchAgents', 'dev.construct.pressure-release.plist')),
|
|
729
|
+
alreadyConfiguredNote: 'Background pressure-release service already installed.',
|
|
730
|
+
envPath,
|
|
697
731
|
});
|
|
698
|
-
|
|
732
|
+
if (laConsent.decision) {
|
|
733
|
+
pressureGuardAgent = installPressureGuardLaunchAgent({
|
|
734
|
+
homeDir,
|
|
735
|
+
rootDir,
|
|
736
|
+
intervalSeconds: Number(managedValues.CONSTRUCT_PRESSURE_GUARD_INTERVAL_SECONDS || 300),
|
|
737
|
+
nodePath: process.execPath,
|
|
738
|
+
});
|
|
739
|
+
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)');
|
|
699
745
|
}
|
|
700
746
|
|
|
701
747
|
if (isYes) {
|
|
@@ -716,67 +762,54 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
|
|
|
716
762
|
console.log(` LaunchAgent: ${pressureGuardAgent.plistPath}`);
|
|
717
763
|
console.log(` LaunchAgent load: ${pressureGuardLoad?.loaded ? 'active' : pressureGuardLoad?.reason || 'pending manual load'}`);
|
|
718
764
|
}
|
|
765
|
+
}
|
|
719
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') {
|
|
720
789
|
const sqlClient = createSqlClient({ ...process.env, ...managedValues });
|
|
721
790
|
if (sqlClient) {
|
|
722
|
-
let readyClient = null;
|
|
723
|
-
let sqlClientClosed = false;
|
|
724
791
|
try {
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
await readyClient.unsafe(migration);
|
|
733
|
-
console.log('\nPostgres schema initialized.');
|
|
734
|
-
|
|
735
|
-
// Restore data from the most recent stash (if any) before syncing.
|
|
736
|
-
if (serviceResult.status === 'ok') {
|
|
737
|
-
const restore = restoreConstructDb({ homeDir });
|
|
738
|
-
if (restore.status === 'restored') {
|
|
739
|
-
console.log(`Construct DB restored from stash: ${restore.stashPath}`);
|
|
740
|
-
} else if (restore.status === 'no-stash') {
|
|
741
|
-
console.log('No stash found — starting with empty construct DB.');
|
|
742
|
-
} else if (restore.status !== 'no-stash') {
|
|
743
|
-
console.log(`Construct DB restore: ${restore.status}`);
|
|
744
|
-
}
|
|
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}`);
|
|
745
799
|
}
|
|
746
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
|
+
|
|
747
806
|
const syncResult = await syncFileStateToSql(rootDir, { env: { ...process.env, ...managedValues }, project: 'construct' });
|
|
748
807
|
console.log(`Hybrid storage sync: ${syncResult.status}`);
|
|
749
808
|
if (syncResult.embeddingModel) console.log(`Embedding model: ${syncResult.embeddingModel}`);
|
|
750
|
-
|
|
751
|
-
// When setup runs inside a downstream project (cwd is not the
|
|
752
|
-
// Construct package itself), seed the vector index from the
|
|
753
|
-
// project's existing docs / AGENTS / plan so the agent has
|
|
754
|
-
// context to compare new intake against from day one. No-op
|
|
755
|
-
// when there's nothing project-like in cwd.
|
|
756
|
-
|
|
757
|
-
const projectCwd = process.cwd();
|
|
758
|
-
const isDownstreamProject = projectCwd !== rootDir && (
|
|
759
|
-
fs.existsSync(path.join(projectCwd, 'docs')) ||
|
|
760
|
-
fs.existsSync(path.join(projectCwd, 'AGENTS.md')) ||
|
|
761
|
-
fs.existsSync(path.join(projectCwd, 'package.json'))
|
|
762
|
-
);
|
|
763
|
-
if (isDownstreamProject) {
|
|
764
|
-
try {
|
|
765
|
-
const projectName = path.basename(projectCwd);
|
|
766
|
-
const projectSync = await syncFileStateToSql(projectCwd, {
|
|
767
|
-
env: { ...process.env, ...managedValues },
|
|
768
|
-
project: projectName,
|
|
769
|
-
});
|
|
770
|
-
console.log(`Project corpus indexed: ${projectName} (${projectSync.documentsSynced || 0} docs, ${projectSync.embeddingsSynced || 0} embeddings) — agent can now compare new intake against existing material.`);
|
|
771
|
-
} catch (err) {
|
|
772
|
-
console.log(`Project corpus index skipped: ${err?.message || 'unknown error'} — run \`construct ingest\` later to seed manually.`);
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
809
|
} catch (error) {
|
|
776
|
-
console.log(`Hybrid storage
|
|
810
|
+
console.log(`Hybrid storage sync failed: ${error?.message || 'unknown error'}`);
|
|
777
811
|
} finally {
|
|
778
|
-
await closeSqlClient(
|
|
779
|
-
if (!sqlClientClosed) await closeSqlClient(sqlClient);
|
|
812
|
+
await closeSqlClient(sqlClient).catch(() => {});
|
|
780
813
|
}
|
|
781
814
|
}
|
|
782
815
|
}
|
|
@@ -796,26 +829,17 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
|
|
|
796
829
|
|
|
797
830
|
// Install is machine setup: the global front-door agent must land on every
|
|
798
831
|
// user-scope surface (opencode/claude/codex/copilot) or a fresh machine fails
|
|
799
|
-
// cross-surface parity.
|
|
800
|
-
// the
|
|
801
|
-
|
|
802
|
-
|
|
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
|
+
runConstruct(['sync', '--quiet']);
|
|
803
840
|
runConstruct(['sync', '--global']);
|
|
804
841
|
runConstruct(['doctor']);
|
|
805
842
|
|
|
806
|
-
// Profile the CWD so per-host skill filtering layers downstream have
|
|
807
|
-
// a cached, host-agnostic signal to work from. Non-fatal on failure.
|
|
808
|
-
try {
|
|
809
|
-
const { detectProjectProfile, writeProfile } = await import('./project-profile.mjs');
|
|
810
|
-
const profile = detectProjectProfile(process.cwd());
|
|
811
|
-
if (profile.tags.length > 0) {
|
|
812
|
-
const profilePath = writeProfile(profile, process.cwd());
|
|
813
|
-
console.log(`\nProject profile: ${profile.tags.join(', ')}`);
|
|
814
|
-
console.log(` Saved to ${profilePath}`);
|
|
815
|
-
console.log(` Run \`construct skills scope\` to see which installed skills apply to this project.`);
|
|
816
|
-
}
|
|
817
|
-
} catch { /* best effort */ }
|
|
818
|
-
|
|
819
843
|
// ── Summary panel ────────────────────────────────────────────────────────
|
|
820
844
|
const setupTs = new Date().toISOString().slice(0, 19).replace(/[:.]/g, '-');
|
|
821
845
|
const setupLogPath = path.join(HOME, '.cx', `setup-${setupTs}.log`);
|
package/lib/storage/backend.mjs
CHANGED
|
@@ -19,6 +19,19 @@ function cleanUrl(url) {
|
|
|
19
19
|
return String(url || '').trim();
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
// postgres surfaces NOTICE/INFO/DEBUG chatter on idempotent DDL ("extension
|
|
23
|
+
// vector already exists, skipping") through its default onnotice handler, which
|
|
24
|
+
// prints the raw message object to the console and clutters install/init output.
|
|
25
|
+
// Drop that chatter; still surface WARNING and worse to stderr so real problems
|
|
26
|
+
// are not swallowed.
|
|
27
|
+
|
|
28
|
+
function notify(notice) {
|
|
29
|
+
const severity = String(notice?.severity || notice?.severity_local || '').toUpperCase();
|
|
30
|
+
if (severity && !['NOTICE', 'INFO', 'DEBUG', 'LOG'].includes(severity)) {
|
|
31
|
+
process.stderr.write(`[postgres ${severity}] ${notice?.message || ''}\n`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
22
35
|
export function createSqlClient(env = process.env) {
|
|
23
36
|
const databaseUrl = cleanUrl(resolveDatabaseUrl(env));
|
|
24
37
|
if (!databaseUrl || !postgres) return null;
|
|
@@ -26,6 +39,7 @@ export function createSqlClient(env = process.env) {
|
|
|
26
39
|
max: Number.parseInt(env.CONSTRUCT_DB_POOL_SIZE || '5', 10),
|
|
27
40
|
idle_timeout: Number.parseInt(env.CONSTRUCT_DB_IDLE_TIMEOUT_MS || '30000', 10),
|
|
28
41
|
connect_timeout: Number.parseInt(env.CONSTRUCT_DB_CONNECT_TIMEOUT_MS || '5000', 10),
|
|
42
|
+
onnotice: notify,
|
|
29
43
|
});
|
|
30
44
|
}
|
|
31
45
|
|