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