@geraldmaron/construct 1.0.21 → 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.
Files changed (117) hide show
  1. package/README.md +23 -5
  2. package/bin/construct +191 -13
  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 +25 -7
  17. package/lib/features.mjs +11 -11
  18. package/lib/git-hooks-path.mjs +61 -0
  19. package/lib/home-namespace.mjs +60 -0
  20. package/lib/hooks/ci-status-check.mjs +62 -40
  21. package/lib/hooks/orchestration-dispatch-guard.mjs +153 -0
  22. package/lib/hooks/pre-push-gate.mjs +15 -6
  23. package/lib/hooks/stop-notify.mjs +32 -17
  24. package/lib/hooks/stop-typecheck.mjs +7 -2
  25. package/lib/host-capabilities.mjs +24 -8
  26. package/lib/host-disposition.mjs +76 -0
  27. package/lib/ingest/provider-extract.mjs +1 -1
  28. package/lib/init-docs.mjs +1 -1
  29. package/lib/init-unified.mjs +320 -219
  30. package/lib/init-update.mjs +4 -84
  31. package/lib/init.mjs +9 -25
  32. package/lib/install/stage-project.mjs +8 -2
  33. package/lib/intent-classifier.mjs +1 -1
  34. package/lib/knowledge/search.mjs +52 -3
  35. package/lib/mcp/server.mjs +57 -14
  36. package/lib/mcp/tools/memory.mjs +2 -2
  37. package/lib/mcp/tools/orchestration-run.mjs +125 -0
  38. package/lib/model-registry.mjs +40 -33
  39. package/lib/observation-store.mjs +6 -2
  40. package/lib/opencode-config.mjs +1 -1
  41. package/lib/orchestration/events.mjs +66 -0
  42. package/lib/orchestration/runtime.mjs +70 -9
  43. package/lib/orchestration/worker.mjs +69 -21
  44. package/lib/orchestration-policy.mjs +27 -3
  45. package/lib/parity.mjs +46 -24
  46. package/lib/policy/unified-gates.mjs +96 -0
  47. package/lib/project-init-shared.mjs +0 -173
  48. package/lib/reconcile/adapter-prune.mjs +105 -0
  49. package/lib/reconcile/agent-instructions-rewrap.mjs +98 -0
  50. package/lib/reconcile/gitignore-coverage.mjs +88 -0
  51. package/lib/reconcile/index.mjs +171 -0
  52. package/lib/reconcile/legacy-doctrine-strip.mjs +139 -0
  53. package/lib/reconcile/legacy-guide-decommit.mjs +67 -0
  54. package/lib/reconcile/legacy-skills-cleanup.mjs +200 -0
  55. package/lib/reconcile/mcp-entry-reconcile.mjs +142 -0
  56. package/lib/reconcile/postgres-namespace.mjs +102 -0
  57. package/lib/runtime/uv-bootstrap.mjs +27 -3
  58. package/lib/schema-infer.mjs +16 -2
  59. package/lib/server/csrf.mjs +14 -2
  60. package/lib/server/index.mjs +95 -0
  61. package/lib/service-manager.mjs +39 -14
  62. package/lib/setup-prompts.mjs +2 -1
  63. package/lib/setup.mjs +165 -141
  64. package/lib/storage/backend.mjs +14 -0
  65. package/lib/storage/unified-storage.mjs +550 -0
  66. package/lib/template-registry.mjs +73 -0
  67. package/lib/term-format.mjs +75 -0
  68. package/lib/uninstall/uninstall.mjs +180 -7
  69. package/package.json +2 -2
  70. package/personas/construct.md +7 -8
  71. package/platforms/claude/settings.template.json +30 -4
  72. package/platforms/opencode/config.template.json +2 -2
  73. package/rules/common/neurodivergent-output.md +66 -0
  74. package/rules/common/tool-invisibility.md +37 -0
  75. package/scripts/sync-specialists.mjs +381 -95
  76. package/skills/operating/orchestration-reference.md +2 -16
  77. package/specialists/policy-inventory.json +14 -0
  78. package/specialists/prompts/cx-accessibility.md +2 -6
  79. package/specialists/prompts/cx-ai-engineer.md +0 -4
  80. package/specialists/prompts/cx-architect.md +3 -5
  81. package/specialists/prompts/cx-business-strategist.md +0 -5
  82. package/specialists/prompts/cx-data-analyst.md +0 -4
  83. package/specialists/prompts/cx-data-engineer.md +0 -4
  84. package/specialists/prompts/cx-debugger.md +2 -6
  85. package/specialists/prompts/cx-designer.md +0 -8
  86. package/specialists/prompts/cx-devil-advocate.md +2 -2
  87. package/specialists/prompts/cx-docs-keeper.md +0 -13
  88. package/specialists/prompts/cx-engineer.md +0 -13
  89. package/specialists/prompts/cx-evaluator.md +2 -2
  90. package/specialists/prompts/cx-explorer.md +4 -5
  91. package/specialists/prompts/cx-legal-compliance.md +4 -5
  92. package/specialists/prompts/cx-operations.md +0 -5
  93. package/specialists/prompts/cx-orchestrator.md +0 -4
  94. package/specialists/prompts/cx-platform-engineer.md +0 -8
  95. package/specialists/prompts/cx-product-manager.md +0 -8
  96. package/specialists/prompts/cx-qa.md +2 -11
  97. package/specialists/prompts/cx-rd-lead.md +0 -5
  98. package/specialists/prompts/cx-release-manager.md +0 -8
  99. package/specialists/prompts/cx-researcher.md +5 -29
  100. package/specialists/prompts/cx-reviewer.md +2 -6
  101. package/specialists/prompts/cx-security.md +2 -11
  102. package/specialists/prompts/cx-sre.md +0 -15
  103. package/specialists/prompts/cx-test-automation.md +0 -4
  104. package/specialists/prompts/cx-trace-reviewer.md +2 -2
  105. package/specialists/prompts/cx-ux-researcher.md +0 -4
  106. package/specialists/registry.json +29 -29
  107. package/templates/distribution/run.mjs +36 -7
  108. package/templates/docs/accessibility-audit.md +56 -0
  109. package/templates/docs/architecture-review.md +59 -0
  110. package/templates/docs/code-review-report.md +46 -0
  111. package/templates/docs/construct_guide.md +14 -14
  112. package/templates/docs/debug-investigation.md +53 -0
  113. package/templates/docs/qa-report.md +48 -0
  114. package/templates/docs/security-audit-report.md +48 -0
  115. package/templates/docs/task-packet.md +49 -0
  116. package/templates/docs/verdict.md +40 -0
  117. 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
