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

@@ -3,8 +3,8 @@
"SessionStart": [
{
"hooks": [
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gsd-ensure-canonical-path.js\"", "timeout": 5 },
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gsd-check-update.js\"" }
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/msd-ensure-canonical-path.js\"", "timeout": 5 },
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/msd-check-update.js\"" }
]
}
],
@@ -12,32 +12,32 @@
{
"matcher": "Write|Edit",
"hooks": [
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gsd-prompt-guard.js\"", "timeout": 5 },
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gsd-read-guard.js\"", "timeout": 5 }
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/msd-prompt-guard.js\"", "timeout": 5 },
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/msd-read-guard.js\"", "timeout": 5 }
]
},
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gsd-worktree-path-guard.js\"", "timeout": 5 }
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/msd-worktree-path-guard.js\"", "timeout": 5 }
]
},
{
"matcher": "Write",
"hooks": [
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gsd-write-guard.js\"", "timeout": 5 }
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/msd-write-guard.js\"", "timeout": 5 }
]
},
{
"matcher": "Read|Grep|Bash",
"hooks": [
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gsd-secret-read-guard.js\"", "timeout": 5 }
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/msd-secret-read-guard.js\"", "timeout": 5 }
]
},
{
"matcher": "Agent|Task",
"hooks": [
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gsd-agent-isolation-guard.js\"", "timeout": 5 }
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/msd-agent-isolation-guard.js\"", "timeout": 5 }
]
}
],
@@ -45,34 +45,34 @@
{
"matcher": "Bash|Edit|Write|MultiEdit|Agent|Task",
"hooks": [
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gsd-context-monitor.js\"", "timeout": 10 }
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/msd-context-monitor.js\"", "timeout": 10 }
]
},
{
"matcher": "Read|WebFetch|WebSearch",
"hooks": [
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gsd-read-injection-scanner.js\"", "timeout": 5 }
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/msd-read-injection-scanner.js\"", "timeout": 5 }
]
}
],
"SubagentStop": [
{
"hooks": [
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gsd-context-monitor.js\"", "timeout": 10 }
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/msd-context-monitor.js\"", "timeout": 10 }
]
}
],
"Stop": [
{
"hooks": [
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gsd-context-monitor.js\"", "timeout": 10 }
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/msd-context-monitor.js\"", "timeout": 10 }
]
}
],
"PreCompact": [
{
"hooks": [
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gsd-context-monitor.js\"", "timeout": 10 }
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/msd-context-monitor.js\"", "timeout": 10 }
]
}
],
@@ -80,7 +80,7 @@
{
"matcher": "config.json",
"hooks": [
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gsd-config-reload.js\"", "timeout": 8 }
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/msd-config-reload.js\"", "timeout": 8 }
]
}
]

View File

@@ -6,10 +6,10 @@
// Why this copy exists: hooks/ runs straight from a raw, unbuilt clone — a
// shipped hook must be able to `require('./lib/cli-exit.js')` relative to
// its own __dirname and terminate through `terminateNow` without depending
// on any build artifact. gsd-core/bin/lib/cli-exit.cjs is gitignored tsc
// on any build artifact. msd-core/bin/lib/cli-exit.cjs is gitignored tsc
// output and doubles as the build sentinel, so it cannot be required from
// here. `.js`, not `.cjs`, to match the hooks/lib/*.js convention. Hence one
// source, three emitted locations (gsd-core/bin/lib, scripts/lib, hooks/lib).
// source, three emitted locations (msd-core/bin/lib, scripts/lib, hooks/lib).
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
@@ -20,7 +20,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
* 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
@@ -50,19 +50,19 @@ const exitCodeFor = (name) => exitCodeRegistryModule.exitCodeFor(name);
const EXIT_ENVELOPE_REASON = 'sdk_fail_fast';
/**
* 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) {
globalThis[JSON_ERROR_MODE_KEY] = !!v;
}
@@ -82,7 +82,7 @@ const HOOK_DENY_CODE = exitCodeFor(HOOK_DENY_NAME);
* `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) {
globalThis[CONTRACT_VERSION_KEY] = v;
}
@@ -90,12 +90,12 @@ function setContractVersion(v) {
* 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
@@ -178,7 +178,7 @@ function projectOutcome(outcome, version) {
* `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) {
globalThis[PENDING_OUTCOME_KEY] = v;
}
@@ -202,10 +202,10 @@ function findExitContractFlag(argv) {
* 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`/
@@ -220,7 +220,7 @@ function resolveContractVersion(opts = {}) {
const argv = opts.argv ?? process.argv;
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;
let resolved;
@@ -406,7 +406,7 @@ function runMain(main) {
* 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
@@ -456,7 +456,7 @@ function terminateNow(outcome, payload, stderrPayload) {
throw new Error(`terminateNow: exit code ${HOOK_DENY_CODE} is reserved to the ${HOOK_DENY_NAME} outcome; `
+ `got outcome ${JSON.stringify(outcome)}`);
}
// 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
@@ -521,7 +521,7 @@ function terminateNow(outcome, payload, stderrPayload) {
process.exit(projected);
}
catch (err) {
// Anything above threw: an unrecognized --exit-contract/GSD_EXIT_CONTRACT
// Anything above threw: an unrecognized --exit-contract/MSD_EXIT_CONTRACT
// value, a non-string/empty/unregistered `outcome`, or the HOOK_DENY
// collision guard. Diagnose on stderr — swallowing this silently would
// make a typo'd outcome name or a bad contract-version env var

View File

@@ -1,6 +1,6 @@
// cursor-workspace.js — shared workspace resolution for Cursor lifecycle hooks (#2587).
//
// No `gsd-hook-version:` marker, deliberately — matching hooks/lib/git-cmd.js.
// No `msd-hook-version:` marker, deliberately — matching hooks/lib/git-cmd.js.
// Neither copy path that stages hooks/lib/*.js substitutes the version
// placeholder (copyLibDir stamps .sh files only), and the managed-hooks
// staleness scan covers top-level hooks/ names, not hooks/lib/. A marker here
@@ -16,10 +16,10 @@
// the single place that turns that payload into a project root, so the three
// hooks cannot drift apart (they previously carried three copies of it).
//
// Consumers: gsd-cursor-session-start.js, gsd-cursor-stop.js,
// gsd-cursor-subagent-start.js. Staged into <config>/hooks/lib/ by
// Consumers: msd-cursor-session-start.js, msd-cursor-stop.js,
// msd-cursor-subagent-start.js. Staged into <config>/hooks/lib/ by
// writeCursorHooksJson (src/runtime-hooks-surface.cts) alongside the scripts
// that require it, and registered in the installer's GSD_HOOK_LIB_FILES so
// that require it, and registered in the installer's MSD_HOOK_LIB_FILES so
// uninstall and the manifest manage it.
'use strict';
@@ -32,7 +32,7 @@ const path = require('path');
*
* Search order:
* 1. Each entry of `workspace_roots` that actually carries .planning/STATE.md
* — so a multi-root workspace whose GSD project is not the first root
* — so a multi-root workspace whose MSD project is not the first root
* still resolves.
* 2. process.cwd(), if IT carries .planning/STATE.md. cwd is a CANDIDATE, not
* merely the empty-roots fallback: an IDE invocation can supply

View File

@@ -1,13 +1,13 @@
'use strict';
// hooks/lib/dispatch-identity.js — the ONE canonical owner of the
// `[gsd:dispatch phase="…" plan="…"]` marker format and its prose fallback
// `[msd:dispatch phase="…" plan="…"]` marker format and its prose fallback
// (#4594, epic #4630 Phase 1). See
// `.gsd/phase/fix-4594-dispatch-identity-seam/40-design.md` for the full
// `.msd/phase/fix-4594-dispatch-identity-seam/40-design.md` for the full
// rationale (behavior table, negative space, rejected alternatives).
//
// COLD-LOAD CONSTRAINT (load-bearing, not a style choice): this module MUST
// require NOTHING — no `fs`, no `path`, and above all nothing under
// `gsd-core/bin/lib/` or `ensure-runtime-build`. The guard hooks that consume
// `msd-core/bin/lib/` or `ensure-runtime-build`. The guard hooks that consume
// this module must load on a raw plugin-marketplace install where the
// compiled lib is absent and the self-healing build seam has not run — a
// hook that dies at module load is worse than one carrying a mirror. See
@@ -16,10 +16,10 @@
// DISPATCH_PHASE_TOKEN_SOURCE is a DELIBERATE MIRROR of
// `CASE_FLEXIBLE_PHASE_NUMBER_TOKEN_SOURCE` in `src/phase-id.cts`
// (ADR-2121 owns the phase-token grammar; `gsd-core/bin/lib/phase-id.cjs` is
// (ADR-2121 owns the phase-token grammar; `msd-core/bin/lib/phase-id.cjs` is
// its compiled form). It cannot be an `require()`-based import: importing the
// compiled lib here would violate the cold-load constraint above (either a
// direct dependency on `gsd-core/bin/lib/` or a forced self-heal via
// direct dependency on `msd-core/bin/lib/` or a forced self-heal via
// `ensure-runtime-build`), which is exactly the failure mode this module
// exists to avoid for the guard hooks that consume it.
//
@@ -34,7 +34,7 @@
// into a loud, in-CI failure.
const DISPATCH_PHASE_TOKEN_SOURCE = '\\d+[A-Za-z]?(?:\\.\\d+)*';
// Bounded marker grammar: `[gsd:dispatch key="value" key2="value2"]`.
// Bounded marker grammar: `[msd:dispatch key="value" key2="value2"]`.
// - Key names bounded to {1,31} — no key we emit or expect is anywhere near
// that long; this is purely a backstop against pathological input.
// - Values bounded to {0,200} and forbidden from containing `"`, `]`, or any
@@ -42,7 +42,7 @@ const DISPATCH_PHASE_TOKEN_SOURCE = '\\d+[A-Za-z]?(?:\\.\\d+)*';
// sibling key — enforced structurally by the negated character class, not
// by a separate validation pass.
// Global flag so `findMarker` can scan forward through a large prompt.
const MARKER_RE = /\[gsd:dispatch((?:\s+[A-Za-z][A-Za-z0-9_-]{0,31}="[^"\]\r\n]{0,200}")*)\s*\]/g;
const MARKER_RE = /\[msd:dispatch((?:\s+[A-Za-z][A-Za-z0-9_-]{0,31}="[^"\]\r\n]{0,200}")*)\s*\]/g;
const MARKER_KV_RE = /([A-Za-z][A-Za-z0-9_-]{0,31})="([^"\]\r\n]{0,200})"/g;
// Prose fallback frame: `execute plan <token> of phase <PHASE TOKEN>`.
@@ -60,7 +60,7 @@ const PROSE_RE = new RegExp(
);
/**
* Render the `[gsd:dispatch phase="…" plan="…"]` marker for a producer to
* Render the `[msd:dispatch phase="…" plan="…"]` marker for a producer to
* embed verbatim in a dispatch description/prompt. Never throws.
*
* Emits only keys whose value is a non-empty string not containing `"`, `]`,
@@ -79,7 +79,7 @@ function renderDispatchIdentityMarker(input) {
}
}
if (parts.length === 0) return '';
return `[gsd:dispatch ${parts.join(' ')}]`;
return `[msd:dispatch ${parts.join(' ')}]`;
}
function emptyResult() {

View File

@@ -1,9 +1,9 @@
'use strict';
// GENERATED FILE — DO NOT EDIT BY HAND.
// Source of truth: gsd-core/bin/shared/exit-codes.json. Regenerate with:
// Source of truth: msd-core/bin/shared/exit-codes.json. Regenerate with:
// node scripts/gen-exit-code-registry.cjs --write
// This exact content is emitted to THREE locations — gsd-core/bin/lib/exit-code-registry.cjs,
// This exact content is emitted to THREE locations — msd-core/bin/lib/exit-code-registry.cjs,
// scripts/lib/exit-code-registry.cjs, and hooks/lib/exit-code-registry.js (the latter two
// committed so scripts/ and hooks/ consumers work on an unbuilt clone) — all byte-compared by
// `npm run lint:generated-sync` (#3905 ADR-3889 Phase 1; #3906 Phase 2 added the second copy;
@@ -52,7 +52,7 @@ const EXIT_CODES = Object.freeze([
code: 80,
name: "DEGRADED",
meaning: "Ran to completion and is reporting a condition through its result payload rather than as a process failure",
owner: "gsd-tools",
owner: "msd-tools",
authorizedBy: "ADR-3889 + ADR-2980",
})
]);

View File

@@ -21,17 +21,17 @@
* const { isGitSubcommand } = require(path.join(__dirname, 'lib', 'git-cmd.js'));
*
* `tokenize()` delegates to the shared `src/token-scanner.cts` seam (ADR-3212
* §4, epic #3212 Phase 3, #3414) — the built `gsd-core/bin/lib/token-scanner.cjs`
* §4, epic #3212 Phase 3, #3414) — the built `msd-core/bin/lib/token-scanner.cjs`
* artifact, not a sibling hooks/-tree file, because hook scripts are staged as
* standalone files at install time and a sibling require is a staging
* dependency that can fail silently (see gsd-workflow-guard.js's own
* dependency that can fail silently (see msd-workflow-guard.js's own
* KIMI_TOOL_NAMES comment for the precedent this follows). Re-exported here
* unchanged — every existing caller's behavior is identical (parity-asserted
* in tests/token-scanner.test.cjs row 5).
*/
const path = require('path');
const { tokenizeShellLike } = require(path.join(__dirname, '..', '..', 'gsd-core', 'bin', 'lib', 'token-scanner.cjs'));
const { tokenizeShellLike } = require(path.join(__dirname, '..', '..', 'msd-core', 'bin', 'lib', 'token-scanner.cjs'));
/**
* Git global options that take a following argument.
@@ -195,7 +195,7 @@ function isGitSubcommand(cmd, sub) {
* `git commit -m WIP`. All were allowed upstream and would have started being
* blocked. Reported in review of #3802.
*
* The defect this DOES fix: `gsd-validate-commit.sh` captured the message with
* The defect this DOES fix: `msd-validate-commit.sh` captured the message with
* `-m[[:space:]]+"([^"]+)"`, and bash `[^"]` matches newlines, so the widely
* used agent-authored commit idiom
*
@@ -224,7 +224,7 @@ function isGitSubcommand(cmd, sub) {
* See the expansion guard in the body (review of #3816, round 4).
*
* KNOWN LIMIT: the DOUBLE-QUOTED delimiter spelling (`<<"EOF"`) is resolvable
* here but unreachable through the caller — `gsd-validate-commit.sh`'s
* here but unreachable through the caller — `msd-validate-commit.sh`'s
* double-quoted `-m` capture stops at the first `"`, which in that spelling is
* the delimiter's own quote, so the resolver only ever sees a truncated opener
* and the commit stays blocked. Its single-quoted `-m` capture DOES deliver the

View File

@@ -3,8 +3,8 @@
// distinguishing a GENUINE negative answer (git ran and said "no") from
// "could not determine" (timeout / spawn failure / signal kill) — #3911.
//
// Proven defect: hooks/gsd-worktree-path-guard.js, hooks/gsd-workflow-guard.js,
// and hooks/gsd-windsurf-pre-write.js each treat spawnSync(git, ..., {timeout})
// Proven defect: hooks/msd-worktree-path-guard.js, hooks/msd-workflow-guard.js,
// and hooks/msd-windsurf-pre-write.js each treat spawnSync(git, ..., {timeout})
// returning a non-zero/empty result as "not applicable" and allow silently.
// A macOS CI run showed three deny cases land at 2084ms/2112ms/2177ms — just
// past the 2000ms budget — each returning exit 0 with EMPTY stdout AND EMPTY
@@ -62,7 +62,7 @@ function classifyGitProbe(result) {
* any exit code, never throws. A no-op when the probe genuinely ran and
* answered (status 0 or non-zero with no error/signal).
*
* @param {string} hookName - e.g. 'gsd-worktree-path-guard'
* @param {string} hookName - e.g. 'msd-worktree-path-guard'
* @param {string} probeLabel - e.g. "git rev-parse --show-toplevel"
* @param {*} result - the raw spawnSync() return value
*/

View File

