@geraldmaron/construct 1.0.24 → 1.1.1

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 (87) hide show
  1. package/README.md +2 -0
  2. package/bin/construct +266 -13
  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/audit-trail.mjs +77 -10
  7. package/lib/beads-client.mjs +9 -0
  8. package/lib/beads-optimistic.mjs +23 -71
  9. package/lib/bridges/copilot-proxy.mjs +116 -0
  10. package/lib/cli-commands.mjs +112 -1
  11. package/lib/comment-lint.mjs +1 -1
  12. package/lib/config/schema.mjs +1 -1
  13. package/lib/demo.mjs +245 -0
  14. package/lib/diagram.mjs +300 -0
  15. package/lib/doctor/index.mjs +1 -1
  16. package/lib/doctor/watchers/process-pressure.mjs +1 -1
  17. package/lib/doctor/watchers/service-health.mjs +1 -1
  18. package/lib/document-extract/docling-client.mjs +16 -6
  19. package/lib/document-extract/docling-sidecar.py +32 -2
  20. package/lib/document-extract.mjs +85 -10
  21. package/lib/document-ingest.mjs +97 -7
  22. package/lib/embed/roadmap.mjs +16 -1
  23. package/lib/embed/semantic.mjs +5 -2
  24. package/lib/engine/consolidate.mjs +160 -3
  25. package/lib/engine/contradiction-judge.mjs +71 -0
  26. package/lib/engine/contradiction.mjs +74 -0
  27. package/lib/handoffs/cleanup.mjs +1 -1
  28. package/lib/hooks/audit-trail.mjs +14 -28
  29. package/lib/host-capabilities.mjs +30 -0
  30. package/lib/ingest/docling-remote.mjs +90 -0
  31. package/lib/ingest/strategy.mjs +1 -1
  32. package/lib/init-unified.mjs +9 -13
  33. package/lib/logging/rotate.mjs +18 -0
  34. package/lib/mcp/server.mjs +124 -12
  35. package/lib/mcp/tool-budget.mjs +53 -0
  36. package/lib/mcp-catalog.json +1 -1
  37. package/lib/model-router.mjs +52 -1
  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-policy.mjs +41 -6
  43. package/lib/persona-sections.mjs +66 -0
  44. package/lib/platforms/capabilities.mjs +100 -0
  45. package/lib/prompt-composer.js +14 -9
  46. package/lib/reconcile/agent-instructions-rewrap.mjs +8 -4
  47. package/lib/reflect/extractor.mjs +14 -1
  48. package/lib/reflect/salience.mjs +65 -0
  49. package/lib/rules-delivery.mjs +122 -0
  50. package/lib/runtime/uv-bootstrap.mjs +32 -17
  51. package/lib/server/index.mjs +1 -1
  52. package/lib/server/langfuse-login.mjs +3 -3
  53. package/lib/service-manager.mjs +42 -4
  54. package/lib/session-store.mjs +1 -1
  55. package/lib/setup.mjs +58 -2
  56. package/lib/specialists/prompt-schema.mjs +162 -0
  57. package/lib/specialists/scaffold.mjs +109 -0
  58. package/lib/storage/embeddings-engine.mjs +19 -5
  59. package/lib/storage/embeddings-local.mjs +6 -3
  60. package/lib/storage/file-lock.mjs +18 -11
  61. package/lib/telemetry/beads-fallback.mjs +40 -0
  62. package/lib/telemetry/hook-calls.mjs +138 -0
  63. package/package.json +4 -2
  64. package/personas/construct.md +12 -12
  65. package/platforms/capabilities.json +76 -0
  66. package/platforms/opencode/sync-config.mjs +121 -25
  67. package/rules/common/neurodivergent-output.md +1 -1
  68. package/rules/web/coding-style.md +8 -0
  69. package/rules/web/design-quality.md +8 -0
  70. package/rules/web/hooks.md +8 -0
  71. package/rules/web/patterns.md +8 -0
  72. package/rules/web/performance.md +8 -0
  73. package/rules/web/security.md +8 -0
  74. package/rules/web/testing.md +8 -0
  75. package/scripts/sync-specialists.mjs +277 -63
  76. package/skills/docs/init-project.md +1 -1
  77. package/specialists/prompts/cx-architect.md +20 -0
  78. package/specialists/prompts/cx-test-automation.md +12 -0
  79. package/templates/docs/construct_guide.md +2 -2
  80. package/lib/ingest/chunker.mjs +0 -94
  81. package/lib/ingest/pipeline.mjs +0 -53
  82. package/lib/ingest/store.mjs +0 -82
  83. package/lib/mode-commands.mjs +0 -122
  84. package/lib/policy/unified-gates.mjs +0 -96
  85. package/lib/profiles/validate-custom.mjs +0 -114
  86. package/lib/services/telemetry-backend.mjs +0 -177
  87. package/lib/storage/fusion.mjs +0 -95
