@geraldmaron/construct 1.0.6 → 1.0.7

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 (123) hide show
  1. package/README.md +19 -17
  2. package/agents/contracts.json +617 -2
  3. package/agents/contracts.schema.json +83 -0
  4. package/agents/prompts/cx-accessibility.md +2 -0
  5. package/agents/prompts/cx-ai-engineer.md +2 -0
  6. package/agents/prompts/cx-architect.md +2 -0
  7. package/agents/prompts/cx-business-strategist.md +2 -0
  8. package/agents/prompts/cx-data-analyst.md +2 -0
  9. package/agents/prompts/cx-data-engineer.md +2 -0
  10. package/agents/prompts/cx-debugger.md +2 -0
  11. package/agents/prompts/cx-designer.md +2 -0
  12. package/agents/prompts/cx-devil-advocate.md +2 -0
  13. package/agents/prompts/cx-docs-keeper.md +2 -0
  14. package/agents/prompts/cx-engineer.md +2 -0
  15. package/agents/prompts/cx-evaluator.md +2 -0
  16. package/agents/prompts/cx-explorer.md +2 -0
  17. package/agents/prompts/cx-legal-compliance.md +2 -0
  18. package/agents/prompts/cx-operations.md +2 -0
  19. package/agents/prompts/cx-orchestrator.md +2 -0
  20. package/agents/prompts/cx-platform-engineer.md +2 -0
  21. package/agents/prompts/cx-product-manager.md +2 -0
  22. package/agents/prompts/cx-qa.md +2 -0
  23. package/agents/prompts/cx-rd-lead.md +2 -0
  24. package/agents/prompts/cx-release-manager.md +2 -0
  25. package/agents/prompts/cx-researcher.md +2 -0
  26. package/agents/prompts/cx-reviewer.md +2 -0
  27. package/agents/prompts/cx-security.md +2 -0
  28. package/agents/prompts/cx-sre.md +2 -0
  29. package/agents/prompts/cx-test-automation.md +2 -0
  30. package/agents/prompts/cx-trace-reviewer.md +2 -0
  31. package/agents/prompts/cx-ux-researcher.md +2 -0
  32. package/agents/registry.json +8 -3
  33. package/bin/construct +209 -32
  34. package/commands/build/feature.md +1 -1
  35. package/commands/build/fix.md +1 -1
  36. package/commands/design/access.md +1 -1
  37. package/commands/design/flow.md +1 -1
  38. package/commands/design/ui.md +1 -1
  39. package/commands/measure/experiment.md +1 -1
  40. package/commands/measure/metrics.md +1 -1
  41. package/commands/measure/results.md +1 -1
  42. package/commands/plan/api.md +1 -1
  43. package/commands/plan/challenge.md +1 -1
  44. package/commands/plan/decide.md +1 -1
  45. package/commands/plan/feature.md +1 -1
  46. package/commands/plan/requirements.md +1 -1
  47. package/commands/remember/context.md +1 -1
  48. package/commands/remember/handoff.md +1 -1
  49. package/commands/remember/runbook.md +1 -1
  50. package/commands/review/code.md +1 -1
  51. package/commands/review/quality.md +1 -1
  52. package/commands/review/security.md +1 -1
  53. package/commands/ship/ready.md +1 -1
  54. package/commands/ship/release.md +1 -1
  55. package/commands/ship/status.md +1 -1
  56. package/commands/understand/docs.md +1 -1
  57. package/commands/understand/this.md +1 -1
  58. package/commands/understand/why.md +1 -1
  59. package/commands/work/clean.md +1 -1
  60. package/commands/work/drive.md +1 -1
  61. package/commands/work/optimize-prompts.md +1 -1
  62. package/commands/work/parallel-review.md +1 -1
  63. package/lib/auto-docs.mjs +7 -7
  64. package/lib/boundary.mjs +126 -0
  65. package/lib/cache-strategy-google.js +26 -31
  66. package/lib/comment-lint.mjs +134 -0
  67. package/lib/contracts/validate.mjs +323 -0
  68. package/lib/daemons/contract.mjs +210 -0
  69. package/lib/docs-verify.mjs +59 -6
  70. package/lib/doctor/cli.mjs +16 -1
  71. package/lib/doctor/index.mjs +3 -1
  72. package/lib/doctor/watchers/consistency.mjs +310 -0
  73. package/lib/doctor/watchers/mcp-protocol.mjs +232 -0
  74. package/lib/document-extract.mjs +211 -1
  75. package/lib/embed/cli.mjs +124 -3
  76. package/lib/embed/daemon.mjs +43 -4
  77. package/lib/embed/docs-lifecycle.mjs +1 -1
  78. package/lib/embed/inbox.mjs +2 -0
  79. package/lib/embed/scheduler.mjs +33 -5
  80. package/lib/evaluator-optimizer.mjs +2 -3
  81. package/lib/hooks/comment-lint.mjs +16 -0
  82. package/lib/hooks/mcp-audit.mjs +2 -1
  83. package/lib/hooks/proactive-activation.mjs +0 -14
  84. package/lib/hooks/rule-verifier.mjs +217 -0
  85. package/lib/hooks/session-optimize.mjs +2 -1
  86. package/lib/init-unified.mjs +55 -65
  87. package/lib/intake/classify.mjs +108 -24
  88. package/lib/intake/daemon.mjs +121 -0
  89. package/lib/intake/filesystem-queue.mjs +6 -1
  90. package/lib/intake/intake-config.mjs +2 -1
  91. package/lib/intake/prepare.mjs +0 -1
  92. package/lib/intake/session-prelude.mjs +7 -1
  93. package/lib/intake/traceability.mjs +90 -0
  94. package/lib/knowledge/research-store.mjs +2 -0
  95. package/lib/maintenance/cleanup.mjs +315 -0
  96. package/lib/mcp/memory-bridge.mjs +276 -0
  97. package/lib/mcp/server.mjs +2 -0
  98. package/lib/mcp/tools/workflow.mjs +25 -0
  99. package/lib/mcp-catalog.json +12 -8
  100. package/lib/mcp-platform-config.mjs +16 -8
  101. package/lib/migrations/index.mjs +106 -0
  102. package/lib/migrations/v1-baseline.mjs +33 -0
  103. package/lib/observation-store.mjs +9 -4
  104. package/lib/outcomes/record.mjs +2 -0
  105. package/lib/profiles/rebrand.mjs +46 -0
  106. package/lib/project-init-shared.mjs +12 -0
  107. package/lib/provider-capabilities.js +20 -7
  108. package/lib/providers/auth-manager.mjs +58 -17
  109. package/lib/reflect.mjs +49 -12
  110. package/lib/server/index.mjs +22 -28
  111. package/lib/session-store.mjs +6 -4
  112. package/lib/setup.mjs +14 -3
  113. package/lib/telemetry/client.mjs +5 -1
  114. package/lib/version.mjs +51 -0
  115. package/lib/worker/trace.mjs +5 -1
  116. package/package.json +4 -1
  117. package/personas/construct.md +3 -1
  118. package/rules/common/development-workflow.md +2 -1
  119. package/rules/common/no-fabrication.md +69 -0
  120. package/rules/common/review-before-change.md +58 -0
  121. package/scripts/sync-agents.mjs +45 -14
  122. package/templates/docs/construct_guide.md +6 -6
  123. package/templates/docs/skill-artifact.md +1 -1
