@geraldmaron/construct 1.0.24 → 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 (60) hide show
  1. package/bin/construct +185 -3
  2. package/lib/agent-instructions/inject.mjs +25 -4
  3. package/lib/audit-rules.mjs +127 -0
  4. package/lib/audit-skills.mjs +43 -1
  5. package/lib/beads-client.mjs +9 -0
  6. package/lib/beads-optimistic.mjs +23 -71
  7. package/lib/bridges/copilot-proxy.mjs +116 -0
  8. package/lib/cli-commands.mjs +5 -1
  9. package/lib/comment-lint.mjs +1 -1
  10. package/lib/config/schema.mjs +1 -1
  11. package/lib/document-extract/docling-client.mjs +16 -6
  12. package/lib/document-extract/docling-sidecar.py +32 -2
  13. package/lib/document-extract.mjs +37 -10
  14. package/lib/document-ingest.mjs +90 -5
  15. package/lib/embed/roadmap.mjs +16 -1
  16. package/lib/engine/consolidate.mjs +160 -3
  17. package/lib/engine/contradiction-judge.mjs +71 -0
  18. package/lib/engine/contradiction.mjs +74 -0
  19. package/lib/host-capabilities.mjs +30 -0
  20. package/lib/ingest/docling-remote.mjs +90 -0
  21. package/lib/ingest/strategy.mjs +1 -1
  22. package/lib/init-unified.mjs +9 -13
  23. package/lib/logging/rotate.mjs +18 -0
  24. package/lib/mcp/server.mjs +124 -12
  25. package/lib/mcp/tool-budget.mjs +53 -0
  26. package/lib/mcp-catalog.json +1 -1
  27. package/lib/ollama/capability-store.mjs +78 -0
  28. package/lib/ollama/provision-context.mjs +344 -0
  29. package/lib/opencode-config.mjs +148 -0
  30. package/lib/opencode-telemetry.mjs +7 -0
  31. package/lib/orchestration-policy.mjs +41 -6
  32. package/lib/platforms/capabilities.mjs +100 -0
  33. package/lib/prompt-composer.js +12 -8
  34. package/lib/reconcile/agent-instructions-rewrap.mjs +8 -4
  35. package/lib/reflect/extractor.mjs +14 -1
  36. package/lib/reflect/salience.mjs +65 -0
  37. package/lib/rules-delivery.mjs +122 -0
  38. package/lib/runtime/uv-bootstrap.mjs +32 -17
  39. package/lib/service-manager.mjs +41 -3
  40. package/lib/setup.mjs +21 -2
  41. package/lib/specialists/prompt-schema.mjs +162 -0
  42. package/lib/specialists/scaffold.mjs +109 -0
  43. package/lib/storage/embeddings-engine.mjs +19 -5
  44. package/lib/telemetry/beads-fallback.mjs +40 -0
  45. package/lib/telemetry/hook-calls.mjs +138 -0
  46. package/package.json +1 -1
  47. package/personas/construct.md +1 -1
  48. package/platforms/capabilities.json +76 -0
  49. package/platforms/opencode/sync-config.mjs +121 -25
  50. package/rules/common/neurodivergent-output.md +1 -1
  51. package/rules/web/coding-style.md +8 -0
  52. package/rules/web/design-quality.md +8 -0
  53. package/rules/web/hooks.md +8 -0
  54. package/rules/web/patterns.md +8 -0
  55. package/rules/web/performance.md +8 -0
  56. package/rules/web/security.md +8 -0
  57. package/rules/web/testing.md +8 -0
  58. package/scripts/sync-specialists.mjs +139 -39
  59. package/specialists/prompts/cx-architect.md +20 -0
  60. package/specialists/prompts/cx-test-automation.md +12 -0
@@ -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,18 +832,26 @@ function makeHooksPortable(hooksJson) {
800
832
  return JSON.stringify(walk(hooksJson));
801
833
  }
802
834
 
803
- const GLOBAL_CLAUDE_HOOK_IDS = new Set([
804
- 'pre:bash:block-no-verify',
805
- 'pre:bash:guard-dangerous',
806
- 'pre:edit:config-protection',
807
- 'pre:edit-guard',
808
- 'post:edit:json-validate',
809
- 'post:edit:scan-secrets',
810
- ]);
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.
811
845
 
