@geraldmaron/construct 1.0.15 → 1.0.17

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 (198) hide show
  1. package/README.md +19 -3
  2. package/bin/construct +376 -98
  3. package/bin/construct-postinstall.mjs +17 -2
  4. package/db/schema/010_cx_scores.sql +51 -0
  5. package/lib/cli-commands.mjs +333 -147
  6. package/lib/contracts/validate.mjs +20 -10
  7. package/lib/contracts/violation-log.mjs +27 -12
  8. package/lib/embedded-contract/audit.mjs +52 -0
  9. package/lib/embedded-contract/capability.mjs +179 -0
  10. package/lib/embedded-contract/contract-version.mjs +39 -0
  11. package/lib/embedded-contract/envelope.mjs +70 -0
  12. package/lib/embedded-contract/index.mjs +71 -0
  13. package/lib/embedded-contract/ingest.mjs +77 -0
  14. package/lib/embedded-contract/model-resolve.mjs +186 -0
  15. package/lib/embedded-contract/redaction.mjs +91 -0
  16. package/lib/embedded-contract/role-facts.mjs +66 -0
  17. package/lib/embedded-contract/triage.mjs +134 -0
  18. package/lib/embedded-contract/workflow-defs.mjs +125 -0
  19. package/lib/embedded-contract/workflow-invoke.mjs +218 -0
  20. package/lib/hooks/config-protection.mjs +12 -5
  21. package/lib/hooks/stop-notify.mjs +7 -0
  22. package/lib/init-unified.mjs +36 -26
  23. package/lib/install/first-invocation.mjs +5 -1
  24. package/lib/intake/classify.mjs +6 -0
  25. package/lib/intake/prepare.mjs +22 -4
  26. package/lib/intake/tables/rnd.mjs +33 -0
  27. package/lib/integrations/intake-integrations.mjs +28 -2
  28. package/lib/mcp/server.mjs +69 -0
  29. package/lib/mcp/tools/embedded-contract.mjs +77 -0
  30. package/lib/mcp/tools/telemetry.mjs +30 -0
  31. package/lib/model-router.mjs +40 -0
  32. package/lib/op-log.mjs +61 -0
  33. package/lib/orchestration-policy.mjs +15 -0
  34. package/lib/roles/catalog.mjs +26 -95
  35. package/lib/roles/gateway.mjs +30 -1
  36. package/lib/scheduler/index.mjs +24 -4
  37. package/lib/server/insights.mjs +12 -0
  38. package/lib/service-manager.mjs +33 -11
  39. package/lib/setup.mjs +73 -10
  40. package/lib/update.mjs +31 -3
  41. package/lib/upgrade.mjs +31 -4
  42. package/lib/validators/skills.mjs +21 -0
  43. package/package.json +9 -3
  44. package/personas/construct.md +2 -0
  45. package/platforms/claude/CLAUDE.md +43 -15
  46. package/scripts/sync-specialists.mjs +32 -5
  47. package/skills/ai/agent-dev.md +2 -0
  48. package/skills/ai/llm-security.md +2 -0
  49. package/skills/ai/ml-ops.md +2 -0
  50. package/skills/ai/orchestration-workflow.md +2 -0
  51. package/skills/ai/prompt-and-eval.md +2 -0
  52. package/skills/ai/prompt-optimizer.md +2 -0
  53. package/skills/ai/rag-system.md +2 -0
  54. package/skills/ai/trace-triage.md +36 -0
  55. package/skills/architecture/api-design.md +2 -0
  56. package/skills/architecture/caching.md +2 -0
  57. package/skills/architecture/cloud-native.md +2 -0
  58. package/skills/architecture/message-queue.md +2 -0
  59. package/skills/architecture/security-arch.md +2 -0
  60. package/skills/compliance/ai-disclosure.md +2 -0
  61. package/skills/compliance/data-privacy.md +2 -0
  62. package/skills/compliance/license-audit.md +2 -0
  63. package/skills/compliance/regulatory-review.md +2 -0
  64. package/skills/development/cpp.md +2 -0
  65. package/skills/development/go.md +2 -0
  66. package/skills/development/java.md +2 -0
  67. package/skills/development/kotlin.md +2 -0
  68. package/skills/development/mobile-crossplatform.md +2 -0
  69. package/skills/development/python.md +2 -0
  70. package/skills/development/rust.md +2 -0
  71. package/skills/development/shell.md +2 -0
  72. package/skills/development/swift.md +2 -0
  73. package/skills/development/typescript.md +2 -0
  74. package/skills/devops/ci-cd.md +2 -0
  75. package/skills/devops/containerization.md +2 -0
  76. package/skills/devops/cost-optimization.md +2 -0
  77. package/skills/devops/data-engineering.md +2 -0
  78. package/skills/devops/database.md +2 -0
  79. package/skills/devops/dependency-management.md +2 -0
  80. package/skills/devops/devsecops.md +2 -0
  81. package/skills/devops/git-workflow.md +2 -0
  82. package/skills/devops/incident-response.md +2 -0
  83. package/skills/devops/monorepo.md +2 -0
  84. package/skills/devops/observability.md +2 -0
  85. package/skills/devops/performance.md +2 -0
  86. package/skills/devops/testing.md +2 -0
  87. package/skills/docs/adr-workflow.md +2 -0
  88. package/skills/docs/backlog-proposal-workflow.md +2 -0
  89. package/skills/docs/customer-profile-workflow.md +2 -0
  90. package/skills/docs/document-ingest-workflow.md +2 -0
  91. package/skills/docs/evidence-ingest-workflow.md +2 -0
  92. package/skills/docs/init-docs.md +2 -0
  93. package/skills/docs/init-project.md +2 -0
  94. package/skills/docs/memo-and-decision-capture.md +45 -0
  95. package/skills/docs/prd-workflow.md +2 -0
  96. package/skills/docs/prfaq-workflow.md +2 -0
  97. package/skills/docs/product-intelligence-review.md +2 -0
  98. package/skills/docs/product-intelligence-workflow.md +2 -0
  99. package/skills/docs/product-signal-workflow.md +2 -0
  100. package/skills/docs/research-workflow.md +2 -0
  101. package/skills/docs/runbook-workflow.md +2 -0
  102. package/skills/docs/strategy-workflow.md +2 -0
  103. package/skills/docs/transcript-synthesis.md +43 -0
  104. package/skills/exploration/dependency-graph-reading.md +2 -0
  105. package/skills/exploration/repo-map.md +2 -0
  106. package/skills/exploration/tracer-bullet-method.md +2 -0
  107. package/skills/exploration/unknown-codebase-onboarding.md +2 -0
  108. package/skills/frameworks/django.md +2 -0
  109. package/skills/frameworks/nextjs.md +2 -0
  110. package/skills/frameworks/react.md +2 -0
  111. package/skills/frameworks/spring-boot.md +2 -0
  112. package/skills/frontend-design/accessibility.md +2 -0
  113. package/skills/frontend-design/component-patterns.md +2 -0
  114. package/skills/frontend-design/engineering.md +2 -0
  115. package/skills/frontend-design/screen-reader-testing.md +34 -0
  116. package/skills/frontend-design/state-management.md +2 -0
  117. package/skills/frontend-design/ui-aesthetics.md +2 -0
  118. package/skills/frontend-design/ux-principles.md +2 -0
  119. package/skills/operating/change-management.md +2 -0
  120. package/skills/operating/incident-response.md +2 -0
  121. package/skills/operating/oncall-rotation.md +2 -0
  122. package/skills/operating/orchestration-reference.md +2 -0
  123. package/skills/operating/raw-data-structuring.md +44 -0
  124. package/skills/operating/unstructured-triage.md +45 -0
  125. package/skills/quality-gates/premortem.md +37 -0
  126. package/skills/quality-gates/review-work.md +2 -0
  127. package/skills/quality-gates/verify-change.md +2 -0
  128. package/skills/quality-gates/verify-module.md +2 -0
  129. package/skills/quality-gates/verify-quality.md +2 -0
  130. package/skills/quality-gates/verify-security.md +2 -0
  131. package/skills/roles/architect.ai-systems.md +2 -0
  132. package/skills/roles/architect.data.md +2 -0
  133. package/skills/roles/architect.enterprise.md +2 -0
  134. package/skills/roles/architect.integration.md +2 -0
  135. package/skills/roles/architect.md +2 -0
  136. package/skills/roles/architect.platform.md +2 -0
  137. package/skills/roles/data-analyst.experiment.md +2 -0
  138. package/skills/roles/data-analyst.md +2 -0
  139. package/skills/roles/data-analyst.product-intelligence.md +2 -0
  140. package/skills/roles/data-analyst.product.md +2 -0
  141. package/skills/roles/data-analyst.telemetry.md +2 -0
  142. package/skills/roles/data-engineer.pipeline.md +2 -0
  143. package/skills/roles/data-engineer.vector-retrieval.md +2 -0
  144. package/skills/roles/data-engineer.warehouse.md +2 -0
  145. package/skills/roles/debugger.md +2 -0
  146. package/skills/roles/designer.accessibility.md +2 -0
  147. package/skills/roles/designer.md +2 -0
  148. package/skills/roles/engineer.ai.md +2 -0
  149. package/skills/roles/engineer.data.md +2 -0
  150. package/skills/roles/engineer.md +2 -0
  151. package/skills/roles/engineer.platform.md +2 -0
  152. package/skills/roles/operator.docs.md +2 -0
  153. package/skills/roles/operator.md +2 -0
  154. package/skills/roles/operator.release.md +2 -0
  155. package/skills/roles/operator.sre.md +2 -0
  156. package/skills/roles/orchestrator.md +2 -0
  157. package/skills/roles/product-manager.ai-product.md +2 -0
  158. package/skills/roles/product-manager.business-strategy.md +2 -0
  159. package/skills/roles/product-manager.enterprise.md +2 -0
  160. package/skills/roles/product-manager.growth.md +2 -0
  161. package/skills/roles/product-manager.md +2 -0
  162. package/skills/roles/product-manager.platform.md +2 -0
  163. package/skills/roles/product-manager.product.md +2 -0
  164. package/skills/roles/qa.ai-eval.md +2 -0
  165. package/skills/roles/qa.api-contract.md +2 -0
  166. package/skills/roles/qa.data-pipeline.md +2 -0
  167. package/skills/roles/qa.md +2 -0
  168. package/skills/roles/qa.test-automation.md +2 -0
  169. package/skills/roles/qa.web-ui.md +2 -0
  170. package/skills/roles/researcher.explorer.md +2 -0
  171. package/skills/roles/researcher.md +2 -0
  172. package/skills/roles/researcher.ux.md +2 -0
  173. package/skills/roles/reviewer.devil-advocate.md +2 -0
  174. package/skills/roles/reviewer.evaluator.md +2 -0
  175. package/skills/roles/reviewer.md +2 -0
  176. package/skills/roles/reviewer.trace.md +2 -0
  177. package/skills/roles/security.ai.md +2 -0
  178. package/skills/roles/security.appsec.md +2 -0
  179. package/skills/roles/security.cloud.md +2 -0
  180. package/skills/roles/security.legal-compliance.md +2 -0
  181. package/skills/roles/security.md +2 -0
  182. package/skills/roles/security.privacy.md +2 -0
  183. package/skills/roles/security.supply-chain.md +2 -0
  184. package/skills/security/blue-team.md +2 -0
  185. package/skills/security/code-audit.md +2 -0
  186. package/skills/security/pentest.md +2 -0
  187. package/skills/security/red-team.md +2 -0
  188. package/skills/security/threat-intel.md +2 -0
  189. package/skills/security/vuln-research.md +2 -0
  190. package/skills/strategy/competitive-landscape.md +2 -0
  191. package/skills/strategy/jobs-to-be-done.md +38 -0
  192. package/skills/strategy/market-research-methods.md +2 -0
  193. package/skills/strategy/narrative-arc.md +2 -0
  194. package/skills/strategy/pricing-positioning.md +2 -0
  195. package/skills/utility/clean-code.md +2 -0
  196. package/specialists/prompts/cx-engineer.md +1 -1
  197. package/specialists/registry.json +18 -9
  198. package/specialists/role-manifests.json +1 -1