@@ -28,7 +28,7 @@ Use headings in order (H2, then H3) and do not skip or nest deeper. One `h1`-equ
28
28
 
29
29
  ## 3. Scannable chunks
30
30
 
31
- Short paragraphs. Bulleted or numbered lists in place of long sentences that pack several facts together. White space between sections. Prefer three short lines over one dense one.
31
+ Short paragraphs with white space between sections; prefer three short lines over one dense one. Use a bulleted or numbered list for genuinely parallel items — a set of options, steps, or independent facts — where scanning is the point. Keep reasoning, cause-and-effect, and narrative in prose: a wall of bullets fragments the logic that connects the points and is harder to follow, not easier. Bullets for what scans, sentences for what reasons.
32
32
 
33
33
  ## 4. Plain, literal language
34
34
 
@@ -1,5 +1,13 @@
1
1
  ---
2
2
  description: Construct web coding style rule. Use when writing or reviewing web code that involves coding style.
3
+ paths:
4
+ - "**/*.tsx"
5
+ - "**/*.jsx"
6
+ - "**/*.css"
7
+ - "**/*.scss"
8
+ - "**/*.html"
9
+ - "**/*.vue"
10
+ - "**/*.svelte"
3
11
  ---
4
12
  > This file extends [common/coding-style.md](../common/coding-style.md) with web-specific frontend content.
5
13
 
@@ -1,5 +1,13 @@
1
1
  ---
2
2
  description: Construct web design quality rule. Use when writing or reviewing web code that involves design quality.
3
+ paths:
4
+ - "**/*.tsx"
5
+ - "**/*.jsx"
6
+ - "**/*.css"
7
+ - "**/*.scss"
8
+ - "**/*.html"
9
+ - "**/*.vue"
10
+ - "**/*.svelte"
3
11
  ---
4
12
  > This file extends [common/patterns.md](../common/patterns.md) with web-specific design-quality guidance.
5
13
 
@@ -1,5 +1,13 @@
1
1
  ---
2
2
  description: Prefer project-local tooling.
3
+ paths:
4
+ - "**/*.tsx"
5
+ - "**/*.jsx"
6
+ - "**/*.css"
7
+ - "**/*.scss"
8
+ - "**/*.html"
9
+ - "**/*.vue"
10
+ - "**/*.svelte"
3
11
  ---
4
12
  # Web Hooks
5
13
 
@@ -1,5 +1,13 @@
1
1
  ---
2
2
  description: Construct web patterns rule. Use when writing or reviewing web code that involves patterns.
3
+ paths:
4
+ - "**/*.tsx"
5
+ - "**/*.jsx"
6
+ - "**/*.css"
7
+ - "**/*.scss"
8
+ - "**/*.html"
9
+ - "**/*.vue"
10
+ - "**/*.svelte"
3
11
  ---
4
12
  > This file extends [common/patterns.md](../common/patterns.md) with web-specific patterns.
5
13
 
@@ -1,5 +1,13 @@
1
1
  ---
2
2
  description: Construct web performance rule. Use when writing or reviewing web code that involves performance.
3
+ paths:
4
+ - "**/*.tsx"
5
+ - "**/*.jsx"
6
+ - "**/*.css"
7
+ - "**/*.scss"
8
+ - "**/*.html"
9
+ - "**/*.vue"
10
+ - "**/*.svelte"
3
11
  ---
4
12
  > This file extends [common/performance.md](../common/performance.md) with web-specific performance content.
5
13
 
@@ -1,5 +1,13 @@
1
1
  ---
2
2
  description: Construct web security rule. Use when writing or reviewing web code that involves security.
3
+ paths:
4
+ - "**/*.tsx"
5
+ - "**/*.jsx"
6
+ - "**/*.css"
7
+ - "**/*.scss"
8
+ - "**/*.html"
9
+ - "**/*.vue"
10
+ - "**/*.svelte"
3
11
  ---
4
12
  > This file extends [common/security.md](../common/security.md) with web-specific security content.
5
13
 
@@ -1,5 +1,13 @@
1
1
  ---
2
2
  description: Construct web testing rule. Use when writing or reviewing web code that involves testing.
3
+ paths:
4
+ - "**/*.tsx"
5
+ - "**/*.jsx"
6
+ - "**/*.css"
7
+ - "**/*.scss"
8
+ - "**/*.html"
9
+ - "**/*.vue"
10
+ - "**/*.svelte"
3
11
  ---
4
12
  > This file extends [common/testing.md](../common/testing.md) with web-specific testing content.
5
13
 
@@ -44,7 +44,11 @@ import {
44
44
  writeCodexConfig,
45
45
  } from "../lib/codex-config.mjs";
46
46
  import { findOpenCodeConfigPath, readOpenCodeConfig, writeOpenCodeConfig } from "../lib/opencode-config.mjs";
