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:
@@ -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 }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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",
|
||||
})
|
||||
]);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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('{{')) {
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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}`);
|
||||
@@ -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.'
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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({}));
|
||||
}
|
||||
});
|
||||
@@ -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({}));
|
||||
@@ -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 ` +
|
||||
@@ -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({}));
|
||||
@@ -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);
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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) });
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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),
|
||||
@@ -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
|
||||
@@ -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 + "\"");
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
@@ -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
|
||||
@@ -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 = '';
|
||||
@@ -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 ` +
|
||||
@@ -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'
|
||||
@@ -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',
|
||||
@@ -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 {
|
||||
Reference in New Issue
Block a user