@@ -442,9 +442,22 @@ async function tryStartDockerDaemon({
442
442
  return false;
443
443
  }
444
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
+ export const SELECTABLE_SERVICES = Object.freeze([
450
+ { key: 'postgres', label: 'Postgres', description: 'Managed pgvector container (or external DATABASE_URL).' },
451
+ { key: 'dashboard', label: 'Dashboard', description: 'Local operations dashboard on http://127.0.0.1:4242.' },
452
+ { key: 'telemetry', label: 'Telemetry', description: 'Trace export / local JSONL traces.' },
453
+ { key: 'memory', label: 'Memory (cm)', description: 'Persistent memory service (cm).' },
454
+ { key: 'opencode', label: 'OpenCode', description: 'OpenCode bridge server.' },
455
+ ]);
456
+
445
457
  export async function startServices({
446
458
  rootDir,
447
459
  homeDir = os.homedir(),
460
+ selected = null,
448
461
  describeRuntimeSupportFn = describeRuntimeSupport,
449
462
  getRuntimePortsFn = getRuntimePorts,
450
463
  startDashboardFn = startDashboard,
@@ -461,6 +474,11 @@ export async function startServices({
461
474
  const ports = await getRuntimePortsFn(homeDir);
462
475
  const envPath = getUserEnvPath(homeDir);
463
476
 
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
+ const wants = (key) => selected === null || selected.has(key);
481
+
464
482
  // On macOS, auto-launch Docker Desktop when the CLI is present but the
465
483
  // daemon is down. Polls up to 60s so downstream Postgres startup sees a working
466
484
  // compose runner instead of "Docker not available".
@@ -487,7 +505,7 @@ export async function startServices({
487
505
  const databaseUrl = liveEnv.DATABASE_URL || '';
488
506
  const usesManagedPostgres = isManagedConstructPostgresUrl(databaseUrl);
489
507
 
490
- if (usesManagedPostgres) {
508
+ if (wants('postgres') && usesManagedPostgres) {
491
509
  if (!isConstructPostgresRunning()) {
492
510
  const pgStart = startConstructPostgres({ rootDir, homeDir, spawnSyncFn: spawnSync, detectDockerComposeFn: detectDockerComposeFn });
493
511
  if (pgStart.status === 'started') {
@@ -515,20 +533,23 @@ export async function startServices({
515
533
  note: pgvector ? 'pgvector enabled' : 'pgvector not installed',
516
534
  });
517
535
  }
518
- } else if (databaseUrl) {
536
+ } else if (wants('postgres') && databaseUrl) {
519
537
  results.push({ name: 'Postgres', url: databaseUrl, status: 'configured', note: 'external database' });
520
538
  }
521
539
 
522
540
  // Dashboard
523
- const dashboard = await startDashboardFn({ rootDir, homeDir, preferredPort: ports.dashboard });
524
- results.push({
525
- name: 'Dashboard',
526
- url: dashboard.url,
527
- status: dashboard.reused ? 'reused' : 'started',
528
- });
541
+ if (wants('dashboard')) {
542
+ 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
+ });
548
+ }
529
549
 
530
550
  const telemetryUrl = liveEnv.CONSTRUCT_TELEMETRY_URL ?? '';
531
551
  const traceBackend = resolveTraceBackend(liveEnv);
552
+ if (wants('telemetry')) {
532
553
  if (traceBackend === 'local') {
533
554
  results.push({
534
555
  name: 'Telemetry',
@@ -561,21 +582,22 @@ export async function startServices({
561
582
  } else {
562
583
  results.push({ name: 'Telemetry', status: 'unavailable', note: 'remote export requested but endpoint is not configured' });
563
584
  }
585
+ }
564
586
 
565
587
  // Memory (cm)
566
- if (support.cm) {
588
+ if (wants('memory') && support.cm) {
567
589
  if (await memoryProbeFn(ports.memory)) {
568
590
  results.push({ name: 'Memory (cm)', url: `http://127.0.0.1:${ports.memory}`, status: 'reused' });
569
591
  } else {
570
592
  spawnDetachedFn('cm', ['serve', '--port', String(ports.memory)], homeDir, 'cm.log');
571
593
  results.push({ name: 'Memory (cm)', url: `http://127.0.0.1:${ports.memory}`, status: 'started' });
572
594
  }
573
- } else {
595
+ } else if (wants('memory')) {
574
596
  results.push({ name: 'Memory (cm)', status: 'unavailable', note: 'cm not installed — run: construct init or brew install dicklesworthstone/tap/cm' });
575
597
  }
576
598
 
577
599
  // OpenCode (optional)
578
- if (support.opencode) {
600
+ if (wants('opencode') && support.opencode) {
579
601
  if (await openCodeProbeFn(ports.bridge)) {
580
602
  results.push({ name: 'OpenCode', url: `http://127.0.0.1:${ports.bridge}`, status: 'reused' });
581
603
  } else {
package/lib/setup.mjs CHANGED
@@ -1,10 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * lib/setup.mjs — Interactive first-run setup wizard for Construct.
3
+ * lib/setup.mjs — Machine-scoped first-run setup wizard for Construct.
4
4
  *
5
- * Guides users through provider selection, API key entry, and model tier
6
- * assignment. Writes the resulting config to ~/.cx/env and optionally to
7
- * project-level .env. Invoked by `construct init` and on first init.
5
+ * Installs cm/cass, configures managed defaults, starts local Postgres
6
+ * (consent-driven), pre-warms the embedding model, wires MCP integrations,
7
+ * and writes ~/.construct/config.env. Invoked by `construct install` and
8
+ * by lib/install/first-invocation.mjs when a TTY user is missing resources.
9
+ *
10
+ * Project-scoped scaffolding (`.cx/`, AGENTS.md, plan.md, adapters) lives
11
+ * in lib/init-unified.mjs — invoked by `construct init`. These two flows
12
+ * are intentionally separate: install runs once per machine, init runs
13
+ * once per repo.
8
14
  */
9
15
 
10
16
  import fs from 'node:fs';
@@ -36,10 +42,15 @@ const LOCAL_POSTGRES_DB = 'construct';
36
42
  const LOCAL_DATABASE_URL = `postgresql://${LOCAL_POSTGRES_USER}:${LOCAL_POSTGRES_PASSWORD}@127.0.0.1:${LOCAL_POSTGRES_PORT}/${LOCAL_POSTGRES_DB}`;
37
43
 
38
44
  function printHelp() {
39
- console.log(`Construct setup
45
+ console.log(`Construct install — machine setup (once per machine)
40
46
 
41
47
  Usage:
42
- construct init [--yes] [--no-docker]
48
+ construct install [--yes] [--no-docker] [--reconfigure]
49
+
50
+ 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
43
54
 
44
55
  What it does:
45
56
  - creates ~/.construct/config.env
@@ -52,7 +63,7 @@ What it does:
52
63
  - runs construct doctor
53
64
  - detects the project tech stack and writes .cx/project-profile.json
54
65
 
55
- Use --yes to run without prompts and accept detected environment defaults.`);
66
+ For project setup (once per repo): construct init`);
56
67
  }
57
68
 
58
69
  function runConstruct(argsList, { optional = false } = {}) {
@@ -465,24 +476,73 @@ export function ensureGitHooksPath({ cwd = process.cwd() } = {}) {
465
476
  };
466
477
  }
467
478
 
479
+ // $HOME/.construct/lib/hooks/* is the resolution path for Claude Code
480
+ // Stop-hook commands. Absent symlink → every Stop hook fails silently,
481
+ // including stop-notify (dashboard cost feed). Idempotent: matching
482
+ // symlink left alone, stale symlink replaced, real dir refused.
483
+
484
+ export function ensureLibSymlink({ homeDir = HOME, rootDir = ROOT_DIR } = {}) {
485
+ const target = path.join(homeDir, '.construct', 'lib');
486
+ const source = path.join(rootDir, 'lib');
487
+ fs.mkdirSync(path.dirname(target), { recursive: true });
488
+ let stat = null;
489
+ try { stat = fs.lstatSync(target); } catch { stat = null; }
490
+ if (!stat) {
491
+ fs.symlinkSync(source, target, 'dir');
492
+ return { status: 'created', target, source };
493
+ }
494
+ if (stat.isSymbolicLink()) {
495
+ const current = fs.readlinkSync(target);
496
+ if (current === source) return { status: 'kept', target, source };
497
+ fs.unlinkSync(target);
498
+ fs.symlinkSync(source, target, 'dir');
499
+ return { status: 'replaced', target, source, previous: current };
500
+ }
501
+ return {
502
+ status: 'conflict',
503
+ target,
504
+ message: `${target} exists and is not a symlink — leaving alone. Move or remove it, then re-run \`construct install\` to wire the hooks.`,
505
+ };
506
+ }
507
+
468
508
  export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME } = {}) {
469
509
  const argSet = new Set(args);
470
510
  const isYes = argSet.has('--yes');
471
511
  const skipDocker = argSet.has('--no-docker');
512
+ const reconfigure = argSet.has('--reconfigure');
472
513
 
473
514
  if (argSet.has('--help') || argSet.has('-h')) {
474
515
  printHelp();
475
516
  return;
476
517
  }
477
518
 
519
+ // Reject unknown flags loudly so a typo (e.g. --reconfig) fails fast with the
520
+ // help text instead of silently running defaults.
521
+
522
+ const KNOWN_FLAGS = new Set(['--yes', '--no-docker', '--reconfigure', '--help', '-h']);
523
+ const unknownFlags = args.filter((a) => a.startsWith('-') && !KNOWN_FLAGS.has(a));
524
+ if (unknownFlags.length) {
525
+ console.error(`Unknown flag(s): ${unknownFlags.join(', ')}`);
526
+ printHelp();
527
+ throw new Error(`Unknown setup flag(s): ${unknownFlags.join(', ')}`);
528
+ }
529
+
478
530
  console.log('Construct setup');
479
531
  console.log('────────────────');
480
532
 
481
533
  const envPath = ensureUserConfig(homeDir);
482
534
  const opencodePath = ensureOpenCodeConfig();
535
+ const libLink = ensureLibSymlink({ homeDir, rootDir });
483
536
 
484
537
  console.log(`User config: ${envPath}`);
485
538
  console.log(`OpenCode config: ${opencodePath}`);
539
+ if (libLink.status === 'created' || libLink.status === 'replaced') {
540
+ console.log(`Hook lib link: ${libLink.target} → ${libLink.source} (${libLink.status})`);
541
+ } else if (libLink.status === 'kept') {
542
+ console.log(`Hook lib link: ${libLink.target} already in place`);
543
+ } else if (libLink.status === 'conflict') {
544
+ console.log(`Hook lib link: ${libLink.message}`);
545
+ }
486
546
  warnIfGlobalCommandIsUnavailable();
487
547
 
488
548
  const cmInstall = ensureCmInstalled({ env: process.env });
@@ -521,6 +581,7 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
521
581
  const pgConsent = await consentToInstall({
522
582
  name: 'postgres',
523
583
  isYes,
584
+ force: reconfigure,
524
585
  alreadyConfigured: Boolean(process.env.DATABASE_URL),
525
586
  alreadyConfiguredNote: 'DATABASE_URL already set — using external database.',
526
587
  envPath,
@@ -576,12 +637,14 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
576
637
  const cheapestConsent = await consentToInstall({
577
638
  name: 'cheapest-provider',
578
639
  isYes,
640
+ force: reconfigure,
579
641
  alreadyConfigured: false,
580
642
  envPath,
581
643
  defaultYes: false,
582
644
  });
583
645
  if (cheapestConsent.decision) {
584
646
  try {
647
+ const { applyToEnv } = await import('./model-router.mjs');
585
648
  const selections = await selectCheapestForAllTiers({ env: process.env });
586
649
  const applied = {};
587
650
  for (const tier of ['reasoning', 'standard', 'fast']) {
@@ -637,7 +700,7 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
637
700
 
638
701
  if (isYes) {
639
702
  console.log('\nManaged setup:');
640
- console.log(` Deployment mode: ${getDeploymentMode(env) || DEFAULT_DEPLOYMENT_MODE} (set in construct.config.json — runtime env override available via ${DEPLOYMENT_MODE_ENV_KEY})`);
703
+ console.log(` Deployment mode: ${getDeploymentMode(process.env) || DEFAULT_DEPLOYMENT_MODE} (set in construct.config.json — runtime env override available via ${DEPLOYMENT_MODE_ENV_KEY})`);
641
704
  console.log(` Vector index: ${managedValues.CONSTRUCT_VECTOR_INDEX_PATH}`);
642
705
  console.log(` Vector model: ${managedValues.CONSTRUCT_VECTOR_MODEL}`);
643
706
  console.log(` Trace backend: ${managedValues.CONSTRUCT_TRACE_BACKEND}`);
@@ -723,12 +786,12 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
723
786
  runConstruct(['mcp', 'add', 'github', '--auto'], { optional: true });
724
787
  } else {
725
788
  console.log('\nManaged defaults written:');
726
- console.log(` Deployment mode: ${getDeploymentMode(env) || DEFAULT_DEPLOYMENT_MODE} (set in construct.config.json — runtime env override available via ${DEPLOYMENT_MODE_ENV_KEY})`);
789
+ console.log(` Deployment mode: ${getDeploymentMode(process.env) || DEFAULT_DEPLOYMENT_MODE} (set in construct.config.json — runtime env override available via ${DEPLOYMENT_MODE_ENV_KEY})`);
727
790
  console.log(` Vector index: ${managedValues.CONSTRUCT_VECTOR_INDEX_PATH}`);
728
791
  console.log(` Trace backend: ${managedValues.CONSTRUCT_TRACE_BACKEND}${managedValues.CONSTRUCT_TELEMETRY_URL ? ` (${managedValues.CONSTRUCT_TELEMETRY_URL})` : ''}`);
729
792
  console.log(` Pressure guard: swap ${managedValues.CONSTRUCT_PRESSURE_GUARD_SWAP_GB} GiB, opencode max ${managedValues.CONSTRUCT_PRESSURE_GUARD_MAX_OPENCODE}`);
730
793
  console.log('\nFor unattended setup, including local Postgres when Docker is running:');
731
- console.log(' construct init --yes');
794
+ console.log(' construct install --yes');
732
795
  }
733
796
 
734
797
  runConstruct(['sync']);
package/lib/update.mjs CHANGED
@@ -10,6 +10,8 @@ import fs from 'node:fs';
10
10
  import path from 'node:path';
11
11
  import { spawnSync } from 'node:child_process';
12
12
 
13
+ import { writeVersionStamp } from './maintenance/cleanup.mjs';
14
+
13
15
  const PACKAGE_NAME = '@geraldmaron/construct';
14
16
 
15
17
  function readPackageJson(dir) {
@@ -70,6 +72,15 @@ export function buildUpdatePlan({ cwd }) {
70
72
  args: [binPath, 'sync', '--no-docs'],
71
73
  cwd: sourceRoot,
72
74
  },
75
+ // Sweep legacy cx-*.md/.toml/.prompt.md left over from older releases
76
+ // at user scope, so the next doctor doesn't surface the warning.
77
+ {
78
+ label: 'Sweep legacy agents at user scope',
79
+ command: process.execPath,
80
+ args: [binPath, 'doctor', '--fix-legacy-agents'],
81
+ cwd: sourceRoot,
82
+ optional: true,
83
+ },
73
84
  {
74
85
  label: 'Run health checks',
75
86
  command: process.execPath,
@@ -84,14 +95,24 @@ function formatCommand(command, args) {
84
95
  return [command, ...args].join(' ');
85
96
  }
86
97
 
87
- function runStep(step, { spawn, env }) {
98
+ function runStep(step, { spawn, env, stdout }) {
88
99
  const result = spawn(step.command, step.args, {
89
100
  cwd: step.cwd,
90
101
  env,
91
102
  stdio: 'inherit',
92
103
  });
93
- if (result?.error) throw result.error;
104
+ if (result?.error) {
105
+ if (step.optional) {
106
+ stdout?.write?.(` (skipped: ${result.error.message})\n`);
107
+ return;
108
+ }
109
+ throw result.error;
110
+ }
94
111
  if ((result?.status ?? 1) !== 0) {
112
+ if (step.optional) {
113
+ stdout?.write?.(` (skipped: exit ${result?.status ?? 1})\n`);
114
+ return;
115
+ }
95
116
  throw new Error(`${step.label} failed with exit code ${result.status ?? 1}`);
96
117
  }
97
118
  }
@@ -107,9 +128,16 @@ export function runUpdate({ cwd, env = process.env, spawn = spawnSync, stdout =
107
128
  for (const step of plan.steps) {
108
129
  stdout.write(`→ ${step.label}\n`);
109
130
  stdout.write(` ${formatCommand(step.command, step.args)}\n`);
110
- runStep(step, { spawn, env });
131
+ runStep(step, { spawn, env, stdout });
111
132
  }
112
133
 
134
+ // Advance the cleanup stamp to the checkout version so the next CLI
135
+ // invocation doesn't run a full cleanup pass on the assumption that
136
+ // an upgrade just happened.
137
+ try {
138
+ writeVersionStamp({ version: plan.version, summary: { freedBytes: 0, durationMs: 0 } });
139
+ } catch { /* stamp write is best-effort */ }
140
+
113
141
  stdout.write('\n✓ Construct updated from current checkout.\n');
114
142
  return plan;
115
143
  }
package/lib/upgrade.mjs CHANGED
@@ -10,6 +10,8 @@ import path from 'node:path';
10
10
  import { spawnSync } from 'node:child_process';
11
11
  import { fileURLToPath } from 'node:url';
12
12
 
13
+ import { writeVersionStamp } from './maintenance/cleanup.mjs';
14
+
13
15
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
16
  const ROOT_DIR = path.resolve(__dirname, '..');
15
17
  const PACKAGE_NAME = '@geraldmaron/construct';
@@ -97,7 +99,7 @@ export function runUpgrade({ cwd = process.cwd(), env = process.env, stdout = pr
97
99
  }
98
100
 
99
101
  stdout.write('Upgrading...\n\n');
100
-
102
+
101
103
  const steps = [
102
104
  {
103
105
  label: 'Fetching latest from npm',
@@ -109,6 +111,15 @@ export function runUpgrade({ cwd = process.cwd(), env = process.env, stdout = pr
109
111
  command: 'construct',
110
112
  args: ['sync', '--no-docs'],
111
113
  },
114
+ // Sweep legacy cx-*.md/.toml/.prompt.md left behind by older releases at
115
+ // user scope. Without this the user gets a doctor warning every session
116
+ // until they manually run `construct doctor --fix-legacy-agents`.
117
+ {
118
+ label: 'Sweeping legacy agents at user scope',
119
+ command: 'construct',
120
+ args: ['doctor', '--fix-legacy-agents'],
121
+ optional: true,
122
+ },
112
123
  {
113
124
  label: 'Running health checks',
114
125
  command: 'construct',
@@ -123,18 +134,34 @@ export function runUpgrade({ cwd = process.cwd(), env = process.env, stdout = pr
123
134
  env,
124
135
  stdio: 'inherit',
125
136
  });
126
-
137
+
127
138
  if (result.error) {
139
+ if (step.optional) {
140
+ stdout.write(` (skipped: ${result.error.message})\n`);
141
+ continue;
142
+ }
128
143
  stdout.write(`\n✗ ${step.label} failed: ${result.error.message}\n`);
129
144
  return { success: false, reason: 'step_failed', step: step.label };
130
145
  }
131
-
146
+
132
147
  if (result.status !== 0) {
148
+ if (step.optional) {
149
+ stdout.write(` (skipped: exit ${result.status})\n`);
150
+ continue;
151
+ }
133
152
  stdout.write(`\n✗ ${step.label} failed with exit code ${result.status}\n`);
134
153
  return { success: false, reason: 'step_failed', step: step.label };
135
154
  }
136
155
  }
137
-
156
+
157
+ // Advance the cleanup stamp so the next CLI invocation doesn't re-run a
158
+ // full cleanup pass under the assumption that the user just upgraded.
159
+ // Without this, every command pays the cleanup cost until something
160
+ // else writes the stamp.
161
+ try {
162
+ writeVersionStamp({ version: latestVersion, summary: { freedBytes: 0, durationMs: 0 } });
163
+ } catch { /* stamp write is best-effort */ }
164
+
138
165
  stdout.write('\n');
139
166
  stdout.write(`✓ Upgraded to ${latestVersion}\n`);
140
167
  return { success: true, upgraded: true, from: currentVersion, to: latestVersion };
@@ -6,6 +6,8 @@
6
6
  * ---
7
7
  * name: <kebab-case>
8
8
  * description: "<≤1024 chars, includes a 'use when' trigger>"
9
+ * [inputs: [<string>, …] — optional, surfaced by capability discovery]
10
+ * [artifactType: <string> — optional, surfaced by capability discovery]
9
11
  * [role/applies_to/inherits/version/profiles/cap for role files]
10
12
  * ---
11
13
  * # <Title>
@@ -100,6 +102,23 @@ function validateDescription(description) {
100
102
  return null;
101
103
  }
102
104
 
105
+ // Optional embedded-contract metadata: surfaced by capability discovery when
106
+ // present. Validated only when present so the field stays opt-in and is
107
+ // populated incrementally — inputs is a list of strings, artifactType a string.
108
+
109
+ function validateOptionalMetadata(frontmatter) {
110
+ const issues = [];
111
+ if ('inputs' in frontmatter && frontmatter.inputs !== null) {
112
+ if (!Array.isArray(frontmatter.inputs) || frontmatter.inputs.some((i) => typeof i !== 'string')) {
113
+ issues.push('frontmatter.inputs must be a list of strings when present');
114
+ }
115
+ }
116
+ if ('artifactType' in frontmatter && frontmatter.artifactType !== null && typeof frontmatter.artifactType !== 'string') {
117
+ issues.push('frontmatter.artifactType must be a string when present');
118
+ }
119
+ return issues;
120
+ }
121
+
103
122
  export function validateSkills(roots) {
104
123
  const dirs = Array.isArray(roots) ? roots : [roots];
105
124
  const errors = [];
@@ -150,6 +169,8 @@ export function validateSkills(roots) {
150
169
  const descErr = validateDescription(frontmatter.description);
151
170
  if (descErr) errors.push(`${rel}: ${descErr}`);
152
171
 
172
+ for (const issue of validateOptionalMetadata(frontmatter)) errors.push(`${rel}: ${issue}`);
173
+
153
174
  const { title, opener } = extractTitleAndOpener(body);
154
175
 
155
176
  if (!title) {
package/package.json CHANGED
@@ -1,11 +1,17 @@
1
1
  {
2
2
  "name": "@geraldmaron/construct",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
4
4
  "type": "module",
5
5
  "description": "Construct — agent orchestration layer for OpenCode, Claude Code, and other coding surfaces",
6
6
  "bin": {
7
7
  "construct": "bin/construct"
8
8
  },
9
+ "exports": {
10
+ ".": "./lib/embedded-contract/index.mjs",
11
+ "./embedded-contract": "./lib/embedded-contract/index.mjs",
12
+ "./lib/*": "./lib/*",
13
+ "./package.json": "./package.json"
14
+ },
9
15
  "files": [
10
16
  "bin/construct",
11
17
  "bin/construct-postinstall.mjs",
@@ -78,8 +84,8 @@
78
84
  },
79
85
  "optionalDependencies": {
80
86
  "@opentelemetry/api": "^1.9.0",
81
- "@opentelemetry/core": "^1.25.0",
82
- "@opentelemetry/exporter-trace-otlp-http": "^0.52.0",
87
+ "@opentelemetry/core": "^2.7.1",
88
+ "@opentelemetry/exporter-trace-otlp-http": "^0.218.0",
83
89
  "@opentelemetry/resources": "^1.25.0",
84
90
  "@opentelemetry/sdk-trace-node": "^1.25.0",
85
91
  "@opentelemetry/semantic-conventions": "^1.25.0"
@@ -34,6 +34,8 @@ Execution model:
34
34
 
35
35
  Devil's advocate is mandatory for new architectural directions, AI/agent workflow changes, security or data-integrity changes, and promoting a temporary capability to persistent.
36
36
 
37
+ Orchestrated dispatches emit a task-packet with `goal`, `intent`, `workCategory`, `riskFlags`, `acceptanceCriteria` before naming specialists (`specialists/contracts.json:construct-to-orchestrator`).
38
+
37
39
  ## Gates and contracts (org-in-a-box)
38
40
 
39
41
  `routeRequest` returns three artifacts; honor all three:
@@ -1,30 +1,58 @@
1
1
  # Construct: Claude Code Integration
2
2
 
3
- This project uses Construct. Personas and specialists are defined in `specialists/registry.json` and synced to Claude Code via `construct sync`.
3
+ This file ships as a reference template alongside the Construct npm package. The canonical project-instructions file is `/CLAUDE.md` at your repo root — that's the file Claude Code actually reads at session start. The structure here mirrors that canonical file so users know what shape their own `/CLAUDE.md` should take after `construct init` scaffolds a project.
4
+
5
+ Personas and specialists are defined in `specialists/registry.json` and rendered into Claude Code agents on every `construct sync`. The single user-facing persona is `construct`; the 28 specialists (cx-engineer, cx-security, cx-devil-advocate, …) are routed internally — you address Construct, Construct dispatches.
6
+
7
+ ## Critical rules (mirror in your project /CLAUDE.md)
8
+
9
+ - **Never fabricate.** Every load-bearing claim cites a verifiable source. When a fact isn't in the source, write `unknown` or `[unverified]`. See `rules/common/no-fabrication.md`.
10
+ - **Confirm the working branch every session.** Session-start surfaces `## Working branch: <name>` at the top of the injected context.
11
+ - **Never commit, push, or merge without asking first.** State the branch, state the action, wait for explicit yes.
12
+ - **Never edit running hook files** without testing them in isolation. A broken hook blocks all tool use.
13
+ - **Hooks fire unconditionally. No skip env vars on quality gates.** If a check fires wrong, repair the check — do not re-introduce `CONSTRUCT_SKIP_*` / `CONSTRUCT_ALLOW_*`.
14
+ - **Never commit directly to main.** Branch, test, then merge.
15
+ - **Run `construct doctor`** after structural changes.
4
16
 
5
17
  ## Workflow roles
6
18
 
7
- Construct is the only intended user-facing surface.
19
+ | Role | What it covers |
20
+ |---|---|
21
+ | **Planning** | Requirements, strategy, architecture, framing challenges |
22
+ | **Implementation** | Builds features and fixes bugs |
23
+ | **Validation** | Quality gates, code review, security, accessibility |
24
+ | **Research** | Docs, debugging, codebase exploration, external research |
25
+ | **Operations** | Releases, dev servers, health checks, observability |
26
+
27
+ Construct routes complex work through the full pipeline (plan → implement → validate → operate). For simple tasks, Construct acts directly without exposing the internal routing.
28
+
29
+ ## Beads issue tracker
30
+
31
+ This project uses **bd (beads)** for issue tracking. Run `bd prime` for the full workflow context.
8
32
 
9
- - **Planning**: requirements, strategy, architecture
10
- - **Implementation**: builds features and fixes bugs
11
- - **Validation**: quality gates, security, accessibility
12
- - **Research**: docs, debugging, codebase exploration
13
- - **Operations**: releases, dev servers, health checks
33
+ ```bash
34
+ bd ready # Find available work
35
+ bd show <id> # View issue details
36
+ bd update <id> --claim # Claim work
37
+ bd close <id> # Complete work
38
+ ```
14
39
 
15
- ## Usage
40
+ Use `bd` for all task tracking. Do NOT use TodoWrite, TaskCreate, or markdown TODO lists.
16
41
 
17
- Talk to Construct normally. It routes complex work through the full pipeline internally:
18
- plan → implement → validate → operate
42
+ ## Session completion
19
43
 
20
- For simple tasks, Construct can act directly without exposing internal routing.
44
+ When ending a work session, **work is not complete until `git push` succeeds.**
21
45
 
22
- All 26 internal specialists (cx-engineer, cx-security, cx-devil-advocate, etc.) are available as subagents.
46
+ ```bash
47
+ git pull --rebase
48
+ git push
49
+ git status # MUST show "up to date with origin"
50
+ ```
23
51
 
24
- ## Tool Calls
52
+ ## Tool calls
25
53
 
26
54
  When using Bash, always provide both `command` and `description` string fields. Do not emit XML-style fallback tool calls.
27
55
 
28
- ## Cross-Tool Memory
56
+ ## Cross-tool memory
29
57
 
30
- Construct uses cass-memory for cross-tool memory. Start the local HTTP server with `cm serve`, then use `memory_search` to find prior context.
58
+ Construct uses cass-memory for cross-tool memory. Start the local HTTP server with `cm serve`, then `memory_search` to find prior context. Memory and beads are complementary: memory holds knowledge, beads hold tasks.
@@ -172,8 +172,17 @@ function detectConstructProject(cwd) {
172
172
 
173
173
  const detectedProject = (!PROJECT_FLAG && !GLOBAL_FLAG) ? detectConstructProject(process.cwd()) : null;
174
174
  const projectDir = PROJECT_FLAG ? process.cwd() : detectedProject;
175
- const lockPath = path.join(projectDir || root, ".cx", "sync.lock");
176
- const stagingDir = path.join(projectDir || root, ".cx", "sync-staging");
175
+
176
+ // Lock and staging are scoped to the tier we actually mutate: a project dir for
177
+ // project-tier writes, the user's HOME for global-tier writes (which land in
178
+ // ~/.claude, ~/.codex, …). Keying the global tier to HOME — not the repo root —
179
+ // means two --global syncs against different HOMEs (e.g. parallel test files in
180
+ // isolated sandboxes) never collide on a shared repo-root lock, and staging
181
+ // renames stay on the same filesystem as their destinations.
182
+
183
+ const stateBase = projectDir || home;
184
+ const lockPath = path.join(stateBase, ".cx", "sync.lock");
185
+ const stagingDir = path.join(stateBase, ".cx", "sync-staging");
177
186
 
178
187
  // Project-tier writes carry every registry entry. Global-tier writes carry only
179
188
  // the `construct` front-door agent — specialists live with the project, not the
@@ -243,7 +252,7 @@ function writeFile(file, content, { stamp = true } = {}) {
243
252
  }
244
253
 
245
254
  // Two-phase: write to staging, commit later.
246
- const rel = path.relative(projectDir || root, file);
255
+ const rel = path.relative(stateBase, file);
247
256
  const stagingPath = path.join(stagingDir, rel);
248
257
  mkdirp(path.dirname(stagingPath));
249
258
  fs.writeFileSync(stagingPath, stamped);
@@ -950,7 +959,16 @@ function getVSCodeSettingsPaths() {
950
959
  path.join(appData, "Code - Insiders", "User", "settings.json"),
951
960
  );
952
961
  }
953
- return candidates.filter(fs.existsSync);
962
+ // settings.json present → ready to merge. settings.json absent but the
963
+ // VS Code User dir exists → installed without ever opening Preferences,
964
+ // and global sync still needs to write the MCP block. The User dir is
965
+ // the installation signal; bootstrap a minimal settings.json there.
966
+
967
+ return candidates.filter((candidate) => {
968
+ if (fs.existsSync(candidate)) return true;
969
+ const userDir = path.dirname(candidate);
970
+ return fs.existsSync(userDir);
971
+ });
954
972
  }
955
973
 
956
974
  function syncVSCode(targetDir = null) {
@@ -991,7 +1009,16 @@ function syncVSCode(targetDir = null) {
991
1009
  let synced = false;
992
1010
  for (const settingsPath of settingsPaths) {
993
1011
  try {
994
- const settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
1012
+ let settings;
1013
+ if (fs.existsSync(settingsPath)) {
1014
+ settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
1015
+ } else {
1016
+ // VS Code installed (User dir present) but never opened → seed an
1017
+ // empty settings.json. Single object literal, no other keys, so
1018
+ // a later launch of VS Code finds it valid and writes user prefs
1019
+ // alongside.
1020
+ settings = {};
1021
+ }
995
1022
  if (!settings["github.copilot.mcpServers"]) settings["github.copilot.mcpServers"] = {};
996
1023
  const mcpServers = settings["github.copilot.mcpServers"];
997
1024
  for (const [id, mcpDef] of Object.entries(registryMcp)) {
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  name: ai-agent-dev
3
3
  description: Use this skill when building AI agents, tool-use systems, or multi-agent workflows.
4
+ inputs: [agent-spec, task-context]
5
+ artifactType: agent-implementation
4
6
  ---
5
7
  # AI Agent Development
6
8
 
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  name: ai-llm-security
3
3
  description: Use this skill when securing LLM-powered applications against prompt injection, data leakage, and misuse.
4
+ inputs: [prompt, model-output, agent-spec]
5
+ artifactType: guidance
4
6
  ---
5
7
  # LLM Security
6
8
 
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  name: ai-ml-ops
3
3
  description: Patterns, anti-patterns, and reference guidance for ML Operations (MLOps). Use when the task involves ml operations (mlops).
4
+ inputs: [eval-dataset, model-output]
5
+ artifactType: guidance
4
6
  ---
5
7
  # ML Operations (MLOps)
6
8