@opengsd/gsd-core 1.16.0 → 1.17.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -2
- package/.opencode/plugins/gsd-core.js +251 -1
- package/.opencode/plugins/gsd-statusline/tui.tsx +55 -0
- package/GEMINI.md +1 -1
- package/agents/gsd-debug-session-manager.compact.md +6 -0
- package/agents/gsd-debug-session-manager.md +6 -0
- package/bin/gsd-mcp-server.js +3 -1
- package/bin/install.js +361 -94
- package/gsd-core/bin/gsd-tools.cjs +17 -2
- package/gsd-core/bin/lib/adr-parser.cjs +37 -5
- package/gsd-core/bin/lib/capability-registry.cjs +132 -80
- package/gsd-core/bin/lib/capability-validator.cjs +2 -1
- package/gsd-core/bin/lib/capability-writer.cjs +2 -1
- package/gsd-core/bin/lib/commands.cjs +28 -8
- package/gsd-core/bin/lib/gate-context-drift.cjs +5 -4
- package/gsd-core/bin/lib/gate-ui-safety.cjs +2 -1
- package/gsd-core/bin/lib/graphify.cjs +3 -2
- package/gsd-core/bin/lib/init.cjs +32 -12
- package/gsd-core/bin/lib/install-engine.cjs +11 -0
- package/gsd-core/bin/lib/install-shadow-report.cjs +12 -0
- package/gsd-core/bin/lib/installer-migrations/011-opencode-mcp-no-prompts.cjs +99 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +54 -1
- package/gsd-core/bin/lib/jsonc-edit.cjs +418 -0
- package/gsd-core/bin/lib/loop-host-contract.cjs +2 -1
- package/gsd-core/bin/lib/mcp-server.cjs +5 -1
- package/gsd-core/bin/lib/opencode-mcp-entry.cjs +39 -0
- package/gsd-core/bin/lib/opencode-version.cjs +486 -0
- package/gsd-core/bin/lib/phase.cjs +21 -0
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +2 -1
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +95 -4
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +58 -3
- package/gsd-core/bin/lib/runtime-slash.cjs +13 -0
- package/gsd-core/bin/lib/smart-entry.cjs +92 -53
- package/gsd-core/bin/lib/state-transition.cjs +67 -9
- package/gsd-core/bin/lib/state.cjs +454 -66
- package/gsd-core/bin/lib/update-context.cjs +18 -1
- package/gsd-core/bin/lib/verification.cjs +107 -17
- package/gsd-core/workflows/code-review.md +16 -1
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +1 -1
- package/gsd-core/workflows/execute-phase.md +3 -1
- package/gsd-core/workflows/resume-project.md +14 -10
- package/hooks/dist/gsd-check-update.js +26 -20
- package/hooks/dist/gsd-context-monitor.js +14 -5
- package/hooks/dist/gsd-secret-read-guard.js +117 -1
- package/hooks/dist/gsd-statusline.js +321 -22
- package/hooks/gsd-check-update.js +26 -20
- package/hooks/gsd-context-monitor.js +14 -5
- package/hooks/gsd-secret-read-guard.js +117 -1
- package/hooks/gsd-statusline.js +321 -22
- package/package.json +1 -1
- package/scripts/gen-capability-registry.cjs +7 -1
- package/scripts/gen-loop-host-contract.cjs +4 -1
- package/scripts/lib/macos-conformance-tier.generated.cjs +4 -0
- package/scripts/lib/platform-conformance-tier.generated.cjs +10 -0
- package/scripts/mutation-matrix.cjs +3 -1
- package/scripts/release-tarball-smoke.cjs +14 -7
- package/vscode/package.json +1 -1
|
@@ -1936,6 +1936,7 @@ const KNOWN_HOST_BEHAVIORS = new Set([
|
|
|
1936
1936
|
'managedHookEvents',
|
|
1937
1937
|
'mcpCompanion',
|
|
1938
1938
|
'namedSubagentsSupported',
|
|
1939
|
+
'nativeCliPlugin',
|
|
1939
1940
|
'nativeModelAliases',
|
|
1940
1941
|
'nativePlugin',
|
|
1941
1942
|
'noPathRewrite',
|
|
@@ -2572,7 +2573,7 @@ function validateLaneProbe(ctx, probe) {
|
|
|
2572
2573
|
if (!isPositiveIntegerMs(probe.timeoutMs)) {
|
|
2573
2574
|
errors.push(
|
|
2574
2575
|
ctx + ' reviewer.probe.timeoutMs must be a positive integer of milliseconds for kind "' +
|
|
2575
|
-
probe.kind + '" — an unbounded probe hangs every /gsd
|
|
2576
|
+
probe.kind + '" — an unbounded probe hangs every /gsd-review invocation ' +
|
|
2576
2577
|
'(got: ' + describeValue(probe.timeoutMs) + ')',
|
|
2577
2578
|
);
|
|
2578
2579
|
}
|
|
@@ -50,6 +50,7 @@ const { planningDir } = planningWorkspaceMod;
|
|
|
50
50
|
const nodefs = require("fs");
|
|
51
51
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
52
52
|
const nodepath = require("path");
|
|
53
|
+
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
53
54
|
// ─── Implementation ───────────────────────────────────────────────────────────
|
|
54
55
|
/**
|
|
55
56
|
* Write-side capability state mutator.
|
|
@@ -305,7 +306,7 @@ function setCapabilityState(cwd, runtimeConfigDir, desired, opts) {
|
|
|
305
306
|
if (entry.enabled === true) {
|
|
306
307
|
const afterCap = after.capabilities.find((c) => c.id === entry.id);
|
|
307
308
|
if (afterCap && afterCap.enabled !== true) {
|
|
308
|
-
errors.push(`cannot enable "${entry.id}": not in the active surface/profile (widen the profile or use
|
|
309
|
+
errors.push(`cannot enable "${entry.id}": not in the active surface/profile (widen the profile or use ${(0, runtime_slash_cjs_1.runtimeCommand)(cwd, 'surface enable')})`);
|
|
309
310
|
}
|
|
310
311
|
}
|
|
311
312
|
// Fix B: desired enabled=false — assert it is actually disabled after write.
|
|
@@ -1233,6 +1233,24 @@ function cmdEffortSyncOpencode(cwd, raw, dryRun, configDir) {
|
|
|
1233
1233
|
// null — it would leave an unresolvable `variant: null` key on disk
|
|
1234
1234
|
// forever. Test presence with its own regex, and only compare values
|
|
1235
1235
|
// once presence is known.
|
|
1236
|
+
// Native OpenCode 2.x form (ADR-5331 D5): the effort rides on the model reference as
|
|
1237
|
+
// `provider/model#variant`, with no separate `variant:` key. When the agent's `model:` carries a
|
|
1238
|
+
// `#` suffix that suffix IS the effort, so the sync edits the model instead of adding a second,
|
|
1239
|
+
// conflicting `variant:` line.
|
|
1240
|
+
const modelLineMatch = /^model:[ \t]*(.+?)[ \t]*$/m.exec(fmMatch.body);
|
|
1241
|
+
const modelValue = modelLineMatch ? modelLineMatch[1].replace(/\r$/, '').replace(/^(["'])(.*)\1$/, '$2') : null;
|
|
1242
|
+
const hashAt = modelValue === null ? -1 : modelValue.lastIndexOf('#');
|
|
1243
|
+
let modelFormContent = null;
|
|
1244
|
+
let modelFormFrom = null;
|
|
1245
|
+
if (modelValue !== null && hashAt > 0) {
|
|
1246
|
+
const base = modelValue.slice(0, hashAt);
|
|
1247
|
+
modelFormFrom = modelValue.slice(hashAt + 1);
|
|
1248
|
+
if (target !== null && modelFormFrom === target) {
|
|
1249
|
+
skipped++;
|
|
1250
|
+
continue;
|
|
1251
|
+
}
|
|
1252
|
+
modelFormContent = setFrontmatterKeyLine(content, 'model', target === null ? base : `${base}#${target}`);
|
|
1253
|
+
}
|
|
1236
1254
|
const variantPresent = /^variant:/m.test(fmMatch.body);
|
|
1237
1255
|
const variantMatch = /^variant:[ \t]*(.+?)[ \t]*$/m.exec(fmMatch.body);
|
|
1238
1256
|
// `null` (key absent) and `''` (key present, value empty) are distinct
|
|
@@ -1241,17 +1259,19 @@ function cmdEffortSyncOpencode(cwd, raw, dryRun, configDir) {
|
|
|
1241
1259
|
// "empty key" indistinguishable in the sync output, even though only one
|
|
1242
1260
|
// of them actually has a `variant:` line to remove.
|
|
1243
1261
|
const currentVariant = variantPresent ? (variantMatch ? variantMatch[1] : '') : null;
|
|
1244
|
-
if (
|
|
1245
|
-
if (
|
|
1262
|
+
if (modelFormContent === null) {
|
|
1263
|
+
if (target === null) {
|
|
1264
|
+
if (!variantPresent) {
|
|
1265
|
+
skipped++;
|
|
1266
|
+
continue;
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
else if (variantPresent && currentVariant === target) {
|
|
1246
1270
|
skipped++;
|
|
1247
1271
|
continue;
|
|
1248
1272
|
}
|
|
1249
1273
|
}
|
|
1250
|
-
|
|
1251
|
-
skipped++;
|
|
1252
|
-
continue;
|
|
1253
|
-
}
|
|
1254
|
-
changes.push({ agent: agentName, from: currentVariant, to: target });
|
|
1274
|
+
changes.push({ agent: agentName, from: modelFormContent === null ? currentVariant : modelFormFrom, to: target });
|
|
1255
1275
|
synced++;
|
|
1256
1276
|
if (!dryRun) {
|
|
1257
1277
|
// Atomic publish AND mode preservation, same discipline as
|
|
@@ -1279,7 +1299,7 @@ function cmdEffortSyncOpencode(cwd, raw, dryRun, configDir) {
|
|
|
1279
1299
|
}
|
|
1280
1300
|
catch { /* non-fatal: fall back to writing without an explicit mode */ }
|
|
1281
1301
|
try {
|
|
1282
|
-
node_fs_1.default.writeFileSync(tmpPath, target === null ? removeFrontmatterKeyLine(content, 'variant') : setFrontmatterKeyLine(content, 'variant', target), originalMode !== undefined ? { mode: originalMode } : undefined);
|
|
1302
|
+
node_fs_1.default.writeFileSync(tmpPath, modelFormContent ?? (target === null ? removeFrontmatterKeyLine(content, 'variant') : setFrontmatterKeyLine(content, 'variant', target)), originalMode !== undefined ? { mode: originalMode } : undefined);
|
|
1283
1303
|
// Not redundant with the `mode` option above: `mode` only applies
|
|
1284
1304
|
// when the file is actually created (O_CREAT). A leftover tmp file
|
|
1285
1305
|
// from an earlier crashed run would be reused (truncated) at its OLD
|
|
@@ -28,6 +28,7 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
28
28
|
const gate_verdict_cjs_1 = require("./gate-verdict.cjs");
|
|
29
29
|
const gate_evidence_cjs_1 = require("./gate-evidence.cjs");
|
|
30
30
|
const gate_config_cjs_1 = require("./gate-config.cjs");
|
|
31
|
+
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
31
32
|
const gate_phase_context_cjs_1 = require("./gate-phase-context.cjs");
|
|
32
33
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-workspace.cjs is an export= CommonJS module
|
|
33
34
|
const planningWorkspace = require("./planning-workspace.cjs");
|
|
@@ -45,13 +46,13 @@ const { defaultPhaseCleanCommitTimesMs } = verificationMod;
|
|
|
45
46
|
function computeContextDrift(contextEffectiveMs, entries) {
|
|
46
47
|
return entries.filter((e) => e.effectiveMs < contextEffectiveMs).map((e) => e.file);
|
|
47
48
|
}
|
|
48
|
-
function buildContextDriftMessage(staleArtifacts, phaseArg) {
|
|
49
|
+
function buildContextDriftMessage(staleArtifacts, phaseArg, projectDir) {
|
|
49
50
|
const parts = [`CONTEXT.md decisions are newer than: ${staleArtifacts.join(', ')}.`];
|
|
50
51
|
if (staleArtifacts.some((f) => f.endsWith('-RESEARCH.md'))) {
|
|
51
|
-
parts.push(`Regenerate research:
|
|
52
|
+
parts.push(`Regenerate research: ${(0, runtime_slash_cjs_1.runtimeCommand)(projectDir, `plan-phase ${phaseArg} --research`)}.`);
|
|
52
53
|
}
|
|
53
54
|
if (staleArtifacts.some((f) => f.endsWith('-PATTERNS.md'))) {
|
|
54
|
-
parts.push(
|
|
55
|
+
parts.push(`Regenerate patterns: delete the PATTERNS.md file, then re-run ${(0, runtime_slash_cjs_1.runtimeCommand)(projectDir, 'plan-phase')}.`);
|
|
55
56
|
}
|
|
56
57
|
if (staleArtifacts.some((f) => f.endsWith('-VALIDATION.md') || (f.endsWith('-SPEC.md') && !f.endsWith('-AI-SPEC.md') && !f.endsWith('-UI-SPEC.md')))) {
|
|
57
58
|
parts.push('Regenerate or manually reconcile VALIDATION.md / SPEC.md against the current decisions.');
|
|
@@ -114,7 +115,7 @@ function runContextDriftGate(cwd, phaseArg) {
|
|
|
114
115
|
// missing or malformed config is "key absent", nothing is printed).
|
|
115
116
|
const action = (0, gate_config_cjs_1.readWorkflowConfigValue)(cwd, 'workflow.context_drift_action').value === 'block' ? 'block' : 'warn';
|
|
116
117
|
const block = staleArtifacts.length > 0 && action === 'block';
|
|
117
|
-
const message = staleArtifacts.length > 0 ? buildContextDriftMessage(staleArtifacts, phaseArg) : '';
|
|
118
|
+
const message = staleArtifacts.length > 0 ? buildContextDriftMessage(staleArtifacts, phaseArg, cwd) : '';
|
|
118
119
|
return (0, gate_verdict_cjs_1.gateVerdict)(block ? 'block' : (staleArtifacts.length > 0 ? 'advisory' : 'pass'), block, {
|
|
119
120
|
block,
|
|
120
121
|
skipped: false,
|
|
@@ -19,6 +19,7 @@ exports.evaluateUiSafetyGate = evaluateUiSafetyGate;
|
|
|
19
19
|
const gate_verdict_cjs_1 = require("./gate-verdict.cjs");
|
|
20
20
|
const gate_phase_context_cjs_1 = require("./gate-phase-context.cjs");
|
|
21
21
|
const gate_evaluation_scope_cjs_1 = require("./gate-evaluation-scope.cjs");
|
|
22
|
+
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
22
23
|
const ui_safety_gate_cjs_1 = require("./ui-safety-gate.cjs");
|
|
23
24
|
const UI_FILE_EXTENSIONS_RE = /\.(tsx|jsx|css|scss|sass|less|vue|svelte|html)$/i;
|
|
24
25
|
const UI_PATH_PATTERNS_RE = /\/(components|pages|views|screens|layouts|ui|frontend)\//i;
|
|
@@ -52,7 +53,7 @@ function computeUiSafetyGate(projectDir, phase) {
|
|
|
52
53
|
const result = { frontend, hasUiFiles, hasUiSpec, block };
|
|
53
54
|
if (block) {
|
|
54
55
|
result.message = `UI files changed in this wave but no UI-SPEC.md exists for Phase ${phase}. ` +
|
|
55
|
-
`Run
|
|
56
|
+
`Run ${(0, runtime_slash_cjs_1.runtimeCommand)(projectDir, `ui-phase ${phase}`)} to generate the design contract before continuing.`;
|
|
56
57
|
}
|
|
57
58
|
if (phaseLookupFailed)
|
|
58
59
|
result.phaseLookupFailed = true;
|
|
@@ -13,6 +13,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
13
13
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
14
14
|
const node_path_1 = __importDefault(require("node:path"));
|
|
15
15
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
16
|
+
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
16
17
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
17
18
|
const capabilityStateMod = require("./capability-state.cjs");
|
|
18
19
|
const { isCapabilityActive } = capabilityStateMod;
|
|
@@ -408,7 +409,7 @@ function graphifyQuery(cwd, term, options = {}) {
|
|
|
408
409
|
const { graphPath, configured } = resolveGraphLocation(cwd, planningDir);
|
|
409
410
|
if (!node_fs_1.default.existsSync(graphPath)) {
|
|
410
411
|
return { error: configured
|
|
411
|
-
? `Configured graph not found at ${graphPath}. Set graphify.graph_path or run
|
|
412
|
+
? `Configured graph not found at ${graphPath}. Set graphify.graph_path or run ${(0, runtime_slash_cjs_1.runtimeCommand)(cwd, 'graphify build')}.`
|
|
412
413
|
: 'No graph built yet. Run graphify build first.' };
|
|
413
414
|
}
|
|
414
415
|
const graph = safeReadJson(graphPath);
|
|
@@ -446,7 +447,7 @@ function graphifyStatus(cwd) {
|
|
|
446
447
|
const { graphPath, configured } = resolveGraphLocation(cwd, planningDir);
|
|
447
448
|
if (!node_fs_1.default.existsSync(graphPath)) {
|
|
448
449
|
return { exists: false, graph_path: graphPath, message: configured
|
|
449
|
-
? `Configured graph not found at ${graphPath}. Set graphify.graph_path or run
|
|
450
|
+
? `Configured graph not found at ${graphPath}. Set graphify.graph_path or run ${(0, runtime_slash_cjs_1.runtimeCommand)(cwd, 'graphify build')}.`
|
|
450
451
|
: 'No graph built yet. Run graphify build to create one.' };
|
|
451
452
|
}
|
|
452
453
|
const stat = node_fs_1.default.statSync(graphPath);
|
|
@@ -157,6 +157,24 @@ function slugifyPhaseName(phaseName) {
|
|
|
157
157
|
// the same reported phase_slug.
|
|
158
158
|
return phaseName ? coreUtils.generateSlugInternal(phaseName, null) : null;
|
|
159
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* #4854: the one place that decides which name an `init` verb emits as
|
|
162
|
+
* `phase_name`. When a phase directory exists, `searchPhaseInDir` derives
|
|
163
|
+
* `phaseInfo.phase_name` from the directory-name remainder — already slugified
|
|
164
|
+
* (`phase.add` writes `${num}-${slug}` dirs), so it comes out byte-identical to
|
|
165
|
+
* `phase_slug`. The ROADMAP carries the curated display name
|
|
166
|
+
* (`### Phase N: <Name>`), so it wins; the disk-derived name is the fallback
|
|
167
|
+
* for a phase the ROADMAP does not describe, then `null`. #3171 fixed this
|
|
168
|
+
* inline for `init execute-phase` only; plan-phase (whose `phase_name` reaches
|
|
169
|
+
* STATE.md through `state planned-phase --name`), verify-work, code-review,
|
|
170
|
+
* discuss-phase-assumptions and phase-op read the raw disk value. `phase_slug`
|
|
171
|
+
* is deliberately not routed through here — it stays disk-derived.
|
|
172
|
+
*/
|
|
173
|
+
function resolvePhaseDisplayName(phaseInfo, roadmapPhase) {
|
|
174
|
+
return roadmapPhase?.['phase_name']
|
|
175
|
+
|| phaseInfo?.['phase_name']
|
|
176
|
+
|| null;
|
|
177
|
+
}
|
|
160
178
|
/**
|
|
161
179
|
* #2994 (review finding, DEFECT.GENERATIVE-FIX): shared archived/not-found
|
|
162
180
|
* fallback applied identically by `cmdInitExecutePhase`, `cmdInitPlanPhase`,
|
|
@@ -164,10 +182,11 @@ function slugifyPhaseName(phaseName) {
|
|
|
164
182
|
* `cmdInitDiscussPhaseAssumptions` — 6 call sites previously reproducing the
|
|
165
183
|
* exact same two-branch control flow verbatim (only the synthetic
|
|
166
184
|
* replacement object's field set differs per caller, supplied here via
|
|
167
|
-
* `buildFallback`). `cmdInitPhaseOp`
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
185
|
+
* `buildFallback`). `cmdInitPhaseOp` does not use this helper (its own fallback
|
|
186
|
+
* object differs by one field, `has_reviews` absent, so it is not a
|
|
187
|
+
* byte-identical copy), but since #4854 it resolves `roadmapPhase` once up
|
|
188
|
+
* front like the others, so `resolvePhaseDisplayName` sees it in the ordinary
|
|
189
|
+
* case too.
|
|
171
190
|
*
|
|
172
191
|
* Behavior-preserving by construction: every original call site either (a)
|
|
173
192
|
* unconditionally computed `roadmapPhase` once up front and then applied
|
|
@@ -924,7 +943,7 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
|
|
|
924
943
|
// the human-curated display name (`### Phase N: <Name>`); prefer it,
|
|
925
944
|
// matching the no-disk fallback above. phase_slug stays disk-derived — it
|
|
926
945
|
// correctly feeds branch-name construction below and is unchanged here.
|
|
927
|
-
phase_name: (roadmapPhase
|
|
946
|
+
phase_name: resolvePhaseDisplayName(phaseInfo, roadmapPhase),
|
|
928
947
|
phase_slug: phaseInfo?.['phase_slug'] || null,
|
|
929
948
|
phase_req_ids,
|
|
930
949
|
plans: phaseInfo?.['plans'] || [],
|
|
@@ -1043,7 +1062,7 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
|
|
|
1043
1062
|
const phase_req_ids = reqExtracted && reqExtracted !== 'TBD' ? reqExtracted : null;
|
|
1044
1063
|
const phaseDirPlan = phaseInfo?.['directory'] || null;
|
|
1045
1064
|
const phaseNumberPlan = phaseInfo?.['phase_number'] || null;
|
|
1046
|
-
const phaseNamePlan = phaseInfo
|
|
1065
|
+
const phaseNamePlan = resolvePhaseDisplayName(phaseInfo, roadmapPhase);
|
|
1047
1066
|
const rawProjectCodePlan = config.project_code || '';
|
|
1048
1067
|
let expectedPhaseDirPlan = null;
|
|
1049
1068
|
if (!phaseDirPlan && phaseNumberPlan && phaseNamePlan) {
|
|
@@ -1689,7 +1708,7 @@ function cmdInitVerifyWork(cwd, phase, raw) {
|
|
|
1689
1708
|
// itself stays relative — evaluateUatPassed above still joins it against cwd.
|
|
1690
1709
|
phase_dir: phaseDir ? toPosixPath(node_path_1.default.join(cwd, phaseDir)) : null,
|
|
1691
1710
|
phase_number: phaseInfo?.['phase_number'] || null,
|
|
1692
|
-
phase_name: phaseInfo
|
|
1711
|
+
phase_name: resolvePhaseDisplayName(phaseInfo, roadmapPhase),
|
|
1693
1712
|
// #2376: verify-work.md's plan_gap_closure step reads these instead of
|
|
1694
1713
|
// hardcoding '.planning/STATE.md' / '.planning/ROADMAP.md' literals.
|
|
1695
1714
|
state_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
|
|
@@ -1761,7 +1780,7 @@ function cmdInitCodeReview(cwd, phase, raw, options = {}) {
|
|
|
1761
1780
|
});
|
|
1762
1781
|
const phaseDir = phaseInfo?.['directory'] || null;
|
|
1763
1782
|
const phaseNumber = phaseInfo?.['phase_number'] || null;
|
|
1764
|
-
const phaseName = phaseInfo
|
|
1783
|
+
const phaseName = resolvePhaseDisplayName(phaseInfo, roadmapPhase);
|
|
1765
1784
|
const fallow = detectFallowConfig(cwd);
|
|
1766
1785
|
const result = {
|
|
1767
1786
|
commit_docs: config.commit_docs,
|
|
@@ -1890,7 +1909,7 @@ function cmdInitDiscussPhaseAssumptions(cwd, phase, raw, options = {}) {
|
|
|
1890
1909
|
});
|
|
1891
1910
|
const phaseDir = phaseInfo?.['directory'] || null;
|
|
1892
1911
|
const phaseNumber = phaseInfo?.['phase_number'] || null;
|
|
1893
|
-
const phaseName = phaseInfo
|
|
1912
|
+
const phaseName = resolvePhaseDisplayName(phaseInfo, roadmapPhase);
|
|
1894
1913
|
// #2994: mirrors discuss-phase-assumptions.md's own auto_advance step
|
|
1895
1914
|
// resolver — `--auto` flag OR the consolidated `check auto-mode --pick
|
|
1896
1915
|
// active` fact (workflow._auto_chain_active OR workflow.auto_advance).
|
|
@@ -1942,8 +1961,10 @@ function cmdInitPhaseOp(cwd, phase, raw) {
|
|
|
1942
1961
|
output(withProjectRoot(cwd, result), raw);
|
|
1943
1962
|
return;
|
|
1944
1963
|
}
|
|
1964
|
+
// #4854: resolved once, up front (a pure read — guardedGetRoadmapPhase has no side effects),
|
|
1965
|
+
// so the ordinary case (phase dir on disk, not archived) also sees the ROADMAP display name.
|
|
1966
|
+
const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
|
|
1945
1967
|
if (phaseInfo?.['archived']) {
|
|
1946
|
-
const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
|
|
1947
1968
|
if (roadmapPhase?.['found']) {
|
|
1948
1969
|
const phaseName = roadmapPhase['phase_name'];
|
|
1949
1970
|
phaseInfo = {
|
|
@@ -1966,7 +1987,6 @@ function cmdInitPhaseOp(cwd, phase, raw) {
|
|
|
1966
1987
|
}
|
|
1967
1988
|
}
|
|
1968
1989
|
if (!phaseInfo) {
|
|
1969
|
-
const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
|
|
1970
1990
|
if (roadmapPhase?.['found']) {
|
|
1971
1991
|
const phaseName = roadmapPhase['phase_name'];
|
|
1972
1992
|
phaseInfo = {
|
|
@@ -1990,7 +2010,7 @@ function cmdInitPhaseOp(cwd, phase, raw) {
|
|
|
1990
2010
|
}
|
|
1991
2011
|
const phaseDir = phaseInfo?.['directory'] || null;
|
|
1992
2012
|
const phaseNumber = phaseInfo?.['phase_number'] || null;
|
|
1993
|
-
const phaseName = phaseInfo
|
|
2013
|
+
const phaseName = resolvePhaseDisplayName(phaseInfo, roadmapPhase);
|
|
1994
2014
|
const rawProjectCode = config.project_code || '';
|
|
1995
2015
|
let expectedPhaseDir = null;
|
|
1996
2016
|
if (!phaseDir && phaseNumber && phaseName) {
|
|
@@ -1741,6 +1741,17 @@ function _installNativePluginIfDeclared(runtime, configDir, behaviors, src) {
|
|
|
1741
1741
|
}
|
|
1742
1742
|
}
|
|
1743
1743
|
}
|
|
1744
|
+
// OpenCode 2.x terminal-client plugin (ADR-5331 Phase 3): a TUI entrypoint kept in its own directory under
|
|
1745
|
+
// plugins/. OpenCode 1.x scans plugins/*.js|ts only, so it never loads this directory.
|
|
1746
|
+
const cli = behaviors.nativeCliPlugin;
|
|
1747
|
+
if (cli && cli.source) {
|
|
1748
|
+
const cliSrc = node_path_1.default.join(src, cli.source);
|
|
1749
|
+
if (installFs().existsSync(cliSrc)) {
|
|
1750
|
+
const cliDest = runtimeArtifactInstallPlan.assertDestWithinConfigHome(configDir, node_path_1.default.join(cli.dir, cli.file));
|
|
1751
|
+
installFs().mkdirSync(node_path_1.default.dirname(cliDest), { recursive: true });
|
|
1752
|
+
installFs().copyFileSync(cliSrc, cliDest);
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1744
1755
|
}
|
|
1745
1756
|
// ---------------------------------------------------------------------------
|
|
1746
1757
|
// _migrateLegacyOpencodeCommandDir
|
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
* (design row #5 / "Not-corruption"). `kindsDiffer` on `ShadowReport` is what
|
|
24
24
|
* lets `renderShadowReport` word the two cases correctly.
|
|
25
25
|
*
|
|
26
|
+
* Same-scope pairs are not reported: a runtime whose layout installs `commands` and `skills` side by side at one
|
|
27
|
+
* scope (OpenCode, Kilo, Augment, CodeBuddy, ZCode) writes both on purpose, and `isGenuinelyShadowed` requires the
|
|
28
|
+
* winner and the loser to sit in different scopes (#5344).
|
|
29
|
+
*
|
|
26
30
|
* ── Report, don't correct (mirrors the resolver's own law) ─────────────────
|
|
27
31
|
* `mismatches` surfaces a declared runtime/scope that disagrees with the
|
|
28
32
|
* probed one (Postel's Law, design doc: liberal in what is accepted, but the
|
|
@@ -218,6 +222,14 @@ function stemFromTrigger(trigger, prefix) {
|
|
|
218
222
|
function isGenuinelyShadowed(t, scopeRecords, prefixFor) {
|
|
219
223
|
if (t.shadowedBy === null)
|
|
220
224
|
return false;
|
|
225
|
+
// This is a CROSS-SCOPE report (see the module comment). A winner and a loser in the SAME scope are the two
|
|
226
|
+
// kinds one install deliberately wrote side by side — OpenCode, Kilo, Augment, CodeBuddy and ZCode install
|
|
227
|
+
// `commands` and `skills` for every trigger at one scope. GSD wrote both, the operator cannot act on it, and
|
|
228
|
+
// the host's own dedupe (OpenCode 1.x lists the command and drops the same-named skill; 2.x keeps them on
|
|
229
|
+
// separate surfaces) is not a loss of reachability. Reporting it printed "72 triggers shadowed" on every
|
|
230
|
+
// install of those five runtimes (#5344).
|
|
231
|
+
if (t.shadowedBy.scope === t.scope)
|
|
232
|
+
return false;
|
|
221
233
|
const winnerRecord = scopeRecords.get(t.shadowedBy.scope);
|
|
222
234
|
const shadowedRecord = scopeRecords.get(t.scope);
|
|
223
235
|
const winnerStem = stemFromTrigger(t.trigger, prefixFor(t.shadowedBy.scope, t.shadowedBy.kind));
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Installer migration: stop the GSD MCP companion from duplicating every GSD command in OpenCode
|
|
4
|
+
* (#5338; epic #5331, ADR-5331 D6).
|
|
5
|
+
*
|
|
6
|
+
* OpenCode turns each MCP prompt into a `<server>:<prompt>` slash command. The GSD companion server
|
|
7
|
+
* advertised its whole prompt catalog, so with the command files the installer already writes every GSD
|
|
8
|
+
* command appeared twice (`gsd-plan-phase` and `gsd:plan-phase`; 143 entries against 72 observed on
|
|
9
|
+
* OpenCode 2.0.27). New installs register the server with `--no-prompts`. This migration converges an
|
|
10
|
+
* existing install by adding that flag to the `gsd` MCP entry GSD itself wrote — and only that entry: a
|
|
11
|
+
* `gsd` server the user edited or defined does not match the installer's shape and is never touched.
|
|
12
|
+
*
|
|
13
|
+
* It applies to OpenCode on both generations (the entry is the same shape), covers the 1.x `mcp.gsd` and
|
|
14
|
+
* the 2.x native `mcp.servers.gsd` locations, and edits the config as text so a commented `opencode.jsonc`
|
|
15
|
+
* keeps its comments and layout (#5334).
|
|
16
|
+
*
|
|
17
|
+
* Is the action safe in non-interactive install?
|
|
18
|
+
* Yes. `rewrite-json` is non-interactive and journaled; the rewrite is a one-token append to a command
|
|
19
|
+
* array the installer wrote.
|
|
20
|
+
*
|
|
21
|
+
* See docs/installer-migrations.md#shipped-migrations and the OpenCode row of
|
|
22
|
+
* docs/installer-migrations.md#runtime-configuration-contract-registry.
|
|
23
|
+
*/
|
|
24
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
25
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
26
|
+
};
|
|
27
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
28
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
29
|
+
const jsonc_edit_cjs_1 = require("../jsonc-edit.cjs");
|
|
30
|
+
const opencode_mcp_entry_cjs_1 = require("../opencode-mcp-entry.cjs");
|
|
31
|
+
/** OpenCode reads `opencode.jsonc` in preference to `opencode.json` when both exist. */
|
|
32
|
+
const CONFIG_CANDIDATES = ['opencode.jsonc', 'opencode.json'];
|
|
33
|
+
function isPlainObject(v) {
|
|
34
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
35
|
+
}
|
|
36
|
+
const migration = {
|
|
37
|
+
id: '2026-10-12-opencode-mcp-no-prompts',
|
|
38
|
+
title: 'Stop the GSD MCP companion duplicating GSD commands in OpenCode',
|
|
39
|
+
description: 'Add --no-prompts to the GSD-written `gsd` MCP server entry in opencode.json(c) (mcp.gsd, and the ' +
|
|
40
|
+
'OpenCode 2.x native mcp.servers.gsd) so OpenCode no longer lists every GSD command a second time as ' +
|
|
41
|
+
'a `gsd:<name>` MCP-prompt command. Only the entry the installer wrote is changed; comments and ' +
|
|
42
|
+
'layout in a JSONC config are preserved (#5338, #5334).',
|
|
43
|
+
introducedIn: '1.17.0',
|
|
44
|
+
runtimes: ['opencode'],
|
|
45
|
+
scopes: ['global', 'local'],
|
|
46
|
+
destructive: false,
|
|
47
|
+
runtimeContract: 'docs/installer-migrations.md#runtime-configuration-contract-registry OpenCode row',
|
|
48
|
+
plan: (ctx) => {
|
|
49
|
+
if (ctx.runtime !== 'opencode')
|
|
50
|
+
return [];
|
|
51
|
+
// The file OpenCode (and the installer's resolveOpencodeConfigPath) treats as THE config.
|
|
52
|
+
const relPath = CONFIG_CANDIDATES.find((name) => node_fs_1.default.existsSync(node_path_1.default.join(ctx.configDir, name)));
|
|
53
|
+
if (!relPath)
|
|
54
|
+
return [];
|
|
55
|
+
let config;
|
|
56
|
+
try {
|
|
57
|
+
config = (0, jsonc_edit_cjs_1.parseJsonc)(node_fs_1.default.readFileSync(node_path_1.default.join(ctx.configDir, relPath), 'utf8'));
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return []; // unparseable: never touch a config we cannot read (same posture as the installer)
|
|
61
|
+
}
|
|
62
|
+
if (!isPlainObject(config) || !isPlainObject(config['mcp']))
|
|
63
|
+
return [];
|
|
64
|
+
const mcp = config['mcp'];
|
|
65
|
+
const edits = [];
|
|
66
|
+
const next = { ...config, mcp: { ...mcp } };
|
|
67
|
+
const nextMcp = next['mcp'];
|
|
68
|
+
const legacy = mcp['gsd'];
|
|
69
|
+
if ((0, opencode_mcp_entry_cjs_1.isGsdMcpEntry)(legacy) && !(0, opencode_mcp_entry_cjs_1.hasNoPromptsFlag)(legacy)) {
|
|
70
|
+
const updated = (0, opencode_mcp_entry_cjs_1.withNoPromptsFlag)(legacy);
|
|
71
|
+
nextMcp['gsd'] = updated;
|
|
72
|
+
edits.push({ path: ['mcp', 'gsd', 'command'], value: updated.command });
|
|
73
|
+
}
|
|
74
|
+
const servers = mcp['servers'];
|
|
75
|
+
if (isPlainObject(servers)) {
|
|
76
|
+
const native = servers['gsd'];
|
|
77
|
+
if ((0, opencode_mcp_entry_cjs_1.isGsdMcpEntry)(native) && !(0, opencode_mcp_entry_cjs_1.hasNoPromptsFlag)(native)) {
|
|
78
|
+
const updated = (0, opencode_mcp_entry_cjs_1.withNoPromptsFlag)(native);
|
|
79
|
+
nextMcp['servers'] = { ...servers, gsd: updated };
|
|
80
|
+
edits.push({ path: ['mcp', 'servers', 'gsd', 'command'], value: updated.command });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (edits.length === 0)
|
|
84
|
+
return [];
|
|
85
|
+
return [
|
|
86
|
+
{
|
|
87
|
+
type: 'rewrite-json',
|
|
88
|
+
relPath,
|
|
89
|
+
value: next,
|
|
90
|
+
edits,
|
|
91
|
+
reason: 'the GSD MCP companion advertised its prompt catalog, which OpenCode lists as a second set of ' +
|
|
92
|
+
'`gsd:<name>` slash commands next to the installed command files',
|
|
93
|
+
ownershipEvidence: "the entry matches the exact shape configureOpencodePermissions writes: type 'local' and a command " +
|
|
94
|
+
"array of `npx … <package> gsd-mcp-server`; a user-edited or user-defined `gsd` server does not match",
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
module.exports = migration;
|
|
@@ -18,6 +18,7 @@ const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs")
|
|
|
18
18
|
const clock_cjs_1 = require("./clock.cjs");
|
|
19
19
|
const install_scope_cjs_1 = require("./install-scope.cjs");
|
|
20
20
|
const security_cjs_1 = require("./security.cjs");
|
|
21
|
+
const jsonc_edit_cjs_1 = require("./jsonc-edit.cjs");
|
|
21
22
|
// #2874 (ADR-58 cleanup phase): this file is the ~1200-line migration
|
|
22
23
|
// plan/apply/rollback/lock/journal engine — almost none of it is on the
|
|
23
24
|
// installRuntimeArtifacts call tree. Only `readInstallManifest` and
|
|
@@ -293,15 +294,25 @@ function readInstallManifest(configDir) {
|
|
|
293
294
|
scope: (0, install_scope_cjs_1.isInstallScopeId)(m.scope) ? m.scope : null,
|
|
294
295
|
};
|
|
295
296
|
}
|
|
297
|
+
function normalizeHostGeneration(raw) {
|
|
298
|
+
if (!raw || typeof raw !== 'object')
|
|
299
|
+
return undefined;
|
|
300
|
+
const r = raw;
|
|
301
|
+
if (r.runtime !== 'opencode' || (r.generation !== 1 && r.generation !== 2))
|
|
302
|
+
return undefined;
|
|
303
|
+
return { runtime: 'opencode', generation: r.generation, version: typeof r.version === 'string' ? r.version : null };
|
|
304
|
+
}
|
|
296
305
|
function readInstallState(configDir) {
|
|
297
306
|
const state = readJsonIfPresent(node_path_1.default.join(configDir, INSTALL_STATE_NAME), STRICT_JSON);
|
|
298
307
|
if (!state || typeof state !== 'object') {
|
|
299
308
|
return { schemaVersion: 1, appliedMigrations: [] };
|
|
300
309
|
}
|
|
301
310
|
const s = state;
|
|
311
|
+
const hostGeneration = normalizeHostGeneration(s.hostGeneration);
|
|
302
312
|
return {
|
|
303
313
|
schemaVersion: typeof s.schemaVersion === 'number' ? s.schemaVersion : 1,
|
|
304
314
|
appliedMigrations: Array.isArray(s.appliedMigrations) ? s.appliedMigrations : [],
|
|
315
|
+
...(hostGeneration ? { hostGeneration } : {}),
|
|
305
316
|
};
|
|
306
317
|
}
|
|
307
318
|
// Strict atomic write for the install state: must never be left half-written.
|
|
@@ -654,6 +665,31 @@ function journalAction(action, status, extras = {}) {
|
|
|
654
665
|
const { value: _value, ...safeAction } = action;
|
|
655
666
|
return { ...safeAction, ...extras, status };
|
|
656
667
|
}
|
|
668
|
+
/**
|
|
669
|
+
* The text a `rewrite-json` action writes. A file with JSONC syntax (comments, trailing commas, or a
|
|
670
|
+
* `.jsonc` name) is edited in place from the action's `edits`, so the user's annotations survive (#5334);
|
|
671
|
+
* everything else, and any edit the text editor cannot place, is written as the action's whole `value`.
|
|
672
|
+
*/
|
|
673
|
+
function renderRewrittenJson(fullPath, action) {
|
|
674
|
+
if (Array.isArray(action.edits) && action.edits.length > 0) {
|
|
675
|
+
let original = null;
|
|
676
|
+
try {
|
|
677
|
+
original = node_fs_1.default.readFileSync(fullPath, 'utf8');
|
|
678
|
+
}
|
|
679
|
+
catch { /* fall through to the value rewrite */ }
|
|
680
|
+
if (original !== null && (fullPath.endsWith('.jsonc') || (0, jsonc_edit_cjs_1.hasJsoncFeatures)(original))) {
|
|
681
|
+
let text = original;
|
|
682
|
+
for (const edit of action.edits) {
|
|
683
|
+
text = (0, jsonc_edit_cjs_1.setJsoncValue)(text, edit.path, edit.value);
|
|
684
|
+
if (text === null)
|
|
685
|
+
break;
|
|
686
|
+
}
|
|
687
|
+
if (text !== null)
|
|
688
|
+
return text;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return JSON.stringify(action.value, null, 2) + '\n';
|
|
692
|
+
}
|
|
657
693
|
function planInstallerMigrations({ configDir, runtime = null, scope = null, migrations, baselineScan = false, now = () => new Date().toISOString(), }) {
|
|
658
694
|
if (!configDir)
|
|
659
695
|
throw new Error('configDir is required');
|
|
@@ -725,6 +761,8 @@ function planInstallerMigrations({ configDir, runtime = null, scope = null, migr
|
|
|
725
761
|
}
|
|
726
762
|
if (action.type === 'rewrite-json') {
|
|
727
763
|
action.value = rawAction.value;
|
|
764
|
+
if (Array.isArray(rawAction.edits))
|
|
765
|
+
action.edits = rawAction.edits;
|
|
728
766
|
action.deleteIfEmpty = rawAction.deleteIfEmpty === true;
|
|
729
767
|
}
|
|
730
768
|
if (rawAction.prompt)
|
|
@@ -896,7 +934,7 @@ function applyInstallerMigrationPlan({ configDir, plan, now = () => new Date().t
|
|
|
896
934
|
}));
|
|
897
935
|
}
|
|
898
936
|
else {
|
|
899
|
-
(0, shell_command_projection_cjs_1.platformWriteSync)(fullPath,
|
|
937
|
+
(0, shell_command_projection_cjs_1.platformWriteSync)(fullPath, renderRewrittenJson(fullPath, action));
|
|
900
938
|
journal.actions.push(journalAction(action, 'rewritten', {
|
|
901
939
|
rollbackRelPath: node_path_1.default.posix.join(rollbackRootRelPath, normalized),
|
|
902
940
|
}));
|
|
@@ -945,6 +983,7 @@ function applyInstallerMigrationPlan({ configDir, plan, now = () => new Date().t
|
|
|
945
983
|
writeInstallState(configDir, {
|
|
946
984
|
schemaVersion: 1,
|
|
947
985
|
appliedMigrations: nextApplied,
|
|
986
|
+
...(state.hostGeneration ? { hostGeneration: state.hostGeneration } : {}),
|
|
948
987
|
});
|
|
949
988
|
return {
|
|
950
989
|
appliedMigrationIds: journal.appliedMigrationIds,
|
|
@@ -1017,6 +1056,7 @@ function markPendingMigrationsApplied({ configDir, plan, now = () => new Date().
|
|
|
1017
1056
|
writeInstallState(configDir, {
|
|
1018
1057
|
schemaVersion: 1,
|
|
1019
1058
|
appliedMigrations: nextApplied,
|
|
1059
|
+
...(state.hostGeneration ? { hostGeneration: state.hostGeneration } : {}),
|
|
1020
1060
|
});
|
|
1021
1061
|
}
|
|
1022
1062
|
return newlyApplied;
|
|
@@ -1073,6 +1113,18 @@ function runInstallerMigrations({ configDir, runtime = null, scope = null, migra
|
|
|
1073
1113
|
// Unused but kept to satisfy eslint — sleepSync is referenced in the original
|
|
1074
1114
|
// and may be used by test code that patches this module.
|
|
1075
1115
|
void sleepSync;
|
|
1116
|
+
/**
|
|
1117
|
+
* Record which OpenCode generation this install targets (ADR-5331 D2; #5337). Idempotent and
|
|
1118
|
+
* atomic; leaves the applied-migrations journal untouched.
|
|
1119
|
+
*/
|
|
1120
|
+
function recordHostGeneration(configDir, info) {
|
|
1121
|
+
const state = readInstallState(configDir);
|
|
1122
|
+
const next = { runtime: 'opencode', generation: info.generation, version: info.version ?? null };
|
|
1123
|
+
const prev = state.hostGeneration;
|
|
1124
|
+
if (prev && prev.generation === next.generation && prev.version === next.version)
|
|
1125
|
+
return;
|
|
1126
|
+
writeInstallState(configDir, { ...state, hostGeneration: next });
|
|
1127
|
+
}
|
|
1076
1128
|
module.exports = {
|
|
1077
1129
|
DEFAULT_MIGRATIONS_DIR,
|
|
1078
1130
|
INSTALL_MIGRATION_LOCK_NAME,
|
|
@@ -1089,6 +1141,7 @@ module.exports = {
|
|
|
1089
1141
|
planInstallerMigrations,
|
|
1090
1142
|
readInstallManifest,
|
|
1091
1143
|
readInstallState,
|
|
1144
|
+
recordHostGeneration,
|
|
1092
1145
|
runInstallerMigrations,
|
|
1093
1146
|
writeInstallState,
|
|
1094
1147
|
};
|