refactor: hard-fork GSD -> MSD (Make Software Done)

Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD
across contents and paths, upstream package/repo coordinates -> @golem15/msd-core
and golem15com/msd-core. Deep links into upstream history, sibling upstream
packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is.

Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line,
package/plugin identity, regenerated lockfile, install-tree fixtures, derived
registries and benchmark baseline; migration checksum baseline re-locked
(MSD keeps its own install state, so no install had applied the old sums);
sort-order and regex-escaped expectations in tests adjusted.
This commit is contained in:
Jakub Zych
2026-10-06 01:47:40 +02:00
parent fe069b2a56
commit a9a7a328e6
2763 changed files with 78465 additions and 78434 deletions

View File

@@ -2,7 +2,7 @@
* Active Workstream Pointer Store Module
*
* Owns active workstream source precedence, session identity, and pointer IO:
* CLI --ws > GSD_WORKSTREAM env > stored active workstream pointer.
* CLI --ws > MSD_WORKSTREAM env > stored active workstream pointer.
*
* ADR-457 build-at-publish: the hand-written bin/lib/active-workstream-store.cjs
* collapsed to a TypeScript source of truth. Behaviour is preserved
@@ -17,7 +17,7 @@ import { probeTty, platformWriteSync, platformReadSync, platformEnsureDir } from
import { isValidActiveWorkstreamName } from './workstream-name-policy.cjs';
const WORKSTREAM_SESSION_ENV_KEYS: ReadonlyArray<string> = [
'GSD_SESSION_KEY',
'MSD_SESSION_KEY',
'CODEX_THREAD_ID',
'CLAUDE_SESSION_ID',
// #3557 — Claude Code (≥ 2.1.132) exports its session id to Bash-tool
@@ -95,7 +95,7 @@ function getWorkstreamSessionKey(): string | null {
// (core-utils.cts), the canonical slug formula — deliberately, not a
// consolidation gap. The text slugified here is never caller-supplied: it
// is always one of the fixed ASCII WORKSTREAM_SESSION_ENV_KEYS identifier
// names above (e.g. "GSD_SESSION_KEY"), which already agree with the
// names above (e.g. "MSD_SESSION_KEY"), which already agree with the
// canonical's output byte-for-byte on this restricted input domain (no
// unicode, no length anywhere near the 60-char truncation boundary) — so
// delegating would add a require() edge for zero behavioral change.
@@ -130,7 +130,7 @@ function getSessionScopedWorkstreamFile(cwd: string, fixedSessionKey?: string |
.digest('hex')
.slice(0, 16);
const dirPath = path.join(os.tmpdir(), 'gsd-workstream-sessions', projectId);
const dirPath = path.join(os.tmpdir(), 'msd-workstream-sessions', projectId);
return {
sessionKey,
dirPath,
@@ -466,8 +466,8 @@ function resolveActiveWorkstream(
if (parsed.value) {
ws = parsed.value;
source = parsed.source ?? 'cli';
} else if (env && typeof env['GSD_WORKSTREAM'] === 'string' && env['GSD_WORKSTREAM'].trim()) {
ws = env['GSD_WORKSTREAM'].trim();
} else if (env && typeof env['MSD_WORKSTREAM'] === 'string' && env['MSD_WORKSTREAM'].trim()) {
ws = env['MSD_WORKSTREAM'].trim();
source = 'env';
} else {
ws = getStored(cwd) || null;
@@ -490,7 +490,7 @@ function applyResolvedWorkstreamEnv(
env: NodeJS.ProcessEnv = process.env
): void {
if (!resolution || !resolution.ws) return;
env['GSD_WORKSTREAM'] = resolution.ws;
env['MSD_WORKSTREAM'] = resolution.ws;
}
export = {

View File

@@ -39,7 +39,7 @@ export interface ImperativeAdapter extends HostIntegrationInterface {
}
export interface CreateImperativeAdapterOptions {
/** Optional overrides forwarded to loadRegistry (cwd, gsdHome, hostVersion). */
/** Optional overrides forwarded to loadRegistry (cwd, msdHome, hostVersion). */
loadOptions?: Record<string, unknown>;
}

View File

@@ -119,13 +119,13 @@ function truncatePostureValue(value: string): string {
* Resolve the agents directory for the given runtime.
*
* Priority:
* 1. GSD_AGENTS_DIR env var (explicit override, any runtime)
* 1. MSD_AGENTS_DIR env var (explicit override, any runtime)
* 2. For claude runtime: __dirname-relative path (agents/ sibling of
* gsd-core/) — correct for repo runs and runtime-config-dir installs,
* msd-core/) — correct for repo runs and runtime-config-dir installs,
* where the sibling agents/ IS the user's agents dir — UNLESS that path
* carries an exact node_modules segment. gsd-tools.cjs lives inside
* gsd-core/bin/ in every install shape, but on an npm-global install
* gsd-core/ sits inside the package (not the runtime config dir) and the
* carries an exact node_modules segment. msd-tools.cjs lives inside
* msd-core/bin/ in every install shape, but on an npm-global install
* msd-core/ sits inside the package (not the runtime config dir) and the
* package ships its own agents/, so the install-relative path resolves
* to the bundled copy and the check validates the package against
* itself — agents_installed can never be false. In that case resolve
@@ -134,25 +134,25 @@ function truncatePostureValue(value: string): string {
* 3. For non-claude runtimes with a manifest-backed project-local install:
* <projectRoot>/<localConfigDir>/agents (or <projectRoot>/agents when
* the runtime's local install targets the project root). Requiring the
* GSD manifest prevents runtime-native project agents from shadowing a
* working global GSD install. Symlinked local agent directories are ignored.
* MSD manifest prevents runtime-native project agents from shadowing a
* working global MSD install. Symlinked local agent directories are ignored.
* 4. For non-claude runtimes: getGlobalConfigDir(runtime)/agents
*
* @param runtime - the active runtime name; defaults to GSD_RUNTIME env, then 'claude'
* @param runtime - the active runtime name; defaults to MSD_RUNTIME env, then 'claude'
* @param projectRoot - canonical project root for local-install discovery
*/
function getAgentsDir(runtime?: string, projectRoot?: string): string {
if (process.env['GSD_AGENTS_DIR']) {
return process.env['GSD_AGENTS_DIR'];
if (process.env['MSD_AGENTS_DIR']) {
return process.env['MSD_AGENTS_DIR'];
}
const resolved = runtime ?? (process.env['GSD_RUNTIME'] || 'claude');
const resolved = runtime ?? (process.env['MSD_RUNTIME'] || 'claude');
if (resolved === 'claude') {
const installRelative = path.join(__dirname, '..', '..', '..', 'agents');
// #3203: a lexical guard, not an install-shape test. It targets the
// layouts where the sibling agents/ is the package's own bundled copy and
// the check would otherwise validate the package against itself; a path
// merely carrying a directory of that name resolves the same way, and a
// non-empty GSD_AGENTS_DIR overrides both.
// non-empty MSD_AGENTS_DIR overrides both.
if (installRelative.split(path.sep).includes('node_modules')) {
return path.join(getGlobalConfigDir('claude'), 'agents');
}
@@ -174,7 +174,7 @@ function getAgentsDir(runtime?: string, projectRoot?: string): string {
return path.join(getGlobalConfigDir(resolved), 'agents');
}
const localAgentsDir = path.join(localConfigDir, 'agents');
const manifestPath = path.join(localConfigDir, 'gsd-file-manifest.json');
const manifestPath = path.join(localConfigDir, 'msd-file-manifest.json');
try {
if (fs.lstatSync(localAgentsDir).isDirectory() && fs.lstatSync(manifestPath).isFile()) {
return localAgentsDir;
@@ -187,13 +187,13 @@ function getAgentsDir(runtime?: string, projectRoot?: string): string {
}
/**
* Check which GSD agents are installed on disk.
* Check which MSD agents are installed on disk.
*
* @param runtime - the active runtime name; defaults to GSD_RUNTIME env, then 'claude'
* @param runtime - the active runtime name; defaults to MSD_RUNTIME env, then 'claude'
* @param projectRoot - canonical project root for local-install discovery
*/
function checkAgentsInstalled(runtime?: string, projectRoot?: string): AgentsInstalledResult {
const resolvedRuntime = runtime ?? (process.env['GSD_RUNTIME'] || 'claude');
const resolvedRuntime = runtime ?? (process.env['MSD_RUNTIME'] || 'claude');
const agentsDir = getAgentsDir(resolvedRuntime, projectRoot);
const expectedAgents = Object.keys(MODEL_PROFILES);
const installed: string[] = [];
@@ -219,7 +219,7 @@ function checkAgentsInstalled(runtime?: string, projectRoot?: string): AgentsIns
}
// ── Manifest-backed completeness check ──────────────────────────────────────
// If a gsd-file-manifest.json exists alongside the agents dir (parent dir),
// If a msd-file-manifest.json exists alongside the agents dir (parent dir),
// verify that every manifest-tracked file for each expected agent is present
// on disk. Missing manifest-tracked files indicate an incomplete install even
// when the plain presence check above passed (e.g. .md present, .toml absent).
@@ -282,14 +282,14 @@ function checkAgentsInstalled(runtime?: string, projectRoot?: string): AgentsIns
*
* Read-only: detects, never repairs (repair is Phase 3, #3243).
*
* @param runtime - the active runtime name; defaults to GSD_RUNTIME env, then 'claude'
* @param runtime - the active runtime name; defaults to MSD_RUNTIME env, then 'claude'
* @param projectRoot - canonical project root for local-install discovery
*/
function checkCodexModelPosture(runtime?: string, projectRoot?: string): CodexModelPostureResult {
// Short-circuit BEFORE any filesystem access: a non-codex runtime must never
// have its agents directory resolved or a stray .toml inspected, however
// violating that file's contents would be if it were ever read (#3242 row 25).
const resolvedRuntime = runtime ?? (process.env['GSD_RUNTIME'] || 'claude');
const resolvedRuntime = runtime ?? (process.env['MSD_RUNTIME'] || 'claude');
if (resolvedRuntime !== 'codex') {
return {
ok: true,
@@ -392,7 +392,7 @@ function checkCodexModelPosture(runtime?: string, projectRoot?: string): CodexMo
// copy every time — it is the only place the `tools:` contract a role
// derives its sandbox_mode from actually lives, in every install shape
// (repo-dev tree or npm-global package, where the package still ships its
// own agents/ alongside bin/ and gsd-core/).
// own agents/ alongside bin/ and msd-core/).
function _canonicalAgentSourceDir(): string {
return path.join(__dirname, '..', '..', '..', 'agents');
}
@@ -411,7 +411,7 @@ function _canonicalAgentSourceDir(): string {
* lazily `require('bin/install.js')` to reach the same derivation, but
* requiring `bin/install.js` runs its whole CLI top-level, including an
* ASCII banner print to stdout, which corrupted every stdout-JSON caller of
* this posture check, e.g. `gsd-tools validate agents`) — so the emitter and
* this posture check, e.g. `msd-tools validate agents`) — so the emitter and
* this posture check can never silently diverge on the same tool-contract
* predicate — the exact generative-fix-divergence shape this epic exists to
* close. `deriveCodexSandboxMode` itself is total (never throws, #3897
@@ -424,11 +424,11 @@ function _canonicalAgentSourceDir(): string {
*
* Read-only: detects, never repairs — same posture as {@link checkCodexModelPosture}.
*
* @param runtime - the active runtime name; defaults to GSD_RUNTIME env, then 'claude'
* @param runtime - the active runtime name; defaults to MSD_RUNTIME env, then 'claude'
* @param projectRoot - canonical project root for local-install discovery
*/
function checkCodexSandboxPosture(runtime?: string, projectRoot?: string): CodexSandboxPostureResult {
const resolvedRuntime = runtime ?? (process.env['GSD_RUNTIME'] || 'claude');
const resolvedRuntime = runtime ?? (process.env['MSD_RUNTIME'] || 'claude');
if (resolvedRuntime !== 'codex') {
return {
ok: true,
@@ -562,20 +562,20 @@ function agentFileExists(agentsDir: string, name: string, runtime: string): bool
/**
* Resolve a per-plan `agent_hint` specialist name to a dispatchable subagent
* type on the active runtime (#1689). Unlike {@link checkAgentsInstalled},
* which validates the fixed GSD roster, this answers "does an agent file for
* which validates the fixed MSD roster, this answers "does an agent file for
* this ARBITRARY name exist in the active runtime's agent dir(s)?" — so a plan
* can opt into a domain specialist (e.g. a Flutter engineer) that shares the
* gsd-executor contract without being part of the built-in roster.
* msd-executor contract without being part of the built-in roster.
*
* Probes BOTH the runtime-canonical agents dir ({@link getAgentsDir}, which
* honors `GSD_AGENTS_DIR`, project-local manifest-backed installs, and the
* honors `MSD_AGENTS_DIR`, project-local manifest-backed installs, and the
* claude install-relative path) AND the runtime's global config agents dir, so
* a specialist installed at either level is recognized. The decision in #1689
* explicitly requires consulting the active runtime's agent dir rather than
* only the Claude runtime's user-global and project-local agent dirs.
*
* @returns the name when a matching agent file exists; `null` when it does not
* (the caller falls back to `gsd-executor`). An empty/whitespace name always
* (the caller falls back to `msd-executor`). An empty/whitespace name always
* returns `null`.
*/
function resolveAgentHint(name: string, runtime?: string, projectRoot?: string): string | null {
@@ -586,7 +586,7 @@ function resolveAgentHint(name: string, runtime?: string, projectRoot?: string):
// path.join and match an unrelated file — that would echo an invalid
// subagent_type and block the wave, defeating fail-closed resolution.
if (trimmed.includes('/') || trimmed.includes('\\') || trimmed.includes('..')) return null;
const resolvedRuntime = runtime ?? (process.env['GSD_RUNTIME'] || 'claude');
const resolvedRuntime = runtime ?? (process.env['MSD_RUNTIME'] || 'claude');
const candidateDirs = new Set<string>();
candidateDirs.add(getAgentsDir(resolvedRuntime, projectRoot));

View File

@@ -52,7 +52,7 @@
* DEFAULT_API_COVERAGE_TERMS
*
* CLI (ADR-3889 Phase 3, #3907):
* echo "$SCOPE" | node gsd-core/bin/lib/api-coverage.cjs [--json]
* echo "$SCOPE" | node msd-core/bin/lib/api-coverage.cjs [--json]
* exit 0 = integration detected, 1 = none (real input, examined, no signal),
* NO_INPUT (registry, src/cli-exit.cts) = stdin empty/whitespace-only,
* UNAVAILABLE (registry) = stdin read failed

View File

@@ -1,11 +1,11 @@
/**
* Canonical GSD artifact registry (ADR-457 build-at-publish: the hand-written
* Canonical MSD artifact registry (ADR-457 build-at-publish: the hand-written
* bin/lib/artifacts.cjs collapsed to a TypeScript source of truth). Behaviour
* is preserved byte-for-behaviour from the prior hand-written .cjs; only types
* are added.
*
* Enumerates the file names that gsd workflows officially produce at the
* .planning/ root level. Used by gsd-health (W019) to flag unrecognized files
* Enumerates the file names that msd workflows officially produce at the
* .planning/ root level. Used by msd-health (W019) to flag unrecognized files
* so stale or misnamed artifacts don't silently mislead agents or reviewers.
*
* Add entries here whenever a new workflow produces a .planning/ root file.
@@ -35,7 +35,7 @@ export const CANONICAL_EXACT: ReadonlySet<string> = new Set([
// Pattern-match canonical file names (regex tests on the basename)
// Each pattern includes the name of the workflow that produces it as a comment.
export const CANONICAL_PATTERNS: ReadonlyArray<RegExp> = [
/^v\d+\.\d+(?:\.\d+)?-MILESTONE-AUDIT\.md$/i, // gsd-complete-milestone (pre-archive)
/^v\d+\.\d+(?:\.\d+)?-MILESTONE-AUDIT\.md$/i, // msd-complete-milestone (pre-archive)
/^v\d+\.\d+(?:\.\d+)?-.*\.md$/i, // other version-stamped planning docs
];

View File

@@ -27,7 +27,7 @@
* DEFAULT_ASSUMPTION_DELTA_TERMS
*
* CLI (ADR-3889 Phase 3, #3907):
* echo "$PHASE_SECTION" | node gsd-core/bin/lib/assumption-delta.cjs [--json]
* echo "$PHASE_SECTION" | node msd-core/bin/lib/assumption-delta.cjs [--json]
* exit 0 = signal detected, 1 = none (real input, examined, no signal),
* NO_INPUT (registry, src/cli-exit.cts) = stdin empty/whitespace-only,
* UNAVAILABLE (registry) = stdin read failed

View File

@@ -1,11 +1,11 @@
'use strict';
/**
* Audit command routers — CLI dispatchers for `gsd-tools audit-uat` and
* `gsd-tools audit-open`.
* Audit command routers — CLI dispatchers for `msd-tools audit-uat` and
* `msd-tools audit-open`.
*
* ADR-959 (phase 4d-impl-3): audit command family cutover.
* Extracted from the hardcoded `case 'audit-uat':` and `case 'audit-open':`
* arms in gsd-tools.cjs. Behaviour is preserved byte-for-behaviour from the
* arms in msd-tools.cjs. Behaviour is preserved byte-for-behaviour from the
* prior inline cases; the dispatch path now flows:
* default → dispatchCapabilityCommand →
* require(audit-command-router.cjs) → routeAuditUat | routeAuditOpen.
@@ -88,7 +88,7 @@ function routeAuditUat({ args, cwd, raw, error, _uat }: RouteAuditUatOptions): v
// Phase 2 (#1646): routes through the Hub for uniform observability and
// HandlerFailure taxonomy. audit-uat has no subcommands — a synthetic 'run'
// defaultSubcommand gives the Hub a single-handler manifest. The dispatch is
// trivial but the observability seam (DispatchEvent, GSD_AUDIT=1 trace) is
// trivial but the observability seam (DispatchEvent, MSD_AUDIT=1 trace) is
// now consistent with graphify/intel/host routers.
routeHubCommandFamily({
family: 'audit-uat',

View File

@@ -3,8 +3,8 @@
*
* Scans all .planning/ artifact categories for items with open/unresolved state.
* Returns structured JSON for workflow consumption.
* Called by: gsd-tools.cjs audit-open
* Used by: /gsd:complete-milestone pre-close gate
* Called by: msd-tools.cjs audit-open
* Used by: /msd:complete-milestone pre-close gate
*
* ADR-457 build-at-publish: the hand-written bin/lib/audit.cjs collapsed
* to a TypeScript source of truth. Behaviour is preserved byte-for-behaviour
@@ -205,7 +205,7 @@ interface AuditResult {
};
}
// The SCOPE BOUNDARY convention's filename (`agents/gsd-executor.md`), shared
// The SCOPE BOUNDARY convention's filename (`agents/msd-executor.md`), shared
// verbatim with the #2287 phase-boundary reader in `uat.cts`.
const DEFERRED_ITEMS_FILENAME = 'deferred-items.md';
@@ -1234,9 +1234,9 @@ function scanContextQuestions(planDir: string, cwd: string): ScanOutcome<Context
* across both .planning/phases (active) and .planning/milestones/vX.Y-phases
* (archived).
*
* The SCOPE BOUNDARY convention (`agents/gsd-executor.md`) has a phase agent
* The SCOPE BOUNDARY convention (`agents/msd-executor.md`) has a phase agent
* log an out-of-scope discovery here rather than fix it. #2287 made that file
* readable at the PHASE boundary (`/gsd-progress` check 7, `audit-uat`); this
* readable at the PHASE boundary (`/msd-progress` check 7, `audit-uat`); this
* scanner closes the remaining reader gap one boundary up, so an entry still
* unresolved at MILESTONE close surfaces in the pre-close audit alongside the
* other eight categories and the existing `[R]/[A]/[C]` prompt applies to it.

View File

@@ -3,7 +3,7 @@
*
* Manages `.planning/WINDOWS.md`: a cross-phase ledger of small defects (stubs,
* TODOs, skipped tests, lint warnings, unrun verifies, unmet truths, deviations).
* When `workflow.windows_enforce` is true, `/gsd-ship` blocks while any entry is
* When `workflow.windows_enforce` is true, `/msd-ship` blocks while any entry is
* `open`; an entry can be `waived` only with a recorded reason or `fixed`.
*
* LEAF MODULE — imports node:fs + node:path, plus two compiled sibling lib
@@ -40,7 +40,7 @@
* Reasoning shape — every cmd* function returns JSON suitable for `--raw`:
* success: { ok: true, ledger: <Ledger>, ... }
* failure: { ok: false, reason: <REASON.*>, message: <string> }
* Failure throws an ExitError-shaped error carrying REASON so the gsd-tools
* Failure throws an ExitError-shaped error carrying REASON so the msd-tools
* dispatcher's `--json-errors` mode emits it as a structured code (CONTRIBUTING.md
* "Prohibited: Raw Text Matching"). The frozen REASON enum is the typed surface
* tests assert against.
@@ -158,7 +158,7 @@ export interface Ledger {
// ─── Errors ────────────────────────────────────────────────────────────────
/**
* Error carrying a REASON code. gsd-tools.cjs's `--json-errors` mode catches
* Error carrying a REASON code. msd-tools.cjs's `--json-errors` mode catches
* this and emits `{ ok: false, reason: err.reason, message: err.message }` to
* stderr; otherwise the message goes to stderr as plain text and the exit
* code is non-zero.
@@ -727,9 +727,9 @@ export function renderLedger(ledger: Ledger): string {
const header = [
'# Broken Windows Ledger',
'',
'> Cross-phase defect register. With `workflow.windows_enforce` enabled, `/gsd-ship` blocks while `open_count > 0`.',
'> Waive with `gsd-tools windows waive <id> "<reason>"` (reason required).',
'> Mark fixed with `gsd-tools windows fixed <id>`.',
'> Cross-phase defect register. With `workflow.windows_enforce` enabled, `/msd-ship` blocks while `open_count > 0`.',
'> Waive with `msd-tools windows waive <id> "<reason>"` (reason required).',
'> Mark fixed with `msd-tools windows fixed <id>`.',
'',
].join('\n');
@@ -906,7 +906,7 @@ interface LockModule {
* diverge). Required LAZILY: capability-lock captures the process start time
* at module load — a `ps` subprocess on macOS, PowerShell on win32 — and
* this module is loaded by lock-free readers (`windows status`, the
* /gsd-ship gate) that must not pay that per-invocation cost (#3780
* /msd-ship gate) that must not pay that per-invocation cost (#3780
* review). `require` is cached, so writers pay it once per process.
*/
let _lockMod: LockModule | null = null;
@@ -1158,7 +1158,7 @@ function writeLedgerAtomic(cwd: string, ledger: Ledger): void {
REASON.WINDOWS_LEDGER_TABLE_DRIFT,
`Ledger table region could not be located in ${p}; refusing to write. Edit the ` +
'fenced JSON block directly — the sole source of truth — or delete the corrupted ' +
'table region and let gsd-tools regenerate it; never hand-edit the rendered table.',
'table region and let msd-tools regenerate it; never hand-edit the rendered table.',
);
}
if (actualTable !== expectedTable) {
@@ -1174,7 +1174,7 @@ function writeLedgerAtomic(cwd: string, ledger: Ledger): void {
REASON.WINDOWS_LEDGER_TABLE_DRIFT,
`Ledger table in ${p} disagrees with the fenced JSON entries (the sole source of ` +
`truth) ${driftDescription}. Edit the fenced JSON block ` +
'directly, or discard the table edit and re-run the command so gsd-tools ' +
'directly, or discard the table edit and re-run the command so msd-tools ' +
'regenerates the table; never hand-edit the rendered table.',
);
}
@@ -1203,7 +1203,7 @@ function emit(obj: unknown): void {
process.stdout.write(JSON.stringify(obj, null, 2));
}
/** `gsd-tools windows status [--raw]`. */
/** `msd-tools windows status [--raw]`. */
export function cmdWindowsStatus(cwd: string, opts: { raw?: boolean } = {}): void {
let ledger: Ledger;
try {
@@ -1219,7 +1219,7 @@ export function cmdWindowsStatus(cwd: string, opts: { raw?: boolean } = {}): voi
emit({ ok: true, ledger });
}
/** `gsd-tools windows append --kind K --phase N [--file F] [--line L] --description D`. */
/** `msd-tools windows append --kind K --phase N [--file F] [--line L] --description D`. */
export function cmdWindowsAppend(
cwd: string,
args: string[],
@@ -1270,7 +1270,7 @@ export function cmdWindowsAppend(
});
}
/** `gsd-tools windows waive <id> "<reason>"`. */
/** `msd-tools windows waive <id> "<reason>"`. */
export function cmdWindowsWaive(
cwd: string,
args: string[],
@@ -1300,7 +1300,7 @@ export function cmdWindowsWaive(
});
}
/** `gsd-tools windows fixed <id>`. */
/** `msd-tools windows fixed <id>`. */
export function cmdWindowsMarkFixed(
cwd: string,
args: string[],

View File

@@ -50,7 +50,7 @@ function _readRawConfigKey(
_warnedRawConfigPaths.add(filePath);
try {
process.stderr.write(
`gsd-tools: warning: failed to parse ${filePath} as JSON — skipping for activation resolution\n`,
`msd-tools: warning: failed to parse ${filePath} as JSON — skipping for activation resolution\n`,
);
} catch { /* stderr might be closed */ }
}

View File

@@ -1,7 +1,7 @@
/**
* Capability consent store — issue #1459 (capability trust model bypassable).
*
* A USER-OWNED store, living OUTSIDE any repository at `${GSD_HOME||homedir()}/.gsd/consent.json`,
* A USER-OWNED store, living OUTSIDE any repository at `${MSD_HOME||homedir()}/.msd/consent.json`,
* that binds each PROJECT-scope third-party capability activation to a decision the user made on
* THIS machine. Before #1459 a project's in-repo ledger entry was treated as the consent signal —
* but a project ledger is repo-plantable, so cloning/forging a repo activated executable surfaces
@@ -29,9 +29,9 @@
* (no migration/back-compat shims needed); the only version is "1".
*
* Exports:
* consentStorePath(gsdHome?) — resolve the store path (GSD_HOME||homedir() rule).
* consentStorePath(msdHome?) — resolve the store path (MSD_HOME||homedir() rule).
* bundleContentHash(capDir) — recomputed sha512 over the whole bundle (the binding).
* readConsentStore(gsdHome?) — bounded, NON-THROWING read; bad input → { records: {} }.
* readConsentStore(msdHome?) — bounded, NON-THROWING read; bad input → { records: {} }.
* hasProjectConsent({...}) — true iff a record matches the recomputed contentHash.
* recordProjectConsent({...}) — atomic+durable+LOCKED write of a project-scope record.
* revokeProjectConsent({...}) — atomic+LOCKED delete of a project-scope record (no-op if absent).
@@ -78,7 +78,7 @@ const CONSENT_LOCK_MAX_ATTEMPTS = 50;
// ---------------------------------------------------------------------------
const CONSENT_SCHEMA_VERSION = '1';
const CONSENT_DIRNAME = '.gsd';
const CONSENT_DIRNAME = '.msd';
const CONSENT_FILE_NAME = 'consent.json';
/**
@@ -212,12 +212,12 @@ function realpathProject(projectRoot: string): string {
// ---------------------------------------------------------------------------
/**
* Resolve the consent store path. Uses the SAME `gsdHome || GSD_HOME || homedir()` rule the loader
* Resolve the consent store path. Uses the SAME `msdHome || MSD_HOME || homedir()` rule the loader
* and CLI use, so a consent record written by the CLI is found by the loader. The store NEVER lives
* under a repository — it is user-owned, machine-local config.
*/
function consentStorePath(gsdHome?: string): string {
const home = gsdHome || process.env['GSD_HOME'] || os.homedir();
function consentStorePath(msdHome?: string): string {
const home = msdHome || process.env['MSD_HOME'] || os.homedir();
return path.join(home, CONSENT_DIRNAME, CONSENT_FILE_NAME);
}
@@ -619,9 +619,9 @@ function isValidConsentRecord(rec: unknown): rec is ConsentRecord {
* (FIFO/device), or wrong-shape store yields an empty `{ records: {} }`. Invalid individual records
* are dropped. A store whose record count exceeds MAX_RECORDS is refused wholesale (hostile DoS).
*/
function readConsentStore(gsdHome?: string): ConsentStore {
function readConsentStore(msdHome?: string): ConsentStore {
const empty: ConsentStore = { records: {} };
const filePath = consentStorePath(gsdHome);
const filePath = consentStorePath(msdHome);
let raw: string | null;
try {
raw = ledgerMod.readSmallRegularFile(filePath, CONSENT_MAX_BYTES);
@@ -667,15 +667,15 @@ function readConsentStore(gsdHome?: string): ConsentStore {
* hasOwnProperty).
*/
function hasProjectConsent(args: {
gsdHome?: string;
msdHome?: string;
projectRoot: string;
id: string;
contentHash: string;
}): boolean {
const { gsdHome, projectRoot, id, contentHash } = args;
const { msdHome, projectRoot, id, contentHash } = args;
if (isUnsafeCapabilityId(id)) return false;
if (typeof contentHash !== 'string' || !contentHash) return false;
const store = readConsentStore(gsdHome);
const store = readConsentStore(msdHome);
const key = consentKey(realpathProject(projectRoot), id);
if (!Object.prototype.hasOwnProperty.call(store.records, key)) return false;
const rec = store.records[key];
@@ -689,8 +689,8 @@ function hasProjectConsent(args: {
type ConsentLock = { path: string; token: string; dev: number | null; ino: number | null };
/** The consent-store lock path — keyed on the consent store DIRECTORY (one lock per machine store). */
function consentLockPath(gsdHome?: string): string {
return path.join(path.dirname(consentStorePath(gsdHome)), '.consent.lock');
function consentLockPath(msdHome?: string): string {
return path.join(path.dirname(consentStorePath(msdHome)), '.consent.lock');
}
/**
@@ -769,8 +769,8 @@ function renameBackoff(): void {
* The on-disk JSON uses the unambiguous JSON-object disk key (WIN-3); the in-memory store is keyed by
* the canonical NUL key, so we re-key here.
*/
function writeConsentStore(gsdHome: string | undefined, store: ConsentStore): void {
const filePath = consentStorePath(gsdHome);
function writeConsentStore(msdHome: string | undefined, store: ConsentStore): void {
const filePath = consentStorePath(msdHome);
const dir = path.dirname(filePath);
fs.mkdirSync(dir, { recursive: true });
@@ -848,7 +848,7 @@ function writeConsentStore(gsdHome: string | undefined, store: ConsentStore): vo
* written. (The OLD code returned a null handle and proceeded unlocked — that is the bug.)
*/
function recordProjectConsent(args: {
gsdHome?: string;
msdHome?: string;
projectRoot: string;
id: string;
integrity: string;
@@ -856,7 +856,7 @@ function recordProjectConsent(args: {
contentHash: string;
reviewerHost?: string;
}): void {
const { gsdHome, projectRoot, id, integrity, disclosureSignature, contentHash, reviewerHost } = args;
const { msdHome, projectRoot, id, integrity, disclosureSignature, contentHash, reviewerHost } = args;
if (isUnsafeCapabilityId(id)) {
throw new Error(
`Invalid capability id "${String(id)}": must match /^[a-z][a-z0-9-]*$/ (kebab-case, lowercase). ` +
@@ -870,27 +870,27 @@ function recordProjectConsent(args: {
);
}
const realRoot = realpathProject(projectRoot);
const lockDir = path.dirname(consentStorePath(gsdHome));
const lockDir = path.dirname(consentStorePath(msdHome));
try { fs.mkdirSync(lockDir, { recursive: true }); } catch { /* best-effort — write also mkdirs */ }
// #1459 finding 3: never proceed UNLOCKED. A null handle (live holder / contention budget exhausted)
// → throw rather than risk a lost update.
const lock = acquireConsentLock(lockDir);
if (lock === null) {
throw new Error(
`recordProjectConsent: could not acquire the consent-store lock at ${consentLockPath(gsdHome)} ` +
`recordProjectConsent: could not acquire the consent-store lock at ${consentLockPath(msdHome)} ` +
`(another writer holds it). Refusing to write the consent store UNLOCKED (a lost-update risk). ` +
`Retry; if a stale lock persists past the deadman it is reclaimed automatically.`,
);
}
try {
const store = readConsentStore(gsdHome);
const store = readConsentStore(msdHome);
const key = consentKey(realRoot, id);
// CONSENT-MAXRECORDS-WRITE-1: enforce the cap BEFORE the write. A re-record of an EXISTING key
// does not grow the store (allowed); only ADDING a new key when already at the cap is refused.
if (!Object.prototype.hasOwnProperty.call(store.records, key) && Object.keys(store.records).length >= MAX_RECORDS) {
throw new Error(
`consent store full: already at the maximum of ${MAX_RECORDS} consent records. Revoke an ` +
`unused consent (gsd capability trust revoke) before recording a new one.`,
`unused consent (msd capability trust revoke) before recording a new one.`,
);
}
store.records[key] = {
@@ -905,7 +905,7 @@ function recordProjectConsent(args: {
// or a non-lane capability keeps a byte-identical record shape.
...(typeof reviewerHost === 'string' && reviewerHost ? { reviewerHost } : {}),
};
writeConsentStore(gsdHome, store);
writeConsentStore(msdHome, store);
} finally {
releaseConsentLock(lock);
}
@@ -920,27 +920,27 @@ function recordProjectConsent(args: {
* doing an unlocked read-modify-write (the lost-update vector). An ABSENT-record no-op still happens
* UNDER the lock (so a concurrent record cannot interleave); only a genuine lock-acquire failure throws.
*/
function revokeProjectConsent(args: { gsdHome?: string; projectRoot: string; id: string }): void {
const { gsdHome, projectRoot, id } = args;
function revokeProjectConsent(args: { msdHome?: string; projectRoot: string; id: string }): void {
const { msdHome, projectRoot, id } = args;
if (isUnsafeCapabilityId(id)) return; // an unsafe id was never stored — nothing to revoke.
const realRoot = realpathProject(projectRoot);
const lockDir = path.dirname(consentStorePath(gsdHome));
const lockDir = path.dirname(consentStorePath(msdHome));
try { fs.mkdirSync(lockDir, { recursive: true }); } catch { /* best-effort — write also mkdirs */ }
// #1459 finding 3: never proceed UNLOCKED — a null handle throws rather than deleting unlocked.
const lock = acquireConsentLock(lockDir);
if (lock === null) {
throw new Error(
`revokeProjectConsent: could not acquire the consent-store lock at ${consentLockPath(gsdHome)} ` +
`revokeProjectConsent: could not acquire the consent-store lock at ${consentLockPath(msdHome)} ` +
`(another writer holds it). Refusing to modify the consent store UNLOCKED (a lost-update risk). ` +
`Retry; if a stale lock persists past the deadman it is reclaimed automatically.`,
);
}
try {
const store = readConsentStore(gsdHome);
const store = readConsentStore(msdHome);
const key = consentKey(realRoot, id);
if (!Object.prototype.hasOwnProperty.call(store.records, key)) return; // absent — no-op.
delete store.records[key];
writeConsentStore(gsdHome, store);
writeConsentStore(msdHome, store);
} finally {
releaseConsentLock(lock);
}
@@ -962,14 +962,14 @@ function revokeProjectConsent(args: { gsdHome?: string; projectRoot: string; id:
* Non-throwing: a missing, corrupt, or oversized store yields `undefined` like any other absence.
*/
function readConsentedReviewerHost(args: {
gsdHome?: string;
msdHome?: string;
projectRoot: string;
id: string;
}): string | undefined {
const { gsdHome, projectRoot, id } = args;
const { msdHome, projectRoot, id } = args;
if (isUnsafeCapabilityId(id)) return undefined;
try {
const store = readConsentStore(gsdHome);
const store = readConsentStore(msdHome);
const key = consentKey(realpathProject(projectRoot), id);
if (!Object.prototype.hasOwnProperty.call(store.records, key)) return undefined;
const host = store.records[key].reviewerHost;

View File

@@ -1,7 +1,7 @@
/**
* Capability ledger module — ADR-1244 Phase 3 (Decision D4).
*
* Manages a per-runtime install manifest (`.gsd-capabilities.json`) that records
* Manages a per-runtime install manifest (`.msd-capabilities.json`) that records
* what each capability install wrote. Serves as the atomic commit point and
* reconciliation basis for Phase 4 upgrade/remove operations.
*
@@ -29,7 +29,7 @@ import crypto from 'node:crypto';
// Constants
// ---------------------------------------------------------------------------
const LEDGER_FILE_NAME = '.gsd-capabilities.json';
const LEDGER_FILE_NAME = '.msd-capabilities.json';
const LEDGER_SCHEMA_VERSION = '1';
// ---------------------------------------------------------------------------
@@ -473,7 +473,7 @@ function readLedgerStrict(runtimeDir: string): LedgerFile | null {
if (typeof ver === 'string' && ver !== LEDGER_SCHEMA_VERSION) {
throw new CorruptLedgerError(
`Capability ledger at ${filePath} uses unsupported ledger schema version "${ver}" ` +
`(this build supports version "${LEDGER_SCHEMA_VERSION}"). Upgrade GSD to a build that ` +
`(this build supports version "${LEDGER_SCHEMA_VERSION}"). Upgrade MSD to a build that ` +
`understands this ledger, or move the file aside to start fresh.`,
filePath,
);

View File

@@ -11,7 +11,7 @@
* directories and edit JSON);
* - executable surfaces are disclosed and consent is required before anything is promoted
* (decline => nothing written);
* - integrity + engines.gsd are verified by the resolver BEFORE staging finalizes;
* - integrity + engines.msd are verified by the resolver BEFORE staging finalizes;
* - remove deletes exactly the ledger-recorded files and surgically strips exactly the
* capability-owned shared-config entries (marker-isolated), touching nothing the user owns.
*
@@ -65,12 +65,12 @@ const trustMod = require('./capability-trust.cjs') as {
signatureForManifest: (manifest: Record<string, unknown>, stagedDir?: string) => string;
};
const consentMod = require('./capability-consent.cjs') as {
recordProjectConsent: (args: { gsdHome?: string; projectRoot: string; id: string; integrity: string; disclosureSignature: string; contentHash: string; reviewerHost?: string }) => void;
revokeProjectConsent: (args: { gsdHome?: string; projectRoot: string; id: string }) => void;
recordProjectConsent: (args: { msdHome?: string; projectRoot: string; id: string; integrity: string; disclosureSignature: string; contentHash: string; reviewerHost?: string }) => void;
revokeProjectConsent: (args: { msdHome?: string; projectRoot: string; id: string }) => void;
/** #1459 CB-1/CB-2: recompute the full-bundle content hash (the consent security binding). */
bundleContentHash: (capDir: string) => string;
/** #1459 IC-05/WIN-2: resolve the consent store path for an unwritable-store warning message. */
consentStorePath: (gsdHome?: string) => string;
consentStorePath: (msdHome?: string) => string;
};
const projectRootMod = require('./project-root.cjs') as {
// #1459 IC-01/CB-4: the canonical consent project root (RECORD site parity with the loader LOOKUP).
@@ -152,7 +152,7 @@ interface LedgerFile {
}
interface LifecycleOptions {
/** Scope root: holds .gsd/capabilities/<id>, the ledger, and shared config files. */
/** Scope root: holds .msd/capabilities/<id>, the ledger, and shared config files. */
runtimeDir: string;
hostVersion: string;
/**
@@ -163,7 +163,7 @@ interface LifecycleOptions {
*/
scope?: 'global' | 'project';
/**
* #1459: the USER-OWNED consent home (`GSD_HOME||homedir()`) where project-scope consent records
* #1459: the USER-OWNED consent home (`MSD_HOME||homedir()`) where project-scope consent records
* live — OUTSIDE any repo. When omitted, no consent record is written/revoked (back-compat for
* callers that have not wired the consent store; the loader then leaves the project cap inactive).
*/
@@ -182,7 +182,7 @@ interface LifecycleOptions {
removeData?: boolean;
/**
* When set, the resolved capability id MUST equal this or the operation is refused with NO writes.
* `gsd capability update <id>` passes the requested id so a source that has been retargeted or
* `msd capability update <id>` passes the requested id so a source that has been retargeted or
* hand-edited to a different manifest id cannot silently act on (and overwrite) another capability.
*/
expectedId?: string;
@@ -201,7 +201,7 @@ interface LifecycleOptions {
// ---------------------------------------------------------------------------
/** Stamp written onto every capability-owned shared-config entry, for surgical removal. */
const CAP_MARKER = '_gsdCapability';
const CAP_MARKER = '_msdCapability';
/**
* #3514 (epic #1900 F21c): what KIND of pin (if any) the source content was pinned with — shared
@@ -234,7 +234,7 @@ function isUnsafeKey(k: string): boolean {
}
function capabilitiesRoot(runtimeDir: string): string {
return path.join(runtimeDir, '.gsd', 'capabilities');
return path.join(runtimeDir, '.msd', 'capabilities');
}
function capDir(runtimeDir: string, id: string): string {
@@ -242,7 +242,7 @@ function capDir(runtimeDir: string, id: string): string {
}
function capDataDir(runtimeDir: string, id: string): string {
return path.join(runtimeDir, '.gsd', 'capability-data', id);
return path.join(runtimeDir, '.msd', 'capability-data', id);
}
/** Errnos from a directory fsync that are tolerated (platforms/filesystems disallowing dir fsync). */
@@ -300,7 +300,7 @@ function newBackupName(id: string): string {
// Non-lock orphan-sweep / id constants (kept local — not part of the shared lock primitive).
/** A `.staging/*` dir younger than this may belong to an in-flight resolve; do not sweep it. */
const STAGING_ORPHAN_MS = 600_000;
/** A `.gsd-capabilities.json.tmp.*` temp younger than this may belong to an in-flight write; spare it (W-3/DUR-5). */
/** A `.msd-capabilities.json.tmp.*` temp younger than this may belong to an in-flight write; spare it (W-3/DUR-5). */
const LEDGER_TMP_ORPHAN_MS = 300_000;
/** Valid capability id (kebab-case). Used to reject tampered ledger keys before acting on them. */
const KEBAB_ID_RE = /^[a-z][a-z0-9-]*$/;
@@ -308,7 +308,7 @@ const KEBAB_ID_RE = /^[a-z][a-z0-9-]*$/;
type LockHandle = { path: string; token: string; dev: number | null; ino: number | null };
/**
* Acquire the capability-mutation lock (the single `.gsd/capabilities/.lock` under runtimeDir),
* Acquire the capability-mutation lock (the single `.msd/capabilities/.lock` under runtimeDir),
* delegating the hardened steal/liveness/deadman protocol to the shared lock primitive. The lockfile
* path is the SAME as before extraction, so all existing #1462 lock tests (which key on a `.lock`
* suffix and call lifecycle.acquireLock(runtimeDir)) keep passing unchanged.
@@ -352,8 +352,8 @@ function writeJsonFileAtomic(file: string, obj: unknown): void {
/**
* Rm a ledger-recorded path only if its REAL location is strictly under runtimeDir's real path.
*
* Lexical containment alone is insufficient: a tampered ledger could record `.gsd/link/victim`
* where `.gsd/link` is a symlink to `/`, and a lexical check would pass while the delete escapes
* Lexical containment alone is insufficient: a tampered ledger could record `.msd/link/victim`
* where `.msd/link` is a symlink to `/`, and a lexical check would pass while the delete escapes
* (Codex R1 H4). So we realpath the parent chain (defeating symlinked components) and `lstat` the
* final component (a symlinked target is unlinked as a link, never followed into a recursive rm).
*
@@ -361,7 +361,7 @@ function writeJsonFileAtomic(file: string, obj: unknown): void {
* classic TOCTOU. It is out of threat model here — both the ledger and runtimeDir are the user's own
* trusted config tree, so an attacker who can tamper the ledger and win that race already has write
* access to delete these files directly (no privilege boundary is crossed). The mutation lock also
* serializes GSD's own operations, and the realpath check defeats the realistic persistent-symlink
* serializes MSD's own operations, and the realpath check defeats the realistic persistent-symlink
* vector.
*/
function safeRmUnder(runtimeDir: string, rel: string): boolean {
@@ -950,7 +950,7 @@ function bindProjectConsent(opts: LifecycleOptions, id: string, integrity: strin
}
try {
consentMod.recordProjectConsent({
gsdHome: opts.consentStoreDir,
msdHome: opts.consentStoreDir,
// #1459 IC-01/CB-4: bind the record's projectRoot through the SINGLE canonical helper so the
// RECORD key matches the loader's LOOKUP key (consentProjectRoot) and `trust revoke`. The bundle
// hash is still taken over the ACTUAL on-disk install location (capDir(opts.runtimeDir, id)).
@@ -969,7 +969,7 @@ function bindProjectConsent(opts: LifecycleOptions, id: string, integrity: strin
// #1459 IC-05/WIN-2: a consent-store write failure (read-only/UNC/NFS store) must NOT turn an
// otherwise-successful install/upgrade into a failure — the bundle is already committed. Surface a
// non-fatal warning (naming the store path so the operator can fix permissions and re-consent via
// `gsd capability trust`), and let the op SUCCEED. The cap is simply inactive until consent writes.
// `msd capability trust`), and let the op SUCCEED. The cap is simply inactive until consent writes.
const storePath = (() => {
try { return consentMod.consentStorePath(opts.consentStoreDir); } catch { return String(opts.consentStoreDir); }
})();
@@ -1017,7 +1017,7 @@ async function installCapability(spec: string, opts: LifecycleOptions): Promise<
// Finding 1 (HIGH): strict ledger PREFLIGHT — BEFORE source resolution, staging, trust, or
// consent. On a corrupt-but-present ledger this must block IMMEDIATELY with a corruption
// reason. The previous order called _resolve first (creating .gsd/capabilities/.staging) and
// reason. The previous order called _resolve first (creating .msd/capabilities/.staging) and
// only strict-read later, so a corrupt ledger could surface as `aborted` (consent) for an
// executable install without --yes BEFORE the corruption was ever reported, and would leave a
// staging dir behind. A non-throwing read here is a READ-ONLY operation: it touches no lock and
@@ -1034,7 +1034,7 @@ async function installCapability(spec: string, opts: LifecycleOptions): Promise<
try {
resolved = await resolve(spec, {
hostVersion,
gsdHome: runtimeDir,
msdHome: runtimeDir,
integrity,
promote: false,
// The lifecycle owns the engines gate via checkEngines (so it can also surface a
@@ -1061,7 +1061,7 @@ async function installCapability(spec: string, opts: LifecycleOptions): Promise<
return { status: 'blocked', id: resolved.id, blockReasons: [`source resolved to capability id "${resolved.id}" but "${opts.expectedId}" was expected; refusing`] };
}
// ROOT FIX 3: reject unsafe capability ids before any promotion or ledger write.
// A .gsd/capabilities/constructor (or __proto__, prototype) bundle must never be promoted —
// A .msd/capabilities/constructor (or __proto__, prototype) bundle must never be promoted —
// the resolved id is untrusted data from the bundle's capability.json.
if (ledgerMod.isUnsafeCapabilityId(resolved.id)) {
return { status: 'blocked', id: resolved.id, blockReasons: [`capability id "${resolved.id}" is unsafe (prototype-pollution key or invalid kebab-case); refusing to install`] };
@@ -1226,7 +1226,7 @@ async function upgradeCapability(spec: string, opts: LifecycleOptions): Promise<
try {
resolved = await resolve(spec, {
hostVersion,
gsdHome: runtimeDir,
msdHome: runtimeDir,
integrity,
promote: false,
// The lifecycle owns the engines gate via checkEngines (so it can also surface a
@@ -1367,7 +1367,7 @@ interface RemoveResult {
* record could NOT be revoked (e.g. the consent-store lock could not be acquired — revokeProjectConsent
* THROWS rather than doing an unlocked delete). The removal is still `removed` (the bundle is gone), but
* a STALE consent record remains that a byte-identical re-drop + forged ledger could reactivate against,
* so the caller must report a NON-CLEAN removal and tell the user to clear it (`gsd capability trust revoke`).
* so the caller must report a NON-CLEAN removal and tell the user to clear it (`msd capability trust revoke`).
*/
consentRevokeFailed?: boolean;
/** Human-readable detail naming the stale consent record when consentRevokeFailed is true. */
@@ -1382,7 +1382,7 @@ interface RemoveResult {
function removeCapability(id: string, opts: LifecycleOptions): RemoveResult {
const { runtimeDir, removeData } = opts;
// Finding 2 (HIGH): READ-ONLY corruption preflight BEFORE acquireLock. acquireLock creates
// .gsd/capabilities and a .lock file; doing it before detecting corruption pollutes the scope
// .msd/capabilities and a .lock file; doing it before detecting corruption pollutes the scope
// (and takes a lock) on a ledger we will refuse anyway. A strict read takes no lock and creates
// no directory, so on a corrupt/IO-error ledger we return blocked with NO lock and NO dir created.
try {
@@ -1432,7 +1432,7 @@ function removeCapability(id: string, opts: LifecycleOptions): RemoveResult {
// If the write fails (EPERM, EBUSY, EXDEV, …) after the files are already deleted, we
// return a typed 'blocked' result with recovery info rather than letting an unhandled
// throw propagate as a CLI stack trace. The ledger would still reference files that no
// longer exist — the user can re-run `gsd capability remove <id>` to retry the commit (the
// longer exist — the user can re-run `msd capability remove <id>` to retry the commit (the
// next install/update/remove also runs the reconcile sweep automatically). There is no
// standalone `reconcile` CLI subcommand (UX-4).
try {
@@ -1447,7 +1447,7 @@ function removeCapability(id: string, opts: LifecycleOptions): RemoveResult {
id,
blockReasons: [
`Capability files were deleted but the ledger commit failed: ${(err as Error).message}. ` +
`To recover: run 'gsd capability remove ${id}' again, or manually inspect and restore ` +
`To recover: run 'msd capability remove ${id}' again, or manually inspect and restore ` +
`the ledger file to remove the stale entry for "${id}".`,
],
};
@@ -1467,13 +1467,13 @@ function removeCapability(id: string, opts: LifecycleOptions): RemoveResult {
try {
// #1459 IC-01/CB-4: revoke under the SAME canonical root the record was written under
// (consentProjectRoot), so a removal actually clears the record the install bound.
consentMod.revokeProjectConsent({ gsdHome: opts.consentStoreDir, projectRoot: projectRootMod.consentProjectRoot(runtimeDir), id });
consentMod.revokeProjectConsent({ msdHome: opts.consentStoreDir, projectRoot: projectRootMod.consentProjectRoot(runtimeDir), id });
} catch (err) {
consentRevokeFailed = true;
consentRevokeWarning =
`removed capability "${id}" but could NOT revoke its project consent record: ${(err as Error).message}. ` +
`The consent record is now STALE — a byte-identical re-drop of this bundle could reactivate against it. ` +
`Clear it manually: gsd capability trust revoke ${id}`;
`Clear it manually: msd capability trust revoke ${id}`;
warnConsent(consentRevokeWarning);
}
}
@@ -1547,7 +1547,7 @@ function reconcileCapabilities(opts: { runtimeDir: string; scope?: 'global' | 'p
if ((opts.scope ?? 'project') !== 'project') return;
try {
consentMod.revokeProjectConsent({
gsdHome: opts.consentStoreDir,
msdHome: opts.consentStoreDir,
projectRoot: projectRootMod.consentProjectRoot(runtimeDir),
id,
});
@@ -1555,7 +1555,7 @@ function reconcileCapabilities(opts: { runtimeDir: string; scope?: 'global' | 'p
};
// Finding 2 (HIGH): READ-ONLY corruption preflight BEFORE acquireLock. acquireLock creates
// .gsd/capabilities and a .lock file; doing it before detecting corruption pollutes the scope
// .msd/capabilities and a .lock file; doing it before detecting corruption pollutes the scope
// (and takes a lock) on a ledger we will refuse to mutate anyway. A strict read takes no lock and
// creates no directory, so on a corrupt/IO-error/broken-symlink ledger we WARN and return WITHOUT
// any filesystem mutation and WITHOUT a lock or directory created. (The in-lock re-read below
@@ -1585,7 +1585,7 @@ function reconcileCapabilities(opts: { runtimeDir: string; scope?: 'global' | 'p
// "no ledger yet" and proceed to sweep backups. lstatSync checks the directory ENTRY itself,
// so a broken symlink is detected and treated as an IO problem requiring user intervention.
if (ledger === null) {
const ledgerFilePath = path.join(runtimeDir, '.gsd-capabilities.json');
const ledgerFilePath = path.join(runtimeDir, '.msd-capabilities.json');
let ledgerEntryExists = false;
try {
fs.lstatSync(ledgerFilePath);
@@ -1758,7 +1758,7 @@ function reconcileCapabilities(opts: { runtimeDir: string; scope?: 'global' | 'p
}
} catch { /* no staging dir */ }
// W-3 / DUR-5: sweep STALE ledger temp orphans (`.gsd-capabilities.json.tmp.<pid>-<nonce>`) from
// W-3 / DUR-5: sweep STALE ledger temp orphans (`.msd-capabilities.json.tmp.<pid>-<nonce>`) from
// the runtime dir. A double-IO-error (or Windows AV lock) during writeLedger's cleanup-unlink can
// leave a temp behind; without this sweep they accumulate forever. Spare recently-created ones,
// which may belong to an in-flight write in another process. Best-effort.
@@ -1820,7 +1820,7 @@ interface OutdatedRecord {
* An empty/missing ledger yields an empty array (non-throwing — readLedger returns null on a missing or
* corrupt-present ledger; the `outdated` report is read-only and degrades to "nothing to report").
*
* @param opts.runtimeDir the scope root holding `.gsd-capabilities.json`.
* @param opts.runtimeDir the scope root holding `.msd-capabilities.json`.
* @param opts.execOverrides threaded to the source peek (test seam — mock git ls-remote / npm view).
*/
function outdatedCapabilities(opts: {
@@ -1899,7 +1899,7 @@ export = {
CAP_MARKER,
// Exported for cross-process-lock unit tests (CONC-1/CONC-2/finding-1). Not part of the public CLI
// surface. #1459 finding 4: the lock primitive now lives in the shared capability-lock module; these
// re-export it (acquireLock here still takes a runtimeDir and computes the `.gsd/capabilities/.lock`
// re-export it (acquireLock here still takes a runtimeDir and computes the `.msd/capabilities/.lock`
// path) and the test seams (`_setLockProbes`/`_resetLockProbes`/`getProcessStartTime`) forward to the
// shared module so the existing #1462 lock tests drive the SAME probe state the primitive reads.
acquireLock,

View File

@@ -8,15 +8,15 @@
* It composes the **first-party frozen registry** (the committed, generated
* `capability-registry.cjs`) with a **validated installed overlay** — third-party
* capability manifests read at runtime from per-scope install roots:
* - global: $GSD_HOME/.gsd/capabilities/<id>/capability.json (GSD_HOME defaults to ~)
* - project: <projectRoot>/.gsd/capabilities/<id>/capability.json
* - global: $MSD_HOME/.msd/capabilities/<id>/capability.json (MSD_HOME defaults to ~)
* - project: <projectRoot>/.msd/capabilities/<id>/capability.json
*
* Invariants enforced over the merged set (first-party ∪ overlay):
* - First-party always wins: an overlay whose `id`, owned skill/agent stem, or
* federated config key collides with first-party (or uses a reserved `gsd-` /
* `gsd-core-` / `anthropic-` id prefix) is rejected.
* federated config key collides with first-party (or uses a reserved `msd-` /
* `msd-core-` / `anthropic-` id prefix) is rejected.
* - Load-time re-gate (default-resilient): an overlay that fails validation or
* whose `engines.gsd` does not satisfy the running GSD version is SKIPPED
* whose `engines.msd` does not satisfy the running MSD version is SKIPPED
* with a warning — it never crashes the loop. A skipped capability that
* declares a `gate` is additionally recorded in
* `_overlay.incompatibleGateCapIds` / `_overlay.blockedGates` so the loop
@@ -50,7 +50,7 @@ interface CapManifest {
commands?: Array<Record<string, unknown>>;
config?: Record<string, unknown>;
gates?: unknown[];
engines?: { gsd?: string };
engines?: { msd?: string };
}
interface ValidatorModule {
@@ -63,7 +63,7 @@ interface ValidatorModule {
/**
* ADR-2782 D4.3 — NON-FATAL diagnostics for an ACCEPTED capability (e.g. an
* unknown field inside a `reviewer` body, which is ignored with a warning
* rather than failing validation so a manifest built for a newer GSD degrades
* rather than failing validation so a manifest built for a newer MSD degrades
* visibly instead of being rejected). Returns warnings; never throws.
* Optional so an older built validator without it still loads.
*/
@@ -93,7 +93,7 @@ interface ConsentModule {
* the repo-plantable ledger integrity nor the executable-only disclosure signature.
*/
hasProjectConsent: (args: {
gsdHome?: string;
msdHome?: string;
projectRoot: string;
id: string;
contentHash: string;
@@ -107,9 +107,9 @@ export interface LoadRegistryOptions {
includeInstalled?: boolean;
/** Working directory used to locate the project-scoped overlay root. */
cwd?: string;
/** Override the global overlay home (defaults to GSD_HOME env or os.homedir()). */
gsdHome?: string;
/** Override the running GSD version used for engines.gsd satisfaction. */
/** Override the global overlay home (defaults to MSD_HOME env or os.homedir()). */
msdHome?: string;
/** Override the running MSD version used for engines.msd satisfaction. */
hostVersion?: string;
/**
* Optional configHome root for load-time write-confinement of installed
@@ -126,7 +126,7 @@ export interface OverlaySkip {
scope: 'global' | 'project';
reason: string;
/**
* #1459 IC-02: a STRUCTURAL discriminant for the skip so consumers (gsd-tools `list`) classify a
* #1459 IC-02: a STRUCTURAL discriminant for the skip so consumers (msd-tools `list`) classify a
* warning by `kind`, not by matching the human-readable `reason` prose (which is free to change).
* `'unconsented'` is the project-scope no-consent-record case the list command marks INACTIVE; other
* skips carry no `kind` (they are first-party-wins / validation / engines / pending diagnostics).
@@ -151,7 +151,7 @@ export interface OverlayMeta {
* Distinct from `warnings`, which records capabilities that were SKIPPED: a
* consumer that treats every `warnings` entry as "inactive" would mislabel an
* active capability if these were folded in. Today this carries unknown-field
* notices from a `reviewer` body built for a newer GSD — the case D4.3 exists
* notices from a `reviewer` body built for a newer MSD — the case D4.3 exists
* for, which validates cleanly and so would otherwise surface nowhere at all.
*/
diagnostics: string[];
@@ -165,7 +165,7 @@ export interface OverlayMeta {
blockedGates: BlockedGate[];
/**
* Absolute install-root directory for each ACCEPTED OVERLAY (third-party) capability that
* declares `commands` — `capId → <scope>/.gsd/capabilities/<capId>`. First-party capabilities
* declares `commands` — `capId → <scope>/.msd/capabilities/<capId>`. First-party capabilities
* are NOT listed here (their command modules ship in `bin/lib/`). ADR-1244 Phase 5 (D7) uses this
* to dispatch a third-party command family by `require()`-ing its router module FROM the install
* root, confined to that root. Only committed (non-`_pending`) capabilities reach this map, so its
@@ -174,8 +174,8 @@ export interface OverlayMeta {
commandRoots: Record<string, string>;
}
const RESERVED_ID_PREFIX = /^(gsd-|gsd-core-|anthropic-)/;
const GSD_HOME_DIRNAME = '.gsd';
const RESERVED_ID_PREFIX = /^(msd-|msd-core-|anthropic-)/;
const MSD_HOME_DIRNAME = '.msd';
/**
* GENEROUS DoS backstop for the bounded per-scope ledger read (mirrors capability-ledger's
* LEDGER_MAX_BYTES). The project-scope ledger is repo-plantable untrusted content; reading it via
@@ -218,10 +218,10 @@ function _setGeneratorForTest(g: GeneratorModule | null): void {
}
/**
* Resolve the running GSD version; fail-closed to '0.0.0' if it cannot be read.
* Resolve the running MSD version; fail-closed to '0.0.0' if it cannot be read.
*
* Prefer the authoritative `gsd-core/VERSION` the installer writes for EVERY runtime
* (libDir = gsd-core/bin/lib/, so `../../VERSION` = gsd-core/VERSION). This is reliable
* Prefer the authoritative `msd-core/VERSION` the installer writes for EVERY runtime
* (libDir = msd-core/bin/lib/, so `../../VERSION` = msd-core/VERSION). This is reliable
* across all installed layouts — including runtimes that get no marker package.json, and
* local installs where the walked-up `../../../package.json` would resolve to the USER's
* own project and report a wrong version (#1920). Fall back to the runtime-root
@@ -233,7 +233,7 @@ export function readHostVersion(libDir: string = __dirname): string {
try {
const v = fs.readFileSync(path.join(libDir, '..', '..', 'VERSION'), 'utf8').trim();
if (SEMVER_PREFIX.test(v)) return v;
} catch { /* not an installed tree (no gsd-core/VERSION) */ }
} catch { /* not an installed tree (no msd-core/VERSION) */ }
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
const pkg: { version?: string } = require(path.join(libDir, '..', '..', '..', 'package.json'));
@@ -245,14 +245,14 @@ export function readHostVersion(libDir: string = __dirname): string {
/**
* Canonicalize a directory path for dedup/scope-escalation comparison. #1459 finding 1 (HIGH): the dedup
* MUST collapse two DIFFERENT LEXICAL paths that name the SAME PHYSICAL directory (a symlink) to one key,
* else a symlinked GSD_HOME aliasing the project root is scanned once as trusted 'global' BEFORE the
* 'project' scan and the in-repo `.gsd/capabilities` bundle bypasses the CB-3 consent gate via aliasing.
* else a symlinked MSD_HOME aliasing the project root is scanned once as trusted 'global' BEFORE the
* 'project' scan and the in-repo `.msd/capabilities` bundle bypasses the CB-3 consent gate via aliasing.
* `fs.realpathSync` resolves symlinks to the physical path; on ENOENT/IO error it falls back to
* `path.resolve` (a not-yet-created overlay dir cannot be realpath'd).
*
* #1459 CONVERGENCE finding 3 (LOW/MED): the realpath FAILURE must be reported to the caller (the
* `realpathFailed` flag), NOT silently swallowed. The old behavior — fall back to `path.resolve` while
* preserving the candidate's ORIGINAL scope — was not strictly fail-safe: a symlinked GSD_HOME whose
* preserving the candidate's ORIGINAL scope — was not strictly fail-safe: a symlinked MSD_HOME whose
* realpath THROWS (a race / odd-FS) would key on its SYMLINK-LEXICAL path, which differs from the
* project candidate's realpath'd key, so the two would NOT merge and the aliased global root would be
* scanned as trusted-'global' (no consent record required) — parking an aliased project tree in the
@@ -280,26 +280,26 @@ function canonicalDir(dir: string): { path: string; realpathFailed: boolean; eno
* The ordered overlay install roots (global first, then project), deduped by
* CANONICAL (realpath'd) absolute path so a single physical directory is never scanned twice (which
* would otherwise self-report a spurious id collision when the project lives
* under the GSD home, or in tests where both resolve to the same fixture).
* under the MSD home, or in tests where both resolve to the same fixture).
*
* #1459 CB-3: when the consent-global home resolves EQUAL to (or an ancestor whose .gsd collides with)
* #1459 CB-3: when the consent-global home resolves EQUAL to (or an ancestor whose .msd collides with)
* a GENUINE project root, the global overlay dir and the project overlay dir are the SAME directory.
* The dedup must NOT then keep it as 'global' (trusted, no consent record required) — that would let an
* in-repo bundle bypass consent simply because GSD_HOME pointed at the repo. On a collision the
* in-repo bundle bypass consent simply because MSD_HOME pointed at the repo. On a collision the
* surviving scope escalates to the MORE RESTRICTIVE 'project' (consent-required), but ONLY when the
* colliding root is a GENUINE marker'd project (a `.planning/` dir or a `.git`). `findProjectRoot` is
* total — it returns `cwd` itself when no marker exists — so a bare GSD_HOME with no project marker
* total — it returns `cwd` itself when no marker exists — so a bare MSD_HOME with no project marker
* (the user's own home; also the test-fixture `cwd === home` no-op) must stay 'global' and NOT spuriously
* demand consent.
*
* #1459 finding 1 (HIGH): BOTH the dedup key AND the CB-3 collision comparison are keyed on the
* realpath'd path (canonicalDir), so a symlinked GSD_HOME that physically IS the project root collides
* realpath'd path (canonicalDir), so a symlinked MSD_HOME that physically IS the project root collides
* and escalates to consent-required 'project' — it can no longer be aliased into the trusted-global slot.
*
* #1459 finding 1 (HIGH, ROUND 6): the trusted-global slot is now gated on PROVABLE distinctness from the
* project tree — realpath(global) AND realpath(project) must BOTH succeed AND resolve to DIFFERENT physical
* paths. The earlier one-sided rule (demote only a realpath-FAILED *global* candidate) still allowed the
* symlinked-GSD_HOME bypass: when GSD_HOME aliases the project root, the GLOBAL candidate realpaths fine
* symlinked-MSD_HOME bypass: when MSD_HOME aliases the project root, the GLOBAL candidate realpaths fine
* while the PROJECT candidate's realpath fails, so the keys never collide and the in-repo bundle stays in
* the no-consent global slot. If distinctness cannot be proven (either realpath throws, or both resolve
* EQUAL) AND there is a genuine project root, the global is demoted to consent-required 'project'.
@@ -315,13 +315,13 @@ function hasGenuineProjectMarker(dir: string): boolean {
return false;
}
function overlayRoots(cwd: string, gsdHome?: string): Array<{ dir: string; scope: 'global' | 'project' }> {
function overlayRoots(cwd: string, msdHome?: string): Array<{ dir: string; scope: 'global' | 'project' }> {
const roots: Array<{ dir: string; scope: 'global' | 'project' }> = [];
const byPath = new Map<string, { dir: string; scope: 'global' | 'project' }>();
const add = (dir: string, scope: 'global' | 'project', canonical: { path: string; realpathFailed: boolean }, genuineProject = false): void => {
const resolved = path.resolve(dir);
// #1459 finding 1: the DEDUP KEY (and thus the CB-3 scope-escalation comparison) is the CANONICAL
// (realpath'd) path, so a symlinked GSD_HOME that physically IS the project root collides here (and
// (realpath'd) path, so a symlinked MSD_HOME that physically IS the project root collides here (and
// escalates below) instead of being scanned as a distinct trusted 'global' root. The SCANNED path
// (`entry.dir`) stays the lexical `path.resolve` value — the readdir/commandRoots path is unchanged
// for the common (non-symlinked) case; only the dedup/escalation decision is realpath-aware.
@@ -329,7 +329,7 @@ function overlayRoots(cwd: string, gsdHome?: string): Array<{ dir: string; scope
const existing = byPath.get(key);
if (existing) {
// CB-3: a dir already claimed escalates to the more restrictive scope ONLY for a GENUINE project
// root — so a real GSD_HOME == projectRoot (incl. via a symlink) still requires consent, while a
// root — so a real MSD_HOME == projectRoot (incl. via a symlink) still requires consent, while a
// marker-less home stays trusted-global (and the test-fixture cwd===home no-op is preserved).
if (existing.scope === 'global' && scope === 'project' && genuineProject) existing.scope = 'project';
return;
@@ -338,8 +338,8 @@ function overlayRoots(cwd: string, gsdHome?: string): Array<{ dir: string; scope
byPath.set(key, entry);
roots.push(entry);
};
const home = gsdHome || process.env['GSD_HOME'] || os.homedir();
const globalDir = path.join(home, GSD_HOME_DIRNAME, 'capabilities');
const home = msdHome || process.env['MSD_HOME'] || os.homedir();
const globalDir = path.join(home, MSD_HOME_DIRNAME, 'capabilities');
const globalCanon = canonicalDir(globalDir);
let projectRoot: string | null = null;
try {
@@ -349,13 +349,13 @@ function overlayRoots(cwd: string, gsdHome?: string): Array<{ dir: string; scope
} catch {
projectRoot = null;
}
const projectDir = projectRoot ? path.join(projectRoot, GSD_HOME_DIRNAME, 'capabilities') : null;
const projectDir = projectRoot ? path.join(projectRoot, MSD_HOME_DIRNAME, 'capabilities') : null;
const projectCanon = projectDir ? canonicalDir(projectDir) : null;
// #1459 finding 1 (HIGH, round 6): the global overlay root is trusted (consent-FREE) ONLY when we can
// PROVE it is a distinct physical directory from the project overlay tree — i.e. realpath(global) AND
// realpath(project) BOTH succeed AND resolve to DIFFERENT physical paths. A one-sided rule (demote only a
// realpath-FAILED *global* candidate) left the symlinked-GSD_HOME bypass open: when GSD_HOME is a symlink
// realpath-FAILED *global* candidate) left the symlinked-MSD_HOME bypass open: when MSD_HOME is a symlink
// alias of the project root, the GLOBAL candidate realpaths fine (stays trusted-global) while the PROJECT
// candidate's realpath fails → the two keys never collide → the in-repo bundle stays in the no-consent
// global slot. So the global is demoted to consent-required 'project' (only when there IS a GENUINE
@@ -373,7 +373,7 @@ function overlayRoots(cwd: string, gsdHome?: string): Array<{ dir: string; scope
// demote the global to consent-required 'project' UNLESS we can PROVE physical distinctness:
// - the project overlay actually exists (or can't be proven absent), AND
// - either realpath can't canonicalize one side (race/odd-FS → can't prove distinct), OR
// - both canonicalize EQUAL (an alias — GSD_HOME physically IS the project root).
// - both canonicalize EQUAL (an alias — MSD_HOME physically IS the project root).
const projectAbsent = projectCanon.realpathFailed && projectCanon.enoent;
if (!projectAbsent) {
const provablyDistinct =
@@ -407,8 +407,8 @@ function projectRootFor(cwd: string): string {
}
/**
* Read the per-scope ledger co-located with an overlay root (the root is `<scope>/.gsd/capabilities`,
* so its ledger is `<scope>/.gsd-capabilities.json`) and classify its ids:
* Read the per-scope ledger co-located with an overlay root (the root is `<scope>/.msd/capabilities`,
* so its ledger is `<scope>/.msd-capabilities.json`) and classify its ids:
* - `pending`: ids carrying an in-flight `_pending` intent (crashed/uncommitted install/upgrade)
* — must not be activated until reconciliation completes.
* - `committed`: ids with a ledger entry and NO `_pending` — i.e. an install the user actually
@@ -438,7 +438,7 @@ function ledgerOverlayIds(ledger: LedgerModule, rootDir: string): {
const pending = new Set<string>();
const committed = new Set<string>();
try {
const ledgerPath = path.join(rootDir, '..', '..', '.gsd-capabilities.json');
const ledgerPath = path.join(rootDir, '..', '..', '.msd-capabilities.json');
// #1459 (HIGH): read the per-scope ledger via the SHARED fd-based bounded reader (open → fstat →
// require regular file → size cap → read exactly size). The previous raw `fs.readFileSync` BLOCKED
// forever on a repo-planted FIFO ledger (a project-scope DoS) and read an oversized file whole.
@@ -504,7 +504,7 @@ function centralConfigKeys(): Set<string> {
* cross-capability suite against. Built the way `loadRegistry` builds its
* accepted map: first-party registry capabilities, then each COMMITTED overlay
* of the TARGET (global) install scope accepted INCREMENTALLY — structural
* validation, engines.gsd against the running host, then the FULL
* validation, engines.msd against the running host, then the FULL
* cross-capability suite; an overlay joins only if the suite stays clean
* after adding it (load's invariant: first-party alone is clean, so any new
* error is that overlay's fault — skip it, never fail the seed). The seed is
@@ -515,7 +515,7 @@ function centralConfigKeys(): Set<string> {
* (#1459 CB-3).
*
* Scope: only the install TARGET scope is walked — `stageValidated` promotes
* into `${gsdHome}/.gsd/capabilities`, so target scope == global.
* into `${msdHome}/.msd/capabilities`, so target scope == global.
* Project-scope overlays are deliberately NOT seeded (they additionally
* require user consent to activate at load; the issue asks for overlays "in
* the target scope"). The candidate is added by the caller LAST, mirroring
@@ -528,7 +528,7 @@ function centralConfigKeys(): Set<string> {
*/
export function crossValidationSeed(
cwd: string,
gsdHome: string,
msdHome: string,
hostVersion: string,
validator: ValidatorModule,
semver: SemverModule,
@@ -538,7 +538,7 @@ export function crossValidationSeed(
const centralKeys = centralConfigKeys();
// eslint-disable-next-line @typescript-eslint/no-require-imports
const ledger: LedgerModule = require('./capability-ledger.cjs') as LedgerModule;
for (const root of overlayRoots(cwd, gsdHome)) {
for (const root of overlayRoots(cwd, msdHome)) {
if (root.scope !== 'global') continue; // seed the TARGET scope only
const { committed } = ledgerOverlayIds(ledger, root.dir);
for (const id of committed) {
@@ -557,7 +557,7 @@ export function crossValidationSeed(
continue; // unreadable overlay — skip (same rule as load)
}
// Same per-overlay pre-filters load applies before the cross suite:
// structural validity, then engines.gsd against the running host.
// structural validity, then engines.msd against the running host.
// validateCapability itself is not total over malformed array entries
// (#1461 finding 1) — a throw skips the overlay, as load skips it.
try {
@@ -567,7 +567,7 @@ export function crossValidationSeed(
}
const engines = (cap as Record<string, unknown>)['engines'];
if (engines && typeof engines === 'object' && !Array.isArray(engines)) {
const range = (engines as Record<string, unknown>)['gsd'];
const range = (engines as Record<string, unknown>)['msd'];
if (typeof range === 'string' && range && !semver.semverSatisfies(hostVersion, range)) continue;
}
// Incremental accept: the overlay joins only if the FULL suite stays
@@ -653,9 +653,9 @@ export function loadRegistry(options: LoadRegistryOptions = {}): Registry {
const cwd = options.cwd || process.cwd();
const hostVersion = options.hostVersion || readHostVersion();
// The user-owned consent home — SAME `gsdHome || GSD_HOME || homedir()` rule the CLI uses, so the
// The user-owned consent home — SAME `msdHome || MSD_HOME || homedir()` rule the CLI uses, so the
// consent the CLI records is the consent the loader checks. The consent store NEVER lives in a repo.
const gsdHome = options.gsdHome || process.env['GSD_HOME'] || os.homedir();
const msdHome = options.msdHome || process.env['MSD_HOME'] || os.homedir();
const warnings: OverlaySkip[] = [];
// ADR-2782 D4.3 — non-fatal notices for capabilities that are ACCEPTED (see OverlayMeta).
@@ -708,7 +708,7 @@ export function loadRegistry(options: LoadRegistryOptions = {}): Registry {
return centralKeys;
};
for (const root of overlayRoots(cwd, options.gsdHome)) {
for (const root of overlayRoots(cwd, options.msdHome)) {
let entries: fs.Dirent[];
try {
entries = fs.readdirSync(root.dir, { withFileTypes: true });
@@ -767,7 +767,7 @@ export function loadRegistry(options: LoadRegistryOptions = {}): Registry {
// manifest with `gates: [null]` (or `steps: [null]` / `contributions: [null]`) makes
// validateCapability THROW `Cannot read properties of null (reading 'point')`. That throw was
// OUTSIDE any per-candidate guard → it escaped loadRegistry and crashed EVERY consumer
// (loop-resolver, config-loader, surface, capability-state, gsd-tools). ADR-1244 D2 mandates a
// (loop-resolver, config-loader, surface, capability-state, msd-tools). ADR-1244 D2 mandates a
// malformed overlay is SKIPPED with a warning, never crashes the loop. Wrapping the whole body
// (manifest already parsed above) means ANY throw from ANY validator/step becomes a structured
// `skip()` + continue to the next candidate — which ALSO fail-closes a declared gate (the `skip`
@@ -778,7 +778,7 @@ export function loadRegistry(options: LoadRegistryOptions = {}): Registry {
try {
// 1. Reserved namespace — third-party may not impersonate first-party.
if (RESERVED_ID_PREFIX.test(id)) {
skip('id uses a reserved first-party prefix (gsd-/gsd-core-/anthropic-)');
skip('id uses a reserved first-party prefix (msd-/msd-core-/anthropic-)');
continue;
}
// 2. Per-capability structural + version-envelope validation.
@@ -809,10 +809,10 @@ export function loadRegistry(options: LoadRegistryOptions = {}): Registry {
: [];
const familyClash = families.find((f) => claimedFamilies.has(f));
if (familyClash) { skip('owns command family "' + familyClash + '" already owned by another capability'); continue; }
// 4. Load-time engines.gsd re-gate.
const range = cap.engines?.gsd;
// 4. Load-time engines.msd re-gate.
const range = cap.engines?.msd;
if (typeof range === 'string' && range && !semver.semverSatisfies(hostVersion, range)) {
skip('incompatible with GSD ' + hostVersion + ' (requires engines.gsd "' + range + '")');
skip('incompatible with MSD ' + hostVersion + ' (requires engines.msd "' + range + '")');
continue;
}
// 5. #1459 — USER-OWNED CONSENT GATE (TRUST-1 + TRUST-3). For a PROJECT-scope overlay the
@@ -860,7 +860,7 @@ export function loadRegistry(options: LoadRegistryOptions = {}): Registry {
let consented = false;
try {
consented = consentMod.hasProjectConsent({
gsdHome,
msdHome,
projectRoot: projectRootFor(cwd),
id,
contentHash: consentMod.bundleContentHash(capDir),
@@ -872,7 +872,7 @@ export function loadRegistry(options: LoadRegistryOptions = {}): Registry {
// DISCOVERED-BUT-INACTIVE: no user consent record on this machine. NOT a gate block (an
// unconsented project gate is not a real installed gate — same fail-open posture as
// `_pending`); it simply does not contribute any surface. #1459 IC-02: tag the skip with the
// structural `kind: 'unconsented'` so gsd-tools `list` marks it INACTIVE by discriminant, not
// structural `kind: 'unconsented'` so msd-tools `list` marks it INACTIVE by discriminant, not
// by matching the (changeable) reason prose. NOTE (convergence finding 1): we `continue` here
// BEFORE materializeHookFragments, so an unconsented project overlay's fragment files are never
// read — a forged FIFO/oversized fragment cannot hang/OOM the loop.
@@ -905,7 +905,7 @@ export function loadRegistry(options: LoadRegistryOptions = {}): Registry {
// #1461 OVL-1 (HIGH): these validators are CONTRACTED to RETURN error arrays, but one can THROW
// (e.g. validateConsumesGlobal asserting on a duplicate producer). An unguarded throw here
// escapes loadRegistry and crashes EVERY consumer (loop-resolver, config-loader, surface,
// capability-state, gsd-tools). ADR-1244 D2: a malformed overlay is SKIPPED with a warning,
// capability-state, msd-tools). ADR-1244 D2: a malformed overlay is SKIPPED with a warning,
// never crashes the loop. So a throwing validator is treated EXACTLY like a validation failure:
// drop this one candidate (with a warning) and continue — the rest of the overlay set is
// unaffected. (The returns-errors path below is unchanged.)
@@ -944,7 +944,7 @@ export function loadRegistry(options: LoadRegistryOptions = {}): Registry {
// Accepted.
//
// ADR-2782 D4.3: an unknown field inside a `reviewer` body is IGNORED WITH A
// WARNING rather than failing validation, so a lane built for a newer GSD
// WARNING rather than failing validation, so a lane built for a newer MSD
// degrades to accepted-but-partially-understood instead of being rejected.
// That case validates cleanly, so without this call it would surface
// nowhere at runtime — the build-time generator only ever sees first-party

View File

@@ -1,7 +1,7 @@
/**
* Shared cross-process mutual-exclusion lock primitive — #1459 finding 4 + #1462 finding 1.
*
* A SINGLE hardened lockfile protocol shared by BOTH capability-lifecycle (the `.gsd/capabilities/.lock`
* A SINGLE hardened lockfile protocol shared by BOTH capability-lifecycle (the `.msd/capabilities/.lock`
* mutation lock) and capability-consent (the consent-store `.consent.lock`). Before this extraction the
* two locks had DIFFERENT, divergent steal policies: the lifecycle lock was hardened (#1462 — pid +
* process-start-time identity + hard deadman, never steals a verified-live same-host holder), while the

View File

@@ -2,7 +2,7 @@
* capability-source.cts — Capability source resolver (ADR-1244 Phase 3, Decision D3).
*
* One seam `resolveCapabilitySource(spec, opts)` with an adapter per source kind.
* Each adapter: fetch → verify integrity/SHA → check engines.gsd → return a STAGED,
* Each adapter: fetch → verify integrity/SHA → check engines.msd → return a STAGED,
* VALIDATED bundle.
*
* SECURITY CONTRACT:
@@ -10,7 +10,7 @@
* - All subprocesses routed through shell-command-projection.cjs seam (windowsHide,
* argv arrays, no shell string interpolation).
* - Integrity verified BEFORE extraction when provided.
* - engines.gsd pre-checked before staging.
* - engines.msd pre-checked before staging.
* - Full validator suite run on manifest before finalizing.
* - Staging atomicity: stage under .staging/<id>-<pid>-<ts>/, renameSync on success,
* rmSync on any failure.
@@ -91,10 +91,10 @@ type TarExecFn = (program: string, args: string[], opts?: { cwd?: string; timeou
/** Options accepted by resolveCapabilitySource. */
interface ResolveOptions {
/** Running GSD version. Defaults to package.json version, fail-closed to '0.0.0'. */
/** Running MSD version. Defaults to package.json version, fail-closed to '0.0.0'. */
hostVersion?: string;
/** Override the GSD home directory (where .gsd/capabilities/ lives). */
gsdHome?: string;
/** Override the MSD home directory (where .msd/capabilities/ lives). */
msdHome?: string;
/** Expected integrity string (`sha512-<base64>`). When provided, integrity is verified
* before any bytes are committed to the final location. */
integrity?: string;
@@ -106,7 +106,7 @@ interface ResolveOptions {
*/
promote?: boolean;
/**
* When true, SKIP the resolver's `engines.gsd` hard-throw during staging. The caller becomes
* When true, SKIP the resolver's `engines.msd` hard-throw during staging. The caller becomes
* responsible for the engines gate. Used by capability-lifecycle so its own `checkEngines` can
* gate the install AND surface a `compatVersions` downgrade hint (the resolver throw would
* pre-empt that). Staging remains copy-only and is never promoted by the lifecycle when the
@@ -310,7 +310,7 @@ function realHttpsGet(url: string): Promise<HttpResponse> {
}
const req = _httpsGetImpl(
url,
{ headers: { 'User-Agent': 'gsd-core-capability-source/1.0' } },
{ headers: { 'User-Agent': 'msd-core-capability-source/1.0' } },
(res) => {
// DOS-1: reject early if the server ADVERTISES a body over the cap — no bytes buffered.
const contentLength = Number(res.headers?.['content-length']);
@@ -374,10 +374,10 @@ function _setCapabilitySourceHttpGet(fn: HttpGetFn | null): void {
// ---------------------------------------------------------------------------
/**
* Resolve the running GSD version; fail-closed to '0.0.0'.
* Resolve the running MSD version; fail-closed to '0.0.0'.
*
* Prefer the authoritative `gsd-core/VERSION` the installer writes for EVERY runtime
* (libDir = gsd-core/bin/lib/, so `../../VERSION` = gsd-core/VERSION), so installed
* Prefer the authoritative `msd-core/VERSION` the installer writes for EVERY runtime
* (libDir = msd-core/bin/lib/, so `../../VERSION` = msd-core/VERSION), so installed
* layouts report the true version even when the walked-up `../../../package.json` is
* absent or belongs to the user's own project (#1920). Fall back to the runtime-root
* package.json (dev/source tree), then fail-closed. Mirrors resolveVersionFrom() (#1383).
@@ -387,7 +387,7 @@ function readHostVersion(libDir: string = __dirname): string {
try {
const v = fs.readFileSync(path.join(libDir, '..', '..', 'VERSION'), 'utf8').trim();
if (SEMVER_PREFIX.test(v)) return v;
} catch { /* not an installed tree (no gsd-core/VERSION) */ }
} catch { /* not an installed tree (no msd-core/VERSION) */ }
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const pkg = require(path.join(libDir, '..', '..', '..', 'package.json')) as { version?: string };
@@ -748,20 +748,20 @@ function assertSafeTarMembers(execTar: TarExecFn, tgzPath: string): void {
function stageValidated(opts: {
sourceDir: string;
id: string;
gsdHome: string;
msdHome: string;
hostVersion: string;
source: string;
integrity: string | null;
promote?: boolean;
skipEnginesGate?: boolean;
}): ResolveResult {
const { sourceDir, id, gsdHome, hostVersion, source, integrity } = opts;
const { sourceDir, id, msdHome, hostVersion, source, integrity } = opts;
const promote = opts.promote !== false;
// Safety: validate id before using it in a path.
assertSafeId(id);
const capabilitiesRoot = path.join(gsdHome, '.gsd', 'capabilities');
const capabilitiesRoot = path.join(msdHome, '.msd', 'capabilities');
const stagingRoot = path.join(capabilitiesRoot, '.staging');
const stagingDir = path.join(stagingRoot, `${id}-${process.pid}-${Date.now()}`);
const finalDir = path.join(capabilitiesRoot, id);
@@ -807,14 +807,14 @@ function stageValidated(opts: {
`capability.json not found in staged directory: ${stagingDir}`,
);
// engines.gsd pre-check — reject before staging finalizes (unless the caller owns the gate).
// engines.msd pre-check — reject before staging finalizes (unless the caller owns the gate).
const engines = cap['engines'];
if (!opts.skipEnginesGate && engines && typeof engines === 'object' && !Array.isArray(engines)) {
const gsdRange = (engines as Record<string, unknown>)['gsd'];
if (typeof gsdRange === 'string' && gsdRange) {
if (!semverMod.semverSatisfies(hostVersion, gsdRange)) {
const msdRange = (engines as Record<string, unknown>)['msd'];
if (typeof msdRange === 'string' && msdRange) {
if (!semverMod.semverSatisfies(hostVersion, msdRange)) {
throw new Error(
`Capability requires engines.gsd "${gsdRange}" but running GSD is ${hostVersion}`
`Capability requires engines.msd "${msdRange}" but running MSD is ${hostVersion}`
);
}
}
@@ -851,7 +851,7 @@ function stageValidated(opts: {
const seedLoader = require('./capability-loader.cjs') as {
crossValidationSeed: (
cwd: string,
gsdHome: string,
msdHome: string,
hostVersion: string,
validator: ValidatorModule,
semver: { semverSatisfies: (version: unknown, range: unknown) => boolean },
@@ -860,7 +860,7 @@ function stageValidated(opts: {
/* eslint-enable @typescript-eslint/no-require-imports */
const { capMap, centralKeys } = seedLoader.crossValidationSeed(
process.cwd(),
gsdHome,
msdHome,
hostVersion,
capValidator,
semverMod,
@@ -986,7 +986,7 @@ function parseSpec(spec: string): ParsedSpec {
function resolveLocal(
parsed: ParsedSpec,
opts: ResolveOptions,
gsdHome: string,
msdHome: string,
hostVersion: string
): ResolveResult {
// #1460 CS-1: a local path is a directory tree, not a single downloadable artifact, so there is
@@ -1011,13 +1011,13 @@ function resolveLocal(
const id = typeof cap['id'] === 'string' ? cap['id'] : '';
if (!id) throw new Error('capability.json missing "id" field');
return stageValidated({ sourceDir: absPath, id, gsdHome, hostVersion, source: parsed.raw, integrity: null, promote: opts.promote, skipEnginesGate: opts.skipEnginesGate });
return stageValidated({ sourceDir: absPath, id, msdHome, hostVersion, source: parsed.raw, integrity: null, promote: opts.promote, skipEnginesGate: opts.skipEnginesGate });
}
function resolveGit(
parsed: ParsedSpec,
opts: ResolveOptions,
gsdHome: string,
msdHome: string,
hostVersion: string
): ResolveResult {
const execGit = opts.execOverrides?.git ?? shellSeam.execGit;
@@ -1030,7 +1030,7 @@ function resolveGit(
throw new Error('integrity pinning is not supported for git sources; pin the commit with #sha:<commit>');
}
const cloneDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-cap-git-'));
const cloneDir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-cap-git-'));
try {
// Clone (copy only — no hooks execute on clone, no npm install).
const cloneResult = execGit(['clone', '--depth', '1', '--', parsed.target, cloneDir], { timeout: 60_000 });
@@ -1060,7 +1060,7 @@ function resolveGit(
const id = typeof cap['id'] === 'string' ? cap['id'] : '';
if (!id) throw new Error('capability.json missing "id" field');
return stageValidated({ sourceDir: cloneDir, id, gsdHome, hostVersion, source: parsed.raw, integrity: null, promote: opts.promote, skipEnginesGate: opts.skipEnginesGate });
return stageValidated({ sourceDir: cloneDir, id, msdHome, hostVersion, source: parsed.raw, integrity: null, promote: opts.promote, skipEnginesGate: opts.skipEnginesGate });
} finally {
try { fs.rmSync(cloneDir, { recursive: true, force: true }); } catch { /* best-effort */ }
}
@@ -1069,15 +1069,15 @@ function resolveGit(
function resolveNpm(
parsed: ParsedSpec,
opts: ResolveOptions,
gsdHome: string,
msdHome: string,
hostVersion: string
): ResolveResult {
const execNpm = opts.execOverrides?.npm ?? shellSeam.execNpm;
// tar override: injected for tests; default delegates to shell seam execTool.
const execTar: TarExecFn = opts.execOverrides?.tar ?? shellSeam.execTool;
const tmpPackDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-cap-npm-pack-'));
const extractDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-cap-npm-ext-'));
const tmpPackDir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-cap-npm-pack-'));
const extractDir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-cap-npm-ext-'));
try {
// npm pack — creates a tarball. CRITICAL: `npm pack` runs prepack/prepare
@@ -1132,7 +1132,7 @@ function resolveNpm(
const id = typeof cap['id'] === 'string' ? cap['id'] : '';
if (!id) throw new Error('capability.json missing "id" field');
return stageValidated({ sourceDir, id, gsdHome, hostVersion, source: parsed.raw, integrity: computedIntegrity, promote: opts.promote, skipEnginesGate: opts.skipEnginesGate });
return stageValidated({ sourceDir, id, msdHome, hostVersion, source: parsed.raw, integrity: computedIntegrity, promote: opts.promote, skipEnginesGate: opts.skipEnginesGate });
} finally {
try { fs.rmSync(tmpPackDir, { recursive: true, force: true }); } catch { /* best-effort */ }
try { fs.rmSync(extractDir, { recursive: true, force: true }); } catch { /* best-effort */ }
@@ -1142,7 +1142,7 @@ function resolveNpm(
async function resolveTarball(
parsed: ParsedSpec,
opts: ResolveOptions,
gsdHome: string,
msdHome: string,
hostVersion: string
): Promise<ResolveResult> {
// tar override: injected for tests; default delegates to shell seam execTool.
@@ -1157,7 +1157,7 @@ async function resolveTarball(
verifyIntegrity(resp.body, opts.integrity);
}
const extractDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-cap-tar-'));
const extractDir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-cap-tar-'));
const tgzPath = path.join(extractDir, '_download.tgz');
try {
@@ -1185,7 +1185,7 @@ async function resolveTarball(
const id = typeof cap['id'] === 'string' ? cap['id'] : '';
if (!id) throw new Error('capability.json missing "id" field');
return stageValidated({ sourceDir, id, gsdHome, hostVersion, source: parsed.raw, integrity: computedIntegrity, promote: opts.promote, skipEnginesGate: opts.skipEnginesGate });
return stageValidated({ sourceDir, id, msdHome, hostVersion, source: parsed.raw, integrity: computedIntegrity, promote: opts.promote, skipEnginesGate: opts.skipEnginesGate });
} finally {
try { fs.rmSync(extractDir, { recursive: true, force: true }); } catch { /* best-effort */ }
}
@@ -1196,27 +1196,27 @@ async function resolveTarball(
// ---------------------------------------------------------------------------
/**
* Resolve a capability spec, validate it, and stage it into the GSD capabilities dir.
* Resolve a capability spec, validate it, and stage it into the MSD capabilities dir.
*
* @param spec - Source spec string. Kind auto-detected via parseSpec.
* @param opts - Optional overrides for hostVersion, gsdHome, integrity, exec/http seams.
* @param opts - Optional overrides for hostVersion, msdHome, integrity, exec/http seams.
* @returns - Resolved bundle descriptor with stagedDir path.
*/
async function resolveCapabilitySource(spec: string, opts: ResolveOptions = {}): Promise<ResolveResult> {
const parsed = parseSpec(spec);
const hostVersion = opts.hostVersion ?? readHostVersion();
const gsdHome = opts.gsdHome ?? process.env['GSD_HOME'] ?? os.homedir();
const msdHome = opts.msdHome ?? process.env['MSD_HOME'] ?? os.homedir();
switch (parsed.kind) {
case 'local':
return resolveLocal(parsed, opts, gsdHome, hostVersion);
return resolveLocal(parsed, opts, msdHome, hostVersion);
case 'git':
return resolveGit(parsed, opts, gsdHome, hostVersion);
return resolveGit(parsed, opts, msdHome, hostVersion);
case 'npm':
return resolveNpm(parsed, opts, gsdHome, hostVersion);
return resolveNpm(parsed, opts, msdHome, hostVersion);
case 'tarball':
return resolveTarball(parsed, opts, gsdHome, hostVersion);
return resolveTarball(parsed, opts, msdHome, hostVersion);
case 'registry':
throw new Error(
'registry source kind is not yet implemented (no first-party registry endpoint)'

View File

@@ -4,7 +4,7 @@
* Unified capability-state resolver that composes the three toggle systems
* (install profile, runtime surface, config activation) into one per-capability
* view. The loop resolver consumes this state so workflow dispatch and the
* `gsd-tools capability state` diagnostic share the same enablement answer.
* `msd-tools capability state` diagnostic share the same enablement answer.
*
* Exports (three things, mirroring loop-resolver):
* resolveCapabilityState({ registry, installedSkills, surfacedSkills, config, cwd })
@@ -25,7 +25,7 @@
* - ./surface.cjs (resolveSurface)
* - ./config-loader.cjs (loadConfig)
* - ./runtime-homes.cjs (getGlobalConfigDir — for runtimeConfigDir auto-detection)
* - ./runtime-slash.cjs (resolveRuntime — GSD_RUNTIME > config.runtime > 'claude' precedence)
* - ./runtime-slash.cjs (resolveRuntime — MSD_RUNTIME > config.runtime > 'claude' precedence)
* - capability-registry.cjs (loaded at call time)
*/
@@ -314,30 +314,30 @@ function resolveCapabilityState(input: ResolveCapabilityStateInput): ResolveCapa
// ─── I/O command handler ───────────────────────────────────────────────────────
/**
* Derive the commands/gsd path from __dirname (which resolves to
* gsd-core/bin/lib/ at runtime). The source tree is:
* <repo>/gsd-core/bin/lib/capability-state.cjs
* <repo>/commands/gsd/*.md
* So we walk up three levels: lib/ → bin/ → gsd-core/ → <repo>/, then
* into commands/gsd/.
* Derive the commands/msd path from __dirname (which resolves to
* msd-core/bin/lib/ at runtime). The source tree is:
* <repo>/msd-core/bin/lib/capability-state.cjs
* <repo>/commands/msd/*.md
* So we walk up three levels: lib/ → bin/ → msd-core/ → <repo>/, then
* into commands/msd/.
*/
function _resolveCommandsGsdDir(): string {
// __dirname = gsd-core/bin/lib/
function _resolveCommandsMsdDir(): string {
// __dirname = msd-core/bin/lib/
const repoRoot = path.resolve(__dirname, '..', '..', '..');
return path.join(repoRoot, 'commands', 'gsd');
return path.join(repoRoot, 'commands', 'msd');
}
/**
* Build a skill dependency manifest from an INSTALLED runtime's skills directory.
*
* In an installed runtime (e.g. Codex at ~/.codex), gsd skills live as
* configDir/skills/gsd-STEM/SKILL.md. There is no commands/gsd source tree.
* In an installed runtime (e.g. Codex at ~/.codex), msd skills live as
* configDir/skills/msd-STEM/SKILL.md. There is no commands/msd source tree.
* This function scans that installed layout and builds the same
* Map shape that loadSkillsManifest produces from sources.
*
* Stem extraction: a directory named gsd-secure-phase maps to stem secure-phase.
* Only directories whose names start with gsd- are included so user-created
* skills (without the gsd- prefix) are not accidentally pulled in.
* Stem extraction: a directory named msd-secure-phase maps to stem secure-phase.
* Only directories whose names start with msd- are included so user-created
* skills (without the msd- prefix) are not accidentally pulled in.
*
* The requires: field is parsed via the shared parseRequires helper (the same
* parser loadSkillsManifest uses), so the two paths cannot drift.
@@ -356,15 +356,15 @@ function _loadInstalledSkillsManifest(configDir: string): Map<string, string[]>
}
for (const entry of entries) {
if (!entry.isDirectory()) continue;
if (!entry.name.startsWith('gsd-')) continue;
// Strip the 'gsd-' prefix to get the skill stem
const stem = entry.name.slice(4); // 'gsd-'.length === 4
if (!entry.name.startsWith('msd-')) continue;
// Strip the 'msd-' prefix to get the skill stem
const stem = entry.name.slice(4); // 'msd-'.length === 4
if (!stem) continue;
const skillMdPath = path.join(skillsDir, entry.name, 'SKILL.md');
// Parity with loadSkillsManifest: a stem exists only when its artifact
// file is present. loadSkillsManifest registers a stem per .md FILE (and
// tolerates an unreadable file as []), but never invents a stem for which
// no file exists. Mirror that here: a stale gsd-<stem>/ directory with no
// no file exists. Mirror that here: a stale msd-<stem>/ directory with no
// SKILL.md must NOT register the stem — otherwise the capability would be
// wrongly reported surfaced/enabled and a verify:post hook would render
// for a skill that cannot run.
@@ -391,11 +391,11 @@ function _loadInstalledSkillsManifest(configDir: string): Map<string, string[]>
}
/**
* #1858 — Build a skill dependency manifest from a FLAT commands/gsd-<stem>.md
* source layout (the Claude local project install shape, where the `gsd-`
* #1858 — Build a skill dependency manifest from a FLAT commands/msd-<stem>.md
* source layout (the Claude local project install shape, where the `msd-`
* prefix is baked into each filename at the commands/ level and there is no
* commands/gsd/ subdir). Strips the `gsd-` prefix so stems match the nested
* loader's output (gsd-validate-phase.md → validate-phase, same as nested
* commands/msd/ subdir). Strips the `msd-` prefix so stems match the nested
* loader's output (msd-validate-phase.md → validate-phase, same as nested
* validate-phase.md).
*
* Map shape is identical to loadSkillsManifest: each stem maps to its
@@ -404,20 +404,20 @@ function _loadInstalledSkillsManifest(configDir: string): Map<string, string[]>
* flat and nested paths cannot drift.
*
* Returns an empty Map when the parent directory does not exist or contains
* no gsd-*.md files (so _resolveManifest can use size>0 as the "flat layout
* no msd-*.md files (so _resolveManifest can use size>0 as the "flat layout
* present" signal and fall through to the installed-skills branch otherwise).
*/
function _loadFlatCommandsGsdManifest(commandsParentDir: string, workflowsDir?: string): Map<string, string[]> {
function _loadFlatCommandsMsdManifest(commandsParentDir: string, workflowsDir?: string): Map<string, string[]> {
// #3798: derive agents from the command body PLUS the workflow files it
// references, exactly like loadSkillsManifest does for the nested layout —
// the flat (#1858) layout retained the original defect otherwise, and the
// parity test in tests/capability-state.test.cjs asserts the two loaders
// produce identical _calls_agents_* sets. Default: <pkg>/gsd-core/workflows
// produce identical _calls_agents_* sets. Default: <pkg>/msd-core/workflows
// one level above the commands dir — the shape of both the repo checkout
// (<repo>/commands/) and a flat runtime install (the runtime config dir's
// commands/ with the package's gsd-core/ beside it).
// commands/ with the package's msd-core/ beside it).
const flatWorkflowsDir = workflowsDir
|| path.resolve(commandsParentDir, '..', 'gsd-core', 'workflows');
|| path.resolve(commandsParentDir, '..', 'msd-core', 'workflows');
const manifest = new Map<string, string[]>();
let entries: fs.Dirent[];
try {
@@ -427,9 +427,9 @@ function _loadFlatCommandsGsdManifest(commandsParentDir: string, workflowsDir?:
}
for (const entry of entries) {
if (!entry.isFile()) continue;
if (!entry.name.startsWith('gsd-')) continue;
if (!entry.name.startsWith('msd-')) continue;
if (!entry.name.endsWith('.md')) continue;
// Strip 'gsd-' prefix (4 chars) and '.md' suffix (3 chars) → stem.
// Strip 'msd-' prefix (4 chars) and '.md' suffix (3 chars) → stem.
const stem = entry.name.slice(4, -3);
if (!stem) continue;
// Mirror loadSkillsManifest's try/catch structure exactly: wrap read +
@@ -458,14 +458,14 @@ function _loadFlatCommandsGsdManifest(commandsParentDir: string, workflowsDir?:
* Resolve the skill dependency manifest for capability-state resolution.
*
* Resolution order:
* 1. If commandsGsdDir exists, load from the nested source layout
* (repo-checkout behavior: <repo>/commands/gsd/*.md).
* 2. #1858 — otherwise, if the flat source layout is present (gsd-<stem>.md
* files in dirname(commandsGsdDir)), load from there. This is the Claude
* local project install shape where commands/gsd/ does not exist but
* commands/gsd-<stem>.md files do.
* 1. If commandsMsdDir exists, load from the nested source layout
* (repo-checkout behavior: <repo>/commands/msd/*.md).
* 2. #1858 — otherwise, if the flat source layout is present (msd-<stem>.md
* files in dirname(commandsMsdDir)), load from there. This is the Claude
* local project install shape where commands/msd/ does not exist but
* commands/msd-<stem>.md files do.
* 3. #1160 — otherwise, fall back to installed skills at
* configDir/skills/gsd-[stem]/SKILL.md.
* configDir/skills/msd-[stem]/SKILL.md.
*
* In an installed runtime both source trees are absent; only the skills/
* layout exists. Returning an empty manifest caused resolveSurface to
@@ -473,15 +473,15 @@ function _loadFlatCommandsGsdManifest(commandsParentDir: string, workflowsDir?:
* capability appear unsurfaced even when the skill was physically installed
* (#1160) or authored as a flat command file (#1858).
*/
function _resolveManifest(commandsGsdDir: string, configDir: string): Map<string, string[]> {
if (fs.existsSync(commandsGsdDir)) {
return loadSkillsManifest(commandsGsdDir);
function _resolveManifest(commandsMsdDir: string, configDir: string): Map<string, string[]> {
if (fs.existsSync(commandsMsdDir)) {
return loadSkillsManifest(commandsMsdDir);
}
// #1858: flat source layout — gsd-<stem>.md files at dirname(commandsGsdDir).
// Only claim the flat branch when it actually has gsd-*.md files; otherwise
// fall through to the installed-skills branch (a commands/ dir with no gsd
// #1858: flat source layout — msd-<stem>.md files at dirname(commandsMsdDir).
// Only claim the flat branch when it actually has msd-*.md files; otherwise
// fall through to the installed-skills branch (a commands/ dir with no msd
// files must not shadow an installed skills/ tree).
const flat = _loadFlatCommandsGsdManifest(path.dirname(commandsGsdDir));
const flat = _loadFlatCommandsMsdManifest(path.dirname(commandsMsdDir));
if (flat.size > 0) return flat;
return _loadInstalledSkillsManifest(configDir);
}
@@ -494,7 +494,7 @@ function _resolveManifest(commandsGsdDir: string, configDir: string): Map<string
*
* runtimeConfigDir resolution (when not provided or empty):
* Detects the active runtime via the canonical precedence:
* process.env.GSD_RUNTIME → config.runtime → 'claude'
* process.env.MSD_RUNTIME → config.runtime → 'claude'
* (using resolveRuntime() from runtime-slash.cjs, the same precedence used
* by profile-output.cjs and the rest of the runtime resolution chain).
* Then calls getGlobalConfigDir(detectedRuntime) from runtime-homes.cjs —
@@ -530,7 +530,7 @@ function resolveCapabilityRuntimeState(
// Resolve runtimeConfigDir using the canonical runtime-homes resolver.
// When not provided, the active runtime is detected via the canonical
// precedence: process.env.GSD_RUNTIME → config.runtime → 'claude'
// precedence: process.env.MSD_RUNTIME → config.runtime → 'claude'
// (mirrors resolveRuntime() from runtime-slash.cjs and the precedence used
// by profile-output.cjs and the rest of the runtime resolution chain).
// getGlobalConfigDir(detectedRuntime) is then called, which honours the
@@ -545,7 +545,7 @@ function resolveCapabilityRuntimeState(
getGlobalConfigDir: (runtime: string) => string;
};
// #2003: an explicit --runtime override bypasses the persisted-runtime
// fallback (GSD_RUNTIME → config.runtime → 'claude') so, e.g., a repo with
// fallback (MSD_RUNTIME → config.runtime → 'claude') so, e.g., a repo with
// persisted runtime:"codex" resolves the Claude config dir when the operator
// is driving from Claude Code. Canonicalize via runtime-name-policy (handles
// aliases like codex-app → codex); if canonicalization yields nothing, fall
@@ -577,7 +577,7 @@ function resolveCapabilityRuntimeState(
const runtimeSlash = require('./runtime-slash.cjs') as {
resolveRuntime: (projectDir: string | null | undefined) => string;
};
// Detect the active runtime via GSD_RUNTIME → config.runtime → 'claude'.
// Detect the active runtime via MSD_RUNTIME → config.runtime → 'claude'.
// resolveRuntime reads config.json directly (no side effects) and returns
// a lowercased canonical runtime name.
const detectedRuntime = runtimeSlash.resolveRuntime(cwd);
@@ -597,10 +597,10 @@ function resolveCapabilityRuntimeState(
// reflected in installed/surfaced state exactly like first-party capabilities.
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { loadRegistry } = require('./capability-loader.cjs') as { loadRegistry: (opts?: Record<string, unknown>) => Record<string, unknown> };
// #1459 IC-04: thread the consent home (process.env.GSD_HOME) EXPLICITLY so the overlay's global root
// #1459 IC-04: thread the consent home (process.env.MSD_HOME) EXPLICITLY so the overlay's global root
// and the project-scope consent lookup resolve to the SAME user-owned home this consumer sees — a
// legitimately-consented project cap then reports ACTIVE here (not falsely inactive at the wrong home).
const registry = loadRegistry({ includeInstalled: true, cwd, gsdHome: process.env['GSD_HOME'] });
const registry = loadRegistry({ includeInstalled: true, cwd, msdHome: process.env['MSD_HOME'] });
// ── Resolve installed skills (from install profile) ──────────────────────────
// Distinguish "no profile marker → default full" (legitimate) from a thrown
@@ -608,10 +608,10 @@ function resolveCapabilityRuntimeState(
// installedSkills='*' as if the install profile were truly unlimited).
let installedSkills: Set<string> | '*';
try {
const commandsGsdDir = _resolveCommandsGsdDir();
const commandsMsdDir = _resolveCommandsMsdDir();
// Fix #1160: use _resolveManifest so installed-runtime layouts (where
// commands/gsd is absent) fall back to <configDir>/skills/gsd-*/SKILL.md.
const manifest = _resolveManifest(commandsGsdDir, resolvedConfigDir);
// commands/msd is absent) fall back to <configDir>/skills/msd-*/SKILL.md.
const manifest = _resolveManifest(commandsMsdDir, resolvedConfigDir);
const profileName = readActiveProfile(resolvedConfigDir) ?? 'full';
const resolvedInstall = resolveProfile({
modes: profileName.split(',').map((s: string) => s.trim()),
@@ -630,10 +630,10 @@ function resolveCapabilityRuntimeState(
// ── Resolve surfaced skills (from runtime surface) ────────────────────────────
let surfacedSkills: Set<string>;
try {
const commandsGsdDir = _resolveCommandsGsdDir();
const commandsMsdDir = _resolveCommandsMsdDir();
// Fix #1160: use _resolveManifest so installed-runtime layouts (where
// commands/gsd is absent) fall back to <configDir>/skills/gsd-*/SKILL.md.
const manifest = _resolveManifest(commandsGsdDir, resolvedConfigDir);
// commands/msd is absent) fall back to <configDir>/skills/msd-*/SKILL.md.
const manifest = _resolveManifest(commandsMsdDir, resolvedConfigDir);
const surfaceResult = resolveSurface(resolvedConfigDir, manifest, undefined, registry);
// resolveSurface returns { name, skills: Set<string>, agents: Set<string> }
// (always a concrete Set — full profile is materialized)
@@ -686,7 +686,7 @@ function cmdCapabilityState(
options: Record<string, unknown> = {},
): void {
// #2003: thread an explicit --runtime override so the config-dir resolution
// bypasses the persisted-runtime fallback (GSD_RUNTIME → config.runtime).
// bypasses the persisted-runtime fallback (MSD_RUNTIME → config.runtime).
const runtimeOverride = typeof options['runtime'] === 'string' ? options['runtime'] : undefined;
const result = resolveCapabilityRuntimeState(cwd, runtimeConfigDir, undefined, runtimeOverride);
for (const warning of result.warnings) {
@@ -734,9 +734,9 @@ export = {
isCapabilityActive,
cmdCapabilityState,
// Exported for tests
_resolveCommandsGsdDir,
_resolveCommandsMsdDir,
_loadInstalledSkillsManifest,
_loadFlatCommandsGsdManifest,
_loadFlatCommandsMsdManifest,
_resolveManifest,
_isSafePropKey,
};

View File

@@ -6,7 +6,7 @@
* PURE module. It computes *what* a capability would do and *whether* policy allows it; it
* never mutates the filesystem and never performs I/O beyond reading staged files to confirm
* declared executable artifacts exist. The actual consent decision (yes/no) is passed in by the
* caller — GSD has no interactive-prompt layer in lib (the runtime/CLI edge owns that), so the
* caller — MSD has no interactive-prompt layer in lib (the runtime/CLI edge owns that), so the
* gate stays testable and side-effect-free. See docs/explanation/capability-trust-model.md.
*
* LEAF MODULE — imports ONLY: node:fs, node:path, and ./semver-compare.cjs.
@@ -19,7 +19,7 @@
* class existed, so no already-consented capability re-prompts on upgrade. The RESOLVED host (as
* opposed to the declared `hostConfigKey`) is disclosed to a human but deliberately EXCLUDED from
* the signature — the loader has no config resolver and must compute the same signature as the
* lifecycle (constraint 2, `.gsd/phase/chore-2796-reviewer-trust-disclosure/40-design.md`).
* lifecycle (constraint 2, `.msd/phase/chore-2796-reviewer-trust-disclosure/40-design.md`).
*
* ADR-2363 D5 (#3248): a capability's declared `skills` are INSTRUCTION surfaces — their bodies are
* copied verbatim into the user's agent instruction context, so their reach is bounded only by what
@@ -43,7 +43,7 @@
* collectInstructionSurfaces(...) — the instruction-surface collector, independently testable
* checkReservedNamespace(id) — is this id in a reserved namespace?
* evaluateSourceAllowed(parsed,...) — strictKnownRegistries enforcement
* checkEngines(manifest, host) — engines.gsd hard gate + compatVersions downgrade
* checkEngines(manifest, host) — engines.msd hard gate + compatVersions downgrade
* evaluateInstallTrust(args) — compose: source + namespace + engines + disclosure
* executableSetChanged(old, new) — did the executable surface set change between versions?
* summarizeDisclosure(disclosure) — human-readable consent-prompt lines
@@ -70,7 +70,7 @@ const semverMod = require('./semver-compare.cjs') as {
* id begins with any of these is rejected at install so it cannot impersonate a first-party
* one. Match is case-insensitive on the normalized id.
*/
const RESERVED_NAMESPACES = ['gsd-', 'gsd-core-', 'anthropic-'];
const RESERVED_NAMESPACES = ['msd-', 'msd-core-', 'anthropic-'];
/**
* ADR-2782 D5's gating requirement (#2796): every reviewer lane is piped the plan text,
@@ -312,7 +312,7 @@ interface InstructionSurface {
interface Disclosure {
/** Hook scripts the capability registers (each runs as a runtime hook command). */
hooks: HookSurface[];
/** Command modules the capability ships (each is require()'d into the GSD CLI process). */
/** Command modules the capability ships (each is require()'d into the MSD CLI process). */
commandModules: CommandModuleSurface[];
/** MCP servers the capability declares (each spawned by the host runtime) — name AND command. */
mcpServers: McpServerSurface[];
@@ -368,7 +368,7 @@ interface SourceVerdict {
interface EnginesVerdict {
/** Does the capability's *current* version run on this host? */
compatible: boolean;
/** The declared engines.gsd range, or null if unconstrained. */
/** The declared engines.msd range, or null if unconstrained. */
range: string | null;
satisfiedBy: 'engines' | 'compatVersions' | 'unconstrained' | null;
/** When the current version is incompatible but compatVersions names one that works. */
@@ -578,7 +578,7 @@ function collectHookSurfaces(
/**
* Collect the `commands` executable-surface class: [{ family, module, router? }] — modules
* require()'d into the GSD CLI process. Extracted, BEHAVIOR UNCHANGED — see `collectHookSurfaces`.
* require()'d into the MSD CLI process. Extracted, BEHAVIOR UNCHANGED — see `collectHookSurfaces`.
*/
function collectCommandSurfaces(
manifest: CapabilityManifest,
@@ -656,7 +656,7 @@ function collectMcpSurfaces(manifest: CapabilityManifest): McpServerSurface[] {
const cwd = asString(cfg['cwd']);
// Finding 5 (MEDIUM, #1459): capture the FULL config (every declared field the writer persists),
// not just the whitelisted ones. Prototype-pollution-safe: copy only own enumerable keys and
// never the dangerous keys. The CAP_MARKER the writer stamps on persist (`_gsdCapability`) is the
// never the dangerous keys. The CAP_MARKER the writer stamps on persist (`_msdCapability`) is the
// capability id (constant per cap), so it does not perturb the signature; we copy config as
// DECLARED here (pre-stamp) and the writer adds the marker at write time.
const rawConfig: Record<string, unknown> = {};
@@ -1098,18 +1098,18 @@ function evaluateSourceAllowed(parsed: ParsedSpec, strict: StrictKnownRegistries
}
// ---------------------------------------------------------------------------
// engines.gsd hard gate + compatVersions downgrade
// engines.msd hard gate + compatVersions downgrade
// ---------------------------------------------------------------------------
/**
* Hard-gate a manifest against the running host version via engines.gsd, consulting
* Hard-gate a manifest against the running host version via engines.msd, consulting
* compatVersions for a graceful-downgrade target when the current version is incompatible.
*/
function checkEngines(manifest: CapabilityManifest, hostVersion: string): EnginesVerdict {
const engines = manifest.engines;
let range: string | null = null;
if (engines && typeof engines === 'object' && !Array.isArray(engines)) {
const g = (engines as Record<string, unknown>)['gsd'];
const g = (engines as Record<string, unknown>)['msd'];
if (typeof g === 'string' && g) range = g;
}
@@ -1124,9 +1124,9 @@ function checkEngines(manifest: CapabilityManifest, hostVersion: string): Engine
const compat = manifest.compatVersions;
let best: string | undefined;
if (compat && typeof compat === 'object' && !Array.isArray(compat)) {
for (const [capVer, gsdRange] of Object.entries(compat as Record<string, unknown>)) {
if (typeof gsdRange !== 'string' || !gsdRange) continue;
if (!semverMod.semverSatisfies(hostVersion, gsdRange)) continue;
for (const [capVer, msdRange] of Object.entries(compat as Record<string, unknown>)) {
if (typeof msdRange !== 'string' || !msdRange) continue;
if (!semverMod.semverSatisfies(hostVersion, msdRange)) continue;
if (best === undefined || semverMod.isSemverNewer(capVer, best)) best = capVer;
}
}
@@ -1146,7 +1146,7 @@ function checkEngines(manifest: CapabilityManifest, hostVersion: string): Engine
* executable-surface disclosure. `allowed` is true only when no gate blocks; `requiresConsent`
* is true when allowed AND the capability ships any executable surface.
*
* engines.gsd is also enforced inside resolveCapabilitySource at resolve time; re-checking here
* engines.msd is also enforced inside resolveCapabilitySource at resolve time; re-checking here
* is defense-in-depth and lets callers surface a compatVersions downgrade hint.
*/
function evaluateInstallTrust(args: InstallTrustArgs): InstallTrustVerdict {
@@ -1170,7 +1170,7 @@ function evaluateInstallTrust(args: InstallTrustArgs): InstallTrustVerdict {
? ` (compatVersions offers ${engines.downgradeTo} for this host)`
: '';
blockReasons.push(
`capability requires engines.gsd "${engines.range}" but host is ${hostVersion}${hint}`,
`capability requires engines.msd "${engines.range}" but host is ${hostVersion}${hint}`,
);
}
@@ -1497,7 +1497,7 @@ function summarizeInstructionSurfaces(disclosure: Disclosure): string[] {
* bounds the length. These lines are joined with `\n` and written RAW to stderr on the
* needs-consent path (`capability-command-router.cjs`), so an unescaped value could forge a line or
* rewrite/clear output already printed — defeating the informed-consent guarantee this function
* exists to provide. GSD-authored literals (fallback placeholders, headings, `<redacted>`) are never
* exists to provide. MSD-authored literals (fallback placeholders, headings, `<redacted>`) are never
* escaped — only manifest-supplied data is.
*/
/**
@@ -1539,7 +1539,7 @@ function summarizeDisclosure(disclosure: Disclosure): string[] {
}
if (disclosure.commandModules.length > 0) {
lines.push(
` command modules (${disclosure.commandModules.length}): require()'d into the GSD CLI process`,
` command modules (${disclosure.commandModules.length}): require()'d into the MSD CLI process`,
);
for (const m of disclosure.commandModules) {
// TRUST2-3 (#1459): show the router (which exported fn runs) so the user consents to the exact entry point.
@@ -1696,7 +1696,7 @@ function summarizeDisclosure(disclosure: Disclosure): string[] {
/**
* #3514 (F21c): the one-line integrity status for the consent prompt, or null when the caller
* supplied no `integrityPin` (legacy — no line, byte-identical output). GSD-authored literals,
* supplied no `integrityPin` (legacy — no line, byte-identical output). MSD-authored literals,
* never manifest data — no escaping needed. A consent-prompt claim must be EXACT: a git
* commit-pinned source renders its own line, never "sha512 pin" — no sha512 was supplied
* (isolated review finding).

View File

@@ -7,14 +7,14 @@
* cmdCapabilitySet(cwd, runtimeConfigDir, capId, options, raw)
*
* Projection rules (three axes: install, surface, config):
* - enabled axis: mutates .gsd-surface.json via readSurface/writeSurface
* - enabled axis: mutates .msd-surface.json via readSurface/writeSurface
* - gates axis: mutates .planning/config.json via setConfigValues (batched)
* - materialize: optionally calls applySurface to write skill files
* - re-resolve: always calls resolveCapabilityRuntimeState for the return value
*
* Dependencies (leaf modules only — no circular risk):
* - ./io.cjs (output, error)
* - ./capability-state.cjs (resolveCapabilityRuntimeState, _resolveManifest, _resolveCommandsGsdDir)
* - ./capability-state.cjs (resolveCapabilityRuntimeState, _resolveManifest, _resolveCommandsMsdDir)
* - ./surface.cjs (readSurface, writeSurface, applySurface)
* - ./install-profiles.cjs (readActiveProfile)
* - ./config.cjs (setConfigValues)
@@ -26,7 +26,7 @@
import ioMod = require('./io.cjs');
const { output: coreOutput } = ioMod;
// ExitError (NOT process.exit) is how every gsd-tools command signals a non-zero exit: runMain
// ExitError (NOT process.exit) is how every msd-tools command signals a non-zero exit: runMain
// translates it to process.exitCode so buffered stdout flushes first. Calling process.exit() here
// truncates a just-written --raw JSON payload before the reader sees it (a real silent-output bug).
// eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -35,7 +35,7 @@ const { ExitError } = cliExitMod;
// eslint-disable-next-line @typescript-eslint/no-require-imports
import capabilityStateMod = require('./capability-state.cjs');
const { resolveCapabilityRuntimeState, _resolveManifest, _resolveCommandsGsdDir } = capabilityStateMod;
const { resolveCapabilityRuntimeState, _resolveManifest, _resolveCommandsMsdDir } = capabilityStateMod;
// eslint-disable-next-line @typescript-eslint/no-require-imports
import surfaceMod = require('./surface.cjs');
@@ -161,7 +161,7 @@ function setCapabilityState(
// is STILL rejected because it is absent from the composed capabilities map.
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { loadRegistry } = require('./capability-loader.cjs') as { loadRegistry: (opts?: Record<string, unknown>) => Record<string, unknown> };
const registry = loadRegistry({ includeInstalled: true, cwd, gsdHome: process.env['GSD_HOME'] });
const registry = loadRegistry({ includeInstalled: true, cwd, msdHome: process.env['MSD_HOME'] });
const capabilitiesMap = (
registry['capabilities'] && typeof registry['capabilities'] === 'object' && !Array.isArray(registry['capabilities'])
? registry['capabilities']
@@ -373,8 +373,8 @@ function setCapabilityState(
// (see resolveRuntimeArtifactLayout's #2322 doc comment).
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const layout = runtimeArtifactLayout.resolveRuntimeArtifactLayout(runtime, resolvedConfigDir, scope, registry);
const commandsGsdDir = _resolveCommandsGsdDir();
const manifest = _resolveManifest(commandsGsdDir, resolvedConfigDir);
const commandsMsdDir = _resolveCommandsMsdDir();
const manifest = _resolveManifest(commandsMsdDir, resolvedConfigDir);
// #1575: applySurface now accepts opts.resolveAttribution so surface-path
// agents get the same Co-Authored-By trailer as the install path. The
// resolver is not threaded here yet — the CLI command handler does not have
@@ -412,7 +412,7 @@ function setCapabilityState(
const afterCap = after.capabilities.find((c: CapabilityStateEntry) => c.id === entry.id);
if (afterCap && afterCap.enabled !== true) {
errors.push(
`cannot enable "${entry.id}": not in the active surface/profile (widen the profile or use /gsd:surface enable)`,
`cannot enable "${entry.id}": not in the active surface/profile (widen the profile or use /msd:surface enable)`,
);
}
}
@@ -455,7 +455,7 @@ function setCapabilityState(
}
/**
* CLI command entry point for `gsd-tools capability set`.
* CLI command entry point for `msd-tools capability set`.
*
* Builds one DesiredCapability from the provided options, calls setCapabilityState,
* then prints the result. When raw=true emits JSON; else emits a human summary.

View File

@@ -306,8 +306,8 @@ function loadDecisionExtraction(contextPath: string): { trackable: Decision[]; o
* (#2492, #1365 fail-loud, #2770 empty-arg fail-closed).
*
* Invocation (the context path may be supplied EITHER way; #4130 follow-up):
* gsd_run check decision-coverage-plan <phase-dir> <context-path> (positional, the workflow caller's form)
* gsd_run check decision-coverage-plan --context <path> [<phase-dir>]
* msd_run check decision-coverage-plan <phase-dir> <context-path> (positional, the workflow caller's form)
* msd_run check decision-coverage-plan --context <path> [<phase-dir>]
*
* `--context <path>` follows the sibling flag convention (`check predicate`,
* #2008): `--flag value` pairs parsed by the shared partitionPredicateArgs
@@ -555,7 +555,7 @@ function cmdDecisionCoverageVerify(projectDir: string, args: string[], raw: bool
* block = frontend && hasFrontendEvidence && !hasUiSpec (#3312: gate fires when
* UI work is detected AND the repo has static frontend evidence but no spec exists)
*
* Invocable as: gsd_run check ui-plan-gate <phase>
* Invocable as: msd_run check ui-plan-gate <phase>
*
* Uses checkUiPresence from ui-safety-gate.cjs — does NOT reimplement frontend detection.
* Uses getRoadmapPhaseWithFallback + findPhaseInternal from leaf modules for phase data.
@@ -705,8 +705,8 @@ function cmdUiPlanGate(projectDir: string, args: string[], raw: boolean): void {
* block = frontend && hasUiFiles && !hasUiSpec
*
* Args: check ui-safety-gate <phase>
* Invocable as: gsd_run check ui-safety-gate <phase>
* or gsd_run check ui.safety-gate <phase> (dots normalized to hyphens)
* Invocable as: msd_run check ui-safety-gate <phase>
* or msd_run check ui.safety-gate <phase> (dots normalized to hyphens)
*
* Uses checkUiPresence from ui-safety-gate.cjs — does NOT reimplement frontend detection.
* Checks whether any files changed in recent git history match frontend file patterns.
@@ -809,7 +809,7 @@ function computeUiSafetyGate(projectDir: string, phase: string): {
if (block) {
result.message = `UI files changed in this wave but no UI-SPEC.md exists for Phase ${phase}. ` +
`Run /gsd:ui-phase ${phase} to generate the design contract before continuing.`;
`Run /msd:ui-phase ${phase} to generate the design contract before continuing.`;
}
if (phaseLookupFailed) result.phaseLookupFailed = true;
return result;
@@ -831,7 +831,7 @@ function cmdUiSafetyGate(projectDir: string, args: string[], raw: boolean): void
* tdd-review-checkpoint: end-of-phase advisory check that scans type:tdd plans
* for RED/GREEN/REFACTOR gate-sequence compliance and surfaces a review table.
*
* Logic from gsd-core/references/tdd.md <end_of_phase_review> and
* Logic from msd-core/references/tdd.md <end_of_phase_review> and
* execute-phase.md <step name="tdd_review_checkpoint"> (now removed).
*
* Returns JSON:
@@ -1116,7 +1116,7 @@ function resolvePhaseDirOrEmpty(projectDir: string, phase: string): string {
* see verify-command-grounding.cjs for the recognizer contract.
*
* Args: check verify-command-paths <phase>
* Invocable as: gsd_run check verify-command-paths <phase>
* Invocable as: msd_run check verify-command-paths <phase>
*
* When the phase cannot be resolved to a directory, this emits a non-throwing
* degraded JSON payload (status/commands/counts all zeroed, `readError`
@@ -1169,7 +1169,7 @@ function cmdVerifyCommandPaths(projectDir: string, args: string[], raw: boolean)
* direction — see verify-command-grounding.cjs for the recognizer contract.
*
* Args: check verify-failure-directions <phase>
* Invocable as: gsd_run check verify-failure-directions <phase>
* Invocable as: msd_run check verify-failure-directions <phase>
*
* When the phase cannot be resolved to a directory, this emits a non-throwing
* degraded JSON payload (status/commands/counts all zeroed, `readError`
@@ -1226,7 +1226,7 @@ function cmdVerifyFailureDirections(projectDir: string, args: string[], raw: boo
* This gate is always advisory (passed: true) — it never blocks phase advancement.
*
* Args: check gap-analysis.plan-post <phase-dir> [phase-req-ids]
* Invocable as: gsd_run check gap-analysis.plan-post <phase-dir> [phase-req-ids]
* Invocable as: msd_run check gap-analysis.plan-post <phase-dir> [phase-req-ids]
*/
function cmdGapAnalysisPlanPost(projectDir: string, args: string[], raw: boolean): void {
// args[0] = 'check', args[1] = 'gap-analysis-plan-post' (normalized), args[2] = phaseDir, args[3] = phaseReqIds
@@ -1383,7 +1383,7 @@ function parsePredicateFlags(args: string[]): Record<string, string> {
* routed per the gate's `onError`.
*
* Invocation:
* gsd_run check predicate --predicate '<json>' \
* msd_run check predicate --predicate '<json>' \
* [--phase-dir <dir>] [--phase-number <n>] [--phase-req-ids <ids>] --raw
*
* The subprocess runs at the runtime project root (the `cwd` passed to this
@@ -1472,7 +1472,7 @@ function cmdApiCoverageVerifyPre(projectDir: string, args: string[], raw: boolea
// A token like ".." or "." carries no phase identity → unresolvable.
if (token === '.' || token === '..') token = '';
// Not a GSD project (no phases tree at all) → fail-open: nothing to gate.
// Not a MSD project (no phases tree at all) → fail-open: nothing to gate.
if (!fs.existsSync(phasesRoot)) {
output(
{
@@ -1480,7 +1480,7 @@ function cmdApiCoverageVerifyPre(projectDir: string, args: string[], raw: boolea
passed: true,
coverage_present: false,
detected: false,
message: 'api-coverage: no .planning/phases directory; gate skipped (not a GSD project layout)',
message: 'api-coverage: no .planning/phases directory; gate skipped (not a MSD project layout)',
},
raw,
undefined,
@@ -1503,7 +1503,7 @@ function cmdApiCoverageVerifyPre(projectDir: string, args: string[], raw: boolea
// The phases tree EXISTS but THIS phase could not be resolved. For a
// BLOCKING gate, fail-closed: a missing phase dir must not silently bypass
// the coverage requirement. (Distinguished from "no .planning at all"
// above, which is a genuine non-GSD-project → pass.)
// above, which is a genuine non-MSD-project → pass.)
output(
{
block: true,
@@ -1835,9 +1835,9 @@ function readPhaseScope(projectDir: string, phaseDir: string, phaseNumber: strin
function routeCheckCommand({ args, cwd, raw }: RouteCheckCommandOptions): void {
// Normalize dots to hyphens in the subcommand so both forms are accepted.
// This makes `check.query = "ui.plan-gate"` (dotted form in capability.json gates)
// directly runnable as `gsd_run check ui.plan-gate` — the dot is normalized to
// directly runnable as `msd_run check ui.plan-gate` — the dot is normalized to
// `ui-plan-gate` before routing. The generic gate-dispatch in §5.6 reads
// `check.query` from the active gate hook and runs `gsd_run check ${hook.check.query}`,
// `check.query` from the active gate hook and runs `msd_run check ${hook.check.query}`,
// so the declared query must be dispatchable exactly as declared.
const rawSubcommand = args[1];
const subcommand = typeof rawSubcommand === 'string' ? rawSubcommand.replace(/\./g, '-') : rawSubcommand;
@@ -1896,9 +1896,9 @@ function routeCheckCommand({ args, cwd, raw }: RouteCheckCommandOptions): void {
if (subcommand === 'verify-schema-drift') {
// Delegates to verify.schema-drift — drift capability gate at execute:wave:post (blocking).
// Dot-to-hyphen normalization means query "verify.schema-drift" routes here.
// Honor GSD_SKIP_SCHEMA_CHECK=true to bypass the gate (preserves the original inline gate behavior).
// Honor MSD_SKIP_SCHEMA_CHECK=true to bypass the gate (preserves the original inline gate behavior).
const phaseArg = typeof args[2] === 'string' ? args[2] : '';
const skipSchemaCheck = process.env['GSD_SKIP_SCHEMA_CHECK'] === 'true';
const skipSchemaCheck = process.env['MSD_SKIP_SCHEMA_CHECK'] === 'true';
cmdVerifySchemaDrift(cwd, phaseArg, skipSchemaCheck, raw);
return;
}
@@ -1930,7 +1930,7 @@ function routeCheckCommand({ args, cwd, raw }: RouteCheckCommandOptions): void {
// The deterministic test-tier prohibition PRODUCER/gate (#1259, ADR-550 D5d). Locates the
// wired mechanical check (node-test or lint-rule), confirms fail-first, runs it, builds
// enforcementEvidence, and emits the dispositionForProhibition verdict. Invocable as
// `gsd_run check prohibition-enforcement <request.json>`.
// `msd_run check prohibition-enforcement <request.json>`.
routeProhibitionEnforcement(args, raw);
return;
}

View File

@@ -1,7 +1,7 @@
/**
* CJS Command Router Adapter Module
*
* Compatibility routing for gsd-tools.cjs command families. Uses generated
* Compatibility routing for msd-tools.cjs command families. Uses generated
* command metadata for availability and small family-local argument shapers for
* CJS handler calls.
*
@@ -14,7 +14,7 @@
import commandRoutingHub = require('./command-routing-hub.cjs');
const { createHub, ERROR_KINDS } = commandRoutingHub;
// #2620 (ADR-0174 §6): the Hub defaults to a no-op logger and the live CLI
// dispatch path never injected the reference DispatchLogger, so GSD_AUDIT and
// dispatch path never injected the reference DispatchLogger, so MSD_AUDIT and
// config.audit.enabled were inert. Inject the reference logger ONLY when
// observability is opt-in enabled; when off, inject nothing so the Hub stays
// byte-for-byte silent (preserving the default dispatch output contract, incl.
@@ -162,7 +162,7 @@ function routeHubCommandFamily({
if (result.ok) return;
if (result.kind === ERROR_KINDS.UnknownCommand) {
// Phase 2 (#1646): pass SDK_UNKNOWN_COMMAND as the second arg so the
// JSON-error envelope (GSD_JSON_ERRORS=1) preserves the typed reason
// JSON-error envelope (MSD_JSON_ERRORS=1) preserves the typed reason
// for downstream consumers. Additive for host routers (their existing
// one-arg `error` callbacks ignore the second arg); required for
// capability routers whose tests assert on `reason === 'sdk_unknown_command'`.
@@ -172,7 +172,7 @@ function routeHubCommandFamily({
if (result.kind === ERROR_KINDS.InvalidArgs) {
// Amendment #1642 (#1644): when the handler provided exitReason, pass it
// as the second arg to error() so the JSON-error envelope
// (GSD_JSON_ERRORS=1) preserves the typed ERROR_REASON value for
// (MSD_JSON_ERRORS=1) preserves the typed ERROR_REASON value for
// downstream consumers. When exitReason is absent, call error(msg) with
// exactly one arg — byte-identical with prior behavior.
const invalidArgs = result as { reason: string; exitReason?: string };

View File

@@ -1,6 +1,6 @@
/**
* Claude orchestration command router — CLI dispatcher for
* `gsd-tools claude-orchestration <subcommand>`.
* `msd-tools claude-orchestration <subcommand>`.
*
* #1143 — thin CLI adapter over the pure `claude-orchestration.cjs` module.
* Lets execute-phase (or any orchestrator) invoke the Workflow-backend
@@ -15,8 +15,8 @@
* detect-backend [--runtime <id>] [--agent-sdk-version <ver>] [--no-nested-dispatch]
* Resolves whether the Workflow backend should activate. Both flags are
* OPTIONAL (#2590): `--runtime` falls back to the canonical
* `GSD_RUNTIME > config.runtime > 'claude'` chain, and
* `--agent-sdk-version` to `GSD_AGENT_SDK_VERSION` then the installed
* `MSD_RUNTIME > config.runtime > 'claude'` chain, and
* `--agent-sdk-version` to `MSD_AGENT_SDK_VERSION` then the installed
* @anthropic-ai/claude-agent-sdk version. Reads the
* `claude_orchestration.*` keys from .planning/config.json. Emits
* { available, backend, reason }.
@@ -67,7 +67,7 @@ interface RouterOpts {
function usage(error: (msg: string, reason?: string) => void): void {
error(
'Usage: gsd-tools claude-orchestration <detect-backend|emit-workflow|resolve-wave-dispatch> [...]\n' +
'Usage: msd-tools claude-orchestration <detect-backend|emit-workflow|resolve-wave-dispatch> [...]\n' +
' detect-backend [--runtime <id>] [--agent-sdk-version <ver>] [--no-nested-dispatch]\n' +
' emit-workflow --waves <path> --run-id <id> [--phase-dir <dir>] [--budget <n>] [--executor-model <id>]\n' +
' resolve-wave-dispatch --waves <path> --run-id <id> [--runtime <id>] [--agent-sdk-version <ver>] [--no-nested-dispatch] [--phase-dir <dir>] [--budget <n>] [--executor-model <id>]',
@@ -150,8 +150,8 @@ function resolveInstalledAgentSdkVersion(cwd: string): string | undefined {
* and `resolve-wave-dispatch` pass to the pure detection seam.
*/
function resolveDetectionArgs(args: string[], cwd?: string): { runtimeId: string; hostIntegration: { dispatch: { nested: boolean; background: boolean } }; agentSdkVersion: string | undefined } {
// #2590: the old fallback chain was `--runtime > GSD_RUNTIME > 'unknown'`,
// diverging from the canonical `GSD_RUNTIME > config.runtime > 'claude'` used
// #2590: the old fallback chain was `--runtime > MSD_RUNTIME > 'unknown'`,
// diverging from the canonical `MSD_RUNTIME > config.runtime > 'claude'` used
// by runtime-slash.resolveRuntime — so ANY manual invocation without
// --runtime reported `runtime_not_claude` on a perfectly ordinary Claude
// project. Delegate to the canonical resolver instead of re-deriving it.
@@ -159,7 +159,7 @@ function resolveDetectionArgs(args: string[], cwd?: string): { runtimeId: string
// Explicit flag wins (lets a caller pin a version); then the environment;
// then the actually-installed SDK.
const agentSdkVersion = argValue(args, '--agent-sdk-version')
|| process.env['GSD_AGENT_SDK_VERSION']
|| process.env['MSD_AGENT_SDK_VERSION']
|| resolveInstalledAgentSdkVersion(cwd || process.cwd());
const noNested = args.includes('--no-nested-dispatch');
const hostIntegration = noNested ? { dispatch: { nested: false, background: true } } : CAPABLE_HOST;
@@ -167,7 +167,7 @@ function resolveDetectionArgs(args: string[], cwd?: string): { runtimeId: string
}
/**
* #2686 — resolve the `gsd-executor` model this dispatch should carry.
* #2686 — resolve the `msd-executor` model this dispatch should carry.
*
* Defaults from the project config rather than requiring a flag. The Workflow
* backend previously emitted no model at all, so `model_overrides` /
@@ -184,7 +184,7 @@ function resolveExecutorModel(args: string[], cwd: string): string | undefined {
const pinned = argValue(args, '--executor-model');
if (pinned !== undefined) return pinned;
try {
const resolved = modelResolver.resolveModelInternal(cwd, 'gsd-executor') as unknown;
const resolved = modelResolver.resolveModelInternal(cwd, 'msd-executor') as unknown;
return typeof resolved === 'string' ? resolved : undefined;
} catch {
return undefined;

View File

@@ -2,7 +2,7 @@
* Claude Orchestration Capability — Workflow-tool backend detection + emitter
*
* #1143 — adopts Claude Code's Workflow tool (the engine behind `/effort ultracode`)
* as an optional, runtime-gated parallel-execution backend for the GSD loop.
* as an optional, runtime-gated parallel-execution backend for the MSD loop.
*
* This module is the pure, testable core of the capability. It owns two seams:
*
@@ -13,9 +13,9 @@
*
* emitWorkflowScript({ phaseDir, waves, runId, budgetTokens? })
* → { ok:true, script, summary } | { ok:false, reason }
* Maps GSD's wave/plan model 1:1 onto Workflow primitives:
* Maps MSD's wave/plan model 1:1 onto Workflow primitives:
* wave → sequential `parallel()` stage barriers,
* plan → `agent(brief, { agentType:'gsd-executor', isolation:'worktree' })`
* plan → `agent(brief, { agentType:'msd-executor', isolation:'worktree' })`
* — UNLESS the plan's `use_worktree` is explicitly `false`, in which case
* `isolation` is omitted entirely for that plan (#2772 / #2285 finding 1:
* a submodule-touching plan must never be forced into worktree isolation
@@ -24,7 +24,7 @@
* (the same overlap rule execute-phase already applies inline),
* resumeFromRunId → wired to the phase run id,
* budgetTokens → a shared token pool.
* The emitted script composes the SAME gsd-executor agent the inline path
* The emitted script composes the SAME msd-executor agent the inline path
* uses, with per-plan worktree isolation mirroring the inline path's own
* per-plan decision, so it produces the same artifacts/commits (criterion 2).
* It is a generated string consumed by the orchestrator; this module never
@@ -32,7 +32,7 @@
*
* Design laws:
* - Gall's Law: ship a small working slice that composes existing primitives
* (gsd-executor + worktree isolation) rather than reinventing them.
* (msd-executor + worktree isolation) rather than reinventing them.
* - Greenspun's Tenth Rule (cited in #1143): adopt the Workflow tool's
* barrier/pipeline/budget/resume semantics instead of hand-rolling them.
* - Postel's Law: liberal in input (missing fields → inline), conservative in
@@ -288,7 +288,7 @@ interface EmitInput {
runId: string;
budgetTokens?: number;
/**
* #2686 — the resolved `gsd-executor` model, threaded so this backend honors
* #2686 — the resolved `msd-executor` model, threaded so this backend honors
* model_overrides / model_policy / model_profile exactly as the inline path
* does. Omitted from the emitted options when it is `"inherit"`, empty, or not
* a string (#2517: an empty model 404s on runtimes without native tier
@@ -391,7 +391,7 @@ function emittableModel(executorModel: unknown): string | undefined {
* Workflow backend; it must never diverge from the inline path's per-plan gate.
*/
function agentOptions(p: Plan, executorModel?: unknown): string {
const parts = ['agentType: "gsd-executor"'];
const parts = ['agentType: "msd-executor"'];
if (p.use_worktree !== false) parts.push('isolation: "worktree"');
// #2686: carry the resolved executor model so this backend honors
// model_overrides / model_policy / model_profile exactly as the inline path.
@@ -511,8 +511,8 @@ function emitWorkflowScript(input: EmitInput | null | undefined): EmitOk | EmitE
// template interpolation), and its titles are matched EXACTLY against the
// phase() calls emitted below.
lines.push('export const meta = {');
lines.push(' name: ' + quoteString('gsd-execute-' + runId) + ',');
lines.push(' description: ' + quoteString('GSD wave dispatch for ' + phaseDir) + ',');
lines.push(' name: ' + quoteString('msd-execute-' + runId) + ',');
lines.push(' description: ' + quoteString('MSD wave dispatch for ' + phaseDir) + ',');
lines.push(' phases: [');
for (const w of waves) {
lines.push(' { title: ' + quoteString('Wave ' + w.id) + ', detail: '
@@ -521,10 +521,10 @@ function emitWorkflowScript(input: EmitInput | null | undefined): EmitOk | EmitE
lines.push(' ],');
lines.push('}');
lines.push('');
lines.push('// GSD Workflow script — generated by the claude-orchestration capability (#1143)');
lines.push('// MSD Workflow script — generated by the claude-orchestration capability (#1143)');
lines.push('// phase: ' + phaseDir);
lines.push('// BETA: preview-grade; on any failure the orchestrator falls back to inline dispatch.');
lines.push('// Composes the SAME gsd-executor agent as the inline path, so artifacts (SUMMARY.md)');
lines.push('// Composes the SAME msd-executor agent as the inline path, so artifacts (SUMMARY.md)');
lines.push('// and commits are produced identically. Worktree isolation is per-plan (use_worktree)');
lines.push('// and mirrors execute-phase.md step 2.5\'s submodule gate exactly (#2772 / #2285).');
// #2686 / ADR-1411: state which model was applied — or that none resolved —
@@ -541,7 +541,7 @@ function emitWorkflowScript(input: EmitInput | null | undefined): EmitOk | EmitE
// interpolating here safe.
const provenanceModel = emittableModel(executorModel);
if (provenanceModel !== undefined) {
lines.push('// model: ' + quoteString(provenanceModel) + ' (resolved for gsd-executor, same source as the inline path)');
lines.push('// model: ' + quoteString(provenanceModel) + ' (resolved for msd-executor, same source as the inline path)');
} else {
lines.push('// model: none applied — resolved to "inherit"/empty, so each agent inherits the');
lines.push('// orchestrator model (#2517: emitting an empty model 404s on some runtimes).');
@@ -564,7 +564,7 @@ function emitWorkflowScript(input: EmitInput | null | undefined): EmitOk | EmitE
// #3302: the generated script must hand the per-agent executor results back
// to the orchestrator so it can feed the wave merge chain. Emitted right
// after the header comments (meta stays the first statement): a helper that
// extracts the executor's <worktree_metadata> JSON (agents/gsd-executor.md
// extracts the executor's <worktree_metadata> JSON (agents/msd-executor.md
// <worktree_metadata_capture>) from one agent() result, plus the outcomes
// accumulator the stage barriers below push into. `metadata` is null when
// the result carried no parseable block — the LOUD-failure input the
@@ -573,7 +573,7 @@ function emitWorkflowScript(input: EmitInput | null | undefined): EmitOk | EmitE
lines.push('// orchestrator can record it into WAVE_WORKTREE_MANIFEST after the run');
lines.push('// (worktree.record-agent -> worktree.cleanup-wave, the same manifest-scoped');
lines.push('// merge chain inline dispatch feeds). null = absent/unparseable/interrupted.');
lines.push('function gsdWorktreeMetadata(agentResult) {');
lines.push('function msdWorktreeMetadata(agentResult) {');
lines.push(' if (typeof agentResult !== \'string\') return null;');
lines.push(' const m = agentResult.match(/<worktree_metadata>([\\s\\S]*?)<\\/worktree_metadata>/);');
lines.push(' if (m === null) return null;');
@@ -584,7 +584,7 @@ function emitWorkflowScript(input: EmitInput | null | undefined): EmitOk | EmitE
lines.push(' return null;');
lines.push(' }');
lines.push('}');
lines.push('const gsdAgentOutcomes = [];');
lines.push('const msdAgentOutcomes = [];');
lines.push('');
const stagesByWave: string[][][] = [];
@@ -618,7 +618,7 @@ function emitWorkflowScript(input: EmitInput | null | undefined): EmitOk | EmitE
// order — the documented parallel() contract) so each plan's outcome can
// be tagged and returned below. Discarding them stranded every
// worktree-wf_* branch: the merge chain had no input (#3302).
lines.push('const gsdStage_' + wi + '_' + si + ' = await parallel([');
lines.push('const msdStage_' + wi + '_' + si + ' = await parallel([');
for (const p of stagePlans) {
lines.push(' () => agent(' + quoteString(p.brief) + ', ' + agentOptions(p, executorModel) + '),');
}
@@ -627,11 +627,11 @@ function emitWorkflowScript(input: EmitInput | null | undefined): EmitOk | EmitE
// so attribution survives out-of-order completion and needs no runtime
// introspection. expects_worktree mirrors agentOptions' own per-plan
// decision (use_worktree !== false).
lines.push('gsdAgentOutcomes.push(');
lines.push('msdAgentOutcomes.push(');
for (let pi = 0; pi < stagePlans.length; pi++) {
const p = stagePlans[pi];
const tail = pi < stagePlans.length - 1 ? ',' : '';
lines.push(' { plan: ' + quoteString(p.id) + ', expects_worktree: ' + (p.use_worktree !== false) + ', metadata: gsdWorktreeMetadata(gsdStage_' + wi + '_' + si + '[' + pi + ']) }' + tail);
lines.push(' { plan: ' + quoteString(p.id) + ', expects_worktree: ' + (p.use_worktree !== false) + ', metadata: msdWorktreeMetadata(msdStage_' + wi + '_' + si + '[' + pi + ']) }' + tail);
}
lines.push(')');
}
@@ -641,10 +641,10 @@ function emitWorkflowScript(input: EmitInput | null | undefined): EmitOk | EmitE
// #3302: the script's top-level return value is what the Workflow tool hands
// back to the orchestrator. One { plan, expects_worktree, metadata } entry
// per dispatched plan; the orchestrator records every worktree entry via
// `gsd_run query worktree.record-agent` and HALTS on a null metadata entry
// `msd_run query worktree.record-agent` and HALTS on a null metadata entry
// for an expects_worktree plan (see the execute:wave:pre fragment) — a
// silently-empty manifest is the exact #3302 failure mode.
lines.push('return gsdAgentOutcomes');
lines.push('return msdAgentOutcomes');
const script = lines.join('\n');

View File

@@ -3,7 +3,7 @@
* json-error-mode and contract-version cells.
*
* Must import nothing but `node:fs` and `./exit-code-registry.cjs` — this
* source is emitted to TWO locations, gsd-core/bin/lib/cli-exit.cjs (tsc
* source is emitted to TWO locations, msd-core/bin/lib/cli-exit.cjs (tsc
* build output) and scripts/lib/cli-exit.cjs (a generated, committed
* artifact regenerated by scripts/gen-scripts-cli-exit.cjs), and the latter
* must load on an unbuilt clone before anything under ./lib exists. The
@@ -35,19 +35,19 @@ const EXIT_ENVELOPE_REASON = 'sdk_fail_fast' as const;
/**
* Process-level flag: when true, error paths emit structured JSON to stderr
* instead of plain text. Set by gsd-tools.cjs when the CLI is invoked with
* instead of plain text. Set by msd-tools.cjs when the CLI is invoked with
* `--json-errors`; re-exported by io.cts, which is where most callers reach it.
*
* Held in a Symbol-keyed cell on globalThis rather than in module scope, and
* that is load-bearing: this module is emitted to TWO locations
* (gsd-core/bin/lib/cli-exit.cjs and the generated scripts/lib/cli-exit.cjs),
* (msd-core/bin/lib/cli-exit.cjs and the generated scripts/lib/cli-exit.cjs),
* so a process that loads both would get two independent module instances. A
* module-level `let` would give them two independent flags — one copy could
* think json mode is on while the other thought it was off, which is exactly
* the divergence class ADR-3889 exists to remove. One cell, keyed by a
* registry Symbol, makes that unrepresentable.
*/
const JSON_ERROR_MODE_KEY = Symbol.for('gsd.exit.jsonErrorMode');
const JSON_ERROR_MODE_KEY = Symbol.for('msd.exit.jsonErrorMode');
function setJsonErrorMode(v: unknown): void {
(globalThis as unknown as Record<symbol, boolean>)[JSON_ERROR_MODE_KEY] = !!v;
@@ -73,7 +73,7 @@ type ContractVersion = 'v1' | 'v2';
* `resolveContractVersion` is the only writer; `terminateNow`/`runMain`
* read it internally when projecting a declared outcome.
*/
const CONTRACT_VERSION_KEY = Symbol.for('gsd.exit.contractVersion');
const CONTRACT_VERSION_KEY = Symbol.for('msd.exit.contractVersion');
function setContractVersion(v: ContractVersion): void {
(globalThis as unknown as Record<symbol, ContractVersion>)[CONTRACT_VERSION_KEY] = v;
@@ -83,12 +83,12 @@ function setContractVersion(v: ContractVersion): void {
* Resolve the active exit-contract version, wiring the ambient process to the
* two terminators (ADR-3889 §4/§3). Mirrors how JSON_ERROR_MODE_KEY already
* works: a process-global cell means no entrypoint needs per-call wiring, so
* a `scripts/` tool or a hook gets the same behaviour as `gsd-tools` without
* this module touching either (P8 owns `gsd-tools`; P7 owns hooks).
* a `scripts/` tool or a hook gets the same behaviour as `msd-tools` without
* this module touching either (P8 owns `msd-tools`; P7 owns hooks).
*
* Precedence: if the cell already holds an explicit version, that wins —
* this is what lets `setContractVersion` override the ambient process (a
* later `GSD_EXIT_CONTRACT=v2` in the same process must NOT unseat an
* later `MSD_EXIT_CONTRACT=v2` in the same process must NOT unseat an
* explicit `setContractVersion('v1')` call). Otherwise resolve from argv/env
* via `resolveContractVersion`, which itself persists the result into the
* cell — so this is a one-time resolution per process; every later read is
@@ -169,7 +169,7 @@ function projectOutcome(outcome: unknown, version: unknown): number {
* `main()` returning void would inherit a stale DEGRADED from an unrelated,
* earlier call — this is the leak #3912 review found and fixed.
*/
const PENDING_OUTCOME_KEY = Symbol.for('gsd.exit.pendingOutcome');
const PENDING_OUTCOME_KEY = Symbol.for('msd.exit.pendingOutcome');
function setPendingOutcome(v: unknown): void {
(globalThis as unknown as Record<symbol, unknown>)[PENDING_OUTCOME_KEY] = v;
@@ -198,10 +198,10 @@ function findExitContractFlag(argv: readonly string[]): string | undefined {
* against it without re-parsing argv/env itself.
*
* Precedence: an explicit `--exit-contract=<v>` flag BEATS
* `GSD_EXIT_CONTRACT`, in both directions (flag=v1 + env=v2 -> v1; flag=v2 +
* `MSD_EXIT_CONTRACT`, in both directions (flag=v1 + env=v2 -> v1; flag=v2 +
* env=v1 -> v2). Neither present -> 'v1' (the documented default). An empty
* env var reads as UNSET, not as an explicit empty selection — a shell that
* exports `GSD_EXIT_CONTRACT=` with nothing after the `=` must not silently
* exports `MSD_EXIT_CONTRACT=` with nothing after the `=` must not silently
* select a version.
*
* Casing is decided, not accidental: only the exact lowercase tokens `v1`/
@@ -217,7 +217,7 @@ function resolveContractVersion(opts: { argv?: readonly string[]; env?: NodeJS.P
const env = opts.env ?? process.env;
const flagValue = findExitContractFlag(argv);
const rawEnvValue = env.GSD_EXIT_CONTRACT;
const rawEnvValue = env.MSD_EXIT_CONTRACT;
const envValue = rawEnvValue === undefined || rawEnvValue === '' ? undefined : rawEnvValue;
const selected = flagValue !== undefined ? flagValue : envValue;
@@ -405,7 +405,7 @@ function runMain(main: () => number | string | void | Promise<number | string |
* fd 2 gets the SAME serialized `payload` fd 1 got — unchanged behavior.
* When provided, fd 2 gets THIS instead: a string is written raw
* (verbatim, not JSON-stringified), anything else is JSON-stringified
* like `payload`. This exists because `hooks/gsd-write-guard.js`'s
* like `payload`. This exists because `hooks/msd-write-guard.js`'s
* emitBlock does NOT write the same bytes to both streams today — it
* writes the full JSON `output` to stdout but only the plain-text
* `output.reason` STRING to stderr, because Kimi's native hook bus reads
@@ -459,7 +459,7 @@ function terminateNow(outcome: string, payload: unknown, stderrPayload?: unknown
);
}
// m2 (round 5, hooks/gsd-write-guard.js:159-175): emission must itself be
// m2 (round 5, hooks/msd-write-guard.js:159-175): emission must itself be
// exception-safe. A failed write (EPIPE, a full pipe buffer, a throwing
// fs.writeSync in a test) must NOT change the exit code — if it propagated
// out of this function, a caller whose payload could not be delivered
@@ -526,7 +526,7 @@ function terminateNow(outcome: string, payload: unknown, stderrPayload?: unknown
// directive is needed here for the one sanctioned process.exit call site.
process.exit(projected);
} catch (err) {
// Anything above threw: an unrecognized --exit-contract/GSD_EXIT_CONTRACT
// Anything above threw: an unrecognized --exit-contract/MSD_EXIT_CONTRACT
// value, a non-string/empty/unregistered `outcome`, or the HOOK_DENY
// collision guard. Diagnose on stderr — swallowing this silently would
// make a typo'd outcome name or a bad contract-version env var

View File

@@ -3,12 +3,12 @@
/**
* cli-skew-check.cts — CLI version-skew detection (#1754).
*
* Pure function: compares the resolved gsd-tools.cjs path to the project root.
* Pure function: compares the resolved msd-tools.cjs path to the project root.
* If the resolved CLI is OUTSIDE the project root while a project-local install
* EXISTS, returns a warning string (the caller writes it to stderr). Non-blocking.
*
* Catches the shadowing scenario from #1748: a stale global canary CLI (e.g.
* from the retired @gsd-build/sdk) shadowing the project-local GSD install.
* from the retired @gsd-build/sdk) shadowing the project-local MSD install.
*
* The function is PURE (no I/O) — the caller provides the resolved path, the
* project root, and whether a project-local install exists. This makes it
@@ -20,9 +20,9 @@ import path from 'node:path';
/**
* Check for CLI version skew.
*
* @param opts.resolvedPath - The absolute path of the running gsd-tools.cjs (__filename).
* @param opts.resolvedPath - The absolute path of the running msd-tools.cjs (__filename).
* @param opts.projectRoot - The project root (from findProjectRoot), or null if no project.
* @param opts.projectLocalExists - Whether a project-local gsd-tools.cjs exists.
* @param opts.projectLocalExists - Whether a project-local msd-tools.cjs exists.
* @returns A warning string if skew is detected, or null if no skew.
*/
export function checkCliSkew(opts: {
@@ -43,5 +43,5 @@ export function checkCliSkew(opts: {
const hint = resolvedPath.includes('@gsd-build')
? ' If @gsd-build/sdk: npm uninstall -g @gsd-build/sdk'
: '';
return `⚠ GSD: ${resolvedPath} may shadow project-local GSD.${hint}`;
return `⚠ MSD: ${resolvedPath} may shadow project-local MSD.${hint}`;
}

View File

@@ -31,18 +31,18 @@ export interface Clock {
const _realSleepBuf = new Int32Array(new SharedArrayBuffer(4));
/**
* Parse GSD_NOW_MS to a valid pinned epoch millisecond value, or return null.
* Parse MSD_NOW_MS to a valid pinned epoch millisecond value, or return null.
*
* Accepts ONLY strict decimal integer strings within JS Date bounds
* (abs(ms) <= 8.64e15). Rejects empty strings, whitespace-only, floats
* ('12.5'), scientific notation ('1e30'), and out-of-range values.
*
* Returns null (fall back to Date.now()) for any invalid or absent input.
* Returns null when GSD_TEST_MODE is not set.
* Returns null when MSD_TEST_MODE is not set.
*/
function _pinnedNowMs(): number | null {
if (!process.env.GSD_TEST_MODE) return null;
const raw = process.env.GSD_NOW_MS;
if (!process.env.MSD_TEST_MODE) return null;
const raw = process.env.MSD_NOW_MS;
if (typeof raw !== 'string') return null;
const t = raw.trim();
if (!/^-?\d+$/.test(t)) return null; // reject '', 'abc', '1e30', '12.5'
@@ -55,7 +55,7 @@ export const realClock: Clock = {
/**
* Return current epoch milliseconds.
*
* When both GSD_TEST_MODE and GSD_NOW_MS are set (subprocess time-pin adapter,
* When both MSD_TEST_MODE and MSD_NOW_MS are set (subprocess time-pin adapter,
* issue #474), returns the pinned millisecond value so all date-stamping in the
* subprocess SUT is deterministic. In production, falls back to Date.now().
*
@@ -91,8 +91,8 @@ export const realClock: Clock = {
/**
* Return today's date as a YYYY-MM-DD string in the HOST-LOCAL calendar day.
* Uses this.now() so the subprocess time-pin adapter (GSD_NOW_MS) is honoured
* exactly as today()/nowIso() are — deterministic when GSD_NOW_MS and TZ are
* Uses this.now() so the subprocess time-pin adapter (MSD_NOW_MS) is honoured
* exactly as today()/nowIso() are — deterministic when MSD_NOW_MS and TZ are
* both pinned (#2136).
*
* Operator-facing date-only fields (last_activity, "completed <date>", etc.)

View File

@@ -4,7 +4,7 @@
* TypeScript source of truth). Behaviour is preserved byte-for-behaviour from
* the prior hand-written .cjs; only types are added.
*
* Each cluster is a named group of skill stems. Clusters are used by /gsd:surface
* Each cluster is a named group of skill stems. Clusters are used by /msd:surface
* to enable/disable a cohesive group of skills without reinstall.
*
* Cluster membership may overlap (a skill can live in two clusters). The union
@@ -12,7 +12,7 @@
* are flagged by surface-clusters.test.cjs.
*
* Source: docs/research/2026-05-12-skill-surface-budget.md §3.2 (verified
* against commands/gsd/ listing in surface-clusters.test.cjs).
* against commands/msd/ listing in surface-clusters.test.cjs).
*/
export type ClusterName =

View File

@@ -1,7 +1,7 @@
/**
* Path-scoped code-review depth resolution for #2554 (ADR-457 build-at-publish:
* a pure TypeScript source of truth compiled to
* gsd-core/bin/lib/code-review-depth.cjs).
* msd-core/bin/lib/code-review-depth.cjs).
*
* Zero I/O, zero clock, zero deps. Resolves the effective review depth
* (`quick` | `standard` | `deep`) from, in priority order: the `--depth=`
@@ -10,7 +10,7 @@
* config value, and finally the `standard` default — then applies the
* pre-existing large-scope `deep` → `standard` downgrade.
*
* See .gsd/phase/feat-2554-support-path-scoped-code-review-depth-ov/40-design.md
* See .msd/phase/feat-2554-support-path-scoped-code-review-depth-ov/40-design.md
* for the full behavior table and negative-space notes this module encodes.
*/
@@ -52,7 +52,7 @@ export const DEPTH_TIERS: readonly DepthTier[] = Object.freeze(['quick', 'standa
/**
* A `deep` resolution over more than this many changed files downgrades to
* `standard` (pre-existing scope guard; see gsd-core/workflows/code-review.md).
* `standard` (pre-existing scope guard; see msd-core/workflows/code-review.md).
*/
export const LARGE_SCOPE_THRESHOLD = 50;

View File

@@ -1,5 +1,5 @@
/**
* Typed flag parser for the /gsd:code-review command (ADR-457 build-at-publish:
* Typed flag parser for the /msd:code-review command (ADR-457 build-at-publish:
* the hand-written bin/lib/code-review-flags.cjs collapsed to a TypeScript
* source of truth). Behaviour is preserved byte-for-behaviour from the prior
* hand-written .cjs; only types are added.
@@ -30,7 +30,7 @@ export type CodeReviewWorkflow = 'code-review.md' | 'code-review-fix.md';
/**
* Parse code-review flags from an argv array. The first positional argument
* (phase number) is ignored — phase validation is handled by
* `gsd-tools query init.phase-op`. Unknown flags are silently ignored.
* `msd-tools query init.phase-op`. Unknown flags are silently ignored.
*/
export function parseCodeReviewFlags(argv: string[]): CodeReviewFlags {
const flags: CodeReviewFlags = {

View File

@@ -427,7 +427,7 @@ export function stripReasoningEffort(doc: CodexAgentDoc): CodexAgentDoc {
// lazily `require(bin/install.js)` to reach this derivation — but requiring
// `bin/install.js` runs its whole top-level script, including the ASCII
// banner print to stdout, which corrupted every stdout-JSON caller downstream
// of `checkCodexSandboxPosture` (`gsd-tools validate agents`). This module is
// of `checkCodexSandboxPosture` (`msd-tools validate agents`). This module is
// a genuine leaf with no top-level side effects, so both `bin/install.js` and
// `agent-install-check.cts` import the derivation from here instead — ONE
// owner, no second predicate (routing `src/` through `bin/install.js` was
@@ -458,7 +458,7 @@ export function stripReasoningEffort(doc: CodexAgentDoc): CodexAgentDoc {
* so a stale or orphaned entry fails loudly instead of being silently
* honored forever.
*
* `gsd-nyquist-auditor` is the 17th entry, added by the list-form `tools:`
* `msd-nyquist-auditor` is the 17th entry, added by the list-form `tools:`
* parse fix (#3897 follow-up): its `tools:` frontmatter uses YAML block-list
* form (`tools:` + indented `- Item` lines) and declares both Write and
* Edit, but the original `extractToolsLine`/`extractFrontmatterField`
@@ -523,7 +523,7 @@ function _codexToolsDeclareWriteOrEdit(toolsRaw: string): boolean {
// from BOTH edges of a candidate identity before it is compared against the
// hold set. A bare .trim() only strips the ASCII whitespace \s already
// covers; the hold bypass this closes (a trailing space, \n/\r, or NBSP
// after `gsd-doc-writer`) is exactly a value that survives .trim() untouched.
// after `msd-doc-writer`) is exactly a value that survives .trim() untouched.
// Spelled entirely as \u escapes (never a literal invisible codepoint in
// the source), matching this module's own BOM_CHAR convention above: NBSP
// (U+00A0), the zero-width space/non-joiner/joiner + LTR/RTL marks
@@ -554,7 +554,7 @@ const SANDBOX_IDENTITY_SUSPICIOUS_RE = /[^a-z0-9._-]/;
* never dodge the hold lookup by hiding behind a directory prefix.
* 2. trim ASCII whitespace AND the NBSP/zero-width/control-char class
* ({@link SANDBOX_IDENTITY_EDGE_RE}) from both ends.
* 3. strip trailing dots (`gsd-doc-writer.` collapses to the same key).
* 3. strip trailing dots (`msd-doc-writer.` collapses to the same key).
* 4. `String.prototype.normalize('NFKC')` — canonicalizes compatibility
* variants (e.g. fullwidth `g` U+FF47 folds to ASCII `g`) to their
* standard form so a widened-by-Unicode-lookalike name collapses onto
@@ -599,7 +599,7 @@ export function normalizeSandboxIdentity(raw: unknown): string | null {
* predicate, and it is deliberately not an attempt to enumerate Unicode
* confusables (Turkish dotted/dotless I, fullwidth forms, NFD combining
* marks, ...). Every one of the 35 shipped roster files is pure ASCII
* (`gsd-[a-z-]+`), so a non-ASCII-after-normalization identity cannot be a
* (`msd-[a-z-]+`), so a non-ASCII-after-normalization identity cannot be a
* legitimate shipped role — flagging it `suspicious` (which
* {@link deriveCodexSandboxMode} turns into `read-only`) has zero false
* positives against real content, and refuses to widen on anything this
@@ -646,7 +646,7 @@ export function isSandboxHeld(
// #3897 rung 4/list-form fix: handles BOTH shapes a `tools:` key can take —
// (a) inline: `tools: Read, Write, Edit` on the same line as the key, and
// (b) YAML block-list form: `tools:` alone, followed by indented `- Item`
// lines (`agents/gsd-nyquist-auditor.md`, `agents/gsd-security-auditor.md`
// lines (`agents/msd-nyquist-auditor.md`, `agents/msd-security-auditor.md`
// are the only two roster files using this shape today). The former
// implementation was a single `/^tools:\s*(.+)$/m` regex — `\s*` swallows the
// newline after a bare `tools:` key, so it silently matched into the FIRST
@@ -760,8 +760,8 @@ function _deriveCodexSandboxModeFromTools(agentName: string | string[] | null |
* document reader.
*
* `agentName` is one or more HOLD-LOOKUP IDENTITIES: pass the agent's
* canonical source filename stem (e.g. `gsd-doc-writer` for
* `agents/gsd-doc-writer.md`) as a plain string for the single-identity call
* canonical source filename stem (e.g. `msd-doc-writer` for
* `agents/msd-doc-writer.md`) as a plain string for the single-identity call
* sites this signature always supported, OR an array of every identity that
* can influence the emitted artifact (filename stem AND frontmatter `name:`)
* when the caller — like `bin/install.js`'s per-agent emit loop — has both

View File

@@ -5,7 +5,7 @@
*
* ADR-3473 §8.4 ("failure is a value"): `parseNamedArgs` is now strict and
* returns a `Result` instead of silently accepting unrecognized or stray
* positional tokens. See .gsd/phase/feat-3884-failure-is-a-value/40-design.md
* positional tokens. See .msd/phase/feat-3884-failure-is-a-value/40-design.md
* for the full behavior table (A1-A18) and negative-space section (N1-N8).
*
* Shared helpers for command-family adapters to project argv tokens into
@@ -33,10 +33,10 @@ export interface NamedArgSpec {
/**
* Optional-value flags (added for `--wave`, ADR-3473 Bucket-A correction —
* `--wave` was misclassified as Bucket C and is in fact a documented,
* shipped, user-facing form: commands/gsd/execute-phase.md:4,48 and
* gsd-core/workflows/execute-phase.md:84 extract the value from
* shipped, user-facing form: commands/msd/execute-phase.md:4,48 and
* msd-core/workflows/execute-phase.md:84 extract the value from
* `$ARGUMENTS` and forward it to the workflow, not to this CLI seam).
* #2932 (commands/gsd/execute-phase.md:53) records the flag's semantics as
* #2932 (commands/msd/execute-phase.md:53) records the flag's semantics as
* token-PRESENCE: `--wave N` is active only if the literal `--wave` token
* is present. This CLI layer therefore only needs to know the flag was
* *seen* — the value belongs to the workflow, not to `data`.
@@ -79,7 +79,7 @@ function isPlainObject(v: unknown): v is Record<string, unknown> {
}
/**
* ADR-3473 Decision 2: both ends of this seam are gsd-core's own source, so a
* ADR-3473 Decision 2: both ends of this seam are msd-core's own source, so a
* malformed spec (a stale call site still using the legacy
* `parseNamedArgs(args, valueFlags, booleanFlags)` shape, or a missing spec
* entirely) is an internal invariant violation, not user input — it throws

View File

@@ -3,17 +3,17 @@
/**
* Command Roster Module
*
* Read-only helper for discovering canonical commands/gsd command stems and
* applying the shared GSD slash-command namespace transform.
* Read-only helper for discovering canonical commands/msd command stems and
* applying the shared MSD slash-command namespace transform.
*/
import path from 'node:path';
import fs from 'node:fs';
// #2874 (ADR-58 cleanup phase): readGsdCommandNames is reached from
// #2874 (ADR-58 cleanup phase): readMsdCommandNames is reached from
// installRuntimeArtifacts's call tree (skillsKind's stage() closure reads it
// for cross-referencing), but COMMANDS_DIR (below) is resolved by walking up
// from THIS MODULE'S OWN compiled __dirname to locate the GSD PACKAGE'S OWN
// commands/gsd/ source tree — not anything under the install destination.
// from THIS MODULE'S OWN compiled __dirname to locate the MSD PACKAGE'S OWN
// commands/msd/ source tree — not anything under the install destination.
// This is the exact same "package's own source, not the destination" case
// findInstallSourceRoot/findAgentsSourceRoot document in
// runtime-artifact-layout.cts's Step 2 (see that comment and
@@ -42,12 +42,12 @@ const slashCommandTransformer = require('../../../scripts/fix-slash-commands.cjs
};
// Mirrors scripts/fix-slash-commands.cjs's own `COMMANDS_DIR` computation
// (`path.join(__dirname, '..', 'commands', 'gsd')` from repo-root/scripts) —
// (`path.join(__dirname, '..', 'commands', 'msd')` from repo-root/scripts) —
// same target directory, resolved from this module's own compiled location
// (repo-root/gsd-core/bin/lib) instead, so the walk-up depth differs.
const COMMANDS_DIR = path.join(__dirname, '..', '..', '..', 'commands', 'gsd');
// (repo-root/msd-core/bin/lib) instead, so the walk-up depth differs.
const COMMANDS_DIR = path.join(__dirname, '..', '..', '..', 'commands', 'msd');
function readGsdCommandNames(): string[] {
function readMsdCommandNames(): string[] {
try {
return fs.readdirSync(COMMANDS_DIR)
.filter((f: string) => f.endsWith('.md'))
@@ -61,7 +61,7 @@ function readGsdCommandNames(): string[] {
}
export = {
readGsdCommandNames,
readMsdCommandNames,
transformContentToHyphen: slashCommandTransformer.transformContentToHyphen,
transformContent: slashCommandTransformer.transformContent,
buildPattern: slashCommandTransformer.buildPattern,

View File

@@ -74,7 +74,7 @@ const { extractFrontmatter, agentScalarNeedsDoubleQuoting, escapeDoubleQuotedSca
// eslint-disable-next-line @typescript-eslint/no-require-imports
import modelProfiles = require('./model-profiles.cjs');
const { MODEL_PROFILES, VALID_PHASE_TYPES } = modelProfiles;
import { formatGsdSlash, resolveRuntime } from './runtime-slash.cjs';
import { formatMsdSlash, resolveRuntime } from './runtime-slash.cjs';
import { realClock } from './clock.cjs';
import { clampPercent, renderProgressBar } from './phase-lifecycle.cjs';
// eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -177,7 +177,7 @@ interface EffortSyncChange {
* `cmdStats` builds `phasesByNumber` by scanning on-disk phase directories.
* When two directories normalize to the same phase key (e.g. `05-real/` and
* `05-real-stray/`), the status field must be folded by precedence rather
* than overwritten last-write-wins — otherwise `/gsd-stats` reports whatever
* than overwritten last-write-wins — otherwise `/msd-stats` reports whatever
* directory `fs.readdirSync` happened to yield last, which is non-deterministic
* across platforms and can silently call a `Complete` phase `Not Started`.
*/
@@ -927,13 +927,13 @@ function removeEffortFrontmatter(content: string): string {
}
/**
* #488 — Re-sync effort: frontmatter in all installed gsd-*.md agent files to
* #488 — Re-sync effort: frontmatter in all installed msd-*.md agent files to
* match the current effort config, without requiring a full reinstall.
*
* Uses install-time resolution (readGsdEffectiveEffortConfig + resolveInstallTimeEffort
* Uses install-time resolution (readMsdEffectiveEffortConfig + resolveInstallTimeEffort
* from bin/install.js) rather than the runtime resolver (resolveEffortInternal), because
* the sync must mirror what install actually wrote: home defaults merged with project config.
* The runtime resolver (loadConfig) does not merge ~/.gsd/defaults.json when a project
* The runtime resolver (loadConfig) does not merge ~/.msd/defaults.json when a project
* .planning/config.json exists, so it would silently ignore home-level effort changes.
*/
function cmdEffortSync(cwd: string, raw: boolean, opts?: { dryRun?: boolean; configDir?: string; runtime?: string }): void {
@@ -968,16 +968,16 @@ function cmdEffortSync(cwd: string, raw: boolean, opts?: { dryRun?: boolean; con
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/unbound-method
const { getGlobalConfigDir } = require('./runtime-homes.cjs') as { getGlobalConfigDir(runtime: string, explicitDir?: string | null): string };
// Use install-time resolvers: they merge ~/.gsd/defaults.json with project config,
// Use install-time resolvers: they merge ~/.msd/defaults.json with project config,
// matching the exact logic used when agents were originally installed. #2071: these
// live in the shipped sibling install-effort-resolver.cjs (extracted from the
// package-root bin/install.js, which the installer never copies into a runtime home).
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/unbound-method
const { readGsdEffectiveEffortConfig, resolveInstallTimeEffort } = require('./install-effort-resolver.cjs') as {
readGsdEffectiveEffortConfig(cwd: string): Record<string, unknown> | null;
const { readMsdEffectiveEffortConfig, resolveInstallTimeEffort } = require('./install-effort-resolver.cjs') as {
readMsdEffectiveEffortConfig(cwd: string): Record<string, unknown> | null;
resolveInstallTimeEffort(cfg: Record<string, unknown> | null, agentName: string): string;
};
const effortCfg = readGsdEffectiveEffortConfig(cwd);
const effortCfg = readMsdEffectiveEffortConfig(cwd);
const agentsDir = path.join(opts.configDir || getGlobalConfigDir(runtime), 'agents');
@@ -988,7 +988,7 @@ function cmdEffortSync(cwd: string, raw: boolean, opts?: { dryRun?: boolean; con
// Skip symlinks — only write regular files to avoid clobbering symlink targets.
const files = fs.readdirSync(agentsDir).filter(f => {
if (!f.startsWith('gsd-') || !f.endsWith('.md')) return false;
if (!f.startsWith('msd-') || !f.endsWith('.md')) return false;
try { return fs.lstatSync(path.join(agentsDir, f)).isFile(); } catch { return false; }
}).sort(); // #3706: sorted like the codex and
// opencode branches — readdir order is platform-dependent, so leaving it unsorted makes the
@@ -1354,7 +1354,7 @@ function cmdEffortSyncCodex(raw: boolean, dryRun: boolean, configDir?: string):
/**
* #3706 — the OpenCode branch of `cmdEffortSync`. Maintains the `variant:`
* frontmatter key install now bakes into every `~/.config/opencode/agents/
* gsd-*.md` (or configDir-relative equivalent), mirroring exactly what
* msd-*.md` (or configDir-relative equivalent), mirroring exactly what
* install writes: a resolved universal effort clamped through
* `clampEffortForHost('opencode', ...)`. Null means the key must be ABSENT —
* #3533 (10d): an absent key is the correct state under `inherit`, and a
@@ -1378,20 +1378,20 @@ function cmdEffortSyncOpencode(cwd: string, raw: boolean, dryRun: boolean, confi
const files = fs
.readdirSync(agentsDir)
.filter(f => {
if (!f.startsWith('gsd-') || !f.endsWith('.md')) return false;
if (!f.startsWith('msd-') || !f.endsWith('.md')) return false;
try { return fs.lstatSync(path.join(agentsDir, f)).isFile(); } catch { return false; }
})
.sort();
// Use install-time resolvers: they merge ~/.gsd/defaults.json with project
// Use install-time resolvers: they merge ~/.msd/defaults.json with project
// config, matching the exact logic used when agents were originally
// installed. Resolved once, outside the loop, like the claude branch.
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/unbound-method
const { readGsdEffectiveEffortConfig, resolveInstallTimeEffort } = require('./install-effort-resolver.cjs') as {
readGsdEffectiveEffortConfig(cwd: string): Record<string, unknown> | null;
const { readMsdEffectiveEffortConfig, resolveInstallTimeEffort } = require('./install-effort-resolver.cjs') as {
readMsdEffectiveEffortConfig(cwd: string): Record<string, unknown> | null;
resolveInstallTimeEffort(cfg: Record<string, unknown> | null, agentName: string): string;
};
const effortCfg = readGsdEffectiveEffortConfig(cwd);
const effortCfg = readMsdEffectiveEffortConfig(cwd);
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/unbound-method
const { clampEffortForHost } = require('./model-catalog.cjs') as { clampEffortForHost(host: string, effort: string): string | null };
@@ -1513,7 +1513,7 @@ function cmdEffortSyncOpencode(cwd: string, raw: boolean, dryRun: boolean, confi
//
// Known limitation, deliberately not fixed here: `output()` only honors its
// third argument when `raw === true`, and this command's process always
// exits 0 regardless of the summary string — so `if gsd-tools effort sync;
// exits 0 regardless of the summary string — so `if msd-tools effort sync;
// then` reads success in a shell even on a run where every write failed.
// Making the exit code reflect failure would be a CLI-contract change
// affecting all three cmdEffortSync* branches (claude, codex, opencode) and
@@ -1657,7 +1657,7 @@ function resolveCommitDocsPolicy(
// rather than reusing `skipped_commit_docs_false` — telling a user "commit_docs
// is false" when their project setting is actually `true` would be actively
// misleading (design "Rejected" §3). `config` keeps the pre-existing string
// unchanged: `agents/gsd-executor.md` pattern-matches on it (D2).
// unchanged: `agents/msd-executor.md` pattern-matches on it (D2).
const COMMIT_DOCS_SKIP_REASON: Record<Exclude<CommitDocsSource, 'default'>, string> = {
phase: 'skipped_commit_docs_phase_false',
config: 'skipped_commit_docs_false',
@@ -3277,7 +3277,7 @@ async function cmdWebsearch(query: string | undefined, options: WebsearchOptions
params.set('freshness', options.freshness);
}
const rawTimeout = parseInt(process.env['GSD_WEBSEARCH_TIMEOUT_MS'] as string, 10);
const rawTimeout = parseInt(process.env['MSD_WEBSEARCH_TIMEOUT_MS'] as string, 10);
const timeoutMs = (Number.isInteger(rawTimeout) && rawTimeout > 0) ? rawTimeout : 10000;
const MAX_RETRIES = 2;
@@ -3759,7 +3759,7 @@ function cmdScaffold(cwd: string, type: string, options: ScaffoldOptions, raw: b
switch (type) {
case 'context': {
filePath = path.join(phaseDir as string, `${padded}-CONTEXT.md`);
content = `---\nphase: "${padded}"\nname: "${name || (phaseInfo?.['phase_name'] as string | undefined) || 'Unnamed'}"\ncreated: ${today}\n---\n\n# Phase ${phase}: ${name || (phaseInfo?.['phase_name'] as string | undefined) || 'Unnamed'} — Context\n\n## Decisions\n\n_Decisions will be captured during ${String(formatGsdSlash('discuss-phase', resolveRuntime(cwd)))} ${phase}_\n\n## Discretion Areas\n\n_Areas where the executor can use judgment_\n\n## Deferred Ideas\n\n_Ideas to consider later_\n`;
content = `---\nphase: "${padded}"\nname: "${name || (phaseInfo?.['phase_name'] as string | undefined) || 'Unnamed'}"\ncreated: ${today}\n---\n\n# Phase ${phase}: ${name || (phaseInfo?.['phase_name'] as string | undefined) || 'Unnamed'} — Context\n\n## Decisions\n\n_Decisions will be captured during ${String(formatMsdSlash('discuss-phase', resolveRuntime(cwd)))} ${phase}_\n\n## Discretion Areas\n\n_Areas where the executor can use judgment_\n\n## Deferred Ideas\n\n_Ideas to consider later_\n`;
break;
}
case 'uat': {
@@ -4068,7 +4068,7 @@ function cmdStats(cwd: string, format: string | undefined, raw: boolean): void {
* detection or precedence, so it cannot diverge from `cmdCommit`'s decision
* for the same staged tree (#3588 Part 1: this guard was previously
* phase-blind, reading only project-level `commit_docs` and directly
* contradicting `gsd-tools query commit`'s phase-aware resolution).
* contradicting `msd-tools query commit`'s phase-aware resolution).
*
* Staged paths are read via `git diff --cached --name-only -z`, NUL-
* delimited, rather than the LF-delimited default. Without `-z`, git
@@ -4121,15 +4121,15 @@ function cmdCheckCommit(cwd: string, raw: boolean): void {
/**
* Stable sentinel line identifying a `.git/hooks/pre-commit` file as ours.
* Detection is by PRESENCE of this line, not byte-equality (design "Identifying
* 'our' hook") — a user who appends a line to a GSD-written hook must not make
* 'our' hook") — a user who appends a line to a MSD-written hook must not make
* it unrecognizable, and a hook lacking this line must never be overwritten or
* deleted by `commit-docs-guard enable`/`disable`.
*/
const COMMIT_DOCS_GUARD_MARKER = '# gsd-core:commit-docs-guard';
const COMMIT_DOCS_GUARD_MARKER = '# msd-core:commit-docs-guard';
/**
* Locate `gsd-core/workflows/_runtime-launcher.snippet.sh` — the SAME
* gsd-tools-resolution chain every shipped workflow/agent bash block uses
* Locate `msd-core/workflows/_runtime-launcher.snippet.sh` — the SAME
* msd-tools-resolution chain every shipped workflow/agent bash block uses
* (scripts/sync-runtime-launcher.cjs) — by walking up from this module's own
* compiled location rather than a fixed literal `../..` join, so the walk
* tolerates the module living at a different depth under an alternate build
@@ -4150,9 +4150,9 @@ function findRuntimeLauncherSnippet(): string {
/**
* Build the literal `.git/hooks/pre-commit` content `commit-docs-guard enable`
* writes. Reuses the canonical gsd_run resolution preamble byte-for-byte
* writes. Reuses the canonical msd_run resolution preamble byte-for-byte
* (read from disk, never hand-copied — see findRuntimeLauncherSnippet) so this
* hook resolves `gsd-tools` exactly the way every other shipped workflow bash
* hook resolves `msd-tools` exactly the way every other shipped workflow bash
* block does, and cannot drift from it.
*
* LF-only (#3588 A2): the snippet file and every literal line here are joined
@@ -4167,14 +4167,14 @@ function buildCommitDocsGuardHookScript(): string {
COMMIT_DOCS_GUARD_MARKER,
'# Refuses a commit that stages .planning/ files when `commit_docs` resolves',
'# false (honoring any per-phase override). Installed by',
'# `gsd-tools commit-docs-guard enable`; remove with',
'# `gsd-tools commit-docs-guard disable`. See',
'# `msd-tools commit-docs-guard enable`; remove with',
'# `msd-tools commit-docs-guard disable`. See',
'# docs/how-to/keep-planning-docs-private.md.',
'set -euo pipefail',
'',
preamble,
'',
'gsd_run check-commit --raw',
'msd_run check-commit --raw',
];
return lines.join('\n') + '\n';
}
@@ -4245,7 +4245,7 @@ function isCommitDocsGuardHook(content: string): boolean {
}
/**
* `gsd-tools commit-docs-guard enable` — write `.git/hooks/pre-commit`.
* `msd-tools commit-docs-guard enable` — write `.git/hooks/pre-commit`.
* Behavior table (40-design.md rows 1-3, 8-9): refuses to clobber a foreign
* hook, refuses when `core.hooksPath` would make our own write inert, and is
* idempotent when already enabled.
@@ -4276,7 +4276,7 @@ function cmdCommitDocsGuardEnable(cwd: string, raw: boolean): void {
if (existing !== null) {
if (!isCommitDocsGuardHook(existing)) {
error(
`refusing to overwrite an existing pre-commit hook at ${hookPath} that GSD did not write. ` +
`refusing to overwrite an existing pre-commit hook at ${hookPath} that MSD did not write. ` +
`Remove or rename it, or wire commit-docs-guard into it by hand.`,
ERROR_REASON.COMMIT_DOCS_GUARD_FOREIGN_HOOK,
);
@@ -4294,7 +4294,7 @@ function cmdCommitDocsGuardEnable(cwd: string, raw: boolean): void {
}
/**
* `gsd-tools commit-docs-guard disable` — remove `.git/hooks/pre-commit`
* `msd-tools commit-docs-guard disable` — remove `.git/hooks/pre-commit`
* ONLY when it is the hook we wrote (marker presence). Never deletes a
* foreign hook (design row 5); a missing hook is a no-op success, not an
* error (row 6).
@@ -4315,7 +4315,7 @@ function cmdCommitDocsGuardDisable(cwd: string, raw: boolean): void {
if (!isCommitDocsGuardHook(existing)) {
error(
`refusing to remove the pre-commit hook at ${hookPath}: it does not carry the ` +
`${COMMIT_DOCS_GUARD_MARKER} marker, so GSD did not write it.`,
`${COMMIT_DOCS_GUARD_MARKER} marker, so MSD did not write it.`,
ERROR_REASON.COMMIT_DOCS_GUARD_FOREIGN_HOOK,
);
}

View File

@@ -3,22 +3,22 @@
/**
* CommonJS module-type marker — single source of truth (#2544).
*
* GSD stages its own hook scripts and native plugin adapters as `.js` files.
* MSD stages its own hook scripts and native plugin adapters as `.js` files.
* Node resolves a `.js` file's module type by walking up for the nearest
* `package.json`, so an ambient `"type": "module"` above the install location
* makes every one of those scripts fail with `require is not defined`. GSD
* makes every one of those scripts fail with `require is not defined`. MSD
* pins them to CommonJS by writing a minimal `{"type":"commonjs"}` marker.
*
* Two rules govern that marker, and this module exists so both are enforced in
* exactly one place:
*
* 1. **Write only where GSD owns the contents.** The marker belongs in the
* directories GSD fills with its own `.js` files (`hooks/`, and the
* 1. **Write only where MSD owns the contents.** The marker belongs in the
* directories MSD fills with its own `.js` files (`hooks/`, and the
* `nativePlugin.dir` for the runtimes that declare one) — never at the
* runtime's shared config root, which on OpenCode and Kilo is documented,
* user-writable territory for declaring local-plugin npm dependencies.
*
* 2. **Never overwrite a file GSD did not write.** Before #2544 the install
* 2. **Never overwrite a file MSD did not write.** Before #2544 the install
* path wrote the marker unconditionally while the uninstall path already
* compared content before unlinking. That asymmetry is the defect: the
* discipline existed in the codebase, it was simply not applied on the
@@ -26,7 +26,7 @@
* `ensureCommonJsMarker` and `removeCommonJsMarker`, so install and
* uninstall cannot drift apart again.
*
* Ownership is decided by exact content match against the marker GSD itself
* Ownership is decided by exact content match against the marker MSD itself
* writes — the same test the uninstall path has always used.
*/
@@ -39,19 +39,19 @@ import path from 'node:path';
import installFsAdapter = require('./install-fs-adapter.cjs');
const { installFs } = installFsAdapter;
/** The exact marker content GSD writes (and the only content it will remove). */
/** The exact marker content MSD writes (and the only content it will remove). */
export const COMMONJS_MARKER = '{"type":"commonjs"}';
/** File bytes written to disk — the marker plus a trailing newline. */
export const COMMONJS_MARKER_CONTENT = `${COMMONJS_MARKER}\n`;
/**
* `absent` — no package.json here; GSD may create one.
* `gsd-owned` — content is exactly GSD's marker; GSD may rewrite or remove it.
* `foreign` — anything else, including a present-but-unreadable file. GSD
* `absent` — no package.json here; MSD may create one.
* `msd-owned` — content is exactly MSD's marker; MSD may rewrite or remove it.
* `foreign` — anything else, including a present-but-unreadable file. MSD
* must leave it strictly alone.
*/
export type MarkerOwnership = 'absent' | 'gsd-owned' | 'foreign';
export type MarkerOwnership = 'absent' | 'msd-owned' | 'foreign';
/**
* Outcome of an `ensureCommonJsMarker` call, for caller-side reporting.
@@ -82,25 +82,25 @@ export function classifyMarker(dir: string): MarkerOwnership {
try {
// lstat, not existsSync: existsSync follows symlinks and reports `false`
// for a DANGLING one, which would classify the path `absent` and let the
// write below follow the link and land outside the directory GSD owns.
// write below follow the link and land outside the directory MSD owns.
stat = installFs().lstatSync(markerPath);
} catch (err) {
if ((err as NodeJS.ErrnoException).code === 'ENOENT') return 'absent';
return 'foreign';
}
// Anything that is not a regular file (symlink, directory, socket) is not
// something GSD wrote, so it is never ours to overwrite or remove.
// something MSD wrote, so it is never ours to overwrite or remove.
if (!stat.isFile()) return 'foreign';
try {
const content = installFs().readFileSync(markerPath, 'utf8');
return content.trim() === COMMONJS_MARKER ? 'gsd-owned' : 'foreign';
return content.trim() === COMMONJS_MARKER ? 'msd-owned' : 'foreign';
} catch {
return 'foreign';
}
}
/**
* Write the CommonJS marker into `dir`, unless a file GSD does not own is
* Write the CommonJS marker into `dir`, unless a file MSD does not own is
* already there.
*
* Creates `dir` when needed. Returns what happened so the caller can report
@@ -119,7 +119,7 @@ export function classifyMarker(dir: string): MarkerOwnership {
export function ensureCommonJsMarker(dir: string): MarkerWriteOutcome {
const ownership = classifyMarker(dir);
if (ownership === 'foreign') return 'preserved-foreign';
if (ownership === 'gsd-owned') return 'unchanged';
if (ownership === 'msd-owned') return 'unchanged';
try {
installFs().mkdirSync(dir, { recursive: true });
// Exclusive create closes the gap between classifying and writing: if
@@ -135,10 +135,10 @@ export function ensureCommonJsMarker(dir: string): MarkerWriteOutcome {
/**
* Remove the CommonJS marker from `dir` — only when the content is exactly
* the marker GSD writes. Returns true when a file was removed.
* the marker MSD writes. Returns true when a file was removed.
*/
export function removeCommonJsMarker(dir: string): boolean {
if (classifyMarker(dir) !== 'gsd-owned') return false;
if (classifyMarker(dir) !== 'msd-owned') return false;
try {
installFs().unlinkSync(markerPathFor(dir));
return true;

View File

@@ -14,7 +14,7 @@
* `-REFACTOR.md` artifact) -> readBaseline / writeBaseline (I/O, atomic
* temp-then-rename, never throws).
*
* Authoritative surface: .gsd/phase/feat-1953-complexity-triggered-refactor/41-api-contract.md
* Authoritative surface: .msd/phase/feat-1953-complexity-triggered-refactor/41-api-contract.md
* Executable spec: tests/complexity-trigger.test.cjs
*
* Out of scope for this module: git invocation, config reads,
@@ -733,14 +733,14 @@ export function analyzeSource(
/**
* Normalizes separators unconditionally (never via path.sep). True iff the
* extension is analyzable and the path is not under tests/ or
* gsd-core/bin/lib/.
* msd-core/bin/lib/.
*/
export function isAnalyzablePath(relPath: string): boolean {
const normalized = relPath.replace(/\\/g, '/');
const ext = path.posix.extname(normalized);
if (!ANALYZABLE_EXTENSIONS.includes(ext)) return false;
if (normalized === 'tests' || normalized.startsWith('tests/')) return false;
if (normalized === 'gsd-core/bin/lib' || normalized.startsWith('gsd-core/bin/lib/')) return false;
if (normalized === 'msd-core/bin/lib' || normalized.startsWith('msd-core/bin/lib/')) return false;
return true;
}

View File

@@ -50,8 +50,8 @@ const { UNUSABLE_REASON: _UNUSABLE_REASON, warnUnusableInput: _warnUnusableInput
// eslint-disable-next-line @typescript-eslint/no-require-imports
import federatedConfigModule = require('./federated-config.cjs');
const { mergeFederatedConfig } = federatedConfigModule;
// The capability-registry.cjs is generated and lives in the same gsd-core/bin/lib/ output dir.
// Both config-loader.cjs and capability-registry.cjs land in gsd-core/bin/lib/ at build time.
// The capability-registry.cjs is generated and lives in the same msd-core/bin/lib/ output dir.
// Both config-loader.cjs and capability-registry.cjs land in msd-core/bin/lib/ at build time.
// This is the FROZEN first-party registry — used as the test-seam default and the
// fallback. Overlay (installed third-party) config-key federation is cwd-dependent
// and composed PER loadConfig CALL by _federatedConfigSchema(cwd) below (ADR-1244 D2),
@@ -259,7 +259,7 @@ function _warnUnknownProfileOverrides(parsed: Record<string, unknown>, configLab
_warnedConfigKeys.add(key);
try {
process.stderr.write(
`gsd: warning — config key "runtime" has unknown value "${runtime}". ` +
`msd: warning — config key "runtime" has unknown value "${runtime}". ` +
`Known runtimes: ${[...(KNOWN_RUNTIMES)].sort().join(', ')}. ` +
`Resolution will fall back to safe defaults. (#2517)\n`
);
@@ -276,7 +276,7 @@ function _warnUnknownProfileOverrides(parsed: Record<string, unknown>, configLab
_warnedConfigKeys.add(key);
try {
process.stderr.write(
`gsd: warning — model_profile_overrides.${overrideRuntime}.* uses ` +
`msd: warning — model_profile_overrides.${overrideRuntime}.* uses ` +
`unknown runtime "${overrideRuntime}". Known runtimes: ` +
`${[...(KNOWN_RUNTIMES)].sort().join(', ')}. (#2517)\n`
);
@@ -291,7 +291,7 @@ function _warnUnknownProfileOverrides(parsed: Record<string, unknown>, configLab
_warnedConfigKeys.add(key);
try {
process.stderr.write(
`gsd: warning — model_profile_overrides.${overrideRuntime}.${tierName} ` +
`msd: warning — model_profile_overrides.${overrideRuntime}.${tierName} ` +
`uses unknown tier "${tierName}". Allowed tiers: opus, sonnet, haiku. (#2517)\n`
);
} catch { /* ok */ }
@@ -313,7 +313,7 @@ function _warnUnknownProfileOverrides(parsed: Record<string, unknown>, configLab
_warnedConfigKeys.add(pkey);
try {
process.stderr.write(
`gsd: warning — model_policy.provider has unknown value "${provider}". ` +
`msd: warning — model_policy.provider has unknown value "${provider}". ` +
`Known providers: ${[...(KNOWN_PROVIDERS)].sort().join(', ')}. ` +
`For manual model IDs use provider="custom". (#49)\n`
);
@@ -330,7 +330,7 @@ function _warnUnknownProfileOverrides(parsed: Record<string, unknown>, configLab
_warnedConfigKeys.add(key);
try {
process.stderr.write(
`gsd: warning — model_policy.runtime_tiers.${pruntime}.* uses ` +
`msd: warning — model_policy.runtime_tiers.${pruntime}.* uses ` +
`unknown runtime "${pruntime}". Known runtimes: ` +
`${[...(KNOWN_RUNTIMES)].sort().join(', ')}. (#49)\n`
);
@@ -345,7 +345,7 @@ function _warnUnknownProfileOverrides(parsed: Record<string, unknown>, configLab
_warnedConfigKeys.add(key);
try {
process.stderr.write(
`gsd: warning — model_policy.runtime_tiers.${pruntime}.${tierName} ` +
`msd: warning — model_policy.runtime_tiers.${pruntime}.${tierName} ` +
`uses unknown tier "${tierName}". Allowed: opus, sonnet, haiku. (#49)\n`
);
} catch { /* ok */ }
@@ -374,11 +374,11 @@ function _resetRuntimeWarningCacheForTests(): void {
// ─── #3532 (10b): shadowed global-defaults diagnostic ────────────────────────
// The keys Branch D's `_globalBaseCfg` demonstrably honors from
// ~/.gsd/defaults.json when no project config exists. Under a project
// ~/.msd/defaults.json when no project config exists. Under a project
// .planning/config.json (Branch A — every real project) the global file is
// never opened, so each of these set globally is silently inert for resolution.
// `effort` is in Branch D's honored set but is EXCLUDED from the shadow warning:
// the install-time effort sync (readGsdEffectiveEffortConfig) DOES merge the
// the install-time effort sync (readMsdEffectiveEffortConfig) DOES merge the
// global file, so warning on it would be false for the channel users actually
// control via `effort sync`. Keep this list in lockstep with `_globalBaseCfg`
// below — the per-key canary in tests/config-loader.test.cjs fails first on
@@ -421,7 +421,7 @@ function _warnShadowedGlobalDefaults(globalDefaults: Record<string, unknown>, gl
_warnedShadowedGlobalKeys.add(dedupKey);
try {
process.stderr.write(
`gsd-tools: warning: ${globalPath} sets ${shadowed.join(', ')} but a project config ` +
`msd-tools: warning: ${globalPath} sets ${shadowed.join(', ')} but a project config ` +
`takes precedence here — those global keys are ignored for model resolution. (#3532)\n`,
);
} catch { /* stderr might be closed in some test harnesses */ }
@@ -499,7 +499,7 @@ function _federatedConfigSchema(cwd?: string): Record<string, unknown> | undefin
const loaderMod: { loadRegistry: (o?: Record<string, unknown>) => { configSchema?: Record<string, unknown> } } = require('./capability-loader.cjs');
// #1459 IC-04: thread the consent home explicitly so a consented project cap's federated config
// key resolves at the SAME user-owned home that gated its activation (never the wrong home).
const schema = loaderMod.loadRegistry({ includeInstalled: true, cwd, gsdHome: process.env['GSD_HOME'] }).configSchema;
const schema = loaderMod.loadRegistry({ includeInstalled: true, cwd, msdHome: process.env['MSD_HOME'] }).configSchema;
if (schema && typeof schema === 'object') return schema;
} catch { /* fall back to first-party */ }
}
@@ -651,7 +651,7 @@ function _warnUnusableConfig(fault: ConfigFault): void {
? 'is not valid JSON'
: `could not be read (${fault.code})`;
process.stderr.write(
`gsd-tools: warning: ${fault.path} ${what} — its settings were NOT applied; using defaults instead\n`,
`msd-tools: warning: ${fault.path} ${what} — its settings were NOT applied; using defaults instead\n`,
);
}
@@ -667,7 +667,7 @@ function _warnUnusableConfig(fault: ConfigFault): void {
* A2: ws null + config.json found → source:'root', degraded:false, reason:'resolved'|'configured_empty'
* B: catch + .planning/ + rootParsed set (ws fallback) → source:'root', degraded:true, reason:'workstream_fallback'
* C: catch + .planning/ + rootParsed null (federated defaults) → source:'builtin-defaults', degraded:false, reason:'not_configured'
* D: catch + no .planning/ + ~/.gsd/defaults.json readable → source:'global-defaults', degraded:false, reason:'not_configured'
* D: catch + no .planning/ + ~/.msd/defaults.json readable → source:'global-defaults', degraded:false, reason:'not_configured'
* E: catch + no .planning/ + no global → source:'builtin-defaults', degraded:false, reason:'not_configured'
*
* ORTHOGONAL to all of the above (#1880, ADR-1411 "corrupt is not absent"): if
@@ -698,7 +698,7 @@ function loadConfigResolvedInternal(cwd: string, options: Record<string, unknown
? options['workstream']
: (options['workstreamContext'] && Object.prototype.hasOwnProperty.call(options['workstreamContext'], 'ws'))
? (options['workstreamContext'] as Record<string, unknown>)['ws']
: (process.env['GSD_WORKSTREAM'] || null);
: (process.env['MSD_WORKSTREAM'] || null);
const ws = typeof activeWorkstream === 'string' ? activeWorkstream.trim() || null : null;
// wsRequested: true when caller explicitly requested a non-empty workstream.
// Used for source labeling (Fix 4) and early absent-dir intercept (Fix 2).
@@ -876,7 +876,7 @@ function loadConfigResolvedInternal(cwd: string, options: Record<string, unknown
if (!_warnedUnknownConfigKeys.has(warnKey)) {
_warnedUnknownConfigKeys.add(warnKey);
process.stderr.write(
`gsd-tools: warning: unknown config key(s) in .planning/config.json: ${unknownKeys.join(', ')} — these will be ignored\n`
`msd-tools: warning: unknown config key(s) in .planning/config.json: ${unknownKeys.join(', ')} — these will be ignored\n`
);
}
}
@@ -1005,13 +1005,13 @@ function loadConfigResolvedInternal(cwd: string, options: Record<string, unknown
const source: ConfigSource = wsRequested ? 'workstream' : 'root';
// #3532 (10b): a parsed project config means Branch D never runs, so every
// key ~/.gsd/defaults.json sets that Branch D would honor is silently inert
// key ~/.msd/defaults.json sets that Branch D would honor is silently inert
// here. Observation only — one deduped stderr warning; precedence is
// untouched. Faults in the global file stay silent in this branch (the
// project config governs; the nearer file is the actionable one).
try {
const shadowHome = process.env['GSD_HOME'] || os.homedir();
const shadowPath = path.join(shadowHome, '.gsd', 'defaults.json');
const shadowHome = process.env['MSD_HOME'] || os.homedir();
const shadowPath = path.join(shadowHome, '.msd', 'defaults.json');
const shadowRead = _readConfigFile(shadowPath);
if (shadowRead.kind === 'ok') {
_warnShadowedGlobalDefaults(shadowRead.data, shadowPath);
@@ -1043,7 +1043,7 @@ function loadConfigResolvedInternal(cwd: string, options: Record<string, unknown
} catch {
// Fix 2: Early intercept — workstream requested but ws config.json absent (or dir absent)
// AND root config was loaded. Covers BOTH "dir exists, no config.json" AND "dir absent".
// This delivers the #1366 acceptance criterion: nonexistent GSD_WORKSTREAM yields root, degraded.
// This delivers the #1366 acceptance criterion: nonexistent MSD_WORKSTREAM yields root, degraded.
//
// Both fallback recursions below forward `options` and override ONLY `workstream`.
// A bare `{ workstream: null }` silently dropped every other option, so a caller's
@@ -1073,11 +1073,11 @@ function loadConfigResolvedInternal(cwd: string, options: Record<string, unknown
}
// Branch D or E: no .planning/
try {
const home = process.env['GSD_HOME'] || os.homedir();
const globalDefaultsPath = path.join(home, '.gsd', 'defaults.json');
const home = process.env['MSD_HOME'] || os.homedir();
const globalDefaultsPath = path.join(home, '.msd', 'defaults.json');
const globalRead = _readConfigFile(globalDefaultsPath);
if (globalRead.kind === 'fault') {
// ~/.gsd/defaults.json is present but unusable. Only report it when the
// ~/.msd/defaults.json is present but unusable. Only report it when the
// project config did not already fail — the nearer file is the one the
// user is most likely to be able to act on.
if (!configFault) configFault = globalRead.fault;
@@ -1098,7 +1098,7 @@ function loadConfigResolvedInternal(cwd: string, options: Record<string, unknown
?? defaults.post_planning_gaps,
// #3894: same nested-alias shape as post_planning_gaps above — the key
// was silently dropped from global defaults, so it was unavailable at
// user scope AND inert at project scope on the /gsd-quick path.
// user scope AND inert at project scope on the /msd-quick path.
research_before_questions: (globalDefaults['research_before_questions'])
?? (globalDefaults['workflow'] as Record<string, unknown> | undefined)?.['research_before_questions']
?? defaults.research_before_questions,
@@ -1122,7 +1122,7 @@ function loadConfigResolvedInternal(cwd: string, options: Record<string, unknown
response_language: (globalDefaults['response_language']) || null,
// #2069: forward model_policy / model_profile_overrides / runtime so the global-defaults
// path is at parity with the project-config path (which forwards these three from
// parsed['…'] at the top of this function). Without these entries, ~/.gsd/defaults.json
// parsed['…'] at the top of this function). Without these entries, ~/.msd/defaults.json
// silently drops them — model_policy/provider/budget etc. are honored when set in a
// project but ignored when set globally.
runtime: (globalDefaults['runtime']) || null,
@@ -1149,7 +1149,7 @@ function loadConfigResolvedInternal(cwd: string, options: Record<string, unknown
/**
* #4717 — fill an empty `runtime` from the environment, then the per-install
* marker. writeNonClaudeDefaults stamps `runtime` into the SHARED
* ~/.gsd/defaults.json with whichever non-Claude runtime installed first, so
* ~/.msd/defaults.json with whichever non-Claude runtime installed first, so
* direct readers of config.runtime saw another runtime's identity (or
* nothing) on a multi-runtime machine. Copy-on-write: the builtin-defaults
* branch returns a shared object, so never assign into it. An explicit
@@ -1159,7 +1159,7 @@ function fillRuntimeIdentity(resolved: ConfigResolution): ConfigResolution {
const cfg = resolved?.config;
if (!cfg) return resolved;
const runtime = resolveRuntimeNameFromCandidates(
process.env['GSD_RUNTIME'],
process.env['MSD_RUNTIME'],
readInstallRuntimeMarker(),
);
// Only a runtime the name policy can canonicalize is an identity. Unknown
@@ -1174,10 +1174,10 @@ function fillRuntimeIdentity(resolved: ConfigResolution): ConfigResolution {
return { ...resolved, config: { ...cfg, runtime: canonicalRuntime } };
}
// Rung 2 (#4717 stamped-defaults leg): the global-defaults branch forwards
// the SHARED ~/.gsd/defaults.json's runtime verbatim — whichever non-Claude
// the SHARED ~/.msd/defaults.json's runtime verbatim — whichever non-Claude
// runtime installed FIRST stamped that machine-wide file, and every other
// runtime's resolution inherited its identity (the issue's second failure
// shape). When THIS install carries its own identity (GSD_RUNTIME or the
// shape). When THIS install carries its own identity (MSD_RUNTIME or the
// marker), the stamp — not the operator — is speaking: correct it. Project
// and workstream configs are explicit operator intent and are never touched;
// with no install identity of its own the stamped value stays (status quo).

View File

@@ -1,7 +1,7 @@
/**
* Thin adapter — sources schema data from the manifest via the generated
* Configuration Module. All inline literals have been removed; the manifest
* at gsd-core/bin/shared/config-schema.manifest.json is the single source of truth.
* at msd-core/bin/shared/config-schema.manifest.json is the single source of truth.
*
* Imported by:
* - config.cjs (isValidConfigKey validator)
@@ -39,7 +39,7 @@ function _capabilityConfigSchema(cwd?: string): Record<string, unknown> {
const loaderMod: { loadRegistry: (o?: Record<string, unknown>) => { configSchema?: Record<string, unknown> } } = require('./capability-loader.cjs');
// #1459 IC-04: thread the consent home explicitly so a consented project cap's config key
// federates at the SAME user-owned home that gated its activation.
const schema = loaderMod.loadRegistry({ includeInstalled: true, cwd, gsdHome: process.env['GSD_HOME'] }).configSchema;
const schema = loaderMod.loadRegistry({ includeInstalled: true, cwd, msdHome: process.env['MSD_HOME'] }).configSchema;
if (schema && typeof schema === 'object') return schema;
} catch { /* fall back to first-party */ }
}

View File

@@ -1,5 +1,5 @@
/**
* TypeScript type definitions for GSD project config — model_policy block.
* TypeScript type definitions for MSD project config — model_policy block.
*
* These types reflect the model_policy config shape consumed by
* resolveModelPolicy in model-resolver.cjs and validated by config-schema.cjs.
@@ -7,7 +7,7 @@
*
* See feat #49 (model_policy presets) and config-schema.manifest.json.
* Added under ADR-457: TS sources in src/ compile to CJS artifacts in
* gsd-core/bin/lib/ at publish time.
* msd-core/bin/lib/ at publish time.
*
* Resolution precedence (highest → lowest):
* 1. model_overrides[agent]
@@ -18,7 +18,7 @@
*/
/**
* A single tier entry mapping a GSD tier (opus | sonnet | haiku) to a
* A single tier entry mapping a MSD tier (opus | sonnet | haiku) to a
* concrete model ID. The optional `reasoning_effort` field is forwarded to
* runtimes that accept it (e.g. opencode).
*/
@@ -28,7 +28,7 @@ export interface TierEntry {
}
/**
* The three standard GSD tiers for one runtime target. All fields are
* The three standard MSD tiers for one runtime target. All fields are
* optional so callers can supply a partial override (e.g. only `opus`).
*/
export interface RuntimeTiers {
@@ -55,7 +55,7 @@ export interface ModelPolicyConfig {
}
/**
* Minimal subset of the GSD project config that includes `model_policy`.
* Minimal subset of the MSD project config that includes `model_policy`.
* Extend this interface when migrating further config keys to TypeScript.
*/
export interface ProjectConfig {

View File

@@ -122,7 +122,7 @@ const SCHEMA_DEFAULTS: Record<string, unknown> = {
// Derived from the defaults manifest rather than restated, so the manifest
// stays the single source of truth for the smart-zone budget (#2630).
'workflow.smart_zone_tokens': CONFIG_DEFAULTS.smart_zone_tokens,
// #2971: /gsd:pr-branch reads this key directly; an absent key must resolve to the
// #2971: /msd:pr-branch reads this key directly; an absent key must resolve to the
// manifest default rather than "Key not found". Derived from the defaults manifest so
// the manifest stays the single source of truth.
'planning.pr_strict': CONFIG_DEFAULTS.pr_strict,
@@ -301,8 +301,8 @@ function validateShipPrBodySections(value: unknown): void {
*
* Merges (increasing priority):
* 1. Hardcoded defaults — every key that loadConfig() resolves, plus mode/granularity
* 2. User-level defaults from ~/.gsd/defaults.json (if present)
* 3. userChoices — the settings the user explicitly selected during /gsd:new-project
* 2. User-level defaults from ~/.msd/defaults.json (if present)
* 3. userChoices — the settings the user explicitly selected during /msd:new-project
*
* Uses the canonical `git` namespace for branching keys (consistent with VALID_CONFIG_KEYS
* and the settings workflow). loadConfig() handles both flat and nested formats, so this
@@ -315,23 +315,23 @@ function buildNewProjectConfig(userChoices: Record<string, unknown>): Record<str
const homedir = os.homedir();
// Detect API key availability
const braveKeyFile = path.join(homedir, '.gsd', 'brave_api_key');
const braveKeyFile = path.join(homedir, '.msd', 'brave_api_key');
const hasBraveSearch = !!(process.env['BRAVE_API_KEY'] || fs.existsSync(braveKeyFile));
const firecrawlKeyFile = path.join(homedir, '.gsd', 'firecrawl_api_key');
const firecrawlKeyFile = path.join(homedir, '.msd', 'firecrawl_api_key');
const hasFirecrawl = !!(process.env['FIRECRAWL_API_KEY'] || fs.existsSync(firecrawlKeyFile));
const exaKeyFile = path.join(homedir, '.gsd', 'exa_api_key');
const exaKeyFile = path.join(homedir, '.msd', 'exa_api_key');
const hasExaSearch = !!(process.env['EXA_API_KEY'] || fs.existsSync(exaKeyFile));
const tavilyKeyFile = path.join(homedir, '.gsd', 'tavily_api_key');
const tavilyKeyFile = path.join(homedir, '.msd', 'tavily_api_key');
const hasTavilySearch = !!(process.env['TAVILY_API_KEY'] || fs.existsSync(tavilyKeyFile));
const refKeyFile = path.join(homedir, '.gsd', 'ref_api_key');
const refKeyFile = path.join(homedir, '.msd', 'ref_api_key');
const hasRefSearch = !!(process.env['REF_API_KEY'] || fs.existsSync(refKeyFile));
const perplexityKeyFile = path.join(homedir, '.gsd', 'perplexity_api_key');
const perplexityKeyFile = path.join(homedir, '.msd', 'perplexity_api_key');
const hasPerplexity = !!(process.env['PERPLEXITY_API_KEY'] || fs.existsSync(perplexityKeyFile));
const jinaKeyFile = path.join(homedir, '.gsd', 'jina_api_key');
const jinaKeyFile = path.join(homedir, '.msd', 'jina_api_key');
const hasJina = !!(process.env['JINA_API_KEY'] || fs.existsSync(jinaKeyFile));
// Load user-level defaults from ~/.gsd/defaults.json if available
const globalDefaultsPath = path.join(homedir, '.gsd', 'defaults.json');
// Load user-level defaults from ~/.msd/defaults.json if available
const globalDefaultsPath = path.join(homedir, '.msd', 'defaults.json');
let userDefaults: Record<string, unknown> = {};
try {
if (fs.existsSync(globalDefaultsPath)) {
@@ -427,7 +427,7 @@ function buildNewProjectConfig(userChoices: Record<string, unknown>): Record<str
// Claude Code projects, resolving agents to wrong model IDs. `resolve_model_ids`
// already has a per-install guard (#2297); `runtime` gets the same treatment
// by excluding it from the defaults spread. Projects detect the runtime from
// the install path / .gsd-runtime marker, not from a copied config key.
// the install path / .msd-runtime marker, not from a copied config key.
const safeDefaults = { ...userDefaults };
delete safeDefaults['runtime'];
@@ -476,8 +476,8 @@ function buildNewProjectConfig(userChoices: Record<string, unknown>): Record<str
* Command: create a fully-materialized .planning/config.json for a new project.
*
* Accepts user-chosen settings as a JSON string (the keys the user explicitly
* configured during /gsd:new-project). All remaining keys are filled from
* hardcoded defaults and optional ~/.gsd/defaults.json.
* configured during /msd:new-project). All remaining keys are filled from
* hardcoded defaults and optional ~/.msd/defaults.json.
*
* Idempotent: if config.json already exists, returns { created: false }.
*/
@@ -1003,7 +1003,7 @@ function cmdConfigSet(cwd: string, keyPath: string | undefined, value: string |
}
}
// Statusline GSD-state format enum validation
// Statusline MSD-state format enum validation
const VALID_STATE_FORMATS = ['full', 'compact'];
if (kp === 'statusline.state_format') assertEnumValue(parsedValue, val, VALID_STATE_FORMATS, 'statusline.state_format');
@@ -1020,8 +1020,8 @@ function cmdConfigSet(cwd: string, keyPath: string | undefined, value: string |
// and honour in agreement ON THE DOMAIN: the hook falls back to its default
// for a value outside it, so reporting success would be a lie. That agreement
// is per-key and no wider — a value accepted here can still be superseded at
// read time by the hook's pair check, and a scoped write (GSD_PROJECT /
// GSD_WORKSTREAM) lands in a config the hook does not read at all. The PAIR
// read time by the hook's pair check, and a scoped write (MSD_PROJECT /
// MSD_WORKSTREAM) lands in a config the hook does not read at all. The PAIR
// (critical < warning) is deliberately NOT enforced here: config-set writes
// one key per call, so a two-step retune can be transiently inconsistent on
// disk and a check here would reject that intermediate write.
@@ -1291,10 +1291,10 @@ function cmdConfigGet(cwd: string, keyPath: string | undefined, raw: boolean, de
* that file directly.
*/
function resolveFromRootConfig(cwd: string, kp: string): { found: boolean; value: unknown } {
// Only meaningful when a workstream is active (GSD_WORKSTREAM set) — that is what
// Only meaningful when a workstream is active (MSD_WORKSTREAM set) — that is what
// redirects planningDir away from root AND what loadConfigResolved gates root-reading
// on. Gating on `process.env.GSD_WORKSTREAM` (not on a planningDir !== planningRoot
// path inequality) avoids a false trigger under GSD_PROJECT alone, where planningDir
// on. Gating on `process.env.MSD_WORKSTREAM` (not on a planningDir !== planningRoot
// path inequality) avoids a false trigger under MSD_PROJECT alone, where planningDir
// diverges from planningRoot without a workstream and loadConfigResolved does NOT
// inherit root — matching the runtime's own `if (ws)` gate keeps the two surfaces
// from diverging on the project-scoped (non-workstream) case.
@@ -1405,7 +1405,7 @@ function cmdConfigPath(cwd: string, _raw: boolean, workstreamContext: Workstream
*
* Wraps the Configuration Module's migrateOnDisk() for the CLI surface. This
* is the Phase 2 acceptance-criteria deliverable for opt-in migration (#3536):
* users can run `gsd-tools migrate-config` to apply all four legacy-key
* users can run `msd-tools migrate-config` to apply all four legacy-key
* migrations to their .planning/config.json without having to load any config
* implicitly via another command.
*
@@ -1419,7 +1419,7 @@ function cmdConfigPath(cwd: string, _raw: boolean, workstreamContext: Workstream
function cmdMigrateConfig(cwd: string, raw: boolean): void {
const ws = resolveEnvWorkstream();
// #3749: resolve the migration target through the project-aware resolver so
// GSD_PROJECT scopes the write; migrateOnDisk itself cannot (see its
// MSD_PROJECT scopes the write; migrateOnDisk itself cannot (see its
// configPathOverride note).
const scopedConfigPath = path.join(planningDir(cwd, ws || undefined), 'config.json');
const report = migrateOnDisk(cwd, ws || undefined, scopedConfigPath);

View File

@@ -28,7 +28,7 @@ const _require: NodeRequire = require;
// ─── Manifest requires ───────────────────────────────────────────────────────
function loadConfigurationManifest(fileName: string): Record<string, unknown> {
const candidates = [
// Installed runtime layout: gsd-core/bin/shared/*.manifest.json
// Installed runtime layout: msd-core/bin/shared/*.manifest.json
join(__dirname, '..', 'shared', fileName),
];
let lastErr: Error | null = null;
@@ -166,7 +166,7 @@ interface SkippedNormalization {
* The type is the whole diagnostic ("this is a string where an object belongs"),
* and the value is still sitting untouched in the file, so echoing it buys
* nothing. It would cost something: `cmdMigrateConfig` prints this report
* verbatim on `gsd-tools migrate-config --json`, and config values are treated
* verbatim on `msd-tools migrate-config --json`, and config values are treated
* as potentially secret-bearing elsewhere in this module (`maskSecret` on the
* `config set/unset` output path).
*/
@@ -328,7 +328,7 @@ function migrateOnDisk(cwd: string, workstream?: string, configPathOverride?: st
// This module cannot import that sibling (#3571 install-layout contract),
// and its own local planningDir above is deliberately workstream-only —
// resolving here through the local copy made migrate-config under
// GSD_PROJECT rewrite the ROOT config instead of the scoped one.
// MSD_PROJECT rewrite the ROOT config instead of the scoped one.
const configPath = configPathOverride ?? join(planningDir(cwd, workstream), 'config.json');
let raw: string;
try {

View File

@@ -105,7 +105,7 @@
* pre-scan of the raw lines.
*
* ADR-457 build-at-publish: compiled by tsc to
* gsd-core/bin/lib/context-predicates.cjs (gitignored).
* msd-core/bin/lib/context-predicates.cjs (gitignored).
*/
/** A single parsed predicate fact from CONTEXT.md. */

View File

@@ -1,5 +1,5 @@
/**
* Context-utilization classifier for `gsd-health --context` (ADR-457
* Context-utilization classifier for `msd-health --context` (ADR-457
* build-at-publish: the hand-written bin/lib/context-utilization.cjs collapsed
* to a TypeScript source of truth). Behaviour is preserved byte-for-behaviour
* from the prior hand-written .cjs; only types are added.
@@ -7,7 +7,7 @@
* Pure function. Callers pass tokensUsed + contextWindow; the
* classifier returns the percent and one of three states. Recommendation
* strings are NOT in this module — formatting is the renderer's job
* (see `validate context` in gsd-tools.cjs). That separation lets the
* (see `validate context` in msd-tools.cjs). That separation lets the
* copy change without touching this module's tests.
*
* Thresholds:

View File

@@ -225,7 +225,7 @@ const boldLeadInBulletRe = /^\s*-\s+\*\*[A-Z]+[0-9]*-[A-Za-z0-9]/m;
/**
* #3939: a decision bullet's DECLARATION line — the `- **D[phase]-NN … **` bold
* lead-in the three grammars above anchor on — may wrap across a line break.
* Physical line breaks inside a bullet are markdown-insignificant, and GSD's own
* Physical line breaks inside a bullet are markdown-insignificant, and MSD's own
* discuss-phase writer emits the wrapped shape whenever a decision title runs
* past the wrap column. All three grammars require the closing `**` in the same
* string as the `- **D…-` anchor, so a wrapped declaration matched none of them
@@ -501,7 +501,7 @@ function parseDecisionLines(block: string): ParseDecisionLinesResult {
// like a continuation line, before it ever reaches the declaration/
// parse-miss regexes below. A bullet at the SAME or a SHALLOWER indent
// is unaffected — tested exactly as before this fix. See design doc
// .gsd/phase/chore-3414-tokenizer-first-seam/40-design.md §1.3 for why
// .msd/phase/chore-3414-tokenizer-first-seam/40-design.md §1.3 for why
// nesting depth, not bullet content, is the signal that distinguishes
// this from a genuinely malformed top-level declaration.
if (

View File

@@ -2,7 +2,7 @@
* Docs — Commands for the docs-update workflow
*
* Provides `cmdDocsInit` which returns project signals, existing doc inventory
* with GSD marker detection, doc tooling detection, monorepo awareness, and
* with MSD marker detection, doc tooling detection, monorepo awareness, and
* model resolution. Used by Phase 2 to route doc generation appropriately.
*
* ADR-457 build-at-publish: the hand-written bin/lib/docs.cjs collapsed
@@ -32,7 +32,7 @@ import { platformReadSync } from './shell-command-projection.cjs';
// ─── Constants ────────────────────────────────────────────────────────────────
const GSD_MARKER = '<!-- generated-by: gsd-doc-writer -->';
const MSD_MARKER = '<!-- generated-by: msd-doc-writer -->';
const SKIP_DIRS = new Set([
'node_modules', '.git', '.planning', '.claude', '__pycache__',
@@ -43,16 +43,16 @@ const SKIP_DIRS = new Set([
// ─── Private helpers ──────────────────────────────────────────────────────────
/**
* Check whether a file begins with the GSD doc writer marker.
* Check whether a file begins with the MSD doc writer marker.
* Reads the first 500 bytes only — avoids loading large files.
*/
function hasGsdMarker(filePath: string): boolean {
function hasMsdMarker(filePath: string): boolean {
try {
const buf = Buffer.alloc(500);
const fd = fs.openSync(filePath, 'r');
const bytesRead = fs.readSync(fd, buf, 0, 500, 0);
fs.closeSync(fd);
return buf.slice(0, bytesRead).toString('utf-8').includes(GSD_MARKER);
return buf.slice(0, bytesRead).toString('utf-8').includes(MSD_MARKER);
} catch {
return false;
}
@@ -60,7 +60,7 @@ function hasGsdMarker(filePath: string): boolean {
interface DocEntry {
path: string;
has_gsd_marker: boolean;
has_msd_marker: boolean;
}
/**
@@ -82,7 +82,7 @@ function scanExistingDocs(cwd: string): DocEntry[] {
walkDir(abs, depth + 1);
} else if (entry.isFile() && entry.name.toLowerCase().endsWith('.md')) {
const rel = toPosixPath(path.relative(cwd, abs));
results.push({ path: rel, has_gsd_marker: hasGsdMarker(abs) });
results.push({ path: rel, has_msd_marker: hasMsdMarker(abs) });
}
}
} catch { /* directory may not exist — best-effort */ }
@@ -95,7 +95,7 @@ function scanExistingDocs(cwd: string): DocEntry[] {
if (entry.isFile() && entry.name.toLowerCase().endsWith('.md')) {
const abs = path.join(cwd, entry.name);
const rel = toPosixPath(path.relative(cwd, abs));
results.push({ path: rel, has_gsd_marker: hasGsdMarker(abs) });
results.push({ path: rel, has_msd_marker: hasMsdMarker(abs) });
}
}
} catch { /* best-effort */ }
@@ -268,12 +268,12 @@ function detectMonorepoWorkspaces(cwd: string): string[] {
* resolution. Follows the cmdInitMapCodebase pattern.
*
* @example
* node gsd-tools.cjs docs-init --raw
* node msd-tools.cjs docs-init --raw
*/
function cmdDocsInit(cwd: string, raw: boolean): void {
const config = loadConfig(cwd);
const result: Record<string, unknown> = {
doc_writer_model: resolveModelInternal(cwd, 'gsd-doc-writer'),
doc_writer_model: resolveModelInternal(cwd, 'msd-doc-writer'),
commit_docs: config.commit_docs,
existing_docs: scanExistingDocs(cwd),
project_type: detectProjectType(cwd),

View File

@@ -2,7 +2,7 @@
* Codebase Drift Detection (#2003)
*
* Detects structural drift between a committed codebase and the
* `.planning/codebase/STRUCTURE.md` map produced by `gsd-codebase-mapper`.
* `.planning/codebase/STRUCTURE.md` map produced by `msd-codebase-mapper`.
*
* Four categories of drift element:
* - new_dir → a newly-added file whose directory prefix does not appear
@@ -36,7 +36,7 @@
import fs from 'node:fs';
import { platformWriteSync, posixNormalize } from './shell-command-projection.cjs';
import { formatGsdSlash } from './runtime-slash.cjs';
import { formatMsdSlash } from './runtime-slash.cjs';
// ─── Constants ───────────────────────────────────────────────────────────────
@@ -299,7 +299,7 @@ function buildMessage(elements: DriftElement[], affectedPaths: string[], action:
// caller (verify.cmdVerifyCodebaseDrift) resolves the runtime once and
// passes it in via input.runtime so emitted commands match the project
// the caller is targeting, not the current process directory.
const mapCmd = formatGsdSlash('map-codebase', runtime || 'claude');
const mapCmd = formatMsdSlash('map-codebase', runtime || 'claude');
lines.push(
`Run ${String(mapCmd)} --paths ${affectedPaths.join(',')} to refresh planning context.`,
);

View File

@@ -10,7 +10,7 @@
*
* Authored as strict TypeScript (`src/edge-probe.cts`) and compiled by
* `tsc -p tsconfig.build.json` to the gitignored runtime artifact
* `gsd-core/bin/lib/edge-probe.cjs`. Do NOT hand-write the `.cjs`; it is emitted. Tests
* `msd-core/bin/lib/edge-probe.cjs`. Do NOT hand-write the `.cjs`; it is emitted. Tests
* `require()` the built artifact; `pretest` runs `build:lib` first.
*
* Pure and dependency-free: it classifies each requirement's data/behavior shape, filters

View File

@@ -8,7 +8,7 @@
* shape — ADR-1239 lists the wire-shape as an open question:
* "Exact wire-shape of the initialize handshake … Where precisely to cut the
* engine↔host boundary"
* (docs/adr/1239-gsd-embeddable-orchestration-engine.md#open-questions-narrowed-by-the-research).
* (docs/adr/1239-msd-embeddable-orchestration-engine.md#open-questions-narrowed-by-the-research).
* Freezing a 6-point contract before the imperative adapter exists would risk
* rework across Phases 3-6. This slice ships only what the declarative adapter
* (AC1) needs: the kind discriminator + runtime + install/uninstall entry.

View File

@@ -7,8 +7,8 @@
* lives here. Two leaf verbs (a pair, so leaves rather than a family per
* ADR-2346's ">=3 subcommands" rule):
*
* gsd-tools query estimate-check --tokens <n>
* gsd-tools query estimate-calibration
* msd-tools query estimate-check --tokens <n>
* msd-tools query estimate-calibration
*
* Both degrade rather than fail. A missing or corrupt
* `.planning/estimation-calibration.json` yields an inert calibration

View File

@@ -1,6 +1,6 @@
/**
* Deterministic eval scoring verb (#10).
* Moves coverage/infra/overall arithmetic out of the gsd-eval-auditor prompt
* Moves coverage/infra/overall arithmetic out of the msd-eval-auditor prompt
* into code, per the framework's code-delegation discipline.
*/
@@ -47,7 +47,7 @@ function cmdEvalScore(_cwd: string, args: string[], raw: boolean): void {
!Number.isFinite(covered) || !Number.isFinite(total) ||
infra.length !== 5
) {
process.stderr.write('Usage: gsd-tools query eval.score --covered N --total N --infra a,b,c,d,e (each ok|partial|missing)\n');
process.stderr.write('Usage: msd-tools query eval.score --covered N --total N --infra a,b,c,d,e (each ok|partial|missing)\n');
process.exitCode = 1;
return;
}

View File

@@ -1,11 +1,11 @@
// GENERATED FILE — DO NOT EDIT BY HAND.
// Source of truth: gsd-core/bin/shared/exit-codes.json + the ENTRY_FIELD_TYPES table in
// Source of truth: msd-core/bin/shared/exit-codes.json + the ENTRY_FIELD_TYPES table in
// scripts/gen-exit-code-registry.cjs. Regenerate with:
// node scripts/gen-exit-code-registry.cjs --write
//
// Ambient type declaration for exit-code-registry.cjs — a GENERATED,
// committed artifact with no `.cts` source of its own (it is hand-serialized
// from gsd-core/bin/shared/exit-codes.json by scripts/gen-exit-code-registry.cjs,
// from msd-core/bin/shared/exit-codes.json by scripts/gen-exit-code-registry.cjs,
// ADR-3889 §2, #3905/#3906), so tsc has nothing to compile for it. This file
// exists purely so `src/cli-exit.cts`'s `require('./exit-code-registry.cjs')`
// type-checks against the SAME shape the generated artifact actually exports

View File

@@ -11,7 +11,7 @@
* takes injected `fs` and `clock` seams so tests drive it without touching disk
* or wall-clock time (CLAUDE.md clock-seam + injectable-deps conventions).
*
* Build: `src/*.cts` -> `gsd-core/bin/lib/*.cjs` (ADR-457 build-at-publish).
* Build: `src/*.cts` -> `msd-core/bin/lib/*.cjs` (ADR-457 build-at-publish).
*/
import path from 'node:path';

View File

@@ -652,7 +652,7 @@ function countTopLevelKeyShapedLines(region: string): number {
* negative on a file truncated after exactly one key; the benefit is silence on a very common
* Markdown shape. That direction is deliberate and matches the choice already made at zero
* keys: a false positive on valid Markdown is worse than a missed edge, because the
* diagnostic is unconditional and cannot be turned off. Every GSD artefact this guards
* diagnostic is unconditional and cannot be turned off. Every MSD artefact this guards
* (STATE.md, PLAN.md, ROADMAP.md, SUMMARY.md, agent/command docs) carries two or more
* frontmatter keys, so the realistic interruption window stays covered.
*
@@ -921,7 +921,7 @@ function agentScalarNeedsDoubleQuoting(s: string): boolean {
* All-digit strings stay bare as a deliberate, scoped trade-off — not a
* safety guarantee. A leading-zero value (`02`, `017`) also mis-parses under
* a spec reader (to 2, 17). It is left unquoted because zero-padded ids
* (`plan: 01`, `phase: 02`) are the pervasive GSD convention, quoting them
* (`plan: 01`, `phase: 02`) are the pervasive MSD convention, quoting them
* all is the blanket quoting #4053 asked to avoid, and the loss is padding
* rather than identity: `02` and `2` normalize to the same phase; `22.1` and
* `22.10` do not.
@@ -1294,7 +1294,7 @@ function parseMustHavesBlock(content: string, blockName: string): unknown[] {
// to Option C (LLM-derived truths) instead of checking documented contracts.
if (block !== undefined && block !== null) {
process.stderr.write(
`[gsd-tools] WARNING: must_haves.${blockName} block has content but parsed 0 items. ` +
`[msd-tools] WARNING: must_haves.${blockName} block has content but parsed 0 items. ` +
`Possible YAML formatting issue — verification will fall back to LLM-derived truths.\n`
);
}
@@ -1349,7 +1349,7 @@ const FRONTMATTER_SCHEMAS: Record<string, { required: string[]; requiredValues?:
* (e.g. from a botched merge) is fully removed, not just the first block.
*
* Pass `{ once: true }` to stop after the first block. Callers whose input is
* an arbitrary user-authored document — rather than a GSD artefact with a
* an arbitrary user-authored document — rather than a MSD artefact with a
* known doubling failure mode — need this: a body that opens with a
* thematic-break-delimited section is lexically indistinguishable from a
* second frontmatter block, and the greedy loop deletes it silently (#2703).
@@ -1522,7 +1522,7 @@ function cmdFrontmatterValidate(cwd: string, filePath: string, schemaName: strin
// Object.prototype members instead of undefined, so a `!schema` check on the raw
// lookup never fires and the code crashes later on `schema.required.filter` with an
// uncaught TypeError instead of the intended "Unknown schema" message. Confirmed live
// with --schema __proto__. Now that --schema is agent-bound (agents/gsd-planner.md's
// with --schema __proto__. Now that --schema is agent-bound (agents/msd-planner.md's
// $SCHEMA), this is reachable from prompt state, not just an unreachable literal.
// Checked and rejected BEFORE the lookup (rather than `?? undefined`-ing the lookup
// itself) so `schema`'s inferred type stays non-optional and needs no assertion below.

View File

@@ -112,7 +112,7 @@ function parseRequirements(reqMd: unknown): ReqItem[] {
// The **ID** is extracted from the bullet text caller-side — the seam provides
// the raw text; we parse the bold-ID prefix from it here.
const boldIdRe = new RegExp(`^\\*\\*(${ID_PATTERN})\\*\\*\\s*(.*)$`);
// The shipped template (gsd-core/templates/requirements.md) writes
// The shipped template (msd-core/templates/requirements.md) writes
// `- [ ] **AUTH-01**: User can sign up` — a single separator delimiter
// between the bold ID and the description. Strip AT MOST ONE leading
// delimiter (plus its surrounding whitespace) before the final `.trim()`,

View File

@@ -6,10 +6,10 @@
* Background: the loop-resolver renders active gate hooks (carrying their
* `check.predicate` declarations) but, prior to #2008, nothing EVALUATED a
* declared predicate for non-built-in capabilities — `check.query` was the only
* enforced shape (dispatched via `gsd_run check <query>`), and `check.predicate`
* enforced shape (dispatched via `msd_run check <query>`), and `check.predicate`
* was declaration-only. This module is the generic evaluation path.
*
* The workflow gate-dispatch calls this evaluator (via the `gsd_run check predicate`
* The workflow gate-dispatch calls this evaluator (via the `msd_run check predicate`
* subcommand in check-command-router) for any gate whose `check` carries a
* `predicate` instead of a `query`. The evaluator dispatches by `predicate.kind`
* and returns the existing `{ block, message }` gate contract. A THROWN error

View File

@@ -80,7 +80,7 @@ interface EffectiveGitConfig {
/**
* `git.allow_default_branch_commits` escape hatch (#3819). When `true`, the
* resolved base branch is no longer auto-added to `protectedBranches` — for
* a project that legitimately runs GSD directly on its default branch.
* a project that legitimately runs MSD directly on its default branch.
* Explicitly configured `protected_branches` entries are unaffected: this
* flag narrows only the automatic base-branch protection, never a name the
* project named on purpose.
@@ -138,7 +138,7 @@ export function _readGitKey(config: Record<string, unknown>, field: string): unk
* it is deliberately narrower than `loadConfig`. It covers exactly two of
* production's steps — `normalizeLegacyKeys`, then the flat-then-nested lookup
* — over a single `<cwd>/.planning/config.json`. It does NOT apply
* root/workstream `_deepMergeConfig`, builtin or `~/.gsd/defaults.json`
* root/workstream `_deepMergeConfig`, builtin or `~/.msd/defaults.json`
* defaults, or `mergeFederatedConfig`. Tests that assert on any of those must
* drive `loadConfig` instead; the seam's own tests are scoped to normalization
* and shape validation, which is all it reproduces (#3648 review Major 3).
@@ -458,7 +458,7 @@ export function gitWorktreeInfoInternal(
/**
* Resolve the commit that ADDED `<phaseDir>/*-PLAN.md` — the anchor commit
* marking when the phase began (see `.gsd/phase/feat-1953-complexity-triggered-
* marking when the phase began (see `.msd/phase/feat-1953-complexity-triggered-
* refactor/42-router-contract.md`, "Touched-file anchor"). `phaseDir` is a
* project-relative path (backslashes are normalized unconditionally before
* building the pathspec, never via `path.sep` — matches the repo's
@@ -564,9 +564,9 @@ export function changedFilesSince(
// ─── CLI entry point ──────────────────────────────────────────────────────────
/**
* CLI command: `gsd-tools git base-branch`
* CLI command: `msd-tools git base-branch`
* Resolves the default branch and writes it to stdout (raw string, newline-terminated).
* Called by workflows via `gsd_run query git.base-branch`.
* Called by workflows via `msd_run query git.base-branch`.
*/
export function cmdGitBaseBranch(
cwd: string,

View File

@@ -1,9 +1,9 @@
'use strict';
/**
* Graphify command router — CLI subcommand dispatcher for `gsd-tools graphify`.
* Graphify command router — CLI subcommand dispatcher for `msd-tools graphify`.
*
* ADR-959 (phase 4d-impl-2) pilot: first real capability command cutover.
* Extracted from the hardcoded `case 'graphify':` arm in gsd-tools.cjs.
* Extracted from the hardcoded `case 'graphify':` arm in msd-tools.cjs.
* Behaviour is preserved byte-for-behaviour from the prior inline case;
* the dispatch path now flows: default → dispatchCapabilityCommand →
* require(graphify-command-router.cjs) → routeGraphifyCommand.
@@ -78,7 +78,7 @@ function routeGraphifyCommand({ args, cwd, raw, error, _graphify }: RouteGraphif
query: () => {
const term = args[2];
if (!term) {
return makeInvalidArgs('term', 'Usage: gsd-tools graphify query <term>', ERROR_REASON.USAGE);
return makeInvalidArgs('term', 'Usage: msd-tools graphify query <term>', ERROR_REASON.USAGE);
}
const budgetIdx = args.indexOf('--budget');
let budget: number | null = null;
@@ -87,7 +87,7 @@ function routeGraphifyCommand({ args, cwd, raw, error, _graphify }: RouteGraphif
if (rawBudget === undefined || Number.isNaN(parseInt(rawBudget, 10))) {
return makeInvalidArgs(
'--budget',
'Usage: gsd-tools graphify query <term> [--budget <N>]',
'Usage: msd-tools graphify query <term> [--budget <N>]',
ERROR_REASON.USAGE,
);
}

View File

@@ -37,7 +37,7 @@ interface DisabledResponse {
* Return the standard disabled response object.
*/
function disabledResponse(): DisabledResponse {
return { disabled: true, message: 'graphify is not enabled. Enable with: gsd-tools config-set graphify.enabled true' };
return { disabled: true, message: 'graphify is not enabled. Enable with: msd-tools config-set graphify.enabled true' };
}
// ─── Subprocess Helper ───────────────────────────────────────────────────────
@@ -555,7 +555,7 @@ function graphifyQuery(cwd: string, term: string, options: { budget?: number | n
const { graphPath, configured } = resolveGraphLocation(cwd, planningDir);
if (!fs.existsSync(graphPath)) {
return { error: configured
? `Configured graph not found at ${graphPath}. Set graphify.graph_path or run /gsd:graphify build.`
? `Configured graph not found at ${graphPath}. Set graphify.graph_path or run /msd:graphify build.`
: 'No graph built yet. Run graphify build first.' };
}
@@ -598,7 +598,7 @@ function graphifyStatus(cwd: string): unknown {
const { graphPath, configured } = resolveGraphLocation(cwd, planningDir);
if (!fs.existsSync(graphPath)) {
return { exists: false, graph_path: graphPath, message: configured
? `Configured graph not found at ${graphPath}. Set graphify.graph_path or run /gsd:graphify build.`
? `Configured graph not found at ${graphPath}. Set graphify.graph_path or run /msd:graphify build.`
: 'No graph built yet. Run graphify build to create one.' };
}
@@ -627,10 +627,10 @@ function graphifyStatus(cwd: string): unknown {
}
// Auto-update status (#3347). Read .last-build-status.json written by the
// hooks/gsd-graphify-update.sh PostToolUse hook (opt-in via graphify.auto_update,
// hooks/msd-graphify-update.sh PostToolUse hook (opt-in via graphify.auto_update,
// default false). When the most recent auto-build is "failed" or still "running",
// fold that into the existing `stale: true` signal so consumers (gsd-planner,
// gsd-phase-researcher) surface the standard "treat semantic relationships as
// fold that into the existing `stale: true` signal so consumers (msd-planner,
// msd-phase-researcher) surface the standard "treat semantic relationships as
// approximate" annotation without per-consumer prompt changes. The full state
// (running/failed/exit_code/duration_ms/head_at_build) is exposed under
// `last_build` for callers that want richer context.

View File

@@ -1,11 +1,11 @@
/**
* gsd2-import — Reverse migration from GSD-2 (.gsd/) to GSD v1 (.planning/)
* gsd2-import — Reverse migration from GSD-2 (.gsd/) to MSD v1 (.planning/)
*
* Reads a GSD-2 project directory structure and produces a complete
* .planning/ artifact tree in GSD v1 format.
* .planning/ artifact tree in MSD v1 format.
*
* GSD-2 hierarchy: Milestone → Slice → Task
* GSD v1 hierarchy: Milestone (in ROADMAP.md) → Phase → Plan
* MSD v1 hierarchy: Milestone (in ROADMAP.md) → Phase → Plan
*
* Mapping rules:
* - Slices are numbered sequentially across all milestones (01, 02, …)
@@ -22,7 +22,7 @@
import fs from 'node:fs';
import path from 'node:path';
import { platformWriteSync } from './shell-command-projection.cjs';
import { formatGsdSlash, resolveRuntime } from './runtime-slash.cjs';
import { formatMsdSlash, resolveRuntime } from './runtime-slash.cjs';
import { realClock } from './clock.cjs';
import { clampPercent, renderProgressBar } from './phase-lifecycle.cjs';
// eslint-disable-next-line @typescript-eslint/no-require-imports -- core-utils.cjs is an export= CommonJS module
@@ -71,7 +71,7 @@ interface Milestone {
slices: Slice[];
}
interface Gsd2Data {
interface Msd2Data {
projectContent: string | null;
requirements: string | null;
milestones: Milestone[];
@@ -108,7 +108,7 @@ function slugify(title: string): string {
* Find the .gsd/ directory starting from a project root.
* Returns the absolute path or null if not found.
*/
function findGsd2Root(startPath: string): string | null {
function findMsd2Root(startPath: string): string | null {
if (path.basename(startPath) === '.gsd' && fs.existsSync(startPath)) {
return startPath;
}
@@ -183,7 +183,7 @@ function parseTaskMustHaves(content: string): string[] {
// `tasks/` directory convention (`T##-PLAN.md`) during a one-time import —
// it is not this project's `.planning/phases/<phase>/` layout at all, has no
// nested-plans/superseded-status concept, and scanPhasePlans's grammar
// (which is scoped to GSD's OWN phase directories) does not apply here.
// (which is scoped to MSD's OWN phase directories) does not apply here.
function readTasksDir(tasksDir: string): TaskInfo[] {
if (!fs.existsSync(tasksDir)) return [];
@@ -209,14 +209,14 @@ function readTasksDir(tasksDir: string): TaskInfo[] {
/**
* Parse a complete GSD-2 .gsd/ directory into a structured representation.
*/
function parseGsd2(gsdDir: string): Gsd2Data {
const data: Gsd2Data = {
projectContent: readOptional(path.join(gsdDir, 'PROJECT.md')),
requirements: readOptional(path.join(gsdDir, 'REQUIREMENTS.md')),
function parseMsd2(msdDir: string): Msd2Data {
const data: Msd2Data = {
projectContent: readOptional(path.join(msdDir, 'PROJECT.md')),
requirements: readOptional(path.join(msdDir, 'REQUIREMENTS.md')),
milestones: [],
};
const milestonesBase = path.join(gsdDir, 'milestones');
const milestonesBase = path.join(msdDir, 'milestones');
if (!fs.existsSync(milestonesBase)) return data;
const milestoneIds = fs.readdirSync(milestonesBase)
@@ -259,7 +259,7 @@ function parseGsd2(gsdDir: string): Gsd2Data {
// ─── Artifact Builders ──────────────────────────────────────────────────────
/**
* Build a GSD v1 PLAN.md from a GSD-2 task.
* Build a MSD v1 PLAN.md from a GSD-2 task.
*/
function buildPlanMd(task: TaskInfo, phasePrefix: string, planPrefix: string, phaseSlug: string, milestoneTitle: string): string {
const lines = [
@@ -295,7 +295,7 @@ function buildPlanMd(task: TaskInfo, phasePrefix: string, planPrefix: string, ph
}
/**
* Build a GSD v1 SUMMARY.md from a GSD-2 task summary.
* Build a MSD v1 SUMMARY.md from a GSD-2 task summary.
* Strips the GSD-2 frontmatter and preserves the body.
*/
function buildSummaryMd(task: TaskInfo, phasePrefix: string, planPrefix: string): string {
@@ -311,7 +311,7 @@ function buildSummaryMd(task: TaskInfo, phasePrefix: string, planPrefix: string)
// `stripFrontmatter` is the same module's canonical body primitive.
//
// `once` is load-bearing. A GSD-2 summary is an arbitrary user-authored
// document, not a GSD artefact with a known frontmatter-doubling failure
// document, not a MSD artefact with a known frontmatter-doubling failure
// mode, so a body opening with a thematic-break-delimited section
// (`---` / heading / `---`) is far likelier than a corrupt second header —
// and the default greedy loop would delete it without a trace.
@@ -329,7 +329,7 @@ function buildSummaryMd(task: TaskInfo, phasePrefix: string, planPrefix: string)
}
/**
* Build a GSD v1 XX-CONTEXT.md from a GSD-2 slice.
* Build a MSD v1 XX-CONTEXT.md from a GSD-2 slice.
*/
function buildContextMd(slice: Slice, phasePrefix: string): string {
const lines = [
@@ -347,7 +347,7 @@ function buildContextMd(slice: Slice, phasePrefix: string): string {
}
/**
* Build the GSD v1 ROADMAP.md with milestone-sectioned format.
* Build the MSD v1 ROADMAP.md with milestone-sectioned format.
*/
function buildRoadmapMd(milestones: Milestone[], phaseMap: PhaseMapEntry[]): string {
const lines = ['# Roadmap', ''];
@@ -368,7 +368,7 @@ function buildRoadmapMd(milestones: Milestone[], phaseMap: PhaseMapEntry[]): str
}
/**
* Build the GSD v1 STATE.md reflecting the current position in the project.
* Build the MSD v1 STATE.md reflecting the current position in the project.
*/
function buildStateMd(phaseMap: PhaseMapEntry[]): string {
const currentEntry = phaseMap.find(p => !p.slice.done);
@@ -427,7 +427,7 @@ function buildStateMd(phaseMap: PhaseMapEntry[]): string {
* Convert parsed GSD-2 data into a map of relative path → file content.
* All paths are relative to the .planning/ root.
*/
function buildPlanningArtifacts(gsd2Data: Gsd2Data): Map<string, string> {
function buildPlanningArtifacts(gsd2Data: Msd2Data): Map<string, string> {
const artifacts = new Map<string, string>();
// Passthrough files
@@ -489,7 +489,7 @@ function buildPlanningArtifacts(gsd2Data: Gsd2Data): Map<string, string> {
/**
* Format a dry-run preview string for display before writing.
*/
function buildPreview(gsd2Data: Gsd2Data, artifacts: Map<string, string>, projectDir: string): string {
function buildPreview(gsd2Data: Msd2Data, artifacts: Map<string, string>, projectDir: string): string {
const lines = ['Preview — files that will be created in .planning/:'];
for (const rel of artifacts.keys()) {
@@ -508,7 +508,7 @@ function buildPreview(gsd2Data: Gsd2Data, artifacts: Map<string, string>, projec
lines.push('');
lines.push('Cannot migrate automatically:');
lines.push(' - GSD-2 cost/token ledger (no v1 equivalent)');
lines.push(` - GSD-2 database state (rebuilt from files on first ${formatGsdSlash('health', resolveRuntime(projectDir)) as string})`);
lines.push(` - GSD-2 database state (rebuilt from files on first ${formatMsdSlash('health', resolveRuntime(projectDir)) as string})`);
lines.push(' - VS Code extension state');
return lines.join('\n');
@@ -529,10 +529,10 @@ function writePlanningDir(artifacts: Map<string, string>, planningRoot: string):
// ─── Command Handler ──────────────────────────────────────────────────────────
/**
* Entry point called from gsd-tools.cjs.
* Entry point called from msd-tools.cjs.
* Supports: --force, --dry-run, --path <dir>
*/
function cmdFromGsd2(args: string[], cwd: string, raw: boolean): void {
function cmdFromMsd2(args: string[], cwd: string, raw: boolean): void {
const force = args.includes('--force');
const dryRun = args.includes('--dry-run');
@@ -541,13 +541,13 @@ function cmdFromGsd2(args: string[], cwd: string, raw: boolean): void {
? path.resolve(cwd, args[pathIdx + 1])
: cwd;
const gsdDir = findGsd2Root(projectDir);
if (!gsdDir) {
const msdDir = findMsd2Root(projectDir);
if (!msdDir) {
output({ success: false, error: `No .gsd/ directory found in ${projectDir}` }, raw, undefined);
return;
}
const planningRoot = path.join(path.dirname(gsdDir), '.planning');
const planningRoot = path.join(path.dirname(msdDir), '.planning');
if (fs.existsSync(planningRoot) && !force) {
output({
success: false,
@@ -556,7 +556,7 @@ function cmdFromGsd2(args: string[], cwd: string, raw: boolean): void {
return;
}
const gsd2Data = parseGsd2(gsdDir);
const gsd2Data = parseMsd2(msdDir);
const artifacts = buildPlanningArtifacts(gsd2Data);
// Use projectDir (resolved from --path) — not the process cwd — so the
// preview command targets the project actually being imported (#3584).
@@ -579,12 +579,12 @@ function cmdFromGsd2(args: string[], cwd: string, raw: boolean): void {
}
export = {
findGsd2Root,
parseGsd2,
findMsd2Root,
parseMsd2,
buildPlanningArtifacts,
buildPreview,
writePlanningDir,
cmdFromGsd2,
cmdFromMsd2,
// Exported for unit tests
parseSlicesFromRoadmap,
parseMilestoneTitle,

View File

@@ -27,7 +27,7 @@
import hostIntegration = require('./host-integration.cjs');
/** The wire method name for the initialize-style exchange. */
const HANDSHAKE_METHOD = 'gsd/host-initialize';
const HANDSHAKE_METHOD = 'msd/host-initialize';
/**
* Host side: build a JSON-serializable handshake request from a host descriptor.

View File

@@ -20,7 +20,7 @@
* returning no diagnostic for `UNREADABLE`, rather than inventing a new,
* more severe 5th case the original never had.
*
* Design: .gsd/phase/refactor-3309-health-diagnostic-rule-table/40-design.md
* Design: .msd/phase/refactor-3309-health-diagnostic-rule-table/40-design.md
* ("Rule table organization" — Agent installation group)
*/
@@ -62,8 +62,8 @@ function checkAgentInstall(snapshot: PlanningSnapshot): Diagnostic[] {
{
code: 'W010',
severity: SEVERITY.WARNING,
message: `No GSD agents found in ${status.agents_dir} — Task(subagent_type="gsd-*") will fall back to general-purpose`,
remedy: adviseRemedy(`Run the GSD installer: npx ${PACKAGE_NAME}@latest`),
message: `No MSD agents found in ${status.agents_dir} — Task(subagent_type="msd-*") will fall back to general-purpose`,
remedy: adviseRemedy(`Run the MSD installer: npx ${PACKAGE_NAME}@latest`),
},
];
}
@@ -75,7 +75,7 @@ function checkAgentInstall(snapshot: PlanningSnapshot): Diagnostic[] {
code: 'W010',
severity: SEVERITY.WARNING,
message: `Incomplete agent installs (missing generated file): ${status.incomplete_agents.join(', ')} — affected workflows may fall back to general-purpose`,
remedy: adviseRemedy(`Re-run the GSD installer to complete the install: npx ${PACKAGE_NAME}@latest`),
remedy: adviseRemedy(`Re-run the MSD installer to complete the install: npx ${PACKAGE_NAME}@latest`),
},
];
}
@@ -86,8 +86,8 @@ function checkAgentInstall(snapshot: PlanningSnapshot): Diagnostic[] {
{
code: 'W010',
severity: SEVERITY.WARNING,
message: `Missing ${status.missing_agents.length} GSD agents: ${status.missing_agents.join(', ')}; incomplete agent installs (missing generated file): ${status.incomplete_agents.join(', ')} — affected workflows will fall back to general-purpose`,
remedy: adviseRemedy(`Run the GSD installer: npx ${PACKAGE_NAME}@latest`),
message: `Missing ${status.missing_agents.length} MSD agents: ${status.missing_agents.join(', ')}; incomplete agent installs (missing generated file): ${status.incomplete_agents.join(', ')} — affected workflows will fall back to general-purpose`,
remedy: adviseRemedy(`Run the MSD installer: npx ${PACKAGE_NAME}@latest`),
},
];
}
@@ -97,8 +97,8 @@ function checkAgentInstall(snapshot: PlanningSnapshot): Diagnostic[] {
{
code: 'W010',
severity: SEVERITY.WARNING,
message: `Missing ${status.missing_agents.length} GSD agents: ${status.missing_agents.join(', ')} — affected workflows will fall back to general-purpose`,
remedy: adviseRemedy(`Run the GSD installer: npx ${PACKAGE_NAME}@latest`),
message: `Missing ${status.missing_agents.length} MSD agents: ${status.missing_agents.join(', ')} — affected workflows will fall back to general-purpose`,
remedy: adviseRemedy(`Run the MSD installer: npx ${PACKAGE_NAME}@latest`),
},
];
}
@@ -107,7 +107,7 @@ const RULES: Rule[] = [
{
code: 'W010',
severity: SEVERITY.WARNING,
description: 'GSD agent installation missing or incomplete',
description: 'MSD agent installation missing or incomplete',
repairable: false,
check: checkAgentInstall,
},

View File

@@ -35,18 +35,18 @@
* suffix rather than fabricate error text the snapshot never carried.
* - W016's original message interpolates `${slash('ai-integration-phase')}`
* (`verify.cts:1856`), a per-project runtime-resolved value
* (`formatGsdSlash`, `src/runtime-slash.cts`) this rule's `(snapshot) =>
* (`formatMsdSlash`, `src/runtime-slash.cts`) this rule's `(snapshot) =>
* Diagnostic[]` signature has no access to. Hardcodes the canonical
* `/gsd-ai-integration-phase` hyphen form instead, mirroring the sibling
* `/msd-ai-integration-phase` hyphen form instead, mirroring the sibling
* "Phase directory structure" group's W009 rule
* (`src/health-diagnostic-rules/phase-structure.cts`), which hardcodes
* `/gsd-plan-phase` the same way for the identical reason.
* `/msd-plan-phase` the same way for the identical reason.
*
* Design: .gsd/phase/refactor-3309-health-diagnostic-rule-table/40-design.md
* Design: .msd/phase/refactor-3309-health-diagnostic-rule-table/40-design.md
*
* ADR-457 build-at-publish: source in
* src/health-diagnostic-rules/config-validation.cts, compiled to
* gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs (gitignored).
* msd-core/bin/lib/health-diagnostic-rules/config-validation.cjs (gitignored).
*/
// eslint-disable-next-line @typescript-eslint/no-require-imports -- type-only; erased at compile time, no runtime require emitted
@@ -149,7 +149,7 @@ function checkW016(snapshot: PlanningSnapshot): Diagnostic[] {
code: 'W016',
severity: SEVERITY.WARNING,
message:
'config.json: workflow.ai_integration_phase absent (defaults to enabled — run /gsd-ai-integration-phase before planning AI system phases)',
'config.json: workflow.ai_integration_phase absent (defaults to enabled — run /msd-ai-integration-phase before planning AI system phases)',
remedy: { action: REMEDY_ACTION.ADD_AI_INTEGRATION_PHASE_KEY, risk: REMEDY_RISK.NONE, args: {} },
},
];

View File

@@ -23,11 +23,11 @@
* path in the pre-migration code either, matching Phase 11's own convention
* for message-only findings with no automated fix.
*
* Design: .gsd/phase/feat-3310-enhance-3180-the-sibling-validators-shar/40-design.md
* Test matrix: .gsd/phase/feat-3310-enhance-3180-the-sibling-validators-shar/50-test-matrix.md
* Design: .msd/phase/feat-3310-enhance-3180-the-sibling-validators-shar/40-design.md
* Test matrix: .msd/phase/feat-3310-enhance-3180-the-sibling-validators-shar/50-test-matrix.md
*
* ADR-457 build-at-publish: source in src/health-diagnostic-rules/consistency.cts,
* compiled to gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs (gitignored).
* compiled to msd-core/bin/lib/health-diagnostic-rules/consistency.cjs (gitignored).
*/
// eslint-disable-next-line @typescript-eslint/no-require-imports -- type-only; erased at compile time, no runtime require emitted

View File

@@ -1,9 +1,9 @@
/**
* Install Surface Shadowing rule (#2873, epic #2866 Phase 4a — governed by
* `.gsd/phase/feat-2873-cross-scope-shadowing/40-design.md`).
* `.msd/phase/feat-2873-cross-scope-shadowing/40-design.md`).
*
* One code, W028, surfacing `install-shadow-report.cts`'s `ShadowReport` as
* a `/gsd-health` diagnostic — the design doc's row #4 ("one diagnostic,
* a `/msd-health` diagnostic — the design doc's row #4 ("one diagnostic,
* severity WARNING, same projection" as the install-time report). WARNING
* severity, ADVISE remedy with `risk: NONE`: shadowing is never auto-fixable
* (there is no single correct scope to remove) so this is advisory only,
@@ -11,7 +11,7 @@
*
* Reaches OUTSIDE the planning snapshot the same way `agent-install.cts`
* does for W010: `resolveRuntime(cwd)` (`runtime-slash.cjs`) resolves the
* runtime id from `process.env.GSD_RUNTIME` / `config.runtime` / the
* runtime id from `process.env.MSD_RUNTIME` / `config.runtime` / the
* `'claude'` default (never throws), using `snapshot.cwd`
* (`planning-snapshot.cts`'s `buildPlanningSnapshot` — `path.resolve(cwd)`)
* as the project directory. `buildShadowReport` is then called with that
@@ -26,14 +26,14 @@
* vscode — design row #7) to `reason: RESOLVER_UNAVAILABLE`, which
* `renderShadowReport` renders as `[]`; the try/catch around both calls
* below additionally absorbs any other unexpected throw (design row D5),
* since this rule is advisory and must never make `/gsd-health` itself
* since this rule is advisory and must never make `/msd-health` itself
* fail.
*
* Design: .gsd/phase/feat-2873-cross-scope-shadowing/40-design.md
* Design: .msd/phase/feat-2873-cross-scope-shadowing/40-design.md
*
* ADR-457 build-at-publish: source in
* src/health-diagnostic-rules/install-surface-shadowing.cts, compiled to
* gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs
* msd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs
* (gitignored).
*/
@@ -80,7 +80,7 @@ function checkInstallSurfaceShadowing(snapshot: PlanningSnapshot): Diagnostic[]
} catch {
// Advisory rule: an unresolvable runtime, a `configHome.kind === 'none'`
// runtime, or any other unexpected failure degrades to "no finding",
// never a thrown exception that would break `/gsd-health` itself
// never a thrown exception that would break `/msd-health` itself
// (design row D5).
return [];
}
@@ -101,7 +101,7 @@ const RULES: Rule[] = [
{
code: 'W028',
severity: SEVERITY.WARNING,
description: 'A GSD-owned install scope shadows another on this machine',
description: 'A MSD-owned install scope shadows another on this machine',
repairable: false,
check: checkInstallSurfaceShadowing,
},

View File

@@ -8,11 +8,11 @@
* Ported behavior-preserving from `cmdValidateHealth`
* (`src/verify.cts:2301-2354`), the exact call sites for W018/W019.
*
* Design: .gsd/phase/refactor-3309-health-diagnostic-rule-table/40-design.md
* Design: .msd/phase/refactor-3309-health-diagnostic-rule-table/40-design.md
*
* ADR-457 build-at-publish: source in
* src/health-diagnostic-rules/milestone-archive-hygiene.cts, compiled to
* gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs
* msd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs
* (gitignored).
*/
@@ -84,7 +84,7 @@ function checkW019(snapshot: PlanningSnapshot): Diagnostic[] {
diagnostics.push({
code: 'W019',
severity: SEVERITY.WARNING,
message: `Unrecognized .planning/ file: ${filename} — not a canonical GSD artifact`,
message: `Unrecognized .planning/ file: ${filename} — not a canonical MSD artifact`,
remedy: adviseRemedy(
'Move to .planning/milestones/ archive subdir or delete if stale. See templates/README.md for the canonical artifact list.',
),
@@ -106,7 +106,7 @@ const RULES: Rule[] = [
{
code: 'W019',
severity: SEVERITY.WARNING,
description: 'Unrecognized .planning/ root file — not a canonical GSD artifact',
description: 'Unrecognized .planning/ root file — not a canonical MSD artifact',
repairable: false,
check: checkW019,
},

View File

@@ -36,19 +36,19 @@
*
* W009's original message interpolates `${slash('plan-phase')}`
* (`verify.cts:1982`, ``Re-run ${slash('plan-phase')} with --research to
* regenerate``), a per-project runtime-resolved value (`formatGsdSlash`,
* regenerate``), a per-project runtime-resolved value (`formatMsdSlash`,
* `src/runtime-slash.cts`) this rule's `(snapshot) => Diagnostic[]`
* signature has no access to. Hardcodes the canonical `/gsd-plan-phase`
* signature has no access to. Hardcodes the canonical `/msd-plan-phase`
* hyphen form instead, mirroring the sibling "config.json validation"
* group's W016 rule (`src/health-diagnostic-rules/config-validation.cts`),
* which hardcodes `/gsd-ai-integration-phase` the same way for the
* which hardcodes `/msd-ai-integration-phase` the same way for the
* identical reason.
*
* Design: .gsd/phase/refactor-3309-health-diagnostic-rule-table/40-design.md
* Design: .msd/phase/refactor-3309-health-diagnostic-rule-table/40-design.md
*
* ADR-457 build-at-publish: source in
* src/health-diagnostic-rules/phase-structure.cts, compiled to
* gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs (gitignored).
* msd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs (gitignored).
*/
// eslint-disable-next-line @typescript-eslint/no-require-imports -- type-only; erased at compile time, no runtime require emitted
@@ -207,7 +207,7 @@ function checkW009(snapshot: PlanningSnapshot): Diagnostic[] {
code: 'W009',
severity: SEVERITY.WARNING,
message: `Phase ${entry.dir}: has Validation Architecture in RESEARCH.md but no VALIDATION.md`,
remedy: adviseRemedy('Re-run /gsd-plan-phase with --research to regenerate'),
remedy: adviseRemedy('Re-run /msd-plan-phase with --research to regenerate'),
});
}
}

View File

@@ -70,7 +70,7 @@
* checklist-declared phase id regardless of checked state (confirmed by
* direct read: its `buildRoadmapPhaseVariants` call includes BOTH `[x]` and
* `[ ]` checklist entries). Omitting the exclusion here would regress a
* COMMON case: `gsd-core/templates/roadmap.md`'s "Initial Roadmap" shape
* COMMON case: `msd-core/templates/roadmap.md`'s "Initial Roadmap" shape
* declares every phase as an unchecked `- [ ] **Phase N: [Name]**` checklist
* item before any phase directory exists, so a freshly created ROADMAP.md
* would immediately spam one W006 per phase. `snapshot.roadmapPhaseCheckboxes`
@@ -88,11 +88,11 @@
* is the conservative direction (a possible false W006, not a suppressed
* true one).
*
* Design: .gsd/phase/refactor-3309-health-diagnostic-rule-table/40-design.md
* Design: .msd/phase/refactor-3309-health-diagnostic-rule-table/40-design.md
*
* ADR-457 build-at-publish: source in
* src/health-diagnostic-rules/roadmap-disk-consistency.cts, compiled to
* gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs
* msd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs
* (gitignored).
*/
@@ -161,7 +161,7 @@ function dirsForPhase(dirs: string[], phaseId: string, convention: string | null
return dirs.filter((d) => {
// #612: the same convention the primary matcher above got. This fallback
// compares a DIRECTORY-derived token against phase-id variants, so left
// convention-less it reads `GSD.02-05-slug` with the legacy grammar and can
// convention-less it reads `MSD.02-05-slug` with the legacy grammar and can
// never agree with a bracket phase id — the fallback would then be dead on
// precisely the convention it is reached for.
const token = extractPhaseToken(d, convention).toUpperCase();
@@ -221,13 +221,13 @@ function checkW006(snapshot: PlanningSnapshot): Diagnostic[] {
//
// #612 `roadmapSentinelPhaseTokens` — bracket sentinels. Under the
// bracket convention the sentinel-ness lives in the MILESTONE, not
// the token: the heading `### [GSD.999] 07:` yields phase id `07`,
// the token: the heading `### [MSD.999] 07:` yields phase id `07`,
// so a bare-token test cannot see it. The snapshot's
// `buildRoadmapPhaseVariants` call carries the bracket id alongside
// and reports the token here. Empty unless the bracket convention
// is active, so no legacy repo's reading changes.
// #3225 `isSentinelPhaseId` — the legacy/bare leading-int rule (999.x/0.x),
// which sees `### Phase 999:` and is blind to `[GSD.999] 07`.
// which sees `### Phase 999:` and is blind to `[MSD.999] 07`.
//
// Upstream's call is kept one-arg verbatim: every `phaseId` here is a bare
// token, never a `{CODE}.{MM}`-qualified id, so a convention argument could
@@ -289,12 +289,12 @@ function checkW007(snapshot: PlanningSnapshot): Diagnostic[] {
// #612: `collectDiskPhaseEntries` keyed that map through the
// convention-aware extractor, so the token this message names must be
// derived the same way — otherwise a bracket repo's unclaimed
// `GSD.02-05-slug` is reported as phase `GSD.02` rather than `05`.
// `MSD.02-05-slug` is reported as phase `MSD.02` rather than `05`.
const token = extractPhaseToken(dirName, convention);
// #3225: a sentinel dir on disk (999-interim, 0-drafts) is defined as
// never-on-roadmap; it must not trigger W007. #3639: judged on the DIR
// NAME via the dir-aware recognizer — the extracted token is
// milestone-stripped, so a bracket sentinel (GSD.999-07-icebox) was
// milestone-stripped, so a bracket sentinel (MSD.999-07-icebox) was
// invisible to the id predicate and false-fired as an orphan.
if (isSentinelPhaseDir(dirName)) continue;
if (claimedDirs.has(dirName)) continue;

View File

@@ -14,18 +14,18 @@
* (`verify.cts:1682`, ``Run ${slash('new-project')} to create``) and E003's
* interpolates `${slash('new-milestone')}` (`verify.cts:1694`, ``Run
* ${slash('new-milestone')} to create roadmap``) — both per-project
* runtime-resolved values (`formatGsdSlash`, `src/runtime-slash.cts`) this
* runtime-resolved values (`formatMsdSlash`, `src/runtime-slash.cts`) this
* rule's `(snapshot) => Diagnostic[]` signature has no access to (§8.1 rule
* 1 forbids ambient I/O, including `cwd`, inside `check`). Hardcodes the
* canonical `/gsd-new-project`/`/gsd-new-milestone` hyphen form instead,
* canonical `/msd-new-project`/`/msd-new-milestone` hyphen form instead,
* mirroring the sibling "config.json validation" group's W016 rule
* (`src/health-diagnostic-rules/config-validation.cts`), which hardcodes
* `/gsd-ai-integration-phase` the same way for the identical reason.
* `/msd-ai-integration-phase` the same way for the identical reason.
*
* Design: .gsd/phase/refactor-3309-health-diagnostic-rule-table/40-design.md
* Design: .msd/phase/refactor-3309-health-diagnostic-rule-table/40-design.md
*
* ADR-457 build-at-publish: source in src/health-diagnostic-rules/root-existence.cts,
* compiled to gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs (gitignored).
* compiled to msd-core/bin/lib/health-diagnostic-rules/root-existence.cjs (gitignored).
*/
// eslint-disable-next-line @typescript-eslint/no-require-imports -- type-only; erased at compile time, no runtime require emitted
@@ -52,7 +52,7 @@ function checkE002(snapshot: PlanningSnapshot): Diagnostic[] {
code: 'E002',
severity: SEVERITY.ERROR,
message: 'PROJECT.md not found',
remedy: adviseRemedy('/gsd-new-project'),
remedy: adviseRemedy('/msd-new-project'),
},
];
}
@@ -82,7 +82,7 @@ function checkE003(snapshot: PlanningSnapshot): Diagnostic[] {
code: 'E003',
severity: SEVERITY.ERROR,
message: 'ROADMAP.md not found',
remedy: adviseRemedy('/gsd-new-milestone'),
remedy: adviseRemedy('/msd-new-milestone'),
},
];
}

View File

@@ -27,14 +27,14 @@
* - W002's original message interpolates `${slash('health')}`
* (`verify.cts:1770`) and W011's interpolates `${slash('progress')}`
* (`verify.cts:2126`) — both per-project runtime-resolved values
* (`formatGsdSlash`, `src/runtime-slash.cts`) this rule's
* (`formatMsdSlash`, `src/runtime-slash.cts`) this rule's
* `(snapshot) => Diagnostic[]` signature has no access to. Hardcodes the
* canonical `/gsd-health`/`/gsd-progress` hyphen form instead, mirroring
* canonical `/msd-health`/`/msd-progress` hyphen form instead, mirroring
* the sibling "config.json validation" group's W016 rule
* (`src/health-diagnostic-rules/config-validation.cts`), which hardcodes
* `/gsd-ai-integration-phase` the same way for the identical reason.
* `/msd-ai-integration-phase` the same way for the identical reason.
*
* Design: .gsd/phase/refactor-3309-health-diagnostic-rule-table/40-design.md
* Design: .msd/phase/refactor-3309-health-diagnostic-rule-table/40-design.md
*/
// Runtime values (SEVERITY/REMEDY_ACTION/REMEDY_RISK) are needed here, not
@@ -128,7 +128,7 @@ function buildValidPhaseSet(snapshot: PlanningSnapshot): Set<string> {
// #612: the pre-migration source of this third was
// `collectDiskPhases(planBase, convention)`, whose keys came from the
// convention-aware extractor. Left convention-less here, a bracket repo's
// `GSD.02-05-slug` contributes `GSD.02` instead of `05`, so every STATE.md
// `MSD.02-05-slug` contributes `MSD.02` instead of `05`, so every STATE.md
// reference to a real phase falls outside the valid set and W002 fires on
// all of them.
const token = extractPhaseToken(dir, snapshot.phaseIdConvention);
@@ -197,7 +197,7 @@ const RULE_W002: Rule = {
severity: SEVERITY.WARNING,
message: `STATE.md references phase ${ref}, but only phases ${sortedValid.join(', ')} are declared${scopeClause}`,
remedy: adviseRemedy(
'Review STATE.md manually before changing it; /gsd-health --repair will not overwrite an existing STATE.md for phase mismatches',
'Review STATE.md manually before changing it; /msd-health --repair will not overwrite an existing STATE.md for phase mismatches',
),
});
}
@@ -219,7 +219,7 @@ const RULE_W002: Rule = {
* so extracting its leading number is the equivalent-intent read against
* the template STATE.md actually ships. #3280: the label's ladder
* (`buildStateFields`, `src/planning-snapshot.cts`) now leads with the
* frontmatter `current_phase` scalar — the key `gsd-tools state update` /
* frontmatter `current_phase` scalar — the key `msd-tools state update` /
* `state begin-phase` persist — so a bare `"2"` is the expected value on
* the machine-readable format, and this leading-number extraction handles
* it identically.
@@ -255,7 +255,7 @@ const RULE_W011: Rule = {
code: 'W011',
severity: SEVERITY.WARNING,
message: `STATE.md says current phase is ${phaseId} (status: ${statusVal.toLowerCase() || 'unknown'}) but ROADMAP.md shows it as [x] complete — state files may be out of sync`,
remedy: adviseRemedy('Run /gsd-progress to re-derive current position, or manually update STATE.md'),
remedy: adviseRemedy('Run /msd-progress to re-derive current position, or manually update STATE.md'),
},
];
},
@@ -335,7 +335,7 @@ function checkMilestonePrefixW021(snapshot: PlanningSnapshot): Diagnostic[] {
code: 'W021',
severity: SEVERITY.WARNING,
message: `Phase ${entry.phaseId}: integer prefix implies ${expectedMilestone} but listed under ${entry.milestone}`,
remedy: adviseRemedy('gsd-tools roadmap upgrade --convention milestone-prefixed'),
remedy: adviseRemedy('msd-tools roadmap upgrade --convention milestone-prefixed'),
});
}
return diagnostics;

View File

@@ -28,11 +28,11 @@
* (`verify.cts:2233-2242`) via `PlanningSnapshot.cwd` — see `checkW027`'s own
* comment below for the mechanism.
*
* Design: .gsd/phase/refactor-3309-health-diagnostic-rule-table/40-design.md
* Design: .msd/phase/refactor-3309-health-diagnostic-rule-table/40-design.md
*
* ADR-457 build-at-publish: source in
* src/health-diagnostic-rules/worktree-health.cts, compiled to
* gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs (gitignored).
* msd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs (gitignored).
*/
// eslint-disable-next-line @typescript-eslint/no-require-imports

View File

@@ -13,11 +13,11 @@
* back `undefined`. This leaf has NO runtime dependency on anything in that
* cycle, so both sides can depend on it directly.
*
* Design: .gsd/phase/refactor-3309-health-diagnostic-rule-table/40-design.md
* Test matrix: .gsd/phase/refactor-3309-health-diagnostic-rule-table/50-test-matrix.md
* Design: .msd/phase/refactor-3309-health-diagnostic-rule-table/40-design.md
* Test matrix: .msd/phase/refactor-3309-health-diagnostic-rule-table/50-test-matrix.md
*
* ADR-457 build-at-publish: source in src/health-diagnostic-types.cts,
* compiled to gsd-core/bin/lib/health-diagnostic-types.cjs (gitignored).
* compiled to msd-core/bin/lib/health-diagnostic-types.cjs (gitignored).
*/
// eslint-disable-next-line @typescript-eslint/no-require-imports -- type-only; erased at compile time, no runtime require emitted
@@ -79,7 +79,7 @@ interface Rule {
severity: Severity;
/**
* Short, static, human-readable summary of what this rule checks — the
* source of `gsd-core/workflows/health.md`'s generated `<error_codes>`
* source of `msd-core/workflows/health.md`'s generated `<error_codes>`
* table (`scripts/gen-health-docs.cjs`). Deliberately distinct from a
* fired `Diagnostic`'s `message`, which is dynamic/per-instance (e.g.
* W001's message names the specific PROJECT.md section that is missing);

View File

@@ -28,11 +28,11 @@
* at compile time — zero changes to the already-shipped, already-tested
* `planning-snapshot.cts`.
*
* Design: .gsd/phase/refactor-3309-health-diagnostic-rule-table/40-design.md
* Test matrix: .gsd/phase/refactor-3309-health-diagnostic-rule-table/50-test-matrix.md
* Design: .msd/phase/refactor-3309-health-diagnostic-rule-table/40-design.md
* Test matrix: .msd/phase/refactor-3309-health-diagnostic-rule-table/50-test-matrix.md
*
* ADR-457 build-at-publish: source in src/health-diagnostic.cts, compiled to
* gsd-core/bin/lib/health-diagnostic.cjs (gitignored).
* msd-core/bin/lib/health-diagnostic.cjs (gitignored).
*/
import fs from 'node:fs';
@@ -137,7 +137,7 @@ import stateTransitionMod = require('./state-transition.cjs');
const { rebuildStateTransaction } = stateTransitionMod;
import { realClock } from './clock.cjs';
import { platformReadSync as safeReadFile, platformWriteSync } from './shell-command-projection.cjs';
import { formatGsdSlash, resolveRuntime } from './runtime-slash.cjs';
import { formatMsdSlash, resolveRuntime } from './runtime-slash.cjs';
// ─── Evaluator ──────────────────────────────────────────────────────────────
@@ -217,9 +217,9 @@ interface RepairPaths {
*/
function repairPaths(cwd: string): RepairPaths {
// #3749: the project root is PROJECT-aware but deliberately NOT workstream-
// scoped — under GSD_PROJECT, config.json/MILESTONES.md/milestones belong to
// scoped — under MSD_PROJECT, config.json/MILESTONES.md/milestones belong to
// `.planning/<project>/` (the same base init.new-project's config_path
// names); under GSD_WORKSTREAM they stay at the workstream PARENT, keeping
// names); under MSD_WORKSTREAM they stay at the workstream PARENT, keeping
// the documented root-vs-workstream split. planningDir(cwd, null) is exactly
// that base: project env honored, ws env suppressed.
const rootBase = planningDir(cwd, null);
@@ -339,7 +339,7 @@ function runRepairAction(cwd: string, action: RemedyAction, paths: RepairPaths):
.split(path.sep)
.join('/');
const slashRuntime = resolveRuntime(cwd);
const slash = (name: string) => formatGsdSlash(name, slashRuntime) as string;
const slash = (name: string) => formatMsdSlash(name, slashRuntime) as string;
let stateContent = `# Session State\n\n`;
stateContent += `## Project Reference\n\n`;
stateContent += `See: ${projectRef}\n\n`;
@@ -349,7 +349,7 @@ function runRepairAction(cwd: string, action: RemedyAction, paths: RepairPaths):
stateContent += `**Status:** Resuming\n\n`;
stateContent += `## Session Log\n\n`;
stateContent += `- ${realClock.localToday()}: STATE.md regenerated by ${slash('health')} --repair\n`;
// ADR-3473 §8.6: `/gsd-health --repair` is a factory reset, so
// ADR-3473 §8.6: `/msd-health --repair` is a factory reset, so
// preservation must not run; the snapshot of the file being replaced is
// still captured, and `{}` is the correct, legal snapshot of a
// STATE.md that had no parseable frontmatter — which is the usual
@@ -379,7 +379,7 @@ function runRepairAction(cwd: string, action: RemedyAction, paths: RepairPaths):
const missing = computeMissingMilestoneVersions(milestonesArchiveDir, milestonesPath);
const today = realClock.localToday();
const slashRuntime = resolveRuntime(cwd);
const slash = (name: string) => formatGsdSlash(name, slashRuntime) as string;
const slash = (name: string) => formatMsdSlash(name, slashRuntime) as string;
let backfilled = 0;
for (const ver of missing) {
try {

View File

@@ -4,11 +4,11 @@
* The lifecycle-hook ownership model, selected by the negotiated `hookBus`
* axis (host-integration.cts):
*
* - `engine` — GSD owns the bus internally (in-process pub/sub). Used by
* - `engine` — MSD owns the bus internally (in-process pub/sub). Used by
* hosts that have no event bus (VS Code). Full subscribe + emit.
* - `host` — the host fires events; GSD subscribes. Handlers register
* - `host` — the host fires events; MSD subscribes. Handlers register
* locally for a Phase-5 host binding to dispatch to; `emit` delegates to a
* host-supplied emitter (fail-closed until bound — GSD does not drive a
* host-supplied emitter (fail-closed until bound — MSD does not drive a
* host-owned bus).
* - `none` — no bus (Cline-rules). Degrades to rule-text instructions;
* subscribe/emit are no-ops.
@@ -38,7 +38,7 @@ export interface HookBusAdapter {
}
export interface CreateHookBusOptions {
/** Required for `host`: the host's emit primitive (GSD emits → host bus). */
/** Required for `host`: the host's emit primitive (MSD emits → host bus). */
hostEmit?: (event: string, payload?: unknown) => void;
}
@@ -75,12 +75,12 @@ export function createHookBus(
},
});
}
// host: GSD subscribes; emits go to the host-supplied emitter (fail-closed until bound).
// host: MSD subscribes; emits go to the host-supplied emitter (fail-closed until bound).
const hostEmit = options.hostEmit;
return Object.freeze({
bus: 'host',
subscribe(_event: string, _handler: (payload?: unknown) => void) {
// Host owns the bus; GSD's subscriptions are dispatched by a Phase-5 host
// Host owns the bus; MSD's subscriptions are dispatched by a Phase-5 host
// binding that calls the registered handlers when the host fires events.
// Stored host-side; locally this is a seam until that binding lands.
},

View File

@@ -17,7 +17,7 @@
* https://github.com/cline/cline/blob/main/sdk/packages/agents/README.md
*
* UPGRADE 2 — `DefaultGateway.createAgentModel({providerId, modelId})`.
* Resolves GSD's per-subagent `model_overrides` / `model_profile_overrides`
* Resolves MSD's per-subagent `model_overrides` / `model_profile_overrides`
* (already used for OpenCode/Codex passive hosts) into the createAgentModel
* call params for cline's active model mode. The host gateway owns the
* actual model instantiation; this binding resolves WHICH model an
@@ -58,7 +58,7 @@ export const PLANNING_PATH_PATTERN = /(^|[\\/])\.planning([\\/]|$)/;
* identically to the user (cancel→skip, errorMessage→reason).
*/
export const PLANNING_GUARD_REASON: string = Object.freeze(
'GSD: .planning/ artifacts are managed by GSD workflows. Edit them only through a /gsd-* command, not directly.',
'MSD: .planning/ artifacts are managed by MSD workflows. Edit them only through a /msd-* command, not directly.',
);
/**
@@ -148,11 +148,11 @@ export function evaluateBeforeTool(payload: BeforeToolPayload | null | undefined
* `{skip:true, reason}` to block or `undefined` to allow.
*
* This object is the portable plugin a host loads from `~/.cline/plugins/`
* (analogous to `.opencode/plugins/gsd-core.js`). Its `beforeTool` delegates to
* (analogous to `.opencode/plugins/msd-core.js`). Its `beforeTool` delegates to
* the pure `evaluateBeforeTool` so the decision logic is testable without the
* SDK linked.
*/
export const clineGsdPlugin: {
export const clineMsdPlugin: {
name: string;
setup: (ctx: { agentId?: string }) => {
hooks: {
@@ -160,7 +160,7 @@ export const clineGsdPlugin: {
};
};
} = Object.freeze({
name: 'gsd-planning-guard',
name: 'msd-planning-guard',
setup(_ctx: { agentId?: string }) {
return {
hooks: {
@@ -207,12 +207,12 @@ type ProfileOverrideMap = Record<string, Record<string, string>> | null | undefi
type AgentModelParams = { providerId: string; modelId: string };
/**
* Resolve the createAgentModel call params for a cline subagent from GSD's model
* Resolve the createAgentModel call params for a cline subagent from MSD's model
* override config. Mirrors the precedence OpenCode/Codex use (passive hosts
* embed the resolved model into agent frontmatter); for cline's active model
* mode the same resolution flows to `gateway.createAgentModel(params)`.
*
* Precedence (matches GSD's model_overrides > model_profile_overrides contract):
* Precedence (matches MSD's model_overrides > model_profile_overrides contract):
* 1. `modelOverrides[agentType]` — direct per-agent override
* 2. `modelProfileOverrides[profile][agentType]` — profile-scoped override
* 3. null — no override; the host gateway applies its own default
@@ -220,7 +220,7 @@ type AgentModelParams = { providerId: string; modelId: string };
* Pure; never throws. Non-string / empty override values are ignored (fail-safe).
*
* @returns the `{providerId, modelId}` for createAgentModel, or null when no
* override is configured (the gateway default applies — GSD does NOT
* override is configured (the gateway default applies — MSD does NOT
* call createAgentModel in that case).
*/
export function resolveClineAgentModelParams(args: {

View File

@@ -21,14 +21,14 @@
* afterMCPExecution, afterFileEdit, preCompact, stop,
* beforeTabFileRead, afterTabFileEdit, workspaceOpen
*
* GSD registers for the 6 events in the portable floor + subagent lifecycle
* MSD registers for the 6 events in the portable floor + subagent lifecycle
* (AC4a upgrade, #2089):
* sessionStart, postToolUse, preToolUse, stop, subagentStart, subagentStop
*/
'use strict';
/**
* The full set of Cursor hook events GSD can register for.
* The full set of Cursor hook events MSD can register for.
* Frozen closed vocabulary — adding an event requires updating both this set
* and the event→script mapping below.
*/
@@ -47,24 +47,24 @@ export type CursorHookEvent = (typeof CURSOR_HOOK_EVENTS)[number];
* Event → hook-script mapping. Each event maps to a standalone `.js` script
* under `hooks/` that Cursor invokes via `hooks.json`.
*
* Convention: `gsd-cursor-<kebab-event>.js`. The script files are authored in
* Convention: `msd-cursor-<kebab-event>.js`. The script files are authored in
* `hooks/` and copied to `<configDir>/hooks/` during install by
* `runtime-hooks-surface.cts`.
*/
export const CURSOR_EVENT_SCRIPT_MAP: Readonly<Record<string, string>> = Object.freeze({
sessionStart: 'gsd-cursor-session-start.js',
postToolUse: 'gsd-cursor-post-tool.js',
preToolUse: 'gsd-cursor-pre-tool.js',
stop: 'gsd-cursor-stop.js',
subagentStart: 'gsd-cursor-subagent-start.js',
subagentStop: 'gsd-cursor-subagent-stop.js',
sessionStart: 'msd-cursor-session-start.js',
postToolUse: 'msd-cursor-post-tool.js',
preToolUse: 'msd-cursor-pre-tool.js',
stop: 'msd-cursor-stop.js',
subagentStart: 'msd-cursor-subagent-start.js',
subagentStop: 'msd-cursor-subagent-stop.js',
});
/**
* The GSD-managed marker written into each hooks.json entry so the
* reconcile pass can distinguish GSD-owned entries from user-owned ones.
* The MSD-managed marker written into each hooks.json entry so the
* reconcile pass can distinguish MSD-owned entries from user-owned ones.
*/
export const GSD_HOOK_MARKER = 'gsd-managed';
export const MSD_HOOK_MARKER = 'msd-managed';
/**
* Resolve the managed hook events from a runtime descriptor's
@@ -105,7 +105,7 @@ export function resolveManagedHookEvents(
* Pure: returns a deduplicated array of script filenames.
*
* @param events - the managed event names (validated by `resolveManagedHookEvents`)
* @returns array of script filenames (e.g. `['gsd-cursor-session-start.js', ...]`)
* @returns array of script filenames (e.g. `['msd-cursor-session-start.js', ...]`)
*/
export function resolveHookScripts(
events: readonly string[],
@@ -124,7 +124,7 @@ export function resolveHookScripts(
/**
* Build the hooks.json managed-entry manifest for the given events.
* Each entry is `{ type: 'command', command: <cmd>, [GSD_HOOK_MARKER]: true }`.
* Each entry is `{ type: 'command', command: <cmd>, [MSD_HOOK_MARKER]: true }`.
*
* The `command` string is built by the caller (it requires platform-specific
* node-runner resolution from `runtime-hooks-surface.cts`). This function
@@ -147,7 +147,7 @@ export function buildHookBusEntries(
entries[ev] = {
type: 'command',
command: cmd,
[GSD_HOOK_MARKER]: true,
[MSD_HOOK_MARKER]: true,
};
}
}

View File

@@ -3,9 +3,9 @@
*
* External host-plugin authors import ONLY from this entry. It IS the contract:
* everything it re-exports is public + versioned (PROTOCOL_VERSION governs the
* set); everything else in gsd-core is internal. An SDK smoke test
* set); everything else in msd-core is internal. An SDK smoke test
* (tests/sdk-smoke.test.cjs) builds a new host-plugin against this surface only
* — proving an external author can wire a host without reading gsd-core internals.
* — proving an external author can wire a host without reading msd-core internals.
*
* Surface: the negotiated schema + classification, the five adapters
* (declarative/imperative/model/hook/state), and the serialized handshake.

View File

@@ -57,11 +57,11 @@ const HOST_INTEGRATION_AXES = Object.freeze({
// ADR-1239 Codex-binding amendment (#2584): a `dispatch` sub-field — not a new
// axis — declaring how a host isolates concurrent same-wave executors.
// `harness-worktree` — the host's own harness creates + binds a git worktree
// per executor; GSD passes the host's own isolation flag and calls no git
// per executor; MSD passes the host's own isolation flag and calls no git
// itself (host-driven fan-out).
// `orchestrator-worktree` — GSD itself process-spawns each executor with an
// explicit working directory into a worktree GSD created, validated, and
// merges (GSD-driven fan-out; concurrency is OS-level, not the host's).
// `orchestrator-worktree` — MSD itself process-spawns each executor with an
// explicit working directory into a worktree MSD created, validated, and
// merges (MSD-driven fan-out; concurrency is OS-level, not the host's).
// `none` — no isolation primitive; same-wave plans run inline/sequentially
// (the #853 flatten rule).
// `undocumented` is NOT a member here; it is the corpus-wide sentinel above.
@@ -232,7 +232,7 @@ function degradationFor(point: InterfacePoint, axes: Partial<HostIntegrationAxes
case 'model': {
// NOTE (#2481): the effortSurface axis is deliberately NOT folded into this
// level. `modelMode` has graded interface point 3 since Phase A, and every
// existing consumer reads it as "can GSD drive model selection". Widening it
// existing consumer reads it as "can MSD drive model selection". Widening it
// to also mean "…and deliver effort" silently redefines an established
// contract — an `active` host with no declared effort surface would flip
// from `full` to `absent`. Effort is negotiated on its own axis and read
@@ -332,7 +332,7 @@ interface NegotiationResult {
/**
* True iff `v` is a positive (>0) JS safe integer. Single source of truth for
* the dispatch.maxConcurrency contract: used by negotiateHostCapabilities
* below, and by gsd-core/bin/gsd-tools.cjs's routeDispatchCapacity (which
* below, and by msd-core/bin/msd-tools.cjs's routeDispatchCapacity (which
* requires this module's compiled output rather than reimplementing the
* predicate), so the two consumers cannot silently diverge.
*/
@@ -499,7 +499,7 @@ function negotiateHostCapabilities(
// isolation: effective = the host's declared value only if it is a known
// valid vocabulary member; otherwise 'none'. NOT host && engine gated —
// GSD owns the vocabulary, so "engine-known" == "in the valid set" (this
// MSD owns the vocabulary, so "engine-known" == "in the valid set" (this
// still satisfies effective ⊆ host-declared ∩ engine-known).
const hostIso = hostDispatch.isolation;
effectiveIsolation = (typeof hostIso === 'string' && (HOST_INTEGRATION_AXES.isolation as readonly string[]).includes(hostIso))
@@ -678,11 +678,11 @@ function shouldFlattenDispatch(dispatch: UnvalidatedDispatch): boolean {
// ---------------------------------------------------------------------------
// resolveDispatchType — ADR-1239 / epic #2505 Phase 4 (Option A)
//
// Maps a requested GSD subagent name (e.g. "gsd-planner") to the type an
// Maps a requested MSD subagent name (e.g. "msd-planner") to the type an
// Agent() call should actually use on the CURRENT runtime. On runtimes whose
// descriptor declares `hostIntegration.dispatch.namedDispatch: true` (Claude,
// OpenCode, Cursor, …), the requested name is returned unchanged — those hosts
// can dispatch GSD's named subagents directly. On runtimes with
// can dispatch MSD's named subagents directly. On runtimes with
// `namedDispatch: false` (kimi-code — only three built-in subagents
// `coder`/`explore`/`plan`, per moonshotai.github.io/kimi-code/en/customization/
// agents), the name is mapped to the closest built-in by role-suffix
@@ -691,12 +691,12 @@ function shouldFlattenDispatch(dispatch: UnvalidatedDispatch): boolean {
// dispatcher does not need to know the persona — only the toolkit tier.
//
// This is Option A of the Phase 4 design (per-workflow runtime detection via
// `gsd_run query resolve-dispatch-type`), not Option B (PreToolUse mutation) —
// `msd_run query resolve-dispatch-type`), not Option B (PreToolUse mutation) —
// Kimi Code's documented hook API supports only allow/deny on PreToolUse, not
// tool_input rewriting, so a hook-based remap is infeasible (see #2508).
//
// Fail-closed: unknown dispatch shape or missing namedDispatch axis ⇒ return
// the requested name unchanged (named-dispatch is the GSD default; degrading
// the requested name unchanged (named-dispatch is the MSD default; degrading
// to it on unknown runtimes preserves behavior for every runtime already in
// the field).
// ---------------------------------------------------------------------------
@@ -710,7 +710,7 @@ const DISPATCH_TYPE_SUFFIX_MAP: ReadonlyArray<readonly [RegExp, string]> = Objec
[/-?(researcher|mapper|checker|verifier|auditor|analyzer|synthesizer|profiler|curator|classifier|reviewer)$/i, 'explore'],
]);
// Names that are already generic (not gsd-*) and should map to the
// Names that are already generic (not msd-*) and should map to the
// general-purpose built-in on built-in-only runtimes.
const GENERIC_NAMES_TO_CODER: ReadonlySet<string> = Object.freeze(new Set([
'general-purpose', 'general', 'default', 'sonnet', 'opus', 'haiku',
@@ -729,7 +729,7 @@ function resolveDispatchType(requested: unknown, dispatch: UnvalidatedDispatch):
}
// Named-dispatch runtime (namedDispatch: true OR unknown/absent): use the
// requested name unchanged. Absent namedDispatch degrades to named-dispatch
// (the GSD default) so every runtime already in the field keeps working.
// (the MSD default) so every runtime already in the field keeps working.
return requested;
}
@@ -738,7 +738,7 @@ function resolveDispatchType(requested: unknown, dispatch: UnvalidatedDispatch):
// ---------------------------------------------------------------------------
// Host-fireable MANAGED-hook events per `hookEvents` dialect. `hookEvents` is the
// managed-hook dialect — the event names GSD writes into a DECLARATIVE host's
// managed-hook dialect — the event names MSD writes into a DECLARATIVE host's
// settings.json (claude = SessionStart/PreToolUse/…; gemini = BeforeTool/AfterTool).
// This is DISTINCT from the extension-system event surface (below): a host's
// plugin/extension API fires a different, plugin-owned event set. The two must
@@ -763,7 +763,7 @@ function hookEventSurfaceFor(hookEvents: unknown): readonly string[] | null {
// ---------------------------------------------------------------------------
// The events a host's PLUGIN/EXTENSION API exposes — for imperative-embedding
// hosts that load GSD as a plugin. This is a SEPARATE vocabulary + descriptor
// hosts that load MSD as a plugin. This is a SEPARATE vocabulary + descriptor
// field (`extensionEvents`) from `hookEvents`: hookEvents = the managed-hook
// dialect (declarative hosts' settings.json); extensionEvents = the plugin-owned
// event subset (imperative hosts' extension API). They are not the same thing.
@@ -773,13 +773,13 @@ function hookEventSurfaceFor(hookEvents: unknown): readonly string[] | null {
// 'none' = the host exposes no extension surface and the engine owns the bus
// (VS Code). Declarative hosts (no plugin API) do not set `extensionEvents`.
// OpenCode's plugin event surface (ADR-1239 §research; ~25 documented events,
// GSD binds this subset). Hoisted to a named const — rather than duplicated
// MSD binds this subset). Hoisted to a named const — rather than duplicated
// object literals — so the `kilo` dialect below (#2093) can reuse the IDENTICAL
// array instead of a copy-pasted one that could silently drift out of sync.
const OPENCODE_EXTENSION_EVENTS = Object.freeze([
'session.created', 'session.idle', 'experimental.session.compacting',
'tool.execute.before', 'tool.execute.after', 'file.edited',
// #2087 — additional documented plugin events GSD binds (opencode.ai/docs/plugins):
// #2087 — additional documented plugin events MSD binds (opencode.ai/docs/plugins):
// permission decisions + session error surface.
'permission.asked', 'permission.replied', 'session.error',
]);
@@ -789,8 +789,8 @@ const EXTENSION_EVENT_SURFACES: Readonly<Record<string, readonly string[]>> = Ob
// #2093 — Kilo Code is an OpenCode fork sharing the same plugin/extension
// event bus (host hook bus, UPGRADE 1): reuses OPENCODE_EXTENSION_EVENTS
// verbatim (not a re-derivation), so the two dialects stay pinned together
// by construction. See .kilo/plugins/gsd-core.js (copied verbatim from
// .opencode/plugins/gsd-core.js).
// by construction. See .kilo/plugins/msd-core.js (copied verbatim from
// .opencode/plugins/msd-core.js).
kilo: OPENCODE_EXTENSION_EVENTS,
// #2091 — Hermes Agent real plugin hook vocabulary (13 events).
// Cite: https://github.com/nousresearch/hermes-agent/blob/main/website/docs/user-guide/features/hooks.md
@@ -808,7 +808,7 @@ const EXTENSION_EVENT_SURFACES: Readonly<Record<string, readonly string[]>> = Ob
// #2102 Stage 2 — pi's real ExtensionAPI event vocabulary (~30 fine-grained
// extension events; documentation-sourced, ADR-1239 §research). Replaces the
// placeholder single-event ['tool_call'] surface — the Stage 1 value only
// covered the one event pi/gsd.cjs happened to bind at the time, not the
// covered the one event pi/msd.cjs happened to bind at the time, not the
// full declared surface.
pi: Object.freeze([
'session_start', 'project_trust', 'resources_discover', 'input',
@@ -840,8 +840,8 @@ function extensionEventSurfaceFor(extensionEvents: unknown): readonly string[] |
// resolveOrchestratorExec — ADR-1239 Codex-binding amendment (#2584), Phase 2
//
// The `orchestratorExec` descriptor field (sibling of `runtime.hostBehaviors`
// in capability.json) tells GSD how to process-spawn a host's own CLI as the
// executor inside a worktree GSD itself created (`isolation:
// in capability.json) tells MSD how to process-spawn a host's own CLI as the
// executor inside a worktree MSD itself created (`isolation:
// 'orchestrator-worktree'`). Pure, no I/O — this only shapes the argv/cwd a
// caller would pass to a process-spawn primitive; it does not spawn anything
// itself. UNCONSUMED in Phase 2 — no scheduler calls this yet (Phase 3 wires

View File

@@ -3,11 +3,11 @@
*
* `init`'s reported `agent_runtime` was hardcoding `claude` even when run
* inside a Codex session, because resolveRuntime's ladder only checks the
* explicit `GSD_RUNTIME` env var and `.planning/config.json`'s `runtime`
* explicit `MSD_RUNTIME` env var and `.planning/config.json`'s `runtime`
* field — there was no fallback that looked at the actual host process.
*
* This module adds a host-detection rung strictly BELOW those two explicit
* sources: it only runs when neither `GSD_RUNTIME` nor config `runtime` is
* sources: it only runs when neither `MSD_RUNTIME` nor config `runtime` is
* set. It never writes anything (#2297 — no shared-defaults poisoning: this
* module never touches .planning/config.json or any other file). It never
* shells out, so there is no subprocess to time-bound — detection is pure
@@ -79,7 +79,7 @@ const NO_DETECTION: HostRuntimeDetection = { runtime: null, source: 'none', sign
/**
* Detect the host runtime from process environment signals, without ever
* consulting the explicit GSD_RUNTIME/config.json sources (those are a
* consulting the explicit MSD_RUNTIME/config.json sources (those are a
* higher-priority rung handled by resolveExplicitRuntime).
*
* Never throws: the whole body — including the raw `env[key]` reads, which a

View File

@@ -1,10 +1,10 @@
/**
* Manifest-backed init subcommand router.
* Keeps gsd-tools.cjs thin while preserving existing command semantics.
* Keeps msd-tools.cjs thin while preserving existing command semantics.
*
* Phase 6: all init.* subcommands have SDK equivalents and are dispatched
* via executeForCjs (the sync bridge). CJS fallback retained when:
* - GSD_WORKSTREAM is active (workstream-scoped requests fall through to CJS).
* - MSD_WORKSTREAM is active (workstream-scoped requests fall through to CJS).
* - SDK is unavailable (build not present).
*
* CJS-only subcommands: none.
@@ -136,9 +136,9 @@ function routeInitCommand({ init, args, cwd, raw, error }: RouteInitCommandOptio
'execute-phase': () => {
const norm = normalizePhaseAlias(args, error);
// `wave` is an optionalValueFlags entry, not a booleanFlags entry:
// `--wave N` is a documented, shipped form (commands/gsd/execute-phase.md:4,48)
// `--wave N` is a documented, shipped form (commands/msd/execute-phase.md:4,48)
// whose value is consumed by the workflow layer
// (gsd-core/workflows/execute-phase.md:84), not by this CLI seam — see
// (msd-core/workflows/execute-phase.md:84), not by this CLI seam — see
// NamedArgSpec.optionalValueFlags in command-arg-projection.cts.
const namedArgs = parseNamedArgsOrExit(norm.args, { booleanFlags: ['validate', 'tdd'], optionalValueFlags: ['wave'], positionals: 3 }, error);
init.cmdInitExecutePhase(cwd, norm.phase, raw, {
@@ -187,7 +187,7 @@ function routeInitCommand({ init, args, cwd, raw, error }: RouteInitCommandOptio
// #3180 Decision 4a / L2 (ADR-3473 §8.4): `positionals: 'rest'` because
// everything after `init quick` is a free-text description — strict
// undeclared-flag rejection would break
// `/gsd-quick add a --dry-run option`, which works today.
// `/msd-quick add a --dry-run option`, which works today.
const namedArgs = parseNamedArgsOrExit(args, { booleanFlags: ['discuss', 'research', 'validate', 'full'], positionals: 'rest' }, error);
// #2994: `args.slice(2)` is the free-text description, but section-manifest
// gating (buildSectionManifestField, src/init.cts) now requires forwarding
@@ -229,7 +229,7 @@ function routeInitCommand({ init, args, cwd, raw, error }: RouteInitCommandOptio
// are declared because none are documented for these subcommands
// (docs/CLI-TOOLS.md); `--ws` seen in shipped workflows targets the
// separate `query init.verify-work` seam and is stripped before
// reaching `init verify-work` (gsd-core/workflows/verify-work.md:42-45).
// reaching `init verify-work` (msd-core/workflows/verify-work.md:42-45).
'verify-work': () => {
const norm = normalizePhaseAlias(args, error);
parseNamedArgsOrExit(norm.args, { positionals: 3 }, error);
@@ -267,7 +267,7 @@ function routeInitCommand({ init, args, cwd, raw, error }: RouteInitCommandOptio
init.cmdInitProgress(cwd, raw, { forensic: namedArgs['forensic'] });
},
// Keep manager on CJS for now so runtime-specific command rendering
// (e.g. $gsd-* for codex) stays consistent with runtime-slash helpers.
// (e.g. $msd-* for codex) stays consistent with runtime-slash helpers.
manager: () => init.cmdInitManager(cwd, raw),
'complete-milestone': () => init.cmdInitCompleteMilestone(cwd, raw),
autonomous: () => {

View File

@@ -42,7 +42,7 @@ import scanPhasePlans = require('./plan-scan.cjs');
// eslint-disable-next-line @typescript-eslint/no-require-imports -- plan-document.cjs is an export= CommonJS module
import planDocument = require('./plan-document.cjs');
import { stateExtractField } from './state-document.cjs';
import { formatGsdSlash, resolveRuntime } from './runtime-slash.cjs';
import { formatMsdSlash, resolveRuntime } from './runtime-slash.cjs';
import { resolveReportedRuntime } from './host-runtime-detection.cjs';
// eslint-disable-next-line @typescript-eslint/no-require-imports -- commands.cjs is an export= CommonJS module
import commandsMod = require('./commands.cjs');
@@ -364,9 +364,9 @@ function withProjectRoot(cwd: string, result: Record<string, unknown>): Record<s
}
// #4455 follow-up (self-discovered): PROJECT.md is shared across a
// project's own workstreams (never cloned per workstream) but DOES
// respect the separate GSD_PROJECT multi-project namespace (#3749) — see
// respect the separate MSD_PROJECT multi-project namespace (#3749) — see
// cmdInitCompleteMilestone's projectPath comment for the full evidence.
// `ws` explicitly nulled, `project` left to default from GSD_PROJECT.
// `ws` explicitly nulled, `project` left to default from MSD_PROJECT.
// Reading via the workstream-aware planningDir(cwd) meant every init.*
// call's project_title silently vanished whenever a workstream was
// active, since no PROJECT.md ever exists at the workstream path.
@@ -455,16 +455,16 @@ function getInitGitState(cwd: string): GitState {
}
// #2932 (Phase 5, ADR-1671): shipped, generated artifact — see
// scripts/gen-section-manifest.cjs and gsd-core/workflows/section-manifest.json.
// scripts/gen-section-manifest.cjs and msd-core/workflows/section-manifest.json.
// Resolved the same way model-catalog.cts resolves model-catalog.json: relative
// to the compiled module's own directory (gsd-core/bin/lib -> gsd-core/workflows),
// with a GSD_SECTION_MANIFEST env override so tests can point at a temp fixture
// to the compiled module's own directory (msd-core/bin/lib -> msd-core/workflows),
// with a MSD_SECTION_MANIFEST env override so tests can point at a temp fixture
// (missing/malformed-JSON degraded-path coverage) without mutating the shipped
// artifact — the shipped file is a shared, concurrently-read resource across
// parallel test runs and must never be moved/corrupted in place.
const _sectionManifestCandidatePath = (): string =>
process.env['GSD_SECTION_MANIFEST']
? path.resolve(process.env['GSD_SECTION_MANIFEST'])
process.env['MSD_SECTION_MANIFEST']
? path.resolve(process.env['MSD_SECTION_MANIFEST'])
: path.resolve(__dirname, '..', '..', 'workflows', 'section-manifest.json');
/** A manifest entry as shipped on disk: {@link sectionManifest.SelectableSection} plus the `read` step-file path. */
@@ -635,7 +635,7 @@ function readConfigJsonValue(cwd: string, keyPath: readonly string[]): unknown {
/**
* `state:fallow-enabled` ground truth (#2994): resolves `code-review.md`'s
* `structural_pre_pass` fallow config gate — previously re-derived INSIDE the
* gated section body itself (`gsd_run query config-get code_quality.fallow.*`),
* gated section body itself (`msd_run query config-get code_quality.fallow.*`),
* which is circular/self-disabling the moment a section is gated on a fact
* its own body computes (the same hazard `state:chunked-mode` /
* `state:ui-phase-active` document for a compound condition). Fail-closed
@@ -665,7 +665,7 @@ function detectFallowConfig(cwd: string): {
/**
* `state:git-create-tag` ground truth (#2994): resolves `complete-milestone.md`'s
* `git_tag` step config gate — previously re-derived INSIDE a `<config-check>`
* sub-tag at the top of the step itself (`gsd-tools.cjs query config-get
* sub-tag at the top of the step itself (`msd-tools.cjs query config-get
* git.create_tag 2>/dev/null || echo "true"`), gating the step's OWN inclusion
* on a fact only that same step computed. Fail-OPEN default `true` (an unset
* or missing `git.create_tag` key means "create the tag"), matching the
@@ -737,7 +737,7 @@ function detectUiPhaseActive(cwd: string, phaseInfo: Record<string, unknown> | n
const state = resolveCapabilityRuntimeState(cwd, undefined, config) as {
capabilities: Array<{ id: string; enabled?: boolean; active: boolean }>;
};
const registry = loadRegistry({ includeInstalled: true, cwd, gsdHome: process.env['GSD_HOME'] });
const registry = loadRegistry({ includeInstalled: true, cwd, msdHome: process.env['MSD_HOME'] });
const capabilityStatesById = new Map<string, { enabled?: boolean; active: boolean }>();
for (const cap of state.capabilities || []) {
capabilityStatesById.set(cap.id, cap);
@@ -1007,8 +1007,8 @@ function cmdInitExecutePhase(
const threatIdDuplicates = findDuplicateThreatIds(cwd, phaseInfo?.['directory'] as string | undefined);
const result: Record<string, unknown> = {
executor_model: resolveModelInternal(cwd, 'gsd-executor'),
verifier_model: resolveModelInternal(cwd, 'gsd-verifier'),
executor_model: resolveModelInternal(cwd, 'msd-executor'),
verifier_model: resolveModelInternal(cwd, 'msd-verifier'),
tdd_mode: options['tdd'] || Boolean(config.tdd_mode) || false,
commit_docs: config.commit_docs,
@@ -1102,8 +1102,8 @@ function cmdInitExecutePhase(
// planningDir(cwd, null) on the assumption that config.json is shared
// like PROJECT.md (workstream-flag.md's directory diagram marks it
// `# Shared`) — but ADR-0006's own tests (tests/init.test.cjs, "init
// handlers honor GSD_WORKSTREAM") assert config_path IS workstream-scoped
// for execute-phase/new-project/new-milestone/progress, and gsd-test
// handlers honor MSD_WORKSTREAM") assert config_path IS workstream-scoped
// for execute-phase/new-project/new-milestone/progress, and msd-test
// caught the regression immediately. The diagram is stale for
// config.json specifically (same class of staleness already found for
// `milestones/` during the #4455 follow-up) — reverted to the
@@ -1111,7 +1111,7 @@ function cmdInitExecutePhase(
// ADR-governed, tested contract.
config_path: toPosixPath(path.join(planningDir(cwd), 'config.json')),
// #2376: execute-phase.md's verify_phase_goal step reads this instead of
// hardcoding '.planning/REQUIREMENTS.md' into the gsd-verifier spawn prompt.
// hardcoding '.planning/REQUIREMENTS.md' into the msd-verifier spawn prompt.
requirements_path: fs.existsSync(requirementsPath) ? toPosixPath(requirementsPath) : null,
};
@@ -1217,9 +1217,9 @@ function cmdInitPlanPhase(
const requirementsPath = path.join(planningDir(cwd), 'REQUIREMENTS.md');
const result: Record<string, unknown> = {
researcher_model: resolveModelInternal(cwd, 'gsd-phase-researcher'),
planner_model: resolveModelInternal(cwd, 'gsd-planner'),
checker_model: resolveModelInternal(cwd, 'gsd-plan-checker'),
researcher_model: resolveModelInternal(cwd, 'msd-phase-researcher'),
planner_model: resolveModelInternal(cwd, 'msd-planner'),
checker_model: resolveModelInternal(cwd, 'msd-plan-checker'),
tdd_mode: options['tdd'] || Boolean(config.tdd_mode) || false,
granularity,
@@ -1469,13 +1469,13 @@ function cmdInitNewProject(cwd: string, raw: boolean, options: Record<string, un
const config = loadConfig(cwd);
const homedir = os.homedir();
const braveKeyFile = path.join(homedir, '.gsd', 'brave_api_key');
const braveKeyFile = path.join(homedir, '.msd', 'brave_api_key');
const hasBraveSearch = !!(process.env['BRAVE_API_KEY'] || fs.existsSync(braveKeyFile));
const firecrawlKeyFile = path.join(homedir, '.gsd', 'firecrawl_api_key');
const firecrawlKeyFile = path.join(homedir, '.msd', 'firecrawl_api_key');
const hasFirecrawl = !!(process.env['FIRECRAWL_API_KEY'] || fs.existsSync(firecrawlKeyFile));
const exaKeyFile = path.join(homedir, '.gsd', 'exa_api_key');
const exaKeyFile = path.join(homedir, '.msd', 'exa_api_key');
const hasExaSearch = !!(process.env['EXA_API_KEY'] || fs.existsSync(exaKeyFile));
const hasCode = hasCodeFilesInternal(cwd);
@@ -1485,9 +1485,9 @@ function cmdInitNewProject(cwd: string, raw: boolean, options: Record<string, un
const hasCodebaseMap = codebaseMapFiles.length === REQUIRED_CODEBASE_MAP_FILES.length;
const result: Record<string, unknown> = {
researcher_model: resolveModelInternal(cwd, 'gsd-project-researcher'),
synthesizer_model: resolveModelInternal(cwd, 'gsd-research-synthesizer'),
roadmapper_model: resolveModelInternal(cwd, 'gsd-roadmapper'),
researcher_model: resolveModelInternal(cwd, 'msd-project-researcher'),
synthesizer_model: resolveModelInternal(cwd, 'msd-research-synthesizer'),
roadmapper_model: resolveModelInternal(cwd, 'msd-roadmapper'),
commit_docs: config.commit_docs,
@@ -1534,8 +1534,8 @@ function cmdInitNewProject(cwd: string, raw: boolean, options: Record<string, un
// planningDir(cwd, null) on the assumption that config.json is shared
// like PROJECT.md (workstream-flag.md's directory diagram marks it
// `# Shared`) — but ADR-0006's own tests (tests/init.test.cjs, "init
// handlers honor GSD_WORKSTREAM") assert config_path IS workstream-scoped
// for execute-phase/new-project/new-milestone/progress, and gsd-test
// handlers honor MSD_WORKSTREAM") assert config_path IS workstream-scoped
// for execute-phase/new-project/new-milestone/progress, and msd-test
// caught the regression immediately. The diagram is stale for
// config.json specifically (same class of staleness already found for
// `milestones/` during the #4455 follow-up) — reverted to the
@@ -1568,9 +1568,9 @@ function cmdInitNewMilestone(cwd: string, raw: boolean, options: Record<string,
const phaseDirCount = listMilestonePhaseDirs(phasesDir, { cwd }).value.length;
const result: Record<string, unknown> = {
researcher_model: resolveModelInternal(cwd, 'gsd-project-researcher'),
synthesizer_model: resolveModelInternal(cwd, 'gsd-research-synthesizer'),
roadmapper_model: resolveModelInternal(cwd, 'gsd-roadmapper'),
researcher_model: resolveModelInternal(cwd, 'msd-project-researcher'),
synthesizer_model: resolveModelInternal(cwd, 'msd-research-synthesizer'),
roadmapper_model: resolveModelInternal(cwd, 'msd-roadmapper'),
commit_docs: config.commit_docs,
research_enabled: config.research,
@@ -1609,8 +1609,8 @@ function cmdInitNewMilestone(cwd: string, raw: boolean, options: Record<string,
// planningDir(cwd, null) on the assumption that config.json is shared
// like PROJECT.md (workstream-flag.md's directory diagram marks it
// `# Shared`) — but ADR-0006's own tests (tests/init.test.cjs, "init
// handlers honor GSD_WORKSTREAM") assert config_path IS workstream-scoped
// for execute-phase/new-project/new-milestone/progress, and gsd-test
// handlers honor MSD_WORKSTREAM") assert config_path IS workstream-scoped
// for execute-phase/new-project/new-milestone/progress, and msd-test
// caught the regression immediately. The diagram is stale for
// config.json specifically (same class of staleness already found for
// `milestones/` during the #4455 follow-up) — reverted to the
@@ -1641,7 +1641,7 @@ function cmdInitNewMilestone(cwd: string, raw: boolean, options: Record<string,
// SEPARATE, positively-phrased atom whose fact is the inverse is the
// sanctioned resolution (same discipline as `state:chunked-mode` folding
// an OR — never an operator in the grammar itself). Same authoritative
// source as `cmdInitTransition`: `GSD_WORKSTREAM` env, falling back to the
// source as `cmdInitTransition`: `MSD_WORKSTREAM` env, falling back to the
// stored active-workstream pointer (mirrors `cmdInitProgress`'s own
// resolution above).
//
@@ -1691,17 +1691,17 @@ function cmdInitQuick(
: null;
const result: Record<string, unknown> = {
planner_model: resolveModelInternal(cwd, 'gsd-planner'),
executor_model: resolveModelInternal(cwd, 'gsd-executor'),
checker_model: resolveModelInternal(cwd, 'gsd-plan-checker'),
verifier_model: resolveModelInternal(cwd, 'gsd-verifier'),
// #3936: Step 4.75 dispatches gsd-phase-researcher; resolve its own tier
planner_model: resolveModelInternal(cwd, 'msd-planner'),
executor_model: resolveModelInternal(cwd, 'msd-executor'),
checker_model: resolveModelInternal(cwd, 'msd-plan-checker'),
verifier_model: resolveModelInternal(cwd, 'msd-verifier'),
// #3936: Step 4.75 dispatches msd-phase-researcher; resolve its own tier
// (parity with cmdInitPlanPhase) so the research spawn stops pinning
// planner_model.
researcher_model: resolveModelInternal(cwd, 'gsd-phase-researcher'),
// #2072: the quick review step spawns gsd-code-reviewer; resolve its own model
researcher_model: resolveModelInternal(cwd, 'msd-phase-researcher'),
// #2072: the quick review step spawns msd-code-reviewer; resolve its own model
// so model_overrides / models.verification apply (was reusing executor_model).
reviewer_model: resolveModelInternal(cwd, 'gsd-code-reviewer'),
reviewer_model: resolveModelInternal(cwd, 'msd-code-reviewer'),
commit_docs: config.commit_docs,
branch_name: quickBranchName,
@@ -1731,7 +1731,7 @@ function cmdInitQuick(
// builds its `InvocationFacts.flags` Set, so `discussion-phase`/`research-phase`/
// `plan-checker-loop`/`quick-verification` (all gated on their own single
// `flag:--discuss`/`flag:--research`/`flag:--validate` atom) include correctly
// for a bare `/gsd:quick --full` invocation that never passed the individual
// for a bare `/msd:quick --full` invocation that never passed the individual
// tokens — the grammar still sees exactly one atom per marker, no OR.
const sectionManifestOptions: Record<string, unknown> = options['full']
? { ...options, discuss: true, research: true, validate: true }
@@ -1768,12 +1768,12 @@ function cmdInitQuickBatch(
const config = loadConfig(cwd);
const result: Record<string, unknown> = {
planner_model: resolveModelInternal(cwd, 'gsd-planner'),
executor_model: resolveModelInternal(cwd, 'gsd-executor'),
checker_model: resolveModelInternal(cwd, 'gsd-plan-checker'),
verifier_model: resolveModelInternal(cwd, 'gsd-verifier'),
researcher_model: resolveModelInternal(cwd, 'gsd-phase-researcher'),
reviewer_model: resolveModelInternal(cwd, 'gsd-code-reviewer'),
planner_model: resolveModelInternal(cwd, 'msd-planner'),
executor_model: resolveModelInternal(cwd, 'msd-executor'),
checker_model: resolveModelInternal(cwd, 'msd-plan-checker'),
verifier_model: resolveModelInternal(cwd, 'msd-verifier'),
researcher_model: resolveModelInternal(cwd, 'msd-phase-researcher'),
reviewer_model: resolveModelInternal(cwd, 'msd-code-reviewer'),
commit_docs: config.commit_docs,
@@ -1804,7 +1804,7 @@ function cmdInitIngestDocs(cwd: string, raw: boolean): void {
...getInitGitState(cwd),
// #2376: absolute — see comment on phase_dir in cmdInitExecutePhase. The
// classify_parallel/synthesize/route_new_mode spawns in ingest-docs.md
// (gsd-doc-classifier, gsd-doc-synthesizer, gsd-roadmapper) previously
// (msd-doc-classifier, msd-doc-synthesizer, msd-roadmapper) previously
// hardcoded bare '.planning/intel/...', '.planning/PROJECT.md', etc.
// literals into their Agent(prompt=...) blocks; those now interpolate
// these fields instead.
@@ -1922,8 +1922,8 @@ function cmdInitVerifyWork(cwd: string, phase: string, raw: boolean): void {
const uiPhaseActive = detectUiPhaseActive(cwd, phaseInfo);
const result: Record<string, unknown> = {
planner_model: resolveModelInternal(cwd, 'gsd-planner'),
checker_model: resolveModelInternal(cwd, 'gsd-plan-checker'),
planner_model: resolveModelInternal(cwd, 'msd-planner'),
checker_model: resolveModelInternal(cwd, 'msd-plan-checker'),
commit_docs: config.commit_docs,
@@ -1949,7 +1949,7 @@ function cmdInitVerifyWork(cwd: string, phase: string, raw: boolean): void {
// #2994 (resolver-hoist-guard G5): hoisted `state:ui-phase-active` ground
// truth (previously re-derived inline inside the automated_ui_verification
// step body via its own `gsd_run loop render-hooks plan:pre --raw` call —
// step body via its own `msd_run loop render-hooks plan:pre --raw` call —
// a circular, self-disabling resolver, since the section is only read
// when this same fact is already true). Resolved once here, exposed so
// the step body can consume it directly instead of recomputing it.
@@ -2140,7 +2140,7 @@ function cmdInitReview(
* carried: `state:auto-advance-active` — the workflow's own `auto_advance`
* step resolves `--auto` OR a consolidated `check auto-mode --pick active`
* fact (itself `workflow._auto_chain_active` OR `workflow.auto_advance`) via
* a runtime `gsd_run` call; that identical disjunction is folded into ONE
* a runtime `msd_run` call; that identical disjunction is folded into ONE
* boolean FACT here (same discipline as `state:chunked-mode` /
* `state:plan-strategy-converge`), exposed as `auto_advance_active`.
*/
@@ -2427,7 +2427,7 @@ function cmdInitPhaseOp(cwd: string, phase: string, raw: boolean): void {
// 240 matches the bound already vetted by maintainer review on the prior
// attempt at this issue (PR #2662) — re-deriving a different number would be
// pure bikeshedding, not a correctness improvement. See
// .gsd/phase/feat-2618-compact-todo-pointers/40-design.md.
// .msd/phase/feat-2618-compact-todo-pointers/40-design.md.
const PENDING_TODO_BULLET_MAX_CHARS = 240;
const PENDING_TODO_TITLE_FLOOR = 15;
const PENDING_TODO_AREA_FLOOR = 3;
@@ -2448,7 +2448,7 @@ function truncatePendingTodoText(value: string, maxLen: number): string {
/**
* #2618: pure renderer for STATE.md's "### Pending Todos" section BODY (not
* the heading). One bullet per todo, each capped at
* PENDING_TODO_BULLET_MAX_CHARS. `gsd-core/workflows/add-todo.md` and
* PENDING_TODO_BULLET_MAX_CHARS. `msd-core/workflows/add-todo.md` and
* `check-todos.md` splice this string in verbatim instead of free-hand
* editing STATE.md — see the design doc for why this is real, unit-tested
* code rather than a prose algorithm (DEFECT.GENERATIVE-FIX: a prose
@@ -2802,7 +2802,7 @@ function cmdInitMapCodebase(cwd: string, raw: boolean): void {
// below (and verify.cts's codebase drift check) — has_maps/existing_maps
// reading the flat root made the same payload claim a scoped codebase dir
// exists while reporting zero maps, so map-codebase's Refresh/Skip gate
// always forced a re-map under GSD_PROJECT.
// always forced a re-map under MSD_PROJECT.
const codebaseDir = path.join(planningDir(cwd), 'codebase');
let existingMaps: string[] = [];
try {
@@ -2812,7 +2812,7 @@ function cmdInitMapCodebase(cwd: string, raw: boolean): void {
}
const result: Record<string, unknown> = {
mapper_model: resolveModelInternal(cwd, 'gsd-codebase-mapper'),
mapper_model: resolveModelInternal(cwd, 'msd-codebase-mapper'),
commit_docs: config.commit_docs,
search_gitignored: config.search_gitignored,
@@ -2825,7 +2825,7 @@ function cmdInitMapCodebase(cwd: string, raw: boolean): void {
// #2376: absolute — see comment on phase_dir in cmdInitExecutePhase.
// #3964: scoped like verify.cts's codebase drift check (planningDir, not
// the flat planningRoot) so the two surfaces cannot disagree under
// GSD_PROJECT.
// MSD_PROJECT.
codebase_dir: toPosixPath(path.join(planningDir(cwd), 'codebase')),
existing_maps: existingMaps,
@@ -2857,10 +2857,10 @@ function cmdInitManager(cwd: string, raw: boolean): void {
const paths = planningPaths(cwd);
if (!fs.existsSync(paths.roadmap)) {
error(`No ROADMAP.md found. Run ${formatGsdSlash('new-milestone', _slashRuntime) as string} first.`);
error(`No ROADMAP.md found. Run ${formatMsdSlash('new-milestone', _slashRuntime) as string} first.`);
}
if (!fs.existsSync(paths.state)) {
error(`No STATE.md found. Run ${formatGsdSlash('new-milestone', _slashRuntime) as string} first.`);
error(`No STATE.md found. Run ${formatMsdSlash('new-milestone', _slashRuntime) as string} first.`);
}
const rawContent = fs.readFileSync(paths.roadmap, 'utf-8');
const content = extractCurrentMilestone(rawContent, cwd);
@@ -3171,12 +3171,12 @@ function cmdInitManager(cwd: string, raw: boolean): void {
let waitingSignal: unknown = null;
try {
// #3964: mirror cmdSignalWaiting's write locations exactly — `.gsd/`
// #3964: mirror cmdSignalWaiting's write locations exactly — `.msd/`
// first when it exists, else the project-aware planning dir — so the
// signal is read from the project (and location) it is written to.
const gsdWaiting = path.join(cwd, '.gsd', 'WAITING.json');
const waitingPath = fs.existsSync(path.join(cwd, '.gsd'))
? gsdWaiting
const msdWaiting = path.join(cwd, '.msd', 'WAITING.json');
const waitingPath = fs.existsSync(path.join(cwd, '.msd'))
? msdWaiting
: path.join(planningDir(cwd), 'WAITING.json');
const waitingRaw = platformReadSync(waitingPath);
if (waitingRaw !== null) {
@@ -3206,7 +3206,7 @@ function cmdInitManager(cwd: string, raw: boolean): void {
phase_name: phase['name'],
action: 'execute',
reason: `${phase['plan_count'] as number} plans ready, dependencies met`,
command: `${formatGsdSlash('execute-phase', _slashRuntime) as string} ${phase['number'] as string}`,
command: `${formatMsdSlash('execute-phase', _slashRuntime) as string} ${phase['number'] as string}`,
});
} else if (
phase['disk_status'] === 'discussed' ||
@@ -3217,7 +3217,7 @@ function cmdInitManager(cwd: string, raw: boolean): void {
phase_name: phase['name'],
action: 'plan',
reason: 'Context gathered, ready for planning',
command: `${formatGsdSlash('plan-phase', _slashRuntime) as string} ${phase['number'] as string}`,
command: `${formatMsdSlash('plan-phase', _slashRuntime) as string} ${phase['number'] as string}`,
});
} else if (
(phase['disk_status'] === 'empty' || phase['disk_status'] === 'no_directory') &&
@@ -3228,7 +3228,7 @@ function cmdInitManager(cwd: string, raw: boolean): void {
phase_name: phase['name'],
action: 'discuss',
reason: 'Unblocked, ready to gather context',
command: `${formatGsdSlash('discuss-phase', _slashRuntime) as string} ${phase['number'] as string}`,
command: `${formatMsdSlash('discuss-phase', _slashRuntime) as string} ${phase['number'] as string}`,
});
}
}
@@ -3273,7 +3273,7 @@ function cmdInitManager(cwd: string, raw: boolean): void {
);
if (!safe) {
process.stderr.write(
`gsd-tools: warning: manager.flags contains invalid tokens, ignoring: ${val}\n`,
`msd-tools: warning: manager.flags contains invalid tokens, ignoring: ${val}\n`,
);
return '';
}
@@ -3364,7 +3364,7 @@ function cmdInitCompleteMilestone(
const milestonesPath = path.join(planningBase, 'MILESTONES.md');
// #4455 follow-up round 2 (self-discovered regression): PROJECT.md, unlike
// MILESTONES.md, is genuinely SHARED across a project's own workstreams —
// never cloned per workstream. gsd-core/references/workstream-flag.md's
// never cloned per workstream. msd-core/references/workstream-flag.md's
// directory diagram marks it `# Shared`; new-milestone.md states it
// outright ("PROJECT.md is shared across workstreams") and explicitly
// SKIPS writing its `## Current Milestone` heading under an active
@@ -3374,13 +3374,13 @@ function cmdInitCompleteMilestone(
// wrongly generalized from planningPaths()'s structural shape (which
// composes `project` under the workstream base) without checking an
// actual PROJECT.md write path — resolved against planningRoot(cwd)
// (round 2), but that ALSO ignores the separate GSD_PROJECT dimension
// (round 2), but that ALSO ignores the separate MSD_PROJECT dimension
// (multi-project namespacing, #3749: PROJECT.md legitimately lives at
// `.planning/<project>/PROJECT.md` when GSD_PROJECT is set — a real,
// `.planning/<project>/PROJECT.md` when MSD_PROJECT is set — a real,
// tested, pre-existing feature planningRoot's blanket root-only read
// broke), caught by gsd-test on this fix's own first push. `planningDir`
// with `ws` explicitly nulled (never read from GSD_WORKSTREAM) but
// `project` left to default from GSD_PROJECT is the correct middle
// broke), caught by msd-test on this fix's own first push. `planningDir`
// with `ws` explicitly nulled (never read from MSD_WORKSTREAM) but
// `project` left to default from MSD_PROJECT is the correct middle
// ground: respects project-namespacing, ignores workstream-namespacing.
const projectPath = path.join(planningDir(cwd, null), 'PROJECT.md');
// REQUIREMENTS.md is workstream-scoped the same way (planningPaths(cwd).requirements,
@@ -3446,7 +3446,7 @@ function cmdInitAutonomous(
/**
* `docs-update.md`'s dedicated init entry point (#2994, epic #1671 Phase
* 6.3 — final slice). `docs-update.md` previously carried NO `gsd_run query
* 6.3 — final slice). `docs-update.md` previously carried NO `msd_run query
* init.*` call at all: its own `docs-init` command (`cmdDocsInit`,
* src/docs.cts) is a SEPARATE, pre-existing entry point outside this
* `init.*` family and is left untouched here. This function's only job is
@@ -3473,17 +3473,17 @@ function cmdInitDocsUpdate(cwd: string, raw: boolean, options: Record<string, un
/**
* `update.md`'s dedicated init entry point (#2994, epic #1671 Phase 6.3 —
* final slice). `update.md` previously carried NO `gsd_run query init.*`
* call at all: it resolves `gsd-tools.cjs` itself (its own bespoke
* `PREFERRED_CONFIG_DIR`/`PREFERRED_RUNTIME`-aware `$GSD_TOOLS` cascade,
* final slice). `update.md` previously carried NO `msd_run query init.*`
* call at all: it resolves `msd-tools.cjs` itself (its own bespoke
* `PREFERRED_CONFIG_DIR`/`PREFERRED_RUNTIME`-aware `$MSD_TOOLS` cascade,
* `update.md` ~lines 13-45) because the update workflow must run BEFORE any
* install can be assumed resolvable — the canonical launcher preamble's
* fixed candidate list is not a substitute for that cascade, and both
* resolutions assign the identical `$GSD_TOOLS` shell variable, so copying
* resolutions assign the identical `$MSD_TOOLS` shell variable, so copying
* the canonical preamble in ADDITION to the existing cascade would silently
* clobber the value `backup_custom_files`/`restore_custom_files` (later
* steps) still depend on. This function is invoked via that ALREADY
* resolved `$GSD_TOOLS`, not a redundant `gsd_run()` shell function.
* resolved `$MSD_TOOLS`, not a redundant `msd_run()` shell function.
*
* `state:next-channel` ground truth: `--next` OR its documented alias
* `--rc` (same disjunction-to-one-boolean discipline as
@@ -3492,7 +3492,7 @@ function cmdInitDocsUpdate(cwd: string, raw: boolean, options: Record<string, un
* (`TAG="next"`/`TAG="latest"`) — issue #815's regression test
* (`tests/update-workflow.test.cjs`) asserts that literal
* case-statement text stays in `update.md` verbatim (the npm dist-tag
* selection has to run in the workflow's own shell before any `gsd_run`
* selection has to run in the workflow's own shell before any `msd_run`
* round-trip), so `next_channel` exists purely to gate the `channel-banner`
* section's admission — a parallel, consistent-but-not-replacing
* resolution of the same flags.
@@ -3514,22 +3514,22 @@ function cmdInitUpdate(cwd: string, raw: boolean, options: Record<string, unknow
/**
* `transition.md`'s dedicated init entry point (#2994, epic #1671 Phase
* 6.3 — final slice). `transition.md` is an internal workflow (no
* user-facing `/gsd-transition` command) that previously carried NO
* `gsd_run query init.*` call at all; it already establishes `gsd_run()`
* user-facing `/msd-transition` command) that previously carried NO
* `msd_run query init.*` call at all; it already establishes `msd_run()`
* via the canonical launcher preamble in its `update_roadmap_and_state`
* step (before this call's insertion point in `offer_next_phase`), so no
* second preamble copy is needed in the host file.
*
* `state:workstream-active` ground truth: a workstream is active — resolved
* via `GSD_WORKSTREAM` env, falling back to the stored active-workstream
* via `MSD_WORKSTREAM` env, falling back to the stored active-workstream
* pointer (mirrors `cmdInitProgress`'s own `_resolvedWorkstream` resolution
* above, the established authoritative source for "is a workstream active"
* in this file).
*
* `other_active_workstreams` hoists the resolver-in-body hazard out of
* `transition.md`'s `workstream-collision-check` step: that step's body
* previously re-derived this via an inline `gsd_run query workstream.list
* --raw` call gated on the identical `if [ -n "$GSD_WORKSTREAM" ]`
* previously re-derived this via an inline `msd_run query workstream.list
* --raw` call gated on the identical `if [ -n "$MSD_WORKSTREAM" ]`
* condition that now backs this section's OWN admission — resolving it here
* instead reuses `getOtherActiveWorkstreamInventories` (src/workstream-
* inventory.cts), the SAME primitive `workstream.list` itself calls
@@ -3537,7 +3537,7 @@ function cmdInitUpdate(cwd: string, raw: boolean, options: Record<string, unknow
* step's own prose described (excludes the current workstream and any
* workstream whose status contains "milestone complete" or "archived",
* case-insensitively — `isCompletedInventory`), so the step body becomes a
* pure JSON consumer with no `gsd_run` call of its own.
* pure JSON consumer with no `msd_run` call of its own.
*/
function cmdInitTransition(cwd: string, raw: boolean, options: Record<string, unknown> = {}): void {
// #3579 root-cause fix: read-only informational field — peek, don't
@@ -3563,9 +3563,9 @@ function cmdInitTransition(cwd: string, raw: boolean, options: Record<string, un
/**
* `debug.md`'s dedicated init entry point (#3149; prerequisite for #3128).
* `debug.md` previously carried NO `gsd_run query init.*` call at all — it made
* `debug.md` previously carried NO `msd_run query init.*` call at all — it made
* THREE separate round-trips instead: `state.load` (for `commit_docs`,
* `config.response_language` and `debug_dir`), `resolve-model gsd-debugger
* `config.response_language` and `debug_dir`), `resolve-model msd-debugger
* --pick model`, and `config-get workflow.tdd_mode --raw`. Because no
* debug-scoped fact was computed at any entry point, a `when=` atom naming one
* would have evaluated FALSE forever — ADR-1671's admission gate (2) and the
@@ -3584,14 +3584,14 @@ function cmdInitTransition(cwd: string, raw: boolean, options: Record<string, un
* - `debugger_model` — `resolveModelInternal`, which IS what `query
* resolve-model --pick model` returns (`cmdResolveModel`, src/commands.cts).
* - `tdd_mode` — the `Boolean(config.tdd_mode)` idiom `cmdInitExecutePhase` and
* `cmdInitPlanPhase` already use. `/gsd:debug` has no `--tdd` flag, so the
* `cmdInitPlanPhase` already use. `/msd:debug` has no `--tdd` flag, so the
* sibling handlers' `options['tdd'] ||` disjunct is deliberately omitted
* rather than carried as a phantom.
*
* `state.load` is deliberately NOT narrowed — see the note beside its own
* `debug_dir` field. This handler is purely additive alongside it.
*
* `diagnose` is the one flag `/gsd:debug` already documents. Exposing it as a
* `diagnose` is the one flag `/msd:debug` already documents. Exposing it as a
* top-level fact follows `cmdInitUpdate`'s `next_channel` and
* `cmdInitAutonomous`'s `plan_strategy_converge` precedent, and is what makes
* the router's flag forwarding observable. No `when=` atom consumes it yet:
@@ -3605,16 +3605,16 @@ function cmdInitDebug(cwd: string, raw: boolean, options: Record<string, unknown
const result: Record<string, unknown> = {
commit_docs: config.commit_docs,
// #2376: absolute — debug.md builds `debug_file_path` as
// `{debug_dir}/{slug}.md` for its gsd-debug-session-manager spawns, whose
// `{debug_dir}/{slug}.md` for its msd-debug-session-manager spawns, whose
// own cwd may differ from the orchestrator's.
debug_dir: toPosixPath(planningPaths(cwd).debug),
debugger_model: resolveModelInternal(cwd, 'gsd-debugger'),
debugger_model: resolveModelInternal(cwd, 'msd-debugger'),
tdd_mode: Boolean(config.tdd_mode),
diagnose: options['diagnose'] === true,
};
// Additive, optional field — degrades to null while `debug` has no key in
// `gsd-core/workflows/section-manifest.json` (it has no `gsd:section` markers
// `msd-core/workflows/section-manifest.json` (it has no `msd:section` markers
// until #3128). null means "read everything", which is NOT the same as a
// computed empty selection.
result['section_manifest'] = buildSectionManifestField(cwd, null, options, 'debug', {});
@@ -3635,8 +3635,8 @@ function cmdInitProgress(cwd: string, raw: boolean, options: Record<string, unkn
// #1912: fail safe in workstream mode with no active workstream. With no active
// workstream and no --ws, planningDir(cwd) resolves to root .planning — silently
// reporting a stale root milestone. Require an explicit workstream instead.
// Mirror planningDir's resolution (GSD_WORKSTREAM env > stored active pointer) so
// an explicit --ws (which sets GSD_WORKSTREAM) satisfies the check.
// Mirror planningDir's resolution (MSD_WORKSTREAM env > stored active pointer) so
// an explicit --ws (which sets MSD_WORKSTREAM) satisfies the check.
const _availableWorkstreams = listAvailableWorkstreams(cwd);
// #3579 root-cause fix: this is a check, not a consuming read — use the
// non-mutating peek so an unresolvable pointer isn't self-healed (cleared)
@@ -3654,7 +3654,7 @@ function cmdInitProgress(cwd: string, raw: boolean, options: Record<string, unkn
if (_diagnosis.present) {
error(
`init.progress requires a workstream in workstream mode — the active-workstream marker names '${_diagnosis.value}', but it did not resolve: ${describeUnresolvedWorkstreamReason(_diagnosis.reason)}. Root STATE.md (likely stale) would be reported otherwise. ` +
`Pass --ws <name> or run ${formatGsdSlash('workstream set', _slashRuntime) as string} to point it at an existing workstream. ` +
`Pass --ws <name> or run ${formatMsdSlash('workstream set', _slashRuntime) as string} to point it at an existing workstream. ` +
`Available workstreams: ${_availableWorkstreams.join(', ')}`,
ERROR_REASON.WORKSTREAM_MODE_MARKER_UNRESOLVED,
{ marker_value: _diagnosis.value, marker_reason: _diagnosis.reason },
@@ -3662,7 +3662,7 @@ function cmdInitProgress(cwd: string, raw: boolean, options: Record<string, unkn
}
error(
`init.progress requires a workstream in workstream mode — no active workstream is set, so root STATE.md (likely stale) would be reported. ` +
`Pass --ws <name> or run ${formatGsdSlash('workstream set', _slashRuntime) as string} first. ` +
`Pass --ws <name> or run ${formatMsdSlash('workstream set', _slashRuntime) as string} first. ` +
`Available workstreams: ${_availableWorkstreams.join(', ')}`,
ERROR_REASON.WORKSTREAM_MODE_NONE_ACTIVE,
);
@@ -3844,7 +3844,7 @@ function cmdInitProgress(cwd: string, raw: boolean, options: Record<string, unkn
// #2994: the CURRENT phase's number, used both to expose `phase_mvp_mode`
// at the top level (so the `mvp-display` step body can consume an
// already-resolved fact instead of re-invoking `gsd_run query
// already-resolved fact instead of re-invoking `msd_run query
// phase.mvp-mode` itself — that inline resolver would otherwise gate a
// section on a fact the section's own body recomputes, which is circular
// and self-disabling) and to thread a real `phase_number` into
@@ -3857,8 +3857,8 @@ function cmdInitProgress(cwd: string, raw: boolean, options: Record<string, unkn
const phaseMvpMode = detectPhaseMvpMode(cwd, currentPhaseNumber);
const result: Record<string, unknown> = {
executor_model: resolveModelInternal(cwd, 'gsd-executor'),
planner_model: resolveModelInternal(cwd, 'gsd-planner'),
executor_model: resolveModelInternal(cwd, 'msd-executor'),
planner_model: resolveModelInternal(cwd, 'msd-planner'),
commit_docs: config.commit_docs,
@@ -3901,8 +3901,8 @@ function cmdInitProgress(cwd: string, raw: boolean, options: Record<string, unkn
// planningDir(cwd, null) on the assumption that config.json is shared
// like PROJECT.md (workstream-flag.md's directory diagram marks it
// `# Shared`) — but ADR-0006's own tests (tests/init.test.cjs, "init
// handlers honor GSD_WORKSTREAM") assert config_path IS workstream-scoped
// for execute-phase/new-project/new-milestone/progress, and gsd-test
// handlers honor MSD_WORKSTREAM") assert config_path IS workstream-scoped
// for execute-phase/new-project/new-milestone/progress, and msd-test
// caught the regression immediately. The diagram is stale for
// config.json specifically (same class of staleness already found for
// `milestones/` during the #4455 follow-up) — reverted to the
@@ -3951,7 +3951,7 @@ function detectChildRepos(dir: string): { name: string; path: string; has_uncomm
function cmdInitNewWorkspace(cwd: string, raw: boolean): void {
const homedir = process.env['HOME'] || os.homedir();
const defaultBase = path.join(homedir, 'gsd-workspaces');
const defaultBase = path.join(homedir, 'msd-workspaces');
const childRepos = detectChildRepos(cwd);
@@ -3972,7 +3972,7 @@ function cmdInitNewWorkspace(cwd: string, raw: boolean): void {
function cmdInitListWorkspaces(cwd: string, raw: boolean): void {
const homedir = process.env['HOME'] || os.homedir();
const defaultBase = path.join(homedir, 'gsd-workspaces');
const defaultBase = path.join(homedir, 'msd-workspaces');
const workspaces: Record<string, unknown>[] = [];
if (fs.existsSync(defaultBase)) {
@@ -4026,7 +4026,7 @@ function cmdInitListWorkspaces(cwd: string, raw: boolean): void {
function cmdInitRemoveWorkspace(cwd: string, name: string | undefined, raw: boolean): void {
const homedir = process.env['HOME'] || os.homedir();
const defaultBase = path.join(homedir, 'gsd-workspaces');
const defaultBase = path.join(homedir, 'msd-workspaces');
if (!name) {
error('workspace name required for init remove-workspace');
@@ -4302,7 +4302,7 @@ function cmdAgentSkills(
// documented "unconfigured → empty block" contract that agent-skills tests
// pin.
//
// #4407 (ADR-4139 stream 2): this is the one place GSD's own agent-persona
// #4407 (ADR-4139 stream 2): this is the one place MSD's own agent-persona
// content is served through a real code seam rather than an eagerly
// @-included file, so the compact/canonical choice is made here in code
// (a real exit code) instead of a prose config-get gate. Compact is tried
@@ -4310,7 +4310,7 @@ function cmdAgentSkills(
// with the fallback disclosed in the payload itself, never a silent switch.
let agentPayloadVariant: 'compact' | 'canonical' | null = null;
if (!block) {
const runtime = (config && (config['runtime'] as string)) || process.env['GSD_RUNTIME'] || 'claude';
const runtime = (config && (config['runtime'] as string)) || process.env['MSD_RUNTIME'] || 'claude';
if (runtime !== 'claude') {
const agentCheck = checkAgentsInstalled(runtime, projectRoot) as unknown as { agents_dir?: string } | null;
const agentsDir = agentCheck?.agents_dir;
@@ -4326,7 +4326,7 @@ function cmdAgentSkills(
const canonicalContent = readNonEmptyFileOrNull(path.join(agentsDir, `${agentType}.md`));
if (canonicalContent !== null) {
block = compactRequested
? `<!-- gsd: no compact payload registered for ${agentType}; serving canonical -->\n\n${canonicalContent}`
? `<!-- msd: no compact payload registered for ${agentType}; serving canonical -->\n\n${canonicalContent}`
: canonicalContent;
agentPayloadVariant = 'canonical';
}
@@ -4434,7 +4434,7 @@ interface SkillManifest {
skills: SkillEntry[];
roots: RootSummary[];
installation: {
gsd_skills_installed: boolean;
msd_skills_installed: boolean;
legacy_claude_commands_installed: boolean;
};
counts: {
@@ -4512,7 +4512,7 @@ function buildSkillManifest(cwd: string, skillsDir: string | null = null): Skill
{
// Codex's deprecated fallback skill root ($CODEX_HOME/skills). Kept as a
// discovery-only legacy root so pre-move installs remain inventoried;
// GSD no longer installs here (#2088).
// MSD no longer installs here (#2088).
root: '~/.codex/skills',
path: path.join(getGlobalConfigDir('codex'), 'skills'),
scope: 'global',
@@ -4520,15 +4520,15 @@ function buildSkillManifest(cwd: string, skillsDir: string | null = null): Skill
deprecated: true,
},
{
root: '.claude/gsd-core/skills',
path: path.join(os.homedir(), '.claude', 'gsd-core', 'skills'),
root: '.claude/msd-core/skills',
path: path.join(os.homedir(), '.claude', 'msd-core', 'skills'),
scope: 'import-only',
kind: 'skills',
deprecated: true,
},
{
root: '.claude/commands/gsd',
path: path.join(os.homedir(), '.claude', 'commands', 'gsd'),
root: '.claude/commands/msd',
path: path.join(os.homedir(), '.claude', 'commands', 'msd'),
scope: 'legacy-commands',
kind: 'commands',
deprecated: true,
@@ -4580,7 +4580,7 @@ function buildSkillManifest(cwd: string, skillsDir: string | null = null): Skill
}
// Track skill names seen within this root to deduplicate dual-routed concretes
// (e.g. spec-phase nested under both gsd-ns-workflow and gsd-ns-manage).
// (e.g. spec-phase nested under both msd-ns-workflow and msd-ns-manage).
const seenNamesInRoot = new Set<string>();
function pushSkillEntry(
@@ -4640,9 +4640,9 @@ function buildSkillManifest(cwd: string, skillsDir: string | null = null): Skill
// Nested layout: <entry>/skills/<stem>/SKILL.md
// Used by cline, qwen, hermes, augment, trae, antigravity (#69 nested=true).
// Descend exactly one level into <entry>/skills/ — no deeper recursion.
// Scope to gsd-ns-* routers only: never vacuum up an unrelated user skill
// Scope to msd-ns-* routers only: never vacuum up an unrelated user skill
// that happens to have its own `skills/` subdirectory.
if (!entry.name.startsWith('gsd-ns-')) continue;
if (!entry.name.startsWith('msd-ns-')) continue;
const nestedSkillsDir = path.join(rootPath, entry.name, 'skills');
let nestedEntries: fs.Dirent[] = [];
try {
@@ -4672,13 +4672,13 @@ function buildSkillManifest(cwd: string, skillsDir: string | null = null): Skill
return rootCmp !== 0 ? rootCmp : a.name.localeCompare(b.name);
});
const gsdSkillsInstalled = skills.some((skill) => skill.name.startsWith('gsd-'));
const msdSkillsInstalled = skills.some((skill) => skill.name.startsWith('msd-'));
return {
skills,
roots,
installation: {
gsd_skills_installed: gsdSkillsInstalled,
msd_skills_installed: msdSkillsInstalled,
legacy_claude_commands_installed: legacyClaudeCommandsInstalled,
},
counts: {

View File

@@ -2,32 +2,32 @@
* install-effort-resolver — install-time effort resolution (#443), extracted from
* the package-root `bin/install.js` (#2071).
*
* `gsd-tools effort sync` (src/commands.cts `cmdEffortSync`) must mirror what the
* installer writes — home `~/.gsd/defaults.json` merged with the project's
* `msd-tools effort sync` (src/commands.cts `cmdEffortSync`) must mirror what the
* installer writes — home `~/.msd/defaults.json` merged with the project's
* `.planning/config.json` — which the runtime resolver (`resolveEffortInternal`
* via `loadConfig`) does NOT do. It previously reached those two functions via
* `require('../../../bin/install.js')`, but the installer never copies the
* package-root `bin/install.js` into a runtime home, so `effort sync` crashed with
* MODULE_NOT_FOUND in every installed runtime (#2071). Moving the logic here — a
* `src/*.cts` module compiled into the shipped `gsd-core/bin/lib/` tree — lets both
* `src/*.cts` module compiled into the shipped `msd-core/bin/lib/` tree — lets both
* the installer AND `effort sync` require it from a location that is always present,
* keeping a single source of truth (no duplication / drift).
*
* Pure with respect to config: `readGsdEffectiveEffortConfig` performs the config
* Pure with respect to config: `readMsdEffectiveEffortConfig` performs the config
* reads; `resolveInstallTimeEffort` is pure given a pre-merged effort object.
*
* #2875 defect fix: this module sits on the `installRuntimeArtifacts` call
* tree (reached both directly from `runtime-artifact-conversion.cts`'s
* effort-injection rewrite pass, and transitively via
* `install-model-override-resolver.cts`'s `_readGsdConfigFile` reuse) — every
* `install-model-override-resolver.cts`'s `_readMsdConfigFile` reuse) — every
* DESTINATION/config fs touch below routes through `installFs()`
* (install-fs-adapter.cts), matching `retired-artifact-cleanup.cts` /
* `user-artifact-staging.cts`'s existing precedent. `config-defaults.manifest.json`
* (`_getGsdEffortCatalog`) stays on raw `node:fs`, deliberately: it is
* PACKAGE-SOURCE (ships under `gsd-core/bin/shared/`, resolved from
* (`_getMsdEffortCatalog`) stays on raw `node:fs`, deliberately: it is
* PACKAGE-SOURCE (ships under `msd-core/bin/shared/`, resolved from
* `__dirname`, never the install destination), the same class of read
* `install-fs-adapter.cts`'s module doc documents as "DELIBERATELY NOT
* ROUTED" for `findInstallSourceRoot`/`readGsdCommandNames`.
* ROUTED" for `findInstallSourceRoot`/`readMsdCommandNames`.
*/
import fs from 'node:fs';
import path from 'node:path';
@@ -39,7 +39,7 @@ const { installFs } = installFsAdapter;
// eslint-disable-next-line @typescript-eslint/no-require-imports -- model-resolver.cjs is an export= CommonJS module
import modelResolver = require('./model-resolver.cjs');
const { EFFORT_SET: GSD_EFFORT_SET } = modelResolver as { EFFORT_SET: Set<string> };
const { EFFORT_SET: MSD_EFFORT_SET } = modelResolver as { EFFORT_SET: Set<string> };
// eslint-disable-next-line @typescript-eslint/no-require-imports -- model-catalog.cjs is an export= CommonJS module
import modelCatalog = require('./model-catalog.cjs');
const { mergeEffortTierDefaults } = modelCatalog as { mergeEffortTierDefaults: typeof import('./model-catalog.cjs').mergeEffortTierDefaults };
@@ -52,24 +52,24 @@ interface EffortConfig {
}
/**
* #2517 — Read a single GSD config file (defaults.json or per-project
* #2517 — Read a single MSD config file (defaults.json or per-project
* config.json) into a plain object, returning null on missing/empty files
* and warning to stderr on JSON parse failures so silent corruption can't
* mask broken configs (review finding #5).
*/
function _readGsdConfigFile(absPath: string, label: string): Record<string, unknown> | null {
function _readMsdConfigFile(absPath: string, label: string): Record<string, unknown> | null {
if (!installFs().existsSync(absPath)) return null;
let raw: string;
try {
raw = installFs().readFileSync(absPath, 'utf-8');
} catch (err) {
process.stderr.write(`gsd: warning — could not read ${label} (${absPath}): ${(err as Error).message}\n`);
process.stderr.write(`msd: warning — could not read ${label} (${absPath}): ${(err as Error).message}\n`);
return null;
}
try {
return JSON.parse(raw) as Record<string, unknown>;
} catch (err) {
process.stderr.write(`gsd: warning — invalid JSON in ${label} (${absPath}): ${(err as Error).message}\n`);
process.stderr.write(`msd: warning — invalid JSON in ${label} (${absPath}): ${(err as Error).message}\n`);
return null;
}
}
@@ -91,9 +91,9 @@ interface EffortCatalog {
// first time they are needed so that requiring this module in test contexts that
// never trigger an install does NOT produce module-load-time side effects (the
// manifest read + hard throw) that could alter subprocess exit codes or stderr.
let _gsdEffortCatalogCache: EffortCatalog | null = null;
function _getGsdEffortCatalog(): EffortCatalog {
if (_gsdEffortCatalogCache) return _gsdEffortCatalogCache;
let _msdEffortCatalogCache: EffortCatalog | null = null;
function _getMsdEffortCatalog(): EffortCatalog {
if (_msdEffortCatalogCache) return _msdEffortCatalogCache;
// eslint-disable-next-line @typescript-eslint/no-require-imports -- model-catalog.cjs is an export= CommonJS module
const { AGENT_DEFAULT_TIERS, renderEffortForRuntime } = require('./model-catalog.cjs') as {
@@ -105,9 +105,9 @@ function _getGsdEffortCatalog(): EffortCatalog {
renderEffortForRuntime: (runtime: string, effort: string) => { value: string | null };
};
// This module lives in gsd-core/bin/lib/, so the shared manifest is one level
// up in gsd-core/bin/shared/ (bin/lib → bin → shared). (In bin/install.js this
// path was `.., gsd-core, bin, shared` relative to the package-root bin/.)
// This module lives in msd-core/bin/lib/, so the shared manifest is one level
// up in msd-core/bin/shared/ (bin/lib → bin → shared). (In bin/install.js this
// path was `.., msd-core, bin, shared` relative to the package-root bin/.)
const manifestPath = path.join(__dirname, '..', 'shared', 'config-defaults.manifest.json');
let manifestData: { effort?: { routing_tier_defaults?: Record<string, string>; default?: string } };
try {
@@ -117,7 +117,7 @@ function _getGsdEffortCatalog(): EffortCatalog {
} catch (_err) {
// Fail loudly — a missing manifest is a broken install, not a soft degradation.
throw new Error(
`gsd install: cannot load config-defaults.manifest.json at ${manifestPath}: ${(_err as Error).message}`,
`msd install: cannot load config-defaults.manifest.json at ${manifestPath}: ${(_err as Error).message}`,
);
}
@@ -134,13 +134,13 @@ function _getGsdEffortCatalog(): EffortCatalog {
? manifestData.effort.default
: 'high'; // guard: unreachable if manifest is valid
_gsdEffortCatalogCache = {
_msdEffortCatalogCache = {
AGENT_DEFAULT_TIERS,
renderEffortForRuntime,
EFFORT_MANIFEST_TIER_DEFAULTS: tierDefaults,
EFFORT_MANIFEST_DEFAULT: effortDefault,
};
return _gsdEffortCatalogCache;
return _msdEffortCatalogCache;
}
/**
@@ -148,8 +148,8 @@ function _getGsdEffortCatalog(): EffortCatalog {
* exists to remove): the upward walk from a runtime install root looking for
* `.planning/config.json`, capped at 8 ancestor levels, was duplicated
* verbatim three times — once here, and twice more in
* `install-model-override-resolver.cts` (`readGsdEffectiveModelOverrides`,
* `readGsdRuntimeProfileResolver`) after that module was extracted FROM this
* `install-model-override-resolver.cts` (`readMsdEffectiveModelOverrides`,
* `readMsdRuntimeProfileResolver`) after that module was extracted FROM this
* one specifically to stop duplicating shared install-time config logic.
* Single-sourced here so the cap and the walk semantics (depth 0 = targetDir
* itself, depth 7 = the last checked ancestor, 8 levels up is never reached)
@@ -159,7 +159,7 @@ function _getGsdEffortCatalog(): EffortCatalog {
* @returns the first `.planning/config.json` found walking upward from
* `targetDir` (inclusive) through up to 8 ancestor levels, or `null`.
*/
function _findAncestorGsdConfigPath(targetDir: string): string | null {
function _findAncestorMsdConfigPath(targetDir: string): string | null {
let probeDir = path.resolve(targetDir);
for (let depth = 0; depth < 8; depth += 1) {
const candidate = path.join(probeDir, '.planning', 'config.json');
@@ -174,8 +174,8 @@ function _findAncestorGsdConfigPath(targetDir: string): string | null {
/**
* #443 — Read the merged `effort` config block for install-time effort resolution.
*
* Probes the same config sources as readGsdRuntimeProfileResolver (per-project
* `.planning/config.json` wins over `~/.gsd/defaults.json`) but extracts the
* Probes the same config sources as readMsdRuntimeProfileResolver (per-project
* `.planning/config.json` wins over `~/.msd/defaults.json`) but extracts the
* `effort` object instead of the model-profile fields.
*
* Returns the merged `effort` object or null when neither source defines one.
@@ -184,16 +184,16 @@ function _findAncestorGsdConfigPath(targetDir: string): string | null {
*
* @param targetDir Runtime install root (walks up to find .planning/).
*/
function readGsdEffectiveEffortConfig(targetDir: string | null = null): EffortConfig | null {
const homeDefaults = _readGsdConfigFile(
path.join(os.homedir(), '.gsd', 'defaults.json'),
'~/.gsd/defaults.json',
function readMsdEffectiveEffortConfig(targetDir: string | null = null): EffortConfig | null {
const homeDefaults = _readMsdConfigFile(
path.join(os.homedir(), '.msd', 'defaults.json'),
'~/.msd/defaults.json',
);
let projectConfig: Record<string, unknown> | null = null;
if (targetDir) {
const candidate = _findAncestorGsdConfigPath(targetDir);
if (candidate) projectConfig = _readGsdConfigFile(candidate, '.planning/config.json');
const candidate = _findAncestorMsdConfigPath(targetDir);
if (candidate) projectConfig = _readMsdConfigFile(candidate, '.planning/config.json');
}
const homeEffort = (homeDefaults && homeDefaults.effort && typeof homeDefaults.effort === 'object' && !Array.isArray(homeDefaults.effort))
@@ -243,8 +243,8 @@ function readGsdEffectiveEffortConfig(targetDir: string | null = null): EffortCo
* 3. effortCfg.default
* 4. 'high' (hardcoded fallback)
*
* @param effortCfg Result of readGsdEffectiveEffortConfig().
* @param agentName e.g. 'gsd-planner'
* @param effortCfg Result of readMsdEffectiveEffortConfig().
* @param agentName e.g. 'msd-planner'
* @returns Universal effort string (low/medium/high/xhigh/max/minimal)
*/
function resolveInstallTimeEffort(effortCfg: EffortConfig | null, agentName: string): string {
@@ -258,15 +258,15 @@ function resolveInstallTimeEffort(effortCfg: EffortConfig | null, agentName: str
const ao = effortCfg.agent_overrides;
if (ao && typeof ao === 'object' && !Array.isArray(ao)) {
const v = ao[agentName];
if (typeof v === 'string' && GSD_EFFORT_SET.has(v)) return v;
if (typeof v === 'string' && MSD_EFFORT_SET.has(v)) return v;
}
}
// Step 2: routing_tier_defaults keyed by the agent's catalog tier
const { AGENT_DEFAULT_TIERS, EFFORT_MANIFEST_TIER_DEFAULTS, EFFORT_MANIFEST_DEFAULT } = _getGsdEffortCatalog();
const { AGENT_DEFAULT_TIERS, EFFORT_MANIFEST_TIER_DEFAULTS, EFFORT_MANIFEST_DEFAULT } = _getMsdEffortCatalog();
const agentTier = AGENT_DEFAULT_TIERS[agentName];
if (agentTier) {
const isValidEffort = (v: unknown): v is string => typeof v === 'string' && GSD_EFFORT_SET.has(v);
const isValidEffort = (v: unknown): v is string => typeof v === 'string' && MSD_EFFORT_SET.has(v);
const merged = mergeEffortTierDefaults(
EFFORT_MANIFEST_TIER_DEFAULTS,
effortCfg ? effortCfg.routing_tier_defaults : undefined,
@@ -279,21 +279,21 @@ function resolveInstallTimeEffort(effortCfg: EffortConfig | null, agentName: str
// Step 3: effort.default
if (effortCfg) {
const d = effortCfg.default;
if (typeof d === 'string' && GSD_EFFORT_SET.has(d)) return d;
if (typeof d === 'string' && MSD_EFFORT_SET.has(d)) return d;
}
// Step 4: manifest default (sourced from config-defaults.manifest.json effort.default)
// If even the manifest default is invalid, fall back to 'high'.
if (typeof EFFORT_MANIFEST_DEFAULT === 'string' && GSD_EFFORT_SET.has(EFFORT_MANIFEST_DEFAULT)) {
if (typeof EFFORT_MANIFEST_DEFAULT === 'string' && MSD_EFFORT_SET.has(EFFORT_MANIFEST_DEFAULT)) {
return EFFORT_MANIFEST_DEFAULT;
}
return 'high';
}
export = {
readGsdEffectiveEffortConfig,
readMsdEffectiveEffortConfig,
resolveInstallTimeEffort,
_getGsdEffortCatalog,
_readGsdConfigFile,
_findAncestorGsdConfigPath,
_getMsdEffortCatalog,
_readMsdConfigFile,
_findAncestorMsdConfigPath,
};

View File

@@ -78,7 +78,7 @@ function withInstallerPackageSource<T>(
// Reuse the existing compatibility-marker contract through the existing fs
// seam. The marker exists only in this synchronous call tree: no disk state,
// layout export, stage argument, or caller-settable authority flag is added.
const markerPath = path.resolve(configDir, '.gsd-source');
const markerPath = path.resolve(configDir, '.msd-source');
const packageCommandsRoot = runtimeArtifactLayout.findInstallSourceRoot();
const markerBytes = Buffer.from(packageCommandsRoot + '\n');
const base = installFs();
@@ -102,7 +102,7 @@ function withInstallerPackageSource<T>(
function isRuntimeSurfaceSourceUnavailable(message: string): boolean {
return message.startsWith('Runtime Surface source is unavailable or incomplete for ') &&
message.endsWith('install or upgrade gsd-core before materializing this surface.');
message.endsWith('install or upgrade msd-core before materializing this surface.');
}
function assertCorpusTreeHasNoSymlinks(root: string): void {
@@ -148,7 +148,7 @@ function syncRuntimeSurfaceCorpus(source: string, destination: string, configDir
}
assertCorpusTreeHasNoSymlinks(destination);
// Remove only paths the previous manifest proves GSD owned and which the
// Remove only paths the previous manifest proves MSD owned and which the
// executing package no longer ships. Unknown neighbouring files survive.
for (const relative of previousOwnedCorpusFiles(configDir, manifestPrefix)) {
const sourceEntry = path.join(source, ...relative.split('/'));
@@ -178,7 +178,7 @@ function syncRuntimeSurfaceCorpus(source: string, destination: string, configDir
* Provision the raw, installation-owned input needed to re-materialize a
* global Runtime Surface after the executing package tree disappears.
*
* The corpus deliberately lives below the already-installed `gsd-core/`
* The corpus deliberately lives below the already-installed `msd-core/`
* tree and is accepted only after its manifest ownership and hashes verify.
* The compatibility marker does not replace that installed-corpus authority.
*/
@@ -196,16 +196,16 @@ function provisionRuntimeSurfaceCorpus(
}
if (required.size === 0) return;
const corpusRoot = path.join(configDir, 'gsd-core');
const corpusRoot = path.join(configDir, 'msd-core');
if (required.has('commands')) {
const source = runtimeArtifactLayout.findInstallSourceRoot();
const destination = path.join(corpusRoot, 'commands', 'gsd');
syncRuntimeSurfaceCorpus(source, destination, configDir, 'gsd-core/commands/gsd/');
const destination = path.join(corpusRoot, 'commands', 'msd');
syncRuntimeSurfaceCorpus(source, destination, configDir, 'msd-core/commands/msd/');
}
if (required.has('agents')) {
const source = path.join(executingPackageRoot(), 'agents');
const destination = path.join(corpusRoot, 'agents');
syncRuntimeSurfaceCorpus(source, destination, configDir, 'gsd-core/agents/');
syncRuntimeSurfaceCorpus(source, destination, configDir, 'msd-core/agents/');
}
const markerFile = _hostBehaviors(layout.runtime).sourceMarkerFile;
@@ -215,7 +215,7 @@ function provisionRuntimeSurfaceCorpus(
if (hasExistingSymlinkBetween(path.resolve(configDir), markerPath, { allowOptInFollow: isSymlinkedDestOptIn() })) {
throw new Error(`compatibility marker "${markerPath}" contains an untrusted symlink`);
}
installFs().writeFileSync(markerPath, path.join(corpusRoot, 'commands', 'gsd') + '\n', 'utf8');
installFs().writeFileSync(markerPath, path.join(corpusRoot, 'commands', 'msd') + '\n', 'utf8');
} catch {
// The existing installer marker writer owns the user-facing warning and
// keeps marker failure non-fatal. The installed corpus remains usable
@@ -231,13 +231,13 @@ function executingPackageRoot(): string { return path.dirname(path.dirname(runti
// ---------------------------------------------------------------------------
/**
* Single source of truth for user-owned artifacts inside gsd-core/.
* Single source of truth for user-owned artifacts inside msd-core/.
*
* These files are created/refreshed by user-facing workflows (e.g.
* /gsd-profile-user) and must be preserved across reinstalls. Critically, they
* MUST be excluded from gsd-file-manifest.json — otherwise saveLocalPatches()
* /msd-profile-user) and must be preserved across reinstalls. Critically, they
* MUST be excluded from msd-file-manifest.json — otherwise saveLocalPatches()
* will compare a refreshed file against a stale manifest hash and emit a
* spurious "locally modified GSD file" warning (bug #2771).
* spurious "locally modified MSD file" warning (bug #2771).
*
* Invariant: a file is either distribution (manifest-tracked, diff'd against
* manifest) or user artifact (preserved across installs, never diff'd). Never
@@ -245,7 +245,7 @@ function executingPackageRoot(): string { return path.dirname(path.dirname(runti
* writeManifest must agree on this list, which is why it lives here as a
* single constant.
*
* Paths are relative to the gsd-core/ directory.
* Paths are relative to the msd-core/ directory.
*/
const USER_OWNED_ARTIFACTS: string[] = ['USER-PROFILE.md'];
@@ -333,7 +333,7 @@ const SKILLS_CONVERTER_REGISTRY: Record<string, (content: string, skillName: str
* like `../../etc`. This is the line `resolvedFullPath !== resolvedRoot
* && !resolvedFullPath.startsWith(resolvedRoot + path.sep)` below.
* (b) A symlink's resolved real path equals the install root itself — this
* would let `_removeGsdEntries` (the prune pass) wipe the install root,
* would let `_removeMsdEntries` (the prune pass) wipe the install root,
* which is the config-root-wipe threat from #1704 threat model item (b).
*
* What opt-in RELAXES specifically: the "pre-existing symlink that points
@@ -349,7 +349,7 @@ const SKILLS_CONVERTER_REGISTRY: Record<string, (content: string, skillName: str
* @returns true when the caller MUST refuse; false when writes may proceed.
*/
function isSymlinkedDestOptIn(): boolean {
const v = process.env.GSD_ALLOW_SYMLINKED_DEST;
const v = process.env.MSD_ALLOW_SYMLINKED_DEST;
return v === '1' || v === 'true';
}
@@ -461,7 +461,7 @@ function hasExistingSymlinkBetween(
if (segmentLstat.isSymbolicLink()) {
if (!allowFollow) return true;
// Opt-in active: follow the symlink. Refuse if the resolved target is the
// install root itself (threat (b) — would let _removeGsdEntries wipe the
// install root itself (threat (b) — would let _removeMsdEntries wipe the
// root). Other targets are acceptable per the user's explicit opt-in. A
// broken symlink (realpathSync throws) is still refused.
//
@@ -504,18 +504,18 @@ function hasExistingSymlinkBetween(
* (test-matrix E1/E4) — this module never reimplements either decision, only
* reuses them (user-artifact-staging.cts's own module doc, "Confinement").
*
* Fixed location: `<configDir>/.gsd-staging/user-artifacts/` — a sibling of
* Fixed location: `<configDir>/.msd-staging/user-artifacts/` — a sibling of
* every directory this phase's four call sites wipe, so staging survives all
* of them while staying inside configDir (40-design.md "Staging location").
*/
function _resolveUserArtifactStagingRoot(configDir: string): string {
const stagingRoot = runtimeArtifactInstallPlan.assertDestWithinConfigHome(
configDir,
path.posix.join('.gsd-staging', 'user-artifacts'),
path.posix.join('.msd-staging', 'user-artifacts'),
);
if (hasExistingSymlinkBetween(path.resolve(configDir), stagingRoot, { allowOptInFollow: isSymlinkedDestOptIn() })) {
throw new Error(
`_resolveUserArtifactStagingRoot: staging root "${stagingRoot}" contains a symlink the install root "${configDir}" does not trust — refusing to stage. If this is an intentional user-owned symlink layout, re-run with GSD_ALLOW_SYMLINKED_DEST=1.`,
`_resolveUserArtifactStagingRoot: staging root "${stagingRoot}" contains a symlink the install root "${configDir}" does not trust — refusing to stage. If this is an intentional user-owned symlink layout, re-run with MSD_ALLOW_SYMLINKED_DEST=1.`,
);
}
return stagingRoot;
@@ -523,12 +523,12 @@ function _resolveUserArtifactStagingRoot(configDir: string): string {
/**
* Degrade-not-abort wrapper over `_resolveUserArtifactStagingRoot` (defect
* fix — a hostile/broken `.gsd-staging` path, or a symlinked configDir
* itself, e.g. nix-darwin/dotfiles-managed `~/.claude`, GSD_ALLOW_SYMLINKED_DEST's
* fix — a hostile/broken `.msd-staging` path, or a symlinked configDir
* itself, e.g. nix-darwin/dotfiles-managed `~/.claude`, MSD_ALLOW_SYMLINKED_DEST's
* own population) must never brick the command it is called from. Before
* this fix `_resolveUserArtifactStagingRoot` was called UNGUARDED as the
* first statement of both `install()` and `uninstall()` (bin/install.js) —
* `ln -s /nonexistent ~/.claude/.gsd-staging` killed both commands,
* `ln -s /nonexistent ~/.claude/.msd-staging` killed both commands,
* including uninstall, the remedy for the first problem.
*
* Returns `null` (never throws) when staging is unavailable, logging ONE
@@ -544,7 +544,7 @@ function _tryResolveUserArtifactStagingRoot(configDir: string): string | null {
return _resolveUserArtifactStagingRoot(configDir);
} catch (err) {
console.warn(
` [gsd] user-artifact staging unavailable for "${configDir}" (${(err as Error).message}) — proceeding without durable staging for this step.`,
` [msd] user-artifact staging unavailable for "${configDir}" (${(err as Error).message}) — proceeding without durable staging for this step.`,
);
return null;
}
@@ -555,13 +555,13 @@ function _tryResolveUserArtifactStagingRoot(configDir: string): string | null {
// ---------------------------------------------------------------------------
/**
* Migrate a legacy dev-preferences.md (saved from commands/gsd/) into the
* Migrate a legacy dev-preferences.md (saved from commands/msd/) into the
* runtime-aware SKILL.md location used by the writer after #2973.
*
* For runtimes with a nested skills layout (e.g. Hermes: skills/gsd/<stem>/),
* the target is <configDir>/skills/gsd/dev-preferences/SKILL.md.
* For runtimes with a flat skills layout (prefix='gsd-'), the target is
* <configDir>/skills/gsd-dev-preferences/SKILL.md.
* For runtimes with a nested skills layout (e.g. Hermes: skills/msd/<stem>/),
* the target is <configDir>/skills/msd/dev-preferences/SKILL.md.
* For runtimes with a flat skills layout (prefix='msd-'), the target is
* <configDir>/skills/msd-dev-preferences/SKILL.md.
*
* Skips silently if no legacy file was preserved, or if a SKILL.md already
* exists at the new location (don't clobber user-customized skill content
@@ -619,7 +619,7 @@ function _resolveDevPreferencesSkillTarget(targetDir: string, runtime?: string,
const layout: any = runtimeArtifactLayout.resolveRuntimeArtifactLayout(runtime, targetDir, scope as any);
const skillsKindEntry = layout.kinds.find((k: any) => k.kind === 'skills');
if (!skillsKindEntry) return null; // runtime has no skills layout at this scope (e.g. cline local)
const stemName = skillsKindEntry.prefix === '' ? 'dev-preferences' : 'gsd-dev-preferences';
const stemName = skillsKindEntry.prefix === '' ? 'dev-preferences' : 'msd-dev-preferences';
// #2911: same destination-root defect as _copyStaged/applySurface — honor
// skillsKindEntry.home as a FALLBACK-preferred override (e.g. Codex skills
// -> $HOME/.agents) instead of always resolving against targetDir, so a
@@ -630,7 +630,7 @@ function _resolveDevPreferencesSkillTarget(targetDir: string, runtime?: string,
skillDir = path.join(runtimeArtifactInstallPlan.assertDestWithinConfigHome(installRoot, skillsKindEntry.destSubpath), stemName);
} else {
// Legacy fallback for callers that have not yet been updated to pass runtime
skillDir = path.join(runtimeArtifactInstallPlan.assertDestWithinConfigHome(targetDir, 'skills'), 'gsd-dev-preferences');
skillDir = path.join(runtimeArtifactInstallPlan.assertDestWithinConfigHome(targetDir, 'skills'), 'msd-dev-preferences');
}
return { skillFile: path.join(skillDir, 'SKILL.md'), installRoot, hasHomeOverride };
}
@@ -678,7 +678,7 @@ function migrateLegacyDevPreferencesToSkill(
// Security fix: `existsSync` FOLLOWS symlinks and reports `false` for a
// DANGLING one, so the prior `existsSync(skillFile)` check never even saw a
// dangling symlink planted AT the leaf (e.g.
// `<installRoot>/skills/gsd-dev-preferences/SKILL.md ->
// `<installRoot>/skills/msd-dev-preferences/SKILL.md ->
// ~/.ssh/authorized_keys`) — it fell through past this "already migrated"
// bail, past the symlink-escape guard below (which only walks to `skillDir`,
// the parent DIRECTORY, and never lstats the leaf FILE itself), and into
@@ -698,10 +698,10 @@ function migrateLegacyDevPreferencesToSkill(
}
// Symlink-escape guard: reject if any path component between installRoot and
// skillDir is a symlink that would redirect writes outside the install root.
// #2393: honor GSD_ALLOW_SYMLINKED_DEST for intentional user-owned symlink layouts.
// #2393: honor MSD_ALLOW_SYMLINKED_DEST for intentional user-owned symlink layouts.
if (hasExistingSymlinkBetween(path.resolve(installRoot), skillDir, { allowOptInFollow: isSymlinkedDestOptIn() })) {
throw new Error(
`migrateLegacyDevPreferencesToSkill: skillDir "${skillDir}" contains a symlink the install root "${installRoot}" does not trust — refusing to write. If this is an intentional user-owned symlink layout, re-run with GSD_ALLOW_SYMLINKED_DEST=1.`,
`migrateLegacyDevPreferencesToSkill: skillDir "${skillDir}" contains a symlink the install root "${installRoot}" does not trust — refusing to write. If this is an intentional user-owned symlink layout, re-run with MSD_ALLOW_SYMLINKED_DEST=1.`,
);
}
try {
@@ -725,9 +725,9 @@ function migrateLegacyDevPreferencesToSkill(
* the whole dir into destDir.
* For commands/agents kind: iterate .md files and write them into destDir.
* - commands: write as `${prefix}${stem}.md` unless destSubpath already
* encodes the GSD namespace as its last segment (e.g. `commands/gsd`), in
* encodes the MSD namespace as its last segment (e.g. `commands/msd`), in
* which case write as `${stem}.md` (directory IS the namespace).
* - agents: write as-is (files already carry their own `gsd-` prefix).
* - agents: write as-is (files already carry their own `msd-` prefix).
* For kimi-agents kind: recursively copy generated YAML/prompt files.
*/
function _copyStaged(stagedDir: string, destDir: string, kind: any, configDir: string, runtime?: string): void {
@@ -753,10 +753,10 @@ function _copyStaged(stagedDir: string, destDir: string, kind: any, configDir: s
const resolvedDest = runtimeArtifactInstallPlan.assertDestWithinConfigHome(installRoot, destDir);
// Symlink-escape guard: reject if any path component between the install root and
// destDir is a symlink that would redirect writes outside the install root.
// #2393: honor GSD_ALLOW_SYMLINKED_DEST for intentional user-owned symlink layouts.
// #2393: honor MSD_ALLOW_SYMLINKED_DEST for intentional user-owned symlink layouts.
if (hasExistingSymlinkBetween(path.resolve(installRoot), resolvedDest, { allowOptInFollow: isSymlinkedDestOptIn() })) {
throw new Error(
`_copyStaged: destDir "${destDir}" contains a symlink the install root "${installRoot}" does not trust — refusing to write. If this is an intentional user-owned symlink layout, re-run with GSD_ALLOW_SYMLINKED_DEST=1.`,
`_copyStaged: destDir "${destDir}" contains a symlink the install root "${installRoot}" does not trust — refusing to write. If this is an intentional user-owned symlink layout, re-run with MSD_ALLOW_SYMLINKED_DEST=1.`,
);
}
// Use the validated absolute path for the actual writes below.
@@ -765,7 +765,7 @@ function _copyStaged(stagedDir: string, destDir: string, kind: any, configDir: s
installFs().mkdirSync(destDir, { recursive: true });
if (kind.kind === 'skills') {
// Each child of stagedDir is a prefixed skill directory: gsd-help/, etc.
// Each child of stagedDir is a prefixed skill directory: msd-help/, etc.
for (const entry of installFs().readdirSync(stagedDir, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const src = path.join(stagedDir, entry.name);
@@ -783,7 +783,7 @@ function _copyStaged(stagedDir: string, destDir: string, kind: any, configDir: s
// commands or agents
const entries = installFs().readdirSync(stagedDir, { withFileTypes: true });
// For commands: apply prefix unless the destSubpath's last segment already
// represents the GSD namespace (e.g. 'commands/gsd' → last segment 'gsd').
// represents the MSD namespace (e.g. 'commands/msd' → last segment 'msd').
// Single source of truth: runtimeArtifactLayout.isNamespacedByDir (#2871
// Phase 2 review finding — this rule previously drifted independently
// across install-engine.cts / surface.cts / runtime-artifact-layout.cts).
@@ -805,7 +805,7 @@ function _copyStaged(stagedDir: string, destDir: string, kind: any, configDir: s
let destName: string;
if (kind.kind === 'agents') {
// Agent files already carry the gsd- prefix in the source dir.
// Agent files already carry the msd- prefix in the source dir.
// #2099: descriptor-driven via hostBehaviors.agentFileExtension (was
// hardcoded `runtime === 'copilot'`). copilot declares '.agent.md';
// every other runtime's descriptor leaves this unset, so destName falls
@@ -829,26 +829,26 @@ function _copyStaged(stagedDir: string, destDir: string, kind: any, configDir: s
}
// ---------------------------------------------------------------------------
// _removeGsdEntries
// _removeMsdEntries
// ---------------------------------------------------------------------------
/**
* Remove GSD-prefixed entries from destDir matching kind.prefix.
* Remove MSD-prefixed entries from destDir matching kind.prefix.
* For the prefix='' case: the destSubpath IS the namespace — remove the entire
* destDir. (No current runtime uses prefix='' after #947 reversed Hermes; kept
* as a defensive guard for future runtimes.)
*/
function _removeGsdEntries(destDir: string, kind: any): void {
function _removeMsdEntries(destDir: string, kind: any): void {
if (!installFs().existsSync(destDir)) return;
if (kind.kind === 'kimi-agents') {
for (const fileName of ['gsd.yaml', 'gsd.md']) {
for (const fileName of ['msd.yaml', 'msd.md']) {
installFs().rmSync(path.join(destDir, fileName), { force: true });
}
const subagentsDir = path.join(destDir, 'subagents');
if (installFs().existsSync(subagentsDir)) {
for (const entry of installFs().readdirSync(subagentsDir, { withFileTypes: true })) {
if (!entry.isFile()) continue;
if (!entry.name.startsWith('gsd-')) continue;
if (!entry.name.startsWith('msd-')) continue;
if (!entry.name.endsWith('.yaml') && !entry.name.endsWith('.md')) continue;
installFs().rmSync(path.join(subagentsDir, entry.name), { force: true });
}
@@ -856,8 +856,8 @@ function _removeGsdEntries(destDir: string, kind: any): void {
return;
}
if (kind.prefix === '') {
// Whole-namespace removal (Hermes nested case — destSubpath is skills/gsd)
// The directory itself is the GSD namespace, so remove it entirely.
// Whole-namespace removal (Hermes nested case — destSubpath is skills/msd)
// The directory itself is the MSD namespace, so remove it entirely.
installFs().rmSync(destDir, { recursive: true, force: true });
return;
}
@@ -906,28 +906,28 @@ function _restoreDir(dir: string, snapshot: Map<string, Buffer>): void {
// ---------------------------------------------------------------------------
/**
* After the layout-driven install loop writes new gsd-<stem>/ dirs to
* skills/gsd/, remove any pre-existing bare-stem dirs (skills/gsd/<stem>/)
* that correspond to the newly installed gsd-<stem> entries.
* After the layout-driven install loop writes new msd-<stem>/ dirs to
* skills/msd/, remove any pre-existing bare-stem dirs (skills/msd/<stem>/)
* that correspond to the newly installed msd-<stem> entries.
*
* @param nestedGsdDir absolute path to skills/gsd/ category dir
* @param nestedMsdDir absolute path to skills/msd/ category dir
*/
function _removeHermesBareStemDirs(nestedGsdDir: string): void {
if (!installFs().existsSync(nestedGsdDir)) return;
const entries = installFs().readdirSync(nestedGsdDir, { withFileTypes: true });
function _removeHermesBareStemDirs(nestedMsdDir: string): void {
if (!installFs().existsSync(nestedMsdDir)) return;
const entries = installFs().readdirSync(nestedMsdDir, { withFileTypes: true });
// Collect the set of stems that were installed as gsd-<stem>/ this run.
// Collect the set of stems that were installed as msd-<stem>/ this run.
const installedStems = new Set<string>();
for (const entry of entries) {
if (entry.isDirectory() && entry.name.startsWith('gsd-')) {
installedStems.add(entry.name.slice('gsd-'.length)); // e.g. 'quick', 'dev-preferences'
if (entry.isDirectory() && entry.name.startsWith('msd-')) {
installedStems.add(entry.name.slice('msd-'.length)); // e.g. 'quick', 'dev-preferences'
}
}
// Remove any bare <stem>/ dir for which gsd-<stem>/ was just installed.
// Remove any bare <stem>/ dir for which msd-<stem>/ was just installed.
for (const entry of entries) {
if (entry.isDirectory() && !entry.name.startsWith('gsd-') && installedStems.has(entry.name)) {
installFs().rmSync(path.join(nestedGsdDir, entry.name), { recursive: true });
if (entry.isDirectory() && !entry.name.startsWith('msd-') && installedStems.has(entry.name)) {
installFs().rmSync(path.join(nestedMsdDir, entry.name), { recursive: true });
}
}
}
@@ -945,22 +945,22 @@ function _removeHermesBareStemDirs(nestedGsdDir: string): void {
* @param scope
*/
function _runLegacyInstallMigrations(runtime: string, configDir: string, scope: string = 'global'): void {
const legacyCommandsGsd = path.join(configDir, 'commands', 'gsd');
const legacyCommandsMsd = path.join(configDir, 'commands', 'msd');
// Claude / Qwen / Hermes: clean up legacy commands/gsd/ and preserve dev-preferences
// Claude / Qwen / Hermes: clean up legacy commands/msd/ and preserve dev-preferences
// for migration. The actual migration call is deferred to after all layout cleanup so
// that for Hermes the flat skills/gsd-*/ removal (below) does not delete the freshly
// created skills/gsd-dev-preferences/ skill dir.
// that for Hermes the flat skills/msd-*/ removal (below) does not delete the freshly
// created skills/msd-dev-preferences/ skill dir.
let stagedLegacyArtifacts: ReturnType<typeof userArtifactStaging.stageUserArtifacts> | null = null;
if (_hostBehaviors(runtime).legacyCommandsGsdInstallMigration) {
if (installFs().existsSync(legacyCommandsGsd)) {
if (_hostBehaviors(runtime).legacyCommandsMsdInstallMigration) {
if (installFs().existsSync(legacyCommandsMsd)) {
// #2875: staging root resolved lazily, only when there is actually
// something to stage — reused below by every other call site sharing
// this configDir.
// #2875 defect fix: DEGRADE, never abort the whole install, when the
// staging root itself cannot be resolved (e.g. a hostile/broken
// `.gsd-staging` symlink) — skip this legacy-migration block entirely
// rather than wipe legacyCommandsGsd without a durable backup (module
// `.msd-staging` symlink) — skip this legacy-migration block entirely
// rather than wipe legacyCommandsMsd without a durable backup (module
// doc "Failure posture": a wipe having staged nothing is worse than no
// staging at all). The stale legacy dir is simply left in place for a
// future successful run.
@@ -970,28 +970,28 @@ function _runLegacyInstallMigrations(runtime: string, configDir: string, scope:
// crash anywhere in this function — including the Hermes flat-skills
// wipe further down, previously inside the same in-memory-only window
// — survives via recoverOrphanedUserArtifacts on the next run.
stagedLegacyArtifacts = userArtifactStaging.stageUserArtifacts(legacyCommandsGsd, ['dev-preferences.md'], stagingRoot);
installFs().rmSync(legacyCommandsGsd, { recursive: true });
stagedLegacyArtifacts = userArtifactStaging.stageUserArtifacts(legacyCommandsMsd, ['dev-preferences.md'], stagingRoot);
installFs().rmSync(legacyCommandsMsd, { recursive: true });
}
}
}
// Hermes: remove pre-#2841 flat skills/gsd-*/ entries that lived alongside
// the new skills/gsd/ nested layout.
// Hermes: remove pre-#2841 flat skills/msd-*/ entries that lived alongside
// the new skills/msd/ nested layout.
if (runtime === 'hermes') {
const flatSkillsDir = path.join(configDir, 'skills');
if (installFs().existsSync(flatSkillsDir)) {
for (const entry of installFs().readdirSync(flatSkillsDir, { withFileTypes: true })) {
if (entry.isDirectory() && entry.name.startsWith('gsd-')) {
if (entry.isDirectory() && entry.name.startsWith('msd-')) {
installFs().rmSync(path.join(flatSkillsDir, entry.name), { recursive: true });
}
}
}
// Hermes: bare-stem skills/gsd/<stem>/ cleanup is deferred to AFTER the
// Hermes: bare-stem skills/msd/<stem>/ cleanup is deferred to AFTER the
// layout-driven install loop in installRuntimeArtifacts, where the exact set
// of staged gsd-<stem>/ dirs is known. Removing here (before staging) would
// require readGsdCommandNames() which misses skills like 'dev-preferences'
// of staged msd-<stem>/ dirs is known. Removing here (before staging) would
// require readMsdCommandNames() which misses skills like 'dev-preferences'
// that are not in the commands directory. See _removeHermesBareStemDirs().
}
@@ -1035,7 +1035,7 @@ function _runLegacyInstallMigrations(runtime: string, configDir: string, scope:
// BRICKED the command): migrateLegacyDevPreferencesToSkill correctly
// THROWS when it finds a planted/dangling symlink at the skill-file leaf
// (security fix — refusing to write through it is correct) but by this
// point legacyCommandsGsd has ALREADY been wiped (rmSync above) and
// point legacyCommandsMsd has ALREADY been wiped (rmSync above) and
// stagedLegacyArtifacts is the only surviving copy. An unguarded throw
// here propagated straight out of installRuntimeArtifacts, aborting the
// whole install/uninstall WITHOUT ever reaching the restore-or-discard
@@ -1043,7 +1043,7 @@ function _runLegacyInstallMigrations(runtime: string, configDir: string, scope:
// hit the same throw again (same brick-the-command failure mode this
// module's "DEGRADE, never abort" posture, see
// _tryResolveUserArtifactStagingRoot above, already closed for a broken
// `.gsd-staging` path). Degrade identically: catch, warn once, and treat
// `.msd-staging` path). Degrade identically: catch, warn once, and treat
// the batch as unmigrated so the restore branch below fires.
let migrated = false;
let migrationRefused = false;
@@ -1051,7 +1051,7 @@ function _runLegacyInstallMigrations(runtime: string, configDir: string, scope:
migrated = migrateLegacyDevPreferencesToSkill(configDir, savedLegacyArtifacts, runtime, scope);
} catch (err) {
console.warn(
` [gsd] dev-preferences.md migration skipped for "${configDir}" (${(err as Error).message}) — restoring the legacy copy instead.`,
` [msd] dev-preferences.md migration skipped for "${configDir}" (${(err as Error).message}) — restoring the legacy copy instead.`,
);
migrationRefused = true;
}
@@ -1078,8 +1078,8 @@ function _runLegacyInstallMigrations(runtime: string, configDir: string, scope:
if (!nothingLeftUnmigrated && stagedLegacyArtifacts.names.length > 0) {
// Put the whole batch back where it came from rather than losing
// whatever migration did not (or could not) account for.
installFs().mkdirSync(legacyCommandsGsd, { recursive: true });
userArtifactStaging.restoreStagedUserArtifacts(legacyCommandsGsd, stagedLegacyArtifacts);
installFs().mkdirSync(legacyCommandsMsd, { recursive: true });
userArtifactStaging.restoreStagedUserArtifacts(legacyCommandsMsd, stagedLegacyArtifacts);
}
userArtifactStaging.discardStagedUserArtifacts(stagedLegacyArtifacts);
}
@@ -1095,16 +1095,16 @@ function _runLegacyInstallMigrations(runtime: string, configDir: string, scope:
* @returns staged legacy artifacts for post-removal migration, or null
*/
function _runLegacyUninstallCleanup(runtime: string, configDir: string, scope: string = 'global'): ReturnType<typeof userArtifactStaging.stageUserArtifacts> | null {
// commands/gsd/ is a legacy location for Qwen, Hermes, and all Claude installs.
// Prior to #1367 fix, Claude-local used commands/gsd/<cmd>.md (colon-namespaced).
// After #1367, Claude-local uses flat commands/gsd-<cmd>.md. The inline uninstall
// commands/msd/ is a legacy location for Qwen, Hermes, and all Claude installs.
// Prior to #1367 fix, Claude-local used commands/msd/<cmd>.md (colon-namespaced).
// After #1367, Claude-local uses flat commands/msd-<cmd>.md. The inline uninstall
// block (1c) handles removal of flat files; this function handles the legacy
// commands/gsd/ directory for all Claude scopes (global was already included,
// commands/msd/ directory for all Claude scopes (global was already included,
// local is now added since that layout is also legacy post-#1367).
// #2973 / Codex review (bd1f06c9): preserve user-owned dev-preferences.md
// before destructive wipe. Migration to skills/gsd-dev-preferences/SKILL.md
// before destructive wipe. Migration to skills/msd-dev-preferences/SKILL.md
// is deferred and returned so the caller can apply it AFTER layout-driven
// removal — this prevents the layout's gsd-* prefix removal from wiping the
// removal — this prevents the layout's msd-* prefix removal from wiping the
// freshly created skill dir (same pattern as _runLegacyInstallMigrations).
// #2875 (#1874-F19): staged DURABLY to disk (userArtifactStaging), not just
// an in-memory Map — this function's own wipe below is raw `fs`, left
@@ -1114,12 +1114,12 @@ function _runLegacyUninstallCleanup(runtime: string, configDir: string, scope: s
// module does (ambient default: real fs here, since this call is never
// wrapped in withInstallFs).
let stagedLegacyArtifacts: ReturnType<typeof userArtifactStaging.stageUserArtifacts> | null = null;
// commands/gsd/ is a legacy location for Qwen, Hermes, and Claude global.
// commands/msd/ is a legacy location for Qwen, Hermes, and Claude global.
// Claude local is intentionally excluded: the inline uninstall block (1c) handles
// commands/gsd/ for claude local, preserving dev-preferences.md by restoring it
// commands/msd/ for claude local, preserving dev-preferences.md by restoring it
// to the same location (#1423). Using migrateLegacyDevPreferencesToSkill here
// (which would redirect to skills/) conflicts with the test contract for local installs.
const _lu = _hostBehaviors(runtime).legacyCommandsGsdUninstall;
const _lu = _hostBehaviors(runtime).legacyCommandsMsdUninstall;
// #2870: `scope` keeps its exported `string = 'global'` signature (no
// signature change), but every real caller — `uninstallRuntimeArtifacts`'s
// own required `scope` param, always fed a validated 'global' | 'local'
@@ -1128,42 +1128,42 @@ function _runLegacyUninstallCleanup(runtime: string, configDir: string, scope: s
// `= 'global'` default already reproduces today's behavior for an omitted
// scope, so the cast below is safe: `isGlobalScope` never sees a value
// outside its union here.
const isLegacyCommandsGsd = _lu === true || (_lu === 'global' && isGlobalScope(scope as InstallScope));
if (isLegacyCommandsGsd) {
const legacyCommandsGsd = path.join(configDir, 'commands', 'gsd');
if (fs.existsSync(legacyCommandsGsd)) {
const isLegacyCommandsMsd = _lu === true || (_lu === 'global' && isGlobalScope(scope as InstallScope));
if (isLegacyCommandsMsd) {
const legacyCommandsMsd = path.join(configDir, 'commands', 'msd');
if (fs.existsSync(legacyCommandsMsd)) {
// #2875 defect fix: DEGRADE, never abort uninstall, when the staging
// root cannot be resolved — skip this legacy-cleanup block (leave the
// stale dir in place) rather than wipe without a durable backup.
// Uninstall in particular must always be able to proceed past this
// point regardless of a hostile/broken `.gsd-staging` path.
// point regardless of a hostile/broken `.msd-staging` path.
const stagingRoot = _tryResolveUserArtifactStagingRoot(configDir);
if (stagingRoot !== null) {
stagedLegacyArtifacts = userArtifactStaging.stageUserArtifacts(legacyCommandsGsd, ['dev-preferences.md'], stagingRoot);
fs.rmSync(legacyCommandsGsd, { recursive: true });
stagedLegacyArtifacts = userArtifactStaging.stageUserArtifacts(legacyCommandsMsd, ['dev-preferences.md'], stagingRoot);
fs.rmSync(legacyCommandsMsd, { recursive: true });
}
}
}
// Hermes: pre-#2841 flat skills/gsd-*/ entries
// Hermes: pre-#2841 flat skills/msd-*/ entries
if (runtime === 'hermes') {
const flatSkillsDir = path.join(configDir, 'skills');
if (fs.existsSync(flatSkillsDir)) {
for (const entry of fs.readdirSync(flatSkillsDir, { withFileTypes: true })) {
if (entry.isDirectory() && entry.name.startsWith('gsd-')) {
if (entry.isDirectory() && entry.name.startsWith('msd-')) {
fs.rmSync(path.join(flatSkillsDir, entry.name), { recursive: true });
}
}
}
// Hermes: pre-#947 bare-stem skills/gsd/<stem>/ entries (dirs that do NOT
// start with 'gsd-') — the #3664 layout used prefix='' so GSD-owned skills
// had bare names (e.g. skills/gsd/help/). These are stale on uninstall.
const nestedGsdDirForUninstall = path.join(configDir, 'skills', 'gsd');
if (fs.existsSync(nestedGsdDirForUninstall)) {
for (const entry of fs.readdirSync(nestedGsdDirForUninstall, { withFileTypes: true })) {
if (entry.isDirectory() && !entry.name.startsWith('gsd-')) {
fs.rmSync(path.join(nestedGsdDirForUninstall, entry.name), { recursive: true });
// Hermes: pre-#947 bare-stem skills/msd/<stem>/ entries (dirs that do NOT
// start with 'msd-') — the #3664 layout used prefix='' so MSD-owned skills
// had bare names (e.g. skills/msd/help/). These are stale on uninstall.
const nestedMsdDirForUninstall = path.join(configDir, 'skills', 'msd');
if (fs.existsSync(nestedMsdDirForUninstall)) {
for (const entry of fs.readdirSync(nestedMsdDirForUninstall, { withFileTypes: true })) {
if (entry.isDirectory() && !entry.name.startsWith('msd-')) {
fs.rmSync(path.join(nestedMsdDirForUninstall, entry.name), { recursive: true });
}
}
}
@@ -1324,17 +1324,17 @@ function installRuntimeArtifacts(
// were never real to begin with; it cannot make this `if` pass for a
// path the real filesystem would refuse.
const installRoot = (kind && typeof kind.home === 'string' && kind.home !== '') ? kind.home : configDir;
// #2393: honor GSD_ALLOW_SYMLINKED_DEST for intentional user-owned symlink layouts.
// #2393: honor MSD_ALLOW_SYMLINKED_DEST for intentional user-owned symlink layouts.
// Threat model from #1704 / ADR-1239 Phase B preserved: path-traversal and
// resolved-target-equals-root still refuse regardless of opt-in.
if (hasExistingSymlinkBetween(path.resolve(installRoot), dest, { allowOptInFollow: isSymlinkedDestOptIn() })) {
throw new Error(
`installRuntimeArtifacts: destDir "${dest}" contains a symlink the install root "${installRoot}" does not trust — refusing to create. If this is an intentional user-owned symlink layout (e.g. externalized skills/hooks dir, multi-account configHome, or a dotfiles-managed configHome), re-run with GSD_ALLOW_SYMLINKED_DEST=1.`,
`installRuntimeArtifacts: destDir "${dest}" contains a symlink the install root "${installRoot}" does not trust — refusing to create. If this is an intentional user-owned symlink layout (e.g. externalized skills/hooks dir, multi-account configHome, or a dotfiles-managed configHome), re-run with MSD_ALLOW_SYMLINKED_DEST=1.`,
);
}
// #2875 defect fix (--minimal regression closed): a restricted profile
// (e.g. --minimal) can legitimately stage ZERO agents — no skill in
// the profile's closure references a gsd-* role. The pre-#2875-Part-2
// the profile's closure references a msd-* role. The pre-#2875-Part-2
// inline agent-staging loop this generic layout loop's agents handling
// replaced never created `agents/` at all under a minimal install (the
// now-deleted `isMinimalMode` branch skipped the whole step); this
@@ -1356,16 +1356,16 @@ function installRuntimeArtifacts(
// above `installAgentsKindStandalone`) skipped this kind's stale-
// agent prune along with the write whenever a restricted profile
// (e.g. --minimal) staged zero agents — that also skipped
// `_removeGsdEntries`, so a full -> minimal downgrade left every
// previously-installed gsd-*.md/.toml agent file in place. The
// `_removeMsdEntries`, so a full -> minimal downgrade left every
// previously-installed msd-*.md/.toml agent file in place. The
// deleted pre-#2875 inline loop never did that: its stale-cleanup
// pre-pass ran UNCONDITIONALLY, and only the *write* of new agent
// files was gated on minimal mode. Restore that split here: prune
// first (no-ops via `_removeGsdEntries`'s own existsSync check when
// first (no-ops via `_removeMsdEntries`'s own existsSync check when
// `dest` was never created, so a fresh install with nothing staged
// still never creates it below), then skip mkdir/copy when there is
// nothing to write.
_removeGsdEntries(dest, kind);
_removeMsdEntries(dest, kind);
if (stagedAgentFiles.length === 0) {
continue;
}
@@ -1373,21 +1373,21 @@ function installRuntimeArtifacts(
installFs().mkdirSync(dest, { recursive: true });
const preserved: string[] = [];
if (kind.kind === 'skills' && installFs().existsSync(dest)) {
// Pre-prune: snapshot user-owned content before _removeGsdEntries wipes it,
// Pre-prune: snapshot user-owned content before _removeMsdEntries wipes it,
// then restore after. This preserves user dirs across a wipe-and-replace
// install (#2973 / #3664).
//
// All runtimes (incl. Hermes after #947) use prefix='gsd-'.
// _removeGsdEntries removes only gsd-* entries; non-gsd-* user dirs are
// untouched. Preserve the explicit user-owned GSD-prefixed skill
// gsd-dev-preferences, which GSD does not reinstall from source but must
// All runtimes (incl. Hermes after #947) use prefix='msd-'.
// _removeMsdEntries removes only msd-* entries; non-msd-* user dirs are
// untouched. Preserve the explicit user-owned MSD-prefixed skill
// msd-dev-preferences, which MSD does not reinstall from source but must
// survive the prune (#2973).
const toPreserve = new Map<string, Map<string, Buffer>>(); // dirName -> Map<relPath, Buffer>
{
// Preserve explicitly user-owned GSD-prefixed skill dirs.
// gsd-dev-preferences is the sole user-customisable skill in this category.
const USER_OWNED_SKILL_DIRS = ['gsd-dev-preferences'];
// Preserve explicitly user-owned MSD-prefixed skill dirs.
// msd-dev-preferences is the sole user-customisable skill in this category.
const USER_OWNED_SKILL_DIRS = ['msd-dev-preferences'];
for (const dirName of USER_OWNED_SKILL_DIRS) {
const skillDir = path.join(dest, dirName);
if (!installFs().existsSync(skillDir)) continue;
@@ -1396,7 +1396,7 @@ function installRuntimeArtifacts(
}
}
_removeGsdEntries(dest, kind);
_removeMsdEntries(dest, kind);
_copyStaged(item.sourceDir, dest, kind, configDir, runtime);
// Restore user-owned dirs after the prune+copy
@@ -1406,8 +1406,8 @@ function installRuntimeArtifacts(
}
} else {
// For non-skills kinds (commands, agents): no user content to preserve;
// just prune stale gsd-* entries and copy new ones.
_removeGsdEntries(dest, kind);
// just prune stale msd-* entries and copy new ones.
_removeMsdEntries(dest, kind);
_copyStaged(item.sourceDir, dest, kind, configDir, runtime);
}
executedKinds.push({ kind: item.kind, sourceDir: item.sourceDir, destDir: dest, preserved });
@@ -1427,20 +1427,20 @@ function installRuntimeArtifacts(
}
}
// Hermes: after the install loop has written all gsd-<stem>/ dirs to
// skills/gsd/, remove any stale bare-stem dirs (skills/gsd/<stem>/) that
// correspond to the newly installed gsd-<stem> entries. This is the robust
// replacement for the readGsdCommandNames()-based pre-install cleanup that
// Hermes: after the install loop has written all msd-<stem>/ dirs to
// skills/msd/, remove any stale bare-stem dirs (skills/msd/<stem>/) that
// correspond to the newly installed msd-<stem> entries. This is the robust
// replacement for the readMsdCommandNames()-based pre-install cleanup that
// missed skills like 'dev-preferences' (#947 adversarial review).
//
// We run this AFTER the install loop so the installed set is authoritative:
// every gsd-<stem>/ present now was written this run (or was there before
// with the same prefix). User-owned bare dirs with no gsd-<stem> counterpart
// every msd-<stem>/ present now was written this run (or was there before
// with the same prefix). User-owned bare dirs with no msd-<stem> counterpart
// are untouched.
let hermesBareStemCleanup = false;
if (runtime === 'hermes') {
const nestedGsdDirForCleanup = path.join(configDir, 'skills', 'gsd');
_removeHermesBareStemDirs(nestedGsdDirForCleanup);
const nestedMsdDirForCleanup = path.join(configDir, 'skills', 'msd');
_removeHermesBareStemDirs(nestedMsdDirForCleanup);
hermesBareStemCleanup = true;
}
@@ -1450,7 +1450,7 @@ function installRuntimeArtifacts(
// need their declared hostBehaviors.nativePlugin file copied into configDir.
// findInstallSourceRoot resolves the repo/package root independent of
// configDir contents (marker check, then a walk-up from __dirname), so this
// is safe even when configDir has no .gsd-source marker (artifactLayout: []).
// is safe even when configDir has no .msd-source marker (artifactLayout: []).
let nativePluginInstalled = false;
if (behaviors.nativePlugin) {
// Native plugin sources live only in the executing package, never in the
@@ -1502,7 +1502,7 @@ function installRuntimeArtifacts(
* this bespoke OpenCode/Kilo writer never called. Absent -> no third-party
* skills staged (fail closed), matching the seam's own optional-registry
* contract.
* @returns number of gsd-* skill directories written
* @returns number of msd-* skill directories written
*/
function installOpencodeFamilySkills(
runtime: string,
@@ -1549,20 +1549,20 @@ function installOpencodeFamilySkills(
const dest = runtimeArtifactInstallPlan.assertDestWithinConfigHome(installRoot, skillsKindEntry.destSubpath);
// Symlink-escape guard: reject if any path component between installRoot and
// dest is a symlink that would redirect writes outside the install root.
// #2393: honor GSD_ALLOW_SYMLINKED_DEST for intentional user-owned symlink layouts.
// #2393: honor MSD_ALLOW_SYMLINKED_DEST for intentional user-owned symlink layouts.
if (hasExistingSymlinkBetween(path.resolve(installRoot), dest, { allowOptInFollow: isSymlinkedDestOptIn() })) {
throw new Error(
`installOpencodeFamilySkills: destDir "${dest}" contains a symlink the install root "${installRoot}" does not trust — refusing to write. If this is an intentional user-owned symlink layout, re-run with GSD_ALLOW_SYMLINKED_DEST=1.`,
`installOpencodeFamilySkills: destDir "${dest}" contains a symlink the install root "${installRoot}" does not trust — refusing to write. If this is an intentional user-owned symlink layout, re-run with MSD_ALLOW_SYMLINKED_DEST=1.`,
);
}
installFs().mkdirSync(dest, { recursive: true });
// Preserve user-owned GSD-prefixed skill dirs across the gsd-* prune.
// gsd-dev-preferences is generated by the user (via generate-dev-preferences)
// and lives at <configDir>/skills/gsd-dev-preferences — _removeGsdEntries
// Preserve user-owned MSD-prefixed skill dirs across the msd-* prune.
// msd-dev-preferences is generated by the user (via generate-dev-preferences)
// and lives at <configDir>/skills/msd-dev-preferences — _removeMsdEntries
// would otherwise wipe it. Mirrors the preservation in installRuntimeArtifacts
// (#2973).
const USER_OWNED_SKILL_DIRS = ['gsd-dev-preferences'];
const USER_OWNED_SKILL_DIRS = ['msd-dev-preferences'];
const toPreserve = new Map<string, Map<string, Buffer>>(); // dirName -> Map<relPath, Buffer>
for (const dirName of USER_OWNED_SKILL_DIRS) {
const skillDir = path.join(dest, dirName);
@@ -1571,7 +1571,7 @@ function installOpencodeFamilySkills(
if (snap.size > 0) toPreserve.set(dirName, snap);
}
_removeGsdEntries(dest, skillsKindEntry);
_removeMsdEntries(dest, skillsKindEntry);
let count = 0;
const firstPartyStems = new Set<string>();
@@ -1665,7 +1665,7 @@ function installOpencodeFamilySkills(
* via its OWN bespoke writers and never called `resolveRuntimeArtifactLayout`
* for agents at all before this function existed. Declaring a
* `capability.json` `agents` entry for them without this would be inert
* on the real install path while live on `/gsd:surface` (#1879-F15).
* on the real install path while live on `/msd:surface` (#1879-F15).
* 2. **Claude local** (`bin/install.js`'s `install()`, `_isSkillsRuntime ===
* false` branch) — `hostBehaviors.localInstallStyle === 'legacy-flat'`
* routes claude-local's commands/skills through `copyWithPathReplacement`
@@ -1713,7 +1713,7 @@ function installAgentsKindStandalone(
const agentsKindEntry = layout.kinds.find((kind) => kind.kind === 'agents');
if (!agentsKindEntry) return null;
// #3712: this writer selects `agentsKindEntry.home` over targetDir below and then
// prunes that destination via _removeGsdEntries, so it is a fifth route into the
// prunes that destination via _removeMsdEntries, so it is a fifth route into the
// developer's real home. No agents kind declares a `home` override today, so like
// installOpencodeFamilySkills it is guarded against a descriptor change rather
// than a present escape. Scoped to the AGENTS kind alone: passing the whole
@@ -1741,28 +1741,28 @@ function installAgentsKindStandalone(
const installRoot: string = (typeof agentsKindEntry.home === 'string' && agentsKindEntry.home !== '') ? agentsKindEntry.home : targetDir;
const dest = runtimeArtifactInstallPlan.assertDestWithinConfigHome(installRoot, agentsKindEntry.destSubpath);
// Symlink-escape guard — same gate _copyStaged/installOpencodeFamilySkills apply
// to their own writes (#2393 GSD_ALLOW_SYMLINKED_DEST opt-in preserved). Runs
// to their own writes (#2393 MSD_ALLOW_SYMLINKED_DEST opt-in preserved). Runs
// even when nothing will be written this call — the stale-agent prune below
// (`_removeGsdEntries`) still touches `dest` whenever it already exists.
// (`_removeMsdEntries`) still touches `dest` whenever it already exists.
if (hasExistingSymlinkBetween(path.resolve(installRoot), dest, { allowOptInFollow: isSymlinkedDestOptIn() })) {
throw new Error(
`installAgentsKindStandalone: destDir "${dest}" contains a symlink the install root "${installRoot}" does not trust — refusing to write. If this is an intentional user-owned symlink layout, re-run with GSD_ALLOW_SYMLINKED_DEST=1.`,
`installAgentsKindStandalone: destDir "${dest}" contains a symlink the install root "${installRoot}" does not trust — refusing to write. If this is an intentional user-owned symlink layout, re-run with MSD_ALLOW_SYMLINKED_DEST=1.`,
);
}
// #2875 defect fix, corrected: the ORIGINAL fix returned `null` (no-op)
// whenever a restricted profile (e.g. --minimal) staged ZERO agents,
// which — because that early return sat ABOVE the prune call — also
// skipped `_removeGsdEntries`, leaving every previously-installed
// gsd-*.md/.toml agent file in place on a full -> minimal downgrade. The
// skipped `_removeMsdEntries`, leaving every previously-installed
// msd-*.md/.toml agent file in place on a full -> minimal downgrade. The
// deleted pre-#2875 inline loop never did that: its stale-cleanup pre-pass
// ran UNCONDITIONALLY (removing gsd-*.md, plus .toml for codex), and only
// ran UNCONDITIONALLY (removing msd-*.md, plus .toml for codex), and only
// the *write* of new agent files was gated on minimal mode. Restore that
// split: prune first — a no-op via `_removeGsdEntries`'s own existsSync
// split: prune first — a no-op via `_removeMsdEntries`'s own existsSync
// check when `dest` was never created, so a fresh install with nothing
// staged still never creates it below — then skip mkdir/copy (and return
// `null`, matching the doc comment above) when there is nothing to write.
_removeGsdEntries(dest, agentsKindEntry);
_removeMsdEntries(dest, agentsKindEntry);
if (stagedAgentFiles.length === 0) return null;
installFs().mkdirSync(dest, { recursive: true });
@@ -1777,7 +1777,7 @@ function installAgentsKindStandalone(
/**
* Install the flattened commands surface for an OpenCode-family runtime
* (OpenCode/Kilo): commands/gsd/**\/*.md -> command/gsd-<...>.md, with
* (OpenCode/Kilo): commands/msd/**\/*.md -> command/msd-<...>.md, with
* per-runtime frontmatter conversion and path-prefix/attribution rewrites.
*
* Mirrors bin/install.js's copyFlattenedCommands VERBATIM (ADR-1239 /
@@ -1786,10 +1786,10 @@ function installAgentsKindStandalone(
*
* @param runtime - 'opencode' or 'kilo'
* @param destDir - destination directory for flattened commands (recurses with the same destDir)
* @param srcDir - source directory to walk (commands/gsd/, recursing into subdirectories)
* @param srcDir - source directory to walk (commands/msd/, recursing into subdirectories)
* @param pathPrefix - computed config-path prefix for body rewrites
* @param resolveAttribution - injection: (runtime) => attribution string | undefined
* @param prefix - filename prefix accumulator (defaults to 'gsd'; grows on recursion)
* @param prefix - filename prefix accumulator (defaults to 'msd'; grows on recursion)
*/
function installOpencodeFamilyCommands(
runtime: string,
@@ -1797,11 +1797,11 @@ function installOpencodeFamilyCommands(
srcDir: string,
pathPrefix: string,
resolveAttribution: ResolveAttribution = () => undefined,
prefix: string = 'gsd',
prefix: string = 'msd',
): void {
if (!installFs().existsSync(srcDir)) return;
// Remove old gsd-*.md files before copying new ones
// Remove old msd-*.md files before copying new ones
if (installFs().existsSync(destDir)) {
for (const file of installFs().readdirSync(destDir)) {
if (file.startsWith(`${prefix}-`) && file.endsWith('.md')) installFs().unlinkSync(path.join(destDir, file));
@@ -1857,7 +1857,7 @@ function installOpencodeFamilyCommands(
* @param runtime - canonical runtime id (only used for the assertDestWithinConfigHome guard)
* @param configDir - resolved runtime config directory
* @param behaviors - the runtime's hostBehaviors descriptor
* @param src - repo/package root (two levels up from the commands/gsd source dir)
* @param src - repo/package root (two levels up from the commands/msd source dir)
*/
function _installNativePluginIfDeclared(
runtime: string,
@@ -1891,14 +1891,14 @@ function _installNativePluginIfDeclared(
// plugin discovery: OpenCode auto-discovers `plugins/*.{ts,js}` and pi's
// isExtensionFile() accepts only `.ts`/`.js` (see installer-migration
// 006), so a package.json here is never treated as a plugin. Never
// written over a package.json GSD does not own — but when one is already
// written over a package.json MSD does not own — but when one is already
// there, say so: the adapter is CommonJS and will not load under a
// foreign `"type": "module"`, and a silent no-op would leave every guard
// the adapter spawns dead with no diagnostic (the #2305 failure shape).
const markerOutcome = ensureCommonJsMarker(path.dirname(destPath));
if (markerOutcome === 'preserved-foreign') {
console.warn(
` ⚠ ${np.dir}/package.json is not GSD's CommonJS marker — left untouched. `
` ⚠ ${np.dir}/package.json is not MSD's CommonJS marker — left untouched. `
+ `If it declares "type": "module", ${np.file} will not load.`,
);
} else if (markerOutcome === 'failed') {
@@ -1930,7 +1930,7 @@ function _installNativePluginIfDeclared(
* generic branch, which combined-family runtimes otherwise skip entirely).
*
* Ownership safety mirrors installer-migrations 003
* (rename-get-shit-done-to-gsd-core): only files present, and unchanged or
* (rename-get-shit-done-to-msd-core): only files present, and unchanged or
* locally modified, in the PRIOR install manifest under the legacy
* `command/<file>` key are removed here — the materialization call
* immediately following writes the current command set fresh into the new
@@ -1976,7 +1976,7 @@ function _migrateLegacyOpencodeCommandDir(runtime: string, configDir: string, be
return;
}
for (const entry of entries) {
// command/ is a flat directory of gsd-*.md files; skip anything that
// command/ is a flat directory of msd-*.md files; skip anything that
// isn't a plain file (nested dirs, symlinks) rather than guess intent.
if (!entry.isFile()) continue;
const relPath = `${LEGACY_NAME}/${entry.name}`;
@@ -1984,7 +1984,7 @@ function _migrateLegacyOpencodeCommandDir(runtime: string, configDir: string, be
if (classification === 'managed-pristine' || classification === 'managed-modified') {
try { installFs().unlinkSync(path.join(legacyDir, entry.name)); } catch { /* best-effort */ }
}
// 'unknown' (not manifest-tracked) is left untouched — GSD cannot prove
// 'unknown' (not manifest-tracked) is left untouched — MSD cannot prove
// ownership, so it must never be deleted as collateral damage.
}
@@ -2038,14 +2038,14 @@ function installOpencodeFamilyArtifacts(
// ternary (both real call sites and every test call site), so the cast is
// safe: `isGlobalScope` never sees a value outside its union here.
const isGlobal = isGlobalScope(scope as InstallScope);
// findInstallSourceRoot resolves DIRECTLY to the commands/gsd source dir
// (via the .gsd-source marker or a walk-up from __dirname) — every other
// findInstallSourceRoot resolves DIRECTLY to the commands/msd source dir
// (via the .msd-source marker or a walk-up from __dirname) — every other
// call site in runtime-artifact-layout.cts feeds its return value straight
// into stageSkillsForProfile/stageSkillsForRuntimeAsSkills. The repo/package
// root (needed below for the native plugin source) is two levels up.
const commandsGsdDir = runtimeArtifactLayout.findInstallSourceRoot(configDir);
const commandsMsdDir = runtimeArtifactLayout.findInstallSourceRoot(configDir);
const src = packageRoot ?? executingPackageRoot();
const rawCommandsDir = installProfiles.stageSkillsForProfile(commandsGsdDir, resolvedProfile);
const rawCommandsDir = installProfiles.stageSkillsForProfile(commandsMsdDir, resolvedProfile);
const pathPrefix = (runtimeArtifactConversion as any)._computePathPrefix({
isGlobal,
@@ -2104,7 +2104,7 @@ function installOpencodeFamilyArtifacts(
/**
* Layout-driven uninstall orchestrator.
* Runs legacy cleanup first, then uses resolveRuntimeArtifactLayout to
* determine which GSD-owned entries to remove.
* determine which MSD-owned entries to remove.
*
* @param runtime canonical runtime ID
* @param configDir resolved runtime config directory
@@ -2123,14 +2123,14 @@ function uninstallRuntimeArtifacts(
retiredArtifactCleanup.pruneRetiredRuntimeArtifacts(runtime, configDir);
// Legacy cleanup before layout-driven removal (scope-aware to avoid
// removing Claude local commands/gsd/ which is the primary install dir).
// removing Claude local commands/msd/ which is the primary install dir).
// Returns staged user artifacts so we can migrate AFTER layout removal
// (the layout's gsd-* prefix pass would wipe a skill dir created here).
// (the layout's msd-* prefix pass would wipe a skill dir created here).
const stagedLegacyArtifacts = _runLegacyUninstallCleanup(runtime, configDir, scope);
const layout: any = runtimeArtifactLayout.resolveRuntimeArtifactLayout(runtime, configDir, scope as any);
// #3712: uninstall resolves the SAME `kind.home` override as install and then
// prunes it via _removeGsdEntries below, so it is a second escape route into
// prunes it via _removeMsdEntries below, so it is a second escape route into
// the developer's real home, not a read-only path. Guard it identically.
testHomeGuard.assertTestHomeSandboxed('uninstallRuntimeArtifacts', runtime, layout?.kinds, {
os: deps.os, env: deps.env,
@@ -2142,29 +2142,29 @@ function uninstallRuntimeArtifacts(
if (!kind) {
throw new Error(`Runtime artifact uninstall plan referenced unknown kind: ${item.kind}`);
}
_removeGsdEntries(item.destDir, kind);
_removeMsdEntries(item.destDir, kind);
}
// Hermes: after removing gsd-* skill dirs from skills/gsd/, also remove
// the GSD-managed DESCRIPTION.md and then the category dir itself if it
// contains no user content (#947). _removeGsdEntries removed gsd-* dirs
// Hermes: after removing msd-* skill dirs from skills/msd/, also remove
// the MSD-managed DESCRIPTION.md and then the category dir itself if it
// contains no user content (#947). _removeMsdEntries removed msd-* dirs
// but left the category container and DESCRIPTION.md intact.
if (runtime === 'hermes') {
const nestedGsdDir = path.join(configDir, 'skills', 'gsd');
if (fs.existsSync(nestedGsdDir)) {
// Remove GSD-owned DESCRIPTION.md (written by writeHermesCategoryDescription)
fs.rmSync(path.join(nestedGsdDir, 'DESCRIPTION.md'), { force: true });
const nestedMsdDir = path.join(configDir, 'skills', 'msd');
if (fs.existsSync(nestedMsdDir)) {
// Remove MSD-owned DESCRIPTION.md (written by writeHermesCategoryDescription)
fs.rmSync(path.join(nestedMsdDir, 'DESCRIPTION.md'), { force: true });
// Remove the category dir if empty (no user content remaining)
const remaining = fs.readdirSync(nestedGsdDir, { withFileTypes: true });
const remaining = fs.readdirSync(nestedMsdDir, { withFileTypes: true });
if (remaining.length === 0) {
fs.rmSync(nestedGsdDir, { recursive: true, force: true });
fs.rmSync(nestedMsdDir, { recursive: true, force: true });
}
}
}
// #2973 / Codex review (bd1f06c9): migrate dev-preferences.md to the
// runtime-aware SKILL.md location after all layout-driven removal is
// complete. Do NOT restore to commands/gsd/ — the user is uninstalling.
// complete. Do NOT restore to commands/msd/ — the user is uninstalling.
if (stagedLegacyArtifacts) {
// #2875: read the content back from the DISK-staged copy, matching
// _runLegacyInstallMigrations's call site — never restored on failure
@@ -2177,7 +2177,7 @@ function uninstallRuntimeArtifacts(
// symlinked user artifact is recreated AS a symlink in the staging tree,
// never copied by content) would previously have its REFERENT's bytes
// read here and land in SKILL.md verbatim — e.g. a symlink to
// `~/.ssh/id_rsa` gets its private key content written into a file GSD
// `~/.ssh/id_rsa` gets its private key content written into a file MSD
// loads into agent context. A symlink to a DIRECTORY instead throws
// EISDIR uncaught out of this function, which the caller never expected
// and which left the staged entry undiscarded (re-materializing on the
@@ -2225,7 +2225,7 @@ export = {
USER_OWNED_ARTIFACTS,
_runLegacyInstallMigrations,
_runLegacyUninstallCleanup,
_removeGsdEntries,
_removeMsdEntries,
_snapshotDir,
_restoreDir,
_removeHermesBareStemDirs,

View File

@@ -85,7 +85,7 @@
* `installFs().rmSync(entryDir)` unconditionally as its entryDir-clearing
* step, and a `deps.fs` that omits `rmSync` (an easy oversight — nothing in
* the type system catches a plain-JS test object missing a key at runtime)
* silently deleted the real `<configDir>/.gsd-staging/<key>` on disk instead
* silently deleted the real `<configDir>/.msd-staging/<key>` on disk instead
* of failing loudly or staying confined to the fake's own store.
*
* `withInstallFs` now builds a GUARDED adapter instead: every method the
@@ -120,11 +120,11 @@
* ─────────────────────────────────────────────────────────────────────────
* `findInstallSourceRoot` / `findAgentsSourceRoot`'s walk-up-from-__dirname
* step (runtime-artifact-layout.cts) locates THIS PACKAGE'S OWN source tree
* (`commands/gsd/`, `agents/`) — not the install destination — so it uses
* (`commands/msd/`, `agents/`) — not the install destination — so it uses
* `fs.statSync` directly, unrouted. A fake destination adapter's store
* starts empty and is never seeded with real repo paths; routing this walk
* through it would make every fake-adapter install throw
* "could not locate commands/gsd", not gracefully stage nothing. See the
* "could not locate commands/msd", not gracefully stage nothing. See the
* comment at each function's Step 2 for the full argument.
*
* RULE (40-design.md "Known limits"): this seam makes *destination* IO

View File

@@ -7,7 +7,7 @@
* chain across two runtime branches (OpenCode ~24 lines, Kilo ~24 lines) —
* `model_overrides[agent]` > `model_profile_overrides.<runtime>.<tier>` > omit.
* Extracted here — a `src/*.cts` module compiled into the shipped
* `gsd-core/bin/lib/` tree, mirroring `install-effort-resolver.cts`'s existing
* `msd-core/bin/lib/` tree, mirroring `install-effort-resolver.cts`'s existing
* precedent (#2071) — so the descriptor-driven agents pipeline
* (`convertedAgentsKind` / `stageAgentsForRuntimeWithConverter`) and
* bin/install.js's own callers resolve through the SAME code. A single source
@@ -15,8 +15,8 @@
* silently across two hand-maintained copies (the Generative Fix Divergence
* class CLAUDE.md's "Known Defects" section warns about).
*
* `readGsdGlobalModelOverrides` / `readGsdEffectiveModelOverrides` /
* `readGsdRuntimeProfileResolver` are impure (config-file reads);
* `readMsdGlobalModelOverrides` / `readMsdEffectiveModelOverrides` /
* `readMsdRuntimeProfileResolver` are impure (config-file reads);
* `resolveAgentModelOverride` is pure given their pre-resolved outputs.
*
* #2875 defect fix: this module is on the `installRuntimeArtifacts` call tree
@@ -36,16 +36,16 @@ import installFsAdapter = require('./install-fs-adapter.cjs');
const { installFs } = installFsAdapter;
// eslint-disable-next-line @typescript-eslint/no-require-imports -- install-effort-resolver.cjs is an export= CommonJS module
import installEffortResolver = require('./install-effort-resolver.cjs');
const { _readGsdConfigFile, _findAncestorGsdConfigPath } = installEffortResolver as {
_readGsdConfigFile: (absPath: string, label: string) => Record<string, unknown> | null;
_findAncestorGsdConfigPath: (targetDir: string) => string | null;
const { _readMsdConfigFile, _findAncestorMsdConfigPath } = installEffortResolver as {
_readMsdConfigFile: (absPath: string, label: string) => Record<string, unknown> | null;
_findAncestorMsdConfigPath: (targetDir: string) => string | null;
};
// eslint-disable-next-line @typescript-eslint/no-require-imports -- model-catalog.cjs is an export= CommonJS module
import modelCatalog = require('./model-catalog.cjs');
const { MODEL_PROFILES: GSD_MODEL_PROFILES } = modelCatalog as unknown as { MODEL_PROFILES: Record<string, Record<string, string>> };
const { MODEL_PROFILES: MSD_MODEL_PROFILES } = modelCatalog as unknown as { MODEL_PROFILES: Record<string, Record<string, string>> };
// eslint-disable-next-line @typescript-eslint/no-require-imports -- model-resolver.cjs is an export= CommonJS module
import modelResolverModule = require('./model-resolver.cjs');
const { resolveTierEntry: gsdResolveTierEntry, resolveModelPolicy: gsdResolveModelPolicy } = modelResolverModule as {
const { resolveTierEntry: msdResolveTierEntry, resolveModelPolicy: msdResolveModelPolicy } = modelResolverModule as {
resolveTierEntry: (opts: { runtime: string; tier: string; overrides: unknown }) => { model?: string } | null;
resolveModelPolicy: (policy: unknown, tier: string | null | undefined) => string | null;
};
@@ -60,18 +60,18 @@ interface RuntimeProfileResolver {
}
/**
* Read `model_overrides` from `~/.gsd/defaults.json` at install time.
* Read `model_overrides` from `~/.msd/defaults.json` at install time.
* Returns an object mapping agent names to model IDs, or null if the file
* doesn't exist or has no `model_overrides` entry.
*
* Mirrors bin/install.js's prior local `readGsdGlobalModelOverrides` exactly
* Mirrors bin/install.js's prior local `readMsdGlobalModelOverrides` exactly
* (silent try/catch — no stderr warning on malformed JSON, unlike
* `_readGsdConfigFile`'s effort-config sibling — preserved for byte-parity).
* `_readMsdConfigFile`'s effort-config sibling — preserved for byte-parity).
*/
function readGsdGlobalModelOverrides(options: ReadOptions = {}): Record<string, string> | null {
function readMsdGlobalModelOverrides(options: ReadOptions = {}): Record<string, string> | null {
try {
const home = options.homedir ? options.homedir() : os.homedir();
const defaultsPath = path.join(home, '.gsd', 'defaults.json');
const defaultsPath = path.join(home, '.msd', 'defaults.json');
if (!installFs().existsSync(defaultsPath)) return null;
const raw = installFs().readFileSync(defaultsPath, 'utf-8');
const parsed = JSON.parse(raw) as { model_overrides?: unknown };
@@ -86,7 +86,7 @@ function readGsdGlobalModelOverrides(options: ReadOptions = {}): Record<string,
/**
* Effective per-agent `model_overrides` for the install path.
*
* Merges `~/.gsd/defaults.json` (global) with per-project
* Merges `~/.msd/defaults.json` (global) with per-project
* `<project>/.planning/config.json`. Per-project keys win on conflict; keys
* present in only one source are preserved from that source (#2256).
*
@@ -97,17 +97,17 @@ function readGsdGlobalModelOverrides(options: ReadOptions = {}): Record<string,
* Returns a plain `{ agentName: modelId }` object, or `null` when neither
* source defines `model_overrides`.
*/
function readGsdEffectiveModelOverrides(targetDir: string | null = null, options: ReadOptions = {}): Record<string, string> | null {
const global = readGsdGlobalModelOverrides(options);
function readMsdEffectiveModelOverrides(targetDir: string | null = null, options: ReadOptions = {}): Record<string, string> | null {
const global = readMsdGlobalModelOverrides(options);
let projectOverrides: Record<string, string> | null = null;
if (targetDir) {
// #2875 defect fix (Generative Fix Divergence): the 8-deep upward walk to
// `.planning/config.json` is single-sourced in install-effort-resolver.cts
// (`_findAncestorGsdConfigPath`) — this module was itself extracted FROM
// (`_findAncestorMsdConfigPath`) — this module was itself extracted FROM
// that one to stop duplicating shared install-time config logic, so a
// second hand-rolled copy of the walk here defeated the point.
const candidate = _findAncestorGsdConfigPath(targetDir);
const candidate = _findAncestorMsdConfigPath(targetDir);
if (candidate) {
try {
const parsed = JSON.parse(installFs().readFileSync(candidate, 'utf-8')) as { model_overrides?: unknown };
@@ -115,8 +115,8 @@ function readGsdEffectiveModelOverrides(targetDir: string | null = null, options
projectOverrides = parsed.model_overrides as Record<string, string>;
}
} catch {
// Malformed config.json — fall back to global; readGsdRuntimeProfileResolver
// surfaces a parse warning via _readGsdConfigFile already.
// Malformed config.json — fall back to global; readMsdRuntimeProfileResolver
// surfaces a parse warning via _readMsdConfigFile already.
}
}
}
@@ -128,7 +128,7 @@ function readGsdEffectiveModelOverrides(targetDir: string | null = null, options
type AgentTools = Record<string, string[]>;
function readGsdAgentTools(config: Record<string, unknown> | null): AgentTools | null {
function readMsdAgentTools(config: Record<string, unknown> | null): AgentTools | null {
const raw = config?.agent_tools;
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
@@ -153,20 +153,20 @@ function readGsdAgentTools(config: Record<string, unknown> | null): AgentTools |
* selectors; malformed whole files remain harmless absence like the existing
* model override resolver.
*/
function readGsdEffectiveAgentTools(targetDir: string | null = null, options: ReadOptions = {}): AgentTools | null {
function readMsdEffectiveAgentTools(targetDir: string | null = null, options: ReadOptions = {}): AgentTools | null {
const home = options.homedir ? options.homedir() : os.homedir();
const globalConfig = _readGsdConfigFile(path.join(home, '.gsd', 'defaults.json'), 'global defaults');
const globalConfig = _readMsdConfigFile(path.join(home, '.msd', 'defaults.json'), 'global defaults');
let projectConfig: Record<string, unknown> | null = null;
if (targetDir) {
const candidate = _findAncestorGsdConfigPath(targetDir);
const candidate = _findAncestorMsdConfigPath(targetDir);
if (candidate) {
projectConfig = _readGsdConfigFile(candidate, 'project config');
projectConfig = _readMsdConfigFile(candidate, 'project config');
}
}
const global = readGsdAgentTools(globalConfig);
const project = readGsdAgentTools(projectConfig);
const global = readMsdAgentTools(globalConfig);
const project = readMsdAgentTools(projectConfig);
if (projectConfig
&& Object.prototype.hasOwnProperty.call(projectConfig, 'agent_tools')
&& project === null) return {};
@@ -192,25 +192,25 @@ interface RuntimeProfileMergedConfig {
* Build a runtime-aware tier resolver for the install path (#2517).
*
* Probes BOTH per-project `<targetDir>/.planning/config.json` AND
* `~/.gsd/defaults.json`, with per-project keys winning over global.
* `~/.msd/defaults.json`, with per-project keys winning over global.
*
* Returns null if no `runtime` is configured, if `model_profile` is
* `inherit`, or if no project config is reachable AND `~/.gsd/defaults.json`
* `inherit`, or if no project config is reachable AND `~/.msd/defaults.json`
* declares no `model_profile` (#3543 — an unverifiable profile is never
* baked, letting the runtime's own default/session model govern).
*/
function readGsdRuntimeProfileResolver(targetDir: string | null = null): RuntimeProfileResolver | null {
const homeDefaults = _readGsdConfigFile(
path.join(os.homedir(), '.gsd', 'defaults.json'),
'~/.gsd/defaults.json',
function readMsdRuntimeProfileResolver(targetDir: string | null = null): RuntimeProfileResolver | null {
const homeDefaults = _readMsdConfigFile(
path.join(os.homedir(), '.msd', 'defaults.json'),
'~/.msd/defaults.json',
);
// #2875 defect fix (Generative Fix Divergence): same shared walk as
// readGsdEffectiveModelOverrides above — see that call site's comment.
// readMsdEffectiveModelOverrides above — see that call site's comment.
let projectConfig: Record<string, unknown> | null = null;
if (targetDir) {
const candidate = _findAncestorGsdConfigPath(targetDir);
if (candidate) projectConfig = _readGsdConfigFile(candidate, '.planning/config.json');
const candidate = _findAncestorMsdConfigPath(targetDir);
if (candidate) projectConfig = _readMsdConfigFile(candidate, '.planning/config.json');
}
const merged: RuntimeProfileMergedConfig = {
@@ -246,7 +246,7 @@ function readGsdRuntimeProfileResolver(targetDir: string | null = null): Runtime
return {
runtime,
resolve(agentName: string) {
const agentModels = GSD_MODEL_PROFILES[agentName];
const agentModels = MSD_MODEL_PROFILES[agentName];
if (!agentModels) return null;
const tier = agentModels[profile] || agentModels.balanced;
if (!tier) return null;
@@ -275,9 +275,9 @@ function readGsdRuntimeProfileResolver(targetDir: string | null = null): Runtime
const policyForRuntime = merged.model_policy
? { ...(merged.model_policy as Record<string, unknown>), runtime }
: null;
const policyModel = gsdResolveModelPolicy(policyForRuntime, tier);
const policyModel = msdResolveModelPolicy(policyForRuntime, tier);
if (policyModel) return { model: policyModel };
return gsdResolveTierEntry({
return msdResolveTierEntry({
runtime,
tier,
overrides: merged.model_profile_overrides,
@@ -313,9 +313,9 @@ function resolveAgentModelOverride(
}
export = {
readGsdGlobalModelOverrides,
readGsdEffectiveModelOverrides,
readGsdEffectiveAgentTools,
readGsdRuntimeProfileResolver,
readMsdGlobalModelOverrides,
readMsdEffectiveModelOverrides,
readMsdEffectiveAgentTools,
readMsdRuntimeProfileResolver,
resolveAgentModelOverride,
};

View File

@@ -33,7 +33,7 @@ const {
applyAgentPathRewrites: _applyAgentPathRewrites,
processAttribution: _processAttribution,
normalizeAgentBodyForRuntime: _normalizeAgentBodyForRuntime,
readGsdCommandNames: _readGsdCommandNames,
readMsdCommandNames: _readMsdCommandNames,
deriveAgentName: _deriveAgentName,
applyAgentFrontmatterExtensions: _applyAgentFrontmatterExtensions,
appendAgentTools: _appendAgentTools,
@@ -41,7 +41,7 @@ const {
applyAgentPathRewrites: (content: string, runtime: string, pathPrefix: string) => string;
processAttribution: (content: string, attribution: string | null | undefined) => string;
normalizeAgentBodyForRuntime: (content: string, runtime: string, cmdNames: string[]) => string;
readGsdCommandNames: () => string[];
readMsdCommandNames: () => string[];
// #2875 Part 2: single-sourced agent-name derivation + the frontmatter-
// extensions step (effort/disallowedTools injection), driven by the
// runtime descriptor's hostBehaviors.agentFrontmatterExtensions.
@@ -120,7 +120,7 @@ type ProfileName = keyof typeof PROFILES;
* Returns string[] — empty array if no requires: field.
*
* No external YAML parser dependency — hand-parse the single line
* since GSD enforces flow-style arrays for requires:.
* since MSD enforces flow-style arrays for requires:.
*/
function parseRequires(content: string): string[] {
const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/m);
@@ -141,26 +141,26 @@ function parseRequires(content: string): string[] {
/**
* Parse agent references from a skill file's body text.
* Scans the full content for `gsd-<stem>` patterns that correspond to
* real agent files. Returns all unique `gsd-*` stems found in the body.
* Scans the full content for `msd-<stem>` patterns that correspond to
* real agent files. Returns all unique `msd-*` stems found in the body.
*
* The caller is responsible for filtering by which agents actually exist —
* this function returns all syntactically valid `gsd-*` matches.
* this function returns all syntactically valid `msd-*` matches.
*/
function parseCallsAgents(content: string): string[] {
// Match word-boundary gsd-<stem> patterns; stems are lowercase letters and hyphens.
// We use a regex that matches `gsd-` followed by one or more lowercase-alpha-or-hyphen chars.
// This catches `gsd-planner`, `gsd-plan-checker`, etc. in prose and code.
const matches = content.match(/\bgsd-[a-z][a-z-]*/g);
// Match word-boundary msd-<stem> patterns; stems are lowercase letters and hyphens.
// We use a regex that matches `msd-` followed by one or more lowercase-alpha-or-hyphen chars.
// This catches `msd-planner`, `msd-plan-checker`, etc. in prose and code.
const matches = content.match(/\bmsd-[a-z][a-z-]*/g);
if (!matches) return [];
// Deduplicate
return [...new Set(matches)];
}
/**
* Load the requires: dependency graph from a commands/gsd directory.
* Load the requires: dependency graph from a commands/msd directory.
* Also derives calls_agents for each skill by scanning the body text for
* `gsd-*` agent name references. Agent stems are stored under the special
* `msd-*` agent name references. Agent stems are stored under the special
* key `_calls_agents_<stem>` so they don't conflict with skill stems.
*
* #3798: command bodies are thin delegators — the actual `subagent_type=`
@@ -168,19 +168,19 @@ function parseCallsAgents(content: string): string[] {
* (`@…/workflows/<name>.md`). The agent derivation therefore ALSO reads every
* workflow file the command references (plus the workflow's steps/ fragments
* when it is split per the progressive-disclosure pattern) and unions their
* `gsd-*` tokens into the same `_calls_agents_<stem>` set. Without this,
* `msd-*` tokens into the same `_calls_agents_<stem>` set. Without this,
* tiered profiles omitted agents their own installed skills spawn
* (gsd-verifier at execute-phase's verify_phase_goal was the filed repro).
* (msd-verifier at execute-phase's verify_phase_goal was the filed repro).
* Over-inclusion fails safe: a tiered profile installing one extra agent
* costs bytes, never a broken spawn.
*/
const DEFAULT_COMMANDS_DIR = path.resolve(__dirname, '..', '..', '..', 'commands', 'gsd');
const DEFAULT_COMMANDS_DIR = path.resolve(__dirname, '..', '..', '..', 'commands', 'msd');
const DEFAULT_WORKFLOWS_DIR = path.resolve(__dirname, '..', '..', 'workflows');
/**
* Collect the `gsd-*` agent tokens from every workflow file a command body
* Collect the `msd-*` agent tokens from every workflow file a command body
* references. References are the `workflows/<name>.md` path suffixes the
* delegating commands embed (`@…/gsd-core/workflows/<name>.md`). A
* delegating commands embed (`@…/msd-core/workflows/<name>.md`). A
* split workflow (workflows/<name>/steps/*.md) contributes its fragments as
* well, because the parent dispatches into them and the spawns live there.
*/
@@ -197,7 +197,7 @@ function workflowAgentRefs(content: string, workflowsDir: string): string[] {
// Split workflow: union EVERY fragment under the workflow's directory
// (steps/, modes/, templates/…) — the parent dispatches into these per
// the progressive-disclosure pattern, and spawns live in all of them
// (#3798 review: modes/ carried gsd-advisor-researcher's spawn while the
// (#3798 review: modes/ carried msd-advisor-researcher's spawn while the
// parent only named it incidentally in prose).
const fragDir = path.join(workflowsDir, name.slice(0, -3));
const walk = (dir: string): void => {
@@ -448,14 +448,14 @@ function ensureExitCleanup(): void {
}
/**
* Stage a filtered copy of commands/gsd for a resolved profile.
* Stage a filtered copy of commands/msd for a resolved profile.
* In full mode (skills === '*') returns srcDir unchanged (no-op).
*/
function stageSkillsForProfile(srcDir: string, resolvedProfile: ResolvedProfile): string {
if (resolvedProfile.skills === '*') return srcDir;
if (!installFs().existsSync(srcDir)) return srcDir;
const stageDir = mkInstallTempDir('gsd-profile-skills-');
const stageDir = mkInstallTempDir('msd-profile-skills-');
try {
const entries = installFs().readdirSync(srcDir, { withFileTypes: true });
for (const entry of entries) {
@@ -479,13 +479,13 @@ function stageSkillsForProfile(srcDir: string, resolvedProfile: ResolvedProfile)
/**
* Stage a filtered copy of the agents directory for a resolved profile.
* For 'full', returns srcAgentsDir unchanged.
* For tiered profiles, copies only agents whose full stem (e.g. 'gsd-planner')
* For tiered profiles, copies only agents whose full stem (e.g. 'msd-planner')
* is in resolvedProfile.agents — which is populated by resolveProfile() from
* the _calls_agents_* entries in the manifest.
*
* ⚠️ RAW STAGER — ITS OUTPUT IS NOT EMISSION-READY (#2995). This stager performs a
* plain `fs.copyFileSync` and — under the default `full` profile — short-circuits
* and returns the real source directory unstaged. It does NOT strip `gsd:section`
* and returns the real source directory unstaged. It does NOT strip `msd:section`
* markers. It is still called, by `bin/install.js`'s `_stageAgents`, whose output
* feeds the inline agent loop and `installCodexConfig`; both of those compose the
* content themselves before writing, so the raw output never reaches disk. What
@@ -500,14 +500,14 @@ function stageAgentsForProfile(srcAgentsDir: string, resolvedProfile: ResolvedPr
if (resolvedProfile.skills === '*') return srcAgentsDir;
if (!installFs().existsSync(srcAgentsDir)) return srcAgentsDir;
const stageDir = mkInstallTempDir('gsd-profile-agents-');
const stageDir = mkInstallTempDir('msd-profile-agents-');
try {
if (resolvedProfile.agents instanceof Set && resolvedProfile.agents.size > 0) {
const entries = installFs().readdirSync(srcAgentsDir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isFile()) continue;
if (!entry.name.endsWith('.md')) continue;
// Agent stem is the full filename without extension, e.g. "gsd-planner"
// Agent stem is the full filename without extension, e.g. "msd-planner"
const stem = entry.name.slice(0, -3);
if (!resolvedProfile.agents.has(stem)) continue;
installFs().copyFileSync(
@@ -570,7 +570,7 @@ function buildNamespaceBundleMap(srcCommandsDir: string): NamespaceBundleMap {
/**
* Rewrite a converted namespace-router SKILL.md so its routing table points at
* nested sub-skill files instead of bare Skill-tool names (#69). Each table row
* whose final cell carries a `gsd-<stem>` token (optionally with `--flag`
* whose final cell carries a `msd-<stem>` token (optionally with `--flag`
* suffixes) is rewritten to `Read \`skills/<stem>/SKILL.md\`` (flags preserved
* as a note), the `Invoke` column header becomes `Read`, and the
* "Invoke … using the Skill tool" trailer becomes a file-read instruction.
@@ -595,7 +595,7 @@ function transformRouterBodyToNested(converted: string): string {
const lastIdx = cells.length - 2;
if (lastIdx < 1) return line;
const cell = cells[lastIdx];
const m = cell.match(/gsd-([a-z0-9-]+)((?:\s+--[a-z0-9-]+)*)/i);
const m = cell.match(/msd-([a-z0-9-]+)((?:\s+--[a-z0-9-]+)*)/i);
if (!m) return line;
const stem = m[1];
const flags = m[2].trim();
@@ -684,18 +684,18 @@ function capabilityClusterStems(registry: CapabilityRegistry | undefined): Set<s
/**
* #2322 HIGH-3: filesystem marker written into every staged THIRD-PARTY
* capability skill directory (alongside SKILL.md) so a later prune pass
* (surface.cts pruneSkillDirs) can identify the directory as GSD-capability-
* (surface.cts pruneSkillDirs) can identify the directory as MSD-capability-
* owned even after the owning capability has been uninstalled/unsurfaced and
* no longer appears in ANY registry view. Without a persisted marker, an
* orphaned capability skill directory has no first-party manifest entry (the
* skill manifest only ever knows gsd-core's own bundled stems) and
* skill manifest only ever knows msd-core's own bundled stems) and
* pruneSkillDirs' conservative unknown-directory branch would preserve it
* FOREVER — uninstalling a malicious capability would never actually remove
* its already-staged instructions from the agent's context. A directory
* WITHOUT this marker is presumed genuinely user-created (data-loss
* protection is unchanged for that case).
*/
const CAPABILITY_SKILL_MARKER = '.gsd-capability-skill';
const CAPABILITY_SKILL_MARKER = '.msd-capability-skill';
/**
* Look up an installed third-party capability's already-authored SKILL.md for
@@ -717,7 +717,7 @@ const CAPABILITY_SKILL_MARKER = '.gsd-capability-skill';
* undeclared/unregistered sibling directory unreachable by construction.
*
* The install-root path convention (`<capabilitiesRoot>/<capId>/skills/<stem>/
* SKILL.md` under `GSD_HOME || homedir()`) mirrors capability-loader.cts
* SKILL.md` under `MSD_HOME || homedir()`) mirrors capability-loader.cts
* (global overlay root) and capability-source.cts's `stageValidated` finalDir.
*
* Total/non-throwing (#2322 requirement 5): no registry, an unowned stem, a
@@ -728,7 +728,7 @@ const CAPABILITY_SKILL_MARKER = '.gsd-capability-skill';
* staged (fail closed — never a fallback scan).
*
* NOTE: the content returned here is staged AS-IS (no per-file `converter`
* runs on it — unlike gsd-core's flat command `.md`, an installed capability
* runs on it — unlike msd-core's flat command `.md`, an installed capability
* skill is already a complete SKILL.md), but it is NOT immune from the LATER
* runtime-targeted body rewrite pass `applySurface` runs over the ENTIRE
* staged directory (`rewriteStagedSkillBodies`, surface.cts): a `~/.claude/`
@@ -746,8 +746,8 @@ function readInstalledCapabilitySkill(stem: string, registry: CapabilityRegistry
// re-validate its path-segment shape before using it as a literal path
// component in case a future registry composer ever stops guaranteeing that.
if (!isSafeCapabilitySkillStem(capId)) return null;
const home = process.env['GSD_HOME'] || os.homedir();
const capDir = path.join(home, '.gsd', 'capabilities', capId);
const home = process.env['MSD_HOME'] || os.homedir();
const capDir = path.join(home, '.msd', 'capabilities', capId);
const relSkillPath = path.join('skills', stem, 'SKILL.md');
// Defense-in-depth: isSafeCapabilitySkillStem already rejects separators/
// '..'/absolute stems, but re-confirm the resolved read path stays under
@@ -799,12 +799,12 @@ function stageSkillsForRuntimeAsSkills(
}
}
// #2322: stems actually staged from gsd-core's OWN bundled commands/gsd dir
// #2322: stems actually staged from msd-core's OWN bundled commands/msd dir
// this call, so the third-party fill-in pass below can enforce "first-party
// ALWAYS wins on collision" without re-deriving membership.
const firstPartyStems = new Set<string>();
const stageDir = mkInstallTempDir('gsd-profile-runtime-skills-');
const stageDir = mkInstallTempDir('msd-profile-runtime-skills-');
try {
const entries = installFs().readdirSync(srcCommandsDir, { withFileTypes: true });
for (const entry of entries) {
@@ -850,7 +850,7 @@ function stageSkillsForRuntimeAsSkills(
// (see readInstalledCapabilitySkill — NEVER scan-and-first-match). The
// registry union (#2045) already puts every accepted-capability stem into
// a concrete resolvedProfile.skills Set, but srcCommandsDir only ever
// holds gsd-core's own bundled commands — so any stem with no first-party
// holds msd-core's own bundled commands — so any stem with no first-party
// file here was silently dropped (registry says surfaced:true, nothing on
// disk) unless we fill it in from the capability's own install dir.
//
@@ -918,10 +918,10 @@ function stageSkillsForRuntimeAsSkills(
* gap the agents-bypass closure needed): the install root, threaded through
* so the frontmatter-extensions step and (via the converter's own closure in
* `convertedAgentsKind`) model-override resolution can read
* `.planning/config.json` / `~/.gsd/defaults.json` exactly as the inline loop
* `.planning/config.json` / `~/.msd/defaults.json` exactly as the inline loop
* did with its own `targetDir` variable. Optional — a caller with no
* `targetDir` in scope (e.g. the feat-1173 synthetic-descriptor seam tests)
* degrades to `null`, matching `readGsdEffectiveEffortConfig(null)`'s own
* degrades to `null`, matching `readMsdEffectiveEffortConfig(null)`'s own
* global-only-config contract. `projectDir` separates project config discovery
* from that artifact destination for global installs (#4032).
*/
@@ -940,7 +940,7 @@ interface AgentCtx {
* Analogous to `stageCommandsForRuntimeFlat` but for agent `.md` files. Each
* source `.md` is passed through `converter` and written as a flat `${name}.md`
* file in the staging directory. Agent filenames are kept verbatim (no prefix
* added here — the prefix is already embedded in agent stems, e.g. `gsd-planner.md`).
* added here — the prefix is already embedded in agent stems, e.g. `msd-planner.md`).
*
* This is used by the descriptor-driven `dispatchKindEntry` when an `agents` kind
* entry carries a non-null converter (ADR-457 / #1173). When `converter` is null,
@@ -986,7 +986,7 @@ function stageAgentsForRuntimeWithConverter(
): string {
if (!installFs().existsSync(srcAgentsDir)) return srcAgentsDir;
const stageDir = mkInstallTempDir('gsd-profile-runtime-agents-');
const stageDir = mkInstallTempDir('msd-profile-runtime-agents-');
let entries: fs.Dirent[];
try {
// #2284/#2875: readdirSync-ing the shipped agents/ source is isolated in
@@ -994,7 +994,7 @@ function stageAgentsForRuntimeWithConverter(
// corrupted install, or — as the #2284 test suite demonstrates — the
// fail-closed injection harness) produces the SAME "refusing to install"
// fail-closed wording every other agents-dir-unreadable path in this
// codebase uses (bin/install.js's `_resolveAvailableGsdRoles` /
// codebase uses (bin/install.js's `_resolveAvailableMsdRoles` /
// `_assertRoleResolvable`), instead of an unrelated raw fs error message
// escaping uncaught. Hermes's role-dispatch validation depends on the
// SAME shipped agents/ directory being readable; before this runtime also
@@ -1010,8 +1010,8 @@ function stageAgentsForRuntimeWithConverter(
}
try {
// Resolve cmdNames once per staging call (not per file) for performance.
const cmdNames = agentCtx ? _readGsdCommandNames() : [];
const agentTools = installModelOverrideResolver.readGsdEffectiveAgentTools(agentCtx?.projectDir ?? agentCtx?.targetDir ?? null);
const cmdNames = agentCtx ? _readMsdCommandNames() : [];
const agentTools = installModelOverrideResolver.readMsdEffectiveAgentTools(agentCtx?.projectDir ?? agentCtx?.targetDir ?? null);
for (const entry of entries) {
if (!entry.isFile()) continue;
if (!entry.name.endsWith('.md')) continue;
@@ -1024,7 +1024,7 @@ function stageAgentsForRuntimeWithConverter(
}
const agentSourcePath = path.join(srcAgentsDir, entry.name);
let content = installFs().readFileSync(agentSourcePath, 'utf8');
// #2995: strip gsd:section markers FIRST — before path rewrites, attribution,
// #2995: strip msd:section markers FIRST — before path rewrites, attribution,
// and the per-runtime converter. Byte-identical (no-op) for an unmarked agent;
// throws loudly naming the file for a malformed marker, never emitting a
// half-composed agent.
@@ -1075,18 +1075,18 @@ function stageAgentsForRuntimeWithConverter(
*
* The `_copyStaged` commands branch in install.js will add the prefix when
* copying staged files to the destination directory, so staged files must be
* named with just the stem (e.g. `help.md` not `gsd-help.md`).
* named with just the stem (e.g. `help.md` not `msd-help.md`).
*
* The `converter` receives `(content, ${prefix}${stem})` so it can embed the
* full command name (e.g. 'gsd-help') into the document body if needed.
* full command name (e.g. 'msd-help') into the document body if needed.
*
* Used by the `convertedCommandsKind` layout descriptor in
* runtime-artifact-layout.cts (#785 — Cursor 1.6 slash commands).
*
* @param srcCommandsDir source commands directory (e.g. commands/gsd/)
* @param srcCommandsDir source commands directory (e.g. commands/msd/)
* @param resolvedProfile profile filter — '*' for all, Set for subset
* @param converter (content, commandName) → string pure converter
* @param prefix command name prefix (for converter arg), e.g. 'gsd-'
* @param prefix command name prefix (for converter arg), e.g. 'msd-'
*/
function stageCommandsForRuntimeFlat(
srcCommandsDir: string,
@@ -1096,7 +1096,7 @@ function stageCommandsForRuntimeFlat(
): string {
if (!installFs().existsSync(srcCommandsDir)) return srcCommandsDir;
const stageDir = mkInstallTempDir('gsd-profile-runtime-commands-');
const stageDir = mkInstallTempDir('msd-profile-runtime-commands-');
try {
const entries = installFs().readdirSync(srcCommandsDir, { withFileTypes: true });
for (const entry of entries) {
@@ -1124,7 +1124,7 @@ function stageCommandsForRuntimeFlat(
// Profile marker persistence
// ---------------------------------------------------------------------------
const PROFILE_MARKER_NAME = '.gsd-profile';
const PROFILE_MARKER_NAME = '.msd-profile';
/**
* Read the active profile from a runtime config directory.
@@ -1200,7 +1200,7 @@ interface ResolveEffectiveProfileOpts {
function resolveEffectiveProfile({ requestedProfileName, targetDir }: ResolveEffectiveProfileOpts): string {
// 1. Explicit flag overrides everything
if (requestedProfileName != null) return requestedProfileName;
// 2. Marker-driven (gsd update path)
// 2. Marker-driven (msd update path)
const marker = readActiveProfile(targetDir);
if (marker && marker !== 'full') return marker;
// 3. Default
@@ -1246,7 +1246,7 @@ function shouldInstallSkill(skillBaseName: string, resolvedProfileOrMode: Resolv
}
/**
* Stage a filtered copy of the source commands/gsd directory.
* Stage a filtered copy of the source commands/msd directory.
* Back-compat wrapper: maps 'minimal' → core profile, 'full' → full.
*
* @deprecated Use stageSkillsForProfile with a resolved profile instead.
@@ -1255,7 +1255,7 @@ function stageSkillsForMode(srcDir: string, mode: string): string {
if (!isMinimalMode(mode)) return srcDir;
if (!fs.existsSync(srcDir)) return srcDir;
const stageDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-minimal-skills-'));
const stageDir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-minimal-skills-'));
try {
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
for (const entry of entries) {

View File

@@ -5,7 +5,7 @@
* `resolveScope()` turns a bare `'global' | 'local'` string — previously
* re-derived at 12 `isGlobal ? 'global' : 'local'` sites in `bin/install.js`
* plus several downstream consumers — into ONE resolved value produced by
* ONE module. See `.gsd/phase/feat-2870-install-scope-module/40-design.md`
* ONE module. See `.msd/phase/feat-2870-install-scope-module/40-design.md`
* for the full behavior table and rationale; the summary that matters for
* future readers is captured in the comments below.
*

View File

@@ -1,13 +1,13 @@
/**
* install-shadow-report.cts — Cross-Scope Shadow Report Module (#2873, epic
* #2866 Phase 4a — governed by
* `.gsd/phase/feat-2873-cross-scope-shadowing/40-design.md`).
* `.msd/phase/feat-2873-cross-scope-shadowing/40-design.md`).
*
* A read-only PROJECTION over `resolveInstalledSurfaces`
* (`installed-surface-resolver.cts`, #2872 Phase 3). That module answers
* "what is installed"; it is documented there as read-only, and rendering
* plus sanitization are a different concern with a different consumer set
* (installer + `/gsd-health`) — the design doc's "Rejected" #5 is why this is
* (installer + `/msd-health`) — the design doc's "Rejected" #5 is why this is
* a separate leaf module rather than a second export bolted onto the
* resolver.
*
@@ -54,7 +54,7 @@
* `{a, b, c}`, and `resolveTriggerSurface` then reports `commands@local`
* candidates for b and c too — trigger names for artifacts that do not exist
* on disk at that scope. Left unfiltered, this module would tell the user
* `/gsd-b` and `/gsd-c` are shadowed local commands when there is no local
* `/msd-b` and `/msd-c` are shadowed local commands when there is no local
* artifact for either at all — over-reporting that is not cosmetic, since
* the whole point of this report is to make a real failure legible.
*
@@ -78,7 +78,7 @@
* prefix on its output — only the already-composed `trigger` string
* (`${prefix}${stem}`) — so the stem cannot be read off it directly. Rather
* than hand-roll a fixed-offset `trigger.slice(4)` (which would silently
* assume every runtime's prefix is exactly `gsd-` — true today, but not a
* assume every runtime's prefix is exactly `msd-` — true today, but not a
* contract this module owns), the prefix is recovered the honest way: by
* re-resolving that scope's `ArtifactKind` layout (`resolveRuntimeArtifactLayout`
* / `resolveRuntimeArtifactLayoutFromRegistry`, the SAME layout descriptor
@@ -294,7 +294,7 @@ function isGenuinelyShadowed(
* A `resolveInstalledSurfaces` `TypeError` (unknown runtime, or
* `configHome.kind === 'none'`, e.g. vscode — design row #7) degrades to
* `reason: RESOLVER_UNAVAILABLE` rather than propagating: an install-time or
* `/gsd-health` caller must never crash because a runtime has no installable
* `/msd-health` caller must never crash because a runtime has no installable
* config directory. Any other error type is rethrown — mirrors the
* resolver's own `TypeError` narrowing (`resolveInstalledSurfaces`'s sweep
* catch, and `buildScopeRecord`'s stem-derivation catch) so the two cannot
@@ -398,7 +398,7 @@ export function buildShadowReport(runtime: string, opts: ResolveInstalledSurface
/**
* Render a `ShadowReport` to plain lines — no ANSI, no color, no leading
* indent. The caller (installer console output, `/gsd-health` text mode)
* indent. The caller (installer console output, `/msd-health` text mode)
* owns terminal formatting; this keeps the module free of terminal concerns
* and testable without a spawned process. Structured (`--json`) health
* output (design row #17) consumes the typed `ShadowReport` directly and

View File

@@ -1,7 +1,7 @@
/**
* installed-surface-resolver.cts — Installed Surface Resolver Module (#2872,
* ADR-2866, Phase 3 — governed by
* `.gsd/phase/feat-2872-manifest-scope-runtime/40-design.md`).
* `.msd/phase/feat-2872-manifest-scope-runtime/40-design.md`).
*
* `resolveInstalledSurfaces(runtime?, opts?)` answers a question no existing
* module answers: "which surfaces actually exist on THIS machine, across
@@ -39,14 +39,14 @@
* ── Installed-ness is manifest PRESENCE, never the new fields ───────────────
* `installed` is `manifestVersion !== null`. A v1 manifest (no
* `manifestVersion` key at all, `manifestVersion: 1` after normalization) is
* a correct manifest written by an older GSD — never a broken one, never
* a correct manifest written by an older MSD — never a broken one, never
* grounds for `installed: false`, a warning, or a reinstall prompt.
*
* ── Stems come from the manifest, not the source tree ───────────────────────
* `resolveTriggerSurface` needs `stems`. They are derived from the
* INSTALLED manifest's own `files` keys (see the private stem-derivation
* helpers below), never from a roster read of the source tree — a global
* `claude` install ships no `commands/gsd` source, so a roster read would
* `claude` install ships no `commands/msd` source, so a roster read would
* return `[]` in exactly the configuration #2218 is about. The derivation
* consumes `isNamespacedByDir` and `composeCommandFilename`
* (`runtime-artifact-layout.cjs`, #2871 Phase 2's exported helpers) rather
@@ -84,7 +84,7 @@ const _require: NodeRequire = require;
// the shapes here as a second, driftable copy — is the same pattern already
// used elsewhere in this codebase (e.g. `phase.cts`, `config.cts`).
/** One resolved `/gsd-<name>`-style trigger, as `resolveTriggerSurface`
/** One resolved `/msd-<name>`-style trigger, as `resolveTriggerSurface`
* (`runtime-artifact-layout.cts`, #2871 Phase 2) produces it. */
type TriggerSurface = ReturnType<typeof resolveTriggerSurface>[number];
@@ -152,14 +152,14 @@ export interface ResolveInstalledSurfacesOptions {
/**
* A trigger stem is a kebab-case token and nothing else. Verified against the
* real roster: all 71 `commands/gsd/*.md` stems match this, so it has no false
* real roster: all 71 `commands/msd/*.md` stems match this, so it has no false
* negatives today.
*
* This is a SECURITY boundary, not cosmetics. A stem is derived from a manifest
* key — attacker-influenceable text, since a project-local
* `gsd-file-manifest.json` lives inside a repository a user may merely have
* cloned — and Phase 4 (#2873) renders it back to the user as `/gsd-<stem>`.
* Without this, `skills/gsd-../../../x` yields the stem `..`, and control
* `msd-file-manifest.json` lives inside a repository a user may merely have
* cloned — and Phase 4 (#2873) renders it back to the user as `/msd-<stem>`.
* Without this, `skills/msd-../../../x` yields the stem `..`, and control
* characters, newlines, ANSI escapes and RTL-override codepoints all survive
* into that rendered trigger. The `commands` branch happened to be protected by
* its `composeCommandFilename` round-trip; the `skills` branch had no

View File

@@ -5,7 +5,7 @@
* hand-written .cjs; only types are added.
*
* Resolution environment variable surface for #3541 — when the installer
* runs without a TTY (typical /gsd:update path via Claude Code or any
* runs without a TTY (typical /msd:update path via Claude Code or any
* scripted update), prompt-user migration actions cannot be answered
* interactively. Classification-based defaults apply; anything else falls
* through to the hard assertion with a grouped, actionable error message.
@@ -13,51 +13,51 @@
* docs/installer-migrations.md#prompt-user-resolution for the spec.
*/
export const RESOLUTION_ENV_VAR = 'GSD_INSTALLER_MIGRATION_RESOLVE';
export const RESOLUTION_ENV_VAR = 'MSD_INSTALLER_MIGRATION_RESOLVE';
const VALID_CHOICES: ReadonlyArray<string> = ['keep', 'remove'];
// #3628: explicit whitelist of bundled hook files shipped in the npm
// distribution under `hooks/`. The classifier-based auto-removal of these
// files at first-time-baseline scan (added in #3610) is restricted to this
// set — a shape regex like `^hooks/gsd-[^/]+\.(?:js|sh|cjs|mjs)$` also
// set — a shape regex like `^hooks/msd-[^/]+\.(?:js|sh|cjs|mjs)$` also
// matches user-authored custom hooks and retired bundled hooks from prior
// versions, and auto-removing those is silent data loss.
//
// The bug-3628 regression guard asserts this Set stays aligned with the
// on-disk `hooks/` directory in both directions: whitelist-but-missing
// AND shipped-but-not-whitelisted both fail CI.
export const BUNDLED_GSD_HOOK_FILES: ReadonlySet<string> = Object.freeze(new Set([
'hooks/gsd-agent-isolation-guard.js',
'hooks/gsd-check-update-worker.js',
'hooks/gsd-check-update.js',
'hooks/gsd-config-reload.js',
'hooks/gsd-context-monitor.js',
'hooks/gsd-cursor-post-tool.js',
'hooks/gsd-cursor-pre-tool.js',
'hooks/gsd-cursor-session-start.js',
'hooks/gsd-cursor-stop.js',
'hooks/gsd-cursor-subagent-start.js',
'hooks/gsd-cursor-subagent-stop.js',
export const BUNDLED_MSD_HOOK_FILES: ReadonlySet<string> = Object.freeze(new Set([
'hooks/msd-agent-isolation-guard.js',
'hooks/msd-check-update-worker.js',
'hooks/msd-check-update.js',
'hooks/msd-config-reload.js',
'hooks/msd-context-monitor.js',
'hooks/msd-cursor-post-tool.js',
'hooks/msd-cursor-pre-tool.js',
'hooks/msd-cursor-session-start.js',
'hooks/msd-cursor-stop.js',
'hooks/msd-cursor-subagent-start.js',
'hooks/msd-cursor-subagent-stop.js',
// Windsurf/Cascade blocking hooks — registered by writeWindsurfHooksJson (#2100).
'hooks/gsd-windsurf-pre-write.js',
'hooks/gsd-windsurf-pre-command.js',
'hooks/gsd-ensure-canonical-path.js',
'hooks/gsd-graphify-update.sh',
'hooks/msd-windsurf-pre-write.js',
'hooks/msd-windsurf-pre-command.js',
'hooks/msd-ensure-canonical-path.js',
'hooks/msd-graphify-update.sh',
// #3662: portable node resolver staged into hooks/ (not itself a lifecycle
// hook — managed JS hook commands route through it under --portable-hooks).
'hooks/gsd-node-runner.sh',
'hooks/gsd-phase-boundary.sh',
'hooks/gsd-prompt-guard.js',
'hooks/gsd-read-guard.js',
'hooks/gsd-read-injection-scanner.js',
'hooks/gsd-secret-read-guard.js',
'hooks/gsd-session-state.sh',
'hooks/gsd-statusline.js',
'hooks/gsd-update-banner.js',
'hooks/gsd-validate-commit.sh',
'hooks/gsd-workflow-guard.js',
'hooks/gsd-worktree-path-guard.js',
'hooks/gsd-write-guard.js',
'hooks/msd-node-runner.sh',
'hooks/msd-phase-boundary.sh',
'hooks/msd-prompt-guard.js',
'hooks/msd-read-guard.js',
'hooks/msd-read-injection-scanner.js',
'hooks/msd-secret-read-guard.js',
'hooks/msd-session-state.sh',
'hooks/msd-statusline.js',
'hooks/msd-update-banner.js',
'hooks/msd-validate-commit.sh',
'hooks/msd-workflow-guard.js',
'hooks/msd-worktree-path-guard.js',
'hooks/msd-write-guard.js',
]));
// ── Internal action types ─────────────────────────────────────────────────────
@@ -219,28 +219,28 @@ export function summarizeInstallerMigrationResult(result: MigrationResult | null
// categories. Returns null when no safe default applies — caller must
// fall back to the hard assertion / interactive prompt for those.
//
// Stale SDK build artifacts live under gsd-core/sdk/{dist,src}/
// Stale SDK build artifacts live under msd-core/sdk/{dist,src}/
// and are regenerated on every install, so removing them is lossless.
// User-facing skill anchors are the .md files that surface as commands
// to the user — these are user-owned and must be kept.
export function classifyPromptUserAction(action: MigrationAction): ClassifyResult | null {
const relPath = action && action.relPath;
if (typeof relPath !== 'string' || !relPath) return null;
if (/^gsd-core\/sdk\/(dist|src)\//.test(relPath)) {
if (/^msd-core\/sdk\/(dist|src)\//.test(relPath)) {
return { category: 'stale-sdk-build-artifact', choice: 'remove' };
}
if (/^skills\/gsd-[^/]+\/SKILL\.md$/.test(relPath)) {
if (/^skills\/msd-[^/]+\/SKILL\.md$/.test(relPath)) {
return { category: 'user-facing-skill', choice: 'keep' };
}
// #3610 / #3628: bundled GSD hooks shipped under `hooks/`. The whitelist
// #3610 / #3628: bundled MSD hooks shipped under `hooks/`. The whitelist
// is the explicit set of filenames in the npm distribution — files that
// match the shape but are NOT in the whitelist (user-authored hooks,
// retired hooks from prior versions) fall through to the block-or-prompt
// flow so the user retains control. On a first-time-baseline scan the
// installer can safely remove whitelisted hooks because it is about to
// write the fresh bundled versions in their place.
if (BUNDLED_GSD_HOOK_FILES.has(relPath)) {
return { category: 'bundled-gsd-hook', choice: 'remove' };
if (BUNDLED_MSD_HOOK_FILES.has(relPath)) {
return { category: 'bundled-msd-hook', choice: 'remove' };
}
return null;
}
@@ -248,7 +248,7 @@ export function classifyPromptUserAction(action: MigrationAction): ClassifyResul
// Convert a blocked prompt-user action into a concrete plan action.
// `keep` → baseline-preserve-user (idempotent — already on disk).
// `remove` → backup-and-remove (safe: keeps a rollback copy in the
// migration journal under gsd-migration-journal/<runId>-backups/).
// migration journal under msd-migration-journal/<runId>-backups/).
function materializeResolution(action: MigrationAction, choice: string): MigrationAction {
const base: MigrationAction = {
type: '', // overridden in each return branch below

View File

@@ -1,6 +1,6 @@
/**
* Installer migrations engine — plan, apply, and track filesystem-mutation
* migrations for GSD runtime config directories.
* migrations for MSD runtime config directories.
*
* ADR-457 build-at-publish: the hand-written bin/lib/installer-migrations.cjs
* collapsed to a TypeScript source of truth. Behaviour is preserved
@@ -36,9 +36,9 @@ import { tryWithinRootLexical } from './security.cjs';
import installFsAdapter = require('./install-fs-adapter.cjs');
const { installFs } = installFsAdapter;
const MANIFEST_NAME = 'gsd-file-manifest.json';
const INSTALL_STATE_NAME = 'gsd-install-state.json';
const INSTALL_MIGRATION_LOCK_NAME = 'gsd-install-migration.lock';
const MANIFEST_NAME = 'msd-file-manifest.json';
const INSTALL_STATE_NAME = 'msd-install-state.json';
const INSTALL_MIGRATION_LOCK_NAME = 'msd-install-migration.lock';
const DEFAULT_MIGRATIONS_DIR = path.join(__dirname, 'installer-migrations');
const DEFAULT_LOCK_TIMEOUT_MS = 30_000;
const STRICT_JSON = Symbol('strict-json');
@@ -159,9 +159,9 @@ function evaluateRemoveEmptyDir(configDir: string, fullPath: string): string {
*
* `fs.copyFileSync` follows symlinks, so a managed path that has been replaced
* by a link (tampering, or an unexpected user layout) would have had the
* LINK TARGET's bytes copied into `gsd-migration-journal/…-backups/` — e.g. a
* `gsd.cjs` symlinked at `~/.ssh/id_rsa` would land that key's contents in the
* backup tree. Nothing GSD installs is ever a symlink, so the faithful snapshot
* LINK TARGET's bytes copied into `msd-migration-journal/…-backups/` — e.g. a
* `msd.cjs` symlinked at `~/.ssh/id_rsa` would land that key's contents in the
* backup tree. Nothing MSD installs is ever a symlink, so the faithful snapshot
* of a symlinked managed path is the link itself: recreating it preserves
* rollback fidelity (restore re-creates the same link) while never reading the
* referent. Deletion was already safe — `fs.rmSync` unlinks the link, never the
@@ -216,7 +216,7 @@ interface InstallManifest {
files: Record<string, string>;
/**
* Schema version of the manifest DOCUMENT (#2872, ADR-2866 Phase 3) — NOT
* the GSD package version, which `version` above already carries. The two
* the MSD package version, which `version` above already carries. The two
* are deliberately separate fields: `version` holds `pkg.version` and is
* read by the golden-parity fixtures, so overloading it with a schema
* number would be the textbook Hyrum break (same key, new meaning).
@@ -229,8 +229,8 @@ interface InstallManifest {
* requiring a reinstall.
* `>= 2` — records `runtime` and `scope`.
*
* A value written by a NEWER GSD is reported verbatim rather than clamped
* or rejected: two GSD versions on one machine is a supported state, and an
* A value written by a NEWER MSD is reported verbatim rather than clamped
* or rejected: two MSD versions on one machine is a supported state, and an
* older reader must not crash on a newer writer. Consumers branch on
* `>= 2`, never `=== 2`.
*/
@@ -689,7 +689,7 @@ interface EnsureInsideConfigResult {
// as a link, restored as a link, and backed up as a link. The realpath-based
// predicate dereferences exactly the symlinks this module exists to preserve
// and then rejects them for escaping configDir
// ("migration path escapes configDir: extensions/gsd.cjs"). Four tests in
// ("migration path escapes configDir: extensions/msd.cjs"). Four tests in
// tests/installer-migrations.test.cjs pin that behavior.
//
// The containment DECISION now routes through the canonical LEXICAL predicate
@@ -976,11 +976,11 @@ function applyInstallerMigrationPlan({
const appliedAt = now();
const runId = migrationRunId(appliedAt);
const journalRelPath = path.posix.join('gsd-migration-journal', `${runId}.json`);
const journalRelPath = path.posix.join('msd-migration-journal', `${runId}.json`);
const journalPath = path.join(configDir, journalRelPath);
const rollbackRootRelPath = path.posix.join('gsd-migration-journal', `${runId}-rollback`);
const rollbackRootRelPath = path.posix.join('msd-migration-journal', `${runId}-rollback`);
const rollbackRoot = path.join(configDir, rollbackRootRelPath);
const backupRootRelPath = path.posix.join('gsd-migration-journal', `${runId}-backups`);
const backupRootRelPath = path.posix.join('msd-migration-journal', `${runId}-backups`);
const backupRoot = path.join(configDir, backupRootRelPath);
const journal: { schemaVersion: number; appliedAt: string; appliedMigrationIds: string[]; actions: JournalAction[] } = {
schemaVersion: 1,

View File

@@ -16,34 +16,34 @@ const BASELINE_MIGRATION_ID = '2026-05-11-first-time-baseline-scan';
// - docs/installer-migrations.md#runtime-configuration-contract-registry
// - docs/ARCHITECTURE.md#runtime-install-contract-matrix
const RUNTIME_SURFACES: Record<string, string[]> = {
claude: ['gsd-core', 'commands/gsd', 'skills', 'agents', 'hooks', 'settings.json'],
codex: ['gsd-core', 'skills', 'agents', 'hooks', 'config.toml', 'hooks.json'],
gemini: ['gsd-core', 'commands/gsd', 'hooks'],
opencode: ['gsd-core', 'command', 'skills', 'agents'],
kilo: ['gsd-core', 'command', 'skills', 'agents'],
copilot: ['gsd-core', 'skills', 'agents'],
antigravity: ['gsd-core', 'skills', 'agents'],
cursor: ['gsd-core', 'skills', 'agents', 'hooks', 'hooks.json'],
windsurf: ['gsd-core', 'skills', 'agents', 'rules'],
augment: ['gsd-core', 'skills', 'agents'],
trae: ['gsd-core', 'skills', 'agents', 'rules'],
qwen: ['gsd-core', 'skills', 'agents'],
hermes: ['gsd-core', 'skills/gsd', 'agents'],
cline: ['gsd-core', 'skills', 'agents'],
codebuddy: ['gsd-core', 'skills', 'agents'],
claude: ['msd-core', 'commands/msd', 'skills', 'agents', 'hooks', 'settings.json'],
codex: ['msd-core', 'skills', 'agents', 'hooks', 'config.toml', 'hooks.json'],
gemini: ['msd-core', 'commands/msd', 'hooks'],
opencode: ['msd-core', 'command', 'skills', 'agents'],
kilo: ['msd-core', 'command', 'skills', 'agents'],
copilot: ['msd-core', 'skills', 'agents'],
antigravity: ['msd-core', 'skills', 'agents'],
cursor: ['msd-core', 'skills', 'agents', 'hooks', 'hooks.json'],
windsurf: ['msd-core', 'skills', 'agents', 'rules'],
augment: ['msd-core', 'skills', 'agents'],
trae: ['msd-core', 'skills', 'agents', 'rules'],
qwen: ['msd-core', 'skills', 'agents'],
hermes: ['msd-core', 'skills/msd', 'agents'],
cline: ['msd-core', 'skills', 'agents'],
codebuddy: ['msd-core', 'skills', 'agents'],
};
const COMMON_SURFACES = ['gsd-core', 'skills', 'agents', 'hooks'];
const COMMON_SURFACES = ['msd-core', 'skills', 'agents', 'hooks'];
const INTERNAL_TOP_LEVEL_NAMES = new Set([
'gsd-file-manifest.json',
'gsd-install-state.json',
'gsd-migration-backups',
'gsd-migration-journal',
'msd-file-manifest.json',
'msd-install-state.json',
'msd-migration-backups',
'msd-migration-journal',
]);
const USER_OWNED_PATHS = new Set([
'gsd-core/USER-PROFILE.md',
'commands/gsd/dev-preferences.md',
'skills/gsd-dev-preferences/SKILL.md',
'msd-core/USER-PROFILE.md',
'commands/msd/dev-preferences.md',
'skills/msd-dev-preferences/SKILL.md',
]);
let knownGeneratedAgentNames: Set<string> | null = null;
@@ -90,8 +90,8 @@ function scanBaselineFiles(configDir: string, runtime: string | undefined): stri
function isUserOwnedBaselinePath(relPath: string): boolean {
if (USER_OWNED_PATHS.has(relPath)) return true;
const parts = relPath.split('/');
if (parts[0] === 'skills' && parts[1] && !parts[1].startsWith('gsd-')) return true;
if (parts[0] === 'agents' && parts[1] && !parts[1].startsWith('gsd-')) return true;
if (parts[0] === 'skills' && parts[1] && !parts[1].startsWith('msd-')) return true;
if (parts[0] === 'agents' && parts[1] && !parts[1].startsWith('msd-')) return true;
return false;
}
@@ -102,13 +102,13 @@ function listKnownGeneratedAgentNames(): Set<string> {
const agentsDir = path.resolve(__dirname, '..', '..', '..', '..', 'agents');
try {
for (const entry of fs.readdirSync(agentsDir, { withFileTypes: true })) {
if (entry.isFile() && entry.name.startsWith('gsd-') && entry.name.endsWith('.md')) {
if (entry.isFile() && entry.name.startsWith('msd-') && entry.name.endsWith('.md')) {
knownGeneratedAgentNames.add(entry.name.replace(/\.md$/, ''));
}
}
} catch {
// If the source agent directory is unavailable, fail closed and treat
// GSD-looking agent files as user-choice artifacts.
// MSD-looking agent files as user-choice artifacts.
}
return knownGeneratedAgentNames;
@@ -125,11 +125,11 @@ function isKnownGeneratedAgentPath(relPath: string, runtime: string | undefined)
return listKnownGeneratedAgentNames().has(agentName);
}
function isStaleGsdLookingPath(relPath: string): boolean {
function isStaleMsdLookingPath(relPath: string): boolean {
const baseName = path.posix.basename(relPath);
if (/^gsd[-_]/.test(baseName)) return true;
if (/^msd[-_]/.test(baseName)) return true;
const parts = relPath.split('/');
if ((parts[0] === 'skills' || parts[0] === 'agents') && parts[1] && parts[1].startsWith('gsd-')) {
if ((parts[0] === 'skills' || parts[0] === 'agents') && parts[1] && parts[1].startsWith('msd-')) {
return true;
}
return false;
@@ -225,15 +225,15 @@ const migration: InstallerMigration = {
continue;
}
if (isStaleGsdLookingPath(relPath)) {
if (isStaleMsdLookingPath(relPath)) {
actions.push({
type: 'prompt-user',
relPath,
reason: 'GSD-looking file is not proven manifest-managed and needs explicit user choice',
classification: 'stale-gsd-looking',
reason: 'MSD-looking file is not proven manifest-managed and needs explicit user choice',
classification: 'stale-msd-looking',
originalHash: artifact.originalHash ?? null,
currentHash,
prompt: 'Choose whether to remove this stale-looking GSD artifact or keep it as user-owned.',
prompt: 'Choose whether to remove this stale-looking MSD artifact or keep it as user-owned.',
choices: ['keep', 'remove'],
});
continue;

View File

@@ -37,7 +37,7 @@ interface InstallerMigration {
}
const LEGACY_ORPHAN_FILES: ReadonlyArray<string> = [
'hooks/gsd-notify.sh',
'hooks/msd-notify.sh',
'hooks/statusline.js',
];
@@ -61,14 +61,14 @@ const migration: InstallerMigration = {
type: 'remove-managed',
relPath,
reason: 'legacy orphan hook file retired by installer migration',
ownershipEvidence: 'legacy hook path is manifest-managed in gsd-file-manifest.json',
ownershipEvidence: 'legacy hook path is manifest-managed in msd-file-manifest.json',
});
} else if (artifact.classification === 'managed-modified') {
actions.push({
type: 'backup-and-remove',
relPath,
reason: 'legacy orphan hook file retired by installer migration',
ownershipEvidence: 'legacy hook path is manifest-managed in gsd-file-manifest.json',
ownershipEvidence: 'legacy hook path is manifest-managed in msd-file-manifest.json',
});
}
}

View File

@@ -1,5 +1,5 @@
/**
* Installer migration: remove legacy Codex hooks.json GSD hook registrations.
* Installer migration: remove legacy Codex hooks.json MSD hook registrations.
*
* ADR-457 build-at-publish: the hand-written
* bin/lib/installer-migrations/002-codex-legacy-hooks-json.cjs collapsed to a
@@ -107,8 +107,8 @@ function pruneLegacyCodexHooksJsonValue(value: JsonValue, configDir: string): Pr
const migration: InstallerMigration = {
id: '2026-05-11-codex-legacy-hooks-json',
title: 'Remove legacy Codex hooks.json GSD hook registrations',
description: 'Remove legacy Codex hooks.json GSD hook registrations after config.toml migration.',
title: 'Remove legacy Codex hooks.json MSD hook registrations',
description: 'Remove legacy Codex hooks.json MSD hook registrations after config.toml migration.',
introducedIn: '1.50.0',
runtimes: ['codex'],
scopes: ['global', 'local'],
@@ -128,8 +128,8 @@ const migration: InstallerMigration = {
relPath: 'hooks.json',
value: pruned.value,
deleteIfEmpty: true,
reason: 'legacy Codex hooks.json GSD registration retired by installer migration',
ownershipEvidence: 'pruned command matches generated GSD hook command under the install hooks directory',
reason: 'legacy Codex hooks.json MSD registration retired by installer migration',
ownershipEvidence: 'pruned command matches generated MSD hook command under the install hooks directory',
},
];
},

View File

@@ -1,12 +1,12 @@
/**
* Installer migration 003: remove stale legacy get-shit-done/ runtime directory // gsd-allow-legacy-name
* files after the rename to gsd-core/ (#604).
* Installer migration 003: remove stale legacy get-shit-done/ runtime directory // msd-allow-legacy-name
* files after the rename to msd-core/ (#604).
*
* Background: the GSD runtime config subdirectory was renamed from
* get-shit-done/ to gsd-core/ in #604. On upgrade, both directories can exist // gsd-allow-legacy-name
* Background: the MSD runtime config subdirectory was renamed from
* get-shit-done/ to msd-core/ in #604. On upgrade, both directories can exist // msd-allow-legacy-name
* simultaneously. This migration removes prior-manifest-managed files from the
* legacy get-shit-done/ directory during install. Migrations run BEFORE the new // gsd-allow-legacy-name
* runtime is materialized, so gsd-core/ will not yet exist on the first upgrade
* legacy get-shit-done/ directory during install. Migrations run BEFORE the new // msd-allow-legacy-name
* runtime is materialized, so msd-core/ will not yet exist on the first upgrade
* run — the migration must not gate on its presence. If the install fails after
* migrations apply, the framework rolls back by restoring files from rollback
* storage (copied before deletion), so removing legacy files pre-materialization
@@ -58,7 +58,7 @@ function walkLegacyFiles(root: string, relDir: string, baseResolved: string, res
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
// Do not follow symlinks — skip them entirely to avoid out-of-tree traversal.
if (entry.isSymbolicLink()) continue; // gsd-allow-legacy-name (entry under legacy get-shit-done/ dir)
if (entry.isSymbolicLink()) continue; // msd-allow-legacy-name (entry under legacy get-shit-done/ dir)
const relPath = path.posix.join(relDir, entry.name);
// Bounds check: ensure the resolved path stays under configDir.
const resolved = path.resolve(root, relPath);
@@ -71,30 +71,30 @@ function walkLegacyFiles(root: string, relDir: string, baseResolved: string, res
}
}
const REASON = 'legacy runtime directory renamed to gsd-core (#604)';
const REASON = 'legacy runtime directory renamed to msd-core (#604)';
const migration: InstallerMigration = {
id: '2026-06-02-rename-get-shit-done-to-gsd-core', // gsd-allow-legacy-name
title: 'Remove stale legacy get-shit-done/ runtime directory files (#604)', // gsd-allow-legacy-name
id: '2026-06-02-rename-get-shit-done-to-msd-core', // msd-allow-legacy-name
title: 'Remove stale legacy get-shit-done/ runtime directory files (#604)', // msd-allow-legacy-name
description:
'After the config dir rename from get-shit-done/ to gsd-core/ (#604), remove prior-manifest-managed files ' + // gsd-allow-legacy-name
'After the config dir rename from get-shit-done/ to msd-core/ (#604), remove prior-manifest-managed files ' + // msd-allow-legacy-name
'from the stale legacy directory during install (framework rollback restores them if install fails). User-added files are preserved.',
introducedIn: '1.2.0',
scopes: ['global', 'local'],
destructive: true,
plan(ctx: MigrationPlanContext): MigrationAction[] {
const legacyRoot = path.join(ctx.configDir, 'get-shit-done'); // gsd-allow-legacy-name
const legacyRoot = path.join(ctx.configDir, 'get-shit-done'); // msd-allow-legacy-name
// Idempotency: if the legacy directory doesn't exist, nothing to do.
if (!fs.existsSync(legacyRoot)) return [];
// Safety: if the legacy root itself is a symlink to an out-of-tree location,
// do not process it — walking a symlinked dir could emit removes outside configDir.
if (fs.lstatSync(legacyRoot).isSymbolicLink()) return []; // gsd-allow-legacy-name
if (fs.lstatSync(legacyRoot).isSymbolicLink()) return []; // msd-allow-legacy-name
const baseResolved = path.resolve(ctx.configDir);
const relPaths: string[] = [];
walkLegacyFiles(ctx.configDir, 'get-shit-done', baseResolved, relPaths); // gsd-allow-legacy-name
walkLegacyFiles(ctx.configDir, 'get-shit-done', baseResolved, relPaths); // msd-allow-legacy-name
const actions: MigrationAction[] = [];
for (const relPath of relPaths) {
@@ -108,7 +108,7 @@ const migration: InstallerMigration = {
relPath,
reason: REASON,
ownershipEvidence:
'present in prior install manifest as a managed GSD runtime file',
'present in prior install manifest as a managed MSD runtime file',
});
} else if (classification === 'managed-modified') {
actions.push({
@@ -116,7 +116,7 @@ const migration: InstallerMigration = {
relPath,
reason: REASON,
ownershipEvidence:
'managed GSD runtime file, locally modified; backed up before removal',
'managed MSD runtime file, locally modified; backed up before removal',
});
} else if (classification === 'unknown') {
actions.push({

View File

@@ -1,20 +1,20 @@
/**
* Installer migration 004: remove stale gsd-pristine/get-shit-done/ snapshot // gsd-allow-legacy-name
* files after the get-shit-done → gsd-core rename (#604, #934). // gsd-allow-legacy-name
* Installer migration 004: remove stale msd-pristine/get-shit-done/ snapshot // msd-allow-legacy-name
* files after the get-shit-done → msd-core rename (#604, #934). // msd-allow-legacy-name
*
* Background: migration 003 removed legacy runtime files from
* get-shit-done/ but did not touch gsd-pristine/get-shit-done/, the // gsd-allow-legacy-name
* get-shit-done/ but did not touch msd-pristine/get-shit-done/, the // msd-allow-legacy-name
* parallel directory that holds pristine snapshots captured before the rename.
* These snapshot files are GSD-managed (written by the installer, never by the
* user) and reference stale get-shit-done/... key paths that no longer exist // gsd-allow-legacy-name
* These snapshot files are MSD-managed (written by the installer, never by the
* user) and reference stale get-shit-done/... key paths that no longer exist // msd-allow-legacy-name
* in the active layout. When verify-reapply-patches.cjs looks up a backup entry
* keyed under gsd-core/... it finds no matching gsd-pristine/ snapshot, falls
* to over-broad mode, and reports false FAIL_INSTALLED_MISSING / // gsd-allow-legacy-name
* keyed under msd-core/... it finds no matching msd-pristine/ snapshot, falls
* to over-broad mode, and reports false FAIL_INSTALLED_MISSING / // msd-allow-legacy-name
* FAIL_USER_LINES_MISSING for every backed-up pre-rename file (#934).
*
* Fix: walk gsd-pristine/get-shit-done/ and emit remove-managed for each file. // gsd-allow-legacy-name
* These files are always GSD-written snapshots — users never place their own
* files inside gsd-pristine/ — so the classification override
* Fix: walk msd-pristine/get-shit-done/ and emit remove-managed for each file. // msd-allow-legacy-name
* These files are always MSD-written snapshots — users never place their own
* files inside msd-pristine/ — so the classification override
* (managed-pristine) is safe: there is no user content to protect.
*
* Checksum safety: migration 003's body is left untouched. Adding this
@@ -76,44 +76,44 @@ function walkPristineFiles(root: string, relDir: string, baseResolved: string, r
}
}
const REASON = 'stale pristine snapshot from legacy get-shit-done/ dir, orphaned by rename migration 003 (#604, #934)'; // gsd-allow-legacy-name
const REASON = 'stale pristine snapshot from legacy get-shit-done/ dir, orphaned by rename migration 003 (#604, #934)'; // msd-allow-legacy-name
const migration: InstallerMigration = {
id: '2026-06-09-prune-stale-pristine-get-shit-done', // gsd-allow-legacy-name
title: 'Remove stale gsd-pristine/get-shit-done/ snapshot files (#934)', // gsd-allow-legacy-name
id: '2026-06-09-prune-stale-pristine-get-shit-done', // msd-allow-legacy-name
title: 'Remove stale msd-pristine/get-shit-done/ snapshot files (#934)', // msd-allow-legacy-name
description:
'Migration 003 removed runtime files from get-shit-done/ but left the matching pristine snapshot ' + // gsd-allow-legacy-name
'directory gsd-pristine/get-shit-done/ intact. Those snapshots reference stale key paths and cause ' + // gsd-allow-legacy-name
'verify-reapply-patches false positives (#934). Remove all files under gsd-pristine/get-shit-done/ ' + // gsd-allow-legacy-name
'as they are GSD-managed snapshots, never user content.',
'Migration 003 removed runtime files from get-shit-done/ but left the matching pristine snapshot ' + // msd-allow-legacy-name
'directory msd-pristine/get-shit-done/ intact. Those snapshots reference stale key paths and cause ' + // msd-allow-legacy-name
'verify-reapply-patches false positives (#934). Remove all files under msd-pristine/get-shit-done/ ' + // msd-allow-legacy-name
'as they are MSD-managed snapshots, never user content.',
introducedIn: '1.4.3',
scopes: ['global', 'local'],
destructive: true,
plan(ctx: MigrationPlanContext): MigrationAction[] {
const pristineGsdRoot = path.join(ctx.configDir, 'gsd-pristine', 'get-shit-done'); // gsd-allow-legacy-name
const pristineMsdRoot = path.join(ctx.configDir, 'msd-pristine', 'get-shit-done'); // msd-allow-legacy-name
// Idempotency: if the stale pristine subdir doesn't exist, nothing to do.
if (!fs.existsSync(pristineGsdRoot)) return [];
if (!fs.existsSync(pristineMsdRoot)) return [];
// Safety: reject symlinks in ANY ancestor component of the path we will walk
// to prevent following a symlink out of configDir. Check both gsd-pristine/
// and gsd-pristine/get-shit-done/ — either being a symlink could redirect // gsd-allow-legacy-name
// to prevent following a symlink out of configDir. Check both msd-pristine/
// and msd-pristine/get-shit-done/ — either being a symlink could redirect // msd-allow-legacy-name
// the walk to an out-of-tree location.
const pristineParent = path.join(ctx.configDir, 'gsd-pristine');
const pristineParent = path.join(ctx.configDir, 'msd-pristine');
try {
if (fs.lstatSync(pristineParent).isSymbolicLink()) return [];
} catch {
return [];
}
try {
if (fs.lstatSync(pristineGsdRoot).isSymbolicLink()) return []; // gsd-allow-legacy-name
if (fs.lstatSync(pristineMsdRoot).isSymbolicLink()) return []; // msd-allow-legacy-name
} catch {
return [];
}
const baseResolved = path.resolve(ctx.configDir);
const relPaths: string[] = [];
walkPristineFiles(ctx.configDir, path.posix.join('gsd-pristine', 'get-shit-done'), baseResolved, relPaths); // gsd-allow-legacy-name
walkPristineFiles(ctx.configDir, path.posix.join('msd-pristine', 'get-shit-done'), baseResolved, relPaths); // msd-allow-legacy-name
const actions: MigrationAction[] = [];
for (const relPath of relPaths) {
@@ -121,19 +121,19 @@ const migration: InstallerMigration = {
const resolved = path.resolve(ctx.configDir, relPath);
if (resolved !== baseResolved && !resolved.startsWith(baseResolved + path.sep)) continue;
// These files are GSD-managed pristine snapshots — the installer writes
// These files are MSD-managed pristine snapshots — the installer writes
// them during install/upgrade; users never place personal files inside
// gsd-pristine/. Pass classification: 'managed-pristine' explicitly so
// msd-pristine/. Pass classification: 'managed-pristine' explicitly so
// the framework does not downgrade remove-managed to preserve-user when
// the manifest has no entry (these paths were never in the manifest since
// they live under gsd-pristine/, not the tracked runtime dir).
// they live under msd-pristine/, not the tracked runtime dir).
actions.push({
type: 'remove-managed',
relPath,
reason: REASON,
ownershipEvidence:
'GSD-written pristine snapshot under gsd-pristine/get-shit-done/; ' + // gsd-allow-legacy-name
'installer is the sole author of gsd-pristine/ contents; no user content lives here',
'MSD-written pristine snapshot under msd-pristine/get-shit-done/; ' + // msd-allow-legacy-name
'installer is the sole author of msd-pristine/ contents; no user content lives here',
classification: 'managed-pristine',
});
}

View File

@@ -15,9 +15,9 @@
* `commands/` before this fix.
*
* Consequence proven by probe (see tests/installer-migrations.test.cjs): a
* pre-existing, unmanifested file at `commands/gsd-<name>.md` that predates
* any GSD install is silently destroyed by ordinary OpenCode command
* materialization (which unconditionally removes every `gsd-*.md` file under
* pre-existing, unmanifested file at `commands/msd-<name>.md` that predates
* any MSD install is silently destroyed by ordinary OpenCode command
* materialization (which unconditionally removes every `msd-*.md` file under
* its destination before writing the fresh set) with a clean exit code — no
* report, no backup, no prompt. The identical scenario under the
* already-covered legacy `command/` surface instead halts the install with a
@@ -39,7 +39,7 @@
* scoping below.
*
* Classification mirrors 000-first-time-baseline.cts exactly (record-baseline
* for manifest-proven files, prompt-user for stale-GSD-looking unmanifested
* for manifest-proven files, prompt-user for stale-MSD-looking unmanifested
* files, baseline-preserve-user for everything else) — this migration only
* widens the surface scanned, not the classification policy.
*/
@@ -115,8 +115,8 @@ function scanCommandsSurface(configDir: string): string[] {
return [...relPaths];
}
function isStaleGsdLookingPath(relPath: string): boolean {
return /^gsd[-_]/.test(path.posix.basename(relPath));
function isStaleMsdLookingPath(relPath: string): boolean {
return /^msd[-_]/.test(path.posix.basename(relPath));
}
function baselineActionRank(action: BaselineAction): number {
@@ -153,15 +153,15 @@ const migration: InstallerMigration = {
}
const currentHash = artifact.currentHash ?? null;
if (isStaleGsdLookingPath(relPath)) {
if (isStaleMsdLookingPath(relPath)) {
actions.push({
type: 'prompt-user',
relPath,
reason: 'GSD-looking file is not proven manifest-managed and needs explicit user choice',
classification: 'stale-gsd-looking',
reason: 'MSD-looking file is not proven manifest-managed and needs explicit user choice',
classification: 'stale-msd-looking',
originalHash: artifact.originalHash ?? null,
currentHash,
prompt: 'Choose whether to remove this stale-looking GSD artifact or keep it as user-owned.',
prompt: 'Choose whether to remove this stale-looking MSD artifact or keep it as user-owned.',
choices: ['keep', 'remove'],
});
continue;

View File

@@ -1,23 +1,23 @@
/**
* Installer migration: retire pi's stale `extensions/gsd.cjs` after #2470
* renamed the installed native extension to `extensions/gsd.js`.
* Installer migration: retire pi's stale `extensions/msd.cjs` after #2470
* renamed the installed native extension to `extensions/msd.js`.
*
* What old artifact is being retired?
* `extensions/gsd.cjs` — the pre-#2470 dest filename for pi's native
* `extensions/msd.cjs` — the pre-#2470 dest filename for pi's native
* extension. pi auto-discovers extensions by scanning `<agentDir>/extensions/`
* and keeping only names accepted by its own predicate
* (`isExtensionFile()` in @earendil-works/pi-coding-agent:
* `name.endsWith(".ts") || name.endsWith(".js")`). A `.cjs` file is skipped
* SILENTLY — no `/gsd` command, no error, no log line. The file is therefore
* SILENTLY — no `/msd` command, no error, no log line. The file is therefore
* permanently inert, not merely redundant.
*
* How do we prove it is GSD-owned?
* How do we prove it is MSD-owned?
* The installer records the native plugin in the install manifest as
* `<nativePlugin.dir>/<nativePlugin.file>` (bin/install.js, the
* `_hostBehaviors(runtime).nativePlugin` manifest block), so a pre-#2470 pi
* install carries `extensions/gsd.cjs` as a manifest-managed entry. Only a
* install carries `extensions/msd.cjs` as a manifest-managed entry. Only a
* manifest-managed classification produces an action here; an unmanifested
* `gsd.cjs` is treated as a user's own file and preserved.
* `msd.cjs` is treated as a user's own file and preserved.
*
* What happens if the user modified it?
* `backup-and-remove` instead of `remove-managed`, so a patched extension is
@@ -30,13 +30,13 @@
* What runtime and scope does it affect?
* pi only, global and local. No other runtime ever installed this path:
* OpenCode and Kilo — the only other runtimes declaring
* `hostBehaviors.nativePlugin` — both ship `plugins/gsd-core.js`.
* `hostBehaviors.nativePlugin` — both ship `plugins/msd-core.js`.
*
* Is the action safe in non-interactive install?
* Yes. Both emitted action types are non-interactive and journaled; neither
* requires a user choice, and unknown files never produce an action.
*
* Why not `move-managed`? The installer materializes the new `extensions/gsd.js`
* Why not `move-managed`? The installer materializes the new `extensions/msd.js`
* from the package payload in the same run, so moving the stale file onto that
* path would just be overwritten. Retiring the old path is the accurate
* description of the change.
@@ -77,22 +77,22 @@ interface InstallerMigration {
}
/** Pre-#2470 dest filename for pi's native extension. */
const STALE_PI_EXTENSION = 'extensions/gsd.cjs';
const STALE_PI_EXTENSION = 'extensions/msd.cjs';
const OWNERSHIP_EVIDENCE =
'pre-#2470 pi installs record the native extension at extensions/gsd.cjs in ' +
'gsd-file-manifest.json (installer nativePlugin manifest entry)';
'pre-#2470 pi installs record the native extension at extensions/msd.cjs in ' +
'msd-file-manifest.json (installer nativePlugin manifest entry)';
const REASON =
'pi cannot auto-discover a .cjs extension (isExtensionFile accepts only .ts/.js), ' +
'so this file is inert; superseded by extensions/gsd.js (#2470)';
'so this file is inert; superseded by extensions/msd.js (#2470)';
const migration: InstallerMigration = {
id: '2026-07-20-pi-extension-cjs-to-js',
title: 'Retire pi\'s undiscoverable extensions/gsd.cjs',
title: 'Retire pi\'s undiscoverable extensions/msd.cjs',
description:
'Remove the stale extensions/gsd.cjs left by pre-#2470 pi installs, superseded by ' +
'extensions/gsd.js — the suffix pi\'s extension auto-discovery actually accepts.',
'Remove the stale extensions/msd.cjs left by pre-#2470 pi installs, superseded by ' +
'extensions/msd.js — the suffix pi\'s extension auto-discovery actually accepts.',
introducedIn: '1.7.1',
runtimes: ['pi'],
scopes: ['global', 'local'],
@@ -119,7 +119,7 @@ const migration: InstallerMigration = {
},
];
}
// 'unknown' (never GSD-managed), 'missing', and 'managed-missing' all plan
// 'unknown' (never MSD-managed), 'missing', and 'managed-missing' all plan
// nothing: unknown files are preserved by policy, and an absent file needs
// no retirement.
return [];

View File

@@ -5,27 +5,27 @@
* What old artifact is being retired?
* `package.json` at the runtime config root. Before #2544,
* `installSharedHooksBundle(destRootDir)` wrote `{"type":"commonjs"}` there
* unconditionally on every install and every re-install, to pin GSD's staged
* unconditionally on every install and every re-install, to pin MSD's staged
* `.js` hook scripts to CommonJS via Node's ancestor walk. #2544 moved that
* marker into the directories GSD actually fills (`hooks/`, and the
* marker into the directories MSD actually fills (`hooks/`, and the
* `nativePlugin.dir` for runtimes declaring one) and stopped writing the
* config root at all. Without this migration an upgrader keeps BOTH markers:
* the new one under `hooks/` and the stale one at the root, so the config
* root stays pinned to CommonJS and the PR's own claim — that GSD no longer
* root stays pinned to CommonJS and the PR's own claim — that MSD no longer
* writes the shared config root — is false for every install made since the
* marker was introduced, until the user uninstalls.
*
* How do we prove it is GSD-owned?
* How do we prove it is MSD-owned?
* By exact content match, NOT by the manifest. The config-root marker was
* never recorded in `gsd-file-manifest.json` — `writeManifest()` records
* never recorded in `msd-file-manifest.json` — `writeManifest()` records
* `hooks/`, `agents/`, `commands/`, `scripts/`, and the native plugin, and
* has never had a `manifest.files['package.json']` entry — so
* `classifyArtifact('package.json')` answers `unknown` and the planner's
* own guard would downgrade a `remove-managed` to `preserve-user`.
* This migration therefore supplies the "purpose-built detector for an old
* GSD-owned shape" that `docs/installer-migrations.md#remove-managed`
* MSD-owned shape" that `docs/installer-migrations.md#remove-managed`
* sanctions, and declares the resulting classification on the action: the
* file is removed only when its bytes are exactly the marker GSD writes
* file is removed only when its bytes are exactly the marker MSD writes
* (`{"type":"commonjs"}`, trailing whitespace tolerated). That is the same
* predicate `removeCommonJsMarker` has always used on the uninstall side, so
* install, uninstall, and migration cannot drift apart.
@@ -36,7 +36,7 @@
* beyond the single `type` — i.e. every file the #2544 defect destroyed —
* fails the exact-content test and is left exactly as found. There is
* deliberately no `backup-and-remove` branch: a modified file here is not a
* patched GSD artifact, it is somebody else's file.
* patched MSD artifact, it is somebody else's file.
*
* What happens if it is missing?
* No actions. Fresh post-#2544 installs never wrote it, already-migrated
@@ -67,7 +67,7 @@
* Is the action safe in non-interactive install?
* Yes. `remove-managed` is non-interactive and journaled, the executor takes
* a rollback snapshot before unlinking, and no branch of this migration can
* emit `prompt-user`. A file that is not byte-identical to GSD's marker
* emit `prompt-user`. A file that is not byte-identical to MSD's marker
* produces no action at all.
*
* See docs/installer-migrations.md#shipped-migrations and #action-types.
@@ -123,7 +123,7 @@ interface InstallerMigration {
const STALE_ROOT_MARKER = 'package.json';
/**
* The exact marker content GSD wrote. Duplicated as a literal rather than
* The exact marker content MSD wrote. Duplicated as a literal rather than
* imported from `src/commonjs-marker.cts` on purpose: a migration record is a
* frozen historical statement about what a PAST version installed, and it must
* keep matching those bytes even if the live module's constant is ever changed.
@@ -139,7 +139,7 @@ const OWNERSHIP_EVIDENCE =
const REASON =
'superseded by the hooks/ and plugin-dir markers (#2544); leaving it pins the shared '
+ 'config root to CommonJS and keeps GSD occupying a file it no longer writes';
+ 'config root to CommonJS and keeps MSD occupying a file it no longer writes';
const migration: InstallerMigration = {
id: '2026-07-28-retire-config-root-commonjs-marker',
@@ -147,7 +147,7 @@ const migration: InstallerMigration = {
description:
'Remove <configRoot>/package.json when it is exactly the {"type":"commonjs"} marker '
+ 'pre-#2544 installs wrote there, now superseded by markers scoped to the directories '
+ 'GSD owns. A package.json with any other content is left untouched.',
+ 'MSD owns. A package.json with any other content is left untouched.',
introducedIn: '1.8.0',
scopes: ['global', 'local'],
destructive: true,
@@ -157,7 +157,7 @@ const migration: InstallerMigration = {
let stat: import('node:fs').Stats;
try {
// lstat, not existsSync: existsSync follows symlinks and reports false for
// a dangling one. A symlink here is not something GSD wrote, and removing
// a dangling one. A symlink here is not something MSD wrote, and removing
// it is never ours to do — mirrors classifyMarker's fail-closed posture.
stat = fs.lstatSync(markerPath);
} catch {

View File

@@ -1,11 +1,11 @@
/**
* Installer migration: retire Cursor's duplicate commands/ surface (#2644).
*
* Cursor discovers GSD skills as slash-menu entries while also keeping them
* model-invocable. Older GSD releases installed the same workflows again as
* commands/gsd-*.md, so every action appeared twice. The skills remain the
* Cursor discovers MSD skills as slash-menu entries while also keeping them
* model-invocable. Older MSD releases installed the same workflows again as
* commands/msd-*.md, so every action appeared twice. The skills remain the
* sole workflow surface; this migration removes only old command files proven
* managed by gsd-file-manifest.json. Modified files are backed up first and
* managed by msd-file-manifest.json. Modified files are backed up first and
* unmanifested files are preserved.
*/
@@ -30,15 +30,15 @@ interface MigrationPlanContext {
const COMMANDS_DIR = 'commands';
const REASON =
'Cursor exposes skills directly in the slash menu; the parallel command file duplicated the same GSD action (#2644)';
'Cursor exposes skills directly in the slash menu; the parallel command file duplicated the same MSD action (#2644)';
const OWNERSHIP_EVIDENCE =
'pre-#2644 Cursor installs record commands/gsd-*.md in gsd-file-manifest.json';
'pre-#2644 Cursor installs record commands/msd-*.md in msd-file-manifest.json';
const migration = {
id: '2026-07-29-cursor-retire-commands-surface',
title: 'Retire Cursor duplicate commands surface',
description:
'Remove manifest-managed Cursor commands/gsd-*.md files now that skills are the single slash-menu and model-invocation surface.',
'Remove manifest-managed Cursor commands/msd-*.md files now that skills are the single slash-menu and model-invocation surface.',
introducedIn: '1.8.1',
runtimes: ['cursor'],
scopes: ['global', 'local'],
@@ -55,7 +55,7 @@ const migration = {
const actions: MigrationAction[] = [];
for (const entry of entries) {
if (!entry.isFile() || !entry.name.startsWith('gsd-') || !entry.name.endsWith('.md')) continue;
if (!entry.isFile() || !entry.name.startsWith('msd-') || !entry.name.endsWith('.md')) continue;
const relPath = path.posix.join(COMMANDS_DIR, entry.name);
const artifact = ctx.classifyArtifact(relPath);
if (artifact.classification === 'managed-pristine') {

View File

@@ -1,6 +1,6 @@
/**
* Installer migration: retire pi's legacy `<piConfigDir>/hooks/` directory
* after GSD's shared hook bundle moved to `<piConfigDir>/gsd-hooks/` (#3023).
* after MSD's shared hook bundle moved to `<piConfigDir>/msd-hooks/` (#3023).
*
* What old artifact is being retired?
* `hooks/` (and its `hooks/lib/` subdirectory) at the pi config root. pi
@@ -8,19 +8,19 @@
* warns on every startup whenever it exists — pi's
* `checkDeprecatedExtensionDirs()` fires on mere PATH EXISTENCE, not on the
* directory having contents (unlike the sibling `tools/` check, which does
* `readdir` first). GSD used to install its shared hook bundle at exactly
* `readdir` first). MSD used to install its shared hook bundle at exactly
* that reserved path, so every pi install carried the warning permanently.
* The fix moved the install target to `gsd-hooks/`
* The fix moved the install target to `msd-hooks/`
* (`hostBehaviors.sharedHooksDirName`), but an EXISTING install that
* upgrades still has the old `hooks/` tree sitting on disk — nothing
* removes it on its own, so the warning would persist forever without this
* migration.
*
* How do we prove it is GSD-owned?
* How do we prove it is MSD-owned?
* Per file, by manifest membership — the same `classifyArtifact()` check
* every other migration in this directory uses. Pre-#3023 pi installs
* record the shared hook bundle under `hooks/…` keys in
* `gsd-file-manifest.json`; the new install target writes `gsd-hooks/…`
* `msd-file-manifest.json`; the new install target writes `msd-hooks/…`
* keys instead, which this migration structurally never sees because it
* only ever walks the `hooks/` subtree.
*
@@ -108,24 +108,24 @@ interface InstallerMigration {
plan: (ctx: MigrationPlanContext) => MigrationAction[];
}
/** pi's reserved (and, pre-#3023, GSD-populated) legacy hook directory name. */
/** pi's reserved (and, pre-#3023, MSD-populated) legacy hook directory name. */
const HOOKS_DIR = 'hooks';
const FILE_REASON =
"pi's startup check warns whenever hooks/ exists (checkDeprecatedExtensionDirs), and GSD's shared " +
'hook bundle now installs at gsd-hooks/ instead (#3023), so the legacy files are superseded';
"pi's startup check warns whenever hooks/ exists (checkDeprecatedExtensionDirs), and MSD's shared " +
'hook bundle now installs at msd-hooks/ instead (#3023), so the legacy files are superseded';
const FILE_OWNERSHIP_EVIDENCE =
'pre-#3023 pi installs record the shared hook bundle under hooks/… keys in gsd-file-manifest.json; ' +
'the new install target is gsd-hooks/…, which this migration never touches because it only walks the ' +
'pre-#3023 pi installs record the shared hook bundle under hooks/… keys in msd-file-manifest.json; ' +
'the new install target is msd-hooks/…, which this migration never touches because it only walks the ' +
'hooks/ subtree';
const DIR_REASON =
"pi's checkDeprecatedExtensionDirs() warns on hooks/'s mere existence, not its contents (#3023); the " +
'reserved container is retired once every GSD-owned entry inside it is gone';
'reserved container is retired once every MSD-owned entry inside it is gone';
const DIR_OWNERSHIP_EVIDENCE =
'hooks/ and hooks/lib/ are GSD-installed container directories under the pi config root (the pre-#3023 ' +
'hooks/ and hooks/lib/ are MSD-installed container directories under the pi config root (the pre-#3023 ' +
'default of hostBehaviors.sharedHooksDirName); removal is gated on emptiness by the shared ' +
'remove-empty-dir action, so a directory that still holds an unmanifested user file — or any file-level ' +
'action that failed to apply — is left in place rather than assumed empty';
@@ -160,7 +160,7 @@ const migration: InstallerMigration = {
title: "Retire pi's reserved hooks/ directory",
description:
'Remove manifest-managed files under <piConfigDir>/hooks/ and, once empty, the directory itself ' +
'(and its hooks/lib/ subdirectory), now that the shared hook bundle installs at gsd-hooks/ instead. pi ' +
'(and its hooks/lib/ subdirectory), now that the shared hook bundle installs at msd-hooks/ instead. pi ' +
'reserves hooks/ as its own deprecated extension directory and warns on every startup while it exists (#3023).',
introducedIn: '1.9.2',
runtimes: ['pi'],

View File

@@ -64,8 +64,8 @@ interface InstallerMigration {
/** The two retired configHome-relative artifact subpaths. */
const RETIRED_SUBPATHS = ['skills', 'agents'] as const;
/** Only GSD-owned top-level entries are retirement candidates. */
const GSD_PREFIX = 'gsd-';
/** Only MSD-owned top-level entries are retirement candidates. */
const MSD_PREFIX = 'msd-';
const FILE_REASON =
'Antigravity scans ~/.gemini/config/{skills,agents} for global discovery and does not scan the ' +
@@ -73,17 +73,17 @@ const FILE_REASON =
"under the global layout's .gemini/config home override";
const FILE_OWNERSHIP_EVIDENCE =
'pre-#3738 antigravity installs record skills/gsd-*/** and agents/gsd-*.md keys in ' +
'gsd-file-manifest.json at the configHome root; the migration only walks gsd--prefixed entries ' +
'pre-#3738 antigravity installs record skills/msd-*/** and agents/msd-*.md keys in ' +
'msd-file-manifest.json at the configHome root; the migration only walks msd--prefixed entries ' +
'under the two retired subpaths';
const DIR_REASON =
'the retired container directory is removed only once every GSD-owned entry inside it is gone; a ' +
'the retired container directory is removed only once every MSD-owned entry inside it is gone; a ' +
'directory that still holds an unmanifested user file — or any file-level action that failed to ' +
'apply — is left in place by the emptiness re-check';
const DIR_OWNERSHIP_EVIDENCE =
'skills/ and agents/ at the antigravity configHome root are GSD-installed container directories ' +
'skills/ and agents/ at the antigravity configHome root are MSD-installed container directories ' +
'from the pre-#3738 layout; removal is gated on emptiness by the shared remove-empty-dir action';
/**
@@ -115,7 +115,7 @@ const migration: InstallerMigration = {
id: '2026-08-26-antigravity-retire-confighome-artifacts',
title: "Retire Antigravity's configHome skills/agents surfaces",
description:
'Remove manifest-managed skills/gsd-*/ and agents/gsd-*.md under the Antigravity configHome ' +
'Remove manifest-managed skills/msd-*/ and agents/msd-*.md under the Antigravity configHome ' +
'(~/.gemini/antigravity{,-ide,-cli}) — a location Antigravity does not scan for global discovery — ' +
'and retire the now-empty container directories, now that the global layout installs both kinds ' +
'under the ~/.gemini/config home override (#3738). Global scope only; the local .agents workspace ' +
@@ -152,7 +152,7 @@ const migration: InstallerMigration = {
try {
const entries = fs.readdirSync(surfaceRoot, { withFileTypes: true });
for (const entry of entries) {
if (entry.isSymbolicLink() || !entry.name.startsWith(GSD_PREFIX)) continue;
if (entry.isSymbolicLink() || !entry.name.startsWith(MSD_PREFIX)) continue;
const relPath = path.posix.join(subpath, entry.name);
if (entry.isDirectory()) {
dirs.push(relPath);

Some files were not shown because too many files have changed in this diff Show More