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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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