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

@@ -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