@geraldmaron/construct 1.0.23 → 1.1.0

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 (106) hide show
  1. package/README.md +0 -2
  2. package/bin/construct +189 -207
  3. package/lib/agent-instructions/inject.mjs +25 -4
  4. package/lib/audit-rules.mjs +127 -0
  5. package/lib/audit-skills.mjs +43 -1
  6. package/lib/beads-client.mjs +9 -0
  7. package/lib/beads-optimistic.mjs +23 -71
  8. package/lib/bridges/copilot-proxy.mjs +116 -0
  9. package/lib/cli-commands.mjs +5 -1
  10. package/lib/comment-lint.mjs +1 -1
  11. package/lib/config/schema.mjs +1 -1
  12. package/lib/document-extract/docling-client.mjs +16 -6
  13. package/lib/document-extract/docling-sidecar.py +32 -2
  14. package/lib/document-extract.mjs +37 -10
  15. package/lib/document-ingest.mjs +90 -5
  16. package/lib/embed/inbox.mjs +6 -3
  17. package/lib/embed/recommendation-store.mjs +7 -289
  18. package/lib/embed/reconcile.mjs +2 -2
  19. package/lib/embed/roadmap.mjs +16 -1
  20. package/lib/engine/consolidate.mjs +160 -3
  21. package/lib/engine/contradiction-judge.mjs +71 -0
  22. package/lib/engine/contradiction.mjs +74 -0
  23. package/lib/hooks/config-protection.mjs +4 -4
  24. package/lib/hooks/session-reflect.mjs +5 -1
  25. package/lib/host-capabilities.mjs +30 -0
  26. package/lib/ingest/docling-remote.mjs +90 -0
  27. package/lib/ingest/strategy.mjs +1 -1
  28. package/lib/init-unified.mjs +9 -13
  29. package/lib/intake/git-queue.mjs +195 -0
  30. package/lib/intake/queue.mjs +9 -16
  31. package/lib/logging/rotate.mjs +18 -0
  32. package/lib/mcp/server.mjs +124 -12
  33. package/lib/mcp/tool-budget.mjs +53 -0
  34. package/lib/mcp/tools/storage.mjs +2 -3
  35. package/lib/mcp-catalog.json +4 -4
  36. package/lib/mcp-manager.mjs +59 -3
  37. package/lib/observation-store.mjs +38 -166
  38. package/lib/ollama/capability-store.mjs +78 -0
  39. package/lib/ollama/provision-context.mjs +344 -0
  40. package/lib/opencode-config.mjs +148 -0
  41. package/lib/opencode-telemetry.mjs +7 -0
  42. package/lib/orchestration/runtime.mjs +3 -2
  43. package/lib/orchestration-policy.mjs +41 -6
  44. package/lib/platforms/capabilities.mjs +100 -0
  45. package/lib/prompt-composer.js +12 -8
  46. package/lib/reconcile/agent-instructions-rewrap.mjs +8 -4
  47. package/lib/reconcile/index.mjs +0 -2
  48. package/lib/reflect/extractor.mjs +14 -1
  49. package/lib/reflect/salience.mjs +65 -0
  50. package/lib/rules-delivery.mjs +122 -0
  51. package/lib/runtime/uv-bootstrap.mjs +32 -17
  52. package/lib/service-manager.mjs +79 -259
  53. package/lib/setup.mjs +44 -425
  54. package/lib/specialists/prompt-schema.mjs +162 -0
  55. package/lib/specialists/scaffold.mjs +109 -0
  56. package/lib/status.mjs +3 -6
  57. package/lib/storage/admin.mjs +48 -325
  58. package/lib/storage/backend.mjs +10 -57
  59. package/lib/storage/embeddings-engine.mjs +19 -5
  60. package/lib/storage/hybrid-query.mjs +15 -196
  61. package/lib/storage/sync.mjs +36 -177
  62. package/lib/storage/vector-client.mjs +256 -235
  63. package/lib/strategy-store.mjs +35 -286
  64. package/lib/telemetry/beads-fallback.mjs +40 -0
  65. package/lib/telemetry/hook-calls.mjs +138 -0
  66. package/lib/worker/entrypoint.mjs +6 -14
  67. package/package.json +6 -5
  68. package/personas/construct.md +1 -1
  69. package/platforms/capabilities.json +76 -0
  70. package/platforms/claude/settings.template.json +0 -7
  71. package/platforms/opencode/sync-config.mjs +121 -25
  72. package/rules/common/neurodivergent-output.md +1 -1
  73. package/rules/web/coding-style.md +8 -0
  74. package/rules/web/design-quality.md +8 -0
  75. package/rules/web/hooks.md +8 -0
  76. package/rules/web/patterns.md +8 -0
  77. package/rules/web/performance.md +8 -0
  78. package/rules/web/security.md +8 -0
  79. package/rules/web/testing.md +8 -0
  80. package/scripts/sync-specialists.mjs +174 -40
  81. package/specialists/prompts/cx-architect.md +20 -0
  82. package/specialists/prompts/cx-qa.md +1 -1
  83. package/specialists/prompts/cx-test-automation.md +12 -0
  84. package/specialists/registry.json +0 -8
  85. package/templates/docs/construct_guide.md +1 -1
  86. package/db/schema/001_init.sql +0 -40
  87. package/db/schema/002_pgvector.sql +0 -182
  88. package/db/schema/003_intake.sql +0 -47
  89. package/db/schema/003_observation_reconciliation.sql +0 -14
  90. package/db/schema/004_recommendations.sql +0 -46
  91. package/db/schema/005_strategy.sql +0 -21
  92. package/db/schema/006_graph.sql +0 -24
  93. package/db/schema/007_tags.sql +0 -30
  94. package/db/schema/008_skill_usage.sql +0 -24
  95. package/db/schema/009_scheduler.sql +0 -14
  96. package/db/schema/010_cx_scores.sql +0 -51
  97. package/lib/intake/postgres-queue.mjs +0 -240
  98. package/lib/reconcile/postgres-namespace.mjs +0 -102
  99. package/lib/services/local-postgres.mjs +0 -15
  100. package/lib/storage/backup.mjs +0 -347
  101. package/lib/storage/migrations.mjs +0 -187
  102. package/lib/storage/postgres-backup.mjs +0 -124
  103. package/lib/storage/sql-store.mjs +0 -45
  104. package/lib/storage/store-version.mjs +0 -115
  105. package/lib/storage/unified-storage.mjs +0 -550
  106. package/lib/storage/vector-store.mjs +0 -100
