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

@@ -1,5 +1,5 @@
/**
* GSD plugin for OpenCode.ai (CommonJS)
* MSD plugin for OpenCode.ai (CommonJS)
*
* Architecture: SUBPROCESS REUSE. Instead of re-implementing hook logic inside
* the plugin, this file is a thin adapter that spawns the existing Claude Code
@@ -11,36 +11,36 @@
* - block → throw Error (OpenCode returns the error to the model)
* - advisory → output.metadata + console.error (best-effort surfacing)
*
* Namespace conversion (/gsd:xxx → /gsd-xxx) reuses scripts/fix-slash-commands.cjs
* Namespace conversion (/msd:xxx → /msd-xxx) reuses scripts/fix-slash-commands.cjs
* via require(), keeping the single source of truth.
*
* ── Two distribution shapes, one adapter (issue #1914) ─────────────────────
* This single file serves both distribution paths, distinguished at load time
* by REPO_ROOT (path.resolve(__dirname, "../..")):
*
* • Option 1 — file copy (the supported GSD path). `bin/install.js` copies
* this file to <opencodeConfigDir>/plugins/gsd-core.js, so REPO_ROOT is the
* OpenCode config dir. GSD's own install already stages `hooks/*.js` and
* `gsd-core/` there (ADR-857 skips hook *registration* for OpenCode, not the
* • Option 1 — file copy (the supported MSD path). `bin/install.js` copies
* this file to <opencodeConfigDir>/plugins/msd-core.js, so REPO_ROOT is the
* OpenCode config dir. MSD's own install already stages `hooks/*.js` and
* `msd-core/` there (ADR-857 skips hook *registration* for OpenCode, not the
* file copy), so the hook bridge and content rewriting resolve natively.
* Commands/agents/skills are ALREADY registered by GSD's native file copy in
* Commands/agents/skills are ALREADY registered by MSD's native file copy in
* this mode, so the plugin's own config-hook registration is redundant and is
* SKIPPED (see IS_PACKAGE_TREE) to avoid double-registration.
*
* • Option 2 — package / git-spec. When loaded from the package tree (npm
* `main`, or an OpenCode git-spec install), REPO_ROOT is the package root and
* the source layout (commands/gsd/, agents/, skills/) is present. Here the
* the source layout (commands/msd/, agents/, skills/) is present. Here the
* plugin IS the sole registrar, so it registers commands/agents/skills too.
*
* IS_PACKAGE_TREE keys off the presence of the SOURCE command layout
* (commands/gsd/), which only exists in the package tree — never in an installed
* (commands/msd/), which only exists in the package tree — never in an installed
* config dir (that uses the flattened command/ layout). The hook bridge and
* Read-time content rewriting run in BOTH modes; only the config-hook
* registration of commands/agents/skills is gated.
*
* Runtime-specific hooks are deliberately excluded:
* - gsd-statusline.js / gsd-update-banner.js (Claude Code statusline)
* - gsd-cursor-*.js (Cursor-specific)
* - msd-statusline.js / msd-update-banner.js (Claude Code statusline)
* - msd-cursor-*.js (Cursor-specific)
* - *.sh scripts (invoked directly by commands/agents, not hook events)
*/
@@ -51,12 +51,12 @@ const fs = require("fs");
const os = require("os");
const { spawnSync } = require("child_process");
// Resolve REPO_ROOT to the directory that actually holds the GSD payload
// (hooks/ + gsd-core/). This must work across three physical layouts because a
// Resolve REPO_ROOT to the directory that actually holds the MSD payload
// (hooks/ + msd-core/). This must work across three physical layouts because a
// single adapter file serves both distribution shapes (see header):
// • package/git-spec tree: <root>/.opencode/plugins/gsd-core.js → <root>
// • global file-copy: ~/.config/opencode/plugins/gsd-core.js → ~/.config/opencode
// • local file-copy: <proj>/.opencode/plugins/gsd-core.js → <proj>/.opencode
// • package/git-spec tree: <root>/.opencode/plugins/msd-core.js → <root>
// • global file-copy: ~/.config/opencode/plugins/msd-core.js → ~/.config/opencode
// • local file-copy: <proj>/.opencode/plugins/msd-core.js → <proj>/.opencode
// A fixed "../.." only works for the first; the copied layouts sit one level
// shallower. Walking up to the first ancestor containing BOTH payload markers
// resolves all three deterministically. Falls back to the package-tree
@@ -66,7 +66,7 @@ function resolveRepoRoot(startDir) {
for (let i = 0; i < 6; i++) {
if (
fs.existsSync(path.join(dir, "hooks")) &&
fs.existsSync(path.join(dir, "gsd-core"))
fs.existsSync(path.join(dir, "msd-core"))
) {
return dir;
}
@@ -84,16 +84,16 @@ function resolveRepoRoot(startDir) {
// CJS: __dirname is a global, no need to derive from import.meta.url
const REPO_ROOT = resolveRepoRoot(__dirname);
const HOOKS_DIR = path.join(REPO_ROOT, "hooks");
const COMMANDS = path.join(REPO_ROOT, "commands", "gsd");
const COMMANDS = path.join(REPO_ROOT, "commands", "msd");
const AGENTS = path.join(REPO_ROOT, "agents");
const SKILLS = path.join(REPO_ROOT, "skills");
const GSD_CORE = path.join(REPO_ROOT, "gsd-core");
const MSD_CORE = path.join(REPO_ROOT, "msd-core");
// True only when loaded from the package/source tree (Option 2), detected by the
// presence of the SOURCE command layout (commands/gsd/). In an installed OpenCode
// presence of the SOURCE command layout (commands/msd/). In an installed OpenCode
// config dir (Option 1) this directory is absent — the flattened command/ layout
// is used instead — so the plugin skips its own command/agent/skill registration
// and lets GSD's native file copy own that surface (avoids double-registration).
// and lets MSD's native file copy own that surface (avoids double-registration).
const IS_PACKAGE_TREE = fs.existsSync(COMMANDS);
// ---------------------------------------------------------------------------
@@ -198,7 +198,7 @@ function mapToolInput(args) {
* - exit 2: block (Claude convention; reason in stdout JSON)
* - any error: exit 0 silently (hooks swallow their own errors)
*
* @param {string} hookFile filename under hooks/, e.g. "gsd-prompt-guard.js"
* @param {string} hookFile filename under hooks/, e.g. "msd-prompt-guard.js"
* @param {object} payload stdin JSON (hook_event_name, tool_name, ...)
* @param {object} [opts]
* @param {number} [opts.timeout=8000] spawn timeout in ms
@@ -217,9 +217,9 @@ function runHook(hookFile, payload, opts = {}) {
if (!warnedMissingHooks.has(hookFile)) {
warnedMissingHooks.add(hookFile);
console.error(
`[gsd-core] hook script missing: ${hookPath} — ${hookFile} is NOT ` +
"enforced. The GSD install may be incomplete; reinstall (or run " +
"/gsd-update) to restage the hooks/ bundle.",
`[msd-core] hook script missing: ${hookPath} — ${hookFile} is NOT ` +
"enforced. The MSD install may be incomplete; reinstall (or run " +
"/msd-update) to restage the hooks/ bundle.",
);
}
return { stdout: "", exitCode: 0, timedOut: false };
@@ -247,7 +247,7 @@ function runHook(hookFile, payload, opts = {}) {
/**
* In-process check for whether context-usage warnings are disabled in project
* config. Mirrors the exact semantics of the same check inside
* hooks/gsd-context-monitor.js (introduced by #1073): an explicit
* hooks/msd-context-monitor.js (introduced by #1073): an explicit
* `config.hooks.context_warnings === false` disables them; a missing or
* unparseable .planning/config.json keeps them enabled (the default).
*
@@ -278,7 +278,7 @@ function contextWarningsDisabled(cwd) {
* Parse a hook's stdout and apply its effect to the OpenCode output object.
*
* - Block → throw Error(parsed.reason) so OpenCode aborts the tool call
* - Advisory→ append to output.metadata._gsdAdvisory[] and log to stderr
* - Advisory→ append to output.metadata._msdAdvisory[] and log to stderr
* - Silent → no-op
*
* @param {{ stdout: string, exitCode: number }} hookResult
@@ -301,7 +301,7 @@ function handleHookResult(hookResult, output) {
const isBlock = exitCode === 2 || (parsed && parsed.decision === "block");
if (isBlock) {
const reason =
(parsed && parsed.reason) || "Blocked by GSD hook (no reason provided).";
(parsed && parsed.reason) || "Blocked by MSD hook (no reason provided).";
throw new Error(reason);
}
@@ -317,10 +317,10 @@ function handleHookResult(hookResult, output) {
// sequence (prompt guard, read guard, worktree guard, workflow guard).
// Storing a scalar would let a later advisory clobber an earlier one, so
// collect them all.
if (!Array.isArray(output.metadata._gsdAdvisory)) {
output.metadata._gsdAdvisory = [];
if (!Array.isArray(output.metadata._msdAdvisory)) {
output.metadata._msdAdvisory = [];
}
output.metadata._gsdAdvisory.push(advisory);
output.metadata._msdAdvisory.push(advisory);
}
// Best-effort visibility when metadata isn't surfaced to the model
console.error(advisory);
@@ -347,7 +347,7 @@ function parseFrontmatter(content) {
}
// Rewrite @~/.claude/ includes to point at the repo root.
// Also applies /gsd:xxx → /gsd-xxx namespace conversion via the shared
// Also applies /msd:xxx → /msd-xxx namespace conversion via the shared
// transform from scripts/fix-slash-commands.cjs (single source of truth).
function rewriteRefs(content) {
let out = content.replace(/@~\/\.claude\//g, `@${REPO_ROOT}/`);
@@ -370,38 +370,38 @@ function loadDir(dir, keyFn, valFn) {
}
// ---------------------------------------------------------------------------
// Runtime content transform — for Read tool results on GSD-managed files
// Runtime content transform — for Read tool results on MSD-managed files
// ---------------------------------------------------------------------------
// Directories whose .md files may contain ~/.claude/ paths and gsd: namespace
// Directories whose .md files may contain ~/.claude/ paths and msd: namespace
// refs. When the model reads these via the Read tool, we transparently rewrite
// both so OpenCode sees correct paths and hyphen-form command names.
const GSD_MANAGED_DIRS = [
path.join(GSD_CORE, "workflows"),
path.join(GSD_CORE, "references"),
path.join(GSD_CORE, "templates"),
path.join(GSD_CORE, "contexts"),
const MSD_MANAGED_DIRS = [
path.join(MSD_CORE, "workflows"),
path.join(MSD_CORE, "references"),
path.join(MSD_CORE, "templates"),
path.join(MSD_CORE, "contexts"),
COMMANDS,
AGENTS,
SKILLS,
];
function isGsdManagedFile(filePath) {
function isMsdManagedFile(filePath) {
if (!filePath) return false;
const resolved = path.resolve(filePath);
return GSD_MANAGED_DIRS.some(
return MSD_MANAGED_DIRS.some(
(dir) => resolved === dir || resolved.startsWith(dir + path.sep),
);
}
// Rewrite content for OpenCode consumption:
// 1. @-include paths: @~/.claude/ → @<REPO_ROOT>/
// 2. plain-text paths: ~/.claude/gsd-core/ → <GSD_CORE>/
// 3. namespace: gsd:xxx → gsd-xxx (via fix-slash-commands.cjs)
// 2. plain-text paths: ~/.claude/msd-core/ → <MSD_CORE>/
// 3. namespace: msd:xxx → msd-xxx (via fix-slash-commands.cjs)
function rewriteContent(content) {
let out = content;
out = out.replace(/@~\/\.claude\//g, `@${REPO_ROOT}/`);
out = out.replace(/~\/\.claude\/gsd-core\//g, `${GSD_CORE}/`);
out = out.replace(/~\/\.claude\/msd-core\//g, `${MSD_CORE}/`);
const transform = getNamespaceConverter();
if (transform && _cmdNames && _cmdNames.length) {
out = transform(out, _cmdNames);
@@ -415,18 +415,18 @@ function rewriteContent(content) {
//
// OpenCode's skill loader reads SKILL.md files directly from disk and resolves
// @-includes internally — this bypasses our tool.execute hooks. To make
// @~/.claude/gsd-core/... includes resolve, we copy all SKILL.md files to a
// cache directory with paths rewritten to the actual GSD_CORE location.
// @~/.claude/msd-core/... includes resolve, we copy all SKILL.md files to a
// cache directory with paths rewritten to the actual MSD_CORE location.
//
// Only used in package-tree mode (Option 2). In an installed OpenCode config
// dir (Option 1) skills are already staged + registered by GSD's native file
// dir (Option 1) skills are already staged + registered by MSD's native file
// copy, so we never register skills from the plugin (see IS_PACKAGE_TREE).
const SKILLS_CACHE = path.join(
os.homedir(),
".cache",
"opencode",
"gsd-skills",
"msd-skills",
);
function prepareSkillsCache() {
@@ -439,8 +439,8 @@ function prepareSkillsCache() {
// Rewrite @-include paths only; namespace conversion is handled at
// Read-time via tool.execute.after for workflow/reference files.
const rewritten = raw
.replace(/@~\/\.claude\/gsd-core\//g, `@${GSD_CORE}/`)
.replace(/~\/\.claude\/gsd-core\//g, `${GSD_CORE}/`);
.replace(/@~\/\.claude\/msd-core\//g, `@${MSD_CORE}/`)
.replace(/~\/\.claude\/msd-core\//g, `${MSD_CORE}/`);
const destDir = path.join(SKILLS_CACHE, dir);
fs.mkdirSync(destDir, { recursive: true });
fs.writeFileSync(path.join(destDir, "SKILL.md"), rewritten);
@@ -452,25 +452,25 @@ function prepareSkillsCache() {
// Plugin entry
// ===========================================================================
const GsdCorePlugin = async ({ directory } = {}) => {
const MsdCorePlugin = async ({ directory } = {}) => {
if (directory) currentCwd = directory;
return {
// ── Config: register commands / agents / skills paths ──────────────
// Only in package-tree mode (Option 2). In an installed config dir
// (Option 1) GSD's native file copy already registered these, so the
// (Option 1) MSD's native file copy already registered these, so the
// plugin stays out of registration to avoid double-registering.
config: async (config) => {
if (!IS_PACKAGE_TREE) return;
// Commands (commands/gsd/*.md → gsd-<name>)
// Commands (commands/msd/*.md → msd-<name>)
config.command = config.command || {};
const cmds = loadDir(
COMMANDS,
(f) => "gsd-" + f.slice(0, -3),
(f) => "msd-" + f.slice(0, -3),
(body, fm, name) => ({
template: rewriteRefs(body.trim()),
description: fm.description || `GSD ${name.slice(0, -3)} command`,
description: fm.description || `MSD ${name.slice(0, -3)} command`,
}),
);
for (const [k, v] of Object.entries(cmds)) {
@@ -484,7 +484,7 @@ const GsdCorePlugin = async ({ directory } = {}) => {
(f) => f.slice(0, -3),
(body, fm, name) => ({
prompt: rewriteRefs(body.trim()),
description: fm.description || `GSD ${name.slice(0, -3)} agent`,
description: fm.description || `MSD ${name.slice(0, -3)} agent`,
mode: fm.mode || "subagent",
}),
);
@@ -508,7 +508,7 @@ const GsdCorePlugin = async ({ directory } = {}) => {
// ── shell.env ───────────────────────────────────────────────────────
"shell.env": async (_input, output) => {
output.env = output.env || {};
output.env.GSD_DIR = GSD_CORE;
output.env.MSD_DIR = MSD_CORE;
},
// ── tool.execute.before — PreToolUse hooks ─────────────────────────
@@ -517,14 +517,14 @@ const GsdCorePlugin = async ({ directory } = {}) => {
const toolInput = mapToolInput(output.args || {});
const cwd = currentCwd;
// 0. Read path rewrite — redirect ~/.claude/gsd-core/ to actual GSD_CORE
// 0. Read path rewrite — redirect ~/.claude/msd-core/ to actual MSD_CORE
// so the model can read workflow/reference/template files that SKILL.md
// and command templates reference via the canonical Claude path.
if (claudeTool === "Read" && toolInput.file_path) {
const original = toolInput.file_path;
const rewritten = original
.replace(/^~\/\.claude\/gsd-core\//, GSD_CORE + "/")
.replace(/(?:.*)\/\.claude\/gsd-core\//, GSD_CORE + "/");
.replace(/^~\/\.claude\/msd-core\//, MSD_CORE + "/")
.replace(/(?:.*)\/\.claude\/msd-core\//, MSD_CORE + "/");
if (rewritten !== original) {
const args = output.args || {};
if (args.filePath) args.filePath = rewritten;
@@ -539,7 +539,7 @@ const GsdCorePlugin = async ({ directory } = {}) => {
cwd,
};
// NOTE: session_id intentionally omitted for PreToolUse hooks.
// gsd-read-guard.js treats a non-empty session_id as a Claude Code
// msd-read-guard.js treats a non-empty session_id as a Claude Code
// session and skips its advisory. On OpenCode we WANT the advisory.
const prePayload = (overrides = {}) => ({
...basePayload,
@@ -550,42 +550,42 @@ const GsdCorePlugin = async ({ directory } = {}) => {
const isWriteLike = ["Write", "Edit", "MultiEdit"].includes(claudeTool);
// 1. gsd-prompt-guard.js — injection scan on .planning/ writes
// 1. msd-prompt-guard.js — injection scan on .planning/ writes
if (claudeTool === "Write" || claudeTool === "Edit") {
const r = runHook("gsd-prompt-guard.js", prePayload());
const r = runHook("msd-prompt-guard.js", prePayload());
handleHookResult(r, output);
}
// 2. gsd-read-guard.js — read-before-edit advisory
// 2. msd-read-guard.js — read-before-edit advisory
if (claudeTool === "Write" || claudeTool === "Edit") {
const r = runHook("gsd-read-guard.js", prePayload());
const r = runHook("msd-read-guard.js", prePayload());
handleHookResult(r, output);
}
// 3. gsd-worktree-path-guard.js — hard-block edits outside worktree
// 3. msd-worktree-path-guard.js — hard-block edits outside worktree
if (isWriteLike) {
const r = runHook("gsd-worktree-path-guard.js", prePayload());
const r = runHook("msd-worktree-path-guard.js", prePayload());
handleHookResult(r, output);
}
// 4. gsd-write-guard.js — hard-block catastrophic shrink of curated
// 4. msd-write-guard.js — hard-block catastrophic shrink of curated
// .planning/ artifacts (ROADMAP.md, milestones/*-ROADMAP.md, STATE.md)
if (claudeTool === "Write") {
const r = runHook("gsd-write-guard.js", prePayload());
const r = runHook("msd-write-guard.js", prePayload());
handleHookResult(r, output);
}
// 5. gsd-workflow-guard.js — workflow advisory + git-force-add block
// 5. msd-workflow-guard.js — workflow advisory + git-force-add block
// (covers Write/Edit/MultiEdit AND Bash force-add detection)
if (isWriteLike || claudeTool === "Bash") {
const r = runHook("gsd-workflow-guard.js", prePayload());
const r = runHook("msd-workflow-guard.js", prePayload());
handleHookResult(r, output);
}
// 6. gsd-secret-read-guard.js — hard-block reads of .env / .env.<suffix> /
// 6. msd-secret-read-guard.js — hard-block reads of .env / .env.<suffix> /
// .secrets via Read (file_path), Grep (path or glob) and Bash (command)
if (["Read", "Grep", "Bash"].includes(claudeTool)) {
const r = runHook("gsd-secret-read-guard.js", prePayload());
const r = runHook("msd-secret-read-guard.js", prePayload());
handleHookResult(r, output);
}
},
@@ -598,12 +598,12 @@ const GsdCorePlugin = async ({ directory } = {}) => {
const toolInput = mapToolInput(input.args || {});
const cwd = currentCwd;
// GSD content transform — rewrite paths + namespace in Read results
// MSD content transform — rewrite paths + namespace in Read results
// BEFORE injection scanning so the scanner sees the final content.
if (
claudeTool === "Read" &&
output.output &&
isGsdManagedFile(toolInput.file_path)
isMsdManagedFile(toolInput.file_path)
) {
const content =
typeof output.output === "string"
@@ -612,7 +612,7 @@ const GsdCorePlugin = async ({ directory } = {}) => {
output.output = rewriteContent(content);
}
// gsd-read-injection-scanner.js — scan Read/WebFetch/WebSearch results
// msd-read-injection-scanner.js — scan Read/WebFetch/WebSearch results
if (
claudeTool === "Read" ||
claudeTool === "WebFetch" ||
@@ -625,12 +625,12 @@ const GsdCorePlugin = async ({ directory } = {}) => {
tool_response: output.output,
cwd,
};
const r = runHook("gsd-read-injection-scanner.js", payload);
const r = runHook("msd-read-injection-scanner.js", payload);
handleHookResult(r, output);
return;
}
// gsd-context-monitor.js — context usage warnings (Bash/Edit/Write/Task/...)
// msd-context-monitor.js — context usage warnings (Bash/Edit/Write/Task/...)
// Only meaningful when a session_id is tracked (writes metrics sentinel).
// #2697: skip the subprocess spawn entirely when context warnings are
// explicitly disabled in project config — the hook would exit early anyway,
@@ -644,7 +644,7 @@ const GsdCorePlugin = async ({ directory } = {}) => {
session_id: currentSessionId,
cwd,
};
const r = runHook("gsd-context-monitor.js", payload);
const r = runHook("msd-context-monitor.js", payload);
handleHookResult(r, output);
}
},
@@ -657,13 +657,13 @@ const GsdCorePlugin = async ({ directory } = {}) => {
session_id: currentSessionId,
cwd: currentCwd,
};
const r = runHook("gsd-context-monitor.js", payload);
const r = runHook("msd-context-monitor.js", payload);
handleHookResult(r, output);
// Also inject a GSD compaction breadcrumb (mirrors the original plugin)
// Also inject a MSD compaction breadcrumb (mirrors the original plugin)
output.context = output.context || [];
output.context.push(
`[GSD] Active session: ${currentSessionId}. Preserve any in-flight phase/plan state.`,
`[MSD] Active session: ${currentSessionId}. Preserve any in-flight phase/plan state.`,
);
},
@@ -679,14 +679,14 @@ const GsdCorePlugin = async ({ directory } = {}) => {
info?.id || event.sessionID || event.session_id || null;
if (info?.directory) currentCwd = info.directory;
// gsd-ensure-canonical-path.js — no stdin dependency; silent
runHook("gsd-ensure-canonical-path.js", {
// msd-ensure-canonical-path.js — no stdin dependency; silent
runHook("msd-ensure-canonical-path.js", {
hook_event_name: "SessionStart",
session_id: currentSessionId,
cwd: currentCwd,
});
// gsd-check-update.js — spawns its own background worker; no stdin
runHook("gsd-check-update.js", {
// msd-check-update.js — spawns its own background worker; no stdin
runHook("msd-check-update.js", {
hook_event_name: "SessionStart",
session_id: currentSessionId,
cwd: currentCwd,
@@ -709,17 +709,17 @@ const GsdCorePlugin = async ({ directory } = {}) => {
event: "change",
cwd,
};
const r = runHook("gsd-config-reload.js", payload);
const r = runHook("msd-config-reload.js", payload);
// Advisory-only (additionalContext); surface to logs
handleHookResult(r);
return;
}
// session.idle ↔ Claude Stop lifecycle point (#1682 Slice 1b/c).
// OpenCode fires session.idle when the run quiesces. GSD maps it to the
// OpenCode fires session.idle when the run quiesces. MSD maps it to the
// Stop equivalent — the opencode-subset lifecycle peer of compaction
// (compaction preserves state across context-window summarization; idle
// marks end-of-turn). No-op sentinel today (GSD state is already
// marks end-of-turn). No-op sentinel today (MSD state is already
// persisted to .planning/), but it MUST be recognized so the declared
// opencode-subset surface is fully wired and a future Stop-class hook can
// attach without a plugin change.
@@ -728,9 +728,9 @@ const GsdCorePlugin = async ({ directory } = {}) => {
}
// permission.asked / permission.replied — OpenCode permission lifecycle
// (#2087, opencode.ai/docs/plugins). GSD gates tool INPUTS at
// (#2087, opencode.ai/docs/plugins). MSD gates tool INPUTS at
// tool.execute.before (read-guard, injection-scanner); the permission
// grant/deny decision itself carries no GSD workflow-phase contribution,
// grant/deny decision itself carries no MSD workflow-phase contribution,
// so these are recognized sentinels — wired so a future permission-aware
// gate can attach without a plugin change (the engine owns phase
// sequencing; this host bus is session/tool/permission-scoped, never
@@ -739,7 +739,7 @@ const GsdCorePlugin = async ({ directory } = {}) => {
return;
}
// session.error — OpenCode session-error lifecycle point (#2087). No GSD
// session.error — OpenCode session-error lifecycle point (#2087). No MSD
// hook fires here today (loop state is already persisted to .planning/);
// recognized so the declared extension-event surface is fully wired and a
// future error-class hook can attach without a plugin change.
@@ -761,7 +761,7 @@ const GsdCorePlugin = async ({ directory } = {}) => {
// The subtlety: depending on how OpenCode's runtime (Node or Bun) imports a
// CommonJS file, `mod` may be the raw `module.exports` OR an ESM namespace of
// the form `{ default: module.exports, ...syntheticNamedExports }`. A plain
// `module.exports = { id: "gsd-core", server }` literal risks a string `id`
// `module.exports = { id: "msd-core", server }` literal risks a string `id`
// appearing in `Object.values(mod)` (as a raw property, or as a lexer-
// synthesized named export) — which would trip the throw. Two defenses:
// 1. `id` is defined NON-ENUMERABLE, so it never appears in Object.values yet
@@ -772,23 +772,23 @@ const GsdCorePlugin = async ({ directory } = {}) => {
// Result: raw-CJS `Object.values` = `[server]`; ESM `Object.values` =
// `[{server, <id non-enum>}]` — both fully extractable. Test-only helpers hang
// off the `server` FUNCTION (`server._internals`), never as a sibling export.
GsdCorePlugin._internals = {
MsdCorePlugin._internals = {
REPO_ROOT,
IS_PACKAGE_TREE,
mapToolName,
mapToolInput,
parseFrontmatter,
rewriteContent,
isGsdManagedFile,
isMsdManagedFile,
handleHookResult,
GsdCorePlugin,
MsdCorePlugin,
};
const gsdCorePluginExport = { server: GsdCorePlugin };
Object.defineProperty(gsdCorePluginExport, "id", {
value: "gsd-core",
const msdCorePluginExport = { server: MsdCorePlugin };
Object.defineProperty(msdCorePluginExport, "id", {
value: "msd-core",
enumerable: false,
writable: false,
configurable: false,
});
module.exports = gsdCorePluginExport;
module.exports = msdCorePluginExport;