- const LOCAL_DATABASE_URL = `postgresql://${LOCAL_POSTGRES_USER}:${LOCAL_POSTGRES_PASSWORD}@127.0.0.1:${LOCAL_POSTGRES_PORT}/${LOCAL_POSTGRES_DB}`;
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
- --yes accept detected defaults without prompting
52
- --no-docker skip local Postgres / Docker service setup
53
- --reconfigure re-prompt for service consent, ignoring cached answers
54
-
55
- What it does:
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 also regenerates AUTO docs regions)
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: construct-postgres
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:${LOCAL_POSTGRES_PORT}:5432"
159
+ - "127.0.0.1:${port}:5432"
139
160
  volumes:
140
- - construct-postgres-data:/var/lib/postgresql/data
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
- construct-postgres-data:
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: LOCAL_DATABASE_URL,
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
- export function ensureGitHooksPath({ cwd = process.cwd() } = {}) {
435
- const hooksDir = path.join(cwd, '.beads', 'hooks');
436
- if (!fs.existsSync(path.join(hooksDir, 'pre-commit'))) {
437
- return { status: 'skipped', reason: 'no .beads/hooks/pre-commit in this directory' };
438
- }
439
- const inGit = spawnSync('git', ['rev-parse', '--is-inside-work-tree'], { cwd, stdio: 'pipe', encoding: 'utf8' });
440
- if (inGit.status !== 0) {
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) => a.startsWith('-') && !KNOWN_FLAGS.has(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
- pressureGuardAgent = installPressureGuardLaunchAgent({
693
- homeDir,
694
- rootDir,
695
- intervalSeconds: Number(managedValues.CONSTRUCT_PRESSURE_GUARD_INTERVAL_SECONDS || 300),
696
- nodePath: process.execPath,
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
- pressureGuardLoad = loadPressureGuardLaunchAgent({ plistPath: pressureGuardAgent.plistPath });
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
- if (serviceResult.status === 'ok') {
726
- await closeSqlClient(sqlClient);
727
- sqlClientClosed = true;
728
- await waitForSqlReady({ ...process.env, ...managedValues });
729
- }
730
- readyClient = createSqlClient({ ...process.env, ...managedValues });
731
- const migration = await import('node:fs/promises').then((fs) => fs.readFile(new URL('../db/schema/001_init.sql', import.meta.url), 'utf8'));
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 init failed: ${error?.message || 'unknown error'}`);
810
+ console.log(`Hybrid storage sync failed: ${error?.message || 'unknown error'}`);
777
811
  } finally {
778
- await closeSqlClient(readyClient).catch(() => {});
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. Plain sync writes global hooks + project tier but not
800
- // the front-door agent — `--global` is what populates it, so run both.
801
-
802
- runConstruct(['sync']);
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`);
@@ -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