812
- const GLOBAL_CLAUDE_MCP_IDS = new Set([
813
- 'context7',
814
- ]);
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
+ })();
815
855
 
816
856
  function filterGlobalClaudeHooks(hooksJson) {
817
857
  const filtered = {};
@@ -871,6 +911,7 @@ function writeProjectClaudeSettings(targetDir) {
871
911
  existing.mcpServers ??= {};
872
912
  for (const [id, mcpDef] of Object.entries(template.mcpServers)) {
873
913
  if (existing.mcpServers[id]) continue;
914
+ if (!PROJECT_DEFAULT_MCP_IDS.has(id)) continue;
874
915
  existing.mcpServers[id] = mcpDef;
875
916
  }
876
917
  }
@@ -886,6 +927,7 @@ function writeProjectClaudeSettings(targetDir) {
886
927
  existing.mcpServers ??= {};
887
928
  for (const [id, mcpDef] of Object.entries(registryMcp)) {
888
929
  if (existing.mcpServers[id]) continue;
930
+ if (!PROJECT_DEFAULT_MCP_IDS.has(id)) continue;
889
931
  existing.mcpServers[id] = buildClaudeMcpEntry(id, mcpDef, process.env);
890
932
  }
891
933
 
@@ -1347,6 +1389,15 @@ function syncCursor(targetDir = null, wants = true) {
1347
1389
  fs.writeFileSync(rulesPath, body);
1348
1390
  }
1349
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
+ }
1350
1401
  }
1351
1402
  return true;
1352
1403
  }
@@ -1362,22 +1413,39 @@ function opencodePermissions(entry) {
1362
1413
  ? Object.fromEntries(Object.entries(entry.permissions).map(([k, v]) => [k, v]))
1363
1414
  : { edit: entry.canEdit === false ? "deny" : "allow", bash: "allow" };
1364
1415
 
1365
- // The orchestrator is the primary write surface; translate its abstract
1366
- // bash:allow into OpenCode's scoped permission map so destructive commands are
1367
- // denied and remote/history rewrites prompt the commit-approval contract
1368
- // enforced at the tool layer, not just in the prompt. Subagents keep the
1369
- // abstract grant. This scoped shape is an OpenCode translation detail and stays
1370
- // out of the cross-adapter registry.
1371
-
1372
- if (entry.isOrchestrator && perms.bash === "allow") {
1373
- perms.bash = {
1374
- "*": "allow",
1375
- "rm -rf *": "deny",
1376
- "git push *": "ask",
1377
- "git push --force*": "ask",
1378
- "git reset --hard *": "ask",
1379
- };
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";
1380
1447
  }
1448
+
1381
1449
  return perms;
1382
1450
  }
1383
1451
 
@@ -1527,6 +1595,34 @@ function syncOpencode(entries, targetDir = null, wants = true) {
1527
1595
  config.mcp[openCodeId] = buildOpenCodeMcpEntry(id, mcpDef, process.env).entry;
1528
1596
  }
1529
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
+ }
1530
1626
  }
1531
1627
 
1532
1628
  // Sweep cx-* / orchestrator agents that fall outside the current write set.
@@ -1559,12 +1655,16 @@ function syncOpencode(entries, targetDir = null, wants = true) {
1559
1655
  };
1560
1656
  }
1561
1657
 
1658
+ // Pass current Construct model tiers to OpenCode config for native routing.
1659
+ config.construct = config.construct || {};
1660
+ config.construct.models = { ...resolvedModels };
1661
+
1562
1662
  // Seed a cheap auxiliary model for titles and summaries when the user has not
1563
1663
  // chosen one — a cost lever only. The primary `model` stays the user's choice
1564
1664
  // and is never written. Global scope only; project configs inherit it.
1565
1665
 
1566
1666
  if (!targetDir && config.small_model === undefined) {
1567
- config.small_model = "anthropic/claude-haiku-4-5-20251001";
1667
+ config.small_model = resolvedModels.fast || "anthropic/claude-haiku-4-5-20251001";
1568
1668
  }
1569
1669
 
1570
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`.
@@ -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`.