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:
@@ -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 = {
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
];
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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[],
|
||||
|
||||
@@ -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 */ }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)'
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
|
||||
@@ -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.)
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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 */ }
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 (
|
||||
|
||||
20
src/docs.cts
20
src/docs.cts
@@ -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),
|
||||
|
||||
@@ -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.`,
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()`,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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: {} },
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
},
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: () => {
|
||||
|
||||
240
src/init.cts
240
src/init.cts
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
@@ -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({
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 [];
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user