@geraldmaron/construct 1.0.21 → 1.0.23
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 +23 -5
- package/bin/construct +191 -13
- package/bin/construct-postinstall.mjs +25 -15
- package/lib/acp/server.mjs +113 -0
- package/lib/agent-instructions/inject.mjs +94 -0
- package/lib/auto-docs.mjs +10 -2
- package/lib/cli-commands.mjs +43 -15
- package/lib/comment-lint.mjs +115 -0
- package/lib/completions.mjs +7 -1
- package/lib/config/schema.mjs +4 -0
- package/lib/decisions/enforced-baseline.json +2 -0
- package/lib/docs-verify.mjs +15 -18
- package/lib/doctor/cli.mjs +8 -1
- package/lib/document-export.mjs +124 -0
- package/lib/embed/daemon.mjs +1 -1
- package/lib/embed/inbox.mjs +25 -7
- package/lib/features.mjs +11 -11
- package/lib/git-hooks-path.mjs +61 -0
- package/lib/home-namespace.mjs +60 -0
- package/lib/hooks/ci-status-check.mjs +62 -40
- package/lib/hooks/orchestration-dispatch-guard.mjs +153 -0
- package/lib/hooks/pre-push-gate.mjs +15 -6
- package/lib/hooks/stop-notify.mjs +32 -17
- package/lib/hooks/stop-typecheck.mjs +7 -2
- package/lib/host-capabilities.mjs +24 -8
- package/lib/host-disposition.mjs +76 -0
- package/lib/ingest/provider-extract.mjs +1 -1
- package/lib/init-docs.mjs +1 -1
- package/lib/init-unified.mjs +320 -219
- package/lib/init-update.mjs +4 -84
- package/lib/init.mjs +9 -25
- package/lib/install/stage-project.mjs +8 -2
- package/lib/intent-classifier.mjs +1 -1
- package/lib/knowledge/search.mjs +52 -3
- package/lib/mcp/server.mjs +57 -14
- package/lib/mcp/tools/memory.mjs +2 -2
- package/lib/mcp/tools/orchestration-run.mjs +125 -0
- package/lib/model-registry.mjs +40 -33
- package/lib/observation-store.mjs +6 -2
- package/lib/opencode-config.mjs +1 -1
- package/lib/orchestration/events.mjs +66 -0
- package/lib/orchestration/runtime.mjs +70 -9
- package/lib/orchestration/worker.mjs +69 -21
- package/lib/orchestration-policy.mjs +27 -3
- package/lib/parity.mjs +46 -24
- package/lib/policy/unified-gates.mjs +96 -0
- package/lib/project-init-shared.mjs +0 -173
- package/lib/reconcile/adapter-prune.mjs +105 -0
- package/lib/reconcile/agent-instructions-rewrap.mjs +98 -0
- package/lib/reconcile/gitignore-coverage.mjs +88 -0
- package/lib/reconcile/index.mjs +171 -0
- package/lib/reconcile/legacy-doctrine-strip.mjs +139 -0
- package/lib/reconcile/legacy-guide-decommit.mjs +67 -0
- package/lib/reconcile/legacy-skills-cleanup.mjs +200 -0
- package/lib/reconcile/mcp-entry-reconcile.mjs +142 -0
- package/lib/reconcile/postgres-namespace.mjs +102 -0
- package/lib/runtime/uv-bootstrap.mjs +27 -3
- package/lib/schema-infer.mjs +16 -2
- package/lib/server/csrf.mjs +14 -2
- package/lib/server/index.mjs +95 -0
- package/lib/service-manager.mjs +39 -14
- package/lib/setup-prompts.mjs +2 -1
- package/lib/setup.mjs +165 -141
- package/lib/storage/backend.mjs +14 -0
- package/lib/storage/unified-storage.mjs +550 -0
- package/lib/template-registry.mjs +73 -0
- package/lib/term-format.mjs +75 -0
- package/lib/uninstall/uninstall.mjs +180 -7
- package/package.json +2 -2
- package/personas/construct.md +7 -8
- package/platforms/claude/settings.template.json +30 -4
- package/platforms/opencode/config.template.json +2 -2
- package/rules/common/neurodivergent-output.md +66 -0
- package/rules/common/tool-invisibility.md +37 -0
- package/scripts/sync-specialists.mjs +381 -95
- package/skills/operating/orchestration-reference.md +2 -16
- package/specialists/policy-inventory.json +14 -0
- package/specialists/prompts/cx-accessibility.md +2 -6
- package/specialists/prompts/cx-ai-engineer.md +0 -4
- package/specialists/prompts/cx-architect.md +3 -5
- package/specialists/prompts/cx-business-strategist.md +0 -5
- package/specialists/prompts/cx-data-analyst.md +0 -4
- package/specialists/prompts/cx-data-engineer.md +0 -4
- package/specialists/prompts/cx-debugger.md +2 -6
- package/specialists/prompts/cx-designer.md +0 -8
- package/specialists/prompts/cx-devil-advocate.md +2 -2
- package/specialists/prompts/cx-docs-keeper.md +0 -13
- package/specialists/prompts/cx-engineer.md +0 -13
- package/specialists/prompts/cx-evaluator.md +2 -2
- package/specialists/prompts/cx-explorer.md +4 -5
- package/specialists/prompts/cx-legal-compliance.md +4 -5
- package/specialists/prompts/cx-operations.md +0 -5
- package/specialists/prompts/cx-orchestrator.md +0 -4
- package/specialists/prompts/cx-platform-engineer.md +0 -8
- package/specialists/prompts/cx-product-manager.md +0 -8
- package/specialists/prompts/cx-qa.md +2 -11
- package/specialists/prompts/cx-rd-lead.md +0 -5
- package/specialists/prompts/cx-release-manager.md +0 -8
- package/specialists/prompts/cx-researcher.md +5 -29
- package/specialists/prompts/cx-reviewer.md +2 -6
- package/specialists/prompts/cx-security.md +2 -11
- package/specialists/prompts/cx-sre.md +0 -15
- package/specialists/prompts/cx-test-automation.md +0 -4
- package/specialists/prompts/cx-trace-reviewer.md +2 -2
- package/specialists/prompts/cx-ux-researcher.md +0 -4
- package/specialists/registry.json +29 -29
- package/templates/distribution/run.mjs +36 -7
- package/templates/docs/accessibility-audit.md +56 -0
- package/templates/docs/architecture-review.md +59 -0
- package/templates/docs/code-review-report.md +46 -0
- package/templates/docs/construct_guide.md +14 -14
- package/templates/docs/debug-investigation.md +53 -0
- package/templates/docs/qa-report.md +48 -0
- package/templates/docs/security-audit-report.md +48 -0
- package/templates/docs/task-packet.md +49 -0
- package/templates/docs/verdict.md +40 -0
- package/lib/server/static/index.html +0 -1
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
} from "../lib/mcp-platform-config.mjs";
|
|
53
53
|
import { loadConstructEnv } from "../lib/env-config.mjs";
|
|
54
54
|
import { inlineRoleAntiPatterns, PROMPT_WORD_CAP } from "../lib/role-preload.mjs";
|
|
55
|
+
import { loadManifest } from "../lib/roles/manifest.mjs";
|
|
55
56
|
import { resolveActiveProfile } from "../lib/profiles/loader.mjs";
|
|
56
57
|
import { resolveTiersForPrimary } from "../lib/model-router.mjs";
|
|
57
58
|
import { stampFrontmatter } from "../lib/doc-stamp.mjs";
|
|
@@ -156,6 +157,67 @@ const COMPRESS_PERSONAS = process.argv.includes("--compress-personas");
|
|
|
156
157
|
const PROJECT_FLAG = process.argv.includes("--project");
|
|
157
158
|
const GLOBAL_FLAG = process.argv.includes("--global");
|
|
158
159
|
|
|
160
|
+
// --quiet suppresses only the closing one-line summary, not the work or any
|
|
161
|
+
// warning. `construct install` runs the global tier twice (plain `sync` then
|
|
162
|
+
// `sync --global`); in a non-project cwd both land in the same global branch and
|
|
163
|
+
// print the identical "Synced … to global scope" + "Completions updated" lines.
|
|
164
|
+
// Passing --quiet to the first call lets the canonical summary print exactly once
|
|
165
|
+
// from `sync --global`, with no change to what either call writes.
|
|
166
|
+
|
|
167
|
+
const QUIET = process.argv.includes("--quiet") || process.argv.includes("-q");
|
|
168
|
+
const summary = (msg) => { if (!QUIET) console.log(msg); };
|
|
169
|
+
|
|
170
|
+
// Project-tier host selection. `--hosts=claude,codex,…` (or CONSTRUCT_SYNC_HOSTS)
|
|
171
|
+
// restricts which adapter sets the project tier writes, so `construct init` can
|
|
172
|
+
// scaffold only the hosts the user actually has (construct-4xy6 / ADR-0027 §1).
|
|
173
|
+
// Absent → null → write every host, preserving `construct sync` back-compat.
|
|
174
|
+
|
|
175
|
+
import { detectHostCapabilities } from "../lib/host-capabilities.mjs";
|
|
176
|
+
|
|
177
|
+
const HOST_KEYS = ["claude", "codex", "copilot", "opencode", "vscode", "cursor"];
|
|
178
|
+
|
|
179
|
+
function parseHostSelection() {
|
|
180
|
+
const arg = process.argv.find((a) => a.startsWith("--hosts="));
|
|
181
|
+
const raw = arg ? arg.slice("--hosts=".length) : process.env.CONSTRUCT_SYNC_HOSTS;
|
|
182
|
+
if (!raw) {
|
|
183
|
+
// Default to detected hosts if none are explicitly requested.
|
|
184
|
+
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
|
+
};
|
|
193
|
+
try {
|
|
194
|
+
for (const cap of detectHostCapabilities()) {
|
|
195
|
+
if (cap.availability === "installed" && nameToKey[cap.host]) {
|
|
196
|
+
detected.add(nameToKey[cap.host]);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} catch { /* detection is advisory */ }
|
|
200
|
+
|
|
201
|
+
// Config file present means the user has (or had) OpenCode — include it
|
|
202
|
+
// so the sync writes to the existing config rather than pruning it.
|
|
203
|
+
// Binary-based detection misses non-PATH installs and CI-runner setups.
|
|
204
|
+
if (!detected.has("opencode")) {
|
|
205
|
+
try {
|
|
206
|
+
if (fs.existsSync(findOpenCodeConfigPath())) detected.add("opencode");
|
|
207
|
+
} catch { /* advisory */ }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Always include Claude as the baseline if nothing else is detected.
|
|
211
|
+
if (detected.size === 0) detected.add("claude");
|
|
212
|
+
return detected;
|
|
213
|
+
}
|
|
214
|
+
const wanted = new Set(raw.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean));
|
|
215
|
+
return new Set(HOST_KEYS.filter((k) => wanted.has(k)));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const HOST_SELECTION = parseHostSelection();
|
|
219
|
+
const wantsHost = (key) => HOST_SELECTION.has(key);
|
|
220
|
+
|
|
159
221
|
/**
|
|
160
222
|
* A Construct project carries `.construct/` (the launcher staged by
|
|
161
223
|
* `stage-project.mjs`) or `.cx/` (state). When `construct sync` runs inside
|
|
@@ -469,6 +531,64 @@ function buildRoleFooter(entry) {
|
|
|
469
531
|
return `\n\n${lines.join("\n")}`;
|
|
470
532
|
}
|
|
471
533
|
|
|
534
|
+
// Fence + handoff data is the source of truth in specialists/role-manifests.json
|
|
535
|
+
// (events, fence.allowedPaths, fence.allowedBdLabels, fence.approvalRequired,
|
|
536
|
+
// handoffCandidates, outputs.docTypes). Restating that JSON inside each
|
|
537
|
+
// specialist prompt would invite drift across 28 files; the renderer below
|
|
538
|
+
// emits the section from the manifest entry so the JSON is the only authority.
|
|
539
|
+
//
|
|
540
|
+
// Returns the empty string for an unonboarded persona (no manifest entry, or
|
|
541
|
+
// an empty fence block). buildPrompt appends the result verbatim, so the
|
|
542
|
+
// returned string carries its own leading separator.
|
|
543
|
+
|
|
544
|
+
export function renderRoleFrameworkSection(entry) {
|
|
545
|
+
const personaName = String(entry?.name || "").replace(/^cx-/, "");
|
|
546
|
+
if (!personaName) return "";
|
|
547
|
+
const manifest = loadManifest(personaName);
|
|
548
|
+
if (!manifest) return "";
|
|
549
|
+
|
|
550
|
+
const events = Array.isArray(manifest.events) ? manifest.events : [];
|
|
551
|
+
const fence = manifest.fence || {};
|
|
552
|
+
const allowedPaths = Array.isArray(fence.allowedPaths) ? fence.allowedPaths : [];
|
|
553
|
+
const allowedBdLabels = Array.isArray(fence.allowedBdLabels) ? fence.allowedBdLabels : [];
|
|
554
|
+
const approvalRequired = Array.isArray(fence.approvalRequired) ? fence.approvalRequired : [];
|
|
555
|
+
const handoffCandidates = Array.isArray(manifest.handoffCandidates) ? manifest.handoffCandidates : [];
|
|
556
|
+
const docTypes = Array.isArray(manifest.outputs?.docTypes) ? manifest.outputs.docTypes : [];
|
|
557
|
+
|
|
558
|
+
// Empty fence + empty events means the persona is reserved but not wired —
|
|
559
|
+
// no section to render rather than emit a misleading stub.
|
|
560
|
+
|
|
561
|
+
if (!events.length && !allowedPaths.length) return "";
|
|
562
|
+
|
|
563
|
+
const fmt = (xs) => xs.map((x) => `\`${x}\``).join(", ");
|
|
564
|
+
const eventList = events.length ? fmt(events) : "_handoff events_";
|
|
565
|
+
const pathList = allowedPaths.length ? fmt(allowedPaths) : "_none declared_";
|
|
566
|
+
const labelList = allowedBdLabels.length ? fmt(allowedBdLabels) : "_none declared_";
|
|
567
|
+
const approvalList = approvalRequired.length ? fmt(approvalRequired) : "_no approval gate declared_";
|
|
568
|
+
const docTypeList = docTypes.length ? fmt(docTypes) : "role-specific artifacts";
|
|
569
|
+
const handoffList = handoffCandidates.length
|
|
570
|
+
? handoffCandidates.map((c) => `\`next:cx-${c}\``).join(", ")
|
|
571
|
+
: "";
|
|
572
|
+
|
|
573
|
+
const lines = [
|
|
574
|
+
"",
|
|
575
|
+
"## When invoked via the role framework",
|
|
576
|
+
"",
|
|
577
|
+
`Construct may dispatch you in response to ${eventList} events. A bd issue with the event payload exists when dispatched: read it first via \`bd show <id>\`.`,
|
|
578
|
+
"",
|
|
579
|
+
`**Fence** (source of truth: \`specialists/role-manifests.json\` → \`${personaName}\`):`,
|
|
580
|
+
`- Allowed paths: ${pathList}`,
|
|
581
|
+
`- Allowed bd labels: ${labelList}`,
|
|
582
|
+
`- Approval required: ${approvalList}`,
|
|
583
|
+
"",
|
|
584
|
+
`You may freely create, edit, and verify within the fence (allowed paths and labels above). You produce ${docTypeList}. You **must not** commit, push, or operate outside the fence without explicit user approval per \`rules/common/commit-approval.md\`.`,
|
|
585
|
+
];
|
|
586
|
+
if (handoffList) {
|
|
587
|
+
lines.push("", `**Handoff syntax**: append a bd label of the form \`next:cx-<role>\`. Candidates from this role: ${handoffList}.`);
|
|
588
|
+
}
|
|
589
|
+
return `\n\n${lines.join("\n")}`;
|
|
590
|
+
}
|
|
591
|
+
|
|
472
592
|
function buildPrompt(entry, allEntries, platform) {
|
|
473
593
|
let prompt = resolvePromptContract(entry, {
|
|
474
594
|
rootDir: root,
|
|
@@ -485,6 +605,8 @@ function buildPrompt(entry, allEntries, platform) {
|
|
|
485
605
|
|
|
486
606
|
prompt += buildRoleFooter(entry);
|
|
487
607
|
|
|
608
|
+
prompt += renderRoleFrameworkSection(entry);
|
|
609
|
+
|
|
488
610
|
const platformItems = platformGuidance[platform] ?? [];
|
|
489
611
|
const allGuidance = [...sharedGuidance, ...platformItems];
|
|
490
612
|
if (allGuidance.length > 0) {
|
|
@@ -709,22 +831,40 @@ function writeProjectClaudeSettings(targetDir) {
|
|
|
709
831
|
}
|
|
710
832
|
}
|
|
711
833
|
|
|
834
|
+
// The registry is the single source of truth for MCP servers; VS Code, Codex,
|
|
835
|
+
// and OpenCode all wire it. Project Claude must match — above all `construct-mcp`,
|
|
836
|
+
// the server exposing project_context/get_skill/get_template/orchestration_policy
|
|
837
|
+
// that the specialist loop depends on. The curated template omits it, which left
|
|
838
|
+
// Claude Code as the only selected tool without the construct config. Merge the
|
|
839
|
+
// registry on top of the template seed; existing entries win so user edits stick,
|
|
840
|
+
// and buildClaudeMcpEntry resolves each server's path/env.
|
|
841
|
+
const registryMcp = registry.mcpServers ?? {};
|
|
842
|
+
existing.mcpServers ??= {};
|
|
843
|
+
for (const [id, mcpDef] of Object.entries(registryMcp)) {
|
|
844
|
+
if (existing.mcpServers[id]) continue;
|
|
845
|
+
existing.mcpServers[id] = buildClaudeMcpEntry(id, mcpDef, process.env);
|
|
846
|
+
}
|
|
847
|
+
|
|
712
848
|
if (DRY_RUN) return;
|
|
713
849
|
mkdirp(path.dirname(settingsPath));
|
|
714
850
|
writeFile(settingsPath, JSON.stringify(existing, null, 2) + "\n");
|
|
715
851
|
}
|
|
716
852
|
|
|
717
|
-
function syncClaude(entries, targetDir = null) {
|
|
853
|
+
function syncClaude(entries, targetDir = null, wants = true) {
|
|
718
854
|
const claudeAgentsDir = targetDir
|
|
719
855
|
? path.join(targetDir, ".claude", "agents")
|
|
720
856
|
: path.join(home, ".claude", "agents");
|
|
721
|
-
if (!DRY_RUN) mkdirp(claudeAgentsDir);
|
|
857
|
+
if (!DRY_RUN && wants) mkdirp(claudeAgentsDir);
|
|
722
858
|
|
|
723
|
-
//
|
|
724
|
-
//
|
|
725
|
-
//
|
|
859
|
+
// Claude Code and VS Code both read the user-scope `~/.claude/agents/`, so a
|
|
860
|
+
// global front-door agent duplicates the project orchestrator in any editor
|
|
861
|
+
// that reads both scopes (the construct ×2 in the VS Code picker). Global
|
|
862
|
+
// scope therefore writes NO agent file — the project's own
|
|
863
|
+
// `.claude/agents/construct.md` is the front door; global hooks (settings.json)
|
|
864
|
+
// and CLAUDE.md still install. An empty write set sweeps any global agent.
|
|
865
|
+
// Both global and project scope now emit only the front door (Single Front Door).
|
|
726
866
|
|
|
727
|
-
const writeEntries = targetDir
|
|
867
|
+
const writeEntries = (targetDir && wants) ? globalEntries(entries) : [];
|
|
728
868
|
|
|
729
869
|
for (const entry of writeEntries) {
|
|
730
870
|
const name = adapterName(entry);
|
|
@@ -733,7 +873,7 @@ function syncClaude(entries, targetDir = null) {
|
|
|
733
873
|
}
|
|
734
874
|
removeStaleAdapters(claudeAgentsDir, ".md", writeEntries);
|
|
735
875
|
if (!targetDir) {
|
|
736
|
-
sweepLegacyPrefixedFiles(claudeAgentsDir, ".md",
|
|
876
|
+
sweepLegacyPrefixedFiles(claudeAgentsDir, ".md", []);
|
|
737
877
|
}
|
|
738
878
|
|
|
739
879
|
if (targetDir) {
|
|
@@ -824,14 +964,29 @@ function isCodexMcpSupported() {
|
|
|
824
964
|
return true;
|
|
825
965
|
}
|
|
826
966
|
|
|
827
|
-
|
|
967
|
+
// Codex aborts at startup when an MCP server's bearer_token_env_var names an env
|
|
968
|
+
// var that is unset ("Environment variable GITHUB_TOKEN for MCP server github is
|
|
969
|
+
// not set"). Unlike OpenCode — which keeps the `{env:VAR}` ref and resolves it at
|
|
970
|
+
// runtime — Codex must have the credential at sync time, so an entry whose token
|
|
971
|
+
// env var is unresolved is omitted from the Codex config (construct-n6h7).
|
|
972
|
+
// Entries with no credential requirement always pass.
|
|
973
|
+
|
|
974
|
+
function codexMcpEnvResolves(id, def, env = process.env) {
|
|
975
|
+
const entry = buildCodexMcpEntry(id, def, env);
|
|
976
|
+
const tokenVar = entry?.bearer_token_env_var;
|
|
977
|
+
if (!tokenVar) return true;
|
|
978
|
+
const val = env[tokenVar];
|
|
979
|
+
return val !== undefined && val !== "";
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function syncCodex(entries, targetDir = null, wants = true) {
|
|
828
983
|
const codexDir = targetDir
|
|
829
984
|
? path.join(targetDir, ".codex")
|
|
830
985
|
: path.join(home, ".codex");
|
|
831
986
|
const codexAgentsDir = path.join(codexDir, "agents");
|
|
832
|
-
if (!DRY_RUN) mkdirp(codexAgentsDir);
|
|
987
|
+
if (!DRY_RUN && wants) mkdirp(codexAgentsDir);
|
|
833
988
|
|
|
834
|
-
const writeEntries =
|
|
989
|
+
const writeEntries = wants ? globalEntries(entries) : [];
|
|
835
990
|
|
|
836
991
|
for (const entry of writeEntries) {
|
|
837
992
|
writeFile(path.join(codexAgentsDir, `${adapterName(entry)}.toml`), codexAgentToml(entry, entries));
|
|
@@ -847,8 +1002,12 @@ function syncCodex(entries, targetDir = null) {
|
|
|
847
1002
|
const existing = removeDanglingConstructMcpMarkers(removeDanglingConstructMcpTimeouts(readCodexConfig(configPath)));
|
|
848
1003
|
const entryNames = writeEntries.map(adapterName);
|
|
849
1004
|
const registryMcp = registry.mcpServers ?? {};
|
|
1005
|
+
// Seed every supported MCP server (parity with Claude/OpenCode/VS Code/Cursor),
|
|
1006
|
+
// not just tables already present — otherwise Codex never receives `construct-mcp`
|
|
1007
|
+
// and the orchestration tool is unreachable there. existingMcpIds still drives
|
|
1008
|
+
// cleanup of any pre-existing standalone tables so the managed block stays canonical.
|
|
1009
|
+
const mcpIds = Object.keys(registryMcp).filter((id) => isCodexMcpSupported() && codexMcpEnvResolves(id, registryMcp[id], process.env));
|
|
850
1010
|
const existingMcpIds = Object.keys(registryMcp).filter((id) => hasCodexMcpTable(existing, id));
|
|
851
|
-
const mcpIds = existingMcpIds.filter(isCodexMcpSupported);
|
|
852
1011
|
const withoutManagedTables = removeDanglingConstructMcpMarkers(removeTomlTables(
|
|
853
1012
|
removeCodexAgentTables(existing, entryNames),
|
|
854
1013
|
existingMcpIds.flatMap((id) => [`mcp_servers.${id}`, `mcp_servers.${tomlString(id)}`]),
|
|
@@ -856,6 +1015,14 @@ function syncCodex(entries, targetDir = null) {
|
|
|
856
1015
|
const hasAgentsRoot = /^\[agents\]\s*$/m.test(withoutManagedTables);
|
|
857
1016
|
const rootBlock = hasAgentsRoot ? "" : "[agents]\nmax_threads = 6\nmax_depth = 1\n\n";
|
|
858
1017
|
|
|
1018
|
+
// Workspace-write agents (the orchestrator + canEdit specialists) need network
|
|
1019
|
+
// access for WebFetch/context7 — Codex's workspace-write sandbox blocks the
|
|
1020
|
+
// network unless this is set. Skip when the user already manages the table so
|
|
1021
|
+
// their own sandbox settings (e.g. writable_roots) are not clobbered.
|
|
1022
|
+
|
|
1023
|
+
const hasSandboxConfig = /^\[sandbox_workspace_write\]/m.test(withoutManagedTables);
|
|
1024
|
+
const sandboxBlock = hasSandboxConfig ? "" : "[sandbox_workspace_write]\nnetwork_access = true\n\n";
|
|
1025
|
+
|
|
859
1026
|
// In project scope every entry is reachable from the user (Construct dispatches
|
|
860
1027
|
// internal specialists itself, but project teammates may want to address them).
|
|
861
1028
|
// In global scope only the `construct` front-door agent is registered.
|
|
@@ -869,7 +1036,7 @@ config_file = ${tomlString(`agents/${adapterName(e)}.toml`)}
|
|
|
869
1036
|
const mcpBlock = mcpIds
|
|
870
1037
|
.map((id) => serializeCodexMcpTable(id, buildCodexMcpEntry(id, registryMcp[id], process.env)))
|
|
871
1038
|
.join("\n\n");
|
|
872
|
-
const withAgents = replaceManagedBlock(withoutManagedTables, `${rootBlock}${blocks}`);
|
|
1039
|
+
const withAgents = replaceManagedBlock(withoutManagedTables, `${sandboxBlock}${rootBlock}${blocks}`);
|
|
873
1040
|
writeCodexConfig(replaceManagedBlock(
|
|
874
1041
|
withAgents,
|
|
875
1042
|
mcpBlock,
|
|
@@ -897,13 +1064,13 @@ When using this prompt, stay within the role above and adapt to the current repo
|
|
|
897
1064
|
`;
|
|
898
1065
|
}
|
|
899
1066
|
|
|
900
|
-
function syncCopilot(entries, targetDir = null) {
|
|
1067
|
+
function syncCopilot(entries, targetDir = null, wants = true) {
|
|
901
1068
|
const promptsDir = targetDir
|
|
902
1069
|
? path.join(targetDir, ".github", "prompts")
|
|
903
1070
|
: path.join(home, ".github", "prompts");
|
|
904
|
-
if (!DRY_RUN) mkdirp(promptsDir);
|
|
1071
|
+
if (!DRY_RUN && wants) mkdirp(promptsDir);
|
|
905
1072
|
|
|
906
|
-
const writeEntries =
|
|
1073
|
+
const writeEntries = wants ? globalEntries(entries) : [];
|
|
907
1074
|
|
|
908
1075
|
for (const entry of writeEntries) {
|
|
909
1076
|
writeFile(path.join(promptsDir, `${adapterName(entry)}.prompt.md`), copilotPrompt(entry, entries), { stamp: false });
|
|
@@ -913,6 +1080,16 @@ function syncCopilot(entries, targetDir = null) {
|
|
|
913
1080
|
sweepLegacyPrefixedFiles(promptsDir, ".prompt.md", writeEntries.map((e) => `${adapterName(e)}.prompt.md`));
|
|
914
1081
|
}
|
|
915
1082
|
|
|
1083
|
+
// VS Code / Copilot agent mode reads the project's `.claude/agents/*.md`
|
|
1084
|
+
// natively (Claude-format custom agents), so Construct does NOT write a
|
|
1085
|
+
// separate `.github/agents/` set — doing so duplicated every agent in the
|
|
1086
|
+
// picker. A pre-existing `.github/agents/` from an earlier sync is swept.
|
|
1087
|
+
|
|
1088
|
+
if (targetDir) {
|
|
1089
|
+
const staleAgentsDir = path.join(targetDir, ".github", "agents");
|
|
1090
|
+
if (!DRY_RUN && fs.existsSync(staleAgentsDir)) fs.rmSync(staleAgentsDir, { recursive: true, force: true });
|
|
1091
|
+
}
|
|
1092
|
+
|
|
916
1093
|
const instructionsPath = targetDir
|
|
917
1094
|
? path.join(targetDir, ".github", "copilot-instructions.md")
|
|
918
1095
|
: path.join(home, ".github", "copilot-instructions.md");
|
|
@@ -928,7 +1105,7 @@ function syncCopilot(entries, targetDir = null) {
|
|
|
928
1105
|
const list = listEntries.map((e) => `- \`${adapterName(e)}\`: use \`${promptPathPrefix}/${adapterName(e)}.prompt.md\`.`).join("\n");
|
|
929
1106
|
const note = `## ${systemName.charAt(0).toUpperCase() + systemName.slice(1)} Agent Prompts
|
|
930
1107
|
|
|
931
|
-
Copilot
|
|
1108
|
+
For a real multi-specialist run, Copilot agent mode calls the \`orchestration_run\` MCP tool (start the engine with \`construct dashboard\`). The prompts below are reusable role profiles for single-pass work:
|
|
932
1109
|
|
|
933
1110
|
${list || "(no front-door prompts to surface)"}`;
|
|
934
1111
|
|
|
@@ -939,48 +1116,75 @@ ${list || "(no front-door prompts to surface)"}`;
|
|
|
939
1116
|
|
|
940
1117
|
// --- VS Code adapter ---
|
|
941
1118
|
|
|
942
|
-
function
|
|
1119
|
+
function getVSCodeUserDirs() {
|
|
943
1120
|
const platform = os.platform();
|
|
944
|
-
const
|
|
1121
|
+
const dirs = [];
|
|
945
1122
|
if (platform === "darwin") {
|
|
946
|
-
|
|
947
|
-
path.join(home, "Library", "Application Support", "Code", "User"
|
|
948
|
-
path.join(home, "Library", "Application Support", "Code - Insiders", "User"
|
|
1123
|
+
dirs.push(
|
|
1124
|
+
path.join(home, "Library", "Application Support", "Code", "User"),
|
|
1125
|
+
path.join(home, "Library", "Application Support", "Code - Insiders", "User"),
|
|
949
1126
|
);
|
|
950
1127
|
} else if (platform === "linux") {
|
|
951
|
-
|
|
952
|
-
path.join(home, ".config", "Code", "User"
|
|
953
|
-
path.join(home, ".config", "Code - Insiders", "User"
|
|
1128
|
+
dirs.push(
|
|
1129
|
+
path.join(home, ".config", "Code", "User"),
|
|
1130
|
+
path.join(home, ".config", "Code - Insiders", "User"),
|
|
954
1131
|
);
|
|
955
1132
|
} else if (platform === "win32") {
|
|
956
1133
|
const appData = process.env.APPDATA ?? path.join(home, "AppData", "Roaming");
|
|
957
|
-
|
|
958
|
-
path.join(appData, "Code", "User"
|
|
959
|
-
path.join(appData, "Code - Insiders", "User"
|
|
1134
|
+
dirs.push(
|
|
1135
|
+
path.join(appData, "Code", "User"),
|
|
1136
|
+
path.join(appData, "Code - Insiders", "User"),
|
|
960
1137
|
);
|
|
961
1138
|
}
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
// and global sync still needs to write the MCP block. The User dir is
|
|
965
|
-
// the installation signal; bootstrap a minimal settings.json there.
|
|
1139
|
+
return dirs;
|
|
1140
|
+
}
|
|
966
1141
|
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1142
|
+
function getVSCodeUserMcpPaths() {
|
|
1143
|
+
// VS Code's "MCP: Open User Configuration" edits `<User>/mcp.json` (top-level
|
|
1144
|
+
// `servers`). Global sync returns only files that already exist — per-window
|
|
1145
|
+
// MCP config is never seeded, mirroring the non-polluting Cursor/OpenCode
|
|
1146
|
+
// global behavior.
|
|
1147
|
+
|
|
1148
|
+
return getVSCodeUserDirs()
|
|
1149
|
+
.map((dir) => path.join(dir, "mcp.json"))
|
|
1150
|
+
.filter((file) => fs.existsSync(file));
|
|
972
1151
|
}
|
|
973
1152
|
|
|
974
|
-
function syncVSCode(targetDir = null) {
|
|
1153
|
+
function syncVSCode(targetDir = null, wants = true) {
|
|
975
1154
|
const registryMcp = registry.mcpServers ?? {};
|
|
976
1155
|
if (Object.keys(registryMcp).length === 0) return false;
|
|
977
1156
|
|
|
978
1157
|
// Project scope writes a dedicated `.vscode/mcp.json` (VS Code's documented
|
|
979
|
-
// workspace MCP config). Global scope
|
|
980
|
-
//
|
|
1158
|
+
// workspace MCP config, top-level `servers`). Global scope merges into the
|
|
1159
|
+
// user-profile `mcp.json` (the file "MCP: Open User Configuration" edits), and
|
|
1160
|
+
// only when it already exists — global sync never seeds per-window MCP config.
|
|
981
1161
|
|
|
982
1162
|
if (targetDir) {
|
|
983
1163
|
const mcpPath = path.join(targetDir, ".vscode", "mcp.json");
|
|
1164
|
+
|
|
1165
|
+
if (!wants) {
|
|
1166
|
+
if (!DRY_RUN && fs.existsSync(mcpPath)) {
|
|
1167
|
+
// Only delete if it matches our managed block pattern or is a simple
|
|
1168
|
+
// Construct-only file. For now, simple removal of the file if it
|
|
1169
|
+
// matches our expected content.
|
|
1170
|
+
try {
|
|
1171
|
+
const config = JSON.parse(fs.readFileSync(mcpPath, "utf8"));
|
|
1172
|
+
const keys = Object.keys(config?.servers ?? {});
|
|
1173
|
+
const allManaged = keys.every(id => id in registryMcp);
|
|
1174
|
+
if (allManaged) {
|
|
1175
|
+
fs.rmSync(mcpPath, { force: true });
|
|
1176
|
+
try {
|
|
1177
|
+
const vscodeDir = path.dirname(mcpPath);
|
|
1178
|
+
if (fs.existsSync(vscodeDir) && fs.readdirSync(vscodeDir).length === 0) {
|
|
1179
|
+
fs.rmdirSync(vscodeDir);
|
|
1180
|
+
}
|
|
1181
|
+
} catch { /* ignore non-empty */ }
|
|
1182
|
+
}
|
|
1183
|
+
} catch { /* skip cleanup of unreadable file */ }
|
|
1184
|
+
}
|
|
1185
|
+
return false;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
984
1188
|
let config = { servers: {} };
|
|
985
1189
|
if (fs.existsSync(mcpPath)) {
|
|
986
1190
|
try { config = JSON.parse(fs.readFileSync(mcpPath, "utf8")) || { servers: {} }; }
|
|
@@ -1004,50 +1208,40 @@ function syncVSCode(targetDir = null) {
|
|
|
1004
1208
|
return true;
|
|
1005
1209
|
}
|
|
1006
1210
|
|
|
1007
|
-
const
|
|
1008
|
-
if (
|
|
1211
|
+
const mcpPaths = getVSCodeUserMcpPaths();
|
|
1212
|
+
if (mcpPaths.length === 0) return false;
|
|
1009
1213
|
let synced = false;
|
|
1010
|
-
for (const
|
|
1214
|
+
for (const mcpPath of mcpPaths) {
|
|
1011
1215
|
try {
|
|
1012
|
-
|
|
1013
|
-
if (
|
|
1014
|
-
settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
|
|
1015
|
-
} else {
|
|
1016
|
-
// VS Code installed (User dir present) but never opened → seed an
|
|
1017
|
-
// empty settings.json. Single object literal, no other keys, so
|
|
1018
|
-
// a later launch of VS Code finds it valid and writes user prefs
|
|
1019
|
-
// alongside.
|
|
1020
|
-
settings = {};
|
|
1021
|
-
}
|
|
1022
|
-
if (!settings["github.copilot.mcpServers"]) settings["github.copilot.mcpServers"] = {};
|
|
1023
|
-
const mcpServers = settings["github.copilot.mcpServers"];
|
|
1216
|
+
const config = JSON.parse(fs.readFileSync(mcpPath, "utf8")) || {};
|
|
1217
|
+
if (!config.servers) config.servers = {};
|
|
1024
1218
|
for (const [id, mcpDef] of Object.entries(registryMcp)) {
|
|
1025
|
-
const existingEntry =
|
|
1219
|
+
const existingEntry = config.servers[id];
|
|
1026
1220
|
const existing = JSON.stringify(existingEntry ?? "");
|
|
1027
1221
|
const hasPlaceholder = existing.includes("__");
|
|
1028
1222
|
const registryWantsCommand = !mcpDef.type && Array.isArray(mcpDef.args);
|
|
1029
1223
|
const existingIsRemote = existingEntry && (existingEntry.type === 'http' || existingEntry.type === 'remote');
|
|
1030
1224
|
const transportMismatch = registryWantsCommand && existingIsRemote;
|
|
1031
1225
|
if (existingEntry && !hasPlaceholder && !transportMismatch) continue;
|
|
1032
|
-
|
|
1226
|
+
config.servers[id] = buildClaudeMcpEntry(id, mcpDef, process.env);
|
|
1033
1227
|
}
|
|
1034
|
-
|
|
1035
|
-
if (!DRY_RUN) fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
1228
|
+
if (!DRY_RUN) fs.writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n");
|
|
1036
1229
|
synced = true;
|
|
1037
|
-
} catch { /* unreadable
|
|
1230
|
+
} catch { /* unreadable mcp.json */ }
|
|
1038
1231
|
}
|
|
1039
1232
|
return synced;
|
|
1040
1233
|
}
|
|
1041
1234
|
|
|
1042
1235
|
// --- Cursor adapter ---
|
|
1043
1236
|
|
|
1044
|
-
function syncCursor(targetDir = null) {
|
|
1237
|
+
function syncCursor(targetDir = null, wants = true) {
|
|
1045
1238
|
const registryMcp = registry.mcpServers ?? {};
|
|
1046
1239
|
if (Object.keys(registryMcp).length === 0) return false;
|
|
1047
1240
|
|
|
1048
|
-
const
|
|
1049
|
-
? path.join(targetDir, ".cursor"
|
|
1050
|
-
: path.join(home, ".cursor"
|
|
1241
|
+
const cursorDir = targetDir
|
|
1242
|
+
? path.join(targetDir, ".cursor")
|
|
1243
|
+
: path.join(home, ".cursor");
|
|
1244
|
+
const cursorMcpPath = path.join(cursorDir, "mcp.json");
|
|
1051
1245
|
|
|
1052
1246
|
// Global scope only updates Cursor's MCP config when the user has already
|
|
1053
1247
|
// initialized `~/.cursor/mcp.json`; we don't conjure user-scope config out
|
|
@@ -1056,6 +1250,35 @@ function syncCursor(targetDir = null) {
|
|
|
1056
1250
|
|
|
1057
1251
|
if (!targetDir && !fs.existsSync(cursorMcpPath)) return false;
|
|
1058
1252
|
|
|
1253
|
+
if (!wants) {
|
|
1254
|
+
if (!DRY_RUN && fs.existsSync(cursorMcpPath)) {
|
|
1255
|
+
try {
|
|
1256
|
+
const config = JSON.parse(fs.readFileSync(cursorMcpPath, "utf8"));
|
|
1257
|
+
const keys = Object.keys(config?.mcpServers ?? {});
|
|
1258
|
+
const allManaged = keys.every(id => id in registryMcp);
|
|
1259
|
+
if (allManaged) {
|
|
1260
|
+
fs.rmSync(cursorMcpPath, { force: true });
|
|
1261
|
+
// Also clean up rules if this was a project sync
|
|
1262
|
+
if (targetDir) {
|
|
1263
|
+
const rulesPath = path.join(cursorDir, "rules", "construct.mdc");
|
|
1264
|
+
if (fs.existsSync(rulesPath)) fs.rmSync(rulesPath, { force: true });
|
|
1265
|
+
// Remove .cursor if empty
|
|
1266
|
+
try {
|
|
1267
|
+
const rulesDir = path.join(cursorDir, "rules");
|
|
1268
|
+
if (fs.existsSync(rulesDir) && fs.readdirSync(rulesDir).length === 0) {
|
|
1269
|
+
fs.rmdirSync(rulesDir);
|
|
1270
|
+
}
|
|
1271
|
+
if (fs.existsSync(cursorDir) && fs.readdirSync(cursorDir).length === 0) {
|
|
1272
|
+
fs.rmdirSync(cursorDir);
|
|
1273
|
+
}
|
|
1274
|
+
} catch { /* ignore non-empty or access errors */ }
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
} catch { /* skip cleanup of unreadable file */ }
|
|
1278
|
+
}
|
|
1279
|
+
return false;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1059
1282
|
let config = { mcpServers: {} };
|
|
1060
1283
|
if (fs.existsSync(cursorMcpPath)) {
|
|
1061
1284
|
try { config = JSON.parse(fs.readFileSync(cursorMcpPath, "utf8")) || { mcpServers: {} }; }
|
|
@@ -1097,12 +1320,31 @@ function syncCursor(targetDir = null) {
|
|
|
1097
1320
|
// --- OpenCode adapter ---
|
|
1098
1321
|
|
|
1099
1322
|
function opencodePermissions(entry) {
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1323
|
+
// Review-only specialists (canEdit:false) must not modify files — honor the
|
|
1324
|
+
// registry's canEdit contract here as Claude does, rather than defaulting every
|
|
1325
|
+
// specialist to edit:allow.
|
|
1326
|
+
|
|
1327
|
+
const perms = entry.permissions
|
|
1328
|
+
? Object.fromEntries(Object.entries(entry.permissions).map(([k, v]) => [k, v]))
|
|
1329
|
+
: { edit: entry.canEdit === false ? "deny" : "allow", bash: "allow" };
|
|
1330
|
+
|
|
1331
|
+
// The orchestrator is the primary write surface; translate its abstract
|
|
1332
|
+
// bash:allow into OpenCode's scoped permission map so destructive commands are
|
|
1333
|
+
// denied and remote/history rewrites prompt — the commit-approval contract
|
|
1334
|
+
// enforced at the tool layer, not just in the prompt. Subagents keep the
|
|
1335
|
+
// abstract grant. This scoped shape is an OpenCode translation detail and stays
|
|
1336
|
+
// out of the cross-adapter registry.
|
|
1337
|
+
|
|
1338
|
+
if (entry.isOrchestrator && perms.bash === "allow") {
|
|
1339
|
+
perms.bash = {
|
|
1340
|
+
"*": "allow",
|
|
1341
|
+
"rm -rf *": "deny",
|
|
1342
|
+
"git push *": "ask",
|
|
1343
|
+
"git push --force*": "ask",
|
|
1344
|
+
"git reset --hard *": "ask",
|
|
1345
|
+
};
|
|
1104
1346
|
}
|
|
1105
|
-
return
|
|
1347
|
+
return perms;
|
|
1106
1348
|
}
|
|
1107
1349
|
|
|
1108
1350
|
function opencodeTaskPermissions(entry) {
|
|
@@ -1112,16 +1354,52 @@ function opencodeTaskPermissions(entry) {
|
|
|
1112
1354
|
};
|
|
1113
1355
|
}
|
|
1114
1356
|
|
|
1115
|
-
function syncOpencode(entries, targetDir = null) {
|
|
1116
|
-
//
|
|
1117
|
-
//
|
|
1118
|
-
//
|
|
1357
|
+
function syncOpencode(entries, targetDir = null, wants = true) {
|
|
1358
|
+
// OpenCode's resolver reads `<project>/opencode.json`, `opencode.jsonc`, or
|
|
1359
|
+
// `<project>/.opencode/opencode.json` — never `.opencode/config.json`. Write the
|
|
1360
|
+
// namespaced `.opencode/opencode.json` so project agents + MCP actually load; a
|
|
1361
|
+
// prior `.opencode/config.json` was silently ignored by the host. Global scope
|
|
1362
|
+
// writes the user-level config and only when it already exists.
|
|
1119
1363
|
|
|
1120
1364
|
const configPath = targetDir
|
|
1121
|
-
? path.join(targetDir, ".opencode", "
|
|
1365
|
+
? path.join(targetDir, ".opencode", "opencode.json")
|
|
1122
1366
|
: findOpenCodeConfigPath();
|
|
1367
|
+
|
|
1123
1368
|
if (!targetDir && !fs.existsSync(configPath)) return false;
|
|
1124
|
-
|
|
1369
|
+
|
|
1370
|
+
if (!wants) {
|
|
1371
|
+
if (!DRY_RUN && fs.existsSync(configPath)) {
|
|
1372
|
+
try {
|
|
1373
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
1374
|
+
const agentKeys = Object.keys(config?.agent ?? {});
|
|
1375
|
+
const allManaged = agentKeys.every(id => id === 'construct' || (registry.specialists || []).some(s => s.name === id.replace(/^cx-/, '')));
|
|
1376
|
+
if (allManaged) {
|
|
1377
|
+
fs.rmSync(configPath, { force: true });
|
|
1378
|
+
if (targetDir) {
|
|
1379
|
+
const pluginsDir = path.join(targetDir, ".opencode", "plugins");
|
|
1380
|
+
if (fs.existsSync(pluginsDir)) fs.rmSync(pluginsDir, { recursive: true, force: true });
|
|
1381
|
+
try {
|
|
1382
|
+
if (fs.readdirSync(path.join(targetDir, ".opencode")).length === 0) {
|
|
1383
|
+
fs.rmdirSync(path.join(targetDir, ".opencode"));
|
|
1384
|
+
}
|
|
1385
|
+
} catch { /* ignore non-empty */ }
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
} catch { /* skip cleanup */ }
|
|
1389
|
+
}
|
|
1390
|
+
return false;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
if (targetDir) {
|
|
1394
|
+
mkdirp(path.dirname(configPath));
|
|
1395
|
+
// Converge a stale `.opencode/config.json` (a path OpenCode never read) onto
|
|
1396
|
+
// the canonical name — rename to preserve content, or drop it if both exist.
|
|
1397
|
+
const legacyPath = path.join(targetDir, ".opencode", "config.json");
|
|
1398
|
+
if (!DRY_RUN && fs.existsSync(legacyPath)) {
|
|
1399
|
+
if (!fs.existsSync(configPath)) fs.renameSync(legacyPath, configPath);
|
|
1400
|
+
else fs.rmSync(legacyPath);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1125
1403
|
|
|
1126
1404
|
const pluginsDir = targetDir
|
|
1127
1405
|
? path.join(targetDir, ".opencode", "plugins")
|
|
@@ -1129,7 +1407,7 @@ function syncOpencode(entries, targetDir = null) {
|
|
|
1129
1407
|
const managedPluginPath = path.join(pluginsDir, "construct-fallback.js");
|
|
1130
1408
|
const toolkitPluginPath = path.join(root, "platforms", "opencode", "plugins", "construct-fallback.js");
|
|
1131
1409
|
|
|
1132
|
-
const writeEntries =
|
|
1410
|
+
const writeEntries = globalEntries(entries);
|
|
1133
1411
|
|
|
1134
1412
|
const { config } = targetDir
|
|
1135
1413
|
? { config: fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {} }
|
|
@@ -1247,6 +1525,14 @@ function syncOpencode(entries, targetDir = null) {
|
|
|
1247
1525
|
};
|
|
1248
1526
|
}
|
|
1249
1527
|
|
|
1528
|
+
// Seed a cheap auxiliary model for titles and summaries when the user has not
|
|
1529
|
+
// chosen one — a cost lever only. The primary `model` stays the user's choice
|
|
1530
|
+
// and is never written. Global scope only; project configs inherit it.
|
|
1531
|
+
|
|
1532
|
+
if (!targetDir && config.small_model === undefined) {
|
|
1533
|
+
config.small_model = "anthropic/claude-haiku-4-5-20251001";
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1250
1536
|
writeOpenCodeConfig(config, configPath);
|
|
1251
1537
|
|
|
1252
1538
|
const sourcePluginsDir = path.join(root, "platforms", "opencode", "plugins");
|
|
@@ -1427,38 +1713,38 @@ if (COMPRESS_PERSONAS) {
|
|
|
1427
1713
|
acquireLock();
|
|
1428
1714
|
try {
|
|
1429
1715
|
if (projectDir) {
|
|
1430
|
-
syncClaude(entries, projectDir);
|
|
1431
|
-
syncCodex(entries, projectDir);
|
|
1432
|
-
syncCopilot(entries, projectDir);
|
|
1433
|
-
const opencodeOk = syncOpencode(entries, projectDir);
|
|
1434
|
-
const vscodeOk = syncVSCode(projectDir);
|
|
1435
|
-
const cursorOk = syncCursor(projectDir);
|
|
1436
|
-
const cmdCount = syncCommands(projectDir);
|
|
1437
|
-
const skillCount = syncSkills(projectDir);
|
|
1716
|
+
syncClaude(entries, projectDir, wantsHost("claude"));
|
|
1717
|
+
syncCodex(entries, projectDir, wantsHost("codex"));
|
|
1718
|
+
syncCopilot(entries, projectDir, wantsHost("copilot"));
|
|
1719
|
+
const opencodeOk = syncOpencode(entries, projectDir, wantsHost("opencode"));
|
|
1720
|
+
const vscodeOk = syncVSCode(projectDir, wantsHost("vscode"));
|
|
1721
|
+
const cursorOk = syncCursor(projectDir, wantsHost("cursor"));
|
|
1722
|
+
const cmdCount = wantsHost("claude") ? syncCommands(projectDir) : 0;
|
|
1723
|
+
const skillCount = wantsHost("claude") ? syncSkills(projectDir) : 0;
|
|
1438
1724
|
|
|
1439
1725
|
if (DRY_RUN) {
|
|
1440
1726
|
printDryRunDiff();
|
|
1441
1727
|
} else {
|
|
1442
1728
|
commitStaging();
|
|
1443
1729
|
const targets = [
|
|
1444
|
-
"Claude Code",
|
|
1445
|
-
"Codex",
|
|
1446
|
-
"Copilot",
|
|
1730
|
+
wantsHost("claude") && "Claude Code",
|
|
1731
|
+
wantsHost("codex") && "Codex",
|
|
1732
|
+
wantsHost("copilot") && "Copilot",
|
|
1447
1733
|
opencodeOk && "OpenCode",
|
|
1448
1734
|
vscodeOk && "VS Code",
|
|
1449
1735
|
cursorOk && "Cursor",
|
|
1450
1736
|
].filter(Boolean).join(", ");
|
|
1451
|
-
|
|
1737
|
+
summary(`Synced ${entries.length} agents + ${cmdCount} commands + ${skillCount} skills to ${path.relative(process.cwd(), projectDir) || "."} (project mode → ${targets}).`);
|
|
1452
1738
|
}
|
|
1453
1739
|
} else {
|
|
1454
1740
|
const personaCount = entries.filter((e) => e.isOrchestrator).length;
|
|
1455
1741
|
|
|
1456
|
-
syncCodex(entries);
|
|
1457
|
-
syncClaude(entries);
|
|
1458
|
-
syncCopilot(entries);
|
|
1459
|
-
const opencodeOk = syncOpencode(entries);
|
|
1460
|
-
const vscodeOk = syncVSCode();
|
|
1461
|
-
const cursorOk = syncCursor();
|
|
1742
|
+
syncCodex(entries, null, wantsHost("codex"));
|
|
1743
|
+
syncClaude(entries, null, wantsHost("claude"));
|
|
1744
|
+
syncCopilot(entries, null, wantsHost("copilot"));
|
|
1745
|
+
const opencodeOk = syncOpencode(entries, null, wantsHost("opencode"));
|
|
1746
|
+
const vscodeOk = syncVSCode(null, wantsHost("vscode"));
|
|
1747
|
+
const cursorOk = syncCursor(null, wantsHost("cursor"));
|
|
1462
1748
|
syncCommands();
|
|
1463
1749
|
syncSkills();
|
|
1464
1750
|
|
|
@@ -1474,11 +1760,11 @@ try {
|
|
|
1474
1760
|
vscodeOk && "VS Code",
|
|
1475
1761
|
cursorOk && "Cursor",
|
|
1476
1762
|
].filter(Boolean).join(", ");
|
|
1477
|
-
|
|
1763
|
+
summary(`Synced ${personaCount} front-door agent to global scope (${targets}). Specialists, commands, and skills are project-only — run \`construct init\` inside a project to scaffold them.`);
|
|
1478
1764
|
|
|
1479
1765
|
const completionsDir = generateCompletions();
|
|
1480
1766
|
if (completionsDir) {
|
|
1481
|
-
|
|
1767
|
+
summary(`Completions updated → ${completionsDir}`);
|
|
1482
1768
|
}
|
|
1483
1769
|
}
|
|
1484
1770
|
}
|