47
- import { resolvePromptContract } from "../lib/prompt-composer.js";
47
+ import { HEAVY_EXTERNAL_MCP_IDS, LOCAL_SURFACE_MODES, decideTrim, isLocalModel } from "../lib/mcp/tool-budget.mjs";
48
+ import { emitCursorRules } from "../lib/rules-delivery.mjs";
49
+ import { resolvePromptContract, readPromptBody } from "../lib/prompt-composer.js";
50
+ import { renderPersonaForTier } from "../lib/persona-sections.mjs";
51
+ import { getModelVerdict } from "../lib/ollama/capability-store.mjs";
48
52
  import {
49
53
  buildClaudeMcpEntry,
50
54
  buildOpenCodeMcpEntry,
@@ -54,7 +58,7 @@ import { loadConstructEnv } from "../lib/env-config.mjs";
54
58
  import { inlineRoleAntiPatterns, PROMPT_WORD_CAP } from "../lib/role-preload.mjs";
55
59
  import { loadManifest } from "../lib/roles/manifest.mjs";
56
60
  import { resolveActiveProfile } from "../lib/profiles/loader.mjs";
57
- import { resolveTiersForPrimary } from "../lib/model-router.mjs";
61
+ import { resolveTiersForPrimary, resolveCapabilityTier, selectLocalEditorModel } from "../lib/model-router.mjs";
58
62
  import { stampFrontmatter } from "../lib/doc-stamp.mjs";
59
63
  import { buildSkillFrontmatter, stripLeadingFrontmatter } from "../lib/sync/skill-frontmatter.mjs";
60
64
 
@@ -157,6 +161,19 @@ const COMPRESS_PERSONAS = process.argv.includes("--compress-personas");
157
161
  const PROJECT_FLAG = process.argv.includes("--project");
158
162
  const GLOBAL_FLAG = process.argv.includes("--global");
159
163
 
164
+ // --local-surface=on|off|auto controls whether the heavy external MCP servers are
165
+ // disabled to fit a small local-model window. `auto` (default) trims only when the
166
+ // config's own default model is local — so a cloud session keeps context7/github
167
+ // even on a machine that also has Ollama. `on` forces the trim (the lever for users
168
+ // who pick a local model at runtime, leaving the config default unset); `off` keeps
169
+ // every server. CONSTRUCT_LOCAL_SURFACE is the env equivalent.
170
+
171
+ const LOCAL_SURFACE = (() => {
172
+ const arg = process.argv.find((a) => a.startsWith("--local-surface="));
173
+ const raw = (arg ? arg.slice("--local-surface=".length) : process.env.CONSTRUCT_LOCAL_SURFACE || "auto").trim().toLowerCase();
174
+ return LOCAL_SURFACE_MODES.includes(raw) ? raw : "auto";
175
+ })();
176
+
160
177
  // --quiet suppresses only the closing one-line summary, not the work or any
161
178
  // warning. `construct install` runs the global tier twice (plain `sync` then
162
179
  // `sync --global`); in a non-project cwd both land in the same global branch and
@@ -173,8 +190,13 @@ const summary = (msg) => { if (!QUIET) console.log(msg); };
173
190
  // Absent → null → write every host, preserving `construct sync` back-compat.
174
191
 
175
192
  import { detectHostCapabilities } from "../lib/host-capabilities.mjs";
176
-
177
- const HOST_KEYS = ["claude", "codex", "copilot", "opencode", "vscode", "cursor"];
193
+ import {
194
+ HOST_KEYS,
195
+ displayNameToKey,
196
+ hasNativeSubagents as hostHasNativeSubagents,
197
+ globalHookAllowlist,
198
+ globalMcpAllowlist,
199
+ } from "../lib/platforms/capabilities.mjs";
178
200
 
179
201
  function parseHostSelection() {
180
202
  const arg = process.argv.find((a) => a.startsWith("--hosts="));
@@ -182,14 +204,7 @@ function parseHostSelection() {
182
204
  if (!raw) {
183
205
  // Default to detected hosts if none are explicitly requested.
184
206
  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
- };
207
+ const nameToKey = displayNameToKey();
193
208
  try {
194
209
  for (const cap of detectHostCapabilities()) {
195
210
  if (cap.availability === "installed" && nameToKey[cap.host]) {
@@ -427,7 +442,7 @@ const hardDefaults = {
427
442
  // primary's provider. Explicit CX_MODEL_* env wins if set.
428
443
  const primaryFromOpenCode = (() => {
429
444
  try {
430
- const cfg = readOpenCodeConfig(findOpenCodeConfigPath()) ?? {};
445
+ const cfg = readOpenCodeConfig().config ?? {};
431
446
  return cfg.model || cfg.defaultModel || null;
432
447
  } catch { return null; }
433
448
  })();
@@ -589,7 +604,83 @@ export function renderRoleFrameworkSection(entry) {
589
604
  return `\n\n${lines.join("\n")}`;
590
605
  }
591
606
 
592
- function buildPrompt(entry, allEntries, platform) {
607
+ // The native-subagent orchestration micro-prompt. A worked tool-call example lifts
608
+ // small local models' tool-use reliability sharply (bead construct-c16l). Shared by the
609
+ // full path and the capability-tiered local path so both stay in sync.
610
+
611
+ const ORCHESTRATION_MICRO_PROMPT =
612
+ `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` +
613
+ `Example — the user says "add rate limiting to the API". Your first action is a tool call, not prose:\n` +
614
+ ` call orchestration_policy { "task": "add rate limiting to the API" }\n` +
615
+ `Then dispatch the specialists it returns. Always call the tool before answering.`;
616
+
617
+ // Directive for the local editor agent (construct-local). It executes bounded work on
618
+ // the cheap local model and hands planning/reasoning back to the construct architect —
619
+ // the aider architect/editor split. Kept short: a small model must actually obey it.
620
+
621
+ const LOCAL_EDITOR_DIRECTIVE =
622
+ `You are a focused execution agent running on a local model, dispatched by construct to do one bounded job. Do well-scoped edits for the current task and verify them; make the smallest correct change, never a broad rewrite.\n` +
623
+ `You do NOT plan, classify, orchestrate, or spawn other agents. For anything needing multi-file design, architecture or security judgment, dependency or contract changes, or research, STOP and return control to construct — report what needs deeper work and why, rather than attempting it yourself.`;
624
+
625
+ // Warn-and-emit capability advisory. Sizing already consumes the probe verdict
626
+ // (COLLAPSED → floor tier via resolveCapabilityTier); this only nudges the user toward a
627
+ // measured verdict and never suppresses emission. Notice-only, so it auto-suppresses in
628
+ // CI / test / non-TTY per the repo's wrong-context rule — no skip env var.
629
+
630
+ const localAdvisorySeen = new Set();
631
+ function adviseLocalModelCapability(model) {
632
+ if (!model || !isLocalModel(model)) return;
633
+ if (process.env.CI === "true" || process.env.NODE_ENV === "test" || !process.stderr.isTTY) return;
634
+ if (localAdvisorySeen.has(model)) return;
635
+ localAdvisorySeen.add(model);
636
+ const verdict = getModelVerdict(model)?.verdict ?? null;
637
+ if (verdict === "COLLAPSED") {
638
+ console.warn(`[sync] ${model} probed COLLAPSED — emitting at the floor tier with escalation to construct. Re-probe after a Modelfile change: construct doctor --probe-local`);
639
+ } else if (!verdict) {
640
+ console.warn(`[sync] ${model} is local with no coherence verdict — tier inferred from parameter count. For a measured tier: construct doctor --probe-local`);
641
+ }
642
+ }
643
+
644
+ function enforcePromptWordCap(prompt, entry) {
645
+ const wordCount = prompt.split(/\s+/).filter(Boolean).length;
646
+ const effectiveCap = Number(entry.wordCapOverride) > 0 ? entry.wordCapOverride : PROMPT_WORD_CAP;
647
+ if (wordCount > effectiveCap) {
648
+ const msg = `[sync] ${entry.name}: prompt is ${wordCount} words (cap ${effectiveCap})`;
649
+ if (process.env.CONSTRUCT_SYNC_FORCE === '1' || process.argv.includes('--force')) {
650
+ console.warn(`${msg} — proceeding due to --force / CONSTRUCT_SYNC_FORCE=1.`);
651
+ } else {
652
+ console.error(`${msg}`);
653
+ console.error(
654
+ `[sync] Hard cap exceeded. Options:\n` +
655
+ ` - trim the prompt body or move detail to a skill (preferred)\n` +
656
+ ` - set "wordCapOverride": <N> on this entry in specialists/registry.json with a written reason\n` +
657
+ ` - re-run with --force or CONSTRUCT_SYNC_FORCE=1 as a temporary escape hatch\n` +
658
+ `Prompt budget is a hard contract because every over-cap agent degrades every session that dispatches it.`,
659
+ );
660
+ process.exit(1);
661
+ }
662
+ }
663
+ return prompt;
664
+ }
665
+
666
+ function buildPrompt(entry, allEntries, platform, { capabilityTier = 'full' } = {}) {
667
+ const capabilities = { hasNativeSubagents: HOST_KEYS.includes(platform) ? hostHasNativeSubagents(platform) : false };
668
+
669
+ // Capability-tiered local path. A small local model follows a long multi-instruction
670
+ // persona poorly (instruction-following degrades before the window fills), so emit
671
+ // only the persona sections at/below its tier plus the orchestration micro-prompt, and
672
+ // skip the role footer, role-framework, operating-guidance, and model-family blocks —
673
+ // those add instruction load the model cannot track. Cloud models resolve to 'full'
674
+ // and take the unchanged path below, so cloud configs are never slimmed.
675
+
676
+ if (capabilityTier && capabilityTier !== 'full' && entry.promptFile) {
677
+ let slim = renderPersonaForTier(readPromptBody(entry.promptFile, root), capabilityTier);
678
+ if (entry.injectAgentRoster && capabilities.hasNativeSubagents) {
679
+ slim = `${ORCHESTRATION_MICRO_PROMPT}\n\n${slim}`;
680
+ }
681
+ return enforcePromptWordCap(slim, entry);
682
+ }
683
+
593
684
  let prompt = resolvePromptContract(entry, {
594
685
  rootDir: root,
595
686
  registry,
@@ -598,9 +689,19 @@ function buildPrompt(entry, allEntries, platform) {
598
689
 
599
690
  prompt = inlineRoleAntiPatterns(prompt, root, entry.name, console.warn, { preload: entry.preloadRoleGuidance === true });
600
691
 
601
- if (entry.injectAgentRoster && allEntries) {
692
+ // Platform-Native Orchestration Alignment (ADR-0002). Hosts with native subagent
693
+ // routing (OpenCode, VS Code, Cursor) do not get the static specialist roster
694
+ // injected — on a small-context local model the roster alone is ~3-4k tokens and,
695
+ // combined with MCP tool schemas, overruns the model's real context window and
696
+ // collapses output. Those hosts get a tool-bound micro-prompt instead and resolve
697
+ // the chain at runtime via the orchestration_policy MCP tool. Hosts without native
698
+ // routing (Claude Code, Codex) still need the roster to simulate handoffs in text.
699
+
700
+ if (entry.injectAgentRoster && allEntries && !capabilities.hasNativeSubagents) {
602
701
  const roster = buildAgentRoster(allEntries);
603
702
  prompt = `Available specialist agents:\n${roster}\n\n${prompt}`;
703
+ } else if (entry.injectAgentRoster && capabilities.hasNativeSubagents) {
704
+ prompt = `${ORCHESTRATION_MICRO_PROMPT}\n\n${prompt}`;
604
705
  }
605
706
 
606
707
  prompt += buildRoleFooter(entry);
@@ -616,26 +717,7 @@ function buildPrompt(entry, allEntries, platform) {
616
717
 
617
718
  prompt += buildModelGuidanceBlock(entry);
618
719
 
619
- const wordCount = prompt.split(/\s+/).filter(Boolean).length;
620
- const effectiveCap = Number(entry.wordCapOverride) > 0 ? entry.wordCapOverride : PROMPT_WORD_CAP;
621
- if (wordCount > effectiveCap) {
622
- const msg = `[sync] ${entry.name}: prompt is ${wordCount} words (cap ${effectiveCap})`;
623
- if (process.env.CONSTRUCT_SYNC_FORCE === '1' || process.argv.includes('--force')) {
624
- console.warn(`${msg} — proceeding due to --force / CONSTRUCT_SYNC_FORCE=1.`);
625
- } else {
626
- console.error(`${msg}`);
627
- console.error(
628
- `[sync] Hard cap exceeded. Options:\n` +
629
- ` - trim the prompt body or move detail to a skill (preferred)\n` +
630
- ` - set "wordCapOverride": <N> on this entry in specialists/registry.json with a written reason\n` +
631
- ` - re-run with --force or CONSTRUCT_SYNC_FORCE=1 as a temporary escape hatch\n` +
632
- `Prompt budget is a hard contract because every over-cap agent degrades every session that dispatches it.`,
633
- );
634
- process.exit(1);
635
- }
636
- }
637
-
638
- return prompt;
720
+ return enforcePromptWordCap(prompt, entry);
639
721
  }
640
722
 
641
723
  function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }
@@ -800,18 +882,26 @@ function makeHooksPortable(hooksJson) {
800
882
  return JSON.stringify(walk(hooksJson));
801
883
  }
802
884
 
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
- ]);
885
+ const GLOBAL_CLAUDE_HOOK_IDS = globalHookAllowlist('claude');
811
886
 
812
- const GLOBAL_CLAUDE_MCP_IDS = new Set([
813
- 'context7',
814
- ]);
887
+ const GLOBAL_CLAUDE_MCP_IDS = globalMcpAllowlist('claude');
888
+
889
+ // Project scope writes only core-category MCP servers (plus construct-mcp, the
890
+ // orchestration server the specialist loop needs). optional/integration servers
891
+ // (memory, github, sequential-thinking, playwright, …) are opt-in via
892
+ // `construct mcp add` so a project does not silently inherit heavy servers it was
893
+ // never asked for (ADR-0031 §Consequences follow-up). A server already present in
894
+ // the project settings is preserved, so a manual opt-in sticks.
895
+
896
+ const PROJECT_DEFAULT_MCP_IDS = (() => {
897
+ try {
898
+ const catalog = JSON.parse(fs.readFileSync(path.join(root, "lib", "mcp-catalog.json"), "utf8"));
899
+ const arr = catalog.mcps || catalog.servers || [];
900
+ return new Set([...arr.filter((m) => m.category === "core").map((m) => m.id), "construct-mcp"]);
901
+ } catch {
902
+ return new Set(["context7", "construct-mcp"]);
903
+ }
904
+ })();
815
905
 
816
906
  function filterGlobalClaudeHooks(hooksJson) {
817
907
  const filtered = {};
@@ -871,6 +961,7 @@ function writeProjectClaudeSettings(targetDir) {
871
961
  existing.mcpServers ??= {};
872
962
  for (const [id, mcpDef] of Object.entries(template.mcpServers)) {
873
963
  if (existing.mcpServers[id]) continue;
964
+ if (!PROJECT_DEFAULT_MCP_IDS.has(id)) continue;
874
965
  existing.mcpServers[id] = mcpDef;
875
966
  }
876
967
  }
@@ -886,6 +977,7 @@ function writeProjectClaudeSettings(targetDir) {
886
977
  existing.mcpServers ??= {};
887
978
  for (const [id, mcpDef] of Object.entries(registryMcp)) {
888
979
  if (existing.mcpServers[id]) continue;
980
+ if (!PROJECT_DEFAULT_MCP_IDS.has(id)) continue;
889
981
  existing.mcpServers[id] = buildClaudeMcpEntry(id, mcpDef, process.env);
890
982
  }
891
983
 
@@ -1347,6 +1439,15 @@ function syncCursor(targetDir = null, wants = true) {
1347
1439
  fs.writeFileSync(rulesPath, body);
1348
1440
  }
1349
1441
  }
1442
+
1443
+ // Glob-scoped language rules land as managed per-rule .mdc files only when
1444
+ // the project's own files match their globs — Cursor's native auto-attach
1445
+ // convention. See docs/concepts/rules-delivery.md.
1446
+ try {
1447
+ emitCursorRules({ rulesDir: path.join(root, "rules"), targetDir, dryRun: DRY_RUN });
1448
+ } catch (err) {
1449
+ console.warn(`[sync] cursor rules delivery skipped: ${err.message}`);
1450
+ }
1350
1451
  }
1351
1452
  return true;
1352
1453
  }
@@ -1362,22 +1463,39 @@ function opencodePermissions(entry) {
1362
1463
  ? Object.fromEntries(Object.entries(entry.permissions).map(([k, v]) => [k, v]))
1363
1464
  : { edit: entry.canEdit === false ? "deny" : "allow", bash: "allow" };
1364
1465
 
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
- };
1466
+ // Agentic Scope Reduction (ADR-0002). Serializing 100+ MCP tool schemas into a
1467
+ // small local model's prompt overruns its context window and dilutes attention,
1468
+ // collapsing output. OpenCode's per-agent permission map prunes the surface: the
1469
+ // orchestrator keeps only orchestration + core tools and hands execution to
1470
+ // subagents; subagents keep execution tools but not orchestration.
1471
+
1472
+ if (entry.isOrchestrator) {
1473
+ if (perms.bash === "allow") {
1474
+ perms.bash = {
1475
+ "*": "allow",
1476
+ "rm -rf *": "deny",
1477
+ "git push *": "ask",
1478
+ "git push --force*": "ask",
1479
+ "git reset --hard *": "ask",
1480
+ };
1481
+ }
1482
+ // Heavy execution and external-knowledge tools are denied to the orchestrator so
1483
+ // its serialized tool schema stays small; orchestration_policy drives the handoff.
1484
+
1485
+ perms["mcp__construct-mcp__extract_document_text"] = "deny";
1486
+ perms["mcp__construct-mcp__ingest_document"] = "deny";
1487
+ perms["mcp__construct-mcp__scan_file"] = "deny";
1488
+ perms["mcp__github__*"] = "deny";
1489
+ perms["mcp__context7__*"] = "deny";
1490
+ perms["mcp__sequential-thinking__*"] = "deny";
1491
+ perms["mcp__memory__*"] = "deny";
1492
+ } else {
1493
+ // Subagents shouldn't be orchestrating
1494
+ perms["mcp__construct-mcp__orchestration_policy"] = "deny";
1495
+ perms["mcp__construct-mcp__agent_contract"] = "deny";
1496
+ perms["mcp__construct-mcp__broker_check"] = "deny";
1380
1497
  }
1498
+
1381
1499
  return perms;
1382
1500
  }
1383
1501
 
@@ -1527,6 +1645,34 @@ function syncOpencode(entries, targetDir = null, wants = true) {
1527
1645
  config.mcp[openCodeId] = buildOpenCodeMcpEntry(id, mcpDef, process.env).entry;
1528
1646
  }
1529
1647
  }
1648
+
1649
+ // Heavy external MCP servers serialize ~12k tokens of schema into EVERY
1650
+ // agent's request — including the built-in Build/Plan agents the per-agent
1651
+ // permission prune cannot reach. OpenCode 1.15.4 has no per-session tool
1652
+ // filter (chat.params carries no tool list), so disabling the whole server in
1653
+ // opencode.json is the only lever. The decision is INTENT-driven: trim only
1654
+ // when this config's own default model is local (or a local Ollama provider is
1655
+ // registered in it), so a cloud session on a machine that merely also has
1656
+ // Ollama keeps context7/github. decideTrim centralizes the policy; a manual
1657
+ // enabled:true is preserved so a user can re-enable a server they need.
1658
+
1659
+ // A set default model is explicit intent and wins (local → trim, cloud → keep).
1660
+ // Only when no default is chosen does a registered Ollama provider stand in as
1661
+ // soft local intent — so a cloud-default config is never trimmed for merely
1662
+ // listing local models alongside.
1663
+ const configDefaultModel = config.model || config.defaultModel || "";
1664
+ const registersOllamaProvider = Object.keys(config.provider?.ollama?.models || {}).length > 0;
1665
+ const intentModel = configDefaultModel || (registersOllamaProvider ? "ollama" : "");
1666
+ const trimHeavyServers = decideTrim({ surface: LOCAL_SURFACE, defaultModel: intentModel });
1667
+ for (const id of HEAVY_EXTERNAL_MCP_IDS) {
1668
+ const ocId = getOpenCodeMcpId(id);
1669
+ if (!config.mcp[ocId]) continue;
1670
+ if (trimHeavyServers) {
1671
+ if (config.mcp[ocId].enabled !== true) config.mcp[ocId].enabled = false;
1672
+ } else {
1673
+ delete config.mcp[ocId].enabled;
1674
+ }
1675
+ }
1530
1676
  }
1531
1677
 
1532
1678
  // Sweep cx-* / orchestrator agents that fall outside the current write set.
@@ -1542,6 +1688,20 @@ function syncOpencode(entries, targetDir = null, wants = true) {
1542
1688
  }
1543
1689
 
1544
1690
  // Write agents — no model/modelFallback set; agents inherit the global model.
1691
+ //
1692
+ // Capability tier for the orchestrator prompt. Keyed ONLY to an EXPLICIT local default
1693
+ // model — that is a clear intent signal we can size against at sync time. With no
1694
+ // explicit default (the orchestrator runs whatever model the user picks at runtime) or
1695
+ // a cloud default, resolveCapabilityTier returns 'full', so cloud configs and unknown
1696
+ // selections are never slimmed. Per-model slimming of a known pinned model lands on the
1697
+ // construct-local editor agent.
1698
+
1699
+ const orchestratorDefaultModel = config.model || config.defaultModel || "";
1700
+ adviseLocalModelCapability(orchestratorDefaultModel);
1701
+ const orchestratorTier = resolveCapabilityTier({
1702
+ model: orchestratorDefaultModel,
1703
+ verdict: orchestratorDefaultModel ? (getModelVerdict(orchestratorDefaultModel)?.verdict ?? null) : null,
1704
+ });
1545
1705
 
1546
1706
  for (const entry of writeEntries) {
1547
1707
  const name = adapterName(entry);
@@ -1551,7 +1711,9 @@ function syncOpencode(entries, targetDir = null, wants = true) {
1551
1711
  ? `${entry.role} — ${entry.description}`
1552
1712
  : entry.description,
1553
1713
  mode: entry.isOrchestrator ? "all" : "subagent",
1554
- prompt: buildPrompt(entry, entries, "opencode"),
1714
+ prompt: buildPrompt(entry, entries, "opencode", {
1715
+ capabilityTier: entry.isOrchestrator ? orchestratorTier : "full",
1716
+ }),
1555
1717
  permission: {
1556
1718
  ...perms,
1557
1719
  task: opencodeTaskPermissions(entry),
@@ -1559,12 +1721,64 @@ function syncOpencode(entries, targetDir = null, wants = true) {
1559
1721
  };
1560
1722
  }
1561
1723
 
1724
+ // Hybrid split (aider architect/editor). When the fast tier is a LOCAL model, emit a
1725
+ // narrow `construct-local` editor: it does bounded edits on a cheap local model and hands
1726
+ // planning/reasoning back to `construct` (the architect, which stays on the user's chosen
1727
+ // model — we never pin it). The editor's model is NOT the generic fast-tier default
1728
+ // (which for an Ollama family resolves to a non-code generalist); it is the best-installed
1729
+ // CODE model from this config's DECLARED local inventory (OpenCode only uses declared
1730
+ // models), excluding probe-COLLAPSED ones, with the fast tier as a last resort. Its prompt
1731
+ // is sized to the chosen model's capability tier. Deterministic name, so manage it
1732
+ // explicitly: emit when fast is local, delete otherwise, so switching to cloud cleans up.
1733
+
1734
+ const orchestratorEntry = writeEntries.find((e) => e.isOrchestrator) || registry.orchestrator;
1735
+ const orchestratorName = orchestratorEntry ? adapterName(orchestratorEntry) : "construct";
1736
+ const localEditorName = `${orchestratorName}-local`;
1737
+ if (orchestratorEntry?.promptFile && isLocalModel(resolvedModels.fast)) {
1738
+ const declaredLocal = Object.entries(config.provider || {})
1739
+ .flatMap(([pid, pv]) => Object.keys(pv?.models || {}).map((mk) => `${pid}/${mk}`))
1740
+ .filter((id) => isLocalModel(id) && getModelVerdict(id)?.verdict !== "COLLAPSED");
1741
+ const editorModel = selectLocalEditorModel(declaredLocal) || resolvedModels.fast;
1742
+ adviseLocalModelCapability(editorModel);
1743
+ const editorVerdict = getModelVerdict(editorModel)?.verdict ?? null;
1744
+ const editorTier = resolveCapabilityTier({ model: editorModel, verdict: editorVerdict });
1745
+ const editorBody = renderPersonaForTier(readPromptBody(orchestratorEntry.promptFile, root), editorTier);
1746
+ config.agent[localEditorName] = {
1747
+ description: "Local execution agent — bounded edits on the local model; escalates planning and reasoning to construct.",
1748
+ mode: "subagent",
1749
+ model: editorModel,
1750
+ prompt: `${LOCAL_EDITOR_DIRECTIVE}\n\n${editorBody}`,
1751
+ permission: {
1752
+ edit: "allow",
1753
+ bash: { "*": "allow", "rm -rf *": "deny", "git push *": "ask", "git push --force*": "ask", "git reset --hard *": "ask" },
1754
+ "mcp__construct-mcp__orchestration_policy": "deny",
1755
+ "mcp__construct-mcp__agent_contract": "deny",
1756
+ "mcp__construct-mcp__broker_check": "deny",
1757
+ "mcp__github__*": "deny",
1758
+ "mcp__context7__*": "deny",
1759
+ "mcp__sequential-thinking__*": "deny",
1760
+ "mcp__memory__*": "deny",
1761
+ // OpenCode 1.15.4 disables the `task` tool entirely for any restrictive task map
1762
+ // (verified in a sterile run). For an editor that is exactly right: it spawns no
1763
+ // subagents and escalates by RETURNING to the construct agent that dispatched it,
1764
+ // not by dispatching. Deny-all states that intent directly.
1765
+ task: { "*": "deny" },
1766
+ },
1767
+ };
1768
+ } else {
1769
+ delete config.agent[localEditorName];
1770
+ }
1771
+
1772
+ // Pass current Construct model tiers to OpenCode config for native routing.
1773
+ config.construct = config.construct || {};
1774
+ config.construct.models = { ...resolvedModels };
1775
+
1562
1776
  // Seed a cheap auxiliary model for titles and summaries when the user has not
1563
1777
  // chosen one — a cost lever only. The primary `model` stays the user's choice
1564
1778
  // and is never written. Global scope only; project configs inherit it.
1565
1779
 
1566
1780
  if (!targetDir && config.small_model === undefined) {
1567
- config.small_model = "anthropic/claude-haiku-4-5-20251001";
1781
+ config.small_model = resolvedModels.fast || "anthropic/claude-haiku-4-5-20251001";
1568
1782
  }
1569
1783
 
1570
1784
  writeOpenCodeConfig(config, configPath);
@@ -32,7 +32,7 @@ docs/ ← human-readable project documentation
32
32
  1. Treat `.cx/context.md`, `.cx/context.json`, `.cx/workflow.json`, `docs/README.md`, and `docs/architecture.md` as required project state.
33
33
  2. Read them at the start of every meaningful session.
34
34
  3. Update them whenever work changes active reality: decisions, workflow phase, architecture assumptions, or documentation contract.
35
- 4. Run `construct serve` to see the project in the dashboard.
35
+ 4. Run `construct dashboard` to see the project in the dashboard.
36
36
 
37
37
  ## For cx-docs-keeper
38
38
  At session start, check the core docs set. If missing, suggest running `construct init-docs`.
@@ -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`.
@@ -109,7 +109,7 @@ All ports bind to `127.0.0.1` only; nothing is reachable from other machines on
109
109
  | `construct config [mode <m>]` | Show active deployment mode (solo / team / enterprise) or set a new one |
110
110
  | `construct doctor` | Health check across config, services, agents, hooks |
111
111
  | `construct sync` | Regenerate platform adapters (Claude Code, OpenCode, Codex, Cursor) |
112
- | `construct up` / `construct down` | Start / stop local services |
112
+ | `construct dev` / `construct stop` | Start / stop local services |
113
113
  | `construct status` | Live runtime status (services, providers, daemons) |
114
114
  | `construct intake list / show / done / skip / reopen` | Drive the R&D intake queue produced from `.cx/inbox/` |
115
115
  | `construct graph from-intake <id>` | Generate a task graph from a triaged intake packet |
@@ -131,7 +131,7 @@ That refreshes the agents, hooks, and slash commands in `.claude/` and `.constru
131
131
 
132
132
  ```bash
133
133
  construct doctor # most issues surface here with a fix hint
134
- construct down && construct up # restart local services
134
+ construct stop && construct dev # restart local services
135
135
  ```
136
136
 
137
137
  Troubleshooting guide: <https://geraldmaron.github.io/construct/docs/operations/troubleshooting>