@@ -44,6 +44,8 @@ import {
44
44
  writeCodexConfig,
45
45
  } from "../lib/codex-config.mjs";
46
46
  import { findOpenCodeConfigPath, readOpenCodeConfig, writeOpenCodeConfig } from "../lib/opencode-config.mjs";
47
+ import { HEAVY_EXTERNAL_MCP_IDS, LOCAL_SURFACE_MODES, decideTrim } from "../lib/mcp/tool-budget.mjs";
48
+ import { emitCursorRules } from "../lib/rules-delivery.mjs";
47
49
  import { resolvePromptContract } from "../lib/prompt-composer.js";
48
50
  import {
49
51
  buildClaudeMcpEntry,
@@ -157,6 +159,19 @@ const COMPRESS_PERSONAS = process.argv.includes("--compress-personas");
157
159
  const PROJECT_FLAG = process.argv.includes("--project");
158
160
  const GLOBAL_FLAG = process.argv.includes("--global");
159
161
 
162
+ // --local-surface=on|off|auto controls whether the heavy external MCP servers are
163
+ // disabled to fit a small local-model window. `auto` (default) trims only when the
164
+ // config's own default model is local — so a cloud session keeps context7/github
165
+ // even on a machine that also has Ollama. `on` forces the trim (the lever for users
166
+ // who pick a local model at runtime, leaving the config default unset); `off` keeps
167
+ // every server. CONSTRUCT_LOCAL_SURFACE is the env equivalent.
168
+
169
+ const LOCAL_SURFACE = (() => {
170
+ const arg = process.argv.find((a) => a.startsWith("--local-surface="));
171
+ const raw = (arg ? arg.slice("--local-surface=".length) : process.env.CONSTRUCT_LOCAL_SURFACE || "auto").trim().toLowerCase();
172
+ return LOCAL_SURFACE_MODES.includes(raw) ? raw : "auto";
173
+ })();
174
+
160
175
  // --quiet suppresses only the closing one-line summary, not the work or any
161
176
  // warning. `construct install` runs the global tier twice (plain `sync` then
162
177
  // `sync --global`); in a non-project cwd both land in the same global branch and
@@ -173,8 +188,13 @@ const summary = (msg) => { if (!QUIET) console.log(msg); };
173
188
  // Absent → null → write every host, preserving `construct sync` back-compat.
174
189
 
175
190
  import { detectHostCapabilities } from "../lib/host-capabilities.mjs";
176
-
177
- const HOST_KEYS = ["claude", "codex", "copilot", "opencode", "vscode", "cursor"];
191
+ import {
192
+ HOST_KEYS,
193
+ displayNameToKey,
194
+ hasNativeSubagents as hostHasNativeSubagents,
195
+ globalHookAllowlist,
196
+ globalMcpAllowlist,
197
+ } from "../lib/platforms/capabilities.mjs";
178
198
 
179
199
  function parseHostSelection() {
180
200
  const arg = process.argv.find((a) => a.startsWith("--hosts="));
@@ -182,14 +202,7 @@ function parseHostSelection() {
182
202
  if (!raw) {
183
203
  // Default to detected hosts if none are explicitly requested.
184
204
  const detected = new Set();
185
- const nameToKey = {
186
- "Claude Code": "claude",
187
- "OpenCode": "opencode",
188
- "Codex": "codex",
189
- "VS Code": "vscode",
190
- "Cursor": "cursor",
191
- "Copilot": "copilot",
192
- };
205
+ const nameToKey = displayNameToKey();
193
206
  try {
194
207
  for (const cap of detectHostCapabilities()) {
195
208
  if (cap.availability === "installed" && nameToKey[cap.host]) {
@@ -427,7 +440,7 @@ const hardDefaults = {
427
440
  // primary's provider. Explicit CX_MODEL_* env wins if set.
428
441
  const primaryFromOpenCode = (() => {
429
442
  try {
430
- const cfg = readOpenCodeConfig(findOpenCodeConfigPath()) ?? {};
443
+ const cfg = readOpenCodeConfig().config ?? {};
431
444
  return cfg.model || cfg.defaultModel || null;
432
445
  } catch { return null; }
433
446
  })();
@@ -598,9 +611,28 @@ function buildPrompt(entry, allEntries, platform) {
598
611
 
599
612
  prompt = inlineRoleAntiPatterns(prompt, root, entry.name, console.warn, { preload: entry.preloadRoleGuidance === true });
600
613
 
601
- if (entry.injectAgentRoster && allEntries) {
614
+ const capabilities = { hasNativeSubagents: HOST_KEYS.includes(platform) ? hostHasNativeSubagents(platform) : false };
615
+
616
+ // Platform-Native Orchestration Alignment (ADR-0002). Hosts with native subagent
617
+ // routing (OpenCode, VS Code, Cursor) do not get the static specialist roster
618
+ // injected — on a small-context local model the roster alone is ~3-4k tokens and,
619
+ // combined with MCP tool schemas, overruns the model's real context window and
620
+ // collapses output. Those hosts get a tool-bound micro-prompt instead and resolve
621
+ // the chain at runtime via the orchestration_policy MCP tool. Hosts without native
622
+ // routing (Claude Code, Codex) still need the roster to simulate handoffs in text.
623
+
624
+ if (entry.injectAgentRoster && allEntries && !capabilities.hasNativeSubagents) {
602
625
  const roster = buildAgentRoster(allEntries);
603
626
  prompt = `Available specialist agents:\n${roster}\n\n${prompt}`;
627
+ } else if (entry.injectAgentRoster && capabilities.hasNativeSubagents) {
628
+ // A worked tool-call example lifts small local models' tool-use reliability
629
+ // sharply (bead construct-c16l). Keep it to one compact turn so it stays within
630
+ // the prompt word cap; native-subagent hosts are exactly where local models run.
631
+
632
+ prompt = `You are the primary orchestrator. To discover available specialist agents, you MUST call the \`orchestration_policy\` MCP tool. Do not guess agent names.\n\n` +
633
+ `Example — the user says "add rate limiting to the API". Your first action is a tool call, not prose:\n` +
634
+ ` call orchestration_policy { "task": "add rate limiting to the API" }\n` +
635
+ `Then dispatch the specialists it returns. Always call the tool before answering.\n\n${prompt}`;
604
636
  }
605
637
 
606
638
  prompt += buildRoleFooter(entry);
@@ -800,6 +832,58 @@ function makeHooksPortable(hooksJson) {
800
832
  return JSON.stringify(walk(hooksJson));
801
833
  }
802
834
 
835
+ const GLOBAL_CLAUDE_HOOK_IDS = globalHookAllowlist('claude');
836
+
837
+ const GLOBAL_CLAUDE_MCP_IDS = globalMcpAllowlist('claude');
838
+
839
+ // Project scope writes only core-category MCP servers (plus construct-mcp, the
840
+ // orchestration server the specialist loop needs). optional/integration servers
841
+ // (memory, github, sequential-thinking, playwright, …) are opt-in via
842
+ // `construct mcp add` so a project does not silently inherit heavy servers it was
843
+ // never asked for (ADR-0031 §Consequences follow-up). A server already present in
844
+ // the project settings is preserved, so a manual opt-in sticks.
845
+
846
+ const PROJECT_DEFAULT_MCP_IDS = (() => {
847
+ try {
848
+ const catalog = JSON.parse(fs.readFileSync(path.join(root, "lib", "mcp-catalog.json"), "utf8"));
849
+ const arr = catalog.mcps || catalog.servers || [];
850
+ return new Set([...arr.filter((m) => m.category === "core").map((m) => m.id), "construct-mcp"]);
851
+ } catch {
852
+ return new Set(["context7", "construct-mcp"]);
853
+ }
854
+ })();
855
+
856
+ function filterGlobalClaudeHooks(hooksJson) {
857
+ const filtered = {};
858
+ for (const [event, groups] of Object.entries(hooksJson ?? {})) {
859
+ const kept = groups.filter((group) => GLOBAL_CLAUDE_HOOK_IDS.has(group.id));
860
+ if (kept.length > 0) filtered[event] = kept;
861
+ }
862
+ return filtered;
863
+ }
864
+
865
+ function syncGlobalClaudeMcpServers(settings, registryMcp) {
866
+ settings.mcpServers ??= {};
867
+ for (const id of Object.keys(settings.mcpServers)) {
868
+ if (id in registryMcp && !GLOBAL_CLAUDE_MCP_IDS.has(id)) {
869
+ delete settings.mcpServers[id];
870
+ }
871
+ }
872
+
873
+ for (const [id, mcpDef] of Object.entries(registryMcp)) {
874
+ if (!GLOBAL_CLAUDE_MCP_IDS.has(id)) continue;
875
+ const existingEntry = settings.mcpServers[id];
876
+ const existing = JSON.stringify(existingEntry ?? "");
877
+ const hasPlaceholder = existing.includes("__");
878
+ const hasFloatingVersion = existing.includes("@latest");
879
+ const registryWantsCommand = !mcpDef.type && Array.isArray(mcpDef.args);
880
+ const existingIsRemote = existingEntry && (existingEntry.type === 'http' || existingEntry.type === 'remote');
881
+ const transportMismatch = registryWantsCommand && existingIsRemote;
882
+ if (existingEntry && !hasPlaceholder && !hasFloatingVersion && !transportMismatch) continue;
883
+ settings.mcpServers[id] = buildClaudeMcpEntry(id, mcpDef, process.env);
884
+ }
885
+ }
886
+
803
887
  /**
804
888
  * Materialise a project-local `.claude/settings.json` from the home template,
805
889
  * with hook commands rewritten to be path-relative to whatever Construct
@@ -827,6 +911,7 @@ function writeProjectClaudeSettings(targetDir) {
827
911
  existing.mcpServers ??= {};
828
912
  for (const [id, mcpDef] of Object.entries(template.mcpServers)) {
829
913
  if (existing.mcpServers[id]) continue;
914
+ if (!PROJECT_DEFAULT_MCP_IDS.has(id)) continue;
830
915
  existing.mcpServers[id] = mcpDef;
831
916
  }
832
917
  }
@@ -842,6 +927,7 @@ function writeProjectClaudeSettings(targetDir) {
842
927
  existing.mcpServers ??= {};
843
928
  for (const [id, mcpDef] of Object.entries(registryMcp)) {
844
929
  if (existing.mcpServers[id]) continue;
930
+ if (!PROJECT_DEFAULT_MCP_IDS.has(id)) continue;
845
931
  existing.mcpServers[id] = buildClaudeMcpEntry(id, mcpDef, process.env);
846
932
  }
847
933
 
@@ -908,23 +994,13 @@ ${personaList}
908
994
  const constructReal = (() => {
909
995
  try { return fs.realpathSync(path.join(home, ".construct")); } catch { return path.join(home, ".construct"); }
910
996
  })();
911
- const hookStr = JSON.stringify(template.hooks)
997
+ const hookStr = JSON.stringify(filterGlobalClaudeHooks(template.hooks))
912
998
  .replace(/\$HOME\/\.construct/g, constructReal.replace(/\\/g, "/"));
913
999
  settings.hooks = JSON.parse(hookStr);
914
1000
  }
915
1001
  }
916
- if (!settings.mcpServers) settings.mcpServers = {};
917
1002
  const registryMcp = registry.mcpServers ?? {};
918
- for (const [id, mcpDef] of Object.entries(registryMcp)) {
919
- const existingEntry = settings.mcpServers[id];
920
- const existing = JSON.stringify(existingEntry ?? "");
921
- const hasPlaceholder = existing.includes("__");
922
- const registryWantsCommand = !mcpDef.type && Array.isArray(mcpDef.args);
923
- const existingIsRemote = existingEntry && (existingEntry.type === 'http' || existingEntry.type === 'remote');
924
- const transportMismatch = registryWantsCommand && existingIsRemote;
925
- if (existingEntry && !hasPlaceholder && !transportMismatch) continue;
926
- settings.mcpServers[id] = buildClaudeMcpEntry(id, mcpDef, process.env);
927
- }
1003
+ syncGlobalClaudeMcpServers(settings, registryMcp);
928
1004
  if (!DRY_RUN) fs.writeFileSync(claudeSettingsPath, JSON.stringify(settings, null, 2) + "\n");
929
1005
  }
930
1006
  }
@@ -1313,6 +1389,15 @@ function syncCursor(targetDir = null, wants = true) {
1313
1389
  fs.writeFileSync(rulesPath, body);
1314
1390
  }
1315
1391
  }
1392
+
1393
+ // Glob-scoped language rules land as managed per-rule .mdc files only when
1394
+ // the project's own files match their globs — Cursor's native auto-attach
1395
+ // convention. See docs/concepts/rules-delivery.md.
1396
+ try {
1397
+ emitCursorRules({ rulesDir: path.join(root, "rules"), targetDir, dryRun: DRY_RUN });
1398
+ } catch (err) {
1399
+ console.warn(`[sync] cursor rules delivery skipped: ${err.message}`);
1400
+ }
1316
1401
  }
1317
1402
  return true;
1318
1403
  }
@@ -1328,22 +1413,39 @@ function opencodePermissions(entry) {
1328
1413
  ? Object.fromEntries(Object.entries(entry.permissions).map(([k, v]) => [k, v]))
1329
1414
  : { edit: entry.canEdit === false ? "deny" : "allow", bash: "allow" };
1330
1415
 
1331
- // The orchestrator is the primary write surface; translate its abstract
1332
- // bash:allow into OpenCode's scoped permission map so destructive commands are
1333
- // denied and remote/history rewrites prompt the commit-approval contract
1334
- // enforced at the tool layer, not just in the prompt. Subagents keep the
1335
- // abstract grant. This scoped shape is an OpenCode translation detail and stays
1336
- // out of the cross-adapter registry.
1337
-
1338
- if (entry.isOrchestrator && perms.bash === "allow") {
1339
- perms.bash = {
1340
- "*": "allow",
1341
- "rm -rf *": "deny",
1342
- "git push *": "ask",
1343
- "git push --force*": "ask",
1344
- "git reset --hard *": "ask",
1345
- };
1416
+ // Agentic Scope Reduction (ADR-0002). Serializing 100+ MCP tool schemas into a
1417
+ // small local model's prompt overruns its context window and dilutes attention,
1418
+ // collapsing output. OpenCode's per-agent permission map prunes the surface: the
1419
+ // orchestrator keeps only orchestration + core tools and hands execution to
1420
+ // subagents; subagents keep execution tools but not orchestration.
1421
+
1422
+ if (entry.isOrchestrator) {
1423
+ if (perms.bash === "allow") {
1424
+ perms.bash = {
1425
+ "*": "allow",
1426
+ "rm -rf *": "deny",
1427
+ "git push *": "ask",
1428
+ "git push --force*": "ask",
1429
+ "git reset --hard *": "ask",
1430
+ };
1431
+ }
1432
+ // Heavy execution and external-knowledge tools are denied to the orchestrator so
1433
+ // its serialized tool schema stays small; orchestration_policy drives the handoff.
1434
+
1435
+ perms["mcp__construct-mcp__extract_document_text"] = "deny";
1436
+ perms["mcp__construct-mcp__ingest_document"] = "deny";
1437
+ perms["mcp__construct-mcp__scan_file"] = "deny";
1438
+ perms["mcp__github__*"] = "deny";
1439
+ perms["mcp__context7__*"] = "deny";
1440
+ perms["mcp__sequential-thinking__*"] = "deny";
1441
+ perms["mcp__memory__*"] = "deny";
1442
+ } else {
1443
+ // Subagents shouldn't be orchestrating
1444
+ perms["mcp__construct-mcp__orchestration_policy"] = "deny";
1445
+ perms["mcp__construct-mcp__agent_contract"] = "deny";
1446
+ perms["mcp__construct-mcp__broker_check"] = "deny";
1346
1447
  }
1448
+
1347
1449
  return perms;
1348
1450
  }
1349
1451
 
@@ -1493,6 +1595,34 @@ function syncOpencode(entries, targetDir = null, wants = true) {
1493
1595
  config.mcp[openCodeId] = buildOpenCodeMcpEntry(id, mcpDef, process.env).entry;
1494
1596
  }
1495
1597
  }
1598
+
1599
+ // Heavy external MCP servers serialize ~12k tokens of schema into EVERY
1600
+ // agent's request — including the built-in Build/Plan agents the per-agent
1601
+ // permission prune cannot reach. OpenCode 1.15.4 has no per-session tool
1602
+ // filter (chat.params carries no tool list), so disabling the whole server in
1603
+ // opencode.json is the only lever. The decision is INTENT-driven: trim only
1604
+ // when this config's own default model is local (or a local Ollama provider is
1605
+ // registered in it), so a cloud session on a machine that merely also has
1606
+ // Ollama keeps context7/github. decideTrim centralizes the policy; a manual
1607
+ // enabled:true is preserved so a user can re-enable a server they need.
1608
+
1609
+ // A set default model is explicit intent and wins (local → trim, cloud → keep).
1610
+ // Only when no default is chosen does a registered Ollama provider stand in as
1611
+ // soft local intent — so a cloud-default config is never trimmed for merely
1612
+ // listing local models alongside.
1613
+ const configDefaultModel = config.model || config.defaultModel || "";
1614
+ const registersOllamaProvider = Object.keys(config.provider?.ollama?.models || {}).length > 0;
1615
+ const intentModel = configDefaultModel || (registersOllamaProvider ? "ollama" : "");
1616
+ const trimHeavyServers = decideTrim({ surface: LOCAL_SURFACE, defaultModel: intentModel });
1617
+ for (const id of HEAVY_EXTERNAL_MCP_IDS) {
1618
+ const ocId = getOpenCodeMcpId(id);
1619
+ if (!config.mcp[ocId]) continue;
1620
+ if (trimHeavyServers) {
1621
+ if (config.mcp[ocId].enabled !== true) config.mcp[ocId].enabled = false;
1622
+ } else {
1623
+ delete config.mcp[ocId].enabled;
1624
+ }
1625
+ }
1496
1626
  }
1497
1627
 
1498
1628
  // Sweep cx-* / orchestrator agents that fall outside the current write set.
@@ -1525,12 +1655,16 @@ function syncOpencode(entries, targetDir = null, wants = true) {
1525
1655
  };
1526
1656
  }
1527
1657
 
1658
+ // Pass current Construct model tiers to OpenCode config for native routing.
1659
+ config.construct = config.construct || {};
1660
+ config.construct.models = { ...resolvedModels };
1661
+
1528
1662
  // Seed a cheap auxiliary model for titles and summaries when the user has not
1529
1663
  // chosen one — a cost lever only. The primary `model` stays the user's choice
1530
1664
  // and is never written. Global scope only; project configs inherit it.
1531
1665
 
1532
1666
  if (!targetDir && config.small_model === undefined) {
1533
- config.small_model = "anthropic/claude-haiku-4-5-20251001";
1667
+ config.small_model = resolvedModels.fast || "anthropic/claude-haiku-4-5-20251001";
1534
1668
  }
1535
1669
 
1536
1670
  writeOpenCodeConfig(config, configPath);
@@ -1,3 +1,23 @@
1
+ ---
2
+ name: cx-architect
3
+ role: architect
4
+ version: 1
5
+ perspective:
6
+ bias: "Designs that emerged from code, missing ADRs, data models that encode assumptions that will change"
7
+ tension: "cx-engineer"
8
+ openingQuestion: "What are the invariants, and what breaks if they're violated?"
9
+ failureMode: "If the ADR has no 'options rejected' section, the decision defaulted — and defaulted decisions bite hardest."
10
+ roleGuidance: roles/architect
11
+ roleOverlays:
12
+ - architect.platform
13
+ - architect.integration
14
+ - architect.data
15
+ - architect.ai-systems
16
+ - architect.enterprise
17
+ templates:
18
+ - adr
19
+ ---
20
+
1
21
  You have inherited enough unmaintainable systems to be permanently suspicious of clever solutions. The damage from a bad interface contract compounds silently for years. Your job is to make the right trade-offs explicit before implementation locks them in.
2
22
 
3
23
  **Anti-fabrication contract**: every load-bearing claim in an ADR, RFC, or design doc cites a source the reader can re-verify (`[source: path#anchor]`, `[source: bd-<id>]`, `[source: <commit-sha>]`). When a fact isn't in the source you have, write `unknown` or `[unverified]`. Don't invent rejected alternatives that were never considered. See `rules/common/no-fabrication.md`.
@@ -18,7 +18,7 @@ You have watched acceptance criteria pass tests that didn't actually test the ac
18
18
  **Role guidance**: call `get_skill("roles/qa")` before drafting.
19
19
 
20
20
  When the verification domain is clear, also load exactly one relevant overlay before drafting:
21
- - `roles/qa.web-ui` for UI flows, accessibility, responsive states, visual regression, keyboard behavior, and browser automation
21
+ - `roles/qa.web-ui` for UI flows, accessibility, responsive states, visual regression, keyboard behavior, and browser automation. Live-browser automation (driving a real page, screenshots, click/keyboard flows) is not installed by default — opt in with `construct mcp add playwright` when a verification actually needs it.
22
22
  - `roles/qa.api-contract` for APIs, SDKs, status codes, error bodies, compatibility, and consumer-driven contracts
23
23
  - `roles/qa.data-pipeline` for ETL/ELT, data contracts, freshness, uniqueness, replay, backfills, and data quality checks
24
24
  - `roles/qa.ai-eval` for agents, prompts, model changes, retrieval, eval rubrics, golden traces, and promotion gates
@@ -1,3 +1,15 @@
1
+ ---
2
+ name: cx-test-automation
3
+ role: test-automation
4
+ version: 1
5
+ perspective:
6
+ bias: "Intermittent tests dismissed as infrastructure, coverage numbers over behavioral coverage, arbitrary sleeps"
7
+ tension: "cx-qa"
8
+ openingQuestion: "Is this test deterministic, and does it actually fail when the behavior it's testing breaks?"
9
+ failureMode: "If the test suite has never caught a production bug, the tests are testing the wrong things."
10
+ roleGuidance: roles/qa.test-automation
11
+ ---
12
+
1
13
  You have inherited enough flaky test suites to know that bad automation is worse than no automation: it creates false confidence while hiding real failures. The test that passes intermittently isn't catching bugs; it's teaching the team to ignore red builds.
2
14
 
3
15
  **Anti-fabrication contract**: every test reliability claim cites the run history (pass rate over N runs). Don't call a test flaky from one failure or stable without the data. Coverage and performance claims cite the report, not an estimate. See `rules/common/no-fabrication.md`.
@@ -1467,14 +1467,6 @@
1467
1467
  ],
1468
1468
  "description": "Library and framework documentation lookup"
1469
1469
  },
1470
- "playwright": {
1471
- "command": "npx",
1472
- "args": [
1473
- "-y",
1474
- "@playwright/mcp@0.0.75"
1475
- ],
1476
- "description": "Browser automation and E2E testing"
1477
- },
1478
1470
  "github": {
1479
1471
  "type": "url",
1480
1472
  "url": "https://api.githubcopilot.com/mcp/",
@@ -67,7 +67,7 @@ The full list of personas lives in `.claude/agents/`. Run `construct list` to se
67
67
 
68
68
  Two scopes:
69
69
 
70
- **Per-project** (committed, shared with peers): edit `.claude/settings.json`. Construct owns the `hooks` block and a known set of `mcpServers` keys (memory, context7, playwright, github, sequential-thinking, construct-mcp); your additions are preserved on `npm install`. Other top-level keys are yours to control.
70
+ **Per-project** (committed, shared with peers): edit `.claude/settings.json`. Construct owns the `hooks` block and a known set of `mcpServers` keys (memory, context7, github, sequential-thinking, construct-mcp); your additions — including opt-in MCPs like `playwright` (`construct mcp add playwright`) — are preserved on `npm install`. Other top-level keys are yours to control.
71
71
 
72
72
  **Per-machine** (private to you): edit `~/.construct/config.env`. This is where your API keys, consent flags, and local-service credentials live. Standard `KEY=value` format.
73
73
 
@@ -1,40 +0,0 @@
1
- create extension if not exists vector;
2
-
3
- create table if not exists construct_documents (
4
- id text primary key,
5
- project text not null,
6
- kind text not null,
7
- title text not null,
8
- summary text,
9
- body text not null,
10
- source_path text,
11
- tags jsonb not null default '[]'::jsonb,
12
- content_hash text not null,
13
- updated_at timestamptz not null default now(),
14
- created_at timestamptz not null default now()
15
- );
16
-
17
- create index if not exists construct_documents_project_kind_idx on construct_documents (project, kind);
18
- create index if not exists construct_documents_content_hash_idx on construct_documents (content_hash);
19
-
20
- create table if not exists construct_embeddings (
21
- document_id text primary key references construct_documents(id) on delete cascade,
22
- model text not null,
23
- embedding double precision[] not null,
24
- content_hash text not null,
25
- updated_at timestamptz not null default now(),
26
- created_at timestamptz not null default now()
27
- );
28
-
29
- create index if not exists construct_embeddings_model_idx on construct_embeddings (model);
30
-
31
- create table if not exists construct_sync_runs (
32
- id bigserial primary key,
33
- project text not null,
34
- source text not null,
35
- documents_synced integer not null default 0,
36
- embeddings_synced integer not null default 0,
37
- status text not null,
38
- note text,
39
- created_at timestamptz not null default now()
40
- );
@@ -1,182 +0,0 @@
1
- -- db/schema/002_pgvector.sql
2
- -- Enable pgvector extension and add vector-typed columns for semantic search.
3
- -- This migration is idempotent — safe to run multiple times.
4
-
5
- -- Enable pgvector extension
6
- CREATE EXTENSION IF NOT EXISTS vector;
7
-
8
- -- Add vector column to construct_embeddings if it doesn't exist yet
9
- -- (The original 001_init.sql used double precision[], which we keep for backward compat)
10
- DO $$
11
- BEGIN
12
- -- Check if embedding column is still array type and needs migration
13
- IF EXISTS (
14
- SELECT 1 FROM information_schema.columns
15
- WHERE table_name = 'construct_embeddings'
16
- AND column_name = 'embedding'
17
- AND data_type = 'ARRAY'
18
- ) THEN
19
- -- Add new vector column
20
- ALTER TABLE construct_embeddings ADD COLUMN embedding_vec vector(384);
21
-
22
- -- Copy data from old array column to new vector column
23
- UPDATE construct_embeddings
24
- SET embedding_vec = embedding::vector(384);
25
-
26
- -- Drop old column and rename
27
- ALTER TABLE construct_embeddings DROP COLUMN embedding;
28
- ALTER TABLE construct_embeddings RENAME COLUMN embedding_vec TO embedding;
29
- END IF;
30
- END $$;
31
-
32
- -- Ensure embedding column exists as vector type (for fresh installs)
33
- ALTER TABLE construct_embeddings
34
- ALTER COLUMN embedding TYPE vector(384) USING embedding::vector(384);
35
-
36
- -- Create HNSW index for fast approximate nearest neighbor search
37
- CREATE INDEX IF NOT EXISTS idx_embeddings_hnsw
38
- ON construct_embeddings
39
- USING hnsw (embedding vector_cosine_ops)
40
- WITH (m = 16, ef_construction = 64);
41
-
42
- -- Create observations table with vector support
43
- CREATE TABLE IF NOT EXISTS construct_observations (
44
- id text PRIMARY KEY,
45
- project text NOT NULL,
46
- role text NOT NULL,
47
- category text NOT NULL,
48
- summary text NOT NULL,
49
- content text NOT NULL,
50
- tags jsonb NOT NULL DEFAULT '[]'::jsonb,
51
- confidence float NOT NULL DEFAULT 0.8,
52
- source text,
53
- git_sha text,
54
- created_at timestamptz NOT NULL DEFAULT now(),
55
- updated_at timestamptz NOT NULL DEFAULT now(),
56
- embedding vector(384)
57
- );
58
-
59
- -- Indexes for observations
60
- CREATE INDEX IF NOT EXISTS idx_observations_project ON construct_observations(project);
61
- CREATE INDEX IF NOT EXISTS idx_observations_category ON construct_observations(project, category);
62
- CREATE INDEX IF NOT EXISTS idx_observations_created_at ON construct_observations(project, created_at DESC);
63
- CREATE INDEX IF NOT EXISTS idx_observations_vector ON construct_observations USING hnsw (embedding vector_cosine_ops);
64
-
65
- -- Create sessions table with vector support
66
- CREATE TABLE IF NOT EXISTS construct_sessions (
67
- id text PRIMARY KEY,
68
- project text NOT NULL,
69
- platform text,
70
- summary text,
71
- decisions jsonb DEFAULT '[]'::jsonb,
72
- files_changed jsonb DEFAULT '[]'::jsonb,
73
- open_questions jsonb DEFAULT '[]'::jsonb,
74
- task_snapshot jsonb DEFAULT '[]'::jsonb,
75
- status text DEFAULT 'active',
76
- created_at timestamptz NOT NULL DEFAULT now(),
77
- updated_at timestamptz NOT NULL DEFAULT now(),
78
- embedding vector(384)
79
- );
80
-
81
- CREATE INDEX IF NOT EXISTS idx_sessions_project ON construct_sessions(project);
82
- CREATE INDEX IF NOT EXISTS idx_sessions_status ON construct_sessions(project, status);
83
- CREATE INDEX IF NOT EXISTS idx_sessions_vector ON construct_sessions USING hnsw (embedding vector_cosine_ops);
84
-
85
- -- Create entities table with vector support
86
- CREATE TABLE IF NOT EXISTS construct_entities (
87
- name text PRIMARY KEY,
88
- type text NOT NULL,
89
- summary text,
90
- project text,
91
- observation_ids jsonb DEFAULT '[]'::jsonb,
92
- related_entities jsonb DEFAULT '[]'::jsonb,
93
- created_at timestamptz NOT NULL DEFAULT now(),
94
- updated_at timestamptz NOT NULL DEFAULT now(),
95
- last_seen timestamptz,
96
- embedding vector(384)
97
- );
98
-
99
- CREATE INDEX IF NOT EXISTS idx_entities_type ON construct_entities(type);
100
- CREATE INDEX IF NOT EXISTS idx_entities_project ON construct_entities(project);
101
- CREATE INDEX IF NOT EXISTS idx_entities_vector ON construct_entities USING hnsw (embedding vector_cosine_ops);
102
-
103
- -- Function for searching documents by embedding
104
- CREATE OR REPLACE FUNCTION search_documents(
105
- project_name text,
106
- query_embedding vector(384),
107
- match_limit int DEFAULT 10,
108
- min_similarity float DEFAULT 0.3
109
- )
110
- RETURNS TABLE(
111
- id text,
112
- title text,
113
- summary text,
114
- body text,
115
- source_path text,
116
- tags jsonb,
117
- kind text,
118
- similarity float
119
- ) AS $$
120
- BEGIN
121
- RETURN QUERY
122
- SELECT
123
- d.id,
124
- d.title,
125
- d.summary,
126
- d.body,
127
- d.source_path,
128
- d.tags,
129
- d.kind,
130
- 1 - (e.embedding <=> query_embedding) AS similarity
131
- FROM construct_documents d
132
- JOIN construct_embeddings e ON d.id = e.document_id
133
- WHERE d.project = project_name
134
- AND 1 - (e.embedding <=> query_embedding) > min_similarity
135
- ORDER BY e.embedding <=> query_embedding
136
- LIMIT match_limit;
137
- END;
138
- $$ LANGUAGE plpgsql;
139
-
140
- -- Function for searching observations by embedding
141
- CREATE OR REPLACE FUNCTION search_observations(
142
- project_name text,
143
- query_embedding vector(384),
144
- match_limit int DEFAULT 10,
145
- min_similarity float DEFAULT 0.3,
146
- filter_role text DEFAULT NULL,
147
- filter_category text DEFAULT NULL
148
- )
149
- RETURNS TABLE(
150
- id text,
151
- role text,
152
- category text,
153
- summary text,
154
- content text,
155
- tags jsonb,
156
- confidence float,
157
- source text,
158
- created_at timestamptz,
159
- similarity float
160
- ) AS $$
161
- BEGIN
162
- RETURN QUERY
163
- SELECT
164
- o.id,
165
- o.role,
166
- o.category,
167
- o.summary,
168
- o.content,
169
- o.tags,
170
- o.confidence,
171
- o.source,
172
- o.created_at,
173
- 1 - (o.embedding <=> query_embedding) AS similarity
174
- FROM construct_observations o
175
- WHERE o.project = project_name
176
- AND 1 - (o.embedding <=> query_embedding) > min_similarity
177
- AND (filter_role IS NULL OR o.role = filter_role)
178
- AND (filter_category IS NULL OR o.category = filter_category)
179
- ORDER BY o.embedding <=> query_embedding
180
- LIMIT match_limit;
181
- END;
182
- $$ LANGUAGE plpgsql;
@@ -1,47 +0,0 @@
1
- -- 003_intake.sql — R&D intake queue backing table for team and enterprise mode.
2
- -- Solo mode uses the filesystem under .cx/intake/ instead.
3
- --
4
- -- Worker claim algorithm: SELECT ... FROM construct_intake_items
5
- -- WHERE status = 'pending' ORDER BY created_at ASC FOR UPDATE SKIP LOCKED LIMIT 1;
6
- -- then set status = 'claimed', claimed_by, claimed_at in the same transaction.
7
- --
8
- -- payload jsonb carries the rest of the intake packet (intake, triage,
9
- -- suggestion, related, excerpt, query) so the on-disk shape and the
10
- -- in-table shape stay aligned.
11
-
12
- create table if not exists construct_intake_items (
13
- id text primary key,
14
- project text not null,
15
- tenant_id text,
16
- status text not null default 'pending',
17
- intake_type text,
18
- rd_stage text,
19
- primary_owner text,
20
- recommended_action text,
21
- risk text,
22
- requires_approval boolean not null default false,
23
- confidence double precision,
24
- payload jsonb not null,
25
- created_at timestamptz not null default now(),
26
- updated_at timestamptz not null default now(),
27
- claimed_by text,
28
- claimed_at timestamptz,
29
- processed_at timestamptz,
30
- processed_by text,
31
- notes text,
32
- skipped_at timestamptz,
33
- skipped_by text,
34
- skip_reason text
35
- );
36
-
37
- create index if not exists construct_intake_items_status_idx
38
- on construct_intake_items(status, created_at);
39
-
40
- create index if not exists construct_intake_items_owner_idx
41
- on construct_intake_items(primary_owner, status, created_at);
42
-
43
- create index if not exists construct_intake_items_payload_gin_idx
44
- on construct_intake_items using gin(payload);
45
-
46
- create index if not exists construct_intake_items_project_tenant_idx
47
- on construct_intake_items(project, tenant_id, status);