@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.
- package/README.md +2 -0
- package/bin/construct +266 -13
- package/lib/agent-instructions/inject.mjs +25 -4
- package/lib/audit-rules.mjs +127 -0
- package/lib/audit-skills.mjs +43 -1
- package/lib/audit-trail.mjs +77 -10
- package/lib/beads-client.mjs +9 -0
- package/lib/beads-optimistic.mjs +23 -71
- package/lib/bridges/copilot-proxy.mjs +116 -0
- package/lib/cli-commands.mjs +112 -1
- package/lib/comment-lint.mjs +1 -1
- package/lib/config/schema.mjs +1 -1
- package/lib/demo.mjs +245 -0
- package/lib/diagram.mjs +300 -0
- package/lib/doctor/index.mjs +1 -1
- package/lib/doctor/watchers/process-pressure.mjs +1 -1
- package/lib/doctor/watchers/service-health.mjs +1 -1
- package/lib/document-extract/docling-client.mjs +16 -6
- package/lib/document-extract/docling-sidecar.py +32 -2
- package/lib/document-extract.mjs +85 -10
- package/lib/document-ingest.mjs +97 -7
- package/lib/embed/roadmap.mjs +16 -1
- package/lib/embed/semantic.mjs +5 -2
- package/lib/engine/consolidate.mjs +160 -3
- package/lib/engine/contradiction-judge.mjs +71 -0
- package/lib/engine/contradiction.mjs +74 -0
- package/lib/handoffs/cleanup.mjs +1 -1
- package/lib/hooks/audit-trail.mjs +14 -28
- package/lib/host-capabilities.mjs +30 -0
- package/lib/ingest/docling-remote.mjs +90 -0
- package/lib/ingest/strategy.mjs +1 -1
- package/lib/init-unified.mjs +9 -13
- package/lib/logging/rotate.mjs +18 -0
- package/lib/mcp/server.mjs +124 -12
- package/lib/mcp/tool-budget.mjs +53 -0
- package/lib/mcp-catalog.json +1 -1
- package/lib/model-router.mjs +52 -1
- package/lib/ollama/capability-store.mjs +78 -0
- package/lib/ollama/provision-context.mjs +344 -0
- package/lib/opencode-config.mjs +148 -0
- package/lib/opencode-telemetry.mjs +7 -0
- package/lib/orchestration-policy.mjs +41 -6
- package/lib/persona-sections.mjs +66 -0
- package/lib/platforms/capabilities.mjs +100 -0
- package/lib/prompt-composer.js +14 -9
- package/lib/reconcile/agent-instructions-rewrap.mjs +8 -4
- package/lib/reflect/extractor.mjs +14 -1
- package/lib/reflect/salience.mjs +65 -0
- package/lib/rules-delivery.mjs +122 -0
- package/lib/runtime/uv-bootstrap.mjs +32 -17
- package/lib/server/index.mjs +1 -1
- package/lib/server/langfuse-login.mjs +3 -3
- package/lib/service-manager.mjs +42 -4
- package/lib/session-store.mjs +1 -1
- package/lib/setup.mjs +58 -2
- package/lib/specialists/prompt-schema.mjs +162 -0
- package/lib/specialists/scaffold.mjs +109 -0
- package/lib/storage/embeddings-engine.mjs +19 -5
- package/lib/storage/embeddings-local.mjs +6 -3
- package/lib/storage/file-lock.mjs +18 -11
- package/lib/telemetry/beads-fallback.mjs +40 -0
- package/lib/telemetry/hook-calls.mjs +138 -0
- package/package.json +4 -2
- package/personas/construct.md +12 -12
- package/platforms/capabilities.json +76 -0
- package/platforms/opencode/sync-config.mjs +121 -25
- package/rules/common/neurodivergent-output.md +1 -1
- package/rules/web/coding-style.md +8 -0
- package/rules/web/design-quality.md +8 -0
- package/rules/web/hooks.md +8 -0
- package/rules/web/patterns.md +8 -0
- package/rules/web/performance.md +8 -0
- package/rules/web/security.md +8 -0
- package/rules/web/testing.md +8 -0
- package/scripts/sync-specialists.mjs +277 -63
- package/skills/docs/init-project.md +1 -1
- package/specialists/prompts/cx-architect.md +20 -0
- package/specialists/prompts/cx-test-automation.md +12 -0
- package/templates/docs/construct_guide.md +2 -2
- package/lib/ingest/chunker.mjs +0 -94
- package/lib/ingest/pipeline.mjs +0 -53
- package/lib/ingest/store.mjs +0 -82
- package/lib/mode-commands.mjs +0 -122
- package/lib/policy/unified-gates.mjs +0 -96
- package/lib/profiles/validate-custom.mjs +0 -114
- package/lib/services/telemetry-backend.mjs +0 -177
- 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.
|
|
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
|
|
package/rules/web/hooks.md
CHANGED
package/rules/web/patterns.md
CHANGED
|
@@ -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
|
|
package/rules/web/performance.md
CHANGED
|
@@ -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
|
|
package/rules/web/security.md
CHANGED
|
@@ -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
|
|
package/rules/web/testing.md
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
813
|
-
|
|
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
|
-
//
|
|
1366
|
-
//
|
|
1367
|
-
//
|
|
1368
|
-
//
|
|
1369
|
-
//
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
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
|
|
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
|
|
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
|
|
134
|
+
construct stop && construct dev # restart local services
|
|
135
135
|
```
|
|
136
136
|
|
|
137
137
|
Troubleshooting guide: <https://geraldmaron.github.io/construct/docs/operations/troubleshooting>
|