@geraldmaron/construct 1.0.5 → 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 (268) hide show
  1. package/README.md +24 -18
  2. package/agents/contracts.json +617 -2
  3. package/agents/contracts.schema.json +83 -0
  4. package/agents/prompts/cx-accessibility.md +5 -3
  5. package/agents/prompts/cx-ai-engineer.md +9 -7
  6. package/agents/prompts/cx-architect.md +8 -6
  7. package/agents/prompts/cx-business-strategist.md +8 -6
  8. package/agents/prompts/cx-data-analyst.md +9 -7
  9. package/agents/prompts/cx-data-engineer.md +5 -3
  10. package/agents/prompts/cx-debugger.md +8 -6
  11. package/agents/prompts/cx-designer.md +7 -5
  12. package/agents/prompts/cx-devil-advocate.md +6 -4
  13. package/agents/prompts/cx-docs-keeper.md +7 -5
  14. package/agents/prompts/cx-engineer.md +7 -5
  15. package/agents/prompts/cx-evaluator.md +4 -2
  16. package/agents/prompts/cx-explorer.md +10 -8
  17. package/agents/prompts/cx-legal-compliance.md +4 -2
  18. package/agents/prompts/cx-operations.md +7 -5
  19. package/agents/prompts/cx-orchestrator.md +12 -10
  20. package/agents/prompts/cx-platform-engineer.md +7 -5
  21. package/agents/prompts/cx-product-manager.md +6 -4
  22. package/agents/prompts/cx-qa.md +10 -8
  23. package/agents/prompts/cx-rd-lead.md +11 -9
  24. package/agents/prompts/cx-release-manager.md +7 -5
  25. package/agents/prompts/cx-researcher.md +24 -22
  26. package/agents/prompts/cx-reviewer.md +9 -7
  27. package/agents/prompts/cx-security.md +12 -10
  28. package/agents/prompts/cx-sre.md +9 -7
  29. package/agents/prompts/cx-test-automation.md +5 -3
  30. package/agents/prompts/cx-trace-reviewer.md +10 -8
  31. package/agents/prompts/cx-ux-researcher.md +5 -3
  32. package/agents/registry.json +8 -3
  33. package/bin/construct +444 -53
  34. package/commands/build/feature.md +4 -4
  35. package/commands/build/fix.md +8 -8
  36. package/commands/design/access.md +3 -3
  37. package/commands/design/flow.md +3 -3
  38. package/commands/design/ui.md +4 -4
  39. package/commands/measure/experiment.md +5 -5
  40. package/commands/measure/metrics.md +3 -3
  41. package/commands/measure/results.md +4 -4
  42. package/commands/plan/api.md +3 -3
  43. package/commands/plan/challenge.md +3 -3
  44. package/commands/plan/decide.md +3 -3
  45. package/commands/plan/feature.md +7 -7
  46. package/commands/plan/requirements.md +3 -3
  47. package/commands/remember/context.md +5 -5
  48. package/commands/remember/handoff.md +3 -3
  49. package/commands/remember/runbook.md +3 -3
  50. package/commands/review/code.md +8 -8
  51. package/commands/review/quality.md +4 -4
  52. package/commands/review/security.md +3 -3
  53. package/commands/ship/ready.md +3 -3
  54. package/commands/ship/release.md +3 -3
  55. package/commands/ship/status.md +4 -4
  56. package/commands/understand/docs.md +3 -3
  57. package/commands/understand/this.md +4 -4
  58. package/commands/understand/why.md +8 -8
  59. package/commands/work/clean.md +14 -14
  60. package/commands/work/drive.md +10 -10
  61. package/commands/work/optimize-prompts.md +9 -9
  62. package/commands/work/parallel-review.md +8 -8
  63. package/db/schema/006_graph.sql +24 -0
  64. package/examples/provider-plugin/README.md +7 -7
  65. package/examples/seed-observations/README.md +6 -6
  66. package/examples/seed-observations/anti-patterns.md +14 -14
  67. package/examples/seed-observations/decisions.md +4 -4
  68. package/examples/seed-observations/patterns.md +14 -14
  69. package/lib/auto-docs.mjs +13 -8
  70. package/lib/boundary.mjs +126 -0
  71. package/lib/cache-strategy-google.js +26 -31
  72. package/lib/cli-commands.mjs +2 -2
  73. package/lib/comment-lint.mjs +134 -0
  74. package/lib/contracts/validate.mjs +323 -0
  75. package/lib/daemons/contract.mjs +210 -0
  76. package/lib/docs-verify.mjs +59 -6
  77. package/lib/doctor/cli.mjs +16 -1
  78. package/lib/doctor/index.mjs +3 -1
  79. package/lib/doctor/watchers/consistency.mjs +310 -0
  80. package/lib/doctor/watchers/mcp-protocol.mjs +232 -0
  81. package/lib/document-extract.mjs +211 -1
  82. package/lib/embed/cli.mjs +124 -3
  83. package/lib/embed/daemon.mjs +43 -4
  84. package/lib/embed/docs-lifecycle.mjs +1 -1
  85. package/lib/embed/inbox.mjs +2 -0
  86. package/lib/embed/scheduler.mjs +33 -5
  87. package/lib/evaluator-optimizer.mjs +2 -3
  88. package/lib/flavors/loader.mjs +1 -1
  89. package/lib/hooks/comment-lint.mjs +16 -0
  90. package/lib/hooks/mcp-audit.mjs +2 -1
  91. package/lib/hooks/proactive-activation.mjs +0 -14
  92. package/lib/hooks/rule-verifier.mjs +217 -0
  93. package/lib/hooks/session-optimize.mjs +2 -1
  94. package/lib/init-unified.mjs +55 -65
  95. package/lib/intake/classify.mjs +108 -24
  96. package/lib/intake/daemon.mjs +121 -0
  97. package/lib/intake/filesystem-queue.mjs +6 -1
  98. package/lib/intake/intake-config.mjs +2 -1
  99. package/lib/intake/prepare.mjs +0 -1
  100. package/lib/intake/session-prelude.mjs +7 -1
  101. package/lib/intake/traceability.mjs +90 -0
  102. package/lib/knowledge/graph.mjs +213 -0
  103. package/lib/knowledge/research-store.mjs +2 -0
  104. package/lib/maintenance/cleanup.mjs +315 -0
  105. package/lib/mcp/memory-bridge.mjs +276 -0
  106. package/lib/mcp/server.mjs +189 -1
  107. package/lib/mcp/tools/profile.mjs +270 -0
  108. package/lib/mcp/tools/workflow.mjs +25 -0
  109. package/lib/mcp-catalog.json +12 -8
  110. package/lib/mcp-platform-config.mjs +16 -8
  111. package/lib/migrations/index.mjs +106 -0
  112. package/lib/migrations/v1-baseline.mjs +33 -0
  113. package/lib/observation-store.mjs +9 -4
  114. package/lib/outcomes/record.mjs +2 -0
  115. package/lib/profiles/rebrand.mjs +46 -0
  116. package/lib/project-init-shared.mjs +12 -0
  117. package/lib/provider-capabilities.js +20 -7
  118. package/lib/providers/auth-manager.mjs +58 -17
  119. package/lib/reflect.mjs +49 -12
  120. package/lib/server/index.mjs +22 -28
  121. package/lib/session-store.mjs +6 -4
  122. package/lib/setup.mjs +14 -3
  123. package/lib/telemetry/client.mjs +5 -1
  124. package/lib/version.mjs +51 -0
  125. package/lib/worker/trace.mjs +5 -1
  126. package/package.json +4 -1
  127. package/personas/construct.md +3 -1
  128. package/platforms/claude/CLAUDE.md +6 -6
  129. package/rules/common/agents.md +2 -2
  130. package/rules/common/beads-hygiene.md +11 -11
  131. package/rules/common/code-review.md +1 -1
  132. package/rules/common/coding-style.md +1 -1
  133. package/rules/common/comments.md +8 -8
  134. package/rules/common/commit-approval.md +4 -4
  135. package/rules/common/cx-agent-routing.md +2 -2
  136. package/rules/common/cx-skill-routing.md +2 -2
  137. package/rules/common/development-workflow.md +3 -2
  138. package/rules/common/doc-ownership.md +2 -2
  139. package/rules/common/efficiency.md +3 -3
  140. package/rules/common/framing.md +1 -1
  141. package/rules/common/git-workflow.md +1 -1
  142. package/rules/common/no-fabrication.md +69 -0
  143. package/rules/common/patterns.md +1 -1
  144. package/rules/common/performance.md +1 -1
  145. package/rules/common/release-gates.md +7 -7
  146. package/rules/common/research.md +4 -4
  147. package/rules/common/review-before-change.md +58 -0
  148. package/rules/common/security.md +1 -1
  149. package/rules/common/skill-composition.md +8 -8
  150. package/rules/common/testing.md +1 -1
  151. package/rules/golang/coding-style.md +2 -2
  152. package/rules/golang/hooks.md +1 -1
  153. package/rules/golang/patterns.md +1 -1
  154. package/rules/golang/security.md +1 -1
  155. package/rules/golang/testing.md +1 -1
  156. package/rules/python/coding-style.md +1 -1
  157. package/rules/python/hooks.md +1 -1
  158. package/rules/python/patterns.md +1 -1
  159. package/rules/python/security.md +1 -1
  160. package/rules/python/testing.md +1 -1
  161. package/rules/swift/coding-style.md +3 -3
  162. package/rules/swift/hooks.md +2 -2
  163. package/rules/swift/patterns.md +2 -2
  164. package/rules/swift/security.md +4 -4
  165. package/rules/swift/testing.md +2 -2
  166. package/rules/typescript/coding-style.md +1 -1
  167. package/rules/typescript/hooks.md +1 -1
  168. package/rules/typescript/patterns.md +1 -1
  169. package/rules/typescript/security.md +1 -1
  170. package/rules/typescript/testing.md +1 -1
  171. package/rules/web/coding-style.md +1 -1
  172. package/rules/web/design-quality.md +1 -1
  173. package/rules/web/hooks.md +1 -1
  174. package/rules/web/patterns.md +1 -1
  175. package/rules/web/performance.md +1 -1
  176. package/rules/web/security.md +1 -1
  177. package/rules/web/testing.md +1 -1
  178. package/scripts/sync-agents.mjs +45 -14
  179. package/skills/ai/agent-dev.md +1 -1
  180. package/skills/ai/llm-security.md +1 -1
  181. package/skills/ai/ml-ops.md +6 -6
  182. package/skills/ai/orchestration-workflow.md +1 -1
  183. package/skills/ai/prompt-and-eval.md +1 -1
  184. package/skills/ai/prompt-optimizer.md +13 -13
  185. package/skills/ai/rag-system.md +1 -1
  186. package/skills/architecture/api-design.md +1 -1
  187. package/skills/architecture/caching.md +1 -1
  188. package/skills/architecture/cloud-native.md +1 -1
  189. package/skills/architecture/message-queue.md +1 -1
  190. package/skills/architecture/security-arch.md +1 -1
  191. package/skills/compliance/ai-disclosure.md +1 -1
  192. package/skills/compliance/data-privacy.md +1 -1
  193. package/skills/compliance/license-audit.md +2 -2
  194. package/skills/compliance/regulatory-review.md +1 -1
  195. package/skills/development/cpp.md +1 -1
  196. package/skills/development/go.md +1 -1
  197. package/skills/development/java.md +1 -1
  198. package/skills/development/kotlin.md +9 -9
  199. package/skills/development/mobile-crossplatform.md +13 -13
  200. package/skills/development/python.md +1 -1
  201. package/skills/development/rust.md +1 -1
  202. package/skills/development/shell.md +1 -1
  203. package/skills/development/swift.md +6 -6
  204. package/skills/development/typescript.md +1 -1
  205. package/skills/devops/ci-cd.md +5 -5
  206. package/skills/devops/containerization.md +9 -9
  207. package/skills/devops/cost-optimization.md +1 -1
  208. package/skills/devops/data-engineering.md +2 -2
  209. package/skills/devops/database.md +1 -1
  210. package/skills/devops/dependency-management.md +3 -3
  211. package/skills/devops/devsecops.md +1 -1
  212. package/skills/devops/git-workflow.md +1 -1
  213. package/skills/devops/incident-response.md +18 -18
  214. package/skills/devops/monorepo.md +5 -5
  215. package/skills/devops/observability.md +1 -1
  216. package/skills/devops/performance.md +1 -1
  217. package/skills/devops/testing.md +1 -1
  218. package/skills/docs/adr-workflow.md +2 -2
  219. package/skills/docs/backlog-proposal-workflow.md +1 -1
  220. package/skills/docs/customer-profile-workflow.md +1 -1
  221. package/skills/docs/document-ingest-workflow.md +1 -1
  222. package/skills/docs/evidence-ingest-workflow.md +1 -1
  223. package/skills/docs/init-docs.md +15 -15
  224. package/skills/docs/init-project.md +1 -1
  225. package/skills/docs/prd-workflow.md +3 -3
  226. package/skills/docs/prfaq-workflow.md +1 -1
  227. package/skills/docs/product-intelligence-review.md +1 -1
  228. package/skills/docs/product-intelligence-workflow.md +1 -1
  229. package/skills/docs/product-signal-workflow.md +9 -9
  230. package/skills/docs/research-workflow.md +10 -10
  231. package/skills/docs/runbook-workflow.md +2 -2
  232. package/skills/docs/strategy-workflow.md +3 -3
  233. package/skills/exploration/repo-map.md +11 -11
  234. package/skills/frameworks/django.md +15 -15
  235. package/skills/frameworks/nextjs.md +16 -16
  236. package/skills/frameworks/react.md +12 -12
  237. package/skills/frameworks/spring-boot.md +12 -12
  238. package/skills/frontend-design/accessibility.md +6 -6
  239. package/skills/frontend-design/component-patterns.md +1 -1
  240. package/skills/frontend-design/engineering.md +1 -1
  241. package/skills/frontend-design/state-management.md +1 -1
  242. package/skills/frontend-design/ui-aesthetics.md +1 -1
  243. package/skills/frontend-design/ux-principles.md +1 -1
  244. package/skills/operating/orchestration-reference.md +27 -27
  245. package/skills/quality-gates/review-work.md +3 -3
  246. package/skills/quality-gates/verify-change.md +1 -1
  247. package/skills/quality-gates/verify-module.md +1 -1
  248. package/skills/quality-gates/verify-quality.md +1 -1
  249. package/skills/quality-gates/verify-security.md +1 -1
  250. package/skills/routing.md +14 -14
  251. package/skills/security/blue-team.md +1 -1
  252. package/skills/security/code-audit.md +1 -1
  253. package/skills/security/pentest.md +1 -1
  254. package/skills/security/red-team.md +1 -1
  255. package/skills/security/threat-intel.md +1 -1
  256. package/skills/security/vuln-research.md +1 -1
  257. package/skills/utility/clean-code.md +2 -2
  258. package/templates/docs/changelog-entry.md +1 -1
  259. package/templates/docs/construct_guide.md +18 -18
  260. package/templates/docs/meta-prd.md +16 -16
  261. package/templates/docs/one-pager.md +1 -1
  262. package/templates/docs/prd-business.md +1 -1
  263. package/templates/docs/prd-platform.md +1 -1
  264. package/templates/docs/prd.md +17 -17
  265. package/templates/docs/research-brief.md +8 -8
  266. package/templates/docs/rfc.md +1 -1
  267. package/templates/docs/skill-artifact.md +1 -1
  268. package/templates/docs/strategy.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));
@@ -3994,18 +4150,85 @@ const handlers = new Map([
3994
4150
  }
3995
4151
  if (sub === 'set') {
3996
4152
  const id = args[1];
3997
- if (!id) { errorln('Usage: construct profile set <id>'); process.exit(1); }
4153
+ if (!id) {
4154
+ errorln('Usage: construct profile set <id> [--yes] [--dry-run]');
4155
+ process.exit(1);
4156
+ }
3998
4157
  const target = loadProfile(id);
3999
4158
  if (!target) {
4000
4159
  errorln(`Unknown profile: ${id}. Available: ${listProfiles().join(', ')}`);
4001
4160
  process.exit(1);
4002
4161
  }
4162
+ const skipConfirm = args.includes('--yes') || args.includes('-y') || args.includes('--non-interactive');
4163
+ const dryRun = args.includes('--dry-run');
4164
+ const isTTY = process.stdin.isTTY && process.stdout.isTTY;
4165
+
4003
4166
  const { findProjectConfigPath, loadProjectConfig, writeProjectConfig, PROJECT_CONFIG_FILENAME } = await import('../lib/config/project-config.mjs');
4004
- const fsMod = await import('node:fs');
4005
4167
  const pathMod = await import('node:path');
4006
4168
  const found = findProjectConfigPath(process.cwd());
4007
4169
  const cfgPath = found || pathMod.join(process.cwd(), PROJECT_CONFIG_FILENAME);
4008
- const cfg = found ? (loadProjectConfig(process.cwd()) || { version: 1 }) : { version: 1 };
4170
+ // loadProjectConfig returns a wrapper { path, raw, config, source, errors };
4171
+ // operate on the raw user-authored object so the write call sees the
4172
+ // version field that validateProjectConfig requires.
4173
+ const loaded = found ? loadProjectConfig(process.cwd()) : null;
4174
+ const cfg = loaded?.raw ? structuredClone(loaded.raw) : { version: 1 };
4175
+ if (!cfg.version) cfg.version = 1;
4176
+
4177
+ const currentId = cfg.profile || 'rnd';
4178
+ if (currentId === id) {
4179
+ println(`Profile already set to ${id} (${target.displayName}). Nothing to do.`);
4180
+ return;
4181
+ }
4182
+ const current = loadProfile(currentId) || { id: currentId, displayName: currentId, roles: [], intake: { types: [], stages: [] }, departments: [] };
4183
+
4184
+ // Render a structural diff so the operator sees what loop they are switching into.
4185
+ const setDiff = (label, before, after) => {
4186
+ const b = new Set(before || []);
4187
+ const a = new Set(after || []);
4188
+ const added = [...a].filter((x) => !b.has(x));
4189
+ const removed = [...b].filter((x) => !a.has(x));
4190
+ const unchanged = [...a].filter((x) => b.has(x));
4191
+ println(` ${label}:`);
4192
+ if (added.length) println(` + ${added.join(', ')}`);
4193
+ if (removed.length) println(` - ${removed.join(', ')}`);
4194
+ if (!added.length && !removed.length) println(` (no change, ${unchanged.length} entr${unchanged.length === 1 ? 'y' : 'ies'})`);
4195
+ };
4196
+
4197
+ println('');
4198
+ println(`About to switch active profile:`);
4199
+ println(` from: ${currentId} (${current.displayName})`);
4200
+ println(` to: ${id} (${target.displayName})`);
4201
+ println('');
4202
+ println(`Structural diff:`);
4203
+ setDiff('roles', current.roles, target.roles);
4204
+ setDiff('intake.types', current.intake?.types, target.intake?.types);
4205
+ setDiff('intake.stages', current.intake?.stages, target.intake?.stages);
4206
+ const cDept = (current.departments || []).map((d) => d.id);
4207
+ const tDept = (target.departments || []).map((d) => d.id);
4208
+ setDiff('departments', cDept, tDept);
4209
+ println('');
4210
+ println(`Files that will change:`);
4211
+ println(` ${pathMod.relative(process.cwd(), cfgPath)} (profile field: ${currentId} -> ${id})`);
4212
+ println('');
4213
+ println(`The change only writes the profile field. No specialists are added or removed.`);
4214
+ println(`Sync runs against the new profile on next \`construct sync\`.`);
4215
+ println('');
4216
+
4217
+ if (dryRun) {
4218
+ println(`[dry-run] No files were written.`);
4219
+ return;
4220
+ }
4221
+ if (!skipConfirm && isTTY) {
4222
+ const readline = await import('node:readline');
4223
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
4224
+ const answer = await new Promise((res) => rl.question(`Switch profile to ${id}? (Y/n) `, (a) => res(a.trim().toLowerCase())));
4225
+ rl.close();
4226
+ if (answer && answer !== 'y' && answer !== 'yes') {
4227
+ println('Aborted. No config was changed.');
4228
+ return;
4229
+ }
4230
+ }
4231
+
4009
4232
  cfg.profile = id;
4010
4233
  writeProjectConfig(cfgPath, cfg, { validate: true });
4011
4234
  process.stdout.write(`✓ profile set to ${id} (${target.displayName})\n`);
@@ -4014,36 +4237,111 @@ const handlers = new Map([
4014
4237
  if (sub === 'create') {
4015
4238
  const id = args[1];
4016
4239
  if (!id) {
4017
- errorln('Usage: construct profile create <id> [--display="..."] [--role=<id>...] [--department=<id>:<displayName>...]');
4240
+ errorln('Usage: construct profile create <id> [--display="..."] [--role=<id>...] [--department=<id>:<displayName>...] [--yes]');
4241
+ errorln(' Run with no flags for interactive mode; pass --yes to skip the confirmation prompt.');
4242
+ process.exit(1);
4243
+ }
4244
+ if (!/^[a-z][a-z0-9-]{1,30}$/.test(id)) {
4245
+ errorln(`Invalid id "${id}". Must match ^[a-z][a-z0-9-]{1,30}$.`);
4018
4246
  process.exit(1);
4019
4247
  }
4248
+
4020
4249
  const displayArg = args.find((a) => a.startsWith('--display='));
4021
- const displayName = displayArg ? displayArg.split('=')[1] : id;
4022
- const seedRoles = args
4250
+ const skipConfirm = args.includes('--yes') || args.includes('-y') || args.includes('--non-interactive');
4251
+ const dryRun = args.includes('--dry-run');
4252
+ const isTTY = process.stdin.isTTY && process.stdout.isTTY;
4253
+
4254
+ let displayName = displayArg ? displayArg.split('=')[1] : null;
4255
+ let seedRoles = args
4023
4256
  .filter((a) => a.startsWith('--role='))
4024
4257
  .map((a) => a.slice('--role='.length))
4025
4258
  .filter(Boolean);
4026
- const seedDepartments = args
4259
+ let seedDepartments = args
4027
4260
  .filter((a) => a.startsWith('--department='))
4028
4261
  .map((a) => a.slice('--department='.length))
4029
4262
  .map((spec) => {
4030
4263
  const [deptId, deptDisplay] = spec.split(':');
4031
4264
  return { id: deptId, displayName: deptDisplay || deptId };
4032
4265
  });
4266
+
4267
+ // Interactive gather. Only prompt when stdin is a real terminal and the
4268
+ // operator did not pass enough flags to scaffold without input. A
4269
+ // non-TTY invocation with no flags falls through to an empty scaffold
4270
+ // so scripts and CI can still create drafts non-interactively.
4271
+ const needsInteractive = isTTY && !skipConfirm && (!displayName || (seedRoles.length === 0 && seedDepartments.length === 0));
4272
+ if (needsInteractive) {
4273
+ const readline = await import('node:readline');
4274
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
4275
+ const ask = (q, def = '') => new Promise((res) => rl.question(`${q}${def ? ` [${def}]` : ''} `, (a) => res(a.trim() || def)));
4276
+
4277
+ println('');
4278
+ println(`Scaffolding a new profile draft. Three questions, then a preview.`);
4279
+ println(`Background: docs/concepts/persona-research.md and docs/concepts/profile-lifecycle.md`);
4280
+ println('');
4281
+ if (!displayName) displayName = await ask('Display name?', id);
4282
+ if (seedDepartments.length === 0) {
4283
+ const dInput = await ask('Departments (id:Name, id:Name, ...)?', '');
4284
+ if (dInput) seedDepartments = dInput.split(',').map((s) => s.trim()).filter(Boolean).map((spec) => {
4285
+ const [dId, dName] = spec.split(':').map((x) => x.trim());
4286
+ return { id: dId, displayName: dName || dId };
4287
+ });
4288
+ }
4289
+ if (seedRoles.length === 0) {
4290
+ const rInput = await ask('Roles (role-id, role-id, ...)?', '');
4291
+ if (rInput) seedRoles = rInput.split(',').map((s) => s.trim()).filter(Boolean);
4292
+ }
4293
+ rl.close();
4294
+ }
4295
+ if (!displayName) displayName = id;
4296
+
4297
+ const pathMod = await import('node:path');
4298
+ const draftRoot = pathMod.join(process.cwd(), '.cx', 'profiles', `draft-${id}`);
4299
+
4300
+ println('');
4301
+ println(`About to scaffold a draft profile:`);
4302
+ println(` id: ${id}`);
4303
+ println(` displayName: ${displayName}`);
4304
+ println(` departments: ${seedDepartments.length}${seedDepartments.length ? ` (${seedDepartments.map((d) => d.id).join(', ')})` : ''}`);
4305
+ println(` roles: ${seedRoles.length}${seedRoles.length ? ` (${seedRoles.join(', ')})` : ''}`);
4306
+ println('');
4307
+ println(`Files that will be written under ${pathMod.relative(process.cwd(), draftRoot)}/:`);
4308
+ println(` profile.json structural manifest`);
4309
+ println(` requirements.md research brief naming each cx-* specialist`);
4310
+ for (const r of seedRoles) println(` personas/${r}.md`.padEnd(40) + ` persona artifact (filled during discover)`);
4311
+ for (const d of seedDepartments) println(` departments/${d.id}.md`.padEnd(40) + ` department charter (filled during frame)`);
4312
+ println('');
4313
+ println(`Nothing else is touched. No config is changed, no active profile is switched.`);
4314
+ println(`To activate this profile after promotion: construct profile set ${id}`);
4315
+ println('');
4316
+
4317
+ if (dryRun) {
4318
+ println(`[dry-run] No files were written.`);
4319
+ return;
4320
+ }
4321
+ if (!skipConfirm && isTTY) {
4322
+ const readline = await import('node:readline');
4323
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
4324
+ const answer = await new Promise((res) => rl.question('Create draft? (Y/n) ', (a) => res(a.trim().toLowerCase())));
4325
+ rl.close();
4326
+ if (answer && answer !== 'y' && answer !== 'yes') {
4327
+ println('Aborted. Nothing was written.');
4328
+ return;
4329
+ }
4330
+ }
4331
+
4033
4332
  const { createDraftProfile } = await import('../lib/profiles/lifecycle.mjs');
4034
4333
  try {
4035
4334
  const result = createDraftProfile({ cwd: process.cwd(), id, displayName, seedRoles, seedDepartments });
4036
- const pathMod = await import('node:path');
4037
- process.stdout.write(`✓ draft created for ${id}\n`);
4038
- process.stdout.write(` brief: ${pathMod.relative(process.cwd(), result.briefPath)}\n`);
4039
- process.stdout.write(` draft: ${pathMod.relative(process.cwd(), result.draftPath)}\n`);
4040
- if (result.personaPaths.length) process.stdout.write(` personas: ${result.personaPaths.length} scaffold(s) under personas/\n`);
4041
- if (result.departmentPaths.length) process.stdout.write(` departments: ${result.departmentPaths.length} charter(s) under departments/\n`);
4042
- process.stdout.write(`\nNext steps (in order, per docs/concepts/profile-lifecycle.md):\n`);
4043
- process.stdout.write(` 1. Discover: dispatch cx-ux-researcher to fill personas/<role>.md from interviews + primary sources.\n`);
4044
- process.stdout.write(` 2. Frame: dispatch cx-product-manager to fill departments/<dept>.md charters and the intake taxonomy in profile.json.\n`);
4045
- process.stdout.write(` 3. Architect: dispatch cx-architect to reconcile role reuse vs new, and to populate departments[].\n`);
4046
- process.stdout.write(` 4. Validate: dispatch cx-evaluator to run persona-eval and classifier-eval against representative signals.\n`);
4335
+ println('');
4336
+ println(`✓ draft created at ${pathMod.relative(process.cwd(), result.dir)}`);
4337
+ println('');
4338
+ println(`Next steps (in order, per docs/concepts/profile-lifecycle.md):`);
4339
+ println(` 1. Discover (cx-ux-researcher): fill personas/<role>.md from interviews + primary sources.`);
4340
+ println(` 2. Frame (cx-product-manager): fill departments/<dept>.md charters and intake taxonomy.`);
4341
+ println(` 3. Architect (cx-architect): reconcile role reuse vs new; populate departments[] in profile.json.`);
4342
+ println(` 4. Validate (cx-evaluator): run persona-eval and classifier-eval against representative signals.`);
4343
+ println('');
4344
+ println(`Inspect the brief: open ${pathMod.relative(process.cwd(), result.briefPath)}`);
4047
4345
  return;
4048
4346
  } catch (err) {
4049
4347
  errorln(err.message);
@@ -4063,12 +4361,84 @@ const handlers = new Map([
4063
4361
  if (sub === 'archive') {
4064
4362
  const id = args[1];
4065
4363
  const reasonArg = args.find((a) => a.startsWith('--reason='));
4066
- const reason = reasonArg ? reasonArg.split('=')[1] : '';
4067
- if (!id) { errorln('Usage: construct profile archive <id> --reason="..."'); process.exit(1); }
4364
+ let reason = reasonArg ? reasonArg.split('=')[1] : '';
4365
+ if (!id) {
4366
+ errorln('Usage: construct profile archive <id> --reason="..." [--yes] [--dry-run]');
4367
+ process.exit(1);
4368
+ }
4369
+ const skipConfirm = args.includes('--yes') || args.includes('-y') || args.includes('--non-interactive');
4370
+ const dryRun = args.includes('--dry-run');
4371
+ const isTTY = process.stdin.isTTY && process.stdout.isTTY;
4372
+
4373
+ const pathMod = await import('node:path');
4374
+ const fsMod = await import('node:fs');
4375
+ const target = loadProfile(id);
4376
+ if (!target) {
4377
+ errorln(`Unknown profile: ${id}. Curated catalog: ${listProfiles().join(', ')}`);
4378
+ process.exit(1);
4379
+ }
4380
+ const fileURL = await import('node:url');
4381
+ const moduleDir = pathMod.dirname(fileURL.fileURLToPath(import.meta.url));
4382
+ const repoRoot = pathMod.resolve(moduleDir, '..');
4383
+ const srcProfile = pathMod.join(repoRoot, 'profiles', `${id}.json`);
4384
+ const tableRel = typeof target?.intake?.classificationTable === 'string' ? target.intake.classificationTable : null;
4385
+ const srcTable = tableRel ? pathMod.join(repoRoot, tableRel) : null;
4386
+ const dstDir = pathMod.join(repoRoot, 'archive', 'profiles', id);
4387
+
4388
+ // Gather reason interactively if missing. The lifecycle module already
4389
+ // enforces a >= 8 char reason, but the operator should see why before
4390
+ // the call fails.
4391
+ if (!reason && isTTY && !skipConfirm) {
4392
+ const readline = await import('node:readline');
4393
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
4394
+ reason = await new Promise((res) => rl.question('Archive reason (>= 8 chars): ', (a) => res(a.trim())));
4395
+ rl.close();
4396
+ }
4397
+ if (!reason || reason.trim().length < 8) {
4398
+ errorln('A substantive --reason (>= 8 chars) is required. Archiving is permanent in repo history; the reason becomes the archive note.');
4399
+ process.exit(1);
4400
+ }
4401
+
4402
+ println('');
4403
+ println(`About to archive curated profile:`);
4404
+ println(` id: ${id}`);
4405
+ println(` displayName: ${target.displayName || id}`);
4406
+ println(` reason: ${reason}`);
4407
+ println('');
4408
+ println(`Files that will move:`);
4409
+ println(` ${pathMod.relative(process.cwd(), srcProfile)}`);
4410
+ println(` -> ${pathMod.relative(process.cwd(), pathMod.join(dstDir, `${id}.json`))}`);
4411
+ if (srcTable && fsMod.existsSync(srcTable)) {
4412
+ println(` ${pathMod.relative(process.cwd(), srcTable)}`);
4413
+ println(` -> ${pathMod.relative(process.cwd(), pathMod.join(dstDir, pathMod.basename(srcTable)))}`);
4414
+ }
4415
+ println(` ${pathMod.relative(process.cwd(), pathMod.join(dstDir, 'archive-note.md'))} (new)`);
4416
+ println('');
4417
+ println(`What stays:`);
4418
+ println(` Observations under .cx/observations/ are not touched.`);
4419
+ println(` Outcomes under .cx/outcomes/ are not touched.`);
4420
+ println(` Any project still configured with profile=${id} will fall back to rnd on next load.`);
4421
+ println('');
4422
+
4423
+ if (dryRun) {
4424
+ println(`[dry-run] No files were moved.`);
4425
+ return;
4426
+ }
4427
+ if (!skipConfirm && isTTY) {
4428
+ const readline = await import('node:readline');
4429
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
4430
+ const answer = await new Promise((res) => rl.question(`Archive ${id}? (y/N) `, (a) => res(a.trim().toLowerCase())));
4431
+ rl.close();
4432
+ if (answer !== 'y' && answer !== 'yes') {
4433
+ println('Aborted. Nothing was archived.');
4434
+ return;
4435
+ }
4436
+ }
4437
+
4068
4438
  const { archiveProfile } = await import('../lib/profiles/lifecycle.mjs');
4069
4439
  try {
4070
4440
  const { archived } = archiveProfile({ id, reason });
4071
- process.stdout.write(`✓ archived ${id} to ${archived}\n`);
4441
+ process.stdout.write(`✓ archived ${id} to ${pathMod.relative(process.cwd(), archived)}\n`);
4072
4442
  return;
4073
4443
  } catch (err) {
4074
4444
  errorln(err.message);
@@ -4117,6 +4487,27 @@ if (!handler) {
4117
4487
  process.exit(1);
4118
4488
  }
4119
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
+
4120
4511
  // First-invocation resource probe — skipped for hooks, setup, uninstall,
4121
4512
  // doctor, and once BOOTSTRAP_CHECKED=1 is cached in ~/.construct/config.env.
4122
4513
  // On TTY: if anything's missing, prints the status table and offers to run