package/bin/construct CHANGED
@@ -292,7 +292,7 @@ async function cmdList() {
292
292
  async function cmdDoctor() {
293
293
  const args = process.argv.slice(3);
294
294
 
295
- const DAEMON_SUBS = new Set(['status', 'watch', 'stop', 'logs', 'tick', 'report']);
295
+ const DAEMON_SUBS = new Set(['status', 'watch', 'stop', 'logs', 'tick', 'report', 'consistency']);
296
296
  if (args.length > 0 && DAEMON_SUBS.has(args[0])) {
297
297
  const { runCli } = await import('../lib/doctor/cli.mjs');
298
298
  const code = await runCli(args);
@@ -1053,6 +1053,59 @@ async function cmdVersion() {
1053
1053
  println(`construct v${pkg.version}`);
1054
1054
  }
1055
1055
 
1056
+ async function cmdMigrate(args) {
1057
+ const dryRun = args.includes('--dry-run');
1058
+ const pathArg = args.find((a) => a.startsWith('--path='))?.split('=')[1];
1059
+ const fromArg = args.find((a) => a.startsWith('--from='))?.split('=')[1];
1060
+ const { runMigrations, planMigrations, CURRENT_SCHEMA_VERSION } = await import('../lib/migrations/index.mjs');
1061
+
1062
+ const candidates = pathArg
1063
+ ? [pathArg]
1064
+ : [
1065
+ path.join(ROOT_DIR, 'agents', 'registry.json'),
1066
+ path.join(ROOT_DIR, 'agents', 'contracts.json'),
1067
+ path.join(ROOT_DIR, 'agents', 'role-manifests.json'),
1068
+ path.join(process.cwd(), '.cx', 'config.json'),
1069
+ ];
1070
+
1071
+ let anyFailed = false;
1072
+ for (const artifactPath of candidates) {
1073
+ if (!fs.existsSync(artifactPath)) continue;
1074
+ let from;
1075
+ try {
1076
+ const data = JSON.parse(fs.readFileSync(artifactPath, 'utf8'));
1077
+ from = typeof data?.version === 'number' ? data.version : Number(fromArg) || 0;
1078
+ } catch {
1079
+ println(` ⚠ ${artifactPath}: parse error, skipping`);
1080
+ continue;
1081
+ }
1082
+
1083
+ const plan = planMigrations(from, CURRENT_SCHEMA_VERSION);
1084
+ if (plan === null) {
1085
+ println(` ✗ ${artifactPath}: no migration path from v${from} to v${CURRENT_SCHEMA_VERSION}`);
1086
+ anyFailed = true;
1087
+ continue;
1088
+ }
1089
+ if (plan.length === 0) {
1090
+ println(` ✓ ${artifactPath}: already at v${CURRENT_SCHEMA_VERSION}`);
1091
+ continue;
1092
+ }
1093
+
1094
+ println(` ${dryRun ? '·' : '→'} ${artifactPath}: ${plan.length} step(s) (v${from} → v${CURRENT_SCHEMA_VERSION})`);
1095
+ const result = await runMigrations({ artifactPath, fromVersion: from, dryRun });
1096
+ for (const step of result.applied) {
1097
+ println(` v${step.from}→v${step.to}: ${step.summary}`);
1098
+ }
1099
+ if (!result.ok) {
1100
+ println(` ✗ ${result.error}`);
1101
+ anyFailed = true;
1102
+ }
1103
+ }
1104
+
1105
+ if (dryRun) println(`\n(dry-run — no files were written)`);
1106
+ if (anyFailed) process.exit(1);
1107
+ }
1108
+
1056
1109
  async function cmdReview(args) {
1057
1110
  fs.mkdirSync(path.join(HOME, '.cx', 'performance-reviews'), { recursive: true });
1058
1111
  const sub = args[0];
@@ -1550,13 +1603,16 @@ function printActiveConfig() {
1550
1603
  println(`Persisted at: ${getUserEnvPath(HOME)} (${DEPLOYMENT_MODE_ENV_KEY})`);
1551
1604
  }
1552
1605
 
1553
- function printIntakeHelp() {
1554
- println('Construct intake inspect and process the R&D intake queue.');
1606
+ async function printIntakeHelp() {
1607
+ const { getRebrand } = await import('../lib/profiles/rebrand.mjs');
1608
+ const { intakeQueueLabel, signalNoun } = getRebrand(process.cwd());
1609
+ println(`Construct intake — inspect and process the ${intakeQueueLabel}.`);
1555
1610
  println('');
1556
1611
  println('Usage:');
1557
- println(' construct intake list List pending intake packets (ID, type, stage, owner, action).');
1612
+ println(` construct intake list List pending ${signalNoun}s (ID, type, stage, owner, action).`);
1558
1613
  println(' construct intake show <id> Show a single packet: triage, source, excerpt, related docs.');
1559
- println(' construct intake done <id> [--notes=…] Mark a pending packet processed.');
1614
+ println(' construct intake done <id> [--notes=…] [--output=<path>] [--no-output=<rejected|deferred|merged>]');
1615
+ println(' Mark a pending packet processed. --output stamps intake_id + confidence into the artifact frontmatter (provenance).');
1560
1616
  println(' construct intake skip <id> [--reason=…] Mark a pending packet skipped (preserves audit trail).');
1561
1617
  println(' construct intake reopen <id> Move a processed or skipped packet back to pending.');
1562
1618
  println(' construct intake metrics Show pipeline volume, velocity, and processing stats.');
@@ -1580,11 +1636,12 @@ function parseKvFlags(args, keys) {
1580
1636
  return out;
1581
1637
  }
1582
1638
 
1583
- function formatIntakeTable(rows) {
1639
+ function formatIntakeTable(rows, { intakeQueueLabel, signalNoun } = { intakeQueueLabel: 'Intake queue', signalNoun: 'signal' }) {
1584
1640
  if (rows.length === 0) {
1585
- println('No pending intake packets.');
1641
+ println(`No pending ${signalNoun}s.`);
1586
1642
  return;
1587
1643
  }
1644
+ println(`${intakeQueueLabel} (${rows.length} pending):`);
1588
1645
  const headers = ['ID', 'Type', 'Stage', 'Owner', 'Action'];
1589
1646
  const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => String(r[i] || '').length)));
1590
1647
  const fmt = (cells) => cells.map((c, i) => String(c || '').padEnd(widths[i])).join(' ');
@@ -1597,12 +1654,14 @@ async function cmdIntake(args) {
1597
1654
  const sub = args[0];
1598
1655
 
1599
1656
  if (!sub || sub === '--help' || sub === '-h') {
1600
- printIntakeHelp();
1657
+ await printIntakeHelp();
1601
1658
  return;
1602
1659
  }
1603
1660
 
1604
1661
  const { createIntakeQueue } = await import('../lib/intake/queue.mjs');
1662
+ const { getRebrand } = await import('../lib/profiles/rebrand.mjs');
1605
1663
  const cwd = process.cwd();
1664
+ const rebrand = getRebrand(cwd);
1606
1665
  const queue = createIntakeQueue(cwd, process.env);
1607
1666
 
1608
1667
  if (sub === 'list') {
@@ -1611,7 +1670,7 @@ async function cmdIntake(args) {
1611
1670
  const t = p.triage || {};
1612
1671
  return [p.id, t.intakeType || 'unknown', t.rdStage || 'unknown', t.primaryOwner || '—', t.recommendedAction || '—'];
1613
1672
  });
1614
- formatIntakeTable(rows);
1673
+ formatIntakeTable(rows, rebrand);
1615
1674
  return;
1616
1675
  }
1617
1676
 
@@ -1619,8 +1678,9 @@ async function cmdIntake(args) {
1619
1678
  if (sub === 'show') {
1620
1679
  if (!id) { errorln('Usage: construct intake show <id>'); process.exit(1); }
1621
1680
  const entry = queue.read(id);
1622
- if (!entry) { errorln(`No intake packet with id: ${id}`); process.exit(1); }
1681
+ if (!entry) { errorln(`No ${rebrand.signalNoun} with id: ${id}`); process.exit(1); }
1623
1682
  const t = entry.triage || {};
1683
+ println(`${rebrand.signalNoun.charAt(0).toUpperCase() + rebrand.signalNoun.slice(1)} ${entry.id}`);
1624
1684
  println(`ID: ${entry.id}`);
1625
1685
  println(`Status: ${entry.status}`);
1626
1686
  println(`Source: ${entry.intake?.sourcePath || '(none)'}`);
@@ -1712,11 +1772,59 @@ async function cmdIntake(args) {
1712
1772
  }
1713
1773
 
1714
1774
  if (sub === 'done') {
1715
- if (!id) { errorln('Usage: construct intake done <id> [--notes=…]'); process.exit(1); }
1716
- const flags = parseKvFlags(args.slice(2), ['notes']);
1775
+ if (!id) {
1776
+ errorln('Usage: construct intake done <id> [--notes=…] [--output=<path>] [--no-output=<rejected|deferred|merged>]');
1777
+ process.exit(1);
1778
+ }
1779
+ const flags = parseKvFlags(args.slice(2), ['notes', 'output', 'no-output']);
1780
+ const outputPath = flags.output || null;
1781
+ const noOutputOutcome = flags['no-output'] || null;
1782
+
1783
+ if (outputPath && noOutputOutcome) {
1784
+ errorln('--output and --no-output are mutually exclusive');
1785
+ process.exit(1);
1786
+ }
1787
+ if (noOutputOutcome && !['rejected', 'deferred', 'merged'].includes(noOutputOutcome)) {
1788
+ errorln(`--no-output value must be one of: rejected, deferred, merged (got "${noOutputOutcome}")`);
1789
+ process.exit(1);
1790
+ }
1791
+
1792
+ let stamped = null;
1793
+ if (outputPath) {
1794
+ const packet = queue.read(id);
1795
+ if (!packet) {
1796
+ errorln(`No intake packet found: ${id}`);
1797
+ process.exit(1);
1798
+ }
1799
+ const triage = packet.triage || {};
1800
+ try {
1801
+ const { stampIntakeProvenance } = await import('../lib/intake/traceability.mjs');
1802
+ const path = (await import('node:path')).default;
1803
+ const absPath = path.isAbsolute(outputPath) ? outputPath : path.join(cwd, outputPath);
1804
+ stamped = stampIntakeProvenance(absPath, {
1805
+ intakeId: id,
1806
+ confidence: triage.confidence,
1807
+ rationale: triage.rationale,
1808
+ });
1809
+ } catch (err) {
1810
+ errorln(err.message || 'intake stamping failed');
1811
+ process.exit(1);
1812
+ }
1813
+ }
1814
+
1815
+ const notes = flags.notes
1816
+ || (noOutputOutcome ? `intake_outcome: ${noOutputOutcome}` : '')
1817
+ || (outputPath ? `output: ${outputPath}` : '');
1818
+
1717
1819
  try {
1718
- const r = queue.markProcessed(id, { processedBy: 'construct-intake-cli', notes: flags.notes || '' });
1719
- info(`Marked ${r.id} processed.`);
1820
+ const r = queue.markProcessed(id, { processedBy: 'construct-intake-cli', notes });
1821
+ if (stamped) {
1822
+ info(`Marked ${r.id} processed. Stamped ${outputPath} with intake_id=${id} (confidence=${stamped.intake_confidence ?? 'n/a'}).`);
1823
+ } else if (noOutputOutcome) {
1824
+ info(`Marked ${r.id} processed (intake_outcome: ${noOutputOutcome}).`);
1825
+ } else {
1826
+ info(`Marked ${r.id} processed.`);
1827
+ }
1720
1828
  } catch (err) {
1721
1829
  errorln(err.message || 'mark processed failed');
1722
1830
  process.exit(1);
@@ -2741,27 +2849,55 @@ async function cmdSeedTraces(args) {
2741
2849
  async function cmdCleanup() {
2742
2850
  const force = restArgsCache.includes('--pressure-release');
2743
2851
  const quiet = restArgsCache.includes('--quiet');
2744
- const report = runPressureRelease({ env: process.env, force });
2745
-
2746
- if (!quiet) {
2747
- const swap = report.swapUsage
2748
- ? `${(report.swapUsage.usedBytes / (1024 ** 3)).toFixed(1)} GiB used`
2749
- : 'swap unavailable';
2750
- println(`Pressure guard: ${report.pressureTriggered ? 'triggered' : 'standby'} · ${swap}`);
2751
- for (const entry of report.warnings ?? []) {
2752
- const grace = entry.graceMinutes ? `; will terminate after ${entry.graceMinutes}m if still matched` : '';
2753
- println(`Warning pid ${entry.pid} (${entry.reason})${grace}`);
2852
+ const dryRun = restArgsCache.includes('--dry-run');
2853
+ const pressureOnly = restArgsCache.includes('--pressure-only');
2854
+ const diskOnly = restArgsCache.includes('--disk-only');
2855
+
2856
+ // Process pressure release: kill stale dev-agent processes.
2857
+ if (!diskOnly) {
2858
+ const report = runPressureRelease({ env: process.env, force });
2859
+ if (!quiet) {
2860
+ const swap = report.swapUsage
2861
+ ? `${(report.swapUsage.usedBytes / (1024 ** 3)).toFixed(1)} GiB used`
2862
+ : 'swap unavailable';
2863
+ println(`Pressure guard: ${report.pressureTriggered ? 'triggered' : 'standby'} · ${swap}`);
2864
+ for (const entry of report.warnings ?? []) {
2865
+ const grace = entry.graceMinutes ? `; will terminate after ${entry.graceMinutes}m if still matched` : '';
2866
+ println(`Warning pid ${entry.pid} (${entry.reason})${grace}`);
2867
+ }
2868
+ if (report.killed.length) {
2869
+ for (const entry of report.killed) {
2870
+ println(`Stopped pid ${entry.pid} (${entry.reason})`);
2871
+ }
2872
+ } else {
2873
+ println('No stale dev-agent processes needed cleanup.');
2874
+ }
2754
2875
  }
2755
2876
  }
2756
2877
 
2757
- if (!report.killed.length) {
2758
- if (!quiet) println('No stale dev-agent processes needed cleanup.');
2759
- return;
2760
- }
2761
-
2762
- if (!quiet) {
2763
- for (const entry of report.killed) {
2764
- println(`Stopped pid ${entry.pid} (${entry.reason})`);
2878
+ // Disk maintenance: rotate oversized logs, prune stale cache entries.
2879
+ if (!pressureOnly) {
2880
+ const { runFullCleanup, formatBytes } = await import('../lib/maintenance/cleanup.mjs');
2881
+ const summary = runFullCleanup({ homeDir: HOME, env: process.env, dryRun });
2882
+ if (!quiet) {
2883
+ const verb = dryRun ? 'Would free' : 'Freed';
2884
+ println(`Disk maintenance${dryRun ? ' (dry-run)' : ''}: ${verb} ${formatBytes(summary.freedBytes)} in ${summary.durationMs}ms`);
2885
+ const log = summary.embedLog;
2886
+ if (log?.truncated?.length) {
2887
+ for (const t of log.truncated) println(` truncated ${path.relative(HOME, t.path)} (was ${formatBytes(t.sizeWas)})`);
2888
+ }
2889
+ if (log?.removed?.length) {
2890
+ for (const r of log.removed) println(` removed ${path.relative(HOME, r.path)} (${formatBytes(r.size)})`);
2891
+ }
2892
+ if (summary.jsonlLogs?.truncated?.length) {
2893
+ for (const t of summary.jsonlLogs.truncated) println(` truncated ${path.relative(HOME, t.path)} (was ${formatBytes(t.sizeWas)} → ${formatBytes(t.sizeNow)})`);
2894
+ }
2895
+ if (summary.cacheDir?.removed?.length) {
2896
+ println(` pruned ${summary.cacheDir.removed.length} stale cache file(s)`);
2897
+ }
2898
+ for (const err of summary.errors ?? []) {
2899
+ println(` ! ${err.step}: ${err.error}`);
2900
+ }
2765
2901
  }
2766
2902
  }
2767
2903
  }
@@ -2970,6 +3106,12 @@ async function cmdMemory(args) {
2970
3106
  async function cmdLintComments(args) {
2971
3107
  const fix = args.includes('--fix');
2972
3108
  const staged = args.includes('--staged');
3109
+ // CLI invocation enforces block severity for artifact-prose lint (see
3110
+ // lib/comment-lint.mjs#checkArtifactBanned). Operators may opt back into
3111
+ // warn-only by exporting CONSTRUCT_ARTIFACT_LINT_MODE=warn explicitly.
3112
+ if (!process.env.CONSTRUCT_ARTIFACT_LINT_MODE) {
3113
+ process.env.CONSTRUCT_ARTIFACT_LINT_MODE = 'block';
3114
+ }
2973
3115
  const { lintRepo: lint, lintFile, formatResults: fmt } = await import('../lib/comment-lint.mjs');
2974
3116
 
2975
3117
  let results;
@@ -3053,6 +3195,18 @@ async function cmdLintAgents() {
3053
3195
  process.exit(1);
3054
3196
  }
3055
3197
 
3198
+ async function cmdLintContracts() {
3199
+ const { validateContractsFile } = await import('../lib/contracts/validate.mjs');
3200
+ const result = validateContractsFile({ repoRoot: ROOT_DIR });
3201
+ if (result.ok) {
3202
+ console.log('agents/contracts.json: 0 errors');
3203
+ return;
3204
+ }
3205
+ console.error(`agents/contracts.json: ${result.errors.length} error(s)`);
3206
+ for (const err of result.errors) console.error(` ${err}`);
3207
+ process.exit(1);
3208
+ }
3209
+
3056
3210
  async function cmdBackup(args) {
3057
3211
  const sub = args[0];
3058
3212
 
@@ -3426,6 +3580,7 @@ const handlers = new Map([
3426
3580
  ['evals', cmdEvals],
3427
3581
  ['cleanup', cmdCleanup],
3428
3582
  ['version', cmdVersion],
3583
+ ['migrate', cmdMigrate],
3429
3584
  ['docs:update', cmdDocsUpdate],
3430
3585
  ['docs:check', cmdDocsCheck],
3431
3586
  ['docs:site', cmdDocsSite],
@@ -3440,6 +3595,7 @@ const handlers = new Map([
3440
3595
  ['provider', cmdProvider],
3441
3596
  ['lint:research', cmdLintResearch],
3442
3597
  ['lint:agents', cmdLintAgents],
3598
+ ['lint:contracts', cmdLintContracts],
3443
3599
  ['audit', async (args) => {
3444
3600
  const sub = args[0];
3445
3601
  if (sub === 'skills') return runAuditSkillsCli(args.slice(1));
@@ -4331,6 +4487,27 @@ if (!handler) {
4331
4487
  process.exit(1);
4332
4488
  }
4333
4489
 
4490
+ // Self-maintenance on version change. Skipped for high-frequency hook calls
4491
+ // (the harness invokes `construct hook <name>` many times per session and
4492
+ // must stay fast). When the installed version differs from the stamp at
4493
+ // ~/.construct/.cleanup-stamp, we run a single bounded cleanup pass and
4494
+ // update the stamp. Subsequent invocations are a single file read.
4495
+ if (command !== 'hook') {
4496
+ try {
4497
+ const { maybeRunCleanupOnUpgrade, formatBytes } = await import('../lib/maintenance/cleanup.mjs');
4498
+ const pkg = JSON.parse(fs.readFileSync(path.join(ROOT_DIR, 'package.json'), 'utf8'));
4499
+ const result = maybeRunCleanupOnUpgrade({ homeDir: HOME, env: process.env, currentVersion: pkg.version });
4500
+ if (result.ran && result.summary?.freedBytes > 1024 * 1024) {
4501
+ const previous = result.previousVersion ? ` from v${result.previousVersion}` : '';
4502
+ process.stderr.write(`[construct] Maintenance after upgrade${previous} to v${pkg.version}: freed ${formatBytes(result.summary.freedBytes)}.\n`);
4503
+ }
4504
+ } catch (err) {
4505
+ // Maintenance must never block the actual command. Surface the failure
4506
+ // but continue. The next invocation will retry.
4507
+ process.stderr.write(`[construct] startup maintenance skipped: ${err.message}\n`);
4508
+ }
4509
+ }
4510
+
4334
4511
  // First-invocation resource probe — skipped for hooks, setup, uninstall,
4335
4512
  // doctor, and once BOOTSTRAP_CHECKED=1 is cached in ~/.construct/config.env.
4336
4513
  // On TTY: if anything's missing, prints the status table and offers to run
@@ -4,7 +4,7 @@ commands/build/feature.md (Build a feature) implement it end to end, tested and
4
4
  Build a feature: implement it end to end, tested and ready to ship
5
5
  -->
6
6
  ---
7
- description: Build a feature: implement it end to end, tested and ready to ship
7
+ description: "Build a feature: implement it end to end, tested and ready to ship"
8
8
  ---
9
9
 
10
10
  You are Construct. Build the following: $ARGUMENTS
@@ -4,7 +4,7 @@ commands/build/fix.md (Fix something broken) reproduce, find the root cause, app
4
4
  Fix something broken: reproduce, find the root cause, apply the smallest safe change
5
5
  -->
6
6
  ---
7
- description: Fix something broken: reproduce, find the root cause, apply the smallest safe change
7
+ description: "Fix something broken: reproduce, find the root cause, apply the smallest safe change"
8
8
  ---
9
9
 
10
10
  You are Construct.
@@ -4,7 +4,7 @@ commands/design/access.md (Accessibility audit) WCAG 2.1 AA, keyboard, screen re
4
4
  Accessibility audit: WCAG 2.1 AA, keyboard, screen readers, contrast, motion
5
5
  -->
6
6
  ---
7
- description: Accessibility audit: WCAG 2.1 AA, keyboard, screen readers, contrast, motion
7
+ description: "Accessibility audit: WCAG 2.1 AA, keyboard, screen readers, contrast, motion"
8
8
  ---
9
9
 
10
10
  You are Construct. Audit: $ARGUMENTS
@@ -4,7 +4,7 @@ commands/design/flow.md (Map the user flow) entry to success, friction points, j
4
4
  Map the user flow: entry to success, friction points, jobs-to-be-done
5
5
  -->
6
6
  ---
7
- description: Map the user flow: entry to success, friction points, jobs-to-be-done
7
+ description: "Map the user flow: entry to success, friction points, jobs-to-be-done"
8
8
  ---
9
9
 
10
10
  You are Construct. Map the user flow for: $ARGUMENTS
@@ -4,7 +4,7 @@ commands/design/ui.md (Design or review UI) visual hierarchy, states, interactio
4
4
  Design or review UI: visual hierarchy, states, interaction model, accessibility baseline
5
5
  -->
6
6
  ---
7
- description: Design or review UI: visual hierarchy, states, interaction model, accessibility baseline
7
+ description: "Design or review UI: visual hierarchy, states, interaction model, accessibility baseline"
8
8
  ---
9
9
 
10
10
  You are Construct. Design or review the UI for: $ARGUMENTS
@@ -4,7 +4,7 @@ commands/measure/experiment.md (Design an experiment) hypothesis, test, evidence
4
4
  Design an experiment: hypothesis, test, evidence threshold, go/no-go decision
5
5
  -->
6
6
  ---
7
- description: Design an experiment: hypothesis, test, evidence threshold, go/no-go decision
7
+ description: "Design an experiment: hypothesis, test, evidence threshold, go/no-go decision"
8
8
  ---
9
9
 
10
10
  You are Construct. Design an experiment for: $ARGUMENTS
@@ -4,7 +4,7 @@ commands/measure/metrics.md (Define metrics) what to measure, baselines, success
4
4
  Define metrics: what to measure, baselines, success thresholds, instrumentation needed
5
5
  -->
6
6
  ---
7
- description: Define metrics: what to measure, baselines, success thresholds, instrumentation needed
7
+ description: "Define metrics: what to measure, baselines, success thresholds, instrumentation needed"
8
8
  ---
9
9
 
10
10
  You are Construct. Define metrics for: $ARGUMENTS
@@ -4,7 +4,7 @@ commands/measure/results.md (Analyze results) interpret data, separate signal fr
4
4
  Analyze results: interpret data, separate signal from noise, produce a recommendation
5
5
  -->
6
6
  ---
7
- description: Analyze results: interpret data, separate signal from noise, produce a recommendation
7
+ description: "Analyze results: interpret data, separate signal from noise, produce a recommendation"
8
8
  ---
9
9
 
10
10
  You are Construct. Analyze: $ARGUMENTS
@@ -4,7 +4,7 @@ commands/plan/api.md (Design an API) contracts, endpoints, error model, data sch
4
4
  Design an API: contracts, endpoints, error model, data schema
5
5
  -->
6
6
  ---
7
- description: Design an API: contracts, endpoints, error model, data schema
7
+ description: "Design an API: contracts, endpoints, error model, data schema"
8
8
  ---
9
9
 
10
10
  You are Construct. Design the API for: $ARGUMENTS
@@ -4,7 +4,7 @@ commands/plan/challenge.md (Challenge a plan) stress-test assumptions, surface f
4
4
  Challenge a plan: stress-test assumptions, surface failure modes and risks
5
5
  -->
6
6
  ---
7
- description: Challenge a plan: stress-test assumptions, surface failure modes and risks
7
+ description: "Challenge a plan: stress-test assumptions, surface failure modes and risks"
8
8
  ---
9
9
 
10
10
  You are Construct. Challenge the following: $ARGUMENTS
@@ -4,7 +4,7 @@ commands/plan/decide.md (Record a decision) context, options considered, consequ
4
4
  Record a decision: context, options considered, consequences, what it locks in
5
5
  -->
6
6
  ---
7
- description: Record a decision: context, options considered, consequences, what it locks in
7
+ description: "Record a decision: context, options considered, consequences, what it locks in"
8
8
  ---
9
9
 
10
10
  You are Construct. Document the decision about: $ARGUMENTS
@@ -5,7 +5,7 @@ Produces a structured spec using the canonical Construct plan format, saves it
5
5
  to .cx/plans/ as a durable planning artifact linked to the active tracker slice.
6
6
  -->
7
7
  ---
8
- description: Plan a feature: produce a structured spec and link it to the active tracker-backed plan
8
+ description: "Plan a feature: produce a structured spec and link it to the active tracker-backed plan"
9
9
  ---
10
10
 
11
11
  You are Construct. Plan the following: $ARGUMENTS
@@ -4,7 +4,7 @@ commands/plan/requirements.md (Define requirements) what needs to be true for th
4
4
  Define requirements: what needs to be true for this to be done
5
5
  -->
6
6
  ---
7
- description: Define requirements: what needs to be true for this to be done
7
+ description: "Define requirements: what needs to be true for this to be done"
8
8
  ---
9
9
 
10
10
  You are Construct. Define requirements for: $ARGUMENTS
@@ -4,7 +4,7 @@ commands/remember/context.md (Save project context) update .cx/context.md so the
4
4
  Save project context: update .cx/context.md so the next session picks up where this one left off
5
5
  -->
6
6
  ---
7
- description: Save project context: update .cx/context.md so the next session picks up where this one left off
7
+ description: "Save project context: update .cx/context.md so the next session picks up where this one left off"
8
8
  ---
9
9
 
10
10
  You are Construct. Save context for: $ARGUMENTS
@@ -4,7 +4,7 @@ commands/remember/handoff.md (Write a handoff) transfer context so the next sess
4
4
  Write a handoff: transfer context so the next session or person can continue without loss
5
5
  -->
6
6
  ---
7
- description: Write a handoff: transfer context so the next session or person can continue without loss
7
+ description: "Write a handoff: transfer context so the next session or person can continue without loss"
8
8
  ---
9
9
 
10
10
  You are Construct. Write a handoff for: $ARGUMENTS
@@ -4,7 +4,7 @@ commands/remember/runbook.md (Write a runbook) step-by-step procedure for a recu
4
4
  Write a runbook: step-by-step procedure for a recurring task or incident
5
5
  -->
6
6
  ---
7
- description: Write a runbook: step-by-step procedure for a recurring task or incident
7
+ description: "Write a runbook: step-by-step procedure for a recurring task or incident"
8
8
  ---
9
9
 
10
10
  You are Construct. Write a runbook for: $ARGUMENTS
@@ -4,7 +4,7 @@ commands/review/code.md (Code review) correctness, regressions, security, test c
4
4
  Code review: correctness, regressions, security, test coverage
5
5
  -->
6
6
  ---
7
- description: Code review: correctness, regressions, security, test coverage
7
+ description: "Code review: correctness, regressions, security, test coverage"
8
8
  ---
9
9
 
10
10
  You are Construct. Review: $ARGUMENTS
@@ -4,7 +4,7 @@ commands/review/quality.md (Quality audit) complexity, naming, duplication, dead
4
4
  Quality audit: complexity, naming, duplication, dead code, maintainability
5
5
  -->
6
6
  ---
7
- description: Quality audit: complexity, naming, duplication, dead code, maintainability
7
+ description: "Quality audit: complexity, naming, duplication, dead code, maintainability"
8
8
  ---
9
9
 
10
10
  You are Construct. Audit quality for: $ARGUMENTS
@@ -4,7 +4,7 @@ commands/review/security.md (Security scan) secrets, auth, injection, data expos
4
4
  Security scan: secrets, auth, injection, data exposure, dependency risk
5
5
  -->
6
6
  ---
7
- description: Security scan: secrets, auth, injection, data exposure, dependency risk
7
+ description: "Security scan: secrets, auth, injection, data exposure, dependency risk"
8
8
  ---
9
9
 
10
10
  You are Construct. Scan: $ARGUMENTS
@@ -4,7 +4,7 @@ commands/ship/ready.md (Pre-release check) is this ready to ship?
4
4
  Pre-release check: is this ready to ship?
5
5
  -->
6
6
  ---
7
- description: Pre-release check: is this ready to ship?
7
+ description: "Pre-release check: is this ready to ship?"
8
8
  ---
9
9
 
10
10
  You are Construct. Check if the following is ready: $ARGUMENTS
@@ -4,7 +4,7 @@ commands/ship/release.md (Release) plan rollout, changelog, rollback, and post-r
4
4
  Release: plan rollout, changelog, rollback, and post-release verification
5
5
  -->
6
6
  ---
7
- description: Release: plan rollout, changelog, rollback, and post-release verification
7
+ description: "Release: plan rollout, changelog, rollback, and post-release verification"
8
8
  ---
9
9
 
10
10
  You are Construct. Release the following: $ARGUMENTS
@@ -4,7 +4,7 @@ commands/ship/status.md (Status) current project state, workflow, uncommitted ch
4
4
  Status: current project state, workflow, uncommitted changes, recent activity
5
5
  -->
6
6
  ---
7
- description: Status: current project state, workflow, uncommitted changes, recent activity
7
+ description: "Status: current project state, workflow, uncommitted changes, recent activity"
8
8
  ---
9
9
 
10
10
  You are Construct. Report status for: $ARGUMENTS
@@ -4,7 +4,7 @@ commands/understand/docs.md (Look up documentation) current behavior from primar
4
4
  Look up documentation: current behavior from primary sources, not training memory
5
5
  -->
6
6
  ---
7
- description: Look up documentation: current behavior from primary sources, not training memory
7
+ description: "Look up documentation: current behavior from primary sources, not training memory"
8
8
  ---
9
9
 
10
10
  You are Construct. Look up: $ARGUMENTS
@@ -4,7 +4,7 @@ commands/understand/this.md (Explore and understand something) trace execution p
4
4
  Explore and understand something: trace execution paths, map structure, gather evidence
5
5
  -->
6
6
  ---
7
- description: Explore and understand something: trace execution paths, map structure, gather evidence
7
+ description: "Explore and understand something: trace execution paths, map structure, gather evidence"
8
8
  ---
9
9
 
10
10
  You are Construct. Explore and explain: $ARGUMENTS
@@ -4,7 +4,7 @@ commands/understand/why.md (Investigate a failure) why is this broken, what is t
4
4
  Investigate a failure: why is this broken, what is the root cause
5
5
  -->
6
6
  ---
7
- description: Investigate a failure: why is this broken, what is the root cause
7
+ description: "Investigate a failure: why is this broken, what is the root cause"
8
8
  ---
9
9
 
10
10
  You are Construct. Investigate: $ARGUMENTS
@@ -4,7 +4,7 @@ commands/work/clean.md (Remove AI-generated code smells) verbosity, hedging, dea
4
4
  Remove AI-generated code smells: verbosity, hedging, dead comments, generic names
5
5
  -->
6
6
  ---
7
- description: Remove AI-generated code smells: verbosity, hedging, dead comments, generic names
7
+ description: "Remove AI-generated code smells: verbosity, hedging, dead comments, generic names"
8
8
  ---
9
9
 
10
10
  You are cx-reviewer identifying AI slop in: $ARGUMENTS
@@ -4,7 +4,7 @@ commands/work/drive.md (Full autonomous execution) explore, plan, implement, ver
4
4
  Full autonomous execution: explore, plan, implement, verify, loop until done
5
5
  -->
6
6
  ---
7
- description: Full autonomous execution: explore, plan, implement, verify, loop until done
7
+ description: "Full autonomous execution: explore, plan, implement, verify, loop until done"
8
8
  ---
9
9
 
10
10
  You are Construct in drive mode. Execute $ARGUMENTS fully and autonomously without stopping for confirmation.
@@ -4,7 +4,7 @@ commands/work/optimize-prompts.md: Prompt optimization command.
4
4
  Runs the closed-loop prompt optimization workflow using telemetry trace data.
5
5
  -->
6
6
  ---
7
- description: Closed-loop prompt optimization: read telemetry traces, diagnose failures, push improved version to staging
7
+ description: "Closed-loop prompt optimization: read telemetry traces, diagnose failures, push improved version to staging"
8
8
  ---
9
9
 
10
10
  You are cx-trace-reviewer running a prompt optimization cycle for: $ARGUMENTS
@@ -4,7 +4,7 @@ commands/work/parallel-review.md (Adversarial parallel review) 5 reviewers must
4
4
  Adversarial parallel review: 5 reviewers must agree before output ships
5
5
  -->
6
6
  ---
7
- description: Adversarial parallel review: 5 reviewers must agree before output ships
7
+ description: "Adversarial parallel review: 5 reviewers must agree before output ships"
8
8
  ---
9
9
 
10
10
  You are Construct running a parallel adversarial review of: $ARGUMENTS