@@ -4,14 +4,14 @@
* injection-patterns.js — the shared prompt-injection pattern list (#3504, epic #1900).
*
* Single source of truth for the standard injection signatures used by BOTH
* gsd-prompt-guard.js (PreToolUse scan of writes into .planning/) and
* gsd-read-injection-scanner.js (PostToolUse scan of Read/WebFetch/WebSearch
* msd-prompt-guard.js (PreToolUse scan of writes into .planning/) and
* msd-read-injection-scanner.js (PostToolUse scan of Read/WebFetch/WebSearch
* content). Previously each hook carried a byte-identical copy ("inlined for
* hook independence") that could silently drift — a pattern tightened in one
* would stop protecting the other surface.
*
* Why a shared lib require is safe here (the old inlining rationale, retired):
* the installer stages hooks/lib/ from the GSD_HOOK_LIB_FILES allowlist for the
* the installer stages hooks/lib/ from the MSD_HOOK_LIB_FILES allowlist for the
* shared-bundle surfaces (Claude-family settings.json runtimes and Kimi), the
* Cursor stager auto-discovers require('./lib/...') in staged scripts and fails
* the install loudly when a helper is missing (#2587), and the plugin path
@@ -21,7 +21,7 @@
* set runs inside the compiled lib tree; hooks must stay loadable standalone
* without it. The two lists are different surfaces by design, not drift.
*
* Keep this file free of literal 'gsd:' text — the stager rewrites that marker
* Keep this file free of literal 'msd:' text — the stager rewrites that marker
* in staged hook content.
*/
@@ -63,9 +63,9 @@ const INJECTION_PATTERNS = Object.freeze([
// Short single-line label for a matched pattern, used for both the rendered
// advisory prose and the typed `findings[].match` field in BOTH consumer
// hooks. The raw regex source is not user-facing: the #4016 superset pattern
// is ~280 characters, and gsd-prompt-guard.js echoed it verbatim into its
// is ~280 characters, and msd-prompt-guard.js echoed it verbatim into its
// advisory (PR #4061 review nit). Byte-identical to the transform
// gsd-read-injection-scanner.js carried inline since #3523, hoisted here so
// msd-read-injection-scanner.js carried inline since #3523, hoisted here so
// one finding renders the same way in both hooks. Both hooks already require
// this module, so this adds no new staging dependency.
function describePattern(pattern) {

View File

@@ -1,7 +1,7 @@
'use strict';
// hooks/lib/isolation-deny-reason.js — shared, frozen reason-code enum for
// the #3045 dispatch-isolation guards' block/deny decisions
// (hooks/gsd-agent-isolation-guard.js, hooks/gsd-cursor-subagent-start.js).
// (hooks/msd-agent-isolation-guard.js, hooks/msd-cursor-subagent-start.js).
//
// CONTRIBUTING.md ("Prohibited: Raw Text Matching on Test Outputs") bans
// asserting on a hook's free-form, human-readable reason/user_message prose
@@ -10,12 +10,12 @@
// one of these STABLE codes (surfaced on the hook's stdout JSON as
// `reason_code`), so tests assert `out.reason_code === REASON_CODE.X`
// instead of regexing the message (mirrors the REASON enum convention in
// gsd-core/bin/verify-reapply-patches.cjs).
// msd-core/bin/verify-reapply-patches.cjs).
//
// Adding a new code requires updating this enum AND any test that locks the
// documented set.
const REASON_CODE = Object.freeze({
// The compiled runtime library (gsd-core/bin/lib/*.cjs) is missing and
// The compiled runtime library (msd-core/bin/lib/*.cjs) is missing and
// could not be self-built (ensure-runtime-build.cjs's RuntimeBuildError).
RUNTIME_BUILD_FAILED: 'runtime_build_failed',
// The project's dispatch-isolation configuration ('.planning/config.json')
@@ -26,7 +26,7 @@ const REASON_CODE = Object.freeze({
// is missing the harness's isolation flag/kwarg.
HARNESS_FLAG_MISSING: 'harness_flag_missing',
// Isolation resolves to "harness-worktree" but the dispatch payload carries
// no usable subagent_type, so the guard cannot confirm it is a GSD executor.
// no usable subagent_type, so the guard cannot confirm it is a MSD executor.
NO_SUBAGENT_TYPE: 'no_subagent_type',
// Isolation resolves to "harness-worktree" but whether the workspace root
// is an isolated worktree could not be determined (e.g. git unresponsive).
@@ -44,8 +44,8 @@ const REASON_CODE = Object.freeze({
// (same discipline as escaping an untrusted token before embedding it in a
// message, e.g. phase-plan-index's `depends_on` warning).
//
// Originally duplicated byte-for-byte in hooks/gsd-agent-isolation-guard.js
// and hooks/gsd-cursor-subagent-start.js (#4594 F2 review finding) — both
// Originally duplicated byte-for-byte in hooks/msd-agent-isolation-guard.js
// and hooks/msd-cursor-subagent-start.js (#4594 F2 review finding) — both
// hooks already require this dependency-free module, so there is no
// cold-load justification for the duplication the way there is for
// hooks/lib/dispatch-identity.js's grammar mirror. Moved here as the single

View File

@@ -1,7 +1,7 @@
'use strict';
// hooks/lib/isolation-sentinel.js — shared sentinel reader for the #3045
// agent-dispatch isolation guards (hooks/gsd-agent-isolation-guard.js,
// hooks/gsd-cursor-subagent-start.js).
// agent-dispatch isolation guards (hooks/msd-agent-isolation-guard.js,
// hooks/msd-cursor-subagent-start.js).
//
// #3045 BLOCKER: the guards previously keyed enforcement on the capability
// REGISTRY's `dispatch.isolation` ("this host CAN isolate"), not the
@@ -10,12 +10,12 @@
// harness-worktree-capable host — project-level `workflow.use_worktrees:
// false`, the #2474 per-plan submodule degrade, and the #683/#3060
// base-check auto-degrade all resolve to `none` and are NOT bugs
// (gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md:
// (msd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md:
// "Sequential mode … Omit isolation=\"worktree\" from the Agent call").
//
// The workflow already computes ISOLATION deterministically in shell before
// any executor dispatch. CORE REDESIGN (two-review follow-up): the PRIMARY
// write path is now `dispatch-isolation` itself (gsd-tools.cjs
// write path is now `dispatch-isolation` itself (msd-tools.cjs
// routeDispatchIsolation) — it persists mode + harnessFlag + phase/plan
// identifiers to the sentinel as an unconditional side effect of resolving
// them, since the workflow must call it to learn ISOLATION at all.
@@ -23,8 +23,8 @@
// explicit fallback for the per-plan submodule-degrade override and shares
// the exact same atomic-write implementation.
//
// Sentinel path: `<cwd>/.gsd/dispatch-isolation-sentinel.json`. `.gsd` is
// gitignored (root `.gitignore`'s bare `.gsd` entry matches at any depth),
// Sentinel path: `<cwd>/.msd/dispatch-isolation-sentinel.json`. `.msd` is
// gitignored (root `.gitignore`'s bare `.msd` entry matches at any depth),
// and lives inside the checkout that ran the workflow — concurrent worktrees
// of the same repo are separate directories on disk, so each gets its own
// sentinel with no cross-worktree collision, no lock file needed.
@@ -42,7 +42,7 @@
// the primary checkout from an UNTRUSTED executor's own dispatch, not
// from a trusted orchestrator process choosing to reach across).
// - It DOES add an evasion path: silencing the guard via the sentinel
// leaves no trace in `git status` (`.gsd/` is gitignored), where a direct
// leaves no trace in `git status` (`.msd/` is gitignored), where a direct
// edit to a tracked file would.
// Accepted because the threat model this guard defends against is an
// UNCONSENTED, UNVERIFIED dispatch — not a deliberately adversarial
@@ -58,11 +58,11 @@ const fs = require('fs');
const path = require('path');
const { parseDispatchIdentity } = require('./dispatch-identity.js');
// Isolation modes ADR-1239 declares (mirrors gsd-tools.cjs
// Isolation modes ADR-1239 declares (mirrors msd-tools.cjs
// routeDispatchIsolation / routeRecordDispatchIsolation).
const VALID_ISOLATION = new Set(['harness-worktree', 'orchestrator-worktree', 'none']);
const SENTINEL_RELATIVE_PATH = path.join('.gsd', 'dispatch-isolation-sentinel.json');
const SENTINEL_RELATIVE_PATH = path.join('.msd', 'dispatch-isolation-sentinel.json');
// #3045 SECURITY F2 fix: how long a written sentinel is trusted as "this
// dispatch's decision" before a reader falls back to the conservative
@@ -70,7 +70,7 @@ const SENTINEL_RELATIVE_PATH = path.join('.gsd', 'dispatch-isolation-sentinel.js
//
// Previously 4h, on the theory that a slow multi-wave phase execution could
// span well over an hour. That reasoning no longer holds: the #3045 CORE
// REDESIGN makes `dispatch-isolation` (gsd-tools.cjs routeDispatchIsolation)
// REDESIGN makes `dispatch-isolation` (msd-tools.cjs routeDispatchIsolation)
// the sole write path, called as a side effect of resolving ISOLATION — and
// the workflow now re-resolves (and therefore re-records) immediately before
// EVERY plan's dispatch, at the per-plan worktree gate
@@ -97,9 +97,9 @@ function sentinelPath(cwd) {
/**
* Resolve the project root a sentinel should be read from/written to, using
* the SAME derivation gsd-tools.cjs's dispatcher applies to every `--cwd`
* the SAME derivation msd-tools.cjs's dispatcher applies to every `--cwd`
* before invoking a route handler: `findProjectRoot(resolveMainWorktreeCwd(cwd))`
* (gsd-core/bin/gsd-tools.cjs main(), :3506/:3603 — `record-dispatch-isolation`
* (msd-core/bin/msd-tools.cjs main(), :3506/:3603 — `record-dispatch-isolation`
* and `dispatch-isolation` are not in SKIP_ROOT_RESOLUTION, so every write
* goes through both steps).
*
@@ -109,17 +109,17 @@ function sentinelPath(cwd) {
* (the common shape — `.planning/` lives in the main worktree only), the
* writer resolves up to the MAIN worktree and writes there, while the reader
* checked `.planning/config.json` at the raw (unresolved) linked-worktree
* path, found nothing, and silently treated the dispatch as "not a GSD
* path, found nothing, and silently treated the dispatch as "not a MSD
* project" (inert allow) — the guard was reading a sentinel that was never
* written where it looked. Deriving both sides through this one function
* closes that divergence.
*
* `findProjectRoot`/`resolveWorktreeRoot` are read from the sibling
* `gsd-core/bin/lib/*.cjs` modules staged alongside these hooks at install
* `msd-core/bin/lib/*.cjs` modules staged alongside these hooks at install
* time (same pattern the guard hooks already use for
* capability-registry.cjs/runtime-name-policy.cjs) — two directories up from
* `hooks/lib/` (`hooks/lib/isolation-sentinel.js` -> `hooks/` -> repo/install
* root -> `gsd-core/bin/lib/`), mirroring the one-directory-up requires the
* root -> `msd-core/bin/lib/`), mirroring the one-directory-up requires the
* top-level `hooks/*.js` guard scripts already use successfully.
*
* Never throws; any resolution failure (module missing, git unavailable,
@@ -138,11 +138,11 @@ function resolveSentinelRoot(cwd) {
// to the existing catch's degrade-to-raw-`cwd` — unchanged behavior, just
// now attempted-healed-first rather than silently degrading on the first
// cold-tree encounter.
const { ensureRuntimeBuild } = require('../../gsd-core/bin/ensure-runtime-build.cjs');
const { ensureRuntimeBuild } = require('../../msd-core/bin/ensure-runtime-build.cjs');
ensureRuntimeBuild();
const { resolveWorktreeRoot } = require('../../gsd-core/bin/lib/worktree-safety.cjs');
const { resolveWorktreeRoot } = require('../../msd-core/bin/lib/worktree-safety.cjs');
const { root } = resolveWorktreeRoot(cwd);
const { findProjectRoot } = require('../../gsd-core/bin/lib/project-root.cjs');
const { findProjectRoot } = require('../../msd-core/bin/lib/project-root.cjs');
return findProjectRoot(root);
} catch {
return cwd;
@@ -228,8 +228,8 @@ function readSentinel(cwd, { clock = Date } = {}) {
* Agent()/Task() dispatch is FOR. Variadic — accepts any number of text
* sources (short description, full prompt body, etc) and delegates to
* `hooks/lib/dispatch-identity.js::parseDispatchIdentity`, the one canonical
* owner of both the `[gsd:dispatch phase="…" plan="…"]` marker format and its
* prose fallback (see `.gsd/phase/fix-4594-dispatch-identity-seam/40-design.md`).
* owner of both the `[msd:dispatch phase="…" plan="…"]` marker format and its
* prose fallback (see `.msd/phase/fix-4594-dispatch-identity-seam/40-design.md`).
*
* MARKER-FIRST CONTRACT: producers embed a structured marker carrying the
* exact shell values the sentinel itself records (`$PHASE_NUMBER`,

View File

@@ -1,8 +1,8 @@
#!/usr/bin/env bash
# gsd-graphify-rebuild.sh — detached rebuild runner for hooks/gsd-graphify-update.sh.
# msd-graphify-rebuild.sh — detached rebuild runner for hooks/msd-graphify-update.sh.
#
# Usage:
# gsd-graphify-rebuild.sh <STATUS_FILE> <LOCK_FILE> <HEAD_SHA> <MS_START> <GRAPHIFY_BIN>
# msd-graphify-rebuild.sh <STATUS_FILE> <LOCK_FILE> <HEAD_SHA> <MS_START> <GRAPHIFY_BIN>
#
# Writes its own PID into LOCK_FILE on start, removes LOCK_FILE on exit (any cause),
# runs `graphify update .` from the project root (cwd inherited from caller), copies
@@ -45,21 +45,21 @@ STATUS_NAME="ok"
TS_END=$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo "")
# Write the final status file. Use Node for safe JSON encoding.
GSD_STATUS_TS="$TS_END" \
GSD_STATUS_NAME="$STATUS_NAME" \
GSD_EXIT_CODE="$EXIT_CODE" \
GSD_DURATION="$DURATION" \
GSD_HEAD_SHA="$HEAD_SHA" \
GSD_STATUS_FILE="$STATUS_FILE" \
MSD_STATUS_TS="$TS_END" \
MSD_STATUS_NAME="$STATUS_NAME" \
MSD_EXIT_CODE="$EXIT_CODE" \
MSD_DURATION="$DURATION" \
MSD_HEAD_SHA="$HEAD_SHA" \
MSD_STATUS_FILE="$STATUS_FILE" \
node -e '
const fs = require("node:fs");
const status = {
ts: process.env.GSD_STATUS_TS,
status: process.env.GSD_STATUS_NAME,
exit_code: parseInt(process.env.GSD_EXIT_CODE, 10),
duration_ms: parseInt(process.env.GSD_DURATION, 10),
head_at_build: process.env.GSD_HEAD_SHA,
ts: process.env.MSD_STATUS_TS,
status: process.env.MSD_STATUS_NAME,
exit_code: parseInt(process.env.MSD_EXIT_CODE, 10),
duration_ms: parseInt(process.env.MSD_DURATION, 10),
head_at_build: process.env.MSD_HEAD_SHA,
graphify_version: null,
};
fs.writeFileSync(process.env.GSD_STATUS_FILE, JSON.stringify(status, null, 2) + "\n");
fs.writeFileSync(process.env.MSD_STATUS_FILE, JSON.stringify(status, null, 2) + "\n");
' 2>/dev/null || true

View File

@@ -1,51 +1,51 @@
'use strict';
/**
* Authoritative list of GSD-managed hook files.
* Authoritative list of MSD-managed hook files.
*
* Extracted from the worker script into a shared CJS module so that:
* 1. gsd-check-update-worker.js can require() it directly (no source-level
* 1. msd-check-update-worker.js can require() it directly (no source-level
* duplication).
* 2. Tests can assert against the exported array instead of regex-parsing
* the worker source (retiring the pending-migration-to-typed-ir token
* on managed-hooks.test.cjs and orphaned-hooks.test.cjs, per #455).
*
* These are the files GSD ships into ~/.claude/hooks/ (or equivalent) and
* These are the files MSD ships into ~/.claude/hooks/ (or equivalent) and
* checks for staleness after an update. Orphaned files from removed features
* (e.g., gsd-intel-*.js) must NOT be listed here — that would cause permanent
* (e.g., msd-intel-*.js) must NOT be listed here — that would cause permanent
* stale warnings for users who haven't cleaned up manually (#1750).
*/
const MANAGED_HOOKS = [
'gsd-agent-isolation-guard.js',
'gsd-check-update-worker.js',
'gsd-check-update.js',
'gsd-config-reload.js',
'gsd-context-monitor.js',
'gsd-cursor-post-tool.js',
'gsd-cursor-pre-tool.js',
'gsd-cursor-session-start.js',
'gsd-cursor-stop.js',
'gsd-cursor-subagent-start.js',
'gsd-cursor-subagent-stop.js',
'gsd-ensure-canonical-path.js',
'gsd-graphify-update.sh',
'msd-agent-isolation-guard.js',
'msd-check-update-worker.js',
'msd-check-update.js',
'msd-config-reload.js',
'msd-context-monitor.js',
'msd-cursor-post-tool.js',
'msd-cursor-pre-tool.js',
'msd-cursor-session-start.js',
'msd-cursor-stop.js',
'msd-cursor-subagent-start.js',
'msd-cursor-subagent-stop.js',
'msd-ensure-canonical-path.js',
'msd-graphify-update.sh',
// #3662: portable node resolver (helper staged in hooks/; managed JS hook
// commands route through it under --portable-hooks).
'gsd-node-runner.sh',
'gsd-phase-boundary.sh',
'gsd-prompt-guard.js',
'gsd-read-guard.js',
'gsd-read-injection-scanner.js',
'gsd-secret-read-guard.js',
'gsd-session-state.sh',
'gsd-statusline.js',
'gsd-update-banner.js',
'gsd-validate-commit.sh',
'gsd-windsurf-pre-command.js',
'gsd-windsurf-pre-write.js',
'gsd-workflow-guard.js',
'gsd-worktree-path-guard.js',
'gsd-write-guard.js',
'msd-node-runner.sh',
'msd-phase-boundary.sh',
'msd-prompt-guard.js',
'msd-read-guard.js',
'msd-read-injection-scanner.js',
'msd-secret-read-guard.js',
'msd-session-state.sh',
'msd-statusline.js',
'msd-update-banner.js',
'msd-validate-commit.sh',
'msd-windsurf-pre-command.js',
'msd-windsurf-pre-write.js',
'msd-workflow-guard.js',
'msd-worktree-path-guard.js',
'msd-write-guard.js',
];
module.exports = { MANAGED_HOOKS };

View File

@@ -1,11 +1,11 @@
#!/usr/bin/env node
// gsd-hook-version: {{GSD_VERSION}}
// GSD Agent Isolation Dispatch Guard — PreToolUse hook (#3045)
// msd-hook-version: {{MSD_VERSION}}
// MSD Agent Isolation Dispatch Guard — PreToolUse hook (#3045)
//
// Problem: `gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md`
// Problem: `msd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md`
// resolves the project's dispatch isolation correctly
// (`gsd_run query dispatch-isolation --raw`), but DELIVERY of that value into
// the model-authored `Agent(subagent_type="gsd-executor", ...)` call is a
// (`msd_run query dispatch-isolation --raw`), but DELIVERY of that value into
// the model-authored `Agent(subagent_type="msd-executor", ...)` call is a
// prose instruction ("substitute $HARNESS_FLAG's value ... on Claude Code it
// is literally isolation=\"worktree\""). Nothing verifies the model actually
// copied it. When it is omitted, the executor runs and commits directly in
@@ -18,16 +18,16 @@
//
// Applicability (must positively determine all three to act — otherwise
// inert):
// 1. this is a GSD project (`.planning/config.json` exists under cwd),
// 1. this is a MSD project (`.planning/config.json` exists under cwd),
// 2. the project's resolved dispatch isolation is `harness-worktree`,
// 3. the dispatch target is an executor (`subagent_type === "gsd-executor"`;
// 3. the dispatch target is an executor (`subagent_type === "msd-executor"`;
// no other executor-shaped subagent type exists in agents/ today).
//
// Fail-closed exception (#3050 lesson: a guard that cannot verify must not
// answer "safe"): if the project IS a GSD project but the hook cannot read
// answer "safe"): if the project IS a MSD project but the hook cannot read
// or resolve its dispatch-isolation configuration, it DENIES rather than
// defaulting to the "safe-looking" none/allow value that
// `gsd-core/bin/gsd-tools.cjs`'s own `routeDispatchIsolation` degrades to on
// `msd-core/bin/msd-tools.cjs`'s own `routeDispatchIsolation` degrades to on
// error. That existing query is fail-OPEN by design (sequential execution
// is always safe for the SCHEDULER); this guard's job is the opposite
// invariant (never dispatch unisolated when isolation was promised), so it
@@ -43,18 +43,18 @@
// authoritative — `none`/`orchestrator-worktree` ALLOW immediately
// (sequential/orchestrator-managed dispatch is legitimate, not a bug); an
// absent/stale sentinel falls back to a conservative registry+config check
// (GSD_RUNTIME env > .planning/config.json `runtime` > the per-install
// `.gsd-runtime` marker, #3566 — no confident signal degrades to inert rather
// (MSD_RUNTIME env > .planning/config.json `runtime` > the per-install
// `.msd-runtime` marker, #3566 — no confident signal degrades to inert rather
// than guessing 'claude', see resolveRegistryIsolation)
// gated additionally by `workflow.use_worktrees` — read directly, in-process,
// no subprocess spawn.
//
// Triggers on: Agent/Task tool calls with subagent_type === "gsd-executor"
// Triggers on: Agent/Task tool calls with subagent_type === "msd-executor"
// (both names accepted — #3045 MAJOR 1: only Agent was previously matched,
// silently inert on any host/version whose subagent tool is named Task).
// Action: BLOCK (exit 2) when isolation should be enforced and is not
// No-op: any tool other than Agent/Task, non-executor targets, GSD projects
// whose resolved isolation is not harness-worktree, non-GSD projects,
// No-op: any tool other than Agent/Task, non-executor targets, MSD projects
// whose resolved isolation is not harness-worktree, non-MSD projects,
// malformed payloads, or a dispatch that already carries the correct
// isolation parameter.
@@ -75,27 +75,27 @@ const { HOOK_ON_CRASH, allow, deny, crash } = require('./lib/hook-exit.js');
// This guard's outer catch (main(), below) has always exited 0 (fail open):
// that outer catch only covers payload PARSING failing before applicability
// could even be determined (malformed stdin JSON, etc.) — the guard's real
// fail-closed logic (a GSD project whose dispatch-isolation configuration
// fail-closed logic (a MSD project whose dispatch-isolation configuration
// cannot be verified) is handled separately, inside evaluateDispatch/
// resolveIsolationState, and already returns a 'block' decision through the
// normal exit-2 path rather than through this catch. So an unparseable
// payload has nothing to enforce; allowing it preserves today's behavior
// exactly.
const ON_CRASH = HOOK_ON_CRASH.ALLOW;
// #3582: gsd-core/bin/lib/*.cjs (runtime-name-policy.cjs, capability-registry.cjs
// #3582: msd-core/bin/lib/*.cjs (runtime-name-policy.cjs, capability-registry.cjs
// below) are tsc build artifacts (ADR-457), gitignored and absent on a raw
// plugin-marketplace / git-clone install that never ran `npm run build:lib`.
// Self-heal before the first such require (resolveRegistryIsolation, below) —
// see ensureRuntimeBuild's own header for the full rationale. This module
// itself (gsd-core/bin/ensure-runtime-build.cjs) depends on nothing under
// itself (msd-core/bin/ensure-runtime-build.cjs) depends on nothing under
// ./lib, so requiring it here is always safe.
const { ensureRuntimeBuild, RuntimeBuildError } = require('../gsd-core/bin/ensure-runtime-build.cjs');
const { ensureRuntimeBuild, RuntimeBuildError } = require('../msd-core/bin/ensure-runtime-build.cjs');
// No other executor-shaped subagent_type exists in agents/ today
// (verified: only agents/gsd-executor.md). A Set, not a bare string compare,
// (verified: only agents/msd-executor.md). A Set, not a bare string compare,
// so a future sibling executor role can be added here without touching the
// matching logic below.
const EXECUTOR_SUBAGENT_TYPES = new Set(['gsd-executor']);
const EXECUTOR_SUBAGENT_TYPES = new Set(['msd-executor']);
/**
* Parse a registry `harnessIsolationFlag` of the shape `key="value"` (the
@@ -113,20 +113,20 @@ function parseHarnessFlag(flag) {
}
// ─── #3897 rung 2: per-install runtime marker, single canonical owner ────────
// bin/install.js writes `<install>/gsd-core/.gsd-runtime` for EVERY runtime
// install (#2297), co-located with VERSION. Unlike `~/.gsd/defaults.json` —
// bin/install.js writes `<install>/msd-core/.msd-runtime` for EVERY runtime
// install (#2297), co-located with VERSION. Unlike `~/.msd/defaults.json` —
// which is host-wide and names whichever runtime's install ran LAST, the exact
// leakage #2840's config.cjs change exists to prevent — the marker describes
// THIS install, which is the property runtime identity needs on a machine
// with 2+ runtimes. Previously this hook held its own private reader/cache
// (one of four #3897 found); it now delegates to the single canonical owner,
// `src/runtime-slash.cts` (compiled to gsd-core/bin/lib/runtime-slash.cjs),
// `src/runtime-slash.cts` (compiled to msd-core/bin/lib/runtime-slash.cjs),
// reached through `ensureRuntimeBuild()` like every other compiled-lib require
// in this file (`scripts/lint-hooks-runtime-build-seam.cjs`).
function readInstallRuntimeMarker() {
try {
ensureRuntimeBuild();
const runtimeSlash = require('../gsd-core/bin/lib/runtime-slash.cjs');
const runtimeSlash = require('../msd-core/bin/lib/runtime-slash.cjs');
return runtimeSlash.readInstallRuntimeMarker();
} catch {
// Unbuilt runtime library, or any other failure reaching the canonical
@@ -140,7 +140,7 @@ function readInstallRuntimeMarker() {
function _setInstallRuntimeMarkerForTests(value) {
try {
ensureRuntimeBuild();
const runtimeSlash = require('../gsd-core/bin/lib/runtime-slash.cjs');
const runtimeSlash = require('../msd-core/bin/lib/runtime-slash.cjs');
runtimeSlash._setInstallRuntimeMarkerForTests(value);
} catch {
// Test-only seam; an unbuilt library here means the test itself will fail
@@ -152,7 +152,7 @@ function _setInstallRuntimeMarkerForTests(value) {
* Resolve this project's declared `runtime` identity WITHOUT defaulting to
* 'claude' when no explicit signal exists (#3045 MAJOR 2).
*
* `gsd-core/templates/config.json` — the actual scaffold used to write every
* `msd-core/templates/config.json` — the actual scaffold used to write every
* new project's config.json — ships with NO `runtime` key, so "no signal"
* is the COMMON case, not a corner case. Previously this resolution silently
* defaulted to 'claude' in that case, which meant every non-Claude runtime
@@ -162,27 +162,27 @@ function _setInstallRuntimeMarkerForTests(value) {
* -equivalent dispatch — a kwarg that runtime's own tool never accepts.
*
* Returns `{ runtimeId, confident }`. `confident` is true only when an
* explicit signal exists (GSD_RUNTIME env override, a `runtime` key literally
* present in config.json, the per-install `.gsd-runtime` marker, or a
* `runtime` persisted to `~/.gsd/defaults.json` by the installer — see
* explicit signal exists (MSD_RUNTIME env override, a `runtime` key literally
* present in config.json, the per-install `.msd-runtime` marker, or a
* `runtime` persisted to `~/.msd/defaults.json` by the installer — see
* below); false means "cannot determine" and callers must NOT silently
* substitute 'claude' — see resolveRegistryIsolation.
*
* #3045 BLOCKER 2 fix: precedence is GSD_RUNTIME env > config.json `runtime`
* key > `~/.gsd/defaults.json` `runtime`. The first two are unchanged; the
* #3045 BLOCKER 2 fix: precedence is MSD_RUNTIME env > config.json `runtime`
* key > `~/.msd/defaults.json` `runtime`. The first two are unchanged; the
* third is NEW — `bin/install.js`'s `writeNonClaudeDefaults` already persists
* the installed runtime to `~/.gsd/defaults.json` for every non-Claude
* the installed runtime to `~/.msd/defaults.json` for every non-Claude
* runtime install (`defaults.runtime = runtime`, #2395), so this is real,
* already-shipping data, not a new write. Before this fix, "no signal" was
* the COMMON case for any project whose config.json was scaffolded from
* `gsd-core/templates/config.json` (which ships with NO `runtime` key) and
* whose session had no `GSD_RUNTIME` override — i.e. nearly every non-Claude
* `msd-core/templates/config.json` (which ships with NO `runtime` key) and
* whose session had no `MSD_RUNTIME` override — i.e. nearly every non-Claude
* install, since Claude installs never reach `writeNonClaudeDefaults` at all
* (`nativeModelAliases` short-circuits it) and therefore correctly still rely
* on config.json/env. Reading the installer's own persisted signal makes
* "confident" the common case instead.
*
* #3566: the per-install `.gsd-runtime` marker now sits BETWEEN config.json
* #3566: the per-install `.msd-runtime` marker now sits BETWEEN config.json
* and defaults.json. defaults.json is host-wide and names whichever runtime
* installed LAST — on a 2-runtime machine that confidently resolves the WRONG
* runtime (a Codex install's `runtime:"codex"` leaking into Claude projects),
@@ -194,7 +194,7 @@ function _setInstallRuntimeMarkerForTests(value) {
* BLOCKER 2 behavior.
*/
function resolveRuntimeIdentity(cwd, configPath, resolveRuntimeNameFromCandidates) {
const envRuntime = resolveRuntimeNameFromCandidates(process.env.GSD_RUNTIME);
const envRuntime = resolveRuntimeNameFromCandidates(process.env.MSD_RUNTIME);
if (envRuntime) return { runtimeId: envRuntime, confident: true };
// A throw here (corrupt JSON, EISDIR, permission error) propagates to the
@@ -219,7 +219,7 @@ function resolveRuntimeIdentity(cwd, configPath, resolveRuntimeNameFromCandidate
// never a resolution failure (this function only ever throws for the
// config.json read above, which the caller's catch already handles).
try {
const defaultsPath = path.join(os.homedir(), '.gsd', 'defaults.json');
const defaultsPath = path.join(os.homedir(), '.msd', 'defaults.json');
const defaultsRaw = fs.readFileSync(defaultsPath, 'utf-8');
const defaultsParsed = JSON.parse(defaultsRaw);
if (defaultsParsed && typeof defaultsParsed === 'object' && 'runtime' in defaultsParsed) {
@@ -227,7 +227,7 @@ function resolveRuntimeIdentity(cwd, configPath, resolveRuntimeNameFromCandidate
if (defaultsRuntime) return { runtimeId: defaultsRuntime, confident: true };
}
} catch {
// Absent or unreadable ~/.gsd/defaults.json — no signal, fall through.
// Absent or unreadable ~/.msd/defaults.json — no signal, fall through.
}
return { runtimeId: null, confident: false };
@@ -249,11 +249,11 @@ function resolveHarnessFlag(runtimeId, runtimes) {
* Conservative fallback resolution used when the #3045 sentinel is absent or
* stale: re-derive isolation from the registry CAPABILITY, gated by
* `workflow.use_worktrees` (config-schema key confirmed present in
* gsd-core/bin/shared/config-schema.manifest.json's validKeys, so it survives
* msd-core/bin/shared/config-schema.manifest.json's validKeys, so it survives
* loadConfig's whitelist; read directly from the raw config.json here — same
* side-effect-free approach cmdConfigGet itself uses, not through loadConfig).
*
* MAJOR 2: when the runtime cannot be confidently determined (no GSD_RUNTIME
* MAJOR 2: when the runtime cannot be confidently determined (no MSD_RUNTIME
* override, no `runtime` key in config.json — the common case, since the
* project scaffold ships without one), this resolves to 'none' (inert)
* rather than guessing 'claude' and demanding Claude's flag on a host that
@@ -261,7 +261,7 @@ function resolveHarnessFlag(runtimeId, runtimes) {
* runtime would repeat the exact false-positive class the #3045 BLOCKER
* itself was — this is the fallback path only (the sentinel-fresh path above
* already carries the workflow's own confirmed decision + flag, so this
* degrades coverage only for dispatches that happen outside a GSD workflow
* degrades coverage only for dispatches that happen outside a MSD workflow
* run, e.g. a manual Agent() call before any sentinel has been written).
*/
function resolveRegistryIsolation(cwd, configPath) {
@@ -274,8 +274,8 @@ function resolveRegistryIsolation(cwd, configPath) {
// RuntimeBuildError there so it surfaces this seam's actionable message
// instead of being misreported as an unreadable config.json (#3050 lesson).
ensureRuntimeBuild();
const { resolveRuntimeNameFromCandidates } = require('../gsd-core/bin/lib/runtime-name-policy.cjs');
const { runtimes } = require('../gsd-core/bin/lib/capability-registry.cjs');
const { resolveRuntimeNameFromCandidates } = require('../msd-core/bin/lib/runtime-name-policy.cjs');
const { runtimes } = require('../msd-core/bin/lib/capability-registry.cjs');
const { runtimeId, confident } = resolveRuntimeIdentity(cwd, configPath, resolveRuntimeNameFromCandidates);
if (!confident) {
@@ -302,7 +302,7 @@ function resolveRegistryIsolation(cwd, configPath) {
// #3972: the opt-out read shares the ONE owner every other
// isolation-deciding surface uses — planning-workspace's
// worktreesOptedOut ladder (scoped own-key wins; root inherited under
// the GSD_WORKSTREAM gate; strict === false). A flat single-file read
// the MSD_WORKSTREAM gate; strict === false). A flat single-file read
// here made a workstream-LOCAL opt-out invisible, so the sentinel-absent
// fallback denied a sequential dispatch the config explicitly allowed.
// Reached through ensureRuntimeBuild() like every other compiled-lib
@@ -312,7 +312,7 @@ function resolveRegistryIsolation(cwd, configPath) {
let ladderAnswered = false;
try {
ensureRuntimeBuild();
const { worktreesOptedOut } = require('../gsd-core/bin/lib/planning-workspace.cjs');
const { worktreesOptedOut } = require('../msd-core/bin/lib/planning-workspace.cjs');
useWorktrees = !worktreesOptedOut(cwd);
ladderAnswered = true;
} catch {
@@ -350,7 +350,7 @@ function resolveRegistryIsolation(cwd, configPath) {
if (isolation === 'harness-worktree') {
try {
ensureRuntimeBuild();
const { classifyGitHead } = require('../gsd-core/bin/lib/worktree-base-ref.cjs');
const { classifyGitHead } = require('../msd-core/bin/lib/worktree-base-ref.cjs');
if (classifyGitHead({ cwd }).status === 'definitive-absence') {
isolation = 'none';
}
@@ -367,14 +367,14 @@ function resolveRegistryIsolation(cwd, configPath) {
/**
* Resolve this project's dispatch isolation mode, distinguishing three
* outcomes:
* - { gsdProject: false } — not a GSD project, inert
* - { gsdProject: true, error: <Error> } — cannot verify, DENY
* - { gsdProject: true, isolation, harnessFlag } — resolved cleanly
* - { msdProject: false } — not a MSD project, inert
* - { msdProject: true, error: <Error> } — cannot verify, DENY
* - { msdProject: true, isolation, harnessFlag } — resolved cleanly
*
* `.planning/config.json` EXISTING (regardless of whether it can be read) is
* the GSD-project signal, mirroring gsd-workflow-guard.js /
* gsd-context-monitor.js. Any failure reading or parsing it, or requiring the
* sibling registry/policy modules, after that point means "GSD project
* the MSD-project signal, mirroring msd-workflow-guard.js /
* msd-context-monitor.js. Any failure reading or parsing it, or requiring the
* sibling registry/policy modules, after that point means "MSD project
* present, isolation mode unknown" — folded into the DENY path rather than
* silently defaulting to a mode that happens to look safe.
*
@@ -409,7 +409,7 @@ function resolveIsolationState(cwd, { clock = Date, dispatchIds = null } = {}) {
projectExists = false;
}
if (!projectExists) {
return { gsdProject: false, isolation: null, harnessFlag: null, error: null, sentinelDiscarded: null };
return { msdProject: false, isolation: null, harnessFlag: null, error: null, sentinelDiscarded: null };
}
const sentinel = readSentinel(cwd, { clock });
@@ -419,10 +419,10 @@ function resolveIsolationState(cwd, { clock = Date, dispatchIds = null } = {}) {
const applies = sentinelAppliesToDispatch(sentinel, dispatchIds);
if (sentinel.present && !sentinel.stale && applies) {
if (sentinel.isolation !== 'harness-worktree') {
return { gsdProject: true, isolation: sentinel.isolation, harnessFlag: null, error: null, sentinelDiscarded: null };
return { msdProject: true, isolation: sentinel.isolation, harnessFlag: null, error: null, sentinelDiscarded: null };
}
if (sentinel.harnessFlag) {
return { gsdProject: true, isolation: 'harness-worktree', harnessFlag: sentinel.harnessFlag, error: null, sentinelDiscarded: null };
return { msdProject: true, isolation: 'harness-worktree', harnessFlag: sentinel.harnessFlag, error: null, sentinelDiscarded: null };
}
// #3045 BLOCKER 2 fix: the sentinel already PROVED this dispatch requires
// isolation (it resolved harness-worktree) but carries no usable flag —
@@ -434,17 +434,17 @@ function resolveIsolationState(cwd, { clock = Date, dispatchIds = null } = {}) {
// confidently determinable" case, silently returned isolation:'none' and
// ALLOWED the dispatch to run unisolated in the primary checkout — the
// exact failure this guard exists to prevent, and on the default-install
// path (no `runtime` key in gsd-core/templates/config.json), not a corner
// path (no `runtime` key in msd-core/templates/config.json), not a corner
// case. With the #3045 CORE REDESIGN, `dispatch-isolation` always resolves
// and records `harnessFlag` together with `isolation` in one atomic write
// whenever isolation is 'harness-worktree' (routeDispatchIsolation
// degrades to 'none' itself when no flag is declared) — so a fresh
// sentinel with isolation:'harness-worktree' and no flag should not occur
// in practice. This branch is defense-in-depth for a sentinel written by
// an older gsd-tools.cjs, a hand-crafted/corrupted-in-a-*valid*-way
// an older msd-tools.cjs, a hand-crafted/corrupted-in-a-*valid*-way
// sentinel, or any other path that reaches this state; it MUST deny.
return {
gsdProject: true,
msdProject: true,
isolation: null,
harnessFlag: null,
error: new Error(
@@ -469,9 +469,9 @@ function resolveIsolationState(cwd, { clock = Date, dispatchIds = null } = {}) {
try {
const { isolation, harnessFlag } = resolveRegistryIsolation(cwd, configPath);
return { gsdProject: true, isolation, harnessFlag, error: null, sentinelDiscarded };
return { msdProject: true, isolation, harnessFlag, error: null, sentinelDiscarded };
} catch (err) {
return { gsdProject: true, isolation: null, harnessFlag: null, error: err, sentinelDiscarded };
return { msdProject: true, isolation: null, harnessFlag: null, error: err, sentinelDiscarded };
}
}
@@ -506,14 +506,14 @@ function evaluateDispatch(data, { clock = Date } = {}) {
// dispatch is treated as inapplicable rather than trusted. PROMPT FIRST:
// `description` is short, model-authored free text that only carries usable
// identity when the model happens to reproduce the dispatch template
// verbatim, while the canonical `[gsd:dispatch phase="…" plan="…"]` marker
// verbatim, while the canonical `[msd:dispatch phase="…" plan="…"]` marker
// (or, failing that, the prose fallback) lives in the prompt body itself —
// `description` is kept only as a fallback for a marker/prose match that
// exists solely in it.
const dispatchIds = extractDispatchIdentifiers(toolInput.prompt, toolInput.description);
const state = resolveIsolationState(cwd, { clock, dispatchIds });
if (!state.gsdProject) return { action: 'allow' };
if (!state.msdProject) return { action: 'allow' };
if (state.error) {
// #3582: a missing/unbuildable compiled runtime library (RuntimeBuildError,
@@ -526,7 +526,7 @@ function evaluateDispatch(data, { clock = Date } = {}) {
const isBuildFailure = state.error instanceof RuntimeBuildError;
const reason = isBuildFailure
? `Agent isolation guard: cannot resolve this project's dispatch-isolation ` +
`configuration because the GSD runtime library failed to self-build. ` +
`configuration because the MSD runtime library failed to self-build. ` +
`${state.error.message} Refusing to dispatch subagent_type="${subagentType}" until ` +
`the runtime library is built — a guard that cannot verify must not answer "safe" ` +
`(#3050).`
@@ -552,7 +552,7 @@ function evaluateDispatch(data, { clock = Date } = {}) {
`but the Agent() dispatch for subagent_type="${subagentType}" is missing ` +
`${parsed.param}="${parsed.value}". Add ${parsed.param}="${parsed.value}" to the Agent() ` +
`call so the executor runs in an isolated worktree instead of the primary checkout ` +
`(gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md).`;
`(msd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md).`;
if (state.sentinelDiscarded) reason += describeSentinelDiscard(state.sentinelDiscarded);
return { action: 'block', reason, reasonCode: REASON_CODE.HARNESS_FLAG_MISSING, sentinelDiscarded: state.sentinelDiscarded };
}

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env node
// gsd-hook-version: {{GSD_VERSION}}
// Background worker spawned by gsd-check-update.js (SessionStart hook).
// Checks for GSD updates and stale hooks, writes result to cache file.
// msd-hook-version: {{MSD_VERSION}}
// Background worker spawned by msd-check-update.js (SessionStart hook).
// Checks for MSD updates and stale hooks, writes result to cache file.
// Receives paths via environment variables set by the parent hook.
//
// Using a separate file (rather than node -e '<inline code>') avoids the
@@ -12,8 +12,8 @@
const fs = require('fs');
const path = require('path');
// #3582: gsd-core/bin/lib/semver-compare.cjs and package-identity.cjs (and,
// transitively, check-latest-version.cjs's own gsd-core/bin/lib/cli-exit.cjs
// #3582: msd-core/bin/lib/semver-compare.cjs and package-identity.cjs (and,
// transitively, check-latest-version.cjs's own msd-core/bin/lib/cli-exit.cjs
// + shell-command-projection.cjs) are tsc build artifacts (ADR-457),
// gitignored and absent on a raw plugin-marketplace / git-clone install that
// never ran `npm run build:lib`. This worker is a DETACHED SessionStart
@@ -24,12 +24,12 @@ const path = require('path');
// with no visible signal and no cache-file write at all.
//
// This try/require/ensureRuntimeBuild/require/catch shape repeats (with
// different destructured names) in hooks/gsd-check-update.js and
// hooks/gsd-update-banner.js. It is deliberately NOT extracted into a shared
// different destructured names) in hooks/msd-check-update.js and
// hooks/msd-update-banner.js. It is deliberately NOT extracted into a shared
// hooks/lib/ helper: scripts/lint-hooks-runtime-build-seam.cjs enforces this
// exact seam textually, PER FILE — it greps each hooks/ file for its OWN
// literal `require('.../ensure-runtime-build.cjs')` + `ensureRuntimeBuild(`
// call co-occurring with its OWN literal `require('.../gsd-core/bin/lib/*.cjs')`.
// call co-occurring with its OWN literal `require('.../msd-core/bin/lib/*.cjs')`.
// A generic helper taking the compiled module's path as a variable would move
// the literal compiled-lib require OUT of this file and into the helper,
// called with a non-literal argument — the scan's regex (see that script's
@@ -47,20 +47,20 @@ let isSemverNewer = () => false;
let checkLatestVersion = () => ({ ok: false });
let PACKAGE_NAME = null;
try {
const { ensureRuntimeBuild } = require('../gsd-core/bin/ensure-runtime-build.cjs');
const { ensureRuntimeBuild } = require('../msd-core/bin/ensure-runtime-build.cjs');
ensureRuntimeBuild();
({ isSemverNewer } = require('../gsd-core/bin/lib/semver-compare.cjs'));
({ isSemverNewer } = require('../msd-core/bin/lib/semver-compare.cjs'));
// Latest-version lookup is delegated to the single deterministic adapter
// (#498). checkLatestVersion() owns the npm-view call, the timeout/semver
// policy, and the package name — sourced from the baked Package Identity seam.
// The previous `require('../package.json').name` (#378) never yielded a name in
// the installed tree — at the time it resolved to the synthetic
// {"type":"commonjs"} marker GSD wrote at the config root, which has no `.name`,
// so the background check never reported updates. Since #2544 GSD writes no
// {"type":"commonjs"} marker MSD wrote at the config root, which has no `.name`,
// so the background check never reported updates. Since #2544 MSD writes no
// marker there at all, so that require would now fail to resolve outright.
// Either way the name must come from the baked seam, never a walk-up.
({ checkLatestVersion } = require('../gsd-core/bin/check-latest-version.cjs'));
({ PACKAGE_NAME } = require('../gsd-core/bin/lib/package-identity.cjs'));
({ checkLatestVersion } = require('../msd-core/bin/check-latest-version.cjs'));
({ PACKAGE_NAME } = require('../msd-core/bin/lib/package-identity.cjs'));
} catch (e) {
// Runtime library missing/broken and could not self-build — degrade to the
// no-signal fallbacks declared above; the worker still writes a result
@@ -81,9 +81,9 @@ try {
// still runs and writes package_name / installed / latest / update_available.
}
const cacheFile = process.env.GSD_CACHE_FILE;
const projectVersionFile = process.env.GSD_PROJECT_VERSION_FILE;
const globalVersionFile = process.env.GSD_GLOBAL_VERSION_FILE;
const cacheFile = process.env.MSD_CACHE_FILE;
const projectVersionFile = process.env.MSD_PROJECT_VERSION_FILE;
const globalVersionFile = process.env.MSD_GLOBAL_VERSION_FILE;
// Check project directory first (local install), then global
let installed = '0.0.0';
@@ -101,15 +101,15 @@ try {
// Check for stale hooks — compare hook version headers against installed VERSION
// Since #3023 the bundle directory name is resolved from __dirname (this
// worker is staged INSIDE the bundle), not assumed to be configDir/hooks —
// the directory name is runtime-descriptor-driven (e.g. `gsd-hooks/` for pi).
// Only check hooks that GSD currently ships — orphaned files from removed features
// (e.g., gsd-intel-*.js) must be ignored to avoid permanent stale warnings (#1750)
// the directory name is runtime-descriptor-driven (e.g. `msd-hooks/` for pi).
// Only check hooks that MSD currently ships — orphaned files from removed features
// (e.g., msd-intel-*.js) must be ignored to avoid permanent stale warnings (#1750)
// MANAGED_HOOKS is imported from ./managed-hooks-registry.cjs above.
const staleHooks = [];
if (configDir) {
// #3023: the bundle's directory name is runtime-descriptor-driven (pi stages
// it as `gsd-hooks/`), so deriving it as `<configDir>/hooks` silently scanned
// it as `msd-hooks/`), so deriving it as `<configDir>/hooks` silently scanned
// nothing there. This worker is staged INSIDE the bundle, so __dirname is the
// bundle directory by construction — name-agnostic and one fewer assumption.
const hooksDir = __dirname;
@@ -120,7 +120,7 @@ if (configDir) {
try {
const content = fs.readFileSync(path.join(hooksDir, hookFile), 'utf8');
// Match both JS (//) and bash (#) comment styles
const versionMatch = content.match(/(?:\/\/|#) gsd-hook-version:\s*(.+)/);
const versionMatch = content.match(/(?:\/\/|#) msd-hook-version:\s*(.+)/);
if (versionMatch) {
const hookVersion = versionMatch[1].trim();
if (isSemverNewer(installed, hookVersion) && !hookVersion.includes('{{')) {

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env node
// gsd-hook-version: {{GSD_VERSION}}
// Check for GSD updates in background, write result to cache
// msd-hook-version: {{MSD_VERSION}}
// Check for MSD updates in background, write result to cache
// Called by SessionStart hook - runs once per session
const fs = require('fs');
@@ -8,21 +8,21 @@ const path = require('path');
const os = require('os');
const { spawn } = require('child_process');
// #3582: gsd-core/bin/lib/package-identity.cjs is a tsc build artifact
// #3582: msd-core/bin/lib/package-identity.cjs is a tsc build artifact
// (ADR-457), gitignored and absent on a raw plugin-marketplace / git-clone
// install that never ran `npm run build:lib`. This SessionStart hook must
// DEGRADE (fall back to a generic cache filename) rather than crash session
// start. gsd-check-update-worker.js — the process this hook spawns — degrades
// start. msd-check-update-worker.js — the process this hook spawns — degrades
// identically and independently, so the shared fallback literal keeps the
// cache path consistent between writer and reader even in the (rare)
// doubly-degraded case. This try/require/ensureRuntimeBuild/require/catch
// shape is deliberately duplicated (not extracted to hooks/lib/) — see
// gsd-check-update-worker.js's identical #3582 comment for why.
let updateCacheFileName = 'gsd-update-check.json';
// msd-check-update-worker.js's identical #3582 comment for why.
let updateCacheFileName = 'msd-update-check.json';
try {
const { ensureRuntimeBuild } = require('../gsd-core/bin/ensure-runtime-build.cjs');
const { ensureRuntimeBuild } = require('../msd-core/bin/ensure-runtime-build.cjs');
ensureRuntimeBuild();
({ updateCacheFileName } = require('../gsd-core/bin/lib/package-identity.cjs'));
({ updateCacheFileName } = require('../msd-core/bin/lib/package-identity.cjs'));
} catch (e) {
// Runtime library missing/broken and could not self-build — degrade to the
// fallback filename above rather than crash the SessionStart hook.
@@ -36,11 +36,11 @@ const cwd = process.cwd();
function detectConfigDir(baseDir) {
// Check env override first (supports multi-account setups)
const envDir = process.env.CLAUDE_CONFIG_DIR;
if (envDir && fs.existsSync(path.join(envDir, 'gsd-core', 'VERSION'))) {
if (envDir && fs.existsSync(path.join(envDir, 'msd-core', 'VERSION'))) {
return envDir;
}
for (const dir of ['.claude', '.gemini', '.config/kilo', '.kilo', '.config/opencode', '.opencode']) {
if (fs.existsSync(path.join(baseDir, dir, 'gsd-core', 'VERSION'))) {
if (fs.existsSync(path.join(baseDir, dir, 'msd-core', 'VERSION'))) {
return path.join(baseDir, dir);
}
}
@@ -52,12 +52,12 @@ const projectConfigDir = detectConfigDir(cwd);
// Use a shared, tool-agnostic cache directory to avoid multi-runtime
// resolution mismatches where check-update writes to one runtime's cache
// but statusline reads from another (#1421).
const cacheDir = path.join(homeDir, '.cache', 'gsd');
const cacheDir = path.join(homeDir, '.cache', 'msd');
const cacheFile = path.join(cacheDir, updateCacheFileName);
// VERSION file locations (check project first, then global)
const projectVersionFile = path.join(projectConfigDir, 'gsd-core', 'VERSION');
const globalVersionFile = path.join(globalConfigDir, 'gsd-core', 'VERSION');
const projectVersionFile = path.join(projectConfigDir, 'msd-core', 'VERSION');
const globalVersionFile = path.join(globalConfigDir, 'msd-core', 'VERSION');
// Ensure cache directory exists
if (!fs.existsSync(cacheDir)) {
@@ -68,16 +68,16 @@ if (!fs.existsSync(cacheDir)) {
// Spawning a file (rather than node -e '<inline code>') keeps the worker logic
// in plain JS with no template-literal regex-escaping concerns, and makes the
// worker independently testable.
const workerPath = path.join(__dirname, 'gsd-check-update-worker.js');
const workerPath = path.join(__dirname, 'msd-check-update-worker.js');
const child = spawn(process.execPath, [workerPath], {
stdio: 'ignore',
windowsHide: true,
detached: true, // Required on Windows for proper process detachment
env: {
...process.env,
GSD_CACHE_FILE: cacheFile,
GSD_PROJECT_VERSION_FILE: projectVersionFile,
GSD_GLOBAL_VERSION_FILE: globalVersionFile,
MSD_CACHE_FILE: cacheFile,
MSD_PROJECT_VERSION_FILE: projectVersionFile,
MSD_GLOBAL_VERSION_FILE: globalVersionFile,
},
});

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env node
// gsd-hook-version: {{GSD_VERSION}}
// gsd-config-reload.js — FileChanged hook: hot-reload GSD config context
// msd-hook-version: {{MSD_VERSION}}
// msd-config-reload.js — FileChanged hook: hot-reload MSD config context
// Fires when .planning/config.json is modified, created, or deleted.
//
// When the user edits .planning/config.json mid-session, this hook reads the
@@ -41,7 +41,7 @@ process.stdin.on('end', () => {
const filePath = data.file_path || '';
const cwd = data.cwd || process.cwd();
// Only handle the GSD planning config — verify both basename and that the
// Only handle the MSD planning config — verify both basename and that the
// resolved path is .planning/config.json relative to cwd. The hook
// matcher ('config.json') fires on any watched config.json; this guard
// ensures an unrelated config.json in node_modules/ or elsewhere does not
@@ -61,7 +61,7 @@ process.stdin.on('end', () => {
hookSpecificOutput: {
hookEventName: 'FileChanged',
additionalContext:
'GSD config (.planning/config.json) was deleted. ' +
'MSD config (.planning/config.json) was deleted. ' +
'Falling back to built-in defaults for this session.',
},
});
@@ -79,14 +79,14 @@ process.stdin.on('end', () => {
hookSpecificOutput: {
hookEventName: 'FileChanged',
additionalContext:
'GSD config (.planning/config.json) was modified but could not be parsed. ' +
'MSD config (.planning/config.json) was modified but could not be parsed. ' +
'Check the file for JSON syntax errors.',
},
});
}
// Build a concise summary of key config fields the agent cares about
const lines = ['GSD config reloaded (.planning/config.json updated):'];
const lines = ['MSD config reloaded (.planning/config.json updated):'];
if (config.runtime) lines.push(` runtime: ${config.runtime}`);
if (config.mode) lines.push(` mode: ${config.mode}`);

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env node
// gsd-hook-version: {{GSD_VERSION}}
// msd-hook-version: {{MSD_VERSION}}
// Context Monitor - PostToolUse/AfterTool hook (Gemini uses AfterTool)
// Reads context metrics from the statusline bridge file and injects
// warnings when context usage is high. This makes the AGENT aware of
@@ -333,7 +333,7 @@ const handleStdinEnd = () => {
// only one of the three read on EVERY invocation, and it was the only one still reached by a
// bare readFileSync — so the symlink-to-FIFO stall the other two are hardened against was
// still reachable here, on the highest-traffic path in the file. The 4096-byte bound is
// ample: the statusline writes four fixed fields (`gsd-statusline.js`, ~140 bytes with a
// ample: the statusline writes four fixed fields (`msd-statusline.js`, ~140 bytes with a
// UUID session id), so no legitimate bridge approaches it. A refusal throws and lands in the
// rethrow below exactly as an unreadable or malformed bridge already did.
let metricsRaw;
@@ -449,27 +449,27 @@ const handleStdinEnd = () => {
warnData.lastLevel = currentLevel;
writeSentinel(warnPath, JSON.stringify(warnData));
// Detect if GSD is active (has .planning/STATE.md in working directory)
const isGsdActive = fs.existsSync(path.join(cwd, '.planning', 'STATE.md'));
// Detect if MSD is active (has .planning/STATE.md in working directory)
const isMsdActive = fs.existsSync(path.join(cwd, '.planning', 'STATE.md'));
// On CRITICAL with active GSD project, auto-record session state as a
// breadcrumb for /gsd:resume-work (#1974). Fire-and-forget subprocess —
// On CRITICAL with active MSD project, auto-record session state as a
// breadcrumb for /msd:resume-work (#1974). Fire-and-forget subprocess —
// doesn't block the hook or the agent. Fires ONCE per CRITICAL session,
// guarded by warnData.criticalRecorded to prevent repeated overwrites
// of the "crash moment" record on every debounce cycle.
if (isCritical && isGsdActive && !warnData.criticalRecorded) {
if (isCritical && isMsdActive && !warnData.criticalRecorded) {
try {
// Runtime-agnostic path: this hook lives at <runtime-config>/hooks/
// and gsd-tools.cjs lives at <runtime-config>/gsd-core/bin/.
// and msd-tools.cjs lives at <runtime-config>/msd-core/bin/.
// Using __dirname makes this work on Claude Code, OpenCode, Gemini,
// Kilo, etc. without hardcoding ~/.claude/.
const gsdTools = path.join(__dirname, '..', 'gsd-core', 'bin', 'gsd-tools.cjs');
const msdTools = path.join(__dirname, '..', 'msd-core', 'bin', 'msd-tools.cjs');
// Coerce usedPct to a safe number in case bridge file is malformed
const safeUsedPct = Number(usedPct) || 0;
const stoppedAt = `context exhaustion at ${safeUsedPct}% (${new Date().toISOString().split('T')[0]})`;
spawn(
process.execPath,
[gsdTools, 'state', 'record-session', '--stopped-at', stoppedAt],
[msdTools, 'state', 'record-session', '--stopped-at', stoppedAt],
{ cwd, detached: true, stdio: 'ignore', windowsHide: true }
).unref();
warnData.criticalRecorded = true;
@@ -482,16 +482,16 @@ const handleStdinEnd = () => {
// override user preferences — see #884)
let message;
if (isCritical) {
message = isGsdActive
message = isMsdActive
? `CONTEXT CRITICAL: Usage at ${usedPct}%. Remaining: ${remaining}%. ` +
'Context is nearly exhausted. Do NOT start new complex work or write handoff files — ' +
'GSD state is already tracked in STATE.md. Inform the user so they can run ' +
'/gsd:pause-work at the next natural stopping point.'
'MSD state is already tracked in STATE.md. Inform the user so they can run ' +
'/msd:pause-work at the next natural stopping point.'
: `CONTEXT CRITICAL: Usage at ${usedPct}%. Remaining: ${remaining}%. ` +
'Context is nearly exhausted. Inform the user that context is low and ask how they ' +
'want to proceed. Do NOT autonomously save state or write handoff files unless the user asks.';
} else {
message = isGsdActive
message = isMsdActive
? `CONTEXT WARNING: Usage at ${usedPct}%. Remaining: ${remaining}%. ` +
'Context is getting limited. Avoid starting new complex work. If not between ' +
'defined plan steps, inform the user so they can prepare to pause.'

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env node
// gsd-hook-version: {{GSD_VERSION}}
// gsd-cursor-post-tool.js — Cursor postToolUse hook (issue #777)
// msd-hook-version: {{MSD_VERSION}}
// msd-cursor-post-tool.js — Cursor postToolUse hook (issue #777)
//
// Cursor invokes this script after each tool call completes.
// Protocol: JSON from Cursor on stdin; JSON response on stdout.
@@ -67,7 +67,7 @@ process.stdin.on('end', () => {
if (paths.some((p) => PLANNING_PATH_RE.test(p))) {
process.stdout.write(JSON.stringify({
additional_context:
'GSD: .planning/ artifact updated — ensure STATE.md reflects the latest phase and progress.',
'MSD: .planning/ artifact updated — ensure STATE.md reflects the latest phase and progress.',
}));
return;
}

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env node
// gsd-hook-version: {{GSD_VERSION}}
// gsd-cursor-pre-tool.js — Cursor preToolUse hook (ADR-1239 / #2089)
// msd-hook-version: {{MSD_VERSION}}
// msd-cursor-pre-tool.js — Cursor preToolUse hook (ADR-1239 / #2089)
//
// Cursor invokes this script before each tool call executes.
// Protocol: JSON from Cursor on stdin; JSON response on stdout.
@@ -65,7 +65,7 @@ process.stdin.on('end', () => {
if (paths.some((p) => PLANNING_PATH_RE.test(p))) {
process.stdout.write(JSON.stringify({
additional_context:
'GSD: .planning/ write detected — ensure STATE.md reflects the latest phase and progress after this change.',
'MSD: .planning/ write detected — ensure STATE.md reflects the latest phase and progress after this change.',
}));
return;
}

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env node
// gsd-hook-version: {{GSD_VERSION}}
// gsd-cursor-session-start.js — Cursor sessionStart hook (issue #777)
// msd-hook-version: {{MSD_VERSION}}
// msd-cursor-session-start.js — Cursor sessionStart hook (issue #777)
//
// Cursor invokes this script at the start of each agent session.
// Protocol: JSON from Cursor on stdin; JSON response on stdout.
@@ -15,7 +15,7 @@
//
// Behaviour:
// - If .planning/STATE.md is present, injects a brief state reminder.
// - If absent, nudges the user toward /gsd:new-project.
// - If absent, nudges the user toward /msd:new-project.
// - Fails open: any error silently exits 0 so a hook bug never wedges Cursor.
//
// Cursor docs: https://cursor.com/docs/hooks
@@ -26,9 +26,9 @@ const fs = require('fs');
const { allow } = require('./lib/hook-exit.js');
const MSG_PRESENT =
'GSD: .planning/STATE.md is present — review the current phase and any blockers before acting.';
'MSD: .planning/STATE.md is present — review the current phase and any blockers before acting.';
const MSG_ABSENT =
'GSD: no .planning/ workflow found — run /gsd:new-project to start a tracked workflow.';
'MSD: no .planning/ workflow found — run /msd:new-project to start a tracked workflow.';
// Workspace resolution is shared across the Cursor hooks (#2587) — see
// hooks/lib/cursor-workspace.js. Staged next to these scripts by
@@ -51,7 +51,7 @@ process.stdin.on('end', () => {
const msg = statePresent ? MSG_PRESENT : MSG_ABSENT;
process.stdout.write(JSON.stringify({ additional_context: msg }));
} catch {
// Fail open — never block a Cursor session because of a GSD hook error.
// Fail open — never block a Cursor session because of a MSD hook error.
process.stdout.write(JSON.stringify({}));
}
});

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env node
// gsd-hook-version: {{GSD_VERSION}}
// gsd-cursor-stop.js — Cursor stop hook (ADR-1239 / #2089)
// msd-hook-version: {{MSD_VERSION}}
// msd-cursor-stop.js — Cursor stop hook (ADR-1239 / #2089)
//
// Cursor invokes this script when the agent stops responding.
// Protocol: JSON from Cursor on stdin; JSON response on stdout.
@@ -42,7 +42,7 @@ process.stdin.on('end', () => {
if (fs.existsSync(statePath)) {
process.stdout.write(JSON.stringify({
additional_context:
'GSD: Agent stopping — run /gsd:verify-work or /gsd:progress to confirm the phase goal is met before ending the session.',
'MSD: Agent stopping — run /msd:verify-work or /msd:progress to confirm the phase goal is met before ending the session.',
}));
} else {
process.stdout.write(JSON.stringify({}));

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env node
// gsd-hook-version: {{GSD_VERSION}}
// gsd-cursor-subagent-start.js — Cursor subagentStart hook (ADR-1239 / #2089,
// msd-hook-version: {{MSD_VERSION}}
// msd-cursor-subagent-start.js — Cursor subagentStart hook (ADR-1239 / #2089,
// isolation guard #3045)
//
// Cursor invokes this script when a subagent session starts.
@@ -31,9 +31,9 @@
// "deny" with `user_message`.
//
// Behaviour:
// - Injects a brief GSD state reminder so subagents (planner, executor,
// - Injects a brief MSD state reminder so subagents (planner, executor,
// verifier) have the current phase context (unchanged since #2587).
// - NEW (#3045): denies spawning a GSD executor subagent when this
// - NEW (#3045): denies spawning a MSD executor subagent when this
// project's dispatch isolation resolves to "harness-worktree" but the
// session is NOT actually running isolated from the user's primary
// checkout. Cursor's `--worktree` is a SESSION-level flag (no per-call
@@ -44,8 +44,8 @@
// not need: never throws, never blocks a call it cannot evaluate.
// Isolation resolution itself fails CLOSED (denies) for the two cases
// that are load-bearing and are NOT the same as "cannot parse": (a) a
// GSD project resolved to harness-worktree whose isolation state cannot
// be verified, and (b) a harness-worktree GSD project dispatch with no
// MSD project resolved to harness-worktree whose isolation state cannot
// be verified, and (b) a harness-worktree MSD project dispatch with no
// usable subagent_type — a guard that cannot verify must not answer
// "safe" (#3050).
//
@@ -64,30 +64,30 @@ const { allow } = require('./lib/hook-exit.js');
const { resolveStatePath } = require('./lib/cursor-workspace.js');
const { readSentinel, VALID_ISOLATION, extractDispatchIdentifiers, sentinelAppliesToDispatch, buildSentinelDiscard } = require('./lib/isolation-sentinel.js');
const { REASON_CODE, describeSentinelDiscard } = require('./lib/isolation-deny-reason.js');
// #3582: gsd-core/bin/lib/*.cjs (runtime-homes.cjs, worktree-safety.cjs,
// #3582: msd-core/bin/lib/*.cjs (runtime-homes.cjs, worktree-safety.cjs,
// runtime-name-policy.cjs, capability-registry.cjs — required below, inside
// resolveIsolationEvidence and resolveFallbackIsolation) are tsc build
// artifacts (ADR-457), gitignored and absent on a raw plugin-marketplace /
// git-clone install that never ran `npm run build:lib`. Self-heal once, in
// evaluateRootIsolation, before any of those four requires run — see the
// call site below. This module itself depends on nothing under ./lib.
const { ensureRuntimeBuild, RuntimeBuildError } = require('../gsd-core/bin/ensure-runtime-build.cjs');
const { ensureRuntimeBuild, RuntimeBuildError } = require('../msd-core/bin/ensure-runtime-build.cjs');
const MSG_PRESENT =
'GSD: Subagent session started — review .planning/STATE.md for the current phase and any blockers before acting.';
'MSD: Subagent session started — review .planning/STATE.md for the current phase and any blockers before acting.';
const MSG_ABSENT =
'GSD: Subagent session started — no .planning/ workflow found.';
'MSD: Subagent session started — no .planning/ workflow found.';
// GSD's Cursor agent artifacts install with `destSubpath: "agents"`,
// `prefix: "gsd-"`, flat nesting, via the `convertClaudeAgentToCursorAgent`
// MSD's Cursor agent artifacts install with `destSubpath: "agents"`,
// `prefix: "msd-"`, flat nesting, via the `convertClaudeAgentToCursorAgent`
// converter, and `hostIntegration.dispatch.namedDispatch === true`
// (gsd-core/bin/lib/capability-registry.cjs, runtimes.cursor) — i.e. Cursor
// (msd-core/bin/lib/capability-registry.cjs, runtimes.cursor) — i.e. Cursor
// dispatches named subagents by their real agent name, identically to
// Claude. So GSD's executor surfaces as subagent_type === "gsd-executor" on
// Cursor too, the same identifier hooks/gsd-agent-isolation-guard.js checks
// Claude. So MSD's executor surfaces as subagent_type === "msd-executor" on
// Cursor too, the same identifier hooks/msd-agent-isolation-guard.js checks
// for on Claude. A Set, not a bare string compare, so a future sibling
// executor role can be added here without touching the matching logic below.
const EXECUTOR_SUBAGENT_TYPES = new Set(['gsd-executor']);
const EXECUTOR_SUBAGENT_TYPES = new Set(['msd-executor']);
/**
* Runs `realpathFn`, never throwing. A path that cannot be resolved (does not
@@ -165,8 +165,8 @@ function resolveIsolationEvidence(root, { realpath = fs.realpathSync } = {}) {
let managedRoot = null;
try {
// Sibling data/policy module, staged alongside this hook at install time
// (same pattern as hooks/gsd-statusline.js's requires of gsd-core/bin/lib/*).
const { getGlobalConfigDir } = require('../gsd-core/bin/lib/runtime-homes.cjs');
// (same pattern as hooks/msd-statusline.js's requires of msd-core/bin/lib/*).
const { getGlobalConfigDir } = require('../msd-core/bin/lib/runtime-homes.cjs');
managedRoot = path.join(getGlobalConfigDir('cursor'), 'worktrees');
} catch {
managedRoot = null;
@@ -187,7 +187,7 @@ function resolveIsolationEvidence(root, { realpath = fs.realpathSync } = {}) {
let linkageReason = null;
try {
// Sibling data/policy module, staged alongside this hook at install time.
const { resolveWorktreeLinkage } = require('../gsd-core/bin/lib/worktree-safety.cjs');
const { resolveWorktreeLinkage } = require('../msd-core/bin/lib/worktree-safety.cjs');
linkageReason = resolveWorktreeLinkage(root).reason;
} catch {
linkageReason = null;
@@ -195,11 +195,11 @@ function resolveIsolationEvidence(root, { realpath = fs.realpathSync } = {}) {
if (realRoot === null) {
// `root` itself could not be resolved on disk. In the live hook this is
// defense in depth rather than a reachable path today: the GSD-project
// defense in depth rather than a reachable path today: the MSD-project
// existence gate in resolveIsolationDecision already requires `root` to
// resolve (it must contain a readable `.planning/config.json`) before
// evidence is ever consulted, so a workspace root that plainly does not
// exist allows earlier as "not a GSD project" — never here. Kept anyway
// exist allows earlier as "not a MSD project" — never here. Kept anyway
// per finding 2's explicit directive: an unresolvable path must never
// silently read as "isolated".
return { isolated: false, cannotDetermine: true, notApplicable: false };
@@ -227,8 +227,8 @@ function resolveIsolationEvidence(root, { realpath = fs.realpathSync } = {}) {
* paths Cursor is operating on for this hook invocation, not just the first.
*
* #3045 security review (finding 1): a multi-root Cursor workspace whose
* FIRST root is a non-GSD directory (or an isolated worktree) and whose
* SECOND root is the GSD project in the primary checkout must still be
* FIRST root is a non-MSD directory (or an isolated worktree) and whose
* SECOND root is the MSD project in the primary checkout must still be
* caught — every root is a directory the dispatched subagent can reach and
* write to, regardless of position. `hooks/lib/cursor-workspace.js` already
* established the "scan every root" precedent for its own (different)
@@ -248,7 +248,7 @@ function resolveIsolationEvidence(root, { realpath = fs.realpathSync } = {}) {
* hook is Cursor's own config dir (~/.cursor per the comment above), NOT the
* workspace. A relative root would therefore resolve against the wrong
* directory and — because a wrong/nonexistent `.planning/config.json` path
* reads as "not a GSD project" — silently ALLOW a dispatch this guard should
* reads as "not a MSD project" — silently ALLOW a dispatch this guard should
* have evaluated (fail OPEN). Filtering it out here instead makes it "not a
* resolvable workspace root", which degrades the SAME way (allow, step 2 of
* resolveIsolationDecision's applicability list) but for the honest reason.
@@ -267,26 +267,26 @@ function getWorkspaceRoots(data) {
* 1. `subagent_type` is not confidently a NON-executor (a present,
* non-empty string that isn't in EXECUTOR_SUBAGENT_TYPES short-circuits
* to allow immediately, before any project/isolation resolution runs —
* mirrors hooks/gsd-agent-isolation-guard.js checking subagent_type
* mirrors hooks/msd-agent-isolation-guard.js checking subagent_type
* first, and matters here specifically: an unreadable config must never
* deny a dispatch this guard was never going to enforce against),
* 2. a workspace root is resolvable from `workspace_roots`,
* 3. that root is a GSD project (`.planning/config.json` exists there),
* 3. that root is a MSD project (`.planning/config.json` exists there),
* 4. the resolved dispatch isolation is `harness-worktree`,
* 5. `subagent_type` identifies a GSD executor (or is missing/malformed —
* 5. `subagent_type` identifies a MSD executor (or is missing/malformed —
* see the cannot-determine case below),
* 6. the session is NOT actually isolated (resolveIsolationEvidence).
*
* No workspace root at all degrades to allow (step 2), mirroring
* hooks/gsd-agent-isolation-guard.js's own "not a GSD project → allow"
* hooks/msd-agent-isolation-guard.js's own "not a MSD project → allow"
* branch: project-existence is the gate that makes fail-closed apply in the
* first place, so being unable to even locate a candidate project is not
* itself a fail-closed trigger — it is the same "not a GSD project" shape
* itself a fail-closed trigger — it is the same "not a MSD project" shape
* that guard already treats as inert.
*
* Two DISTINCT fail-closed ("cannot determine") reasons per #3050's lesson
* that a guard which cannot verify must not answer "safe" — both scoped to
* "GSD project resolved to harness-worktree", never to a dispatch already
* "MSD project resolved to harness-worktree", never to a dispatch already
* confirmed to be a non-executor:
* - this project's dispatch-isolation configuration cannot be read/resolved
* (registry require/parse failure, or config.json unreadable),
@@ -299,9 +299,9 @@ function getWorkspaceRoots(data) {
* the registry. `none`/`orchestrator-worktree` from a fresh sentinel ALLOW
* immediately — sequential/orchestrator-managed dispatch is legitimate. An
* absent/stale sentinel falls back to `resolveFallbackIsolation` (registry +
* `workflow.use_worktrees`, runtime resolved GSD_RUNTIME env >
* `workflow.use_worktrees`, runtime resolved MSD_RUNTIME env >
* .planning/config.json `runtime` key > 'cursor'). The default is
* confidently "cursor" here — UNLIKE hooks/gsd-agent-isolation-guard.js's own
* confidently "cursor" here — UNLIKE hooks/msd-agent-isolation-guard.js's own
* fallback, which must treat "no explicit signal" as cannot-determine
* because that hook installs across every `hostIntegration.hooksSurface ===
* 'settings-json'` runtime — because THIS script only ever runs as Cursor's
@@ -344,18 +344,18 @@ function resolveIsolationDecision(data, { clock = Date, realpath = fs.realpathSy
}
// ─── #3897 rung 2: per-install runtime marker, single canonical owner ────────
// bin/install.js writes `<install>/gsd-core/.gsd-runtime` beside VERSION for
// bin/install.js writes `<install>/msd-core/.msd-runtime` beside VERSION for
// every runtime install (#2297); this hook ships at `<install>/hooks/`, so the
// marker is the `gsd-core` sibling of this file's own directory. Previously
// marker is the `msd-core` sibling of this file's own directory. Previously
// this hook held its own private reader/cache (one of four #3897 found); it
// now delegates to the single canonical owner, `src/runtime-slash.cts`
// (compiled to gsd-core/bin/lib/runtime-slash.cjs), reached through
// (compiled to msd-core/bin/lib/runtime-slash.cjs), reached through
// `ensureRuntimeBuild()` like the other compiled-lib requires in this file
// (`scripts/lint-hooks-runtime-build-seam.cjs`).
function readInstallRuntimeMarker() {
try {
ensureRuntimeBuild();
const runtimeSlash = require('../gsd-core/bin/lib/runtime-slash.cjs');
const runtimeSlash = require('../msd-core/bin/lib/runtime-slash.cjs');
return runtimeSlash.readInstallRuntimeMarker();
} catch {
// Unbuilt runtime library, or any other failure reaching the canonical
@@ -370,7 +370,7 @@ function readInstallRuntimeMarker() {
function _setInstallRuntimeMarkerForTests(value) {
try {
ensureRuntimeBuild();
const runtimeSlash = require('../gsd-core/bin/lib/runtime-slash.cjs');
const runtimeSlash = require('../msd-core/bin/lib/runtime-slash.cjs');
runtimeSlash._setInstallRuntimeMarkerForTests(value);
} catch {
// Test-only seam; an unbuilt library here means the test itself will fail
@@ -382,37 +382,37 @@ function _setInstallRuntimeMarkerForTests(value) {
* Conservative fallback resolution used when the #3045 sentinel is absent or
* stale for `root`: re-derive isolation from the registry CAPABILITY, gated
* by `workflow.use_worktrees` (config-schema key confirmed present in
* gsd-core/bin/shared/config-schema.manifest.json's validKeys, so it survives
* msd-core/bin/shared/config-schema.manifest.json's validKeys, so it survives
* loadConfig's whitelist; read directly from the raw config.json here, same
* side-effect-free approach cmdConfigGet itself uses).
*
* #3045 MAJOR fix ("Cursor residual false-deny"): previously defaulted
* confidently to 'cursor' whenever no `GSD_RUNTIME`/config.json `runtime`
* confidently to 'cursor' whenever no `MSD_RUNTIME`/config.json `runtime`
* signal existed, purely because this script only ever executes as Cursor's
* OWN `subagentStart` hook — true of the PROCESS, but not evidence the
* PROJECT itself declared an isolation requirement this guard can verify.
* Combined with a stale/absent sentinel (outside `execute-phase`, after
* `.gsd` cleanup, a phase running past the sentinel's staleness window, or a
* `.msd` cleanup, a phase running past the sentinel's staleness window, or a
* base-check-degraded run whose sentinel went stale before a fresh one was
* recorded), that default made every such `gsd-executor` dispatch resolve to
* recorded), that default made every such `msd-executor` dispatch resolve to
* "harness-worktree" and then hard-DENY unless the session happened to be
* running under Cursor's own managed worktree root — a false-deny of
* otherwise legitimate dispatches, unlike `hooks/gsd-agent-isolation-guard.js`,
* otherwise legitimate dispatches, unlike `hooks/msd-agent-isolation-guard.js`,
* which degrades an undeterminable runtime to inert (#3045 MAJOR 2). Aligned
* here: an explicit signal is now required — `GSD_RUNTIME` > config.json
* `runtime` key > the per-install `.gsd-runtime` marker (#3566) >
* `~/.gsd/defaults.json` `runtime` (mirrors the Claude hook's
* here: an explicit signal is now required — `MSD_RUNTIME` > config.json
* `runtime` key > the per-install `.msd-runtime` marker (#3566) >
* `~/.msd/defaults.json` `runtime` (mirrors the Claude hook's
* `resolveRuntimeIdentity`; `bin/install.js`'s `writeNonClaudeDefaults`
* persists the installed runtime there for every non-Claude install,
* including Cursor, so a REAL Cursor+GSD install still resolves confidently
* including Cursor, so a REAL Cursor+MSD install still resolves confidently
* — this only stops GUESSING 'cursor' for a project that never declared any
* runtime signal at all).
*/
function resolveFallbackIsolation(root, configPath) {
const { resolveRuntimeNameFromCandidates } = require('../gsd-core/bin/lib/runtime-name-policy.cjs');
const { runtimes } = require('../gsd-core/bin/lib/capability-registry.cjs');
const { resolveRuntimeNameFromCandidates } = require('../msd-core/bin/lib/runtime-name-policy.cjs');
const { runtimes } = require('../msd-core/bin/lib/capability-registry.cjs');
let runtimeId = resolveRuntimeNameFromCandidates(process.env.GSD_RUNTIME);
let runtimeId = resolveRuntimeNameFromCandidates(process.env.MSD_RUNTIME);
const rawConfig = fs.readFileSync(configPath, 'utf-8');
const parsedConfig = JSON.parse(rawConfig);
if (!runtimeId && parsedConfig && typeof parsedConfig === 'object' && 'runtime' in parsedConfig) {
@@ -420,20 +420,20 @@ function resolveFallbackIsolation(root, configPath) {
}
if (!runtimeId) {
// #3566: the per-install marker, above the host-wide defaults — same fix as
// hooks/gsd-agent-isolation-guard.js's resolveRuntimeIdentity. defaults.json
// hooks/msd-agent-isolation-guard.js's resolveRuntimeIdentity. defaults.json
// is host-wide and names whichever runtime installed LAST (#2840's poison);
// the marker describes THIS install (written for every runtime since #2297).
runtimeId = resolveRuntimeNameFromCandidates(readInstallRuntimeMarker()) || null;
}
if (!runtimeId) {
try {
const defaultsPath = path.join(os.homedir(), '.gsd', 'defaults.json');
const defaultsPath = path.join(os.homedir(), '.msd', 'defaults.json');
const defaultsParsed = JSON.parse(fs.readFileSync(defaultsPath, 'utf-8'));
if (defaultsParsed && typeof defaultsParsed === 'object' && 'runtime' in defaultsParsed) {
runtimeId = resolveRuntimeNameFromCandidates(defaultsParsed.runtime) || null;
}
} catch {
// Absent/unreadable ~/.gsd/defaults.json — no signal, fall through.
// Absent/unreadable ~/.msd/defaults.json — no signal, fall through.
}
}
if (!runtimeId) {
@@ -463,14 +463,14 @@ function resolveFallbackIsolation(root, configPath) {
*/
function evaluateRootIsolation(root, subagentType, { clock = Date, dispatchIds = null, realpath = fs.realpathSync } = {}) {
const configPath = path.join(root, '.planning', 'config.json');
let isGsdProject;
let isMsdProject;
try {
fs.accessSync(configPath, fs.constants.F_OK);
isGsdProject = true;
isMsdProject = true;
} catch {
isGsdProject = false;
isMsdProject = false;
}
if (!isGsdProject) return { action: 'allow' };
if (!isMsdProject) return { action: 'allow' };
// #3582: self-heal the compiled runtime library BEFORE any of its four
// downstream requires (resolveFallbackIsolation's two, resolveIsolationEvidence's
@@ -485,8 +485,8 @@ function evaluateRootIsolation(root, subagentType, { clock = Date, dispatchIds =
return {
action: 'deny',
reason:
`GSD subagent isolation guard: cannot resolve this project's dispatch-isolation ` +
`configuration because the GSD runtime library failed to self-build. ` +
`MSD subagent isolation guard: cannot resolve this project's dispatch-isolation ` +
`configuration because the MSD runtime library failed to self-build. ` +
`${err instanceof RuntimeBuildError ? err.message : String(err && err.message || err)} ` +
`Refusing to allow this subagent to spawn until the runtime library is built — a guard ` +
`that cannot verify must not answer "safe" (#3050).`,
@@ -516,7 +516,7 @@ function evaluateRootIsolation(root, subagentType, { clock = Date, dispatchIds =
return {
action: 'deny',
reason:
`GSD subagent isolation guard: could not read or resolve this project's ` +
`MSD subagent isolation guard: could not read or resolve this project's ` +
`dispatch-isolation configuration ('.planning/config.json' exists under "${root}"). ` +
`Refusing to allow this subagent to spawn without being able to verify whether ` +
`isolation is required — a guard that cannot verify must not answer "safe" (#3050). ` +
@@ -536,10 +536,10 @@ function evaluateRootIsolation(root, subagentType, { clock = Date, dispatchIds =
return {
action: 'deny',
reason:
`GSD subagent isolation guard: this project's dispatch isolation resolves to ` +
`MSD subagent isolation guard: this project's dispatch isolation resolves to ` +
`"harness-worktree", but the subagentStart payload for this dispatch carries no usable ` +
`subagent_type. Refusing to allow it to spawn without being able to confirm whether it ` +
`is a GSD executor — a guard that cannot verify must not answer "safe" (#3050).` +
`is a MSD executor — a guard that cannot verify must not answer "safe" (#3050).` +
(sentinelDiscarded ? describeSentinelDiscard(sentinelDiscarded) : ''),
reasonCode: REASON_CODE.NO_SUBAGENT_TYPE,
sentinelDiscarded,
@@ -554,7 +554,7 @@ function evaluateRootIsolation(root, subagentType, { clock = Date, dispatchIds =
return {
action: 'deny',
reason:
`GSD subagent isolation guard: this project's dispatch isolation resolves to ` +
`MSD subagent isolation guard: this project's dispatch isolation resolves to ` +
`"harness-worktree", but whether "${root}" is running in an isolated Cursor worktree ` +
`could not be determined (git did not respond). Refusing to allow subagent_type=` +
`"${subagentType}" to spawn without being able to verify isolation — a guard that ` +
@@ -568,7 +568,7 @@ function evaluateRootIsolation(root, subagentType, { clock = Date, dispatchIds =
return {
action: 'deny',
reason:
`GSD subagent isolation guard: this project's dispatch isolation resolves to ` +
`MSD subagent isolation guard: this project's dispatch isolation resolves to ` +
`"harness-worktree", but subagent_type="${subagentType}" is about to spawn in "${root}", ` +
`which is not an isolated Cursor worktree — it would edit the user's primary checkout ` +
`directly, with no consent and no warning. Start an isolated session first (the ` +

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env node
// gsd-hook-version: {{GSD_VERSION}}
// gsd-cursor-subagent-stop.js — Cursor subagentStop hook (ADR-1239 / #2089)
// msd-hook-version: {{MSD_VERSION}}
// msd-cursor-subagent-stop.js — Cursor subagentStop hook (ADR-1239 / #2089)
//
// Cursor invokes this script when a subagent session completes.
// Protocol: JSON from Cursor on stdin; JSON response on stdout.
@@ -35,7 +35,7 @@ process.stdin.on('end', () => {
try {
process.stdout.write(JSON.stringify({
additional_context:
'GSD: Subagent completed — review its output and update .planning/STATE.md if the phase progressed.',
'MSD: Subagent completed — review its output and update .planning/STATE.md if the phase progressed.',
}));
} catch {
process.stdout.write(JSON.stringify({}));

View File

@@ -1,22 +1,22 @@
#!/usr/bin/env node
// gsd-hook-version: {{GSD_VERSION}}
// msd-hook-version: {{MSD_VERSION}}
//
// gsd-ensure-canonical-path — SessionStart hook (#997)
// msd-ensure-canonical-path — SessionStart hook (#997)
//
// PROBLEM: GSD agents/commands/templates use markdown `@`-file-includes that
// hardcode the canonical path `@~/.claude/gsd-core/...` (references, workflows,
// PROBLEM: MSD agents/commands/templates use markdown `@`-file-includes that
// hardcode the canonical path `@~/.claude/msd-core/...` (references, workflows,
// templates, contexts, bin). Markdown @-includes expand `~` but do NOT expand
// environment variables, so `${CLAUDE_PLUGIN_ROOT}` cannot be used in them.
// In a classic `bin/install.js` install the canonical path is a real directory
// holding the bundled tree, so the includes resolve. In a Claude Code
// *marketplace plugin* install the plugin manager only unpacks the package
// into the version-pinned plugin cache and never runs `bin/install.js`, so
// `~/.claude/gsd-core/` is never created and every @-include resolves to
// `~/.claude/msd-core/` is never created and every @-include resolves to
// nothing — every agent that depends on one fails (e.g. the executor).
//
// FIX: On SessionStart, when running under a plugin install (CLAUDE_PLUGIN_ROOT
// set and a bundled `gsd-core/` tree found beneath it), ensure
// `~/.claude/gsd-core/` exists and its immutable subdirs (bin, contexts,
// set and a bundled `msd-core/` tree found beneath it), ensure
// `~/.claude/msd-core/` exists and its immutable subdirs (bin, contexts,
// references, templates, workflows) are symlinked to the plugin's bundled tree.
// This changes ZERO @-references, is a no-op in classic installs (where each
// subdir is already a real directory), preserves user-generated files
@@ -37,7 +37,7 @@ const os = require('os');
const { allow } = require('./lib/hook-exit.js');
// Immutable, bundled subdirectories that the canonical path must expose. These
// are the directories `@~/.claude/gsd-core/<subdir>/...` includes point into.
// are the directories `@~/.claude/msd-core/<subdir>/...` includes point into.
// User-generated artifacts (USER-PROFILE.md, STATE.md, VERSION, config, …) are
// NOT in this list and are never created, moved, or deleted by this hook.
const MANAGED_SUBDIRS = ['bin', 'contexts', 'references', 'templates', 'workflows'];
@@ -46,8 +46,8 @@ const MANAGED_SUBDIRS = ['bin', 'contexts', 'references', 'templates', 'workflow
* Resolve the canonical runtime config dir for the active runtime.
*
* Honours CLAUDE_CONFIG_DIR for custom/multi-account setups (mirrors
* gsd-check-update.js detectConfigDir), else falls back to ~/.claude. The
* canonical GSD tree always lives at `<configDir>/gsd-core`.
* msd-check-update.js detectConfigDir), else falls back to ~/.claude. The
* canonical MSD tree always lives at `<configDir>/msd-core`.
*/
function resolveConfigDir(homeDir, env) {
const envDir = env.CLAUDE_CONFIG_DIR;
@@ -58,10 +58,10 @@ function resolveConfigDir(homeDir, env) {
}
/**
* Locate the bundled `gsd-core/` tree beneath a plugin root.
* Locate the bundled `msd-core/` tree beneath a plugin root.
*
* Claude Code unpacks the package so the bundled tree sits at
* `<pluginRoot>/gsd-core/`. Returns the absolute, realpath-normalised path to
* `<pluginRoot>/msd-core/`. Returns the absolute, realpath-normalised path to
* that directory, or null if it is absent / not a directory. Resolving with
* realpath collapses symlinks/.. so the subsequent containment check is sound.
*/
@@ -75,12 +75,12 @@ function resolveBundledTree(pluginRoot) {
} catch (_) {
return null; // plugin root does not exist
}
const bundled = path.join(root, 'gsd-core');
const bundled = path.join(root, 'msd-core');
let bundledReal;
try {
// The bundled tree must be a real directory (or a symlink to one) that
// resolves to a path inside the plugin root. realpathSync throws ENOENT/
// ENOTDIR if <pluginRoot>/gsd-core is absent, so no separate existence
// ENOTDIR if <pluginRoot>/msd-core is absent, so no separate existence
// check is needed. Reject anything that does not resolve to a directory.
bundledReal = fs.realpathSync(bundled);
if (!fs.statSync(bundledReal).isDirectory()) return null;
@@ -88,7 +88,7 @@ function resolveBundledTree(pluginRoot) {
return null;
}
// SECURITY: the resolved bundled tree must stay inside the resolved plugin
// root. A crafted symlink at <pluginRoot>/gsd-core pointing outside the root
// root. A crafted symlink at <pluginRoot>/msd-core pointing outside the root
// is rejected — we never link the canonical path at content we do not own.
const rootWithSep = root.endsWith(path.sep) ? root : root + path.sep;
if (bundledReal !== root && !bundledReal.startsWith(rootWithSep)) {
@@ -132,7 +132,7 @@ function linkPointsAt(linkPath, expectedTarget) {
}
/**
* Ensure the canonical `~/.claude/gsd-core/` path exposes the bundled subdirs.
* Ensure the canonical `~/.claude/msd-core/` path exposes the bundled subdirs.
*
* Pure, dependency-injected core so tests drive it with a fake home, fake
* plugin root, and explicit platform. Returns a structured result describing
@@ -166,7 +166,7 @@ function ensureCanonicalPath(opts = {}) {
}
const configDir = resolveConfigDir(homeDir, env);
const canonicalDir = path.join(configDir, 'gsd-core');
const canonicalDir = path.join(configDir, 'msd-core');
// Inspect the canonical path itself exactly once.
// - If it is a SYMLINK, the user (or another tool) deliberately pointed the
@@ -246,9 +246,9 @@ function ensureCanonicalPath(opts = {}) {
if (existing) {
// lstat().isSymbolicLink() is true for BOTH POSIX symlinks and Windows
// junctions, so this single predicate identifies every GSD-managed link.
// junctions, so this single predicate identifies every MSD-managed link.
if (existing.isSymbolicLink()) {
// A GSD-managed link that is stale or points elsewhere (e.g. previous
// A MSD-managed link that is stale or points elsewhere (e.g. previous
// plugin version after `claude plugin update`). Prune and recreate.
try {
fs.unlinkSync(linkPath);

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# gsd-hook-version: {{GSD_VERSION}}
# gsd-graphify-update.sh — PostToolUse hook (Bash matcher) that auto-rebuilds
# msd-hook-version: {{MSD_VERSION}}
# msd-graphify-update.sh — PostToolUse hook (Bash matcher) that auto-rebuilds
# the project knowledge graph after main HEAD advances on the default branch.
#
# OPT-IN (issue #3347 AC): no-op unless .planning/config.json has BOTH
@@ -11,11 +11,11 @@
# Gates (in fast-fail order — each shaves work off the common non-dispatch path):
# 0. .planning/config.json exists AND $CI unset/empty (#3729 — both are
# parse-free shell tests; running them before the Gate 1 node spawn keeps
# non-GSD repositories and CI off the spawn path entirely, which on
# non-MSD repositories and CI off the spawn path entirely, which on
# Windows otherwise flashes a console window per Bash call)
# 1. Stdin payload present and tool_name == "Bash"
# 2. tool_input.command matches a HEAD-advancing git op (shell-direct or
# the exact `gsd-tools query commit` command shape; the SDK command invokes
# the exact `msd-tools query commit` command shape; the SDK command invokes
# git internally, so the literal "git commit" substring never appears —
# see #3653)
# 3. Inside a git repo
@@ -26,15 +26,15 @@
#
# When all gates pass:
# - Writes .planning/graphs/.last-build-status.json with status="running"
# - Detaches hooks/lib/gsd-graphify-rebuild.sh which copies graphify-out/* to
# - Detaches hooks/lib/msd-graphify-rebuild.sh which copies graphify-out/* to
# .planning/graphs/ and rewrites the status file with status="ok"|"failed"
#
# Returns 0 in all cases. Never blocks the user-facing tool call.
set -uo pipefail
# Gate 0 — GSD project at all, and not CI (#3729). Both are pure shell tests;
# they must precede the Gate 1 node spawn so the common non-GSD/CI path pays
# Gate 0 — MSD project at all, and not CI (#3729). Both are pure shell tests;
# they must precede the Gate 1 node spawn so the common non-MSD/CI path pays
# for zero child processes. Hoisting is behavior-preserving: old Gates 3 and 6
# made the same decisions, just later.
[ -f .planning/config.json ] || exit 0
@@ -74,10 +74,10 @@ if [ "$TOOL_NAME" = "Shell" ]; then TOOL_NAME="Bash"; fi
[ "$TOOL_NAME" = "Bash" ] || exit 0
# Gate 2 — HEAD-advancing git op (shell-direct or exact `gsd-tools query commit`)
# Gate 2 — HEAD-advancing git op (shell-direct or exact `msd-tools query commit`)
case "$COMMAND" in
*"git commit"*|*"git merge"*|*"git pull"*|*"git rebase --continue"*|*"git cherry-pick"*) ;;
*"gsd-tools query commit"|*"gsd-tools query commit "*) ;;
*"msd-tools query commit"|*"msd-tools query commit "*) ;;
*) exit 0 ;;
esac
@@ -136,25 +136,25 @@ STATUS_FILE=".planning/graphs/.last-build-status.json"
TS_START=$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo "")
MS_START=$(node -e 'process.stdout.write(String(Date.now()))' 2>/dev/null || echo "0")
GSD_TS="$TS_START" \
GSD_HEAD="$HEAD_SHA" \
GSD_STATUS_FILE="$STATUS_FILE" \
MSD_TS="$TS_START" \
MSD_HEAD="$HEAD_SHA" \
MSD_STATUS_FILE="$STATUS_FILE" \
node -e '
const fs = require("node:fs");
const status = {
ts: process.env.GSD_TS,
ts: process.env.MSD_TS,
status: "running",
exit_code: null,
duration_ms: null,
head_at_build: process.env.GSD_HEAD,
head_at_build: process.env.MSD_HEAD,
graphify_version: null,
};
fs.writeFileSync(process.env.GSD_STATUS_FILE, JSON.stringify(status, null, 2) + "\n");
fs.writeFileSync(process.env.MSD_STATUS_FILE, JSON.stringify(status, null, 2) + "\n");
' 2>/dev/null || true
# Resolve rebuild helper script (sibling-relative for portability across install layouts)
HOOK_DIR="$(cd "$(dirname "$0")" && pwd)"
REBUILD_SCRIPT="$HOOK_DIR/lib/gsd-graphify-rebuild.sh"
REBUILD_SCRIPT="$HOOK_DIR/lib/msd-graphify-rebuild.sh"
[ -f "$REBUILD_SCRIPT" ] || exit 0
# Detach the rebuild. Spawn as a regular background job so we can capture

View File

@@ -1,10 +1,10 @@
#!/bin/sh
# gsd-hook-version: {{GSD_VERSION}}
# gsd-node-runner.sh — GSD portable node resolver (#3662).
# msd-hook-version: {{MSD_VERSION}}
# msd-node-runner.sh — MSD portable node resolver (#3662).
#
# Managed JS hook commands under --portable-hooks route through this script:
#
# bash "<hooks>/gsd-node-runner.sh" "<baked-node-path>" "<script.js>" [args...]
# bash "<hooks>/msd-node-runner.sh" "<baked-node-path>" "<script.js>" [args...]
#
# so a config root shared across environments (mounted ~/.claude, shared
# containers) resolves node at hook-fire time instead of depending on the
@@ -28,7 +28,7 @@
# buildNodeRunnerChainToken (src/runtime-hooks-surface.cts, #3662) — keep the
# two lists consistent.
#
# Diagnostic escape: GSD_NODE_RUNNER_NO_FALLBACKS=1 disables candidates 2-3
# Diagnostic escape: MSD_NODE_RUNNER_NO_FALLBACKS=1 disables candidates 2-3
# (first-argument-only resolution) — used by the test suite and useful to
# pin down which node a given environment picks.
set -u
@@ -57,7 +57,7 @@ check() {
}
check "$preferred" || {
if [ "${GSD_NODE_RUNNER_NO_FALLBACKS:-0}" != "1" ]; then
if [ "${MSD_NODE_RUNNER_NO_FALLBACKS:-0}" != "1" ]; then
path_node=$(command -v node 2>/dev/null || true)
check "$path_node" ||
check "${HOME:-}/.local/share/mise/shims/node" ||
@@ -70,7 +70,7 @@ check "$preferred" || {
}
if [ -z "$found" ]; then
echo "gsd-node-runner: no usable node found (preferred: ${preferred:-<none>})" >&2
echo "msd-node-runner: no usable node found (preferred: ${preferred:-<none>})" >&2
exit 127
fi

View File

@@ -1,8 +1,8 @@
#!/usr/bin/env bash
# gsd-hook-version: {{GSD_VERSION}}
# gsd-phase-boundary.sh — PostToolUse hook: detect .planning/ file writes
# msd-hook-version: {{MSD_VERSION}}
# msd-phase-boundary.sh — PostToolUse hook: detect .planning/ file writes
# Outputs a reminder when planning files are modified outside normal workflow.
# Uses Node.js for JSON parsing (always available in GSD projects, no jq dependency).
# Uses Node.js for JSON parsing (always available in MSD projects, no jq dependency).
#
# OPT-IN: This hook is a no-op unless config.json has hooks.community: true.
# Enable with: "hooks": { "community": true } in .planning/config.json

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env node
// gsd-hook-version: {{GSD_VERSION}}
// GSD Prompt Injection Guard — PreToolUse hook
// msd-hook-version: {{MSD_VERSION}}
// MSD Prompt Injection Guard — PreToolUse hook
// Scans file content being written to .planning/ for prompt injection patterns.
// Defense-in-depth: catches injected instructions before they enter agent context.
//
@@ -20,11 +20,11 @@ const { HOOK_ON_CRASH, allow, crash } = require('./lib/hook-exit.js');
// to add on top of an already-permitted operation (#3911).
const ON_CRASH = HOOK_ON_CRASH.ALLOW;
// Prompt injection patterns — shared with gsd-read-injection-scanner.js via
// Prompt injection patterns — shared with msd-read-injection-scanner.js via
// hooks/lib/injection-patterns.js so the two surfaces cannot drift (#3504).
// Deliberately a subset of security.cjs's set: hooks stay loadable without the
// compiled lib tree. Staging of the lib helper is allowlisted in
// GSD_HOOK_LIB_FILES (bin/install.js).
// MSD_HOOK_LIB_FILES (bin/install.js).
const { INJECTION_PATTERNS, describePattern } = require('./lib/injection-patterns.js');
// #2304: Kimi's native hook bus delivers Kimi's tool vocabulary in the payload
@@ -76,7 +76,7 @@ function normalizeKimiPayload(data) {
// guard reading an empty string and exiting 0, while the identical write
// without the extra key blocked — a bypass needing no crash at all. The same
// shadowing also preserved a NON-STRING `file_path` (`[]`), which threw
// inside gsd-worktree-path-guard's path.isAbsolute() and reached its outer
// inside msd-worktree-path-guard's path.isAbsolute() and reached its outer
// `catch { process.exit(0) }`: the same crash-to-allow this fix closes
// elsewhere, reached through the guard's own read rather than through
// normalization. Overwriting can only ever narrow what a guard inspects to
@@ -100,7 +100,7 @@ function normalizeKimiPayload(data) {
// trigger. Degrading only the non-coercible entry to '' keeps
// stringification intact for every value that CAN coerce (numbers,
// arrays, plain objects), so nothing downstream — including
// gsd-prompt-guard's scan of new_string — loses content it saw before.
// msd-prompt-guard's scan of new_string — loses content it saw before.
const editText = (v) => { try { return String(v ?? ''); } catch { return ''; } };
// #2595 (review Major 2): reconstruct UNCONDITIONALLY, mirroring the
// `path` decision above rather than merely filling in when the field
@@ -109,7 +109,7 @@ function normalizeKimiPayload(data) {
// no `old_string`/`new_string` at all, so either field appearing in a
// Kimi payload is ALWAYS model-supplied, exactly like `file_path`. Under
// the old `=== undefined` condition a model-supplied `new_string: ""`
// SHADOWED the reconstruction, leaving gsd-prompt-guard's injection scan
// SHADOWED the reconstruction, leaving msd-prompt-guard's injection scan
// reading '' and exiting at its `if (!content)` before it ever saw the
// real `edit[].new` — a one-key bypass of the very scan this fix's
// guarded coercion exists to keep fed. A `typeof` test would NOT close
@@ -186,7 +186,7 @@ process.stdin.on('end', () => {
if (pattern.test(content)) {
// Bounded label, never the raw regex source (#4016 / PR #4061 review):
// the superset pattern's source is ~280 characters and would dominate
// the advisory. Same transform as gsd-read-injection-scanner.js.
// the advisory. Same transform as msd-read-injection-scanner.js.
findings.push({ ruleId: RULE_IDS.INJECTION_PATTERN, match: describePattern(pattern) });
}
}

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env node
// gsd-hook-version: {{GSD_VERSION}}
// GSD Read Guard — PreToolUse hook
// msd-hook-version: {{MSD_VERSION}}
// MSD Read Guard — PreToolUse hook
// Injects advisory guidance when Write/Edit targets an existing file,
// reminding the model to Read the file first.
//
@@ -77,7 +77,7 @@ function normalizeKimiPayload(data) {
// guard reading an empty string and exiting 0, while the identical write
// without the extra key blocked — a bypass needing no crash at all. The same
// shadowing also preserved a NON-STRING `file_path` (`[]`), which threw
// inside gsd-worktree-path-guard's path.isAbsolute() and reached its outer
// inside msd-worktree-path-guard's path.isAbsolute() and reached its outer
// `catch { process.exit(0) }`: the same crash-to-allow this fix closes
// elsewhere, reached through the guard's own read rather than through
// normalization. Overwriting can only ever narrow what a guard inspects to
@@ -101,7 +101,7 @@ function normalizeKimiPayload(data) {
// trigger. Degrading only the non-coercible entry to '' keeps
// stringification intact for every value that CAN coerce (numbers,
// arrays, plain objects), so nothing downstream — including
// gsd-prompt-guard's scan of new_string — loses content it saw before.
// msd-prompt-guard's scan of new_string — loses content it saw before.
const editText = (v) => { try { return String(v ?? ''); } catch { return ''; } };
// #2595 (review Major 2): reconstruct UNCONDITIONALLY, mirroring the
// `path` decision above rather than merely filling in when the field
@@ -110,7 +110,7 @@ function normalizeKimiPayload(data) {
// no `old_string`/`new_string` at all, so either field appearing in a
// Kimi payload is ALWAYS model-supplied, exactly like `file_path`. Under
// the old `=== undefined` condition a model-supplied `new_string: ""`
// SHADOWED the reconstruction, leaving gsd-prompt-guard's injection scan
// SHADOWED the reconstruction, leaving msd-prompt-guard's injection scan
// reading '' and exiting at its `if (!content)` before it ever saw the
// real `edit[].new` — a one-key bypass of the very scan this fix's
// guarded coercion exists to keep fed. A `typeof` test would NOT close

View File

@@ -1,13 +1,13 @@
#!/usr/bin/env node
// gsd-hook-version: {{GSD_VERSION}}
// GSD Read Injection Scanner — PostToolUse hook (#2201)
// msd-hook-version: {{MSD_VERSION}}
// MSD Read Injection Scanner — PostToolUse hook (#2201)
// Pattern-based pre-filter / blocklist: scans content returned by Read, WebFetch,
// and WebSearch for known prompt-injection patterns (regex + heuristic rules).
// This is a static pattern match — NOT a semantic guard, NOT PromptArmor.
// It does NOT understand context, intent, or novel phrasing; it catches
// known injection signatures at ingestion before they enter conversation context.
//
// Defense-in-depth: long GSD sessions hit context compression, and the
// Defense-in-depth: long MSD sessions hit context compression, and the
// summariser does not distinguish user instructions from content read from
// external files. Poisoned instructions that survive compression become
// indistinguishable from trusted context. This hook warns at ingestion time.
@@ -31,7 +31,7 @@ const { HOOK_ON_CRASH, allow, crash } = require('./lib/hook-exit.js');
// check is safer than failing the tool call it is only observing (#3911).
const ON_CRASH = HOOK_ON_CRASH.ALLOW;
// Summarisation-specific patterns (novel — not in gsd-prompt-guard.js).
// Summarisation-specific patterns (novel — not in msd-prompt-guard.js).
// These target instructions specifically designed to survive context compression.
const SUMMARISATION_PATTERNS = [
/when\s+(?:summari[sz]ing|compressing|compacting),?\s+(?:retain|preserve|keep)\s+(?:this|these)/i,
@@ -79,15 +79,15 @@ const MARKDOWN_LINK_PATTERNS = [
},
];
// Standard injection patterns — shared with gsd-prompt-guard.js via
// Standard injection patterns — shared with msd-prompt-guard.js via
// hooks/lib/injection-patterns.js so the two surfaces cannot drift (#3504).
// Staging of the lib helper is allowlisted in GSD_HOOK_LIB_FILES (bin/install.js).
// Staging of the lib helper is allowlisted in MSD_HOOK_LIB_FILES (bin/install.js).
const { INJECTION_PATTERNS, describePattern } = require('./lib/injection-patterns.js');
const ALL_PATTERNS = [...INJECTION_PATTERNS, ...SUMMARISATION_PATTERNS];
// #3023: the staged bundle's directory name is runtime-descriptor-driven, so a
// literal `/<config>/hooks/` fragment cannot reliably identify GSD's own hook
// literal `/<config>/hooks/` fragment cannot reliably identify MSD's own hook
// scripts. This module lives inside the bundle, so __dirname identifies it by
// construction. Normalized to forward slashes to match `p` below.
const OWN_BUNDLE_PREFIX = __dirname.replace(/\\/g, '/').replace(/\/+$/, '') + '/';
@@ -133,8 +133,8 @@ function isExcludedPath(filePath) {
// not change that. Blocking prompt injection on Kimi needs a PreToolUse
// mechanism, or an upstream kimi-cli change. Do not describe this hook as
// "engaged" or "blocking" on Kimi. This block is
// kept byte-identical with the copies in gsd-prompt-guard.js,
// gsd-read-guard.js, and gsd-worktree-path-guard.js — a parity test binds
// kept byte-identical with the copies in msd-prompt-guard.js,
// msd-read-guard.js, and msd-worktree-path-guard.js — a parity test binds
// them (tests/kimi-guard-normalization-parity.test.cjs). Inlined per guard
// (not hooks/lib/): hook scripts are staged as standalone files, and a
// sibling require is a staging dependency that can fail silently.
@@ -174,7 +174,7 @@ function normalizeKimiPayload(data) {
// guard reading an empty string and exiting 0, while the identical write
// without the extra key blocked — a bypass needing no crash at all. The same
// shadowing also preserved a NON-STRING `file_path` (`[]`), which threw
// inside gsd-worktree-path-guard's path.isAbsolute() and reached its outer
// inside msd-worktree-path-guard's path.isAbsolute() and reached its outer
// `catch { process.exit(0) }`: the same crash-to-allow this fix closes
// elsewhere, reached through the guard's own read rather than through
// normalization. Overwriting can only ever narrow what a guard inspects to
@@ -198,7 +198,7 @@ function normalizeKimiPayload(data) {
// trigger. Degrading only the non-coercible entry to '' keeps
// stringification intact for every value that CAN coerce (numbers,
// arrays, plain objects), so nothing downstream — including
// gsd-prompt-guard's scan of new_string — loses content it saw before.
// msd-prompt-guard's scan of new_string — loses content it saw before.
const editText = (v) => { try { return String(v ?? ''); } catch { return ''; } };
// #2595 (review Major 2): reconstruct UNCONDITIONALLY, mirroring the
// `path` decision above rather than merely filling in when the field
@@ -207,7 +207,7 @@ function normalizeKimiPayload(data) {
// no `old_string`/`new_string` at all, so either field appearing in a
// Kimi payload is ALWAYS model-supplied, exactly like `file_path`. Under
// the old `=== undefined` condition a model-supplied `new_string: ""`
// SHADOWED the reconstruction, leaving gsd-prompt-guard's injection scan
// SHADOWED the reconstruction, leaving msd-prompt-guard's injection scan
// reading '' and exiting at its `if (!content)` before it ever saw the
// real `edit[].new` — a one-key bypass of the very scan this fix's
// guarded coercion exists to keep fed. A `typeof` test would NOT close
@@ -279,7 +279,7 @@ process.stdin.on('end', () => {
for (const pattern of ALL_PATTERNS) {
if (pattern.test(content)) {
// Trim pattern source for readable output (shared with gsd-prompt-guard.js)
// Trim pattern source for readable output (shared with msd-prompt-guard.js)
findings.push({
ruleId: RULE_IDS.INJECTION_PATTERN,
match: describePattern(pattern),

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env node
// gsd-hook-version: {{GSD_VERSION}}
// GSD Secret Read Guard — PreToolUse hook (Read | Grep | Bash)
// msd-hook-version: {{MSD_VERSION}}
// MSD Secret Read Guard — PreToolUse hook (Read | Grep | Bash)
//
// Blocks reads of secret files — `.env`, `.env.<suffix>`, `.secrets` — by any
// of the three tools that can put file contents into the conversation: the
@@ -14,7 +14,7 @@
// settings.json. Claude Code 2.1.259 hardened the Bash-side enforcement of
// Read() deny rules so that ANY `cd DIR && cat/grep relative-path` compound
// prompts for approval whenever any Read() deny rule exists — even in `auto`
// permission mode. GSD subagents emit hundreds of those per session. A
// permission mode. MSD subagents emit hundreds of those per session. A
// PreToolUse denial is not a permission rule, so it never arms that check,
// and it applies in `auto` and `bypassPermissions` modes alike. The three
// installer-written strings are retired by the same installer change (they
@@ -24,7 +24,7 @@
// case-INSENSITIVELY so `.ENV` / `.Secrets` are caught on the macOS/Windows
// filesystems where they ARE the secret file — the write guard's `/i` stance):
// .env, .secrets, and .env.<suffix> — EXCEPT .env.example / .env.sample /
// .env.template / .env.dist, which are the non-secret templates GSD's own
// .env.template / .env.dist, which are the non-secret templates MSD's own
// phase prompt tells executors to read.
// Stated cost (#4580): the exemption matches the token's FINAL EXTENSION,
// not the whole name, so the trusted set is `.env.<anything>.example` /
@@ -52,14 +52,14 @@
// and heredocs (one token per body, carrying its `<<` segment). A heredoc
// body is only ever run as a script when its segment's command is a shell
// interpreter (below); a DATA heredoc — `cat <<EOF … EOF`, the agent-
// populated bodies in GSD's own workflows, `git commit -m "$(cat <<'EOF' …
// populated bodies in MSD's own workflows, `git commit -m "$(cat <<'EOF' …
// EOF)"` — is never operand-checked, so prose mentioning `.env` is safe.
// pass 2 groups tokens by segment and evaluates each on its own:
// input redirect targets (`<`, `N<`) are always checked; the command word is
// located past `sudo`/`env VAR=x`/`nohup`-style prefixes; a closed set of
// NON-READING commands (test/[/ls/stat/rm/touch/echo/…) exempts that
// segment's operands — `[ -f .env ]` and `ls .env*` are existence checks
// GSD's own agents run — while `cp`/`mv`/`ln`/`git` are deliberately NOT
// MSD's own agents run — while `cp`/`mv`/`ln`/`git` are deliberately NOT
// exempt (`cp .env x && cat x` launders the name; `git show HEAD:.env`
// reads). A shell interpreter (bash/sh/zsh/dash/ksh/su) has its script scanned
// whether it arrives via `-c '…'`, a `<( )` file operand, a heredoc /
@@ -122,7 +122,7 @@ const MAX_GLOB_ALTERNATIVES = 64;
const NON_SECRET_ENV_SUFFIXES = new Set(['example', 'sample', 'template', 'dist']);
// Command-prefix wrappers to look through when locating the command word at
// the head of a segment (same set as hooks/gsd-windsurf-pre-command.js).
// the head of a segment (same set as hooks/msd-windsurf-pre-command.js).
const CMD_PREFIXES = new Set(['sudo', 'env', 'command', 'nice', 'nohup', 'time', 'doas']);
// Commands whose ordinary operands are file NAMES, never file CONTENTS. A
@@ -1028,7 +1028,7 @@ function normalizeKimiPayload(data) {
// guard reading an empty string and exiting 0, while the identical write
// without the extra key blocked — a bypass needing no crash at all. The same
// shadowing also preserved a NON-STRING `file_path` (`[]`), which threw
// inside gsd-worktree-path-guard's path.isAbsolute() and reached its outer
// inside msd-worktree-path-guard's path.isAbsolute() and reached its outer
// `catch { process.exit(0) }`: the same crash-to-allow this fix closes
// elsewhere, reached through the guard's own read rather than through
// normalization. Overwriting can only ever narrow what a guard inspects to
@@ -1052,7 +1052,7 @@ function normalizeKimiPayload(data) {
// trigger. Degrading only the non-coercible entry to '' keeps
// stringification intact for every value that CAN coerce (numbers,
// arrays, plain objects), so nothing downstream — including
// gsd-prompt-guard's scan of new_string — loses content it saw before.
// msd-prompt-guard's scan of new_string — loses content it saw before.
const editText = (v) => { try { return String(v ?? ''); } catch { return ''; } };
// #2595 (review Major 2): reconstruct UNCONDITIONALLY, mirroring the
// `path` decision above rather than merely filling in when the field
@@ -1061,7 +1061,7 @@ function normalizeKimiPayload(data) {
// no `old_string`/`new_string` at all, so either field appearing in a
// Kimi payload is ALWAYS model-supplied, exactly like `file_path`. Under
// the old `=== undefined` condition a model-supplied `new_string: ""`
// SHADOWED the reconstruction, leaving gsd-prompt-guard's injection scan
// SHADOWED the reconstruction, leaving msd-prompt-guard's injection scan
// reading '' and exiting at its `if (!content)` before it ever saw the
// real `edit[].new` — a one-key bypass of the very scan this fix's
// guarded coercion exists to keep fed. A `typeof` test would NOT close

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# gsd-hook-version: {{GSD_VERSION}}
# gsd-session-state.sh — SessionStart hook: inject project state reminder
# msd-hook-version: {{MSD_VERSION}}
# msd-session-state.sh — SessionStart hook: inject project state reminder
# Outputs STATE.md head on every session start for orientation.
#
# OPT-IN: This hook is a no-op unless config.json has hooks.community: true.
@@ -42,7 +42,7 @@ node -e '
headerLines.push("STATE.md exists - check for blockers and current phase.");
if (stateHead) headerLines.push(stateHead);
} else {
headerLines.push("No .planning/ found - suggest /gsd-new-project if starting new work.");
headerLines.push("No .planning/ found - suggest /msd-new-project if starting new work.");
}
headerLines.push("");
headerLines.push("Config: \"mode\": \"" + configMode + "\"");

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env node
// gsd-hook-version: {{GSD_VERSION}}
// Claude Code Statusline - GSD Edition
// Shows: model | current task (or GSD state) | directory | context usage
// msd-hook-version: {{MSD_VERSION}}
// Claude Code Statusline - MSD Edition
// Shows: model | current task (or MSD state) | directory | context usage
const fs = require('fs');
const path = require('path');
@@ -17,7 +17,7 @@ const { HOOK_ON_CRASH, allow, crash } = require('./lib/hook-exit.js');
// per-render hook). Declared ONCE so that catch's crash() call states its
// policy explicitly rather than inheriting a default (#3911).
const ON_CRASH = HOOK_ON_CRASH.ALLOW;
// #3582: gsd-core/bin/lib/*.cjs (semver-compare.cjs, state-document.cjs,
// #3582: msd-core/bin/lib/*.cjs (semver-compare.cjs, state-document.cjs,
// active-workstream-store.cjs, planning-workspace.cjs — required below) and
// package-identity.cjs are tsc build artifacts (ADR-457), gitignored and
// absent on a raw plugin-marketplace / git-clone install that never ran
@@ -28,7 +28,7 @@ const ON_CRASH = HOOK_ON_CRASH.ALLOW;
// its pure helpers assumes a built tree, same as every other hook test.
if (require.main === module) {
try {
const { ensureRuntimeBuild } = require('../gsd-core/bin/ensure-runtime-build.cjs');
const { ensureRuntimeBuild } = require('../msd-core/bin/ensure-runtime-build.cjs');
ensureRuntimeBuild();
} catch (e) {
// #3911: crash(ON_CRASH, ...) with an undefined payload preserves the
@@ -39,13 +39,13 @@ if (require.main === module) {
crash(ON_CRASH, undefined);
}
}
const { isSemverNewer } = require('../gsd-core/bin/lib/semver-compare.cjs');
const { PACKAGE_NAME, updateCacheFileName } = require('../gsd-core/bin/lib/package-identity.cjs');
const { normalizeStateStatus } = require('../gsd-core/bin/lib/state-document.cjs');
const { isSemverNewer } = require('../msd-core/bin/lib/semver-compare.cjs');
const { PACKAGE_NAME, updateCacheFileName } = require('../msd-core/bin/lib/package-identity.cjs');
const { normalizeStateStatus } = require('../msd-core/bin/lib/state-document.cjs');
const {
renderBracketPhaseDisplay,
renderBracketMilestoneDisplay,
} = require('../gsd-core/bin/lib/phase-id-display.cjs');
} = require('../msd-core/bin/lib/phase-id-display.cjs');
// #2850: reuse the existing workstream resolution seams rather than
// re-implementing CLI>env>store precedence or path construction inline.
// peekActiveWorkstream is the read-only sibling of the store-tier lookup
@@ -55,8 +55,8 @@ const {
// resolveActiveWorkstream's own `getStored` override keeps the CLI>env>store
// precedence itself fully reused (untouched); only the store tier's *write*
// side effect is removed.
const { resolveActiveWorkstream, peekActiveWorkstream } = require('../gsd-core/bin/lib/active-workstream-store.cjs');
const { listAvailableWorkstreams, planningPaths } = require('../gsd-core/bin/lib/planning-workspace.cjs');
const { resolveActiveWorkstream, peekActiveWorkstream } = require('../msd-core/bin/lib/active-workstream-store.cjs');
const { listAvailableWorkstreams, planningPaths } = require('../msd-core/bin/lib/planning-workspace.cjs');
// --- Config + last-command readers ------------------------------------------
@@ -64,7 +64,7 @@ const { listAvailableWorkstreams, planningPaths } = require('../gsd-core/bin/lib
* Walk up from dir looking for .planning/config.json and return its parsed contents.
* Returns {} if not found or unreadable.
*/
function readGsdConfig(dir) {
function readMsdConfig(dir) {
const home = os.homedir();
let current = dir;
for (let i = 0; i < 10; i++) {
@@ -138,19 +138,19 @@ function readLastSlashCommand(transcriptPath) {
let name = content.slice(openIdx + openTag.length, idx).trim();
// Strip a leading slash if present, and any trailing arguments-on-same-line noise.
if (name.startsWith('/')) name = name.slice(1);
// Command names in Claude Code transcripts are plain identifiers like "gsd-plan-phase"
// Command names in Claude Code transcripts are plain identifiers like "msd-plan-phase"
// or namespaced like "plugin:skill". Reject anything with whitespace/newlines/control chars.
if (!name || /[\s\\"<>]/.test(name) || name.length > 80) return null;
return name;
}
// --- GSD state reader -------------------------------------------------------
// --- MSD state reader -------------------------------------------------------
/**
* Read and parse a STATE.md if it exists. Returns the parsed state object,
* `null` when the file is absent, or `null` on any read/parse failure (never
* throws) — the single shared shape for both the flat and workstream reads
* in readGsdState() below.
* in readMsdState() below.
*/
function readStateFileOrNull(statePath) {
if (!fs.existsSync(statePath)) return null;
@@ -166,7 +166,7 @@ function readStateFileOrNull(statePath) {
* has no flat STATE.md but IS in workstream mode (.planning/workstreams/
* present — the single-source-of-truth check `listAvailableWorkstreams`
* shares with the init.progress/phase.complete #1912/#2028 guards, so this
* can't drift from how every other GSD command detects the mode), resolve
* can't drift from how every other MSD command detects the mode), resolve
* the active workstream and read that workstream's STATE.md instead (#2850).
*
* Resolution reuses `resolveActiveWorkstream` (active-workstream-store.cjs)
@@ -181,8 +181,8 @@ function readStateFileOrNull(statePath) {
* - the parsed state object when a flat or workstream STATE.md is found
* - { noActiveWorkstream: true } when workstream mode is active at an
* ancestor but no workstream can be resolved — an observable signal so
* this is distinguishable from "GSD isn't installed here" (#2850)
* - null when no .planning marker is found at all (GSD not present), or
* this is distinguishable from "MSD isn't installed here" (#2850)
* - null when no .planning marker is found at all (MSD not present), or
* when a workstream DOES resolve but its STATE.md doesn't exist yet
* (negative space: mirrors flat-mode's own silent pre-STATE.md window)
*
@@ -194,7 +194,7 @@ function readStateFileOrNull(statePath) {
* repo-pinning check needs. Never derived when false or the stamp is
* absent, so existing callers (default opts) spend zero extra spawns.
*/
function readGsdState(dir, opts = {}) {
function readMsdState(dir, opts = {}) {
const { stateFreshness = false } = opts;
const home = os.homedir();
let current = dir;
@@ -244,7 +244,7 @@ function readGsdState(dir, opts = {}) {
* - nextPhases : array of phase numbers (["4.5"]) for nextAction, null otherwise
* - completedPhases / totalPhases / percent : milestone progress dimension
*
* All new fields default to undefined when absent — formatGsdState() degrades
* All new fields default to undefined when absent — formatMsdState() degrades
* gracefully so existing STATE.md files (without these fields) keep working.
*/
function parseStateMd(content) {
@@ -328,7 +328,7 @@ function parseStateMd(content) {
return state;
}
// #2850: shared literal for formatGsdState/formatGsdStateCompact's "nothing
// #2850: shared literal for formatMsdState/formatMsdStateCompact's "nothing
// resolvable" signal — one source of truth so the two renderers can't drift.
const NO_ACTIVE_WORKSTREAM_LABEL = 'no active workstream';
@@ -347,7 +347,7 @@ function renderProgressBar(percent) {
}
/**
* Format GSD state into display string.
* Format MSD state into display string.
*
* Backward-compatible default (no new fields populated):
* "v1.9 Code Quality · executing · fix-graphiti-deployment (1/5)"
@@ -363,9 +363,9 @@ function renderProgressBar(percent) {
* Progress bar is opt-in: appended to the milestone segment only when
* progress.percent is present in frontmatter; absent → empty string.
*/
function formatGsdState(s, opts = {}) {
function formatMsdState(s, opts = {}) {
// #2850: workstream mode with nothing resolvable — an observable signal,
// never silent emptiness (distinguishes from "GSD isn't installed here").
// never silent emptiness (distinguishes from "MSD isn't installed here").
if (s.noActiveWorkstream) return NO_ACTIVE_WORKSTREAM_LABEL;
const parts = [];
@@ -471,7 +471,7 @@ function contextTokenSuffix(currentUsage) {
// --- Compact state format (opt-in) ---------------------------------------------
/**
* Collapse GSD's status value to a single keyword, built on the canonical
* Collapse MSD's status value to a single keyword, built on the canonical
* normalizer (#2162 approval condition): normalizeStateStatus() in
* state-document.cjs owns the status vocabulary (discussing / planning /
* executing / verifying / completed / paused) so the two can't drift.
@@ -487,7 +487,7 @@ function contextTokenSuffix(currentUsage) {
*/
const CANONICAL_STATUSES = ['discussing', 'planning', 'executing', 'verifying', 'completed', 'paused'];
function shortGsdStatus(status) {
function shortMsdStatus(status) {
if (!status) return null;
const norm = normalizeStateStatus(status, null);
if (CANONICAL_STATUSES.includes(norm)) {
@@ -500,7 +500,7 @@ function shortGsdStatus(status) {
}
/**
* Compact alternative to formatGsdState, selected via
* Compact alternative to formatMsdState, selected via
* `statusline.state_format: "compact"`:
*
* "v1.12 · P7/12 · executing" (phase active)
@@ -509,11 +509,11 @@ function shortGsdStatus(status) {
* "v2.0 · next execute-phase 4.5" (idle with a queued action)
*
* Drops the milestone name and progress bar — the biggest width costs in the
* default format — and collapses narrative statuses via shortGsdStatus().
* default format — and collapses narrative statuses via shortMsdStatus().
* The default "full" format is untouched.
*/
function formatGsdStateCompact(s, opts = {}) {
// #2850: mirrors formatGsdState's observable "nothing resolvable" signal.
function formatMsdStateCompact(s, opts = {}) {
// #2850: mirrors formatMsdState's observable "nothing resolvable" signal.
if (s.noActiveWorkstream) return NO_ACTIVE_WORKSTREAM_LABEL;
const parts = [];
@@ -532,7 +532,7 @@ function formatGsdStateCompact(s, opts = {}) {
parts.push(bracketPhase ?? (s.phaseTotal ? `P${phaseId}/${s.phaseTotal}` : `P${phaseId}`));
}
// Scene exclusivity mirrors formatGsdState's if/else chain: an in-flight
// Scene exclusivity mirrors formatMsdState's if/else chain: an in-flight
// phase (Scene 1, gated on activePhase ONLY — the legacy phaseNum shape
// still completes) wins over milestone-complete (Scene 3), even if a
// non-atomic STATE.md edit leaves percent=100 alongside a lifecycle phase.
@@ -542,7 +542,7 @@ function formatGsdStateCompact(s, opts = {}) {
if (done) {
parts.push('complete');
} else {
const st = shortGsdStatus(s.status);
const st = shortMsdStatus(s.status);
if (st) {
parts.push(st);
} else if (!phaseId && s.nextAction) {
@@ -654,14 +654,14 @@ function buildGitSegment(info) {
// --- STATE.md freshness marker (opt-in, #2734) --------------------------------
//
// Opt-in via `statusline.show_state_freshness: true`. Renders `state ~N
// commits back` inside the GSD-state segment when STATE.md's `state_head`
// commits back` inside the MSD-state segment when STATE.md's `state_head`
// stamp (#2573) is at least STATE_HEAD_ADVISORY_COMMITS commits behind HEAD.
// Same impure-reader -> pure-IR -> pure-formatter shape as the git segment
// above. See .gsd/phase/feat-2734-statusline-state-freshness/40-design.md.
// above. See .msd/phase/feat-2734-statusline-state-freshness/40-design.md.
// Deliberate mirror of the fence in src/state.cts (STATE_HEAD_HASH_RE) — kept
// hook-side rather than requiring state.cjs on the per-render path (measured
// ~20ms; see design doc "Laws that apply"). tests/gsd-statusline.test.cjs
// ~20ms; see design doc "Laws that apply"). tests/msd-statusline.test.cjs
// asserts behavioral parity against readStateHeadFreshness rather than
// comparing source (local/no-source-grep forbids the latter anyway).
const STATE_HEAD_HASH_RE = /^[0-9a-f]{4,40}$/i;
@@ -731,7 +731,7 @@ function parseRevListCounts(text) {
* HEAD (reset/rebase/force-push) -> unknown, never "fresh".
*/
function deriveStateFreshness(root, stamp, deps = {}) {
const { existsSync = fs.existsSync, readConfig = readGsdConfig, readCounts = readStateHeadCommits } = deps;
const { existsSync = fs.existsSync, readConfig = readMsdConfig, readCounts = readStateHeadCommits } = deps;
const raw = typeof stamp === 'string' ? stamp.trim() : '';
const valid = STATE_HEAD_HASH_RE.test(raw);
@@ -775,7 +775,7 @@ function formatStateFreshness(fresh) {
* independently, which had drifted into a live divergence between the two
* entry points — this collapses both onto one resolver.
*
* @param {object} cfg — parsed .planning/config.json (readGsdConfig())
* @param {object} cfg — parsed .planning/config.json (readMsdConfig())
* @returns {{ showLastCommand: boolean, position: 'end'|'front', stateFormat: 'full'|'compact', showGit: boolean, showStateFreshness: boolean, convention: string|null, projectCode: string|null }}
*/
function resolveStatuslineOptions(cfg) {
@@ -827,7 +827,7 @@ function runStatusline() {
// Read .planning config once — used by the context meter (token suffix)
// and the last-command/position block below. Fail-soft to {}.
let cfg = {};
try { cfg = readGsdConfig(dir); } catch (e) {}
try { cfg = readMsdConfig(dir); } catch (e) {}
// Context window display (shows USED percentage scaled to usable context)
// Claude Code reserves a buffer for autocompact. By default this is ~16.5%
@@ -923,27 +923,27 @@ function runStatusline() {
}
}
// GSD state (milestone · status · phase) — shown when no todo task.
// MSD state (milestone · status · phase) — shown when no todo task.
// Format resolved below once config is read (statusline.state_format).
let gsdStateStr = '';
let msdStateStr = '';
// GSD update available?
// MSD update available?
// Read only the per-package shared cache file (#607). The legacy
// runtime-specific fallback has been removed — the per-package filename
// carries lineage and avoids multi-runtime resolution mismatches (#1421).
let gsdUpdate = '';
const cacheFile = path.join(homeDir, '.cache', 'gsd', updateCacheFileName);
let msdUpdate = '';
const cacheFile = path.join(homeDir, '.cache', 'msd', updateCacheFileName);
if (fs.existsSync(cacheFile)) {
try {
const cache = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
const { showUpdate, staleWarning } = evaluateUpdateCache(cache);
if (showUpdate) {
gsdUpdate = '\x1b[33m⬆ /gsd:update\x1b[0m │ ';
msdUpdate = '\x1b[33m⬆ /msd:update\x1b[0m │ ';
}
if (staleWarning === 'dev') {
gsdUpdate += '\x1b[33m⚠ dev install — re-run installer to sync hooks\x1b[0m │ ';
msdUpdate += '\x1b[33m⚠ dev install — re-run installer to sync hooks\x1b[0m │ ';
} else if (staleWarning === 'stale') {
gsdUpdate += '\x1b[31m⚠ stale hooks — run /gsd:update\x1b[0m │ ';
msdUpdate += '\x1b[31m⚠ stale hooks — run /msd:update\x1b[0m │ ';
}
} catch (e) {}
}
@@ -973,25 +973,25 @@ function runStatusline() {
// Never break the statusline on config/transcript/git errors
}
// #2734: readGsdState is inside `if (!task)` deliberately — when a todo
// task is in flight the GSD-state segment is not rendered, so spending a
// #2734: readMsdState is inside `if (!task)` deliberately — when a todo
// task is in flight the MSD-state segment is not rendered, so spending a
// freshness git spawn here would spend a subprocess on discarded output.
if (!task) {
const state = readGsdState(dir, { stateFreshness: options.showStateFreshness }) || {};
gsdStateStr = options.stateFormat === 'compact'
? formatGsdStateCompact(state, options)
: formatGsdState(state, options);
const state = readMsdState(dir, { stateFreshness: options.showStateFreshness }) || {};
msdStateStr = options.stateFormat === 'compact'
? formatMsdStateCompact(state, options)
: formatMsdState(state, options);
}
// Output
const dirname = path.basename(dir);
const middle = task
? `\x1b[1m${task}\x1b[0m`
: gsdStateStr
? `\x1b[2m${gsdStateStr}\x1b[0m`
: msdStateStr
? `\x1b[2m${msdStateStr}\x1b[0m`
: null;
process.stdout.write(composeStatusline({ gsdUpdate, model, ctx, middle, dirname, lastCmdSuffix, gitSuffix, position: options.position }));
process.stdout.write(composeStatusline({ msdUpdate, model, ctx, middle, dirname, lastCmdSuffix, gitSuffix, position: options.position }));
} catch (e) {
// Silent fail - don't break statusline on parse errors
}
@@ -1004,10 +1004,10 @@ function runStatusline() {
* Compose the statusline string from pre-built segments.
*
* @param {object} opts
* @param {string} [opts.gsdUpdate=''] - leading update/stale-hooks warning (already formatted)
* @param {string} [opts.msdUpdate=''] - leading update/stale-hooks warning (already formatted)
* @param {string} opts.model - model display name (plain text; dim styling applied here)
* @param {string} [opts.ctx=''] - context-window meter segment (empty string = absent)
* @param {string|null} [opts.middle=null] - middle segment (todo task or GSD state), null = absent
* @param {string|null} [opts.middle=null] - middle segment (todo task or MSD state), null = absent
* @param {string} opts.dirname - project directory basename (dim styling applied here)
* @param {string} [opts.lastCmdSuffix=''] - last-command suffix, e.g. ' │ last: /foo'
* @param {string} [opts.gitSuffix=''] - git branch/status segment, e.g. ' │ main✓' (after dirname)
@@ -1020,7 +1020,7 @@ function runStatusline() {
* without breaking the statusline.
*/
function composeStatusline({
gsdUpdate = '',
msdUpdate = '',
model,
ctx = '',
middle = null,
@@ -1035,12 +1035,12 @@ function composeStatusline({
const pos = position === 'front' ? 'front' : 'end';
if (pos === 'front') {
if (middle) return `${gsdUpdate}${modelSeg}${ctx} │ ${middle} │ ${dirSeg}${gitSuffix}${lastCmdSuffix}`;
return `${gsdUpdate}${modelSeg}${ctx} │ ${dirSeg}${gitSuffix}${lastCmdSuffix}`;
if (middle) return `${msdUpdate}${modelSeg}${ctx} │ ${middle} │ ${dirSeg}${gitSuffix}${lastCmdSuffix}`;
return `${msdUpdate}${modelSeg}${ctx} │ ${dirSeg}${gitSuffix}${lastCmdSuffix}`;
}
// 'end' — preserved byte-for-byte relative to original inline templates
if (middle) return `${gsdUpdate}${modelSeg} │ ${middle} │ ${dirSeg}${gitSuffix}${ctx}${lastCmdSuffix}`;
return `${gsdUpdate}${modelSeg} │ ${dirSeg}${gitSuffix}${ctx}${lastCmdSuffix}`;
if (middle) return `${msdUpdate}${modelSeg} │ ${middle} │ ${dirSeg}${gitSuffix}${ctx}${lastCmdSuffix}`;
return `${msdUpdate}${modelSeg} │ ${dirSeg}${gitSuffix}${ctx}${lastCmdSuffix}`;
}
function isInstalledAheadOfLatest(installed, latest) {
@@ -1075,14 +1075,14 @@ function evaluateUpdateCache(cache) {
// Export helpers for unit tests. Harmless when run as a script.
module.exports = {
readGsdState, parseStateMd, formatGsdState,
readGsdConfig, getConfigValue, readLastSlashCommand,
readMsdState, parseStateMd, formatMsdState,
readMsdConfig, getConfigValue, readLastSlashCommand,
composeStatusline,
isInstalledAheadOfLatest,
evaluateUpdateCache,
formatTokens,
contextTokenSuffix,
shortGsdStatus, formatGsdStateCompact,
shortMsdStatus, formatMsdStateCompact,
compactModelName,
readGitStatus, parseGitStatus, buildGitSegment,
STATE_HEAD_ADVISORY_COMMITS, isValidStateHeadStamp,
@@ -1116,7 +1116,7 @@ function renderStatusline(data) {
projectCode: null,
};
try {
const cfg = readGsdConfig(dir);
const cfg = readMsdConfig(dir);
options = resolveStatuslineOptions(cfg);
if (options.showLastCommand) {
const lastCmd = readLastSlashCommand(data.transcript_path);
@@ -1129,11 +1129,11 @@ function renderStatusline(data) {
}
} catch (e) { /* swallow */ }
const state = readGsdState(dir, { stateFreshness: options.showStateFreshness }) || {};
const gsdStateStr = options.stateFormat === 'compact'
? formatGsdStateCompact(state, options)
: formatGsdState(state, options);
const middle = gsdStateStr ? `\x1b[2m${gsdStateStr}\x1b[0m` : null;
const state = readMsdState(dir, { stateFreshness: options.showStateFreshness }) || {};
const msdStateStr = options.stateFormat === 'compact'
? formatMsdStateCompact(state, options)
: formatMsdState(state, options);
const middle = msdStateStr ? `\x1b[2m${msdStateStr}\x1b[0m` : null;
return composeStatusline({ model, ctx: '', middle, dirname, lastCmdSuffix, gitSuffix, position: options.position });
}

View File

@@ -1,11 +1,11 @@
#!/usr/bin/env node
// gsd-hook-version: {{GSD_VERSION}}
// SessionStart banner that surfaces GSD update availability when GSD's
// msd-hook-version: {{MSD_VERSION}}
// SessionStart banner that surfaces MSD update availability when MSD's
// statusline isn't installed. Reads the cache that
// gsd-check-update-worker.js writes to ~/.cache/gsd/<updateCacheFileName> (per-package).
// msd-check-update-worker.js writes to ~/.cache/msd/<updateCacheFileName> (per-package).
//
// Opt-in by design: bin/install.js only registers this hook when the user
// declines to install (or replace) the GSD statusline. The presence of the
// declines to install (or replace) the MSD statusline. The presence of the
// SessionStart entry IS the opt-in — there is no separate runtime flag.
//
// See issue #2795 for the rationale.
@@ -16,7 +16,7 @@ const fs = require('fs');
const path = require('path');
const os = require('os');
// #3582: gsd-core/bin/lib/package-identity.cjs is a tsc build artifact
// #3582: msd-core/bin/lib/package-identity.cjs is a tsc build artifact
// (ADR-457), gitignored and absent on a raw plugin-marketplace / git-clone
// install that never ran `npm run build:lib`. This is an opt-in SessionStart
// hook — a build failure here must DEGRADE, not crash session start. With
@@ -26,13 +26,13 @@ const os = require('os');
// silent "print nothing" path below — no separate degrade branch needed.
// This try/require/ensureRuntimeBuild/require/catch shape is deliberately
// duplicated (not extracted to hooks/lib/) — see
// gsd-check-update-worker.js's identical #3582 comment for why.
// msd-check-update-worker.js's identical #3582 comment for why.
let PACKAGE_NAME = null;
let updateCacheFileName = 'gsd-update-check.json';
let updateCacheFileName = 'msd-update-check.json';
try {
const { ensureRuntimeBuild } = require('../gsd-core/bin/ensure-runtime-build.cjs');
const { ensureRuntimeBuild } = require('../msd-core/bin/ensure-runtime-build.cjs');
ensureRuntimeBuild();
({ PACKAGE_NAME, updateCacheFileName } = require('../gsd-core/bin/lib/package-identity.cjs'));
({ PACKAGE_NAME, updateCacheFileName } = require('../msd-core/bin/lib/package-identity.cjs'));
} catch (e) {
// Runtime library missing/broken and could not self-build — degrade to the
// fallbacks above rather than crash the SessionStart hook.
@@ -56,7 +56,7 @@ function buildBannerOutput(state) {
const { cache, parseError, suppressFailureWarning } = state || {};
if (parseError) {
if (suppressFailureWarning) return null;
return { systemMessage: 'GSD update check failed.' };
return { systemMessage: 'MSD update check failed.' };
}
if (!cache) return null;
// Lineage guard: package_name must be present and match this package.
@@ -66,7 +66,7 @@ function buildBannerOutput(state) {
const installed = cache.installed || 'unknown';
const latest = cache.latest || 'unknown';
return {
systemMessage: `GSD update available: ${installed} → ${latest}. Run /gsd:update.`,
systemMessage: `MSD update available: ${installed} → ${latest}. Run /msd:update.`,
};
}
@@ -120,7 +120,7 @@ function recordFailureWarning(sentinelFile, nowSeconds) {
}
function main() {
const cacheDir = path.join(os.homedir(), '.cache', 'gsd');
const cacheDir = path.join(os.homedir(), '.cache', 'msd');
const cacheFile = path.join(cacheDir, updateCacheFileName);
const sentinelFile = path.join(cacheDir, 'banner-failure-warned-at');
const now = Math.floor(Date.now() / 1000);
@@ -133,7 +133,7 @@ function main() {
if (parseError && !suppressFailureWarning) {
// Ensure cache dir exists before writing the sentinel — first-run case
// where ~/.cache/gsd was created by check-update but the parent dir got
// where ~/.cache/msd was created by check-update but the parent dir got
// wiped between runs.
try {
fs.mkdirSync(cacheDir, { recursive: true });

View File

@@ -1,9 +1,9 @@
#!/usr/bin/env bash
# gsd-hook-version: {{GSD_VERSION}}
# gsd-validate-commit.sh — PreToolUse hook: enforce Conventional Commits format
# msd-hook-version: {{MSD_VERSION}}
# msd-validate-commit.sh — PreToolUse hook: enforce Conventional Commits format
# Blocks git commit commands with non-conforming messages (exit 2).
# Allows conforming messages and all non-commit commands (exit 0).
# Uses Node.js for JSON parsing (always available in GSD projects, no jq dependency).
# Uses Node.js for JSON parsing (always available in MSD projects, no jq dependency).
#
# OPT-IN: This hook is a no-op unless config.json has hooks.community: true.
# Enable with: "hooks": { "community": true } in .planning/config.json
@@ -56,14 +56,14 @@ if [ -f .planning/config.json ]; then
# built below, so a configured value can never alter the compiled pattern's
# structure.
BUILTIN_COMMIT_TYPES_CSV=$(IFS=,; echo "${BUILTIN_COMMIT_TYPES[*]}")
CONFIG_OUT=$(GSD_BUILTIN_COMMIT_TYPES="$BUILTIN_COMMIT_TYPES_CSV" node -e "
CONFIG_OUT=$(MSD_BUILTIN_COMMIT_TYPES="$BUILTIN_COMMIT_TYPES_CSV" node -e "
try{
const c=require('./.planning/config.json');
process.stdout.write(c.hooks?.community===true?'1':'0');
process.stdout.write('\n');
const raw=c.hooks?.commit_types;
const list=Array.isArray(raw)?raw:[];
const seen=new Set((process.env.GSD_BUILTIN_COMMIT_TYPES||'').split(',').filter(Boolean));
const seen=new Set((process.env.MSD_BUILTIN_COMMIT_TYPES||'').split(',').filter(Boolean));
for (const t of list){
if (typeof t!=='string') continue;
if (!/^[a-z][a-z0-9-]*\$/.test(t)) continue;
@@ -87,7 +87,7 @@ if [ -f .planning/config.json ]; then
# Could not determine the opt-in flag at all (node missing, JSON parse
# error other than absence, etc.) — distinct from ".planning/config.json
# exists and legitimately disables the hook". Say so and pass, per #3838.
echo "gsd-validate-commit.sh: could not read .planning/config.json (opt-in check) — validator disabled for this call. $(cat "$ENABLED_ERR")" >&2
echo "msd-validate-commit.sh: could not read .planning/config.json (opt-in check) — validator disabled for this call. $(cat "$ENABLED_ERR")" >&2
exit 0
fi
# Pure parameter expansion, not `printf ... | head -1`: same SIGPIPE race
@@ -135,7 +135,7 @@ if [ "$CMD_STATUS" != "0" ]; then
# Could not extract tool_input.command at all (node missing, malformed
# JSON, etc.) — distinct from "there is genuinely no command field". Say
# so and pass, per #3838.
echo "gsd-validate-commit.sh: could not extract tool_input.command from the hook payload — validator disabled for this call. $(cat "$CMD_ERR")" >&2
echo "msd-validate-commit.sh: could not extract tool_input.command from the hook payload — validator disabled for this call. $(cat "$CMD_ERR")" >&2
exit 0
fi
@@ -166,12 +166,12 @@ if [ "$CLASSIFY_STATUS" != "0" ] && [ "$CLASSIFY_STATUS" != "1" ]; then
# (real negative, pass silently) — the ONLY intentional non-zero exit the
# script above ever produces on success. Any other status — 127 node
# missing, or 3 from the try/catch above when the git-cmd.js require chain
# throws (e.g. its built dependency, gsd-core/bin/lib/token-scanner.cjs, is
# throws (e.g. its built dependency, msd-core/bin/lib/token-scanner.cjs, is
# a gitignored build artifact and absent on a fresh checkout — run
# `npm run build:lib`) — means the classifier could not run at all. Say so
# on stderr and pass (#3838): PreToolUse stderr does not disturb the JSON
# protocol.
echo "gsd-validate-commit.sh: could not classify the command via hooks/lib/git-cmd.js (exit $CLASSIFY_STATUS) — validator disabled for this call. If this persists, run \`npm run build:lib\`. $(cat "$CLASSIFY_ERR")" >&2
echo "msd-validate-commit.sh: could not classify the command via hooks/lib/git-cmd.js (exit $CLASSIFY_STATUS) — validator disabled for this call. If this persists, run \`npm run build:lib\`. $(cat "$CLASSIFY_ERR")" >&2
exit 0
fi
if [ "$CLASSIFY_STATUS" = "0" ]; then

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env node
// gsd-hook-version: {{GSD_VERSION}}
// gsd-windsurf-pre-command.js — Windsurf/Cascade pre_run_command hook (ADR-1239 / #2100)
// msd-hook-version: {{MSD_VERSION}}
// msd-windsurf-pre-command.js — Windsurf/Cascade pre_run_command hook (ADR-1239 / #2100)
//
// Cascade (Windsurf's agent) invokes this script before each shell-command
// tool call executes, via the workspace/global hooks.json hook bus.
@@ -254,7 +254,7 @@ function destructiveReason(cmd) {
}
function block(reason) {
deny(undefined, `GSD windsurf pre_run_command guard: ${reason}\n`);
deny(undefined, `MSD windsurf pre_run_command guard: ${reason}\n`);
}
let input = '';

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env node
// gsd-hook-version: {{GSD_VERSION}}
// gsd-windsurf-pre-write.js — Windsurf/Cascade pre_write_code hook (ADR-1239 / #2100)
// msd-hook-version: {{MSD_VERSION}}
// msd-windsurf-pre-write.js — Windsurf/Cascade pre_write_code hook (ADR-1239 / #2100)
//
// Cascade (Windsurf's agent) invokes this script before each file-write tool
// call executes, via the workspace/global hooks.json hook bus.
@@ -16,7 +16,7 @@
// to the agent/user
//
// Behaviour: reimplements the core containment check from
// hooks/gsd-worktree-path-guard.js — block a write whose file_path resolves
// hooks/msd-worktree-path-guard.js — block a write whose file_path resolves
// (via `git rev-parse --show-toplevel`) to a DIFFERENT git root than the
// current working directory, or lands inside a `.git/` internals directory.
// Fails OPEN on any error, timeout, non-git cwd, or missing git binary — a
@@ -35,7 +35,7 @@ const { reportIfUndetermined } = require('./lib/git-probe.js');
// #3911 (ADR-3889 Phase 7): the exit(2) call site (block(), below) is
// migrated to hook-exit.js's deny(undefined, reason) — see
// gsd-windsurf-pre-command.js's identical note for the fixed defect
// msd-windsurf-pre-command.js's identical note for the fixed defect
// (terminateNow's fd 1/fd 2 writes now run in independent try/catch blocks).
const SPAWNOPT = { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000, windowsHide: true };
@@ -61,7 +61,7 @@ function nearestExistingDir(start) {
}
function block(reason) {
deny(undefined, `GSD windsurf pre_write_code guard: ${reason}\n`);
deny(undefined, `MSD windsurf pre_write_code guard: ${reason}\n`);
}
let input = '';
@@ -85,7 +85,7 @@ process.stdin.on('end', () => {
// "no git root here" answer by status/stdout alone — reportIfUndetermined
// is a no-op on a genuine negative and only fires when the probe itself
// could not run. The allow() below is UNCHANGED either way.
reportIfUndetermined('gsd-windsurf-pre-write', 'git rev-parse --show-toplevel (cwd)', cwdTopResult);
reportIfUndetermined('msd-windsurf-pre-write', 'git rev-parse --show-toplevel (cwd)', cwdTopResult);
if (cwdTopResult.status !== 0 || !cwdTopResult.stdout) { allow(undefined); return; }
const cwdTopRaw = cwdTopResult.stdout.trim();
@@ -105,13 +105,13 @@ process.stdin.on('end', () => {
if (!checkDir) { allow(undefined); return; } // synthetic path with no existing ancestor — fail open
const fileTopResult = git(['rev-parse', '--show-toplevel'], checkDir);
reportIfUndetermined('gsd-windsurf-pre-write', 'git rev-parse --show-toplevel (file location)', fileTopResult);
reportIfUndetermined('msd-windsurf-pre-write', 'git rev-parse --show-toplevel (file location)', fileTopResult);
if (fileTopResult.status !== 0 || !fileTopResult.stdout) {
// Not inside any git worktree. Distinguish "inside a .git/ internals
// directory" (dangerous — BLOCK) from "outside all git repos entirely"
// (not the escape vector this guard targets — fail open).
const insideGitDir = git(['rev-parse', '--is-inside-git-dir'], checkDir);
reportIfUndetermined('gsd-windsurf-pre-write', 'git rev-parse --is-inside-git-dir', insideGitDir);
reportIfUndetermined('msd-windsurf-pre-write', 'git rev-parse --is-inside-git-dir', insideGitDir);
if (insideGitDir.status === 0 && insideGitDir.stdout && insideGitDir.stdout.trim() === 'true') {
block(
`'${filePath}' is inside a git internal (.git) directory, not the active project at ` +

View File

@@ -1,11 +1,11 @@
#!/usr/bin/env node
// gsd-hook-version: {{GSD_VERSION}}
// GSD Workflow Guard — PreToolUse hook
// Detects when Claude attempts file edits outside a GSD workflow context
// (no active /gsd- skill or Task subagent) and injects an advisory warning.
// msd-hook-version: {{MSD_VERSION}}
// MSD Workflow Guard — PreToolUse hook
// Detects when Claude attempts file edits outside a MSD workflow context
// (no active /msd- skill or Task subagent) and injects an advisory warning.
//
// This is a SOFT guard for edits — it advises, not blocks. The edit still
// proceeds. The warning nudges Claude to use /gsd:quick or /gsd:fast instead
// proceeds. The warning nudges Claude to use /msd:quick or /msd:fast instead
// of making direct edits that bypass state tracking.
//
// ONE hard block lives here: `git add -f` on an agent/worktree-agent branch
@@ -99,7 +99,7 @@ function currentBranch(cwd) {
// caller's point of view. reportIfUndetermined is a no-op on a genuine
// negative and only emits a diagnostic when the probe itself could not
// run; the '' fallback (and therefore this hook's exit code) is unchanged.
reportIfUndetermined('gsd-workflow-guard', 'git branch --show-current', result);
reportIfUndetermined('msd-workflow-guard', 'git branch --show-current', result);
if (result.status !== 0) return '';
return result.stdout.trim();
}
@@ -192,8 +192,8 @@ function workflowGuardEnabled(cwd) {
// kimi-cli's Shell.Params names its field `command`
// (src/kimi_cli/tools/shell/__init__.py), same as Claude's Bash, so the
// Shell leg needs only the name mapping. This block is kept byte-identical
// with the copies in gsd-prompt-guard.js, gsd-read-guard.js,
// gsd-worktree-path-guard.js, and gsd-read-injection-scanner.js — a parity
// with the copies in msd-prompt-guard.js, msd-read-guard.js,
// msd-worktree-path-guard.js, and msd-read-injection-scanner.js — a parity
// test binds them (tests/kimi-guard-normalization-parity.test.cjs). Inlined
// per guard (not hooks/lib/): hook scripts are staged as standalone files,
// and a sibling require is a staging dependency that can fail silently.
@@ -233,7 +233,7 @@ function normalizeKimiPayload(data) {
// guard reading an empty string and exiting 0, while the identical write
// without the extra key blocked — a bypass needing no crash at all. The same
// shadowing also preserved a NON-STRING `file_path` (`[]`), which threw
// inside gsd-worktree-path-guard's path.isAbsolute() and reached its outer
// inside msd-worktree-path-guard's path.isAbsolute() and reached its outer
// `catch { process.exit(0) }`: the same crash-to-allow this fix closes
// elsewhere, reached through the guard's own read rather than through
// normalization. Overwriting can only ever narrow what a guard inspects to
@@ -257,7 +257,7 @@ function normalizeKimiPayload(data) {
// trigger. Degrading only the non-coercible entry to '' keeps
// stringification intact for every value that CAN coerce (numbers,
// arrays, plain objects), so nothing downstream — including
// gsd-prompt-guard's scan of new_string — loses content it saw before.
// msd-prompt-guard's scan of new_string — loses content it saw before.
const editText = (v) => { try { return String(v ?? ''); } catch { return ''; } };
// #2595 (review Major 2): reconstruct UNCONDITIONALLY, mirroring the
// `path` decision above rather than merely filling in when the field
@@ -266,7 +266,7 @@ function normalizeKimiPayload(data) {
// no `old_string`/`new_string` at all, so either field appearing in a
// Kimi payload is ALWAYS model-supplied, exactly like `file_path`. Under
// the old `=== undefined` condition a model-supplied `new_string: ""`
// SHADOWED the reconstruction, leaving gsd-prompt-guard's injection scan
// SHADOWED the reconstruction, leaving msd-prompt-guard's injection scan
// reading '' and exiting at its `if (!content)` before it ever saw the
// real `edit[].new` — a one-key bypass of the very scan this fix's
// guarded coercion exists to keep fed. A `typeof` test would NOT close
@@ -289,11 +289,11 @@ process.stdin.on('end', () => {
// #3504 test-only fault seam: throws right after parse so the fail-closed
// posture of the outer catch is exercisable — no JSON-expressible input
// throws in this handler today (#2547/#2595 hardened every read). Gated on
// GSD_TEST_MODE as well so a leaked GSD_TEST_WORKFLOW_GUARD_FAULT in a real
// MSD_TEST_MODE as well so a leaked MSD_TEST_WORKFLOW_GUARD_FAULT in a real
// shell cannot wedge a production session. The fault's failure direction
// is CLOSED: the catch below may block, never bypass a block.
if (process.env.GSD_TEST_MODE === '1' && process.env.GSD_TEST_WORKFLOW_GUARD_FAULT === '1') {
throw new Error('GSD_TEST_WORKFLOW_GUARD_FAULT: injected fault');
if (process.env.MSD_TEST_MODE === '1' && process.env.MSD_TEST_WORKFLOW_GUARD_FAULT === '1') {
throw new Error('MSD_TEST_WORKFLOW_GUARD_FAULT: injected fault');
}
const toolName = data.tool_name;
const cwd = data.cwd || process.cwd();
@@ -318,7 +318,7 @@ process.stdin.on('end', () => {
allow(undefined);
}
// Check if we're inside a GSD workflow (Task subagent or /gsd- skill)
// Check if we're inside a MSD workflow (Task subagent or /msd- skill)
// Subagents have a session_id that differs from the parent
// and typically have a description field set by the orchestrator
if (data.tool_input?.is_subagent || data.session_type === 'task') {
@@ -335,12 +335,12 @@ process.stdin.on('end', () => {
(typeof data.tool_input?.path === 'string' && data.tool_input.path) ||
'';
// Allow edits to .planning/ files (GSD state management)
// Allow edits to .planning/ files (MSD state management)
if (filePath.includes('.planning/') || filePath.includes('.planning\\')) {
allow(undefined);
}
// Allow edits to common config/docs files that don't need GSD tracking
// Allow edits to common config/docs files that don't need MSD tracking
const allowedPatterns = [
/\.gitignore$/,
/\.env/,
@@ -354,17 +354,17 @@ process.stdin.on('end', () => {
}
if (!isWorkflowGuardEnabled) {
allow(undefined); // Guard disabled (default) or no GSD project
allow(undefined); // Guard disabled (default) or no MSD project
}
// If we get here: GSD project, guard enabled, file edit outside .planning/,
// If we get here: MSD project, guard enabled, file edit outside .planning/,
// not in a subagent context. Inject advisory warning.
const output = {
hookSpecificOutput: {
hookEventName: "PreToolUse",
additionalContext: `⚠️ WORKFLOW ADVISORY: You're editing ${path.basename(filePath)} directly without a GSD command. ` +
additionalContext: `⚠️ WORKFLOW ADVISORY: You're editing ${path.basename(filePath)} directly without a MSD command. ` +
'This edit will not be tracked in STATE.md or produce a SUMMARY.md. ' +
'Consider using /gsd:fast for trivial fixes or /gsd:quick for larger changes ' +
'Consider using /msd:fast for trivial fixes or /msd:quick for larger changes ' +
'to maintain project state tracking. ' +
'If this is intentional (e.g., user explicitly asked for a direct edit), proceed normally.',
code: 'WORKFLOW_ADVISORY'

View File

@@ -1,11 +1,11 @@
#!/usr/bin/env node
// gsd-hook-version: {{GSD_VERSION}}
// GSD Worktree Path Guard — PreToolUse hook
// msd-hook-version: {{MSD_VERSION}}
// MSD Worktree Path Guard — PreToolUse hook
// Blocks Edit/Write/MultiEdit tool calls that target absolute paths outside the worktree root.
//
// Problem: gsd-executor agents spawned with isolation="worktree" sometimes issue
// Problem: msd-executor agents spawned with isolation="worktree" sometimes issue
// Edit/Write calls with absolute paths rooted at the MAIN repository instead of
// the worktree (issue #260). The prose guard in agents/gsd-executor.md step 0b
// the worktree (issue #260). The prose guard in agents/msd-executor.md step 0b
// is never enforced because the model under load skips it.
//
// This hook enforces the constraint at the tooling layer, making it HARD-BLOCKING.
@@ -96,7 +96,7 @@ function normalizeKimiPayload(data) {
// guard reading an empty string and exiting 0, while the identical write
// without the extra key blocked — a bypass needing no crash at all. The same
// shadowing also preserved a NON-STRING `file_path` (`[]`), which threw
// inside gsd-worktree-path-guard's path.isAbsolute() and reached its outer
// inside msd-worktree-path-guard's path.isAbsolute() and reached its outer
// `catch { process.exit(0) }`: the same crash-to-allow this fix closes
// elsewhere, reached through the guard's own read rather than through
// normalization. Overwriting can only ever narrow what a guard inspects to
@@ -120,7 +120,7 @@ function normalizeKimiPayload(data) {
// trigger. Degrading only the non-coercible entry to '' keeps
// stringification intact for every value that CAN coerce (numbers,
// arrays, plain objects), so nothing downstream — including
// gsd-prompt-guard's scan of new_string — loses content it saw before.
// msd-prompt-guard's scan of new_string — loses content it saw before.
const editText = (v) => { try { return String(v ?? ''); } catch { return ''; } };
// #2595 (review Major 2): reconstruct UNCONDITIONALLY, mirroring the
// `path` decision above rather than merely filling in when the field
@@ -129,7 +129,7 @@ function normalizeKimiPayload(data) {
// no `old_string`/`new_string` at all, so either field appearing in a
// Kimi payload is ALWAYS model-supplied, exactly like `file_path`. Under
// the old `=== undefined` condition a model-supplied `new_string: ""`
// SHADOWED the reconstruction, leaving gsd-prompt-guard's injection scan
// SHADOWED the reconstruction, leaving msd-prompt-guard's injection scan
// reading '' and exiting at its `if (!content)` before it ever saw the
// real `edit[].new` — a one-key bypass of the very scan this fix's
// guarded coercion exists to keep fed. A `typeof` test would NOT close
@@ -180,7 +180,7 @@ process.stdin.on('end', () => {
// is a no-op on a genuine negative and only fires the diagnostic when the
// probe itself could not run. The allow() below is UNCHANGED either way.
reportIfUndetermined(
'gsd-worktree-path-guard',
'msd-worktree-path-guard',
'git rev-parse --git-dir --abbrev-ref HEAD --show-toplevel',
combinedResult
);
@@ -200,15 +200,15 @@ process.stdin.on('end', () => {
allow(undefined); // main repo, submodule, or separate-git-dir — no-op
}
// #1342: Only enforce inside a GSD-managed isolated executor worktree. Those
// #1342: Only enforce inside a MSD-managed isolated executor worktree. Those
// are always on an `agent-*` or legacy `worktree-agent-*` branch (the positive
// allow-list enforced by worktree-branch-check.md, #2924, #1995). A manually-
// created linked worktree (plain non-GSD work, e.g. Claude Code plan-mode) is
// created linked worktree (plain non-MSD work, e.g. Claude Code plan-mode) is
// on the user's own branch, so the guard must be a no-op there. Detached HEAD
// / error → not GSD-managed → no-op.
// / error → not MSD-managed → no-op.
// #3021: accept worktree-wf_<runid>-<n> branches (Workflow backend's naming).
if (!/^((worktree-)?agent-|worktree-wf_)[A-Za-z0-9._/-]+$/.test(branch)) {
allow(undefined); // not a GSD-managed executor worktree — no-op
allow(undefined); // not a MSD-managed executor worktree — no-op
}
// wtTopRaw: the raw --show-toplevel output for the worktree (cwd).
@@ -224,7 +224,7 @@ process.stdin.on('end', () => {
// `path` authoritative: normalization returns early for native Claude Code
// payloads (KIMI_TOOL_NAMES has no 'Edit' entry), so `{"tool_name":"Edit",
// "tool_input":{"file_path":[]}}` reached it untouched — this guard's
// original #260 surface. Same shape as hooks/gsd-windsurf-pre-write.js:75.
// original #260 surface. Same shape as hooks/msd-windsurf-pre-write.js:75.
const rawFilePath = typeof data.tool_input?.file_path === 'string'
? data.tool_input.file_path
: '';
@@ -282,7 +282,7 @@ process.stdin.on('end', () => {
// back-slash inconsistencies) — both values come from the same git binary
// in the same format by definition.
const fileTopResult = git(['rev-parse', '--show-toplevel'], checkDir);
reportIfUndetermined('gsd-worktree-path-guard', 'git rev-parse --show-toplevel (file location)', fileTopResult);
reportIfUndetermined('msd-worktree-path-guard', 'git rev-parse --show-toplevel (file location)', fileTopResult);
if (fileTopResult.status !== 0 || !fileTopResult.stdout) {
// The target's location is not a git work tree. Two sub-cases:
@@ -292,7 +292,7 @@ process.stdin.on('end', () => {
// - Truly outside all git repositories (e.g. ~/.claude/plans/) → not the
// main-repo vector → fail open. (#1342)
const insideGitDir = git(['rev-parse', '--is-inside-git-dir'], checkDir);
reportIfUndetermined('gsd-worktree-path-guard', 'git rev-parse --is-inside-git-dir', insideGitDir);
reportIfUndetermined('msd-worktree-path-guard', 'git rev-parse --is-inside-git-dir', insideGitDir);
if (insideGitDir.status === 0 && insideGitDir.stdout && insideGitDir.stdout.trim() === 'true') {
const output = {
decision: 'block',

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env node
// gsd-hook-version: {{GSD_VERSION}}
// GSD Write Guard — PreToolUse hook
// msd-hook-version: {{MSD_VERSION}}
// MSD Write Guard — PreToolUse hook
// Blocks a whole-file Write that catastrophically shrinks a curated .planning/
// artifact (ROADMAP.md, milestone roadmaps, STATE.md).
//
@@ -45,9 +45,9 @@
// Escape hatches — both named in the block message; a guard whose bypass is
// undocumented gets bypassed with the blunt instrument instead, with every
// other guard disabled at the same time:
// - GSD_ALLOW_PLANNING_SHRINK=1 (env) — for a human running interactively,
// - MSD_ALLOW_PLANNING_SHRINK=1 (env) — for a human running interactively,
// where the variable can actually reach the hook's environment.
// - .planning/.gsd-allow-shrink (single-use sentinel file) — for workflow
// - .planning/.msd-allow-shrink (single-use sentinel file) — for workflow
// steps. A PreToolUse hook inherits the RUNTIME's environment, so a
// per-step env prefix can never reach it (#2255 round 5 M1); the sentinel
// is a transport that code consults, not prose an agent obeys. The step
@@ -107,7 +107,7 @@ const FLOOR_LINES = 40;
// waves it through.
// #4455: workstream-scoped (and optionally project-scoped) variants —
// planningDir(cwd) (src/planning-workspace.cts) resolves to
// `.planning/[<project>/]workstreams/<ws>/...` whenever GSD_WORKSTREAM is
// `.planning/[<project>/]workstreams/<ws>/...` whenever MSD_WORKSTREAM is
// set. Before this, none of these three root-only patterns matched a
// workstream-scoped target at all, so the ENTIRE guard (not just the
// sentinel step — the shrink-ratio check too) silently never engaged for a
@@ -120,7 +120,7 @@ const FLOOR_LINES = 40;
// was needed there.
//
// Same gap exists one level up: planningDir(cwd) ALSO resolves to
// `.planning/<project>/...` when GSD_PROJECT is set with NO GSD_WORKSTREAM
// `.planning/<project>/...` when MSD_PROJECT is set with NO MSD_WORKSTREAM
// (project-only mode — the two env vars are independent; see planningDir's
// own body). None of the patterns above cover that shape either. Found
// during #4455's own review pass (same root cause, one more path variant)
@@ -148,14 +148,14 @@ function countLines(text) {
}
function isOverrideSet() {
const v = process.env.GSD_ALLOW_PLANNING_SHRINK;
const v = process.env.MSD_ALLOW_PLANNING_SHRINK;
return typeof v === 'string' && v !== '' && v !== '0' && v.toLowerCase() !== 'false';
}
// Single-use sentinel (see header). Consulted ONLY at the shrink-block point —
// a write that would pass anyway never burns the token, so first-shrink-wins
// for the write the workflow armed it for.
const SENTINEL_NAME = '.gsd-allow-shrink';
const SENTINEL_NAME = '.msd-allow-shrink';
const SENTINEL_REL = '.planning/' + SENTINEL_NAME;
const SENTINEL_TTL_MS = 15 * 60 * 1000;
@@ -343,7 +343,7 @@ process.stdin.on('end', () => {
emitBlock({
decision: 'block',
readError: err && err.code ? String(err.code) : 'UNKNOWN',
overrideEnvVar: 'GSD_ALLOW_PLANNING_SHRINK',
overrideEnvVar: 'MSD_ALLOW_PLANNING_SHRINK',
overrideSentinel: SENTINEL_REL,
reason:
`Write guard: could not read '${filePath}' to compare against the pending ` +
@@ -351,7 +351,7 @@ process.stdin.on('end', () => {
`'${path.basename(filePath)}' is a curated planning artifact, so this guard ` +
`fails closed rather than risk a blind overwrite. Retry once the file is ` +
`readable, or — if this overwrite is intentional — re-run with the ` +
`environment variable GSD_ALLOW_PLANNING_SHRINK=1 to bypass this guard once.`,
`environment variable MSD_ALLOW_PLANNING_SHRINK=1 to bypass this guard once.`,
});
}
@@ -381,7 +381,7 @@ process.stdin.on('end', () => {
decision: 'block',
oldLines,
newLines,
overrideEnvVar: 'GSD_ALLOW_PLANNING_SHRINK',
overrideEnvVar: 'MSD_ALLOW_PLANNING_SHRINK',
overrideSentinel: SENTINEL_REL,
// Round 9 Major 2: the denial deliberately does NOT explain how to arm
// the sentinel — #973 was an agent reasoning past an advisory, and a
@@ -402,7 +402,7 @@ process.stdin.on('end', () => {
`(#973: a planner collapsed ROADMAP.md 292 → 16 lines this way). To fix: use Edit for ` +
`a scoped change, or Read the full file and include every section in the Write. ` +
`Intentional milestone resets go through the workflow's documented escape hatch; ` +
`interactively, re-run with the environment variable GSD_ALLOW_PLANNING_SHRINK=1 ` +
`interactively, re-run with the environment variable MSD_ALLOW_PLANNING_SHRINK=1 ` +
`to bypass this guard once.`,
});
} catch {