@geraldmaron/construct 1.0.3 โ†’ 1.0.5

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 (157) hide show
  1. package/README.md +51 -42
  2. package/agents/prompts/cx-ai-engineer.md +6 -26
  3. package/agents/prompts/cx-architect.md +1 -0
  4. package/agents/prompts/cx-business-strategist.md +2 -0
  5. package/agents/prompts/cx-data-analyst.md +6 -26
  6. package/agents/prompts/cx-docs-keeper.md +1 -31
  7. package/agents/prompts/cx-explorer.md +1 -0
  8. package/agents/prompts/cx-orchestrator.md +40 -112
  9. package/agents/prompts/cx-platform-engineer.md +2 -22
  10. package/agents/prompts/cx-product-manager.md +2 -1
  11. package/agents/prompts/cx-qa.md +0 -20
  12. package/agents/prompts/cx-rd-lead.md +2 -0
  13. package/agents/prompts/cx-researcher.md +77 -31
  14. package/agents/prompts/cx-security.md +11 -49
  15. package/agents/prompts/cx-sre.md +9 -43
  16. package/agents/prompts/cx-ux-researcher.md +1 -0
  17. package/agents/role-manifests.json +4 -4
  18. package/bin/construct +279 -4
  19. package/commands/understand/research.md +5 -3
  20. package/db/schema/004_recommendations.sql +46 -0
  21. package/db/schema/005_strategy.sql +21 -0
  22. package/lib/auto-docs.mjs +3 -4
  23. package/lib/beads-automation.mjs +16 -7
  24. package/lib/cli-commands.mjs +52 -2
  25. package/lib/comment-lint.mjs +7 -1
  26. package/lib/config/schema.mjs +3 -0
  27. package/lib/embed/conflict-detection.mjs +26 -9
  28. package/lib/embed/customer-profiles.mjs +37 -17
  29. package/lib/embed/daemon.mjs +10 -8
  30. package/lib/embed/recommendation-store.mjs +213 -14
  31. package/lib/embed/workspaces.mjs +53 -18
  32. package/lib/flavors/loader.mjs +136 -0
  33. package/lib/gates-audit.mjs +3 -3
  34. package/lib/health-check.mjs +1 -1
  35. package/lib/hooks/agent-tracker.mjs +22 -3
  36. package/lib/hooks/pre-compact.mjs +3 -0
  37. package/lib/hooks/pre-push-gate.mjs +14 -1
  38. package/lib/hooks/read-tracker.mjs +10 -101
  39. package/lib/hooks/session-optimize.mjs +3 -2
  40. package/lib/hooks/session-reflect.mjs +68 -0
  41. package/lib/host-capabilities.mjs +90 -1
  42. package/lib/init-unified.mjs +25 -2
  43. package/lib/init-update.mjs +246 -131
  44. package/lib/intake/classify.mjs +61 -183
  45. package/lib/intake/prepare.mjs +7 -0
  46. package/lib/intake/tables/creative.mjs +94 -0
  47. package/lib/intake/tables/operations.mjs +85 -0
  48. package/lib/intake/tables/research.mjs +85 -0
  49. package/lib/intake/tables/rnd.mjs +175 -0
  50. package/lib/intent-classifier.mjs +1 -0
  51. package/lib/knowledge/layout.mjs +10 -0
  52. package/lib/knowledge/research-store.mjs +109 -0
  53. package/lib/mcp/tools/telemetry.mjs +30 -78
  54. package/lib/model-router.mjs +61 -1
  55. package/lib/observation-store.mjs +19 -0
  56. package/lib/ollama-manager.mjs +1 -1
  57. package/lib/opencode-telemetry.mjs +4 -5
  58. package/lib/orchestration-policy.mjs +9 -0
  59. package/lib/outcomes/aggregate.mjs +104 -0
  60. package/lib/outcomes/record.mjs +115 -0
  61. package/lib/parity.mjs +121 -21
  62. package/lib/profiles/lifecycle.mjs +388 -0
  63. package/lib/profiles/loader.mjs +123 -0
  64. package/lib/profiles/validate-custom.mjs +114 -0
  65. package/lib/prompt-composer.js +106 -29
  66. package/lib/read-tracker-store.mjs +149 -0
  67. package/lib/reflect/extractor.mjs +193 -0
  68. package/lib/reflect.mjs +89 -2
  69. package/lib/sandbox.mjs +102 -0
  70. package/lib/server/index.mjs +76 -0
  71. package/lib/server/telemetry-login.mjs +17 -25
  72. package/lib/service-manager.mjs +30 -22
  73. package/lib/services/local-postgres.mjs +15 -0
  74. package/lib/services/telemetry-backend.mjs +1 -2
  75. package/lib/setup.mjs +8 -43
  76. package/lib/status.mjs +51 -5
  77. package/lib/storage/backend.mjs +12 -2
  78. package/lib/strategy-store.mjs +371 -0
  79. package/lib/telemetry/backends/local.mjs +6 -4
  80. package/lib/telemetry/client.mjs +185 -0
  81. package/lib/telemetry/ingest.mjs +13 -5
  82. package/lib/telemetry/team-rollup.mjs +9 -2
  83. package/lib/worker/trace.mjs +17 -27
  84. package/package.json +10 -2
  85. package/personas/construct.md +20 -20
  86. package/platforms/claude/settings.template.json +13 -0
  87. package/rules/common/research.md +44 -12
  88. package/scripts/sync-agents.mjs +11 -0
  89. package/skills/docs/backlog-proposal-workflow.md +2 -2
  90. package/skills/docs/customer-profile-workflow.md +1 -1
  91. package/skills/docs/evidence-ingest-workflow.md +5 -5
  92. package/skills/docs/prfaq-workflow.md +1 -1
  93. package/skills/docs/product-intelligence-review.md +1 -1
  94. package/skills/docs/product-intelligence-workflow.md +3 -3
  95. package/skills/docs/product-signal-workflow.md +48 -18
  96. package/skills/docs/research-workflow.md +26 -14
  97. package/skills/docs/strategy-workflow.md +36 -0
  98. package/skills/roles/architect.ai-systems.md +4 -2
  99. package/skills/roles/architect.data.md +4 -2
  100. package/skills/roles/architect.enterprise.md +4 -2
  101. package/skills/roles/architect.integration.md +4 -2
  102. package/skills/roles/architect.md +7 -5
  103. package/skills/roles/architect.platform.md +4 -2
  104. package/skills/roles/data-analyst.experiment.md +4 -2
  105. package/skills/roles/data-analyst.md +9 -7
  106. package/skills/roles/data-analyst.product-intelligence.md +5 -3
  107. package/skills/roles/data-analyst.product.md +4 -2
  108. package/skills/roles/data-analyst.telemetry.md +4 -2
  109. package/skills/roles/data-engineer.pipeline.md +4 -2
  110. package/skills/roles/data-engineer.vector-retrieval.md +4 -2
  111. package/skills/roles/data-engineer.warehouse.md +4 -2
  112. package/skills/roles/debugger.md +7 -5
  113. package/skills/roles/designer.accessibility.md +4 -2
  114. package/skills/roles/designer.md +10 -8
  115. package/skills/roles/engineer.ai.md +4 -2
  116. package/skills/roles/engineer.data.md +5 -3
  117. package/skills/roles/engineer.md +14 -12
  118. package/skills/roles/engineer.platform.md +5 -3
  119. package/skills/roles/operator.docs.md +6 -4
  120. package/skills/roles/operator.md +6 -4
  121. package/skills/roles/operator.release.md +4 -2
  122. package/skills/roles/operator.sre.md +5 -3
  123. package/skills/roles/orchestrator.md +5 -3
  124. package/skills/roles/product-manager.ai-product.md +4 -2
  125. package/skills/roles/product-manager.business-strategy.md +4 -2
  126. package/skills/roles/product-manager.enterprise.md +4 -2
  127. package/skills/roles/product-manager.growth.md +4 -2
  128. package/skills/roles/product-manager.md +6 -4
  129. package/skills/roles/product-manager.platform.md +4 -2
  130. package/skills/roles/product-manager.product.md +4 -2
  131. package/skills/roles/qa.ai-eval.md +4 -2
  132. package/skills/roles/qa.api-contract.md +4 -2
  133. package/skills/roles/qa.data-pipeline.md +4 -2
  134. package/skills/roles/qa.md +7 -5
  135. package/skills/roles/qa.test-automation.md +5 -3
  136. package/skills/roles/qa.web-ui.md +4 -2
  137. package/skills/roles/researcher.explorer.md +4 -2
  138. package/skills/roles/researcher.md +35 -20
  139. package/skills/roles/researcher.ux.md +4 -2
  140. package/skills/roles/reviewer.devil-advocate.md +4 -2
  141. package/skills/roles/reviewer.evaluator.md +4 -2
  142. package/skills/roles/reviewer.md +14 -12
  143. package/skills/roles/reviewer.trace.md +4 -2
  144. package/skills/roles/security.ai.md +4 -2
  145. package/skills/roles/security.appsec.md +4 -2
  146. package/skills/roles/security.cloud.md +4 -2
  147. package/skills/roles/security.legal-compliance.md +4 -2
  148. package/skills/roles/security.md +7 -5
  149. package/skills/roles/security.privacy.md +4 -2
  150. package/skills/roles/security.supply-chain.md +4 -2
  151. package/skills/routing.md +8 -1
  152. package/templates/docs/persona-artifact.md +36 -0
  153. package/templates/docs/research-brief.md +63 -9
  154. package/templates/docs/research-finding.md +26 -0
  155. package/templates/docs/skill-artifact.md +27 -0
  156. package/templates/docs/strategy.md +36 -0
  157. package/templates/homebrew/construct.rb +6 -6
package/bin/construct CHANGED
@@ -186,14 +186,49 @@ function runNodeScript(scriptPath, args = [], extraEnv = {}) {
186
186
  async function cmdStatus() {
187
187
  const jsonOutput = restArgsCache.includes('--json');
188
188
  const status = await buildStatus({ rootDir: ROOT_DIR, cwd: process.cwd(), homeDir: HOME, env: process.env });
189
-
189
+
190
190
  if (jsonOutput) {
191
191
  println(JSON.stringify(status, null, 2));
192
192
  return;
193
193
  }
194
-
194
+
195
195
  println(formatStatusReport(status));
196
-
196
+
197
+ // Append the learning-loops summary so the operator gets a single answer
198
+ // to "is this thing actually learning?" without remembering a second
199
+ // command. Cheap: just reads .cx/observations + .cx/outcomes summaries.
200
+ try {
201
+ const { resolveActiveProfile } = await import('../lib/profiles/loader.mjs');
202
+ const { readSummary } = await import('../lib/outcomes/aggregate.mjs');
203
+ const fs = await import('node:fs');
204
+ const path = await import('node:path');
205
+ const cwd = process.cwd();
206
+ const active = resolveActiveProfile(cwd);
207
+ const obsIdxPath = path.join(cwd, '.cx', 'observations', 'index.json');
208
+ let obsTotal = 0;
209
+ let obs24h = 0;
210
+ if (fs.existsSync(obsIdxPath)) {
211
+ const idx = JSON.parse(fs.readFileSync(obsIdxPath, 'utf8'));
212
+ obsTotal = idx.length;
213
+ const since = Date.now() - 24 * 60 * 60 * 1000;
214
+ obs24h = idx.filter((e) => Date.parse(e.createdAt) >= since).length;
215
+ }
216
+ const researchDir = path.join(cwd, '.cx', 'knowledge', 'external', 'research');
217
+ const researchCount = fs.existsSync(researchDir)
218
+ ? fs.readdirSync(researchDir).filter((f) => f.endsWith('.md')).length
219
+ : 0;
220
+ const summary = readSummary(cwd);
221
+ const outcomeRoles = summary?.roles ? Object.keys(summary.roles).length : 0;
222
+
223
+ println('');
224
+ println('Learning loops');
225
+ println('โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€');
226
+ println(` Profile ${active.id} (${active.displayName || ''})`);
227
+ println(` Observations ${obsTotal} total, ${obs24h} in last 24h`);
228
+ println(` Research files ${researchCount}`);
229
+ println(` Outcome roles ${outcomeRoles}`);
230
+ println(` Detail: npm run learning:status`);
231
+ } catch { /* best-effort surface; never block status */ }
197
232
  }
198
233
 
199
234
  async function cmdShow() {
@@ -421,6 +456,22 @@ async function cmdDoctor() {
421
456
  add('Claude Code agents dir', fs.existsSync(path.join(HOME, '.claude', 'agents')), true);
422
457
  add('Codex agents dir', fs.existsSync(path.join(HOME, '.codex', 'agents')), true);
423
458
  add('Copilot prompts dir', fs.existsSync(path.join(HOME, '.github', 'prompts')), true);
459
+ add('Cursor MCP config', fs.existsSync(path.join(HOME, '.cursor', 'mcp.json')), true);
460
+ const vscodeSettingsPaths = process.platform === 'darwin'
461
+ ? [
462
+ path.join(HOME, 'Library', 'Application Support', 'Code', 'User', 'settings.json'),
463
+ path.join(HOME, 'Library', 'Application Support', 'Code - Insiders', 'User', 'settings.json'),
464
+ ]
465
+ : process.platform === 'linux'
466
+ ? [
467
+ path.join(HOME, '.config', 'Code', 'User', 'settings.json'),
468
+ path.join(HOME, '.config', 'Code - Insiders', 'User', 'settings.json'),
469
+ ]
470
+ : [
471
+ path.join(process.env.APPDATA ?? path.join(HOME, 'AppData', 'Roaming'), 'Code', 'User', 'settings.json'),
472
+ path.join(process.env.APPDATA ?? path.join(HOME, 'AppData', 'Roaming'), 'Code - Insiders', 'User', 'settings.json'),
473
+ ];
474
+ add('VS Code settings file', vscodeSettingsPaths.some((candidate) => fs.existsSync(candidate)), true);
424
475
  add('User config ready', fs.existsSync(getUserEnvPath(HOME)) || fs.existsSync(path.join(HOME, '.construct')), true);
425
476
  add('skills/ directory', fs.existsSync(path.join(ROOT_DIR, 'skills')));
426
477
  add('rules/common/', fs.existsSync(path.join(ROOT_DIR, 'rules', 'common')));
@@ -3217,6 +3268,27 @@ async function cmdHook(args) {
3217
3268
  const [command, ...rest] = process.argv.slice(2);
3218
3269
  const restArgsCache = rest;
3219
3270
 
3271
+ // Parse `--key=value` and repeatable `--source-url=<url>` for `construct knowledge add`.
3272
+ function parseKnowledgeAddArgs(args) {
3273
+ const opts = { slug: null, topic: null, confidence: 'inferred', sources: [] };
3274
+ for (const arg of args) {
3275
+ if (arg.startsWith('--slug=')) opts.slug = arg.slice('--slug='.length);
3276
+ else if (arg.startsWith('--topic=')) opts.topic = arg.slice('--topic='.length);
3277
+ else if (arg.startsWith('--confidence=')) opts.confidence = arg.slice('--confidence='.length);
3278
+ else if (arg.startsWith('--source-url=')) opts.sources.push({ url: arg.slice('--source-url='.length) });
3279
+ else if (arg.startsWith('--source=')) { /* tag, ignored for now */ }
3280
+ }
3281
+ return opts;
3282
+ }
3283
+
3284
+ // Read all of stdin as a UTF-8 string. Returns '' if nothing was piped.
3285
+ async function readStdin() {
3286
+ if (process.stdin.isTTY) return '';
3287
+ let raw = '';
3288
+ for await (const chunk of process.stdin) raw += chunk;
3289
+ return raw;
3290
+ }
3291
+
3220
3292
  const handlers = new Map([
3221
3293
  // Core
3222
3294
  ['dev', cmdUp],
@@ -3252,6 +3324,24 @@ const handlers = new Map([
3252
3324
  ['handoffs', cmdHandoffs],
3253
3325
  ['headhunt', cmdHeadhunt],
3254
3326
  ['init', cmdInit],
3327
+ ['drop', async (args) => {
3328
+ const { runDropCli } = await import('../lib/drop.mjs');
3329
+ return runDropCli(args, { cwd: process.cwd(), env: process.env });
3330
+ }],
3331
+ ['docs', async (args) => {
3332
+ const sub = args[0];
3333
+ if (!sub || sub === '--help' || sub === '-h') {
3334
+ println('Usage: construct docs <check|verify|update|site>');
3335
+ return;
3336
+ }
3337
+ const rest = args.slice(1);
3338
+ if (sub === 'verify') return cmdDocsVerify(rest);
3339
+ if (sub === 'update') return cmdDocsUpdate(rest);
3340
+ if (sub === 'check') return cmdDocsCheck(rest);
3341
+ if (sub === 'site') return cmdDocsSite();
3342
+ errorln(`Unknown docs subcommand: ${sub}. Available: check, verify, update, site`);
3343
+ process.exit(1);
3344
+ }],
3255
3345
  ['docs:verify', cmdDocsVerify],
3256
3346
  ['init:update', cmdInitUpdate],
3257
3347
  ['models', cmdModels],
@@ -3817,7 +3907,185 @@ const handlers = new Map([
3817
3907
  }
3818
3908
  return;
3819
3909
  }
3820
- errorln(`Unknown knowledge subcommand: ${sub}. Available: trends, index`);
3910
+ if (sub === 'add') {
3911
+ const opts = parseKnowledgeAddArgs(args.slice(1));
3912
+ if (!opts.slug || !opts.topic) {
3913
+ errorln('Usage: construct knowledge add --source=research --slug=<id> --topic="..." [--confidence=confirmed|inferred|weak] [--source-url=<url>] (body on stdin)');
3914
+ process.exit(1);
3915
+ }
3916
+ const body = await readStdin();
3917
+ if (!body || body.trim().length < 10) {
3918
+ errorln('construct knowledge add: stdin body is empty or too short');
3919
+ process.exit(1);
3920
+ }
3921
+ const { addResearchFinding } = await import('../lib/knowledge/research-store.mjs');
3922
+ try {
3923
+ const { path: outPath, bytes } = await addResearchFinding({
3924
+ cwd: process.cwd(),
3925
+ slug: opts.slug,
3926
+ topic: opts.topic,
3927
+ body,
3928
+ confidence: opts.confidence,
3929
+ sources: opts.sources,
3930
+ });
3931
+ process.stdout.write(`โœ“ wrote ${outPath} (${bytes} bytes)\n`);
3932
+ return;
3933
+ } catch (err) {
3934
+ errorln(`construct knowledge add: ${err.message}`);
3935
+ process.exit(1);
3936
+ }
3937
+ }
3938
+ errorln(`Unknown knowledge subcommand: ${sub}. Available: trends, index, add`);
3939
+ process.exit(1);
3940
+ }],
3941
+ ['sandbox', async (args) => {
3942
+ const sub = args[0] || 'create';
3943
+ const { createSandbox, listSandboxes, deleteSandbox, pruneSandboxes } = await import('../lib/sandbox.mjs');
3944
+ if (sub === 'create') {
3945
+ const profileArg = args.find((a) => a.startsWith('--profile='));
3946
+ const profile = profileArg ? profileArg.split('=')[1] : null;
3947
+ const { id, path: p } = createSandbox({ profile });
3948
+ process.stdout.write(`โœ“ created sandbox ${id}\n path: ${p}\n`);
3949
+ if (profile) process.stdout.write(` profile: ${profile}\n`);
3950
+ return;
3951
+ }
3952
+ if (sub === 'list') {
3953
+ const all = listSandboxes();
3954
+ if (all.length === 0) { process.stdout.write(' (no sandboxes)\n'); return; }
3955
+ for (const s of all) process.stdout.write(` ${s.id.padEnd(30)} ${s.path}\n`);
3956
+ return;
3957
+ }
3958
+ if (sub === 'delete') {
3959
+ const id = args[1];
3960
+ if (!id) { errorln('Usage: construct sandbox delete <id>'); process.exit(1); }
3961
+ process.stdout.write(deleteSandbox(id) ? `โœ“ removed ${id}\n` : `not found: ${id}\n`);
3962
+ return;
3963
+ }
3964
+ if (sub === 'prune') {
3965
+ const daysArg = args.find((a) => a.startsWith('--days='));
3966
+ const days = daysArg ? Number(daysArg.split('=')[1]) || 7 : 7;
3967
+ const removed = pruneSandboxes({ olderThanDays: days });
3968
+ process.stdout.write(`โœ“ pruned ${removed} sandbox${removed === 1 ? '' : 'es'} older than ${days} day${days === 1 ? '' : 's'}\n`);
3969
+ return;
3970
+ }
3971
+ errorln(`Unknown sandbox subcommand: ${sub}. Available: create, list, delete, prune`);
3972
+ process.exit(1);
3973
+ }],
3974
+ ['profile', async (args) => {
3975
+ const sub = args[0] || 'show';
3976
+ const { listProfiles, loadProfile, resolveActiveProfile } = await import('../lib/profiles/loader.mjs');
3977
+ if (sub === 'list') {
3978
+ for (const id of listProfiles()) {
3979
+ const p = loadProfile(id);
3980
+ process.stdout.write(` ${id.padEnd(14)} ${p?.displayName || ''}\n`);
3981
+ }
3982
+ return;
3983
+ }
3984
+ if (sub === 'show') {
3985
+ const active = resolveActiveProfile(process.cwd());
3986
+ process.stdout.write(JSON.stringify({
3987
+ id: active.id,
3988
+ displayName: active.displayName,
3989
+ tagline: active.tagline,
3990
+ roles: active.roles?.length || 0,
3991
+ intakeTypes: active.intake?.types?.length || 0,
3992
+ }, null, 2) + '\n');
3993
+ return;
3994
+ }
3995
+ if (sub === 'set') {
3996
+ const id = args[1];
3997
+ if (!id) { errorln('Usage: construct profile set <id>'); process.exit(1); }
3998
+ const target = loadProfile(id);
3999
+ if (!target) {
4000
+ errorln(`Unknown profile: ${id}. Available: ${listProfiles().join(', ')}`);
4001
+ process.exit(1);
4002
+ }
4003
+ const { findProjectConfigPath, loadProjectConfig, writeProjectConfig, PROJECT_CONFIG_FILENAME } = await import('../lib/config/project-config.mjs');
4004
+ const fsMod = await import('node:fs');
4005
+ const pathMod = await import('node:path');
4006
+ const found = findProjectConfigPath(process.cwd());
4007
+ const cfgPath = found || pathMod.join(process.cwd(), PROJECT_CONFIG_FILENAME);
4008
+ const cfg = found ? (loadProjectConfig(process.cwd()) || { version: 1 }) : { version: 1 };
4009
+ cfg.profile = id;
4010
+ writeProjectConfig(cfgPath, cfg, { validate: true });
4011
+ process.stdout.write(`โœ“ profile set to ${id} (${target.displayName})\n`);
4012
+ return;
4013
+ }
4014
+ if (sub === 'create') {
4015
+ const id = args[1];
4016
+ if (!id) {
4017
+ errorln('Usage: construct profile create <id> [--display="..."] [--role=<id>...] [--department=<id>:<displayName>...]');
4018
+ process.exit(1);
4019
+ }
4020
+ const displayArg = args.find((a) => a.startsWith('--display='));
4021
+ const displayName = displayArg ? displayArg.split('=')[1] : id;
4022
+ const seedRoles = args
4023
+ .filter((a) => a.startsWith('--role='))
4024
+ .map((a) => a.slice('--role='.length))
4025
+ .filter(Boolean);
4026
+ const seedDepartments = args
4027
+ .filter((a) => a.startsWith('--department='))
4028
+ .map((a) => a.slice('--department='.length))
4029
+ .map((spec) => {
4030
+ const [deptId, deptDisplay] = spec.split(':');
4031
+ return { id: deptId, displayName: deptDisplay || deptId };
4032
+ });
4033
+ const { createDraftProfile } = await import('../lib/profiles/lifecycle.mjs');
4034
+ try {
4035
+ 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`);
4047
+ return;
4048
+ } catch (err) {
4049
+ errorln(err.message);
4050
+ process.exit(1);
4051
+ }
4052
+ }
4053
+ if (sub === 'drafts') {
4054
+ const { listDrafts } = await import('../lib/profiles/lifecycle.mjs');
4055
+ const drafts = listDrafts(process.cwd());
4056
+ if (drafts.length === 0) { process.stdout.write(' (no drafts)\n'); return; }
4057
+ for (const d of drafts) {
4058
+ const flags = [d.hasBrief ? 'brief' : null, d.hasProfile ? 'profile' : null].filter(Boolean).join(', ');
4059
+ process.stdout.write(` ${d.id.padEnd(20)} ${flags}\n`);
4060
+ }
4061
+ return;
4062
+ }
4063
+ if (sub === 'archive') {
4064
+ const id = args[1];
4065
+ 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); }
4068
+ const { archiveProfile } = await import('../lib/profiles/lifecycle.mjs');
4069
+ try {
4070
+ const { archived } = archiveProfile({ id, reason });
4071
+ process.stdout.write(`โœ“ archived ${id} to ${archived}\n`);
4072
+ return;
4073
+ } catch (err) {
4074
+ errorln(err.message);
4075
+ process.exit(1);
4076
+ }
4077
+ }
4078
+ if (sub === 'health') {
4079
+ const id = args[1];
4080
+ if (!id) { errorln('Usage: construct profile health <id> [--days=N]'); process.exit(1); }
4081
+ const daysArg = args.find((a) => a.startsWith('--days='));
4082
+ const windowDays = daysArg ? Number(daysArg.split('=')[1]) || 30 : 30;
4083
+ const { profileHealth } = await import('../lib/profiles/lifecycle.mjs');
4084
+ const report = profileHealth(process.cwd(), id, { windowDays });
4085
+ process.stdout.write(JSON.stringify(report, null, 2) + '\n');
4086
+ return;
4087
+ }
4088
+ errorln(`Unknown profile subcommand: ${sub}. Available: list, show, set, create, drafts, archive, health`);
3821
4089
  process.exit(1);
3822
4090
  }],
3823
4091
  ]);
@@ -3834,6 +4102,13 @@ if (command === '--help' || command === '-h' || command === 'help') {
3834
4102
  process.exit(0);
3835
4103
  }
3836
4104
 
4105
+ // Version flags โ†’ print version and exit
4106
+ if (command === '--version' || command === '-V') {
4107
+ const pkg = JSON.parse(fs.readFileSync(path.join(ROOT_DIR, 'package.json'), 'utf8'));
4108
+ println(`construct v${pkg.version}`);
4109
+ process.exit(0);
4110
+ }
4111
+
3837
4112
  const handler = handlers.get(command);
3838
4113
  if (!handler) {
3839
4114
  errorln(`Unknown command: ${command}`);
@@ -1,10 +1,10 @@
1
1
  <!--
2
- commands/understand/research.md โ€” Research a topic โ€” verify facts from primary sources, separate evidence from inference
2
+ commands/understand/research.md. Research a topic. Verify facts from primary sources, separate evidence from inference.
3
3
 
4
- Research a topic โ€” verify facts from primary sources, separate evidence from inference
4
+ Research a topic. Verify facts from primary sources, separate evidence from inference.
5
5
  -->
6
6
  ---
7
- description: Research a topic โ€” verify facts from primary sources, separate evidence from inference
7
+ description: Research a topic. Verify facts from primary sources, separate evidence from inference.
8
8
  ---
9
9
 
10
10
  You are Construct. Research: $ARGUMENTS
@@ -20,3 +20,5 @@ Method:
20
20
  For each finding: source, date, confidence (confirmed / inferred / weak signal).
21
21
 
22
22
  Output: FINDINGS (with citations) | INFERENCES (labeled) | GAPS | RECOMMENDATION
23
+
24
+ Persistence: after producing the output, run `construct knowledge add --source=research --slug=<topic-slug> --topic="<one-line topic>" --confidence=<confirmed|inferred|weak>` and pass the FINDINGS+INFERENCES+GAPS+RECOMMENDATION block on stdin. The store writes a frontmatter-stamped markdown file under `.cx/knowledge/external/research/` and indexes it for future hybrid search. Pass `--source-url=<url>` repeatedly for each cited source (required when confidence=confirmed).
@@ -0,0 +1,46 @@
1
+ -- 004_recommendations.sql โ€” Recommendations table for team and enterprise mode.
2
+ -- Solo mode uses the JSONL store under .cx/intake/ instead.
3
+ --
4
+ -- Dedup key is enforced at the (project, dedup_key) level โ€” matches the in-memory
5
+ -- dedup contract in lib/embed/recommendation-store.mjs.
6
+ -- Active recommendations: dismissed_at IS NULL AND superseded_at IS NULL.
7
+
8
+ create table if not exists construct_recommendations (
9
+ id text primary key,
10
+ project text not null,
11
+ dedup_key text not null,
12
+ type text not null,
13
+ title text not null,
14
+ reason text,
15
+ lane text,
16
+ signal_count int not null default 1,
17
+ total_signal_count int not null default 1,
18
+ customer_impact int not null default 0,
19
+ recency_bonus int not null default 0,
20
+ strategic_bonus int not null default 0,
21
+ score float not null default 0,
22
+ priority text not null default 'P3',
23
+ source_signal_ids jsonb not null default '[]'::jsonb,
24
+ first_seen timestamptz not null default now(),
25
+ last_seen timestamptz not null default now(),
26
+ dismissed_at timestamptz,
27
+ dismiss_reason text,
28
+ superseded_at timestamptz,
29
+ superseded_by_id text,
30
+ suppressed_until timestamptz,
31
+ suppress_reason text,
32
+ updated_at timestamptz not null default now()
33
+ );
34
+
35
+ create unique index if not exists construct_recommendations_project_dedup_key_idx
36
+ on construct_recommendations (project, dedup_key);
37
+
38
+ create index if not exists construct_recommendations_priority_score_idx
39
+ on construct_recommendations (project, priority, score desc);
40
+
41
+ create index if not exists construct_recommendations_last_seen_idx
42
+ on construct_recommendations (project, last_seen desc);
43
+
44
+ create index if not exists construct_recommendations_active_idx
45
+ on construct_recommendations (project, dismissed_at)
46
+ where dismissed_at is null;
@@ -0,0 +1,21 @@
1
+ -- 005_strategy.sql โ€” Product strategy store for team and enterprise mode.
2
+ -- Solo mode uses ~/.cx/strategy.md instead.
3
+ --
4
+ -- Each row is an immutable version snapshot. The latest version for a project
5
+ -- is the row with the highest version number (or max updated_at when equal).
6
+ -- Callers insert a new row on each write; old versions are retained for history.
7
+
8
+ create table if not exists construct_strategy (
9
+ id bigserial primary key,
10
+ project text not null,
11
+ content text not null,
12
+ version int not null default 1,
13
+ updated_at timestamptz not null default now(),
14
+ updated_by text
15
+ );
16
+
17
+ create unique index if not exists construct_strategy_project_version_idx
18
+ on construct_strategy (project, version);
19
+
20
+ create index if not exists construct_strategy_project_updated_at_idx
21
+ on construct_strategy (project, updated_at desc);
package/lib/auto-docs.mjs CHANGED
@@ -71,7 +71,7 @@ function buildCoreDocsContract() {
71
71
  '| `docs/README.md` | Docs index and maintenance contract | Core docs set or maintenance expectations change |',
72
72
  '| `docs/concepts/architecture.md` | Canonical architecture and invariants | Runtime shape, contracts, boundaries, or major dependencies change |',
73
73
  '',
74
- '`plan.md` is a local working document. `construct init` creates it for the active session, but it is gitignored and not committed โ€” durable work belongs in the tracker (Beads or external).',
74
+ '`plan.md` is a local working document. `construct init` creates it for the active session, but it is gitignored and not committed; durable work belongs in the tracker (Beads or external).',
75
75
  '',
76
76
  'Tracker hierarchy: external tracker (prefer Beads) for durable work, `plan.md` for the local working plan, and cass-memory via MCP `memory` for cross-session recall.',
77
77
  '',
@@ -82,13 +82,13 @@ function buildCoreDocsContract() {
82
82
 
83
83
  const DIR_DESCRIPTIONS = {
84
84
  agents: 'Registry and generated platform adapter chains',
85
- apps: 'User-facing apps shipped from this repo (e.g., apps/docs/ โ€” Fumadocs docs site)',
85
+ apps: 'User-facing apps shipped from this repo (e.g. apps/docs/, the Fumadocs docs site)',
86
86
  bin: 'CLI entrypoint (`construct`)',
87
87
  claude: 'Claude Code integration (agents, settings template)',
88
88
  commands: 'Command prompt assets',
89
89
  codex: 'GitHub Copilot / Codex integration',
90
90
  docs: 'Architecture notes, runbooks, and documentation contract',
91
- telemetry: 'Telemetry backend for agent observability',
91
+ telemetry: 'Local trace capture and optional telemetry export',
92
92
  lib: 'Core runtime: CLI, hooks, MCP, status, sync, workflow',
93
93
  opencode: 'OpenCode integration config',
94
94
  personas: 'Persona prompt definitions',
@@ -470,4 +470,3 @@ export function buildFumadocsReference({ rootDir } = {}) {
470
470
 
471
471
  return { written };
472
472
  }
473
-
@@ -22,6 +22,11 @@ const HANDOFFS_DIR = '.cx/handoffs';
22
22
  const CONFIRM_TIMEOUT = 60000; // 60 second timeout for confirmation prompts
23
23
  const POSITIVE_INTENT = /^(y|yes|yeah|yep|yup|sure|ok|okay|go|proceed|do it)\b/i;
24
24
 
25
+ function beadsConfirmEnabled(env = process.env) {
26
+ const raw = String(env.CONSTRUCT_BEADS_CONFIRM ?? '').trim().toLowerCase();
27
+ return raw === '1' || raw === 'true' || raw === 'yes' || raw === 'on';
28
+ }
29
+
25
30
  // ---------------------------------------------------------------------------
26
31
  // Confirmation Utility
27
32
  // ---------------------------------------------------------------------------
@@ -148,14 +153,18 @@ export async function confirmAndCommit({
148
153
  }
149
154
  console.error('');
150
155
 
151
- const confirmed = await confirm(
152
- 'Commit these changes?',
153
- { yes, default: 'n' }
154
- );
156
+ if (yes || beadsConfirmEnabled()) {
157
+ const confirmed = await confirm(
158
+ 'Commit these changes?',
159
+ { yes, default: 'n' }
160
+ );
155
161
 
156
- if (!confirmed) {
157
- console.error('[beads] Commit cancelled');
158
- return null;
162
+ if (!confirmed) {
163
+ console.error('[beads] Commit cancelled');
164
+ return null;
165
+ }
166
+ } else {
167
+ console.error('[beads] CONSTRUCT_BEADS_CONFIRM is off โ€” committing without interactive prompt.');
159
168
  }
160
169
 
161
170
  runGit(['commit', '-m', message], { cwd });
@@ -79,7 +79,13 @@ export const CLI_COMMANDS = [
79
79
  category: 'Core',
80
80
  core: true,
81
81
  description: 'Sync agent adapters to AI tools',
82
- usage: 'construct sync',
82
+ usage: 'construct sync [--project] [--dry-run] [--no-docs] [--compress-personas]',
83
+ options: [
84
+ { flag: '--project', desc: 'Write project-local Claude adapters into the current repo only' },
85
+ { flag: '--dry-run', desc: 'Preview adapter changes without writing files' },
86
+ { flag: '--no-docs', desc: 'Skip AUTO docs regeneration after syncing adapters' },
87
+ { flag: '--compress-personas', desc: 'Write compressed runtime persona prompts without changing the source prompts' },
88
+ ],
83
89
  },
84
90
  {
85
91
  name: 'intake',
@@ -122,6 +128,50 @@ export const CLI_COMMANDS = [
122
128
  { name: 'search <query>', desc: 'Search customer profiles by name/alias' },
123
129
  ],
124
130
  },
131
+ {
132
+ name: 'knowledge',
133
+ emoji: '๐Ÿง ',
134
+ category: 'Work',
135
+ core: false,
136
+ description: 'Query, index, or add to the project knowledge base',
137
+ usage: 'construct knowledge trends|index|add',
138
+ subcommands: [
139
+ { name: 'trends', desc: 'Show trend report across observations and artifacts' },
140
+ { name: 'index', desc: 'Rebuild the local RAG corpus over .cx/ artifacts' },
141
+ { name: 'add --source=research --slug=<id> --topic="..." [--source-url=<url>]', desc: 'Persist a research finding into .cx/knowledge/external/research/' },
142
+ ],
143
+ },
144
+ {
145
+ name: 'sandbox',
146
+ emoji: '๐Ÿงช',
147
+ category: 'Core',
148
+ core: true,
149
+ description: 'Isolated tmpdir-based environment for QA / specialist dry-runs',
150
+ usage: 'construct sandbox create|list|delete|prune [--profile=<id>]',
151
+ subcommands: [
152
+ { name: 'create [--profile=<id>]', desc: 'Create a new sandbox under ~/.cx/sandboxes/' },
153
+ { name: 'list', desc: 'List existing sandboxes, newest first' },
154
+ { name: 'delete <id>', desc: 'Remove one sandbox by id' },
155
+ { name: 'prune [--days=N]', desc: 'Remove sandboxes older than N days (default 7)' },
156
+ ],
157
+ },
158
+ {
159
+ name: 'profile',
160
+ emoji: '๐Ÿงญ',
161
+ category: 'Core',
162
+ core: true,
163
+ description: 'Manage the active org profile and its lifecycle (draft, promote, archive, health)',
164
+ usage: 'construct profile show|list|set|create|drafts|archive|health',
165
+ subcommands: [
166
+ { name: 'show', desc: 'Show the active profile' },
167
+ { name: 'list', desc: 'List curated profiles' },
168
+ { name: 'set <id>', desc: 'Switch the active profile (writes construct.config.json)' },
169
+ { name: 'create <id> [--display="..."]', desc: 'Scaffold a draft profile + requirements brief under .cx/profiles/draft-<id>/' },
170
+ { name: 'drafts', desc: 'List in-progress draft profiles' },
171
+ { name: 'archive <id> --reason="..."', desc: 'Move a curated profile into archive/profiles/<id>/' },
172
+ { name: 'health <id> [--days=N]', desc: 'Per-profile observation + outcome rollup' },
173
+ ],
174
+ },
125
175
  {
126
176
  name: 'workspace',
127
177
  emoji: '๐Ÿข',
@@ -475,7 +525,7 @@ export const CLI_COMMANDS = [
475
525
  category: 'Advanced',
476
526
  core: false,
477
527
  description: 'Show version',
478
- usage: 'construct version',
528
+ usage: 'construct version | construct --version',
479
529
  },
480
530
  {
481
531
  name: 'cleanup',
@@ -42,7 +42,13 @@ function requiresHeader(rel) {
42
42
  if (rel.startsWith('lib/server/static/')) {
43
43
  return { required: false, type: null };
44
44
  }
45
- const type = (jsMatch && ext !== '.html') ? 'js' : (jsMatch || mdMatch) ? 'md' : null;
45
+ // Markdown files always get markdown-style headers, even if the directory
46
+ // glob (e.g. ^tests/) primarily targets JS sources. Without this, .md docs
47
+ // co-located with .mjs tests were forced into /** */ format.
48
+ let type;
49
+ if (ext === '.md') type = mdMatch || jsMatch ? 'md' : null;
50
+ else if (jsMatch && ext !== '.html') type = 'js';
51
+ else type = (jsMatch || mdMatch) ? 'md' : null;
46
52
  return { required: jsMatch || mdMatch, type };
47
53
  }
48
54
 
@@ -20,6 +20,7 @@ export const CONFIG_SCHEMA_VERSION = 1;
20
20
 
21
21
  export const DEPLOYMENT_MODES = ['solo', 'team', 'enterprise'];
22
22
  export const MCP_BROKER_VALUES = ['auto', 'on', 'off'];
23
+ export const DEFAULT_PROFILE_ID = 'rnd';
23
24
 
24
25
  export const DEFAULT_PROJECT_CONFIG = Object.freeze({
25
26
  version: CONFIG_SCHEMA_VERSION,
@@ -31,6 +32,7 @@ export const DEFAULT_PROJECT_CONFIG = Object.freeze({
31
32
  tenantId: null,
32
33
  }),
33
34
  providers: Object.freeze({}),
35
+ profile: DEFAULT_PROFILE_ID,
34
36
  autoEmbed: false,
35
37
  telemetry: Object.freeze({
36
38
  enabled: true,
@@ -77,6 +79,7 @@ export const FIELD_RULES = {
77
79
  },
78
80
  },
79
81
  providers: { type: 'object', required: false },
82
+ profile: { type: 'string', required: false, maxLength: 40 },
80
83
  autoEmbed: { type: 'boolean', required: false },
81
84
  telemetry: {
82
85
  type: 'object',