diff --git a/CONTEXT.md b/CONTEXT.md index 28cd47e20..f3745f80e 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -239,7 +239,7 @@ Module owning runtime identity normalization at runtime-selection seams. Canonic Pure, no-write Module owning the **detection rung** of runtime identity — ADR-2313 Phase 5 (#3245, folded from #2320). The Runtime Name Policy Module normalizes the two *explicit* signals (`MSD_RUNTIME`, `.planning/config.json:runtime`); this module answers the different question those two cannot: *which host is this process actually running inside* when neither is set. Before it, `init` reported `agent_runtime: claude` inside a Codex session, because the ladder ended at a hardcoded default. `detectHostRuntime(deps?) → {runtime, source, signal}` is the typed surface tests assert against (`source` ∈ `session-env|config-home|none`); it probes, in order, the frozen `CODEX_SESSION_ENV_SIGNALS` table (`CODEX_SANDBOX`, `CODEX_SANDBOX_NETWORK_DISABLED` — injected by Codex into shell-tool children per openai/codex `AGENTS.md`; absent under `sandbox_mode = "danger-full-access"`, so best-effort), then an explicitly-exported `CODEX_HOME` whose `config.toml` exists — the marker FILENAME is single-sourced from the Update-Context Module's `inferPreferredRuntime` (`CODEX_CONFIG_MARKER`, re-exported here), while the TRUTHINESS RULE deliberately differs: `inferPreferredRuntime` accepts a bare, unchecked `CODEX_HOME` as sufficient to resolve an update context, whereas this module additionally requires the marker file to exist, because it asserts session identity rather than resolving an update context and needs the stronger signal — a difference pinned by a test in `tests/host-runtime-detection.test.cjs` rather than left implicit. `resolveReportedRuntime(projectDir, deps?)` composes the whole ladder: `MSD_RUNTIME` > config `runtime` > detection > `'claude'`. Three invariants are load-bearing and each has a test: it **never writes** (#2297 shared-defaults poisoning — no `~/.msd/defaults.json`, no config mutation); it **never shells out**, so there is no subprocess to time-bound and no degraded-on-timeout path to design; and the **default `~/.codex/config.toml` is never probed**, because every machine that has run Codex carries that file and probing it would misreport Claude Code sessions as codex. _Avoid_: calling this "runtime resolution" — `resolveRuntime` (Runtime Slash Module) keeps its own frozen `MSD_RUNTIME > config > 'claude'` contract and its 71 dependents, including `formatMsdSlash`'s command-style decision, are deliberately untouched; only `withProjectRoot`'s reported `agent_runtime` consumes the detection rung. Sources: `src/host-runtime-detection.cts` → `msd-core/bin/lib/host-runtime-detection.cjs`; the `resolveExplicitRuntime` seam it composes lives in `src/runtime-slash.cts`. See ADR-2313. ### Host-Integration Interface -Pure, additive, no-I/O Module owning the versioned, negotiated contract over the six host-integration interface points (command, dispatch, model, hooks, state, artifact) — ADR-1239 Phase A. Extends the ADR-1016 runtime descriptor with nine closed-vocabulary axes carried under `capability.json` `runtime.hostIntegration`: `embeddingMode` (`imperative|declarative`), `commandSurface` (`slash-file|slash-programmatic|slash-toml|palette|prose-only`), `dispatch` (`{namedDispatch,nested,maxDepth,background,backgroundDispatch,subagentToolkit,isolation}`), `modelMode` (`active|passive`), `hookBus` (`host|engine|none`), `stateIO` (`filesystem|sandboxed-storage|session-log-append`), `transport` (`mcp|native-extension`), `runtime` (`node|bun|sandboxed-web|python|go|rust|electron|other`), `effortSurface` (`argv|none` — how reasoning effort reaches the host; ADR-1239 amendment #2481, the first axis whose consumer is an invocation-time argument rather than an install-time artifact). `dispatch.isolation` (`harness-worktree|orchestrator-worktree|none` — how a host isolates concurrent same-wave executors; ADR-1239 Codex-binding amendment #2584; consumed by the phase scheduler since #2584 Phase 3 and, since #2652, by every single-agent dispatch site — `quick.md`, `diagnose-issues.md`, `execute-plan.md` — which resolve it through the canonical `msd-core/references/dispatch-isolation-gate.md` rather than branching on a runtime id; and, since #2486, by the runtime-neutral diagnostics — `/msd:settings` gates its Worktrees recommendation and `/msd:health` raises W025 from this axis, read through the sentinel-free `inspect-dispatch-isolation` verb). `resolveOrchestratorExec(orchestratorExec, cwd) → { ok:true, command, args, cwd } | { ok:false, reason }` (#2584 Phase 2, pure, no I/O — resolves the `runtime.orchestratorExec` descriptor field, a sibling of `runtime.hostBehaviors` in `capability.json` carrying `{command, args?, cwdFlag?}`, into the concrete argv/cwd a process-spawn primitive would use for a `dispatch.isolation: orchestrator-worktree` host; appends `[cwdFlag, cwd]` to `args` when `cwdFlag` is a non-empty string, e.g. codex `exec --cd `, opencode `run --dir `, kimi `--work-dir `; when `cwdFlag` is `null`/absent — kimi-code's process-cwd case — no flag is appended and `cwd` alone is returned for the caller to bind via the subprocess's own working-directory option; fail-closed `missing_command`/`invalid_cwd`/`invalid_args`/`invalid_cwd_flag`; CONSUMED since #2584 Phase 3 — `routeDispatchIsolation` resolves it into the `exec` field of `msd_run query dispatch-isolation --json`, and `msd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md` process-spawns that `command`/`args`/`cwd`; #2652 adds a second consumer, `_negotiatedDispatchIsolation`, which probes it at install time against a placeholder target to decide whether an `orchestrator-worktree` declaration actually resolves). Interface: `negotiateHostCapabilities(host, engine?) → { protocolVersion, effective, points, warnings }` enforcing the trust-boundary invariant `effective ⊆ host-declared ∩ engine-known` (never augment with an undeclared or unknown/future-`protocolVersion` value — fail-closed via the most-restrictive-known `SAFE_DEFAULTS`); `degradationFor(point, axes) → { level, fallback }` (a pure Full/Degraded/Absent ladder table, never throws); `profileOf(axes) → 'programmatic-cli'|'declarative-cli'|'ide'|null`; plus `PROTOCOL_VERSION` (integer, starts at 1 — distinct from the package `version`/`engines.msd` semver), `HOST_INTEGRATION_AXES` (the frozen closed vocabulary, single source of truth), `PROFILE_BASELINES`, and `shouldFlattenDispatch(dispatch) → boolean` (ADR-1239 Phase B / #1708 — graduates the #853 rule: returns `true` = run the orchestrator inline UNLESS the host is documented to background a nesting-capable orchestrator (`background === true && backgroundDispatch === true`); fail-closed to inline; exposed to the plan/execute workflows via the `msd_run query dispatch-should-flatten --raw` CLI, which replaced the former scattered `RUNTIME === 'codex'` prose check). The runtime-descriptor validator (`msd-core/bin/lib/capability-validator.cjs` `validateRuntimeBody`) mirrors the closed vocabulary inline (exported as `_HOST_INTEGRATION_VOCAB`) and is kept in lock-step by the parity guard `tests/host-integration-validator-parity.test.cjs`. Orthogonal axes (resolved explicitly per ADR-1239 Phase A): `commandStyle` (MSD emission style, retained) vs `commandSurface` (host surface type); `hookEvents` dialect vs `hookBus` ownership (a host with `hooksSurface:none` may still be `hookBus:host` — e.g. opencode); `runtimeCompat` (feature→host) vs these negotiated runtime→engine axes. Phase A defined the interface; Phase B (#1679) wires it incrementally — `destSubpath` write-confinement (#1704) and the typed documentation-sourced #853 dispatch-flatten (#1708, the first consumer of a negotiated `dispatch` axis); adapters/MCP/host-bindings remain Phases C–E. Source of truth: `msd-core/bin/lib/host-integration.cjs` (generated from `src/host-integration.cts`). See ADR-1239 and ADR-1016. +Pure, additive, no-I/O Module owning the versioned, negotiated contract over the six host-integration interface points (command, dispatch, model, hooks, state, artifact) — ADR-1239 Phase A. Extends the ADR-1016 runtime descriptor with nine closed-vocabulary axes carried under `capability.json` `runtime.hostIntegration`: `embeddingMode` (`imperative|declarative`), `commandSurface` (`slash-file|slash-programmatic|slash-toml|palette|prose-only`), `dispatch` (`{namedDispatch,nested,maxDepth,background,backgroundDispatch,subagentToolkit,isolation}`), `modelMode` (`active|passive`), `hookBus` (`host|engine|none`), `stateIO` (`filesystem|sandboxed-storage|session-log-append`), `transport` (`mcp|native-extension`), `runtime` (`node|bun|sandboxed-web|python|go|rust|electron|other`), `effortSurface` (`argv|none` — how reasoning effort reaches the host; ADR-1239 amendment #2481, the first axis whose consumer is an invocation-time argument rather than an install-time artifact). `dispatch.isolation` (`harness-worktree|orchestrator-worktree|none` — how a host isolates concurrent same-wave executors; ADR-1239 Codex-binding amendment #2584; consumed by the phase scheduler since #2584 Phase 3 and, since #2652, by every single-agent dispatch site — `quick.md`, `diagnose-issues.md`, `execute-plan.md` — which resolve it through the canonical `msd-core/references/dispatch-isolation-gate.md` rather than branching on a runtime id; and, since #2486, by the runtime-neutral diagnostics — `/msd:settings` gates its Worktrees recommendation and `/msd:health` raises W025 from this axis, read through the sentinel-free `inspect-dispatch-isolation` verb). `resolveOrchestratorExec(orchestratorExec, cwd) → { ok:true, command, args, cwd } | { ok:false, reason }` (#2584 Phase 2, pure, no I/O — resolves the `runtime.orchestratorExec` descriptor field, a sibling of `runtime.hostBehaviors` in `capability.json` carrying `{command, args?, cwdFlag?}`, into the concrete argv/cwd a process-spawn primitive would use for a `dispatch.isolation: orchestrator-worktree` host; appends `[cwdFlag, cwd]` to `args` when `cwdFlag` is a non-empty string, e.g. codex `exec --cd `, opencode `run --dir `; when `cwdFlag` is `null`/absent — the process-cwd case — no flag is appended and `cwd` alone is returned for the caller to bind via the subprocess's own working-directory option; fail-closed `missing_command`/`invalid_cwd`/`invalid_args`/`invalid_cwd_flag`; CONSUMED since #2584 Phase 3 — `routeDispatchIsolation` resolves it into the `exec` field of `msd_run query dispatch-isolation --json`, and `msd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md` process-spawns that `command`/`args`/`cwd`; #2652 adds a second consumer, `_negotiatedDispatchIsolation`, which probes it at install time against a placeholder target to decide whether an `orchestrator-worktree` declaration actually resolves). Interface: `negotiateHostCapabilities(host, engine?) → { protocolVersion, effective, points, warnings }` enforcing the trust-boundary invariant `effective ⊆ host-declared ∩ engine-known` (never augment with an undeclared or unknown/future-`protocolVersion` value — fail-closed via the most-restrictive-known `SAFE_DEFAULTS`); `degradationFor(point, axes) → { level, fallback }` (a pure Full/Degraded/Absent ladder table, never throws); `profileOf(axes) → 'programmatic-cli'|'declarative-cli'|'ide'|null`; plus `PROTOCOL_VERSION` (integer, starts at 1 — distinct from the package `version`/`engines.msd` semver), `HOST_INTEGRATION_AXES` (the frozen closed vocabulary, single source of truth), `PROFILE_BASELINES`, and `shouldFlattenDispatch(dispatch) → boolean` (ADR-1239 Phase B / #1708 — graduates the #853 rule: returns `true` = run the orchestrator inline UNLESS the host is documented to background a nesting-capable orchestrator (`background === true && backgroundDispatch === true`); fail-closed to inline; exposed to the plan/execute workflows via the `msd_run query dispatch-should-flatten --raw` CLI, which replaced the former scattered `RUNTIME === 'codex'` prose check). The runtime-descriptor validator (`msd-core/bin/lib/capability-validator.cjs` `validateRuntimeBody`) mirrors the closed vocabulary inline (exported as `_HOST_INTEGRATION_VOCAB`) and is kept in lock-step by the parity guard `tests/host-integration-validator-parity.test.cjs`. Orthogonal axes (resolved explicitly per ADR-1239 Phase A): `commandStyle` (MSD emission style, retained) vs `commandSurface` (host surface type); `hookEvents` dialect vs `hookBus` ownership (a host with `hooksSurface:none` may still be `hookBus:host` — e.g. opencode); `runtimeCompat` (feature→host) vs these negotiated runtime→engine axes. Phase A defined the interface; Phase B (#1679) wires it incrementally — `destSubpath` write-confinement (#1704) and the typed documentation-sourced #853 dispatch-flatten (#1708, the first consumer of a negotiated `dispatch` axis); adapters/MCP/host-bindings remain Phases C–E. Source of truth: `msd-core/bin/lib/host-integration.cjs` (generated from `src/host-integration.cts`). See ADR-1239 and ADR-1016. ### Statusline Host-integration hook (`hooks/msd-statusline.js`) that renders the session status line: model name, context-window meter, workspace directory, and the MSD-state segment (`formatMsdState()` projecting `.planning/` STATE.md). `readMsdState()` is workstream-aware (#2850): when the walk-up finds no flat `.planning/STATE.md` but lands on a `.planning/workstreams/` directory, it resolves the active workstream via `resolveActiveWorkstream` (`active-workstream-store.cts`), called with an empty args array — only its env>store precedence applies for this caller, since the CLI leg is inert without argv — and `planningPaths`/`listAvailableWorkstreams` (`planning-workspace.cts`) for path/mode resolution, the same seams every other workstream-aware command uses, and reads that workstream's `STATE.md` instead. The store tier is `peekActiveWorkstream`, a read-only sibling of `getActiveWorkstream` that never deletes a stale/invalid pointer file — a renderer invoked on every prompt must never mutate persistent state as a side effect of drawing a screen (`getActiveWorkstream`'s self-heal is correct for a command, not a render). When workstream mode is detected but nothing resolves, it returns a `{noActiveWorkstream:true}` sentinel that `formatMsdState`/`formatMsdStateCompact` render as `"no active workstream"` — observable, never silent emptiness. Opt-in segments are gated by `.planning/config.json` keys (`statusline.show_last_command`, `statusline.context_position`, plus the approved `statusline.show_context_tokens`, `statusline.state_format`, `statusline.show_git` and `statusline.show_state_freshness`), each registered across `msd-core/bin/shared/config-schema.manifest.json` + `src/config.cts` + the `loadConfig` whitelist + `docs/CONFIGURATION.md`. The compact MSD-state format consumes the canonical status vocabulary from `normalizeStateStatus()` (STATE.md Document Module) rather than a parallel keyword list. Config resolution for every `statusline.*` key is centralized in `resolveStatuslineOptions(cfg)` — the two entry points (`runStatusline()`, the stdin path, and `renderStatusline(data)`, the test-facing renderer) previously duplicated it byte-for-byte, and a single resolver is what keeps a newly-added key from reaching only one of them (#2734). The opt-in **STATE.md freshness marker** (`statusline.show_state_freshness`, #2734) renders `state ~N commits back` in both renderers when STATE.md's `state_head` stamp is at least `STATE_HEAD_ADVISORY_COMMITS` (20) commits behind HEAD — the same threshold `validate.health`'s W024 uses, not `> 0`, because `commit_docs: true` advances HEAD by one on every STATE sync and a `> 0` threshold would alarm permanently on a fresh project. It follows the git segment's impure-reader → pure-IR → pure-formatter shape (`readStateHeadCommits` → `deriveStateFreshness` → `formatStateFreshness`), spends exactly one bounded `git rev-list --left-right --count` per render (ancestry and distance in one spawn) and none when disabled, and mirrors `src/state.cts`'s hash fence and `projectOwnsItsRepo`/`sub_repos` degradation guards hook-side rather than requiring `state.cjs` on the per-render path; `tests/msd-statusline.test.cjs` binds the two copies by **behavioral** differential parity against `readStateHeadFreshness`, not a source comparison. Every degradation yields the tri-state *unknown* (marker absent), never a "fresh" claim the project cannot substantiate. **Data-source boundary (ADR-2164):** the statusline sources only local, read-only data — it refines the stdin payload Claude Code already sends and may add a new *local* source (e.g. `git`), but does not read credentials or call external/network APIs for data; account/usage/platform-level state is out of scope. @@ -248,13 +248,13 @@ Host-integration hook (`hooks/msd-statusline.js`) that renders the session statu Module owning the layout-driven runtime-artifact install pipeline — `installRuntimeArtifacts`, `uninstallRuntimeArtifacts`, `installOpencodeFamilySkills`, and their cluster helpers (`_copyStaged`, `_snapshotDir`/`_restoreDir`, legacy-migration + MSD-entry pruning, user-artifact preserve/restore). Extracted from the 12k-line `bin/install.js` (ADR-1239 Phase B, #1679) so adapters import the engine instead of reaching into the installer. Commit-attribution resolution stays in `bin/install.js` and is injected via a `resolveAttribution` parameter (the engine takes no config I/O). Source: `src/install-engine.cts` -> `msd-core/bin/lib/install-engine.cjs`. ### CommonJS Marker Module -Module owning the `{"type":"commonjs"}` module-type marker MSD writes beside its own staged `.js` files (#2544) — `markerPathFor`, `classifyMarker`, `ensureCommonJsMarker`, `removeCommonJsMarker`, and the `COMMONJS_MARKER` / `COMMONJS_MARKER_CONTENT` constants. Exists so two rules are enforced in exactly one place: **write only where MSD owns the contents** (`hooks/`, and the `nativePlugin.dir` for runtimes declaring one — never the shared runtime config root, which on OpenCode and Kilo is documented, user-writable territory for local-plugin npm dependencies), and **never overwrite a file MSD did not write**. Ownership is decided by exact content match, the same predicate the uninstall path always used; `classifyMarker` is the shared seam behind both the write and the remove path so install and uninstall cannot drift apart again. Fails **closed** throughout: `lstat` (not `existsSync`) so a dangling symlink is never classified `absent`; anything that is not a regular file is `foreign`; a present-but-unreadable file is `foreign`, never downgraded to the permissive answer; the write uses `flag:'wx'` so anything appearing between classify and write yields `preserved-foreign` rather than a follow-or-overwrite. `ensureCommonJsMarker` **never throws** — an unwritable directory returns `failed` and both call sites warn and continue, matching the best-effort posture of every other marker interaction. The stale pre-#2544 config-root marker is retired by `src/installer-migrations/007-retire-config-root-commonjs-marker.cts` (and, for kimi's out-of-configDir root, by `bin/install.js` directly). Source: `src/commonjs-marker.cts` -> `msd-core/bin/lib/commonjs-marker.cjs`. Test anchor: `tests/commonjs-marker.test.cjs`. +Module owning the `{"type":"commonjs"}` module-type marker MSD writes beside its own staged `.js` files (#2544) — `markerPathFor`, `classifyMarker`, `ensureCommonJsMarker`, `removeCommonJsMarker`, and the `COMMONJS_MARKER` / `COMMONJS_MARKER_CONTENT` constants. Exists so two rules are enforced in exactly one place: **write only where MSD owns the contents** (`hooks/`, and the `nativePlugin.dir` for runtimes declaring one — never the shared runtime config root, which on OpenCode and Kilo is documented, user-writable territory for local-plugin npm dependencies), and **never overwrite a file MSD did not write**. Ownership is decided by exact content match, the same predicate the uninstall path always used; `classifyMarker` is the shared seam behind both the write and the remove path so install and uninstall cannot drift apart again. Fails **closed** throughout: `lstat` (not `existsSync`) so a dangling symlink is never classified `absent`; anything that is not a regular file is `foreign`; a present-but-unreadable file is `foreign`, never downgraded to the permissive answer; the write uses `flag:'wx'` so anything appearing between classify and write yields `preserved-foreign` rather than a follow-or-overwrite. `ensureCommonJsMarker` **never throws** — an unwritable directory returns `failed` and both call sites warn and continue, matching the best-effort posture of every other marker interaction. The stale pre-#2544 config-root marker is retired by `src/installer-migrations/007-retire-config-root-commonjs-marker.cts`. Source: `src/commonjs-marker.cts` -> `msd-core/bin/lib/commonjs-marker.cjs`. Test anchor: `tests/commonjs-marker.test.cjs`. ### Installer Migration Authoring Guard Module Module owning validation for Installer Migration Module records and planned actions. It enforces migration metadata, explicit install scopes, ownership evidence for destructive/config actions, and runtime contract citations for runtime config rewrites before a migration can enter planning or apply. ### Installer Module -Primary installer for all runtimes. Single production file: `bin/install.js` (hand-authored JS — it is NOT generated from `src/*.cts`; ADR-1508 keeps it hand-authored deliberately, and no `npm run build` step emits it). Exports: `install(isGlobal, runtime[, configDir])` → typed result `{ runtime, configDir, settingsPath, settings, statuslineCommand, updateBannerCommand, configuredEntrypoints }`; `uninstall(isGlobal, runtime[, configDir])`; `installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile)`; `uninstallRuntimeArtifacts(runtime, configDir, scope)`; `writeManifest(configDir, runtime)`. Runtime enum: `allRuntimes` (6 values: claude, antigravity, codex, cursor, opencode, zcode). Directory helpers: `getDirName(runtime)` → local dir name; `getConfigDirFromHome(runtime, isGlobal)` → shell-quoted path fragment. Per-runtime global config-dir resolution is delegated to `msd-core/bin/lib/runtime-homes.cjs:getGlobalConfigDir(runtime[, explicitDir])` — the canonical, env-var–aware projection (`explicitDir` override + opencode/kilo `*_CONFIG` file-path precedence); the legacy in-installer `getGlobalDir`/`getOpencodeGlobalDir`/`getKiloGlobalDir` were retired into it (#56). The same module exposes `detectAntigravityDirAmbiguity(opts)` — a side-effect-free probe reporting whether multiple `~/.gemini/antigravity{,-ide,-cli}` dirs coexist and which one MSD's `msd-core/VERSION` marker (the `dot-home-nested` `probeExists`) resolves to, for installer / `/msd-update` operator guidance when a pre-#217 install landed in the wrong sibling dir (#1441). Runtime-specific helpers: `resolveKiloConfigPath(configDir)`, `configureKiloPermissions(isGlobal[, explicitDir])`. Claude-specific permission helpers: `mergeClaudePermissions(settings)` — non-destructively appends MSD-owned allow entries (see `MSD_CLAUDE_ALLOW_PERMISSIONS`) to a Claude Code settings object and filters out the retired legacy forms (`MSD_CLAUDE_LEGACY_ALLOW_PERMISSIONS` #2278; `MSD_CLAUDE_LEGACY_DENY_PERMISSIONS` #4221 — the `Read(.env*)` deny rules are retired in favor of the managed `msd-secret-read-guard.js` hook, and an emptied `deny` array is deleted); called from `finishInstall` for `runtime === 'claude'` only; uninstall removes exactly these entries (#768). Layout-driven artifact copy/removal delegates to `msd-core/bin/lib/runtime-artifact-layout.cjs:resolveRuntimeArtifactLayout` (throws `TypeError` for unknown runtimes). Five runtimes with non-recursive skill loaders (cline, qwen, hermes, augment, trae) use a nested router layout: 6 `msd-ns-*` router bundles emitted as top-level skills, with concrete skills nested at `/skills//SKILL.md` (hermes prefix='': `skills/msd/ns-*/…`). claude (reverted from nested per #924 — the Skill tool errors on unrouted names) and antigravity (one-level scan, but concrete skills must be top-level discoverable) plus the remaining skills-runtimes (cursor, codex, copilot, windsurf, codebuddy, opencode, kilo) use the flat `skills/msd-/` layout. See Skill Surface Budget Module and Runtime Artifact Layout Module. `finishInstall` calls `assertConfiguredEntrypoints` (Runtime Hooks Surface Module, dedup'd by `(configPath, scriptPath)` first) before its own `writeSettings` — a failing settings-json-surface install/update never persists that write. `installAllRuntimes`'s `finalize()` runs the same check once over the aggregate set before its `printSummaries()` loop calls `finishInstall` per runtime, so the per-runtime slice is checked twice; the second pass is a dozen-odd `statSync`/`accessSync` calls and is left unconditional rather than gated on a caller-supplied bypass flag. Codex/Cursor/Windsurf/Kimi/Cline writers persist their own hooks.json/config.toml/`.clinerules/hooks/PreToolUse` directly inside `install()`, ahead of `installAllRuntimes`'s aggregate `assertConfiguredEntrypoints` call, so a validation failure there is reported (and blocks the completion message) before that file is reverted where a rollback exists. Codex's `install()` result exposes two rollbacks: `rollbackInstallerMigrations` — like every other runtime's — is the installer-migrations-only closure the name describes, and the Codex-only `rollbackPreInstallSnapshot` binds `restoreCodexSnapshot` (#3245's full pre-install snapshot restore — config.toml, hooks.json, skills/, agents/, VERSION). `installAllRuntimes`'s `rollbackFinalizedInstallerMigrations` picks `rollbackPreInstallSnapshot` only when the triggering error is tagged `configuredEntrypointValidation` (set by `assertConfiguredEntrypoints`); any other finalize-stage exception gets `rollbackInstallerMigrations`, so an unrelated sibling failure (e.g. EACCES on another runtime's permission write) cannot un-install a Codex install that already succeeded. The discriminator is the error's KIND, not which runtime owns the bad entrypoint: the aggregate gate is all-or-nothing, so ANY runtime's invalid entrypoint reverts the Codex snapshot — including when Codex's own entrypoints were fine and its "Done!" summary already printed (#4249). Cursor/Windsurf/Kimi/Cline have no equivalent snapshot, so their result binds only the narrow rollback and a validation failure leaves their just-written file unrevertable (#4249). `bannerOpts.configuredEntrypoints` is the only source `finishInstall` validates — a caller other than `installAllRuntimes` that omits it gets no entrypoint validation at all. +Primary installer for all runtimes. Single production file: `bin/install.js` (hand-authored JS — it is NOT generated from `src/*.cts`; ADR-1508 keeps it hand-authored deliberately, and no `npm run build` step emits it). Exports: `install(isGlobal, runtime[, configDir])` → typed result `{ runtime, configDir, settingsPath, settings, statuslineCommand, updateBannerCommand, configuredEntrypoints }`; `uninstall(isGlobal, runtime[, configDir])`; `installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile)`; `uninstallRuntimeArtifacts(runtime, configDir, scope)`; `writeManifest(configDir, runtime)`. Runtime enum: `allRuntimes` (6 values: claude, antigravity, codex, cursor, opencode, zcode). Directory helpers: `getDirName(runtime)` → local dir name; `getConfigDirFromHome(runtime, isGlobal)` → shell-quoted path fragment. Per-runtime global config-dir resolution is delegated to `msd-core/bin/lib/runtime-homes.cjs:getGlobalConfigDir(runtime[, explicitDir])` — the canonical, env-var–aware projection (`explicitDir` override + opencode/kilo `*_CONFIG` file-path precedence); the legacy in-installer `getGlobalDir`/`getOpencodeGlobalDir`/`getKiloGlobalDir` were retired into it (#56). The same module exposes `detectAntigravityDirAmbiguity(opts)` — a side-effect-free probe reporting whether multiple `~/.gemini/antigravity{,-ide,-cli}` dirs coexist and which one MSD's `msd-core/VERSION` marker (the `dot-home-nested` `probeExists`) resolves to, for installer / `/msd-update` operator guidance when a pre-#217 install landed in the wrong sibling dir (#1441). Runtime-specific helpers: `resolveKiloConfigPath(configDir)`, `configureKiloPermissions(isGlobal[, explicitDir])`. Claude-specific permission helpers: `mergeClaudePermissions(settings)` — non-destructively appends MSD-owned allow entries (see `MSD_CLAUDE_ALLOW_PERMISSIONS`) to a Claude Code settings object and filters out the retired legacy forms (`MSD_CLAUDE_LEGACY_ALLOW_PERMISSIONS` #2278; `MSD_CLAUDE_LEGACY_DENY_PERMISSIONS` #4221 — the `Read(.env*)` deny rules are retired in favor of the managed `msd-secret-read-guard.js` hook, and an emptied `deny` array is deleted); called from `finishInstall` for `runtime === 'claude'` only; uninstall removes exactly these entries (#768). Layout-driven artifact copy/removal delegates to `msd-core/bin/lib/runtime-artifact-layout.cjs:resolveRuntimeArtifactLayout` (throws `TypeError` for unknown runtimes). Five runtimes with non-recursive skill loaders (cline, qwen, hermes, augment, trae) use a nested router layout: 6 `msd-ns-*` router bundles emitted as top-level skills, with concrete skills nested at `/skills//SKILL.md` (hermes prefix='': `skills/msd/ns-*/…`). claude (reverted from nested per #924 — the Skill tool errors on unrouted names) and antigravity (one-level scan, but concrete skills must be top-level discoverable) plus the remaining skills-runtimes (cursor, codex, copilot, windsurf, codebuddy, opencode, kilo) use the flat `skills/msd-/` layout. See Skill Surface Budget Module and Runtime Artifact Layout Module. `finishInstall` calls `assertConfiguredEntrypoints` (Runtime Hooks Surface Module, dedup'd by `(configPath, scriptPath)` first) before its own `writeSettings` — a failing settings-json-surface install/update never persists that write. `installAllRuntimes`'s `finalize()` runs the same check once over the aggregate set before its `printSummaries()` loop calls `finishInstall` per runtime, so the per-runtime slice is checked twice; the second pass is a dozen-odd `statSync`/`accessSync` calls and is left unconditional rather than gated on a caller-supplied bypass flag. Codex/Cursor/Windsurf/Cline writers persist their own hooks.json/config.toml/`.clinerules/hooks/PreToolUse` directly inside `install()`, ahead of `installAllRuntimes`'s aggregate `assertConfiguredEntrypoints` call, so a validation failure there is reported (and blocks the completion message) before that file is reverted where a rollback exists. Codex's `install()` result exposes two rollbacks: `rollbackInstallerMigrations` — like every other runtime's — is the installer-migrations-only closure the name describes, and the Codex-only `rollbackPreInstallSnapshot` binds `restoreCodexSnapshot` (#3245's full pre-install snapshot restore — config.toml, hooks.json, skills/, agents/, VERSION). `installAllRuntimes`'s `rollbackFinalizedInstallerMigrations` picks `rollbackPreInstallSnapshot` only when the triggering error is tagged `configuredEntrypointValidation` (set by `assertConfiguredEntrypoints`); any other finalize-stage exception gets `rollbackInstallerMigrations`, so an unrelated sibling failure (e.g. EACCES on another runtime's permission write) cannot un-install a Codex install that already succeeded. The discriminator is the error's KIND, not which runtime owns the bad entrypoint: the aggregate gate is all-or-nothing, so ANY runtime's invalid entrypoint reverts the Codex snapshot — including when Codex's own entrypoints were fine and its "Done!" summary already printed (#4249). Cursor/Windsurf/Cline have no equivalent snapshot, so their result binds only the narrow rollback and a validation failure leaves their just-written file unrevertable (#4249). `bannerOpts.configuredEntrypoints` is the only source `finishInstall` validates — a caller other than `installAllRuntimes` that omits it gets no entrypoint validation at all. ### I/O Module Module owning the tool's CLI I/O primitives: `output()` result emission (with large-payload temp-file spillover via `MSD_TEMP_DIR`/`ensureMsdTempDir`/`reapStaleTempFiles`), `error()` stderr emission with exit-code mapping, and the JSON-error-mode toggle (`setJsonErrorMode`/`getJsonErrorMode`, `ERROR_REASON`). **Degraded result vs fault (ADR-2980, #2980):** the two emitters are a deliberate two-channel failure contract, not a drift. A **fault** is `error(message, reason)` — stderr, exit **1**, structured `{ok:false,reason,message}` envelope under `--json-errors`. A **degraded result** is `output({ error: … })` — stdout, exit **0**, `--json-errors` does not apply — and means the command ran to completion and is reporting a condition (absent artifact, and in practice also missing-argument and unusable-input cases) through its result; a caller detects it by inspecting the payload, never by exit code. Ratified across **60 sites in 9 modules** (`state` 25, `verify` 8, `workstream` 7, `frontmatter` 6, `commands` 5, `template` 3, `gsd2-import` 2, `phase` 2, `roadmap` 2) because normalizing them to exit 1 is a Hyrum's Law break over a CRITICAL radius (`get_impact(cmdStateSnapshot)`; `output` has 170 direct callers). #2966/#2980 record "42 sites" — that counts only literals whose FIRST key is `error` (the `output\(\{\s*error:` regex); 18 more put another key first (`{found:false, error}`) and are identical in contract, so 60 is the population and 42 is a subset. New code prefers the fault path or a named-field result (`{updated:false, reason}`), not a 61st site. Known cost carried by the decision: the exit code does not distinguish absent from unusable, which is ADR-1411's "corrupt is not absent" open edge. Docs: `docs/json-errors.md` → "Degraded results vs faults". Extracted from the Core module per ADR-857 rollout phase 1 (#859) so feature modules (`graphify`, `intel`, `audit`, `profile-pipeline`) depend on a small I/O seam instead of the core god-module; the `core.cjs` re-export spine was retired in epic #1267, so callers import this leaf directly. Source of truth: `msd-core/bin/lib/io.cjs` (generated from `src/io.cts`). @@ -320,22 +320,22 @@ Shared, pure, no-I/O seam owning priority-ordered composition of content fragmen ### Workflow Fragments Module -Pure, no-I/O seam owning in-file `` / `` marker parsing and composition for MSD workflow markdown (ADR-1671 Decision item 1 + migration step 4 + open questions 1 & 2; epic #1671 Phase 3, #2930). `parseWorkflowSections` partitions a document into explicit (marked) and gap (unmarked, `explicit: false`) sections in document order — a marker line is removed in full (text + its own terminator), so an unmarked workflow (88 of 89 today) parses to exactly one implicit gap fragment and round-trips byte-identical. `toFragments` maps sections to Context Composer Module fragments, every one `{kind: 'verbatim'}` — non-lossiness in this phase is a structural guarantee of the strategy set, never a large-budget trick. `composeWorkflow` is the emission entry point: parse → `toFragments` → `composeWithinBudget` → `renderFragments`, run BEFORE the per-runtime converters so a marker attribute is stripped before any path-rewrite regex can reach it. **The grammar is deliberately CLOSED** (Greenspun's Tenth Rule): `when=` takes exactly one atom from the frozen `WHEN_VOCABULARY` — **29 atoms** (4 at #2930, widened 4→14 by #2992, 14→19 by #2993, 19→29 by #2994, each requiring a coordinated ADR-1671 amendment) — with no boolean operators, negation, or nesting; an unknown `when=` value throws rather than being silently dropped, and widening the vocabulary requires an ADR amendment, not an organic edit. Two gates govern admission: a named consuming section of at least 400 bytes, and a fact the init seam demonstrably computes at a real entry point — an atom failing the second evaluates `false` forever, so its marker looks like working gating while silently disabling itself. Any condition that cannot reduce to a single boolean is not an atom: a compound is resolved upstream in the FACT by the init seam (`state:chunked-mode`), never in the grammar, and negation is expressed as its own positively-phrased atom (`state:flat-mode`), never as an operator. Fence and HTML-comment interleaving is scanned in one left-to-right pass with two mutually exclusive states, reusing the discipline from the Context Predicates module's fence/comment scan (the two-pass design that caused #2928's silent-skip-to-EOF defect). `when=` was parsed and validated but not acted on at #2930; applicability selection shipped in Phase 5 (#2932, Section Manifest Module) and the rollout is complete across **15 workflows / 37 sections** (pilot `execute-phase.md` at #2930 — retargeted from `plan-phase.md`, which sat 36 B under the ADR-857 `PRE_PHASE6` gate and could not absorb marker overhead until #2993 fragmentized it; the remaining 13 LARGE/XL workflows at #2994). **Emission scope covers `agents/` as of #2995**, so a marker in an agent file is stripped at emit rather than shipped verbatim into every runtime — composition runs at two call sites (`stageAgentsForRuntimeWithConverter`, with `agentsKind`/`kimiAgentsKind` routed through it, and `bin/install.js`'s inline agent loop) plus `installCodexConfig`'s per-agent TOML read, always BEFORE any path rewrite. **`when=` GATING remains workflow-only**: `section-manifest.json` is keyed `{workflows: …}` and `gen-section-manifest.cjs` scans only `msd-core/workflows/*.md`, so an agent atom has no consumer and would fail admission gate (2); agents are size-managed by extraction to `msd-core/references/` per `DEFECT.AGENT-FILE-SIZE-CAP-BREACH` instead. Source of truth: `msd-core/bin/lib/workflow-fragments.cjs` (generated from `src/workflow-fragments.cts`). Test anchors: `tests/workflow-fragments.test.cjs`, `tests/workflow-fragments.property.test.cjs`, `tests/workflow-fragments-emission.install.test.cjs`. +Pure, no-I/O seam owning in-file `` / `` marker parsing and composition for MSD workflow markdown (ADR-1671 Decision item 1 + migration step 4 + open questions 1 & 2; epic #1671 Phase 3, #2930). `parseWorkflowSections` partitions a document into explicit (marked) and gap (unmarked, `explicit: false`) sections in document order — a marker line is removed in full (text + its own terminator), so an unmarked workflow (88 of 89 today) parses to exactly one implicit gap fragment and round-trips byte-identical. `toFragments` maps sections to Context Composer Module fragments, every one `{kind: 'verbatim'}` — non-lossiness in this phase is a structural guarantee of the strategy set, never a large-budget trick. `composeWorkflow` is the emission entry point: parse → `toFragments` → `composeWithinBudget` → `renderFragments`, run BEFORE the per-runtime converters so a marker attribute is stripped before any path-rewrite regex can reach it. **The grammar is deliberately CLOSED** (Greenspun's Tenth Rule): `when=` takes exactly one atom from the frozen `WHEN_VOCABULARY` — **29 atoms** (4 at #2930, widened 4→14 by #2992, 14→19 by #2993, 19→29 by #2994, each requiring a coordinated ADR-1671 amendment) — with no boolean operators, negation, or nesting; an unknown `when=` value throws rather than being silently dropped, and widening the vocabulary requires an ADR amendment, not an organic edit. Two gates govern admission: a named consuming section of at least 400 bytes, and a fact the init seam demonstrably computes at a real entry point — an atom failing the second evaluates `false` forever, so its marker looks like working gating while silently disabling itself. Any condition that cannot reduce to a single boolean is not an atom: a compound is resolved upstream in the FACT by the init seam (`state:chunked-mode`), never in the grammar, and negation is expressed as its own positively-phrased atom (`state:flat-mode`), never as an operator. Fence and HTML-comment interleaving is scanned in one left-to-right pass with two mutually exclusive states, reusing the discipline from the Context Predicates module's fence/comment scan (the two-pass design that caused #2928's silent-skip-to-EOF defect). `when=` was parsed and validated but not acted on at #2930; applicability selection shipped in Phase 5 (#2932, Section Manifest Module) and the rollout is complete across **15 workflows / 37 sections** (pilot `execute-phase.md` at #2930 — retargeted from `plan-phase.md`, which sat 36 B under the ADR-857 `PRE_PHASE6` gate and could not absorb marker overhead until #2993 fragmentized it; the remaining 13 LARGE/XL workflows at #2994). **Emission scope covers `agents/` as of #2995**, so a marker in an agent file is stripped at emit rather than shipped verbatim into every runtime — composition runs at two call sites (`stageAgentsForRuntimeWithConverter`, with `agentsKind` routed through it, and `bin/install.js`'s inline agent loop) plus `installCodexConfig`'s per-agent TOML read, always BEFORE any path rewrite. **`when=` GATING remains workflow-only**: `section-manifest.json` is keyed `{workflows: …}` and `gen-section-manifest.cjs` scans only `msd-core/workflows/*.md`, so an agent atom has no consumer and would fail admission gate (2); agents are size-managed by extraction to `msd-core/references/` per `DEFECT.AGENT-FILE-SIZE-CAP-BREACH` instead. Source of truth: `msd-core/bin/lib/workflow-fragments.cjs` (generated from `src/workflow-fragments.cts`). Test anchors: `tests/workflow-fragments.test.cjs`, `tests/workflow-fragments.property.test.cjs`, `tests/workflow-fragments-emission.install.test.cjs`. ### Section Manifest Module Pure, no-I/O `when=` evaluator over `InvocationFacts`, mapping a document-order list of parsed `msd:section` sections (Workflow Fragments Module) to an included/excluded partition for one concrete invocation (ADR-1671 Decision items 3 & 4 + migration step 6; epic #1671 Phase 5, #2932). **The evaluator is a LOOKUP, not a parser** — `WHEN_PREDICATES` is a total map from each frozen `WHEN_VOCABULARY` entry (imported unchanged from `workflow-fragments.cjs`, never redeclared) to exactly one predicate over `InvocationFacts` — `{flags: ReadonlySet, phaseNumber: string|null, hasPriorPhases: boolean}` plus optional already-resolved booleans (`needsCodebaseMap`, `phaseMvpMode`, `worktreesEnabled`, `chunkedMode`, `uiPhaseActive`, `fallowEnabled`, …), every field a plain value the caller computed before `selectSections` runs; `flags` is a `ReadonlySet` rather than a plain object because `.has()` carries no prototype hazard — and note `parseNamedArgs` NEVER returns `undefined` for an absent flag (booleans come back `false`, value keys `null`), so "present in the options record" is not token presence — as of ADR-3473 §8.4 / #3884, `parseNamedArgs(args, spec)` returns a `Result` (`{ok:true,data} | {ok:false,kind:'InvalidArgs',...}`, spec requiring an explicit `positionals: number|'rest'`), so this predicate now describes `.data`'s fields on the `ok:true` branch, not a bare returned object; the null/`false`-never-`undefined` guarantee on those fields is unchanged. It MUST NOT tokenize, split on operators, or interpret `when=` structure — the moment it parses, the ad-hoc language Greenspun's Tenth Rule warns against has begun. `selectSections(sections, facts)` returns `{included, excluded}` id arrays that together contain every input id exactly once, in the same relative document order, never mutating the input. An unrecognized `when=` value fails closed via a `TypeError` carrying `.reason = REASON.UNKNOWN_WHEN` — never silently excluded — matching the discipline Phase 3 already established for the same vocabulary at parse time. Every predicate treats an absent fact key as falsy without throwing, since the caller (the init CLI seam) may not always populate every field. A coordinated-change guard runs at module load: every `WHEN_VOCABULARY` entry must have exactly one predicate here, so a 5th vocabulary entry added without a matching predicate fails loudly at load time rather than silently falling through to `REASON.UNKNOWN_WHEN` only at run time. Selection output is generated ahead of time into the committed `msd-core/workflows/section-manifest.json` (`scripts/gen-section-manifest.cjs`, reusing `parseWorkflowSections` unchanged — a second marker parser here would be the `DEFECT.GENERATIVE-FIX` divergence class) rather than derived from markers at run time, because markers are stripped at emit and the installed parent carries no `msd:section` metadata. Source of truth: `msd-core/bin/lib/section-manifest.cjs` (generated from `src/section-manifest.cts`). Test anchors: `tests/section-manifest.test.cjs`, `tests/section-manifest.property.test.cjs`, `tests/gen-section-manifest.test.cjs`. ### Runtime Artifact Layout Module -Module owning the per-runtime mapping from artifact kind to filesystem placement. ADR-3660 defines the typed `kinds` per runtime (`commands`, `agents`, `skills`) with destination subpath, prefix, and stage adapter (with per-runtime converters in `bin/install.js`: `convertClaudeCommandToClaudeSkill`, `…CodexSkill`, `…CopilotSkill`, `…AntigravitySkill`). Owns the per-runtime `nested` skill-bundle decision (#69): a `skillsKind` flag in `src/runtime-artifact-layout.cts` drives whether a runtime receives the nested router layout (6 `msd-ns-*` routers + concrete skills under `/skills//`) or the flat `skills/msd-/` layout; the evidence/doc-link matrix is recorded in a comment above `resolveRuntimeArtifactLayout`. Phase 1 applies this seam to the Runtime Surface Module (`surface.cjs:applySurface`); as of #813, `applySurface` applies the same per-runtime skill-body path rewrites as `installRuntimeArtifacts` for `skills` kinds — re-surfacing no longer overwrites installed SKILL.md bodies with converter-default `~/.claude` paths. Per ADR-1508 / #1511 the former `getInstallExports`/`loadInstallExports` relay (a `MSD_TEST_MODE`-guarded `require('bin/install.js')` by which `surface.cjs` reached `computePathPrefix`/`applyRuntimeContentRewritesInPlace`) was DELETED from this module; content rewriting now lives in the Runtime Artifact Conversion Module and `surface.cjs:applySurface` calls its `rewriteStagedSkillBodies` directly. The resolved `scope` is still carried on the `Layout` object so `applySurface` derives the same `pathPrefix` (global `$HOME` form vs. absolute) as a fresh install. Phase 2 is planned to migrate install/uninstall in `bin/install.js` so all lifecycle sites iterate one shared layout table instead of re-encoding runtime layout logic. This design is intended to remove the #3659 class of omissions. Migrations remain under the Installer Migration Module (ADR-0008). The `.msd-source` marker (#1477) is a two-party provisioning contract that lets source resolution succeed on the Claude global skills layout, which ships `msd-core/{bin,contexts,references,templates,workflows}` but no `commands/msd` source tree for `findInstallSourceRoot` to walk up to: the writer is `bin/install.js`, which writes `/.msd-source` (content: the absolute path to its own `commands/msd`, terminated by a newline) when `runtime === 'claude' && isGlobal`, guarded by `fs.existsSync` so a half-published package never writes a dangling marker; the reader is `findInstallSourceRoot(configDir)`, which prefers the marker over its walk-up but falls through to the walk-up if the marker is absent, dangling, or empty/whitespace-only. #2871 Phase 2 widens the Module from placement to placement **+** trigger resolution: `resolveTriggerSurface(runtime, scopes, { stems, routerStems?, childToRouters?, registry? }) -> TriggerSurface[]` answers "what does a user type" rather than "where does a file land" — a new, pure function alongside `resolveRuntimeArtifactLayout` (untouched, still 7 callers), never a widened signature. Only `commands` and `skills` are trigger-bearing; `agents`/`kimi-agents` are excluded entirely — an agent is invoked through the Agent/Task tool's `subagent_type`, a separate dispatch interface point, never a `/msd-` a user types (ADR-2866 amendment below). Each `TriggerSurface` names its `trigger`, `kind`, `scope`, `destPath` (computed through the SAME `namespacedByDir` branch `_copyStaged` uses), `registration` (`'direct'` | `'via-router'`, the latter naming the owning router's `routerTrigger` for a nested-router runtime's concrete child skill — #69), and `shadowedBy` (the winning sibling entry, or `null`). The winner across scopes and kinds is decided by scope rank first (Install Scope Module's `scopeRank`, consumed not re-derived — global outranks local) then by the runtime's new `runtime.triggerPrecedence` descriptor axis (ordered kind names, highest priority first; default `['skills', 'commands']`, required-with-default so a pre-#2871 `capability.json` keeps validating). `shadowedBy` ships unread this phase — Phase 4 (#2873) is its first consumer. See ADR-3660. +Module owning the per-runtime mapping from artifact kind to filesystem placement. ADR-3660 defines the typed `kinds` per runtime (`commands`, `agents`, `skills`) with destination subpath, prefix, and stage adapter (with per-runtime converters in `bin/install.js`: `convertClaudeCommandToClaudeSkill`, `…CodexSkill`, `…CopilotSkill`, `…AntigravitySkill`). Owns the per-runtime `nested` skill-bundle decision (#69): a `skillsKind` flag in `src/runtime-artifact-layout.cts` drives whether a runtime receives the nested router layout (6 `msd-ns-*` routers + concrete skills under `/skills//`) or the flat `skills/msd-/` layout; the evidence/doc-link matrix is recorded in a comment above `resolveRuntimeArtifactLayout`. Phase 1 applies this seam to the Runtime Surface Module (`surface.cjs:applySurface`); as of #813, `applySurface` applies the same per-runtime skill-body path rewrites as `installRuntimeArtifacts` for `skills` kinds — re-surfacing no longer overwrites installed SKILL.md bodies with converter-default `~/.claude` paths. Per ADR-1508 / #1511 the former `getInstallExports`/`loadInstallExports` relay (a `MSD_TEST_MODE`-guarded `require('bin/install.js')` by which `surface.cjs` reached `computePathPrefix`/`applyRuntimeContentRewritesInPlace`) was DELETED from this module; content rewriting now lives in the Runtime Artifact Conversion Module and `surface.cjs:applySurface` calls its `rewriteStagedSkillBodies` directly. The resolved `scope` is still carried on the `Layout` object so `applySurface` derives the same `pathPrefix` (global `$HOME` form vs. absolute) as a fresh install. Phase 2 is planned to migrate install/uninstall in `bin/install.js` so all lifecycle sites iterate one shared layout table instead of re-encoding runtime layout logic. This design is intended to remove the #3659 class of omissions. Migrations remain under the Installer Migration Module (ADR-0008). The `.msd-source` marker (#1477) is a two-party provisioning contract that lets source resolution succeed on the Claude global skills layout, which ships `msd-core/{bin,contexts,references,templates,workflows}` but no `commands/msd` source tree for `findInstallSourceRoot` to walk up to: the writer is `bin/install.js`, which writes `/.msd-source` (content: the absolute path to its own `commands/msd`, terminated by a newline) when `runtime === 'claude' && isGlobal`, guarded by `fs.existsSync` so a half-published package never writes a dangling marker; the reader is `findInstallSourceRoot(configDir)`, which prefers the marker over its walk-up but falls through to the walk-up if the marker is absent, dangling, or empty/whitespace-only. #2871 Phase 2 widens the Module from placement to placement **+** trigger resolution: `resolveTriggerSurface(runtime, scopes, { stems, routerStems?, childToRouters?, registry? }) -> TriggerSurface[]` answers "what does a user type" rather than "where does a file land" — a new, pure function alongside `resolveRuntimeArtifactLayout` (untouched, still 7 callers), never a widened signature. Only `commands` and `skills` are trigger-bearing; `agents` is excluded entirely — an agent is invoked through the Agent/Task tool's `subagent_type`, a separate dispatch interface point, never a `/msd-` a user types (ADR-2866 amendment below). Each `TriggerSurface` names its `trigger`, `kind`, `scope`, `destPath` (computed through the SAME `namespacedByDir` branch `_copyStaged` uses), `registration` (`'direct'` | `'via-router'`, the latter naming the owning router's `routerTrigger` for a nested-router runtime's concrete child skill — #69), and `shadowedBy` (the winning sibling entry, or `null`). The winner across scopes and kinds is decided by scope rank first (Install Scope Module's `scopeRank`, consumed not re-derived — global outranks local) then by the runtime's new `runtime.triggerPrecedence` descriptor axis (ordered kind names, highest priority first; default `['skills', 'commands']`, required-with-default so a pre-#2871 `capability.json` keeps validating). `shadowedBy` ships unread this phase — Phase 4 (#2873) is its first consumer. See ADR-3660. Issue #4132 keeps `resolveRuntimeArtifactLayout` as the single no-I/O placement resolver. Ordinary kind stage closures lazily select one complete provider shared by every source class the layout requires. Global installs provision canonical raw commands under `msd-core/commands/msd` and agents under `msd-core/agents`, with manifest-owned refresh/removal; installer staging first uses that normal installed/marker selection and privately retries against the executing package only when source resolution fails before creating any staged output. Deployed Runtime Surface staging prefers the complete installation-owned corpus, then a complete independent legacy `.msd-source` compatibility provider, and otherwise fails closed with an install/upgrade diagnostic. Provider independence and installed-corpus integrity are synchronous admission checks over the filesystem state observed during provider selection; same-user concurrent mutation after resolution remains outside this issue's threat model. No layout may mix commands and agents from different providers. ### Runtime Artifact Conversion Module -Sibling Module to Runtime Artifact Layout Module. Owns projection from canonical Claude-authored command/agent/skill markdown into runtime-specific artifact bodies, including converter selection, frontmatter/body normalization, runtime path rewrites, and staged artifact generation. Runtime Artifact Layout remains responsible for filesystem placement (`kind`, destination subpath, prefix, nesting); Runtime Artifact Conversion owns the content Implementation behind that placement seam so install, uninstall/surface parity, and future plugin/package projections stop reaching back through `bin/install.js` for converter functions or `MSD_TEST_MODE`-guarded installer exports. Chosen direction: sibling Module, not an expanded Layout Module, to preserve ADR-3660's narrow placement responsibility while deepening artifact content locality. First slice: relocate only the layout-reached conversion family (`convertClaudeCommandTo*Skill`, converted command-file emitters, `buildKimiAgentArtifacts`) plus the minimal helper closure they need; do not leave helper dependencies in `bin/install.js` because that would preserve the same shallow seam under a new filename. Installer integration decision: `bin/install.js` imports the conversion Module at top level and re-exports the moved names for compatibility; the conversion Module must not import `bin/install.js` or Runtime Artifact Layout, so the dependency direction becomes installer/layout Adapters -> conversion Module, never conversion -> installer. First-slice Interface decision: export the existing compatibility names only; do not introduce a grouped `convertRuntimeArtifact` Interface until after relocation proves byte-for-byte behavior. SHIPPED (ADR-1508): the converter family relocated in #1510 Phase 1 (`getDirName`→runtime-name-policy, `processAttribution` here); #1511 Phase 2 moved the content-rewrite engine here in full — `_applyRuntimeRewrites` (per-runtime switch, injected attribution), the staged-content walkers `applyRuntimeContentRewritesInPlace`/`applyRuntimeContentRewritesForCommandsInPlace`, `computePathPrefix` (private; `_computePathPrefix` for tests), and the deep public seam `rewriteStagedSkillBodies`/`rewriteStagedCommandBodies({runtime,configDir,scope,homedir?,platform?,resolveAttribution?})`. #4377 extends `computePathPrefix` with `projectRelative?` and `localDirName?`: an explicitly opted-in local install may emit the descriptor-derived project-relative prefix, while globals and unsafe/unrepresentable descriptor values retain the absolute fallback. The exported test seams `_relativeIncludesEnabled`, `_projectRelativePrefix`, `_isRelativePathPrefix`, and `_withShellDefaultsPreserved` pin that policy; the last is also the single balanced `${VAR:-...}` masking implementation used by both rewrite paths so nested launcher defaults remain absolute. `bin/install.js` binds these back (single owner, exports preserved); `getCommitAttribution` stays in `bin/install.js` (impure install-time config I/O) and is injected. The `getInstallExports` relay in Runtime Artifact Layout Module was deleted; the dependency direction installer/layout → conversion (never upward) is now enforced. Exception: opencode and kilo path-prefix rewriting is a deliberate `bin/install.js`-owned pre-conversion step (`applyOpencodeFamilyPathPrefix`) per #784, not a violation of the single-owner rule. Source: `msd-core/bin/lib/runtime-artifact-conversion.cjs`. Also exports `resolveVersionFrom(libDir)` — a lazy, defensive MSD-version resolver (installed-tree `msd-core/VERSION` first, then the source/npm `package.json` three dirs up, both validated against the repo's shared semver-prefix shape, degrading to `''` on failure) that replaced a module-load-time `require('../../../package.json')` which crashed on runtimes whose root carries no `package.json` (e.g. Codex) (#1383). #4032 added `appendAgentTools` as step 3 of the ADR-1235 pre-converter pipeline (`stageAgentsForRuntimeWithConverter`, `src/install-profiles.cts` — not this module): appends `readMsdEffectiveAgentTools` grants (Install Model Override Resolver Module) into the canonical agent's `tools:` frontmatter — line-surgical, both inline-comma and YAML block-list forms — before the runtime-specific converter runs, so every converter (including Kilo's `mcp__*`→`{server}_{tool}` permission mapping and ZCode's `mcp__*` omission policy) sees configured grants without a duplicated per-runtime write path. +Sibling Module to Runtime Artifact Layout Module. Owns projection from canonical Claude-authored command/agent/skill markdown into runtime-specific artifact bodies, including converter selection, frontmatter/body normalization, runtime path rewrites, and staged artifact generation. Runtime Artifact Layout remains responsible for filesystem placement (`kind`, destination subpath, prefix, nesting); Runtime Artifact Conversion owns the content Implementation behind that placement seam so install, uninstall/surface parity, and future plugin/package projections stop reaching back through `bin/install.js` for converter functions or `MSD_TEST_MODE`-guarded installer exports. Chosen direction: sibling Module, not an expanded Layout Module, to preserve ADR-3660's narrow placement responsibility while deepening artifact content locality. First slice: relocate only the layout-reached conversion family (`convertClaudeCommandTo*Skill`, converted command-file emitters) plus the minimal helper closure they need; do not leave helper dependencies in `bin/install.js` because that would preserve the same shallow seam under a new filename. Installer integration decision: `bin/install.js` imports the conversion Module at top level and re-exports the moved names for compatibility; the conversion Module must not import `bin/install.js` or Runtime Artifact Layout, so the dependency direction becomes installer/layout Adapters -> conversion Module, never conversion -> installer. First-slice Interface decision: export the existing compatibility names only; do not introduce a grouped `convertRuntimeArtifact` Interface until after relocation proves byte-for-byte behavior. SHIPPED (ADR-1508): the converter family relocated in #1510 Phase 1 (`getDirName`→runtime-name-policy, `processAttribution` here); #1511 Phase 2 moved the content-rewrite engine here in full — `_applyRuntimeRewrites` (per-runtime switch, injected attribution), the staged-content walkers `applyRuntimeContentRewritesInPlace`/`applyRuntimeContentRewritesForCommandsInPlace`, `computePathPrefix` (private; `_computePathPrefix` for tests), and the deep public seam `rewriteStagedSkillBodies`/`rewriteStagedCommandBodies({runtime,configDir,scope,homedir?,platform?,resolveAttribution?})`. #4377 extends `computePathPrefix` with `projectRelative?` and `localDirName?`: an explicitly opted-in local install may emit the descriptor-derived project-relative prefix, while globals and unsafe/unrepresentable descriptor values retain the absolute fallback. The exported test seams `_relativeIncludesEnabled`, `_projectRelativePrefix`, `_isRelativePathPrefix`, and `_withShellDefaultsPreserved` pin that policy; the last is also the single balanced `${VAR:-...}` masking implementation used by both rewrite paths so nested launcher defaults remain absolute. `bin/install.js` binds these back (single owner, exports preserved); `getCommitAttribution` stays in `bin/install.js` (impure install-time config I/O) and is injected. The `getInstallExports` relay in Runtime Artifact Layout Module was deleted; the dependency direction installer/layout → conversion (never upward) is now enforced. Exception: opencode and kilo path-prefix rewriting is a deliberate `bin/install.js`-owned pre-conversion step (`applyOpencodeFamilyPathPrefix`) per #784, not a violation of the single-owner rule. Source: `msd-core/bin/lib/runtime-artifact-conversion.cjs`. Also exports `resolveVersionFrom(libDir)` — a lazy, defensive MSD-version resolver (installed-tree `msd-core/VERSION` first, then the source/npm `package.json` three dirs up, both validated against the repo's shared semver-prefix shape, degrading to `''` on failure) that replaced a module-load-time `require('../../../package.json')` which crashed on runtimes whose root carries no `package.json` (e.g. Codex) (#1383). #4032 added `appendAgentTools` as step 3 of the ADR-1235 pre-converter pipeline (`stageAgentsForRuntimeWithConverter`, `src/install-profiles.cts` — not this module): appends `readMsdEffectiveAgentTools` grants (Install Model Override Resolver Module) into the canonical agent's `tools:` frontmatter — line-surgical, both inline-comma and YAML block-list forms — before the runtime-specific converter runs, so every converter (including Kilo's `mcp__*`→`{server}_{tool}` permission mapping and ZCode's `mcp__*` omission policy) sees configured grants without a duplicated per-runtime write path. ### Runtime Artifact Install Plan Module -Module owning install-time staging and content-rewrite selection for a pre-resolved Runtime Artifact Layout. Interface: `createRuntimeArtifactInstallPlan({ layout, resolvedProfile, homedir?, platform?, resolveAttribution?, deps? }) -> { ok:true, plan:{ items, cleanupDirs } } | { ok:false, kind:'stage_failed'|'rewrite_failed', message, cleanupDirs, failedKind? }`. It iterates `layout.kinds` in order, calls each kind's `stage(resolvedProfile)`, delegates `commands` to Runtime Artifact Conversion `rewriteStagedCommandBodies`, delegates `skills` and `kimi-agents` to `rewriteStagedSkillBodies`, leaves non-rewritten kinds unchanged, and projects copy items as `{ kind, sourceDir, destDir }`. It deliberately does not prune, copy, run legacy migrations, print output, or execute cleanup; those remain Installer Module adapter responsibilities until later slices wire the plan into `bin/install.js`. **Write-confinement (ADR-1239 Phase B / #1679):** the exported pure `assertDestWithinConfigHome(configDir, destSubpath) -> resolvedDest` is the security gate — every kind's `destDir` is computed through it on both the install and uninstall plan paths, so a `destSubpath` that escapes `configHome` (`../../etc`, a NUL byte, etc.) is rejected at plan-build time with a clear error; `surface.cjs:applySurface` and `bin/install.js:installOpencodeFamilySkills` route their joins through the same helper, and `_copyStaged` carries a defense-in-depth containment check. This is security-load-bearing for the Phase C third-party-descriptor loader (which is where an untrusted `destSubpath` could arrive). Source: `msd-core/bin/lib/runtime-artifact-install-plan.cjs`. See Runtime Artifact Layout Module and Runtime Artifact Conversion Module. +Module owning install-time staging and content-rewrite selection for a pre-resolved Runtime Artifact Layout. Interface: `createRuntimeArtifactInstallPlan({ layout, resolvedProfile, homedir?, platform?, resolveAttribution?, deps? }) -> { ok:true, plan:{ items, cleanupDirs } } | { ok:false, kind:'stage_failed'|'rewrite_failed', message, cleanupDirs, failedKind? }`. It iterates `layout.kinds` in order, calls each kind's `stage(resolvedProfile)`, delegates `commands` to Runtime Artifact Conversion `rewriteStagedCommandBodies`, delegates `skills` to `rewriteStagedSkillBodies`, leaves non-rewritten kinds unchanged, and projects copy items as `{ kind, sourceDir, destDir }`. It deliberately does not prune, copy, run legacy migrations, print output, or execute cleanup; those remain Installer Module adapter responsibilities until later slices wire the plan into `bin/install.js`. **Write-confinement (ADR-1239 Phase B / #1679):** the exported pure `assertDestWithinConfigHome(configDir, destSubpath) -> resolvedDest` is the security gate — every kind's `destDir` is computed through it on both the install and uninstall plan paths, so a `destSubpath` that escapes `configHome` (`../../etc`, a NUL byte, etc.) is rejected at plan-build time with a clear error; `surface.cjs:applySurface` and `bin/install.js:installOpencodeFamilySkills` route their joins through the same helper, and `_copyStaged` carries a defense-in-depth containment check. This is security-load-bearing for the Phase C third-party-descriptor loader (which is where an untrusted `destSubpath` could arrive). Source: `msd-core/bin/lib/runtime-artifact-install-plan.cjs`. See Runtime Artifact Layout Module and Runtime Artifact Conversion Module. ### Install Fs Adapter Module Narrow, enumerated fs seam for the `installRuntimeArtifacts` call tree (`src/install-engine.cts`) — lands ADR-58's never-shipped `cleanup` rollout step (`registry → adapter → helpers → cleanup`, #2874, epic #2866 Phase 5). `installRuntimeArtifacts` now returns the executed plan it ran (`{ runtime, scope, kinds: [{kind, sourceDir, destDir, preserved}], cleanup: [{dir, ok}], postSteps }`) instead of `void`, including on the `combinedFamilyInstall` (OpenCode/Kilo) early-return path — no path may return `undefined` after this phase. Failure is unchanged: stage/rewrite errors still throw rather than becoming an `ok:false` value, so a caller cannot read success-shaped data off a failure path. Delivery is an ambient single mutable adapter (`current`), swapped for the duration of one synchronous install via `withInstallFs(deps.fs, fn)` and always restored in a `finally` — a `deps` parameter threaded through every function on the call tree (`install-profiles.cts`, `runtime-artifact-conversion.cts`, `commonjs-marker.cts`, `installer-migrations.cts`'s two reachable entry points) was rejected as a dozen+-site touch for no behavioral gain over the ambient swap, extending rather than replacing `createRuntimeArtifactInstallPlan`'s existing `deps` bag precedent (Runtime Artifact Install Plan Module). An injected `deps.fs` is a PARTIAL adapter merged over real `node:fs`; any method it omits — except `realpathSync`, the one method documented to degrade gracefully — now THROWS immediately if actually called, naming the missing method, rather than silently resolving to the real filesystem (#2875 defect fix: the prior silent fall-through let a fake adapter missing e.g. `rmSync` perform real destructive IO unnoticed). **Routes destination IO only, by design**: every write/probe against the install destination (copies, removals, snapshot/restore of preserved skill dirs, the manifest read) is fake-able; locating this package's own source tree (`findInstallSourceRoot`/`findAgentsSourceRoot`'s walk-up-from-`__dirname`, `readMsdCommandNames`) stays real and unrouted — a destination-fake's store starts empty and was never seeded with the repo's own paths, so routing that lookup would make every fake-adapter install throw instead of staging. The symlink-escape guard (`hasExistingSymlinkBetween`) and `assertDestWithinConfigHome` keep their REFUSAL DECISIONS outside this seam — only their `existsSync`/`lstatSync`/`realpathSync` probes route through it, so an injected fake can change what a probe observes but never flip the security decision itself. Writes remain byte-identical to pre-#2874 (AC4/AC5); existing `void`-ignoring callers (`bin/install.js`) are unaffected. Source: `msd-core/bin/lib/install-fs-adapter.cjs` (generated from `src/install-fs-adapter.cts`). See Runtime Artifact Install Plan Module, ADR-58. @@ -466,7 +466,7 @@ A per-agent narrative entry written by `mempalace_diary_write`. MSD's `msd-mempa The `mempalace.memory_mode` config key controlling how authoritative MemPalace is during recall/capture relative to MSD's native memory. Three wired values: `augment` (default — palace is an additive recall layer; native memory stays authoritative; lowest coupling), `kg_backend` (knowledge-graph queries resolve against MemPalace's temporal graph as the primary source, `.planning/graphs/` as fallback; non-KG drawer recall stays additive), `replace` (recall resolves through the palace as the source of truth, native artifacts as fallback). Every mode is `onError:skip` and default-resilient — an unreachable palace degrades to native memory and MSD keeps writing `.planning/graphs/`, so no mode loses memory. Read at hook-render time; switching is a config change, not a reinstall. Cross-mode migration of existing `.planning/graphs/` into the palace is a separate, not-yet-implemented concern (PRD/ADR §17 open question). See MemPalace Settings in `docs/CONFIGURATION.md`. ### Runtime Hooks Surface Module -Standalone hook-surface writer module extracted from `bin/install.js` as ADR-857 phase 5f-1 (behavior-preserving relocation, no logic change). Owns: Cline rules-body/agents-md/pre-tool-use hook generation (`buildClineRulesBody`, `buildClineAgentsMdBody`, `buildClinePreToolUseHook`, `mergeMsdAgentsMd`, `writeClineArtifacts`); Cursor `hooks.json` lifecycle (`buildCursorHookEntry`, `isManagedCursorHookEntry`, `reconcileCursorHooksJson`, `writeCursorHooksJson`, `removeCursorHooksJson`); Copilot session-hook config (`buildCopilotHookConfig`, `writeCopilotHookConfig`); Codex hook-block and event management (`buildCodexHookBlock`, `rewriteLegacyCodexHookBlock`, `reconcileCodexHooksJsonEvent`, `reconcileCodexHooksJsonSessionStart`, `ensureCodexHooksJsonSessionStart`, `removeCodexHooksJsonEvent`, `removeCodexHooksJsonSessionStart`, `buildCodexHookWindowsShimIR`, `cleanupOrphanedCodexContextMonitorScript`, `isMsdOwnedCodexContextMonitorScript`, `hooksJsonReferencesCodexContextMonitor` — #2586, the last three replacing the deleted `ensureCodexHooksJsonEvent`: MSD no longer adds Codex context-monitor hook-event registrations, only recognizes and removes stale ones left by a pre-#2586 install, then deletes the orphaned script/`.cmd` shim once unreferenced and content-verified as MSD-owned); Kimi native config.toml `[[hooks]]` lifecycle (`buildKimiHooksTomlBlock`, `stripKimiHooksTomlBlock`, `writeKimiHooksToml`, `removeKimiHooksToml` — #2095 EoS/kimi Upgrade 1, the first genuinely NEW hook surface added post-relocation rather than a behavior-preserving move: kimi's `[[hooks]]` array lives in its own native `config.toml`, resolved by `resolveKimiHooksTomlDir` in Runtime Homes Module to a directory deliberately separate from kimi's MSD configDir, wrapped in `# MSD Hooks BEGIN`/`END` marker comments for idempotent reinstall); and shared hook command helpers (`buildHookCommand`, `rewriteLegacyManagedNodeHookCommands`, `normalizeNodePath`, `resolveNodeRunner`, and — #3662 — `buildNodeRunnerChainToken`, the POSIX-sh runner token that resolves node at hook-fire time for non-portable managed JS hooks, plus the `NODE_RUNNER_RESOLVER_HOOK` basename of the staged `hooks/msd-node-runner.sh` resolver that portable installs route through with the baked node path as its first argument). `bin/install.js` delegates to this module via thin wrappers and re-exports its functions unchanged so existing tests require no modification. Also owns post-install entrypoint validation (#4154/#4249): `buildHookCommand` and every writer's `recordConfiguredHookCommand` record a `ConfiguredEntrypoint {runtime, configPath, scriptPath, interpreterCandidates?, selfExecutable?, platform, command?}` for each hook they compute — including an already-registered hook whose register-only-if-absent guard leaves its on-disk `command` stale, since `scriptPath`/`interpreterCandidates` are derived from `configDir`/`hookName`, not from that command string. `validateConfiguredEntrypoints(entries)` runs up to three checks per entry, none of which execute anything. First, always: `statSync`s `scriptPath` (must be a file) then `accessSync(R_OK)`s it — required even for a self-executable shebang script, since its kernel-invoked interpreter still opens and reads it, not just execs it. Second, only when `selfExecutable: true`: `accessSync(X_OK)`. Every producer that needs this sets the flag explicitly, rather than it being inferred from an absent `interpreterCandidates` — a Windows-Claude `.sh` hook direct-invoked via `shellHookOmitsBashRunner`, Codex's Windows `.cmd` shim (no shebang; relies on Windows' own extension-based dispatch), and Cline's hybrid `#!/usr/bin/env node` hook all set it; the check is skipped entirely when `entry.platform` is `win32` (POSIX mode bits don't mean executable there — mirroring `resolveExecutableBinary`'s own X_OK carve-out), so Cline is the only one of the three where this check runs. Third, only when `interpreterCandidates` is present: at least one must resolve via `resolveExecutableBinary(candidate, {requireExecutable:true})` (for a Node hook, the first candidate is `normalizeNodePath`'d — the SAME stable version-manager alias `buildNodeRunnerChainToken` bakes as its own first choice, not the raw, always-resolving `process.execPath`; a `.sh` hook's candidate is its resolved `bash` path instead; Cline's is `['node']`, since unlike every other MSD JS hook — which bakes an absolute install-time-resolved node path specifically to avoid this — its interpreter is looked up on PATH by `env` at hook-fire time). Failures carry `reason: 'missing'|'unreadable'|'wrong-file-type'|'unresolved-interpreter'|'not-executable'` (`unreadable` for an `EACCES` `statSync`, distinct from a genuinely absent path). See Installer Module for the caller-side gate, which deduplicates entries by `(configPath, scriptPath)` first (writers can push the same pair more than once, e.g. Kimi's context-monitor hook across several events). Source: `src/runtime-hooks-surface.cts`. Built output: `msd-core/bin/lib/runtime-hooks-surface.cjs`. +Standalone hook-surface writer module extracted from `bin/install.js` as ADR-857 phase 5f-1 (behavior-preserving relocation, no logic change). Owns: Cline rules-body/agents-md/pre-tool-use hook generation (`buildClineRulesBody`, `buildClineAgentsMdBody`, `buildClinePreToolUseHook`, `mergeMsdAgentsMd`, `writeClineArtifacts`); Cursor `hooks.json` lifecycle (`buildCursorHookEntry`, `isManagedCursorHookEntry`, `reconcileCursorHooksJson`, `writeCursorHooksJson`, `removeCursorHooksJson`); Copilot session-hook config (`buildCopilotHookConfig`, `writeCopilotHookConfig`); Codex hook-block and event management (`buildCodexHookBlock`, `rewriteLegacyCodexHookBlock`, `reconcileCodexHooksJsonEvent`, `reconcileCodexHooksJsonSessionStart`, `ensureCodexHooksJsonSessionStart`, `removeCodexHooksJsonEvent`, `removeCodexHooksJsonSessionStart`, `buildCodexHookWindowsShimIR`, `cleanupOrphanedCodexContextMonitorScript`, `isMsdOwnedCodexContextMonitorScript`, `hooksJsonReferencesCodexContextMonitor` — #2586, the last three replacing the deleted `ensureCodexHooksJsonEvent`: MSD no longer adds Codex context-monitor hook-event registrations, only recognizes and removes stale ones left by a pre-#2586 install, then deletes the orphaned script/`.cmd` shim once unreferenced and content-verified as MSD-owned); and shared hook command helpers (`buildHookCommand`, `rewriteLegacyManagedNodeHookCommands`, `normalizeNodePath`, `resolveNodeRunner`, and — #3662 — `buildNodeRunnerChainToken`, the POSIX-sh runner token that resolves node at hook-fire time for non-portable managed JS hooks, plus the `NODE_RUNNER_RESOLVER_HOOK` basename of the staged `hooks/msd-node-runner.sh` resolver that portable installs route through with the baked node path as its first argument). `bin/install.js` delegates to this module via thin wrappers and re-exports its functions unchanged so existing tests require no modification. Also owns post-install entrypoint validation (#4154/#4249): `buildHookCommand` and every writer's `recordConfiguredHookCommand` record a `ConfiguredEntrypoint {runtime, configPath, scriptPath, interpreterCandidates?, selfExecutable?, platform, command?}` for each hook they compute — including an already-registered hook whose register-only-if-absent guard leaves its on-disk `command` stale, since `scriptPath`/`interpreterCandidates` are derived from `configDir`/`hookName`, not from that command string. `validateConfiguredEntrypoints(entries)` runs up to three checks per entry, none of which execute anything. First, always: `statSync`s `scriptPath` (must be a file) then `accessSync(R_OK)`s it — required even for a self-executable shebang script, since its kernel-invoked interpreter still opens and reads it, not just execs it. Second, only when `selfExecutable: true`: `accessSync(X_OK)`. Every producer that needs this sets the flag explicitly, rather than it being inferred from an absent `interpreterCandidates` — a Windows-Claude `.sh` hook direct-invoked via `shellHookOmitsBashRunner`, Codex's Windows `.cmd` shim (no shebang; relies on Windows' own extension-based dispatch), and Cline's hybrid `#!/usr/bin/env node` hook all set it; the check is skipped entirely when `entry.platform` is `win32` (POSIX mode bits don't mean executable there — mirroring `resolveExecutableBinary`'s own X_OK carve-out), so Cline is the only one of the three where this check runs. Third, only when `interpreterCandidates` is present: at least one must resolve via `resolveExecutableBinary(candidate, {requireExecutable:true})` (for a Node hook, the first candidate is `normalizeNodePath`'d — the SAME stable version-manager alias `buildNodeRunnerChainToken` bakes as its own first choice, not the raw, always-resolving `process.execPath`; a `.sh` hook's candidate is its resolved `bash` path instead; Cline's is `['node']`, since unlike every other MSD JS hook — which bakes an absolute install-time-resolved node path specifically to avoid this — its interpreter is looked up on PATH by `env` at hook-fire time). Failures carry `reason: 'missing'|'unreadable'|'wrong-file-type'|'unresolved-interpreter'|'not-executable'` (`unreadable` for an `EACCES` `statSync`, distinct from a genuinely absent path). See Installer Module for the caller-side gate, which deduplicates entries by `(configPath, scriptPath)` first (writers can push the same pair more than once, e.g. one hook across several events). Source: `src/runtime-hooks-surface.cts`. Built output: `msd-core/bin/lib/runtime-hooks-surface.cjs`. ### Runtime Config Adapter Registry Module owning the explicit per-runtime config-mutation dispatch table for the installer. `resolveRuntimeConfigIntent(runtime)` projects a typed config intent — `installSurface` (`settings-json` | `codex-toml` | `copilot-instructions` | `cline-rules` | `cursor-hooks-json` | `profile-marker-only`), `writesSharedSettings` (the `finishInstall` shared-settings write gate), and `finishPermissionWriter` (`opencode` | `kilo` | `antigravity` | none) — that `bin/install.js` dispatches on instead of inline `runtime === '...'` branching. Owns adapter selection only: it performs no filesystem IO and does not execute config mutations (the install/finishInstall handlers and the per-runtime writers do that). Unknown runtimes fail loudly with a `TypeError`, guarded by an `Object.hasOwn` own-property check so prototype-chain keys (`__proto__`, `constructor`) also throw. Also exports `resolveInstallPlan(runtime)` — the ADR-58 `InstallPlan` capstone — which collects the install-level descriptor axes (`installSurface`, `writesSharedSettings`, `finishPermissionWriter`, `hookEvents`, `extendedHookEvents`, `hooksSurface`, `sandboxTier`) into one typed `InstallPlan` value consumed by `install()` and `finishInstall()` in `bin/install.js`. `sandboxTier` (`none` | `codex-agent-sandbox`) gates per-agent `sandbox_mode` emission in the codex TOML path and fails loud on a missing/invalid value (#1151). The spatial axes (`configHome`, `artifactLayout`, `commandStyle`) remain behind their self-resolving adapter modules and are not part of the plan; they are the execution adapters. Realizes both the adapter-selection and plan-collection halves of the Runtime Install Policy Module boundary. Source: `msd-core/bin/lib/runtime-config-adapter-registry.cjs`. See ADR-58, #60. @@ -602,7 +602,7 @@ Default-off Capability (`capabilities/live-dom-uat/capability.json`, `role: feat The enforced cross-phase defect register operationalizing MSD's no-defer discipline as a tracked artifact (#1950). Markdown file at `.planning/WINDOWS.md` (project-level, cross-phase) with YAML frontmatter carrying scalar counts (`schema_version`, `open_count`, `waived_count`, `fixed_count`, `total_count`, `last_updated`) for the FAST path the gate reads via jq without parsing JSON, plus a JSON code block as the AUTHORITATIVE entries source; the two cross-check and fail closed on drift. The rendered markdown table is a THIRD projection of that same source and is cross-checked the same way, but at the WRITE seam rather than the read seam (#3689): `writeLedgerAtomic` compares the on-disk table against `renderTable()` and refuses with `WINDOWS_LEDGER_TABLE_DRIFT` before writing, so a hand-edited cell is never silently reverted and a table-only row is never silently erased. Deliberately NOT enforced in `parseLedger`: hardening the read would break `windows status` and the ship gate on exactly the ledgers an operator needs to inspect. Each entry: `{ id, kind, phase, file, line, description, status, reason, recorded_at, resolved_at }`; kinds are closed (`stub | todo | fixme | skipped-test | lint-warning | unmet-truth | unrun-verify | deviation`); statuses are closed (`open | waived | fixed`). The `broken-windows` Capability (`capabilities/broken-windows/capability.json`) registers one `ship:pre` gate with predicate `artifact-frontmatter-equals WINDOWS.md open_count == 0`; federated config key `workflow.windows_enforce` (default `false` — opt-in enforcement, tracking-only by default so a project can adopt the ledger before turning the gate on). Population is best-effort and never blocks execution: `agents/msd-executor.md` appends stubs/skipped-tests/unrun-verifies via `msd_run windows append` after writing SUMMARY.md. Source of truth: `src/broken-windows.cts` → `msd-core/bin/lib/broken-windows.cjs` (pure `parseLedger`/`renderLedger`/`appendWindow`/`markWaived`/`markFixed` + I/O `cmdWindowsStatus`/`Append`/`Waive`/`MarkFixed`); CLI surface `msd-tools windows status|append|waive|fixed`. Ship gate enforcement is a `capId == "broken-windows"` named specialization inside `msd-core/workflows/ship.md` preflight's generic `kind == "gate"` dispatch loop (sibling to the `security` specialization; #3559 made that loop generic, so every OTHER capability's `ship:pre` gate is now evaluated through `msd_run check predicate` instead of being resolved and silently dropped, while these two keep their bespoke fail-closed reads and are each visited exactly once); it reads `msd_run windows status --raw` and fails closed on a non-zero/non-numeric `open_count` (an unparseable ledger is itself a broken window). `/msd:progress` surfaces the open+waived count. The ledger is optional and backward-compatible: a project with no `.planning/WINDOWS.md` reports `open_count: 0` and ships cleanly, and with `workflow.windows_enforce=false` (the default) ship never blocks on it. Frozen `REASON` enum: `WINDOWS_LEDGER_MISSING | WINDOWS_LEDGER_MALFORMED | WINDOWS_LEDGER_TABLE_DRIFT | WINDOWS_LEDGER_LOCK | WINDOWS_ID_NOT_FOUND | WINDOWS_ALREADY_RESOLVED | WINDOWS_WAIVE_REASON_EMPTY | WINDOWS_INVALID_KIND | WINDOWS_INVALID_FILE | WINDOWS_INVALID_TEXT | WINDOWS_INVALID_ID | WINDOWS_APPEND_MISSING_FIELD | WINDOWS_USAGE | WINDOWS_OK` — surfaced through `--json-errors` for typed test assertions (`WINDOWS_LEDGER_LOCK`, #3780: the mutating commands serialize on the `.planning/.WINDOWS.lock` cross-process lock, shared with refactor-trigger-command-router's own ledger writers; it fires only when a live writer holds the lock past the bounded retry budget. `WINDOWS_INVALID_TEXT` predates this list and was omitted from it). Test seam: `tests/broken-windows.test.cjs`. Origin: *The Pragmatic Programmer* Topic 3 (Hunt & Thomas — software transplant of Wilson & Kelling's broken-windows metaphor) plus Cunningham's debt metaphor (decay accrues interest ⇒ accounting, not just habit). ### Emitted Artifact Provenance -Cross-seam principle (ADR-2719, epic #2719): a committed artifact that is a pure function of the source tree is not reviewable state — it is derived state wearing a review costume, and it must be *attributable* rather than *pinned*. Concept, not a Module: it ships nothing, so it takes no `Module` suffix (follows the `### Resolution Provenance` precedent). Scope is the emitted-artifact family named by `RULESET.EMITTED_ATTRIBUTION`. The principle: every emitted path whose hash moved between `next` HEAD and PR HEAD must be attributable — through a declarative provenance table — to a path the pull request actually changed; unattributable deltas are a hard failure that *names them* rather than an anomaly a reviewer must notice inside 7,500 lines of hex. Totality is enforced, so an emitted path matching no rule fails loudly instead of passing through unattributed. The escape hatch is a commit trailer on the PR's own commits (ADR-3942, superseding ADR-2719 §3), not a committed document — two structurally distinct key spaces (separate maps, closing a latent defect where a growth key could satisfy a hash lookup by naming coincidence and vice versa): `Emitted-Drift-Ack-Hash:` keyed on the emitted path (always contains `/`), `Emitted-Drift-Ack-Growth:` keyed on the bare filename under `msd-core/workflows/` or `agents/` — key and reason separated by ` — ` (space, em dash, space; split on the FIRST occurrence), deliberately not a flag or env var — a trailer appears in the PR's commit list ONLY when something rippled unexpectedly, so adding one IS the alarm, whereas today 100% of emitted-byte changes touch fixtures and touching them signals nothing. Read from `git log $(git merge-base HEAD)..HEAD` — the SAME merge-base `changedPaths` uses via `git diff base...HEAD`, so the ack set and the change set cannot disagree about which commits are this PR's; a two-dot range would be a defect. "Spent" no longer exists: a merged trailer is out of range by construction, not by computation, since there is no base-side copy to compare against. An uncomputable range (shallow clone) THROWS rather than passing vacuously — zero trailers means a PR needing one fails, a false red rather than a false green. `staleAcks` is retained: a trailer declaring a key nothing consumed is still a hard error, and the message names WHICH key space. This design is the terminus of a chain that began because the ack was PR-lifetime data kept in permanent shared state: the single legacy `tests/emitted-drift-ack.json` was a guaranteed merge-conflict cell (#2789; 5 of 6 conflicting PRs in one open queue collided on it and nothing else); #2914 split it into per-PR fragments under `tests/emitted-drift-acks/`, ending the FILE conflict but not the KEY conflict, since two sources could never name the same path; #3078 found a fully-spent fragment left on `next` still walled off every path it owned until swept; #3842 and #3823 tried automated and hand-authored sweeps and each created the next conflict; #3875's timed sweeper automated the remedy but could not merge its own PRs. ADR-3942 ends the chain by moving the ack off the tree entirely (see `RULESET.EMITTED_ATTRIBUTION`). The same differential machine carries the size ratchet: growth is reported with exact byte deltas and needs the same acknowledgment, so anti-creep survives without pinning a number. Distinguish from the absolute check that remains: `tests/fixtures/install-tree/*.json` stays committed and normally-merged (ADR-2719 §7) because "the installer stopped shipping X" must fail with no attribution reasoning involved. Delivery was phased — #2721 naming + interim merge relief, #2722 provenance table + totality guard, #2723 differential check dual-run beside `golden-install-parity.test.cjs`, #2724 cutover (COMPLETE: the dual-run window observed agreement on real PRs after fixing #2750/#2760, and the golden fixtures/test/generator/merge-driver bridge are now deleted; the differential is the sole gate). The table LANDED in #2722 as `tests/helpers/emitted-provenance.cjs` (19 rules, guarded by `tests/emitted-provenance.test.cjs`); it maps emitted path → repo source and is TOTAL over EVERY emitted path in all 19 manifests — exactly one rule per path, with zero-match, two-match, AND dead-rule (a rule matching nothing) all hard failures, so table rot is loud in both directions. Deliberately NO path/family counts are recorded here: those move with every shipped-content edit, and a hand-maintained number in glossary canon is the exact silent-drift failure this whole seam exists to end. The guard recomputes them from the fixtures on every run — read them from a failure message, never from prose. What IS stable is the rule count, which changes only when a new emitted family or host appears. Note the surface is materially wider than #2722 estimated from `claude.json` alone (its "13 families / 15-20 rules" was a single-runtime sample; the 19-manifest surface spans runtime-specific roots — `.agents/`, `.kimi/hooks/`, `command/`, `agents/subagents/`, `.clinerules/`, `plugins/`, `extensions/`, `.msd/`, the hermes `skills/msd/` category and the #69 nested `skills//skills//` layout). Two design invariants carry forward to #2723: emitted SHAPES are hard-coded (deriving them from the installer would make the guard tautological — it would follow any installer change silently), while source PATHS may read a first-party descriptor where that descriptor is the sole declaration (`hostBehaviors.nativePlugin.source`); and attribution is keyed on `(rel, runtime)`, never `rel` alone, because one emitted path has different sources per host (`plugins/msd-core.js` ← `.opencode/` vs `.kilo/`). Emitted skills attribute to `commands/msd/*.md`, NEVER the repo `skills/` dir — that dir is itself generated from `commands/msd` by `scripts/gen-plugin-skills.cjs`, so attributing to it is false attribution that still passes totality. Totality does NOT catch a rule pointing at the WRONG source (the recorded residual); the guard against that is the companion assertion that every attributed source EXISTS in the repo, which caught three real cases while the table was built (Copilot's `.agent.md` rename, Kimi's code-literal `agents/msd.{yaml,md}` root agent, and Copilot's `hooks/msd-session.json`). A `sources` entry ending in `/` is a PREFIX, not a file — and prefix matching is SEGMENT-AWARE, so a source of `agents/` must not attribute `agentsfoo/x.md`. The differential check LANDED in #2723 as `tests/helpers/emitted-diff.cjs` (the conservation law, a PURE function — no fs/git/installer/clock) + `tests/helpers/emitted-baseline.cjs` (baseline resolution), guarded by `tests/emitted-attribution.test.cjs`. It ran DUAL beside `golden-install-parity.test.cjs` through the #2723 dual-run window with both green and fixtures untouched; #2724 deleted the golden fixtures/test/generator and the check is now the sole gate. Purity is deliberate and load-bearing: the naive one-big-integration-test shape would need ~38 installer spawns per assertion, so the four failing-first criteria would not in practice get written — which is exactly how a phase ships promised-but-not-built. Buckets are CONSERVED: every moved emitted path lands in exactly one of `attributed | unattributable | acked` (property-tested), and a path the provenance table cannot resolve surfaces as an ERROR rather than a silent skip. Four asymmetries worth knowing: an ADDED emitted key is a ripple too (not just modified ones); `synthesized` paths are exempt but `code-derived` ones are NOT (that is why Phase 2 refused to mark them exempt — exempt means permanently blind); SHRINKAGE needs no ack while growth does (gating shrinkage would punish what the ratchet wants); and a STALE ack — a declared key nothing consumed — is a hard failure (#2789 built spent/live detection against a base-relative document that persisted after merge, requiring `readAckFileAtRef`/`baseAck`/`spentAcks` and a `git show`-vs-`ls-tree` absence check to tell "never explained anything" from "already merged, and reddening `next` and every branching PR when it wasn't told apart" (#2768); ADR-3942 makes staleness structural instead of computed — every trailer read is definitionally THIS diff's, scoped by `git merge-base HEAD`, so there is no base copy to compare against and no re-arm-by-reword mechanic to defend, and the base-document corruption hazard the standalone ack linter existed to guard against is gone with the document). Since ADR-3942 there is exactly ONE ack source and it is not a file: the `Emitted-Drift-Ack-Hash:` / `Emitted-Drift-Ack-Growth:` trailers on the PR's own commits, read by `readAckTrailers` (`tests/helpers/emitted-runtime.cjs`) over `git log $(git merge-base HEAD)..HEAD` and parsed by `parseAckTrailers` (`tests/helpers/emitted-diff.cjs`) into TWO structurally distinct key-space maps — closing the latent defect where one shared `paths` map let a growth key satisfy a hash lookup by naming coincidence. Absent = no acks; a declared key nothing consumed is a hard `staleAcks` error that now names WHICH space; a non-empty reason per key is required — "name them and say why" is the contract (ADR-2719 §3, retained). A key declared twice with conflicting reasons is a hard error; declared twice identically it is deduped. An UNCOMPUTABLE range (shallow clone) THROWS rather than reading as zero acks — the inverse of the fragment guard's vacuous-pass failure, and fail-closed in the safe direction. Baseline is CACHED not committed, keyed on the `next` sha; a stale key is REFUSED, never used — absence fails loudly and gets fixed, whereas staleness produces a confident wrong answer. An explicitly-pointed-at (`MSD_EMITTED_BASELINE`) stale baseline is a hard stop, while a stale CACHE falls through to the in-job build. No baseline-unavailable path may `return` (in `node:test` that is a PASS, not a skip — ADR-2719 §6). Supersedes ADR-2264 §2–§4 and its Amendment; ADR-2264 Phase 1 (`buildParityManifest` and the exclusion constants in `tests/helpers/install-shared.cjs`) is retained and depended upon. +Cross-seam principle (ADR-2719, epic #2719): a committed artifact that is a pure function of the source tree is not reviewable state — it is derived state wearing a review costume, and it must be *attributable* rather than *pinned*. Concept, not a Module: it ships nothing, so it takes no `Module` suffix (follows the `### Resolution Provenance` precedent). Scope is the emitted-artifact family named by `RULESET.EMITTED_ATTRIBUTION`. The principle: every emitted path whose hash moved between `next` HEAD and PR HEAD must be attributable — through a declarative provenance table — to a path the pull request actually changed; unattributable deltas are a hard failure that *names them* rather than an anomaly a reviewer must notice inside 7,500 lines of hex. Totality is enforced, so an emitted path matching no rule fails loudly instead of passing through unattributed. The escape hatch is a commit trailer on the PR's own commits (ADR-3942, superseding ADR-2719 §3), not a committed document — two structurally distinct key spaces (separate maps, closing a latent defect where a growth key could satisfy a hash lookup by naming coincidence and vice versa): `Emitted-Drift-Ack-Hash:` keyed on the emitted path (always contains `/`), `Emitted-Drift-Ack-Growth:` keyed on the bare filename under `msd-core/workflows/` or `agents/` — key and reason separated by ` — ` (space, em dash, space; split on the FIRST occurrence), deliberately not a flag or env var — a trailer appears in the PR's commit list ONLY when something rippled unexpectedly, so adding one IS the alarm, whereas today 100% of emitted-byte changes touch fixtures and touching them signals nothing. Read from `git log $(git merge-base HEAD)..HEAD` — the SAME merge-base `changedPaths` uses via `git diff base...HEAD`, so the ack set and the change set cannot disagree about which commits are this PR's; a two-dot range would be a defect. "Spent" no longer exists: a merged trailer is out of range by construction, not by computation, since there is no base-side copy to compare against. An uncomputable range (shallow clone) THROWS rather than passing vacuously — zero trailers means a PR needing one fails, a false red rather than a false green. `staleAcks` is retained: a trailer declaring a key nothing consumed is still a hard error, and the message names WHICH key space. This design is the terminus of a chain that began because the ack was PR-lifetime data kept in permanent shared state: the single legacy `tests/emitted-drift-ack.json` was a guaranteed merge-conflict cell (#2789; 5 of 6 conflicting PRs in one open queue collided on it and nothing else); #2914 split it into per-PR fragments under `tests/emitted-drift-acks/`, ending the FILE conflict but not the KEY conflict, since two sources could never name the same path; #3078 found a fully-spent fragment left on `next` still walled off every path it owned until swept; #3842 and #3823 tried automated and hand-authored sweeps and each created the next conflict; #3875's timed sweeper automated the remedy but could not merge its own PRs. ADR-3942 ends the chain by moving the ack off the tree entirely (see `RULESET.EMITTED_ATTRIBUTION`). The same differential machine carries the size ratchet: growth is reported with exact byte deltas and needs the same acknowledgment, so anti-creep survives without pinning a number. Distinguish from the absolute check that remains: `tests/fixtures/install-tree/*.json` stays committed and normally-merged (ADR-2719 §7) because "the installer stopped shipping X" must fail with no attribution reasoning involved. Delivery was phased — #2721 naming + interim merge relief, #2722 provenance table + totality guard, #2723 differential check dual-run beside `golden-install-parity.test.cjs`, #2724 cutover (COMPLETE: the dual-run window observed agreement on real PRs after fixing #2750/#2760, and the golden fixtures/test/generator/merge-driver bridge are now deleted; the differential is the sole gate). The table LANDED in #2722 as `tests/helpers/emitted-provenance.cjs` (19 rules, guarded by `tests/emitted-provenance.test.cjs`); it maps emitted path → repo source and is TOTAL over EVERY emitted path in all 19 manifests — exactly one rule per path, with zero-match, two-match, AND dead-rule (a rule matching nothing) all hard failures, so table rot is loud in both directions. Deliberately NO path/family counts are recorded here: those move with every shipped-content edit, and a hand-maintained number in glossary canon is the exact silent-drift failure this whole seam exists to end. The guard recomputes them from the fixtures on every run — read them from a failure message, never from prose. What IS stable is the rule count, which changes only when a new emitted family or host appears. Note the surface is materially wider than #2722 estimated from `claude.json` alone (its "13 families / 15-20 rules" was a single-runtime sample; the 19-manifest surface spans runtime-specific roots — `.agents/`, `command/`, `agents/subagents/`, `.clinerules/`, `plugins/`, `extensions/`, `.msd/`, the hermes `skills/msd/` category and the #69 nested `skills//skills//` layout). Two design invariants carry forward to #2723: emitted SHAPES are hard-coded (deriving them from the installer would make the guard tautological — it would follow any installer change silently), while source PATHS may read a first-party descriptor where that descriptor is the sole declaration (`hostBehaviors.nativePlugin.source`); and attribution is keyed on `(rel, runtime)`, never `rel` alone, because one emitted path has different sources per host (`plugins/msd-core.js` ← `.opencode/` vs `.kilo/`). Emitted skills attribute to `commands/msd/*.md`, NEVER the repo `skills/` dir — that dir is itself generated from `commands/msd` by `scripts/gen-plugin-skills.cjs`, so attributing to it is false attribution that still passes totality. Totality does NOT catch a rule pointing at the WRONG source (the recorded residual); the guard against that is the companion assertion that every attributed source EXISTS in the repo, which caught two real cases while the table was built (Copilot's `.agent.md` rename and Copilot's `hooks/msd-session.json`). A `sources` entry ending in `/` is a PREFIX, not a file — and prefix matching is SEGMENT-AWARE, so a source of `agents/` must not attribute `agentsfoo/x.md`. The differential check LANDED in #2723 as `tests/helpers/emitted-diff.cjs` (the conservation law, a PURE function — no fs/git/installer/clock) + `tests/helpers/emitted-baseline.cjs` (baseline resolution), guarded by `tests/emitted-attribution.test.cjs`. It ran DUAL beside `golden-install-parity.test.cjs` through the #2723 dual-run window with both green and fixtures untouched; #2724 deleted the golden fixtures/test/generator and the check is now the sole gate. Purity is deliberate and load-bearing: the naive one-big-integration-test shape would need ~38 installer spawns per assertion, so the four failing-first criteria would not in practice get written — which is exactly how a phase ships promised-but-not-built. Buckets are CONSERVED: every moved emitted path lands in exactly one of `attributed | unattributable | acked` (property-tested), and a path the provenance table cannot resolve surfaces as an ERROR rather than a silent skip. Four asymmetries worth knowing: an ADDED emitted key is a ripple too (not just modified ones); `synthesized` paths are exempt but `code-derived` ones are NOT (that is why Phase 2 refused to mark them exempt — exempt means permanently blind); SHRINKAGE needs no ack while growth does (gating shrinkage would punish what the ratchet wants); and a STALE ack — a declared key nothing consumed — is a hard failure (#2789 built spent/live detection against a base-relative document that persisted after merge, requiring `readAckFileAtRef`/`baseAck`/`spentAcks` and a `git show`-vs-`ls-tree` absence check to tell "never explained anything" from "already merged, and reddening `next` and every branching PR when it wasn't told apart" (#2768); ADR-3942 makes staleness structural instead of computed — every trailer read is definitionally THIS diff's, scoped by `git merge-base HEAD`, so there is no base copy to compare against and no re-arm-by-reword mechanic to defend, and the base-document corruption hazard the standalone ack linter existed to guard against is gone with the document). Since ADR-3942 there is exactly ONE ack source and it is not a file: the `Emitted-Drift-Ack-Hash:` / `Emitted-Drift-Ack-Growth:` trailers on the PR's own commits, read by `readAckTrailers` (`tests/helpers/emitted-runtime.cjs`) over `git log $(git merge-base HEAD)..HEAD` and parsed by `parseAckTrailers` (`tests/helpers/emitted-diff.cjs`) into TWO structurally distinct key-space maps — closing the latent defect where one shared `paths` map let a growth key satisfy a hash lookup by naming coincidence. Absent = no acks; a declared key nothing consumed is a hard `staleAcks` error that now names WHICH space; a non-empty reason per key is required — "name them and say why" is the contract (ADR-2719 §3, retained). A key declared twice with conflicting reasons is a hard error; declared twice identically it is deduped. An UNCOMPUTABLE range (shallow clone) THROWS rather than reading as zero acks — the inverse of the fragment guard's vacuous-pass failure, and fail-closed in the safe direction. Baseline is CACHED not committed, keyed on the `next` sha; a stale key is REFUSED, never used — absence fails loudly and gets fixed, whereas staleness produces a confident wrong answer. An explicitly-pointed-at (`MSD_EMITTED_BASELINE`) stale baseline is a hard stop, while a stale CACHE falls through to the in-job build. No baseline-unavailable path may `return` (in `node:test` that is a PASS, not a skip — ADR-2719 §6). Supersedes ADR-2264 §2–§4 and its Amendment; ADR-2264 Phase 1 (`buildParityManifest` and the exclusion constants in `tests/helpers/install-shared.cjs`) is retained and depended upon. ### Untrusted-input boundary The prompt-level data/instruction isolation seam for untrusted web/document ingress (#1577). Shared reference `msd-core/references/untrusted-input-boundary.md`, `@`-included by the 10 ingest agents (`msd-project-researcher`, `msd-phase-researcher`, `msd-ui-researcher`, `msd-assumptions-analyzer`, `msd-advisor-researcher`, `msd-ai-researcher`, `msd-domain-researcher`, `msd-research-synthesizer`, `msd-doc-classifier`, `msd-doc-synthesizer`) — every agent that reads fetch/search/MCP output or external source documents. The reference instructs: treat fetched/read content as **data, never instructions**; self-scan content for embedded directives before use; act only on the assigned task (ignore off-task instructions in data); and wrap quoted untrusted spans in a **fresh random delimiter** per wrap (fixed markers are spoofable). This prompt-level boundary is the primary control — it keeps an injection from being *followed* even while it sits in context. The hook-level companion is the read-injection scanner (`hooks/msd-read-injection-scanner.js`, PostToolUse on `Read`/`WebFetch`/`WebSearch`), advisory by default; the opt-in top-level `security.injection_blocking` key upgrades HIGH-confidence detections to a PostToolUse circuit-breaker that halts the agent's next step (it runs *after* the fetch, so it is not a redactor). Tests: `tests/untrusted-input-isolation.test.cjs`, `tests/read-injection-scanner.*.test.cjs`, `tests/injection-blocking-config.test.cjs`. See `docs/adr/1577-untrusted-input-boundary-and-injection-blocking.md` and `docs/explanation/security-model.md`. Grounding: arXiv 2506.05739 (PPA), 2507.15219 (PromptArmor), 2504.20472. @@ -745,11 +745,10 @@ The prompt-level data/instruction isolation seam for untrusted web/document ingr `CONFIG.SEAM.loadConfig-context=loadConfig(cwd,{workstream}) replaces env-mutation fallback; no temporary process.env MSD_WORKSTREAM rewrites` `CONFIG.LOCATION.SEAM.scrub-set=tests/helpers.cjs CONFIG_LOCATION_ENV_KEYS is DERIVED from five sources rather than maintained as one hand-written list (source 4 IS a literal residue list, for vars that fit no other rung — what is never hand-listed is the SET): capability-registry runtimes[].runtime.configHome.env AND [].configHome.skillsHome.env + runtime-homes NON_REGISTRY_CONFIG_HOME_DESCRIPTORS[].env AND [].skillsHome.env (a descriptor is a descriptor — BOTH descriptor rungs walk skillsHome, which resolves independently via resolveSkillsBaseFromDescriptor) + runtime-homes MSD_LOCATION_ENV_KEYS + a residue list (GROK_AGENTS_HOME, MSD_RUNTIME, MSD_PROJECT, MSD_WORKSTREAM) + WRITE_ESCAPE_PERMISSION_ENV_KEYS (MSD_ALLOW_SYMLINKED_DEST — a permission, not a location: it names no path but disarms the symlink-escape guard, so blanking it makes the guard STRICTER, never looser); adding a config-location var means making it ENUMERABLE at one of those sources, not appending a literal` `CONFIG.LOCATION.SEAM.two-families=runtime configHomes (where a third-party runtime keeps config, registry- or descriptor-declared) and MSD's OWN location vars (MSD_HOME -> $MSD_HOME/.msd store, MSD_AGENTS_DIR -> getAgentsDir priority 1) are DISTINCT families; no registry derivation reaches the second, and treating a miss there as a registry gap is what produced review round 2` -`CONFIG.LOCATION.SEAM.kimi-two-homes=kimi declares TWO config-location vars: KIMI_CONFIG_DIR (registry, generic Agent-Skills root via resolveKimiGlobalDir) and KIMI_SHARE_DIR (KIMI_HOOKS_TOML_DESCRIPTOR, kimi's OWN native config.toml carrying MSD's [[hooks]] block via resolveKimiHooksTomlDir); a registry-only derivation covers the first and silently misses the second` `CONFIG.LOCATION.SEAM.in-process-scrub=TEST_ENV_BASE reaches CHILD env only; a test calling install() IN-PROCESS must additionally use helpers.scrubConfigLocationEnv() in beforeEach + its restorer in afterEach — HOME/USERPROFILE sandboxing is NOT sufficient because getGlobalConfigDir is env-FIRST` `LIVE-CONFIG.GUARD.SEAM.module=scripts/live-config-guard.cjs (deliberately NOT scripts/lib/, which the installer copies to users wholesale while uninstall removes only an allowlist; excluded from the npm tarball via package.json files[] together with its whole require chain run-tests.cjs/affected-tests-lib.cjs/run-affected-tests.cjs — a partial exclusion trips the #2858 shipped-requires-only-shipped gate); exports [resolveLiveConfigRoots, resolveExtraWatchTargets, snapshotLiveConfig, diffLiveConfig, formatViolations, newestMtime]; driven by scripts/run-tests.cjs pre/post suite` `LIVE-CONFIG.GUARD.SEAM.scope=ownership-based, never whole-root: MSD_OWNED_ENTRIES top-level footprint + children whose name startsWith MSD_ARTIFACT_PREFIX ('msd-') under MSD_PREFIXED_PARENTS (dirs shared with the host agent); watching a shared root wholesale false-positives on the host's own writes and a guard that cries wolf gets disabled` -`LIVE-CONFIG.GUARD.SEAM.non-root-targets=resolveExtraWatchTargets covers THREE live write surfaces that are not runtime config ROOTS (skills bases are a DELIBERATE non-target — the config-root layout misfires beneath them, so they need their own layout): $MSD_HOME/.msd watched WHOLESALE (exclusively MSD-owned, so the shared-root trap does not apply) plus ONE config.toml per NON_REGISTRY_CONFIG_HOME_DESCRIPTORS entry, each watched as a SINGLE FILE (those roots belong to their products) — today three targets, since #2755 split Kimi CLI (~/.kimi, KIMI_SHARE_DIR) from Kimi Code (~/.kimi-code, KIMI_CODE_HOME); the targets are DERIVED by iterating that array, never by calling a named resolver, so a further descriptor is picked up without editing the guard PROVIDED it owns the same NON_REGISTRY_OWNED_FILE ('config.toml') — one that owns a different filename needs a per-descriptor mapping, the named residual the guard states at its own definition. SECOND RESIDUAL: config.toml is not all MSD writes into those roots — installSharedHooksBundle also populates /hooks/, which is UNWATCHED; closing it is a layout decision, like skills bases; passed to snapshotLiveConfig explicitly so a fixture-root caller cannot pull the real ~/.msd into its snapshot` +`LIVE-CONFIG.GUARD.SEAM.non-root-targets=resolveExtraWatchTargets covers THREE live write surfaces that are not runtime config ROOTS (skills bases are a DELIBERATE non-target — the config-root layout misfires beneath them, so they need their own layout): $MSD_HOME/.msd watched WHOLESALE (exclusively MSD-owned, so the shared-root trap does not apply) plus ONE config.toml per NON_REGISTRY_CONFIG_HOME_DESCRIPTORS entry, each watched as a SINGLE FILE (those roots belong to their products); the targets are DERIVED by iterating that array, never by calling a named resolver, so a further descriptor is picked up without editing the guard PROVIDED it owns the same NON_REGISTRY_OWNED_FILE ('config.toml') — one that owns a different filename needs a per-descriptor mapping, the named residual the guard states at its own definition. SECOND RESIDUAL: config.toml is not all MSD writes into those roots — installSharedHooksBundle also populates /hooks/, which is UNWATCHED; closing it is a layout decision, like skills bases; passed to snapshotLiveConfig explicitly so a fixture-root caller cannot pull the real ~/.msd into its snapshot` `LIVE-CONFIG.GUARD.SEAM.truncation=MAX_ENTRIES/MAX_DEPTH bound the walk; a bound hit sets truncated and diffLiveConfig emits kind:'unverified' — a truncated scan MUST NOT read as clean; boundary covered at {limit-1,limit,limit+1} via newestMtime's injected budget plus fast-check monotonicity, per RULESET.TESTS.boundary-coverage + RULESET.TESTS.property-based-testing` `LIVE-CONFIG.GUARD.SEAM.severity=reports by default locally; CI wires MSD_STRICT_LIVE_CONFIG_GUARD=1 on Linux/macOS lanes (test.yml, all three test jobs) so a suite-produced leak FAILS those runs; Windows lanes stay report-only pending the documented pre-existing USERPROFILE sweep (~190 test sites sandbox HOME alone) — promote once that lands; skipped by MSD_SKIP_LIVE_CONFIG_GUARD=1` `LIVE-CONFIG.GUARD.SEAM.ci-blind=the AMBIENT-ENV half stays CI-blind — CI never has these vars set, so green CI is not evidence for it; what strict mode catches in CI is the suite's own default-root leaks (HOME/USERPROFILE-derived), the guard remains the only loud signal for ambient-var escapes` diff --git a/docs/CONTEXT-INDEX.json b/docs/CONTEXT-INDEX.json index 91b86554b..ab092c01d 100644 --- a/docs/CONTEXT-INDEX.json +++ b/docs/CONTEXT-INDEX.json @@ -1,10 +1,10 @@ { "schemaVersion": 1, - "count": 286, + "count": 285, "classes": { "ARCH": 1, "CI": 2, - "CONFIG": 5, + "CONFIG": 4, "EXEC": 8, "LEARNING": 1, "LIVE-CONFIG": 6, @@ -46,11 +46,6 @@ "klass": "CONFIG", "value": "TEST_ENV_BASE reaches CHILD env only; a test calling install() IN-PROCESS must additionally use helpers.scrubConfigLocationEnv() in beforeEach + its restorer in afterEach — HOME/USERPROFILE sandboxing is NOT sufficient because getGlobalConfigDir is env-FIRST" }, - { - "id": "CONFIG.LOCATION.SEAM.kimi-two-homes", - "klass": "CONFIG", - "value": "kimi declares TWO config-location vars: KIMI_CONFIG_DIR (registry, generic Agent-Skills root via resolveKimiGlobalDir) and KIMI_SHARE_DIR (KIMI_HOOKS_TOML_DESCRIPTOR, kimi's OWN native config.toml carrying MSD's [[hooks]] block via resolveKimiHooksTomlDir); a registry-only derivation covers the first and silently misses the second" - }, { "id": "CONFIG.LOCATION.SEAM.scrub-set", "klass": "CONFIG", @@ -124,7 +119,7 @@ { "id": "LIVE-CONFIG.GUARD.SEAM.non-root-targets", "klass": "LIVE-CONFIG", - "value": "resolveExtraWatchTargets covers THREE live write surfaces that are not runtime config ROOTS (skills bases are a DELIBERATE non-target — the config-root layout misfires beneath them, so they need their own layout): $MSD_HOME/.msd watched WHOLESALE (exclusively MSD-owned, so the shared-root trap does not apply) plus ONE config.toml per NON_REGISTRY_CONFIG_HOME_DESCRIPTORS entry, each watched as a SINGLE FILE (those roots belong to their products) — today three targets, since #2755 split Kimi CLI (~/.kimi, KIMI_SHARE_DIR) from Kimi Code (~/.kimi-code, KIMI_CODE_HOME); the targets are DERIVED by iterating that array, never by calling a named resolver, so a further descriptor is picked up without editing the guard PROVIDED it owns the same NON_REGISTRY_OWNED_FILE ('config.toml') — one that owns a different filename needs a per-descriptor mapping, the named residual the guard states at its own definition. SECOND RESIDUAL: config.toml is not all MSD writes into those roots — installSharedHooksBundle also populates /hooks/, which is UNWATCHED; closing it is a layout decision, like skills bases; passed to snapshotLiveConfig explicitly so a fixture-root caller cannot pull the real ~/.msd into its snapshot" + "value": "resolveExtraWatchTargets covers THREE live write surfaces that are not runtime config ROOTS (skills bases are a DELIBERATE non-target — the config-root layout misfires beneath them, so they need their own layout): $MSD_HOME/.msd watched WHOLESALE (exclusively MSD-owned, so the shared-root trap does not apply) plus ONE config.toml per NON_REGISTRY_CONFIG_HOME_DESCRIPTORS entry, each watched as a SINGLE FILE (those roots belong to their products); the targets are DERIVED by iterating that array, never by calling a named resolver, so a further descriptor is picked up without editing the guard PROVIDED it owns the same NON_REGISTRY_OWNED_FILE ('config.toml') — one that owns a different filename needs a per-descriptor mapping, the named residual the guard states at its own definition. SECOND RESIDUAL: config.toml is not all MSD writes into those roots — installSharedHooksBundle also populates /hooks/, which is UNWATCHED; closing it is a layout decision, like skills bases; passed to snapshotLiveConfig explicitly so a fixture-root caller cannot pull the real ~/.msd into its snapshot" }, { "id": "LIVE-CONFIG.GUARD.SEAM.scope", diff --git a/eslint-rules/lib/portability-vocab.cjs b/eslint-rules/lib/portability-vocab.cjs index e03d55c8c..359750226 100644 --- a/eslint-rules/lib/portability-vocab.cjs +++ b/eslint-rules/lib/portability-vocab.cjs @@ -60,17 +60,15 @@ const PATH_RETURNING_FNS = [ // #2088 (ADR-1239 upgrade 3): resolves the on-disk skills-install dir honoring // a skills-kind `home` override (e.g. Codex → $HOME/.agents/skills). '_resolveSkillsRootDir', - // #3664: shared kind-destination resolver (skills/agents/kimi-agents kinds). + // #3664: shared kind-destination resolver (skills/agents kinds). '_kindDestDir', 'getGlobalSkillDir', 'getGlobalSkillDisplayPath', 'resolveSkillsBaseFromDescriptor', 'resolveConfigHomeFromDescriptor', 'resolveKimiGlobalDir', - // #2095 (EoS/kimi): resolves the directory holding Kimi CLI's OWN native - // config.toml (~/.kimi by default, KIMI_SHARE_DIR override) — a sibling of, - // and deliberately separate from, resolveKimiGlobalDir's generic - // Agent-Skills root above. + // #2095: resolves the directory holding a host's OWN native config.toml — a + // sibling of, and deliberately separate from, the generic Agent-Skills root above. 'resolveKimiHooksTomlDir', 'resolveAntigravityGlobalDir', 'getGlobalDir', @@ -190,7 +188,7 @@ function isPathReturningCall(node) { if (PATH_RETURNING_FNS.includes(dotted)) return true; } - // Bare call: getGlobalConfigDir(), resolveKimiGlobalDir(), etc. + // Bare call: getGlobalConfigDir(), getGlobalSkillsBase(), etc. if (callee.type === 'Identifier') { if (PATH_RETURNING_FNS.includes(callee.name)) return true; } diff --git a/eslint-rules/normalize-path-in-content.cjs b/eslint-rules/normalize-path-in-content.cjs index bf70762eb..23c3f70ed 100644 --- a/eslint-rules/normalize-path-in-content.cjs +++ b/eslint-rules/normalize-path-in-content.cjs @@ -121,7 +121,7 @@ function isContentPathReturningCall(node) { if (CONTENT_PATH_FNS.has(dotted)) return true; } - // Bare call: getGlobalConfigDir(), resolveKimiGlobalDir(), etc. + // Bare call: getGlobalConfigDir(), getGlobalSkillsBase(), etc. if (callee.type === 'Identifier') { if (CONTENT_PATH_FNS.has(callee.name)) return true; } diff --git a/examples/dynamic-context-management/CONTEXT-INDEX.json b/examples/dynamic-context-management/CONTEXT-INDEX.json index 9d0e30f81..60e0b6871 100644 --- a/examples/dynamic-context-management/CONTEXT-INDEX.json +++ b/examples/dynamic-context-management/CONTEXT-INDEX.json @@ -1,15 +1,15 @@ { "schemaVersion": 1, - "count": 286, + "count": 285, "classes": { "ARCH": 1, "CI": 2, - "CONFIG": 5, + "CONFIG": 4, "EXEC": 8, - "MSD-RESEARCH": 6, "LEARNING": 1, "LIVE-CONFIG": 6, "META": 4, + "MSD-RESEARCH": 6, "PHASE": 8, "PLANNING": 3, "PR": 2, @@ -30,1483 +30,1477 @@ "id": "ARCH.SKILL.improve-codebase.next-candidates", "klass": "ARCH", "value": "[Workstream Progress Projection Module]", - "line": 721 + "line": 729 }, { "id": "CI.GATE.changeset-lint", "klass": "CI", "value": "hard-fail for user-facing code diffs unless .changeset/* or PR has no-changelog label", - "line": 705 + "line": 713 }, { "id": "CI.GATE.issue-link-required", "klass": "CI", "value": "hard-fail if PR body lacks closes/fixes/resolves #", - "line": 704 + "line": 712 }, { "id": "CONFIG.LOCATION.SEAM.in-process-scrub", "klass": "CONFIG", "value": "TEST_ENV_BASE reaches CHILD env only; a test calling install() IN-PROCESS must additionally use helpers.scrubConfigLocationEnv() in beforeEach + its restorer in afterEach — HOME/USERPROFILE sandboxing is NOT sufficient because getGlobalConfigDir is env-FIRST", - "line": 741 - }, - { - "id": "CONFIG.LOCATION.SEAM.kimi-two-homes", - "klass": "CONFIG", - "value": "kimi declares TWO config-location vars: KIMI_CONFIG_DIR (registry, generic Agent-Skills root via resolveKimiGlobalDir) and KIMI_SHARE_DIR (KIMI_HOOKS_TOML_DESCRIPTOR, kimi's OWN native config.toml carrying MSD's [[hooks]] block via resolveKimiHooksTomlDir); a registry-only derivation covers the first and silently misses the second", - "line": 740 + "line": 748 }, { "id": "CONFIG.LOCATION.SEAM.scrub-set", "klass": "CONFIG", "value": "tests/helpers.cjs CONFIG_LOCATION_ENV_KEYS is DERIVED from five sources rather than maintained as one hand-written list (source 4 IS a literal residue list, for vars that fit no other rung — what is never hand-listed is the SET): capability-registry runtimes[].runtime.configHome.env AND [].configHome.skillsHome.env + runtime-homes NON_REGISTRY_CONFIG_HOME_DESCRIPTORS[].env AND [].skillsHome.env (a descriptor is a descriptor — BOTH descriptor rungs walk skillsHome, which resolves independently via resolveSkillsBaseFromDescriptor) + runtime-homes MSD_LOCATION_ENV_KEYS + a residue list (GROK_AGENTS_HOME, MSD_RUNTIME, MSD_PROJECT, MSD_WORKSTREAM) + WRITE_ESCAPE_PERMISSION_ENV_KEYS (MSD_ALLOW_SYMLINKED_DEST — a permission, not a location: it names no path but disarms the symlink-escape guard, so blanking it makes the guard STRICTER, never looser); adding a config-location var means making it ENUMERABLE at one of those sources, not appending a literal", - "line": 738 + "line": 746 }, { "id": "CONFIG.LOCATION.SEAM.two-families", "klass": "CONFIG", "value": "runtime configHomes (where a third-party runtime keeps config, registry- or descriptor-declared) and MSD's OWN location vars (MSD_HOME -> $MSD_HOME/.msd store, MSD_AGENTS_DIR -> getAgentsDir priority 1) are DISTINCT families; no registry derivation reaches the second, and treating a miss there as a registry gap is what produced review round 2", - "line": 739 + "line": 747 }, { "id": "CONFIG.SEAM.loadConfig-context", "klass": "CONFIG", "value": "loadConfig(cwd,{workstream}) replaces env-mutation fallback; no temporary process.env MSD_WORKSTREAM rewrites", - "line": 737 + "line": 745 }, { "id": "EXEC.CLASSIFY.classes", "klass": "EXEC", "value": "{class:'quota-exceeded'|'classify-handoff-bug'|'unknown-failure', sentinel?, retryAfterSeconds?}", - "line": 967 + "line": 974 }, { "id": "EXEC.CLASSIFY.cross-runtime", "klass": "EXEC", "value": "Anthropic/CC: usage limit|rate limit|quota|429|retry-after; Copilot CLI: rate_limit (stem); Codex CLI: 429|usage_limit_reached|too many requests", - "line": 969 + "line": 976 }, { "id": "EXEC.CLASSIFY.handler", "klass": "EXEC", "value": "msd-core/bin/lib/agent-command-router.cjs:classifyAgentFailure (registered via command-aliases.cjs; mutation:false outputMode:json)", - "line": 965 + "line": 972 }, { "id": "EXEC.CLASSIFY.precedence", "klass": "EXEC", "value": "quota sentinel wins over classifyHandoffIfNeeded bug when both appear", - "line": 970 + "line": 977 }, { "id": "EXEC.CLASSIFY.proactive-signal-not-usable", "klass": "EXEC", "value": "Anthropic exposes anthropic-ratelimit-* headers + Agent SDK RateLimitEvent; Claude Code subprocess does NOT forward to hooks/statusline today (upstream #33820, #22407, #32796)", - "line": 972 + "line": 979 }, { "id": "EXEC.CLASSIFY.retry-after-parser", "klass": "EXEC", "value": "\\bretry[-_ ]after[:\\s]+(\\d+)\\b avoids embedded-word false matches like noretry-after", - "line": 971 + "line": 978 }, { "id": "EXEC.CLASSIFY.sentinel-order", "klass": "EXEC", "value": "most specific first: 429 beats too-many-requests; resource_exhausted beats quota (array order in src/agent-command-router.cts QUOTA_SENTINELS checks resource_exhausted before quota); case-insensitive; canonical sentinel value is lower-cased form", - "line": 968 + "line": 975 }, { "id": "EXEC.CLASSIFY.workflow", "klass": "EXEC", "value": "msd-core/workflows/execute-phase.md step 7; class-distinct prompts (quota-to-wait-for-reset; classify-handoff-bug-to-spot-check; unknown-to-continue/stop)", - "line": 966 - }, - { - "id": "MSD-RESEARCH.CONTEXT-DISCIPLINE", - "klass": "MSD-RESEARCH", - "value": "less-context levers: subagent isolation + compact provider output + fetches-to-disk + cache-returns-digest; API clear_tool_uses/memory tool are the conceptual model, not a Claude Code harness knob", - "line": 486 - }, - { - "id": "MSD-RESEARCH.INTEGRATION.L2-hybrid", - "klass": "MSD-RESEARCH", - "value": "code owns cache+legitimacy+confidence+provider-pick (msd-tools query research-plan/research-store/package-legitimacy); MCP owns the fetch; agent returns RESEARCH.md path, never raw fetches", - "line": 484 - }, - { - "id": "MSD-RESEARCH.MODULE.package-legitimacy", - "klass": "MSD-RESEARCH", - "value": "registry-API verdicts (npm/PyPI/crates.io injectable adapters) computed from thresholds {minAgeDays:30,minWeeklyDownloads:1000,requireRepo:true}; verdict OK|SUS|SLOP per package; slopcheck=optional adapter that can only escalate, never the install-or-degrade gate", - "line": 483 - }, - { - "id": "MSD-RESEARCH.MODULE.research-provider", - "klass": "MSD-RESEARCH", - "value": "single source of truth PROVIDER_WATERFALL (docs Context7->Ref->Jina->websearch; web Exa->Tavily->Perplexity->Brave->websearch; scrape Firecrawl->Jina); planResearch returns cache-hits+fetch-plan; classifyConfidence stamps HIGH|MEDIUM|LOW by provider AUTHORITY + verification EVIDENCE (HIGH requires code-computed ground-truth corroboration e.g. legitimacyVerdict OK; provider authority alone caps at MEDIUM; SLOP caps at LOW); Firecrawl is scrape-only (not in the docs or web legs)", - "line": 482 - }, - { - "id": "MSD-RESEARCH.MODULE.research-store", - "klass": "MSD-RESEARCH", - "value": "content-addressed cache; key=sha256(ecosystem+library+version+query+kind); getResearch->{hit,stale} never throws (mirrors graphify staleness); ttlForSource curated HIGH 30d|MED 7d|web LOW 1d; tiers: curated-doc kinds -> ~/.msd/research-cache (cross-project), web/synthesis -> project .planning/research/.cache", - "line": 481 - }, - { - "id": "MSD-RESEARCH.PROVIDER.availability", - "klass": "MSD-RESEARCH", - "value": "config flags brave_search/exa_search/firecrawl/tavily_search/ref_search/perplexity/jina (env _API_KEY or ~/.msd/_api_key); context7/jina/websearch always available; planResearch falls through waterfall to websearch terminal", - "line": 485 + "line": 973 }, { "id": "LEARNING.prompt-budget.boundary-gap", "klass": "LEARNING", "value": "PR #3708 commit 2df566ed reserved NOTE_RESERVE_TOKENS in pressure-threshold AND in minSet pre-check; both buggy paths only fire when baseTokens ∈ (effectiveBudget - NOTE_RESERVE_TOKENS, effectiveBudget]; original test suite used budgets far from that band so neither path was exercised; fix bde1ae8f confines NOTE_RESERVE accounting to post-trim assembly path only; future budget/limit code MUST add boundary fixtures per RULESET.TESTS.boundary-coverage.fixtures", - "line": 650 + "line": 658 }, { "id": "LIVE-CONFIG.GUARD.SEAM.ci-blind", "klass": "LIVE-CONFIG", "value": "the AMBIENT-ENV half stays CI-blind — CI never has these vars set, so green CI is not evidence for it; what strict mode catches in CI is the suite's own default-root leaks (HOME/USERPROFILE-derived), the guard remains the only loud signal for ambient-var escapes", - "line": 747 + "line": 754 }, { "id": "LIVE-CONFIG.GUARD.SEAM.module", "klass": "LIVE-CONFIG", "value": "scripts/live-config-guard.cjs (deliberately NOT scripts/lib/, which the installer copies to users wholesale while uninstall removes only an allowlist; excluded from the npm tarball via package.json files[] together with its whole require chain run-tests.cjs/affected-tests-lib.cjs/run-affected-tests.cjs — a partial exclusion trips the #2858 shipped-requires-only-shipped gate); exports [resolveLiveConfigRoots, resolveExtraWatchTargets, snapshotLiveConfig, diffLiveConfig, formatViolations, newestMtime]; driven by scripts/run-tests.cjs pre/post suite", - "line": 742 + "line": 749 }, { "id": "LIVE-CONFIG.GUARD.SEAM.non-root-targets", "klass": "LIVE-CONFIG", - "value": "resolveExtraWatchTargets covers THREE live write surfaces that are not runtime config ROOTS (skills bases are a DELIBERATE non-target — the config-root layout misfires beneath them, so they need their own layout): $MSD_HOME/.msd watched WHOLESALE (exclusively MSD-owned, so the shared-root trap does not apply) plus ONE config.toml per NON_REGISTRY_CONFIG_HOME_DESCRIPTORS entry, each watched as a SINGLE FILE (those roots belong to their products) — today three targets, since #2755 split Kimi CLI (~/.kimi, KIMI_SHARE_DIR) from Kimi Code (~/.kimi-code, KIMI_CODE_HOME); the targets are DERIVED by iterating that array, never by calling a named resolver, so a further descriptor is picked up without editing the guard PROVIDED it owns the same NON_REGISTRY_OWNED_FILE ('config.toml') — one that owns a different filename needs a per-descriptor mapping, the named residual the guard states at its own definition. SECOND RESIDUAL: config.toml is not all MSD writes into those roots — installSharedHooksBundle also populates /hooks/, which is UNWATCHED; closing it is a layout decision, like skills bases; passed to snapshotLiveConfig explicitly so a fixture-root caller cannot pull the real ~/.msd into its snapshot", - "line": 744 + "value": "resolveExtraWatchTargets covers THREE live write surfaces that are not runtime config ROOTS (skills bases are a DELIBERATE non-target — the config-root layout misfires beneath them, so they need their own layout): $MSD_HOME/.msd watched WHOLESALE (exclusively MSD-owned, so the shared-root trap does not apply) plus ONE config.toml per NON_REGISTRY_CONFIG_HOME_DESCRIPTORS entry, each watched as a SINGLE FILE (those roots belong to their products); the targets are DERIVED by iterating that array, never by calling a named resolver, so a further descriptor is picked up without editing the guard PROVIDED it owns the same NON_REGISTRY_OWNED_FILE ('config.toml') — one that owns a different filename needs a per-descriptor mapping, the named residual the guard states at its own definition. SECOND RESIDUAL: config.toml is not all MSD writes into those roots — installSharedHooksBundle also populates /hooks/, which is UNWATCHED; closing it is a layout decision, like skills bases; passed to snapshotLiveConfig explicitly so a fixture-root caller cannot pull the real ~/.msd into its snapshot", + "line": 751 }, { "id": "LIVE-CONFIG.GUARD.SEAM.scope", "klass": "LIVE-CONFIG", "value": "ownership-based, never whole-root: MSD_OWNED_ENTRIES top-level footprint + children whose name startsWith MSD_ARTIFACT_PREFIX ('msd-') under MSD_PREFIXED_PARENTS (dirs shared with the host agent); watching a shared root wholesale false-positives on the host's own writes and a guard that cries wolf gets disabled", - "line": 743 + "line": 750 }, { "id": "LIVE-CONFIG.GUARD.SEAM.severity", "klass": "LIVE-CONFIG", "value": "reports by default locally; CI wires MSD_STRICT_LIVE_CONFIG_GUARD=1 on Linux/macOS lanes (test.yml, all three test jobs) so a suite-produced leak FAILS those runs; Windows lanes stay report-only pending the documented pre-existing USERPROFILE sweep (~190 test sites sandbox HOME alone) — promote once that lands; skipped by MSD_SKIP_LIVE_CONFIG_GUARD=1", - "line": 746 + "line": 753 }, { "id": "LIVE-CONFIG.GUARD.SEAM.truncation", "klass": "LIVE-CONFIG", "value": "MAX_ENTRIES/MAX_DEPTH bound the walk; a bound hit sets truncated and diffLiveConfig emits kind:'unverified' — a truncated scan MUST NOT read as clean; boundary covered at {limit-1,limit,limit+1} via newestMtime's injected budget plus fast-check monotonicity, per RULESET.TESTS.boundary-coverage + RULESET.TESTS.property-based-testing", - "line": 745 + "line": 752 }, { "id": "META.RULE.brief-must-cite-doc", "klass": "META", "value": "agent prompts MUST quote the canonical doc line being applied; paraphrasing from predicate memory drifts and produces violations", - "line": 806 + "line": 813 }, { "id": "META.RULE.brief-no-paraphrase", "klass": "META", "value": "writing \"k040 — never leave changelog box unchecked\" caused 5 of 8 agents to edit CHANGELOG.md in violation of CONTRIBUTING.md L110", - "line": 807 + "line": 814 }, { "id": "META.RULE.canonical-source-precedence", "klass": "META", "value": "CONTRIBUTING.md > docs/adr/* > CONTEXT.md > agent memory", - "line": 804 + "line": 811 }, { "id": "META.RULE.read-contributing-first", "klass": "META", "value": "read CONTRIBUTING.md sections \"Pull Request Guidelines\" + \"CHANGELOG Entries\" before EVERY agent dispatch", - "line": 805 + "line": 812 + }, + { + "id": "MSD-RESEARCH.CONTEXT-DISCIPLINE", + "klass": "MSD-RESEARCH", + "value": "less-context levers: subagent isolation + compact provider output + fetches-to-disk + cache-returns-digest; API clear_tool_uses/memory tool are the conceptual model, not a Claude Code harness knob", + "line": 491 + }, + { + "id": "MSD-RESEARCH.INTEGRATION.L2-hybrid", + "klass": "MSD-RESEARCH", + "value": "code owns cache+legitimacy+confidence+provider-pick (msd-tools query research-plan/research-store/package-legitimacy); MCP owns the fetch; agent returns RESEARCH.md path, never raw fetches", + "line": 489 + }, + { + "id": "MSD-RESEARCH.MODULE.package-legitimacy", + "klass": "MSD-RESEARCH", + "value": "registry-API verdicts (npm/PyPI/crates.io injectable adapters) computed from thresholds {minAgeDays:30,minWeeklyDownloads:1000,requireRepo:true}; verdict OK|SUS|SLOP per package; slopcheck=optional adapter that can only escalate, never the install-or-degrade gate", + "line": 488 + }, + { + "id": "MSD-RESEARCH.MODULE.research-provider", + "klass": "MSD-RESEARCH", + "value": "single source of truth PROVIDER_WATERFALL (docs Context7->Ref->Jina->websearch; web Exa->Tavily->Perplexity->Brave->websearch; scrape Firecrawl->Jina); planResearch returns cache-hits+fetch-plan; classifyConfidence stamps HIGH|MEDIUM|LOW by provider AUTHORITY + verification EVIDENCE (HIGH requires code-computed ground-truth corroboration e.g. legitimacyVerdict OK; provider authority alone caps at MEDIUM; SLOP caps at LOW); Firecrawl is scrape-only (not in the docs or web legs)", + "line": 487 + }, + { + "id": "MSD-RESEARCH.MODULE.research-store", + "klass": "MSD-RESEARCH", + "value": "content-addressed cache; key=sha256(ecosystem+library+version+query+kind); getResearch->{hit,stale} never throws (mirrors graphify staleness); ttlForSource curated HIGH 30d|MED 7d|web LOW 1d; tiers: curated-doc kinds -> ~/.msd/research-cache (cross-project), web/synthesis -> project .planning/research/.cache", + "line": 486 + }, + { + "id": "MSD-RESEARCH.PROVIDER.availability", + "klass": "MSD-RESEARCH", + "value": "config flags brave_search/exa_search/firecrawl/tavily_search/ref_search/perplexity/jina (env _API_KEY or ~/.msd/_api_key); context7/jina/websearch always available; planResearch falls through waterfall to websearch terminal", + "line": 490 }, { "id": "PHASE.REQ-LINE.SEAM.cap", "klass": "PHASE", "value": "REQ_TOKEN_SCAN_LIMIT=2048 bounds every predicate including each range participant's NEIGHBOURS, not just the operator; a bound hit sets oversizedTokens and takes the unverified kind — an unexaminable token MUST NOT read as clean, and the cap bounds the WORK, never the warning; boundary covered at {2047,2048,2049} across BOTH capped predicate families per RULESET.TESTS.boundary-coverage", - "line": 752 + "line": 759 }, { "id": "PHASE.REQ-LINE.SEAM.census-domains", "klass": "PHASE", "value": "TWO open domains, each censused in-source with its NOT-reached consequence: range-operator spellings (reached: ..+, seven Unicode dashes plus ASCII -, …, to/thru/through; not reached: →, ~, ..=, ..<, until, up to) and comma-SUBSTITUTE separators (round 4's 26-spelling sweep concluded 'exactly ; and : ' because it swept the ONE-SIDED form for ;/: and only the BARE and SYMMETRIC forms for every other separator — different members tested in different shapes, so the answer was forced; re-swept round 5 FULLY CROSSED at 21 separators x {bare,trailing-space,leading-space,both} = 84, driven: 26 select both, 24 already warn, 34 UNDER-SELECT SILENTLY and all 34 are one-sided attachment — | / + & \\ > . ! ? • · ؛ ; , - ~ and/plus — so R4 covers TWO CHARACTERS of a WIDE-OPEN domain, never the whole of it); R4's own NOT-reached set is therefore styling-only decoration, every non-;/: attachment, anything inside a MATCHED parenthetical, and any decorated id whose prefix is on NO selected id (REQ-01, FOO-02: x stays silent even when FOO-02 is real); the prefix gate is NOT complete in the other direction either — a citation SHARING a selected prefix (REQ-01, see REQ-7: sec 3) still fires and nothing at token level separates it from a real drop", - "line": 754 + "line": 761 }, { "id": "PHASE.REQ-LINE.SEAM.gap-checker-divergence", "klass": "PHASE", "value": "normalizePhaseReqIds (src/gap-checker.cts) is a SECOND parser of the same ROADMAP value and DIVERGES on four axes — ranges (expanded there, never here, deliberately per #3697) | placeholder vocabulary (whole trimmed value after stripping parens there, LEAD token here) | parentheses (stripped there, not here) | ID shape (PHASE_REQ_ID_SHAPE_RE is wider) — pinned in BOTH directions by #3697-17 rather than unified, because unifying would change what phase.complete MARKS; consequence is user-visible: RANGE-01..RANGE-05 reports 5 requirements to gap analysis and 0 to phase complete", - "line": 755 + "line": 762 }, { "id": "PHASE.REQ-LINE.SEAM.kinds", "klass": "PHASE", "value": "three, carried as a machine code BESIDE the prose, never instead of it — req-line-misparse (ID-shaped content demonstrably not selected) | req-line-range-reading (R2 alone fired on selected endpoints and NO RULE NAMED A DROPPED ID — rule-scoped, never line-global: an unselected parenthetical is carried by the skipped-text rider, not by this code — so the voice must NOT claim a parse failure; it defers to req-line-unverified when the cap left a token UNCLASSIFIED, which is narrower than 'a token past the cap' — a long token the SELECTOR ITSELF took can be exempt, so the condition is oversizedTokens being non-empty, never the mere presence of a long token — see SEAM.cap for the exemption's own rule) | req-line-unverified (a token past the cap: the line was not classified, which is not the same as clean); emitted as the additive result field requirements_line_warning, because warnings[] is a documented string[] rendered by execute-phase.md and re-typing its elements is a breaking output-contract change; ABSENT entirely on a clean line", - "line": 751 + "line": 758 }, { "id": "PHASE.REQ-LINE.SEAM.module", "klass": "PHASE", "value": "src/phase.cts owns the ROADMAP **Requirements**: line seam as TWO module-scope functions, extracted so the parser is directly testable (a closure inside cmdPhaseComplete is reachable only by spawning the CLI, which no fast-check property can do): analyzeRequirementsLine(rawLine) -> RequirementsLineAnalysis, formatRequirementsLineWarning(phaseNum,rawLine,analysis) -> {code,message}|null; both exported, plus REQ_LINE_WARNING_CODE", - "line": 748 + "line": 755 }, { "id": "PHASE.REQ-LINE.SEAM.placeholder", "klass": "PHASE", "value": "TBD/NONE as the LEAD token only, and R3b's non-empty test keys on VISIBLE content (a line of only zero-width/bidi/variation-selector codepoints is empty; an invisible INSIDE a token is decoration and R4 reports the drop); that gate — not the ID-shape gate — is what holds the whole #2334/#2339 negative space silent under R3b (measured: 15 of 15 fixtures held by non-zero selection or placeholderLed, 0 by ID shape)", - "line": 753 + "line": 760 }, { "id": "PHASE.REQ-LINE.SEAM.rules", "klass": "PHASE", "value": "R1 whole-token range | R2 spaced operator between two selected interior-implying endpoints | R2' operator glued to one endpoint | R3 zero selection with ID-shaped residue | R3b zero selection on any non-placeholder non-empty line (#3697 AC-1b/AC-4) | R4 an ID the selector dropped to DECORATION — the TRIGGER is exactly a glued ;/: at either end OR an embedded invisible, never styling: quotes/backticks/emphasis are TOLERATED around the id (shaved before the test) but do NOT fire on their own, so `**REQ-01**, REQ-02` and `**REQ-01**; REQ-02` are BOTH silent — plus outside any MATCHED parenthetical AND sharing a prefix with a SELECTED id (square brackets stripped as the selector strips them; parentheses deliberately NOT, they are the citation marker) | over-cap unclassified; warn is their disjunction, named rather than inlined in the return literal", - "line": 750 + "line": 757 }, { "id": "PHASE.REQ-LINE.SEAM.selector-identity", "klass": "PHASE", "value": "citedReqIds is BYTE-IDENTICAL to the pre-extraction expression and is the ONLY thing that reaches the ledger; every rule below adds to warnings[] and NOTHING else — a change that alters what phase.complete MARKS is out of this seam's contract, not a refinement of it", - "line": 749 + "line": 756 }, { "id": "PLANNING.PATH.PARITY.project-scope", "klass": "PLANNING", "value": ".planning/ (never .planning/projects/); mirror planning-workspace.cjs planningDir()", - "line": 732 + "line": 740 }, { "id": "PLANNING.PATH.SEAM.helpers", "klass": "PLANNING", "value": "helpers.planningPaths delegates to workspacePlanningPaths + resolveWorkspaceContext; precedence explicit-ws > env-ws > env-project > root", - "line": 733 + "line": 741 }, { "id": "PLANNING.PATH.SEAM.init-handlers", "klass": "PLANNING", "value": "[initExecutePhase, initPlanPhase, initPhaseOp, initMilestoneOp] consume helpers.planningPaths().planning (no direct relPlanningPath join)", - "line": 734 + "line": 742 }, { "id": "PR.3267.POSTMORTEM.recovery", "klass": "PR", "value": "[issue#3270 created, label approved-enhancement applied, PR reopened, body includes \"Closes #3270\", label no-changelog applied]", - "line": 709 + "line": 717 }, { "id": "PR.3267.POSTMORTEM.root-cause", "klass": "PR", "value": "[missing issue link, missing changeset/no-changelog]", - "line": 708 + "line": 716 }, { "id": "PRED.k320.canonical-source", "klass": "PRED", "value": "CONTRIBUTING.md L193-211", - "line": 810 + "line": 817 }, { "id": "PRED.k320.ci-enforcement", "klass": "PRED", "value": "scripts/changeset/lint.cjs", - "line": 816 + "line": 823 }, { "id": "PRED.k320.ci-paths-monitored", "klass": "PRED", "value": "bin/ msd-core/ src/ agents/ commands/ hooks/ sdk/src/ sdk/prompts/", - "line": 817 + "line": 824 }, { "id": "PRED.k320.cure", "klass": "PRED", "value": "drop .changeset/--.md fragment ONLY", - "line": 812 + "line": 819 }, { "id": "PRED.k320.evidence", "klass": "PRED", "value": "PR #3302 merge-conflict against #3308 CHANGELOG.md row 2026-05-09", - "line": 819 + "line": 826 }, { "id": "PRED.k320.opt-out-label", "klass": "PRED", "value": "no-changelog", - "line": 815 + "line": 822 }, { "id": "PRED.k320.recovery", "klass": "PRED", "value": "open Removed-typed cleanup PR deleting only the redundant row", - "line": 818 + "line": 825 }, { "id": "PRED.k320.rule", "klass": "PRED", "value": "do not edit CHANGELOG.md in feature/fix/enhancement PRs", - "line": 811 + "line": 818 }, { "id": "PRED.k320.signal", "klass": "PRED", "value": "changelog-direct-edit-forbidden", - "line": 809 + "line": 816 }, { "id": "PRED.k320.tool", "klass": "PRED", "value": "npm run changeset -- --type --pr --body \"...\"", - "line": 813 + "line": 820 }, { "id": "PRED.k320.types", "klass": "PRED", "value": "Added|Changed|Deprecated|Removed|Fixed|Security", - "line": 814 + "line": 821 }, { "id": "PRED.k321.evidence", "klass": "PRED", "value": "PRs #3304/#3305 (2026-05-09): real Minor/Major findings in body, 0 threads", - "line": 825 + "line": 832 }, { "id": "PRED.k321.poll-shape", "klass": "PRED", "value": "parse pulls//reviews body AND graphql reviewThreads", - "line": 823 + "line": 830 }, { "id": "PRED.k321.resolution", "klass": "PRED", "value": "address in code; no GraphQL resolveReviewThread needed for body-only findings", - "line": 824 + "line": 831 }, { "id": "PRED.k321.shape", "klass": "PRED", "value": "CR posts \"[!CAUTION] outside the diff\" findings in review BODY, not in reviewThreads", - "line": 822 + "line": 829 }, { "id": "PRED.k321.signal", "klass": "PRED", "value": "cr-outside-diff-range-finding", - "line": 821 + "line": 828 }, { "id": "PRED.k322.cure-1", "klass": "PRED", "value": "2nd retrigger ~10min after first ack", - "line": 830 + "line": 837 }, { "id": "PRED.k322.cure-2", "klass": "PRED", "value": "if silent at 50min, treat as silent-pass with maintainer flag in merge-commit body", - "line": 831 + "line": 838 }, { "id": "PRED.k322.distinct-from", "klass": "PRED", "value": "k080", - "line": 828 + "line": 835 }, { "id": "PRED.k322.evidence", "klass": "PRED", "value": "PR #3306 (2026-05-09): 0 reviews after 50min + 2 retriggers", - "line": 833 + "line": 840 }, { "id": "PRED.k322.merge-gate-impact", "klass": "PRED", "value": "k070 real_coderabbit_review_present unsatisfied; requires maintainer judgment", - "line": 832 + "line": 839 }, { "id": "PRED.k322.shape", "klass": "PRED", "value": "ack posted, real review never lands within [5s, 410s] cooldown after burst of N PRs <15min", - "line": 829 + "line": 836 }, { "id": "PRED.k322.signal", "klass": "PRED", "value": "cr-sustained-throttle", - "line": 827 + "line": 834 }, { "id": "PRED.k323.cure-alt", "klass": "PRED", "value": "consolidate into single PR when 2+ issues share root cause", - "line": 838 + "line": 845 }, { "id": "PRED.k323.cure-pre-dispatch", "klass": "PRED", "value": "brief one agent canonical-owner; brief others to EXCLUDE shared site", - "line": 837 + "line": 844 }, { "id": "PRED.k323.evidence", "klass": "PRED", "value": "#3300 (#3297) overlapped #3306 (#3298) on add-backlog.md hunks 2026-05-09", - "line": 840 + "line": 847 }, { "id": "PRED.k323.recovery", "klass": "PRED", "value": "close smaller PR as \"subsumed by #N\" or rebase second to drop overlap hunk", - "line": 839 + "line": 846 }, { "id": "PRED.k323.shape", "klass": "PRED", "value": "2+ open issues touch same canonical bug site; each fix's sibling-audit produces overlapping diff", - "line": 836 + "line": 843 }, { "id": "PRED.k323.signal", "klass": "PRED", "value": "sibling-audit-cross-pr-overlap", - "line": 835 + "line": 842 }, { "id": "PRED.k324.cure", "klass": "PRED", "value": "verify via gh api on every agent-completion notification; never trust narrative", - "line": 844 + "line": 851 }, { "id": "PRED.k324.evidence", "klass": "PRED", "value": "2026-05-09 session: 5+ mid-monitor terminations across PRs #3232/#3271/#3251/#3255/#3262", - "line": 846 + "line": 853 }, { "id": "PRED.k324.k095-restatement", "klass": "PRED", "value": "k095 confirmed shape: agent reports \"waiting for monitor\" / \"tests still running\" then terminates", - "line": 843 + "line": 850 }, { "id": "PRED.k324.poll-shape", "klass": "PRED", "value": "gh pr view --json mergeStateStatus,statusCheckRollup + pulls//reviews + graphql reviewThreads + issues//comments tail", - "line": 845 + "line": 852 }, { "id": "PRED.k324.signal", "klass": "PRED", "value": "agent-terminates-mid-monitor", - "line": 842 + "line": 849 }, { "id": "PRED.k325.cleanup", "klass": "PRED", "value": "git worktree remove --force for aged agent worktrees", - "line": 851 + "line": 858 }, { "id": "PRED.k325.cure", "klass": "PRED", "value": "detached-HEAD: git checkout --detach $(git ls-remote origin ); modify; commit; git push --force-with-lease=: origin HEAD:refs/heads/", - "line": 850 + "line": 857 }, { "id": "PRED.k325.evidence", "klass": "PRED", "value": "2026-05-09 CHANGELOG.md strip on PRs #3300/#3302/#3304/#3305 required detached-HEAD", - "line": 852 + "line": 859 }, { "id": "PRED.k325.shape", "klass": "PRED", "value": "git checkout errors \"already used by worktree at \"", - "line": 849 + "line": 856 }, { "id": "PRED.k325.signal", "klass": "PRED", "value": "worktree-branch-lock-on-force-push", - "line": 848 + "line": 855 }, { "id": "PRED.k326.cure", "klass": "PRED", "value": "quote canonical doc verbatim in brief; mentally simulate \"if all N agents follow this brief literally, do they violate any rule?\"", - "line": 856 + "line": 863 }, { "id": "PRED.k326.evidence", "klass": "PRED", "value": "2026-05-09 brief \"k040 — update CHANGELOG.md\" → 5 of 8 agents violated CONTRIBUTING.md L110", - "line": 857 + "line": 864 }, { "id": "PRED.k326.shape", "klass": "PRED", "value": "N parallel agents amplify a single brief-vs-doc contradiction into N violations", - "line": 855 + "line": 862 }, { "id": "PRED.k326.signal", "klass": "PRED", "value": "brief-contradicts-canonical-doc", - "line": 854 + "line": 861 }, { "id": "PRED.k327.ack-shape", "klass": "PRED", "value": "body \"✅ Actions performed - Full review triggered\"", - "line": 860 + "line": 867 }, { "id": "PRED.k327.cooldown-normal", "klass": "PRED", "value": "[5s, 410s]", - "line": 863 + "line": 870 }, { "id": "PRED.k327.cooldown-throttled", "klass": "PRED", "value": "k322", - "line": 864 + "line": 871 }, { "id": "PRED.k327.distinguish-key", "klass": "PRED", "value": "len(pulls//reviews) — ack=0, real=≥1", - "line": 862 + "line": 869 }, { "id": "PRED.k327.real-review-shape", "klass": "PRED", "value": "body starts \"Actionable comments posted: N\" OR \"[!CAUTION] Some comments are outside the diff\"", - "line": 861 + "line": 868 }, { "id": "PRED.k327.signal", "klass": "PRED", "value": "cr-ack-vs-real-review", - "line": 859 + "line": 866 }, { "id": "PRED.k328.audit-list", "klass": "PRED", "value": "[heading-matches-class, closing-keyword-present, changeset-fragment-or-no-changelog-label]", - "line": 869 + "line": 876 }, { "id": "PRED.k328.canonical-source", "klass": "PRED", "value": "CONTRIBUTING.md L48,L64,L81 (template links) + .github/PULL_REQUEST_TEMPLATE/{fix,enhancement,feature}.md L1 (heading text)", - "line": 867 + "line": 874 }, { "id": "PRED.k328.k100-restatement", "klass": "PRED", "value": "heading must match issue class: bug→## Fix PR, enhancement→## Enhancement PR, feature→## Feature PR", - "line": 868 + "line": 875 }, { "id": "PRED.k328.signal", "klass": "PRED", "value": "pr-template-typed-heading-required", - "line": 866 + "line": 873 }, { "id": "PRED.k329.body", "klass": "PRED", "value": "**** — . (#)", - "line": 875 + "line": 882 }, { "id": "PRED.k329.canonical-source", "klass": "PRED", "value": "CONTRIBUTING.md L196-202 + .changeset/README.md", - "line": 872 + "line": 879 }, { "id": "PRED.k329.filename", "klass": "PRED", "value": ".changeset/--.md", - "line": 873 + "line": 880 }, { "id": "PRED.k329.frontmatter", "klass": "PRED", "value": "---\\\\ntype: \\\\npr: \\\\n---", - "line": 874 + "line": 881 }, { "id": "PRED.k329.observed-clean", "klass": "PRED", "value": "#3299 sunny-ibex-wave, #3301 sturdy-rams-caper, #3306 3298-phase-dir-prefix-drift-workflows", - "line": 876 + "line": 883 }, { "id": "PRED.k329.signal", "klass": "PRED", "value": "changeset-fragment-canonical-shape", - "line": 871 + "line": 878 }, { "id": "PRED.k330.fallback", "klass": "PRED", "value": "append predicate-format findings directly to CONTEXT.md", - "line": 880 + "line": 887 }, { "id": "PRED.k330.shape", "klass": "PRED", "value": "mempalace MCP tools require explicit user call; AI cannot trigger", - "line": 879 + "line": 886 }, { "id": "PRED.k330.signal", "klass": "PRED", "value": "mempalace-diary-not-callable-by-ai", - "line": 878 + "line": 885 }, { "id": "PRED.k331.cure", "klass": "PRED", "value": "gh pr close with NO --comment flag", - "line": 885 + "line": 892 }, { "id": "PRED.k331.evidence", "klass": "PRED", "value": "2026-05-09 wave-3: violation on #3300 close, deleted within 30s", - "line": 887 + "line": 894 }, { "id": "PRED.k331.k101-restatement", "klass": "PRED", "value": "k101 includes close-time --comment flag; rationale belongs in subsuming PR's squash-merge body", - "line": 884 + "line": 891 }, { "id": "PRED.k331.recovery", "klass": "PRED", "value": "if violation lands, gh api -X DELETE repos///issues/comments/", - "line": 886 + "line": 893 }, { "id": "PRED.k331.shape", "klass": "PRED", "value": "instruction \"close with no comment (rationale)\" — parenthetical is rationale, NOT comment body", - "line": 883 + "line": 890 }, { "id": "PRED.k331.signal", "klass": "PRED", "value": "close-with-no-comment-is-literal", - "line": 882 + "line": 889 }, { "id": "PROBE.ci.surface", "klass": "PROBE", "value": "the contract (parse/validate, projection round-trip, fail-closed guards), NEVER the LLM judgment (ADR-550 D5)", - "line": 619 + "line": 627 }, { "id": "PROBE.core.seam", "klass": "PROBE", "value": "analyzeCoverage(items,resolutions?,validators) ingests ALREADY-proposed items; does NOT assume deterministic propose (ADR-550 D7b)", - "line": 611 + "line": 619 }, { "id": "PROBE.edge.verification", "klass": "PROBE", "value": "explicit|backstop", - "line": 613 + "line": 621 }, { "id": "PROBE.family", "klass": "PROBE", "value": "edge-probe(shape-axis)+prohibition-probe(must-NOT-axis)+ui-consideration-probe(UI-state-axis), shared probe-core, run as spec-phase/ui-phase soft gates (ADR-550 D7; #1867)", - "line": 609 + "line": 617 }, { "id": "PROBE.item.axes", "klass": "PROBE", "value": "status{resolved|dismissed|unresolved} x verification{|null} — orthogonal; the lifecycle enum carries no verification fact (ADR-550 D7a)", - "line": 612 + "line": 620 }, { "id": "PROBE.principle", "klass": "PROBE", "value": "verifier-reach-equals-spec-reach (a goal-backward verifier only checks assertions that exist; probes make omitted assertions exist before code) — ADR-857 verification-substrate boundary; docs/design/verifier-reach.md", - "line": 608 + "line": 616 }, { "id": "PROBE.prohib.verification", "klass": "PROBE", "value": "test|judgment", - "line": 614 + "line": 622 }, { "id": "PROBE.protocol", "klass": "PROBE", "value": "recall(adversarial over-generate)->precision(drop routine-engineering); dismissals require a non-empty reason", - "line": 610 + "line": 618 }, { "id": "PROBE.ui.axis", "klass": "PROBE", "value": "MIXED — closed compiled shape-rooted 8 (empty/loading/error/populated/partial/overflow/zero-one-many/long-text) via ui-consideration-probe adapter; open UX (real-time/a11y/i18n-RTL) prose-owned in references/domain-probes.md, NOT compiled (#1867)", - "line": 616 + "line": 624 }, { "id": "PROBE.ui.seam", "klass": "PROBE", "value": "ui-phase Step 9.5 post-verification: element-cue classify -> propose-then-confirm (partial-cue mitigation, Goodhart) -> autoResolve --auto floor (never dismiss; unclassified stays unresolved #1110) -> ## UI Considerations write-back -> plan-phase `## UI Considerations` lift rule (#1867)", - "line": 618 + "line": 626 }, { "id": "PROBE.ui.text_en", "klass": "PROBE", "value": "optional English translation of Element.text read by classification (text_en ?? text), engine input never user-facing output; mirrors the edge adapter's #3717 field; empty/whitespace/non-string fails closed (#4657)", - "line": 617 + "line": 625 }, { "id": "PROBE.ui.verification", "klass": "PROBE", "value": "explicit|backstop", - "line": 615 + "line": 623 }, { "id": "PROC.AGENT-DISPATCH.completion-verify", "klass": "PROC", "value": "run k324.poll-shape on every agent-completion notification", - "line": 891 + "line": 898 }, { "id": "PROC.AGENT-DISPATCH.parallel-overlap-audit", "klass": "PROC", "value": "before dispatching N sibling-audit fixers, compute file-set union and assign canonical owners", - "line": 890 + "line": 897 }, { "id": "PROC.AGENT-DISPATCH.preflight", "klass": "PROC", "value": "[read-CONTRIBUTING.md-fresh, read-relevant-ADRs, cite-specific-line-in-brief, require-closing-keyword, require-changeset-fragment, forbid-CHANGELOG.md-edit, require-isolation-worktree, forbid-self-PR-comment, mandate-trust-but-verify]", - "line": 889 + "line": 896 }, { "id": "PROC.MERGE-WAVE.changelog-strip-pattern", "klass": "PROC", "value": "detached-HEAD per k325 + git checkout main -- CHANGELOG.md + commit + force-with-lease", - "line": 895 + "line": 902 }, { "id": "PROC.MERGE-WAVE.merge-tool", "klass": "PROC", "value": "gh pr merge --squash --delete-branch", - "line": 896 + "line": 903 }, { "id": "PROC.MERGE-WAVE.merge-tool-warning", "klass": "PROC", "value": "delete-branch may fail with \"used by worktree at\" — harmless; remote branch still deleted", - "line": 897 + "line": 904 }, { "id": "PROC.MERGE-WAVE.ordering", "klass": "PROC", "value": "[wave1: isolated-files, wave2: CHANGELOG-only-overlap (better: strip per k320), wave3: same-file-overlap with explicit decision]", - "line": 893 + "line": 900 }, { "id": "PROC.MERGE-WAVE.preflight", "klass": "PROC", "value": "gh pr view --json files for every PR; identify overlap pairs; surface to maintainer", - "line": 894 + "line": 901 }, { "id": "PROC.PARALLEL-FIX-DISPATCH.observed", "klass": "PROC", "value": "#3541 + #3542 dispatched simultaneously this session; PRs #3546 #3547 opened green; one syntax slip caught by AGENT-RETIRED-SLASH-SYNTAX-DRIFT and fixed before second PR opened", - "line": 976 + "line": 983 }, { "id": "PROC.PARALLEL-FIX-DISPATCH.pattern", "klass": "PROC", "value": "bot triage brief → worktree per branch → parallel sub-agents do rubber-duck/RCA/TDD implementation only → top-level orchestrator owns commit + msd-test + push + PR + changeset-pr-backfill", - "line": 974 + "line": 981 }, { "id": "PROC.PARALLEL-FIX-DISPATCH.rationale", "klass": "PROC", "value": "long-running test runs need cross-turn notifications (orchestrator-only); CONTRIBUTING.md gh-templates-first hook requires session-scoped Read calls sub-agents wouldn't otherwise make; sequencing test runs avoids MSD-TEST-CONCURRENT-OUTPUT-COLLISION", - "line": 975 + "line": 982 }, { "id": "PROC.TRIAGE.comment-shape", "klass": "PROC", "value": "lead with \"duplicate of #NNNN, fixed by PR #MMMM, in v1.X.Y\"; show current code snippet proving bug-surface gone; give @latest and @next upgrade commands; close", - "line": 979 + "line": 986 }, { "id": "PROC.TRIAGE.no-duplicate-label", "klass": "PROC", "value": "this repo has no duplicate label; framing lives in comment text + closing the issue", - "line": 980 + "line": 987 }, { "id": "PROC.TRIAGE.routing-incoming", "klass": "PROC", "value": "stale-bug-already-fixed to close as duplicate of originating issue + cite fix PR + first stable tag; release-publish-or-backport to ready-for-human; reporter-can-self-test to awaiting-retest", - "line": 978 + "line": 985 }, { "id": "PROHIB.canon-referral", "klass": "PROHIB", "value": "OWASP/GDPR/fairness-canon are REFERRED to /msd:secure-phase+eslint, never minted as prohibitions (ADR-550 D6)", - "line": 621 + "line": 629 }, { "id": "PROHIB.descriptor.shape", "klass": "PROHIB", "value": "5 FLAT scalars (check_kind,check_target,check_rule,check_violation_fixture,check_clean_fixture) — NEVER a nested check:{} (parseMustHavesBlock is a flat parser, src/frontmatter.cts)", - "line": 626 + "line": 634 }, { "id": "PROHIB.enforce.adr", "klass": "PROHIB", "value": "docs/adr/1606-prohibition-enforcement-verify-seam.md (verify-time enforcement seam) + docs/adr/550-spec-phase-probe-contract.md (spec-phase contract)", - "line": 629 + "line": 637 }, { "id": "PROHIB.enforce.causation", "klass": "PROHIB", "value": "clean-fixture control proves the red is content-caused not env-var-set; MANDATORY for node-test (#1906 supersedes #1346 opt-in) — absent clean-fixture ⇒ node-test un-provable/fail-closed; lint-rule needs none (its subject IS the linted file)", - "line": 625 + "line": 633 }, { "id": "PROHIB.enforce.failfirst", "klass": "PROHIB", "value": "MACHINE-PROVEN against an author-supplied violation fixture (#1279); caller failFirst attestation DEMOTED to a non-authoritative hint (FF-08)", - "line": 624 + "line": 632 }, { "id": "PROHIB.enforce.green-rule", "klass": "PROHIB", "value": "passed iff provenFailFirst===true && run.passed===true (runProhibitionEnforcement); every miss/fail/un-provable HARD-GATES both modes via dispositionForProhibition's fail-closed default", - "line": 622 + "line": 630 }, { "id": "PROHIB.enforce.kinds", "klass": "PROHIB", "value": "node-test (non-vacuous red via isNonVacuousNodeTestRed; pass-side vacuity via isNonVacuousNodeTestPass) | lint-rule (eslint --format json filtered by ruleId)", - "line": 623 + "line": 631 }, { "id": "PROHIB.judgment-tier", "klass": "PROHIB", "value": "never-silent / never-hard-halt soft gate; autonomous emits \"unverified-prohibition — human review recommended\" (exogenous grading, ADR-550 D4)", - "line": 628 + "line": 636 }, { "id": "PROHIB.rail", "klass": "PROHIB", "value": "core verify rail, non-toggleable (ADR-857 verification-substrate boundary / decision #6); the verifier<->predicate contract is NOT an off-by-default capability", - "line": 627 + "line": 635 }, { "id": "PROHIB.recall", "klass": "PROHIB", "value": "LLM-prose; no compiled prohibition-probe recall engine (only the schema/projection layer is code, ADR-550 D7b)", - "line": 620 + "line": 628 }, { "id": "RELEASE-NOTES.ANTI-PATTERN", "klass": "RELEASE-NOTES", "value": "raw \"What's Changed\" PR list as final body for hotfix or feature release; \"Full Changelog only\" body for tagged release with >0 user-facing fixes", - "line": 786 + "line": 793 }, { "id": "RELEASE-NOTES.ANTI-PATTERN.implementation-first", "klass": "RELEASE-NOTES", "value": "do not lead bullet with file path or function name; lead with symptom/user-visible behavior", - "line": 787 + "line": 794 }, { "id": "RELEASE-NOTES.ANTI-PATTERN.risk-commentary", "klass": "RELEASE-NOTES", "value": "do not include \"may break\", \"be careful\", \"test thoroughly\" - release notes state what changed, not hedges about what might go wrong", - "line": 788 + "line": 795 }, { "id": "RELEASE-NOTES.DEFAULT-STATE", "klass": "RELEASE-NOTES", "value": "auto-generated body is \"What's Changed\" PR list + Full Changelog link; treat as draft, not final", - "line": 762 + "line": 769 }, { "id": "RELEASE-NOTES.EXAMPLE.hotfix", "klass": "RELEASE-NOTES", "value": "v1.41.1 (https://github.com/open-gsd/gsd-core/releases/tag/v1.41.1) - 14 fixes grouped by 6 subgroups", - "line": 790 + "line": 797 }, { "id": "RELEASE-NOTES.EXAMPLE.minor-auto-acceptable", "klass": "RELEASE-NOTES", "value": "v1.41.0 - kept auto-generated body; many small fixes with clean conventional-commit titles", - "line": 792 + "line": 799 }, { "id": "RELEASE-NOTES.EXAMPLE.rc", "klass": "RELEASE-NOTES", "value": "v1.7.0-rc.1 (https://github.com/open-gsd/gsd-core/releases/tag/v1.7.0-rc.1) - intro + Added/Changed/Fixed/Documentation taxonomy", - "line": 791 + "line": 798 }, { "id": "RELEASE-NOTES.GATE.hotfix", "klass": "RELEASE-NOTES", "value": "manual edit required; auto-generated body for vX.Y.{Z>0} is \"Full Changelog only\" and must be replaced with structured body", - "line": 763 + "line": 770 }, { "id": "RELEASE-NOTES.GATE.minor", "klass": "RELEASE-NOTES", "value": "auto-generated body acceptable when PR titles are clean; promote to structured body when >20 PRs or contains feature+refactor+fix mix", - "line": 765 + "line": 772 }, { "id": "RELEASE-NOTES.GATE.rc", "klass": "RELEASE-NOTES", "value": "manual edit recommended; auto-generated PR list is acceptable for early RCs but final RC before vX.Y.0 should match standard", - "line": 764 + "line": 771 }, { "id": "RELEASE-NOTES.RELEASE-STREAM.main-branch", "klass": "RELEASE-NOTES", "value": "next (RCs) + latest (stable); install via @next or @latest", - "line": 797 + "line": 804 }, { "id": "RELEASE-NOTES.RELEASE-STREAM.rule", "klass": "RELEASE-NOTES", "value": "streams do not mix; do not document @next in hotfix/stable notes", - "line": 798 + "line": 805 }, { "id": "RELEASE-NOTES.SCOPE", "klass": "RELEASE-NOTES", "value": "GitHub Releases body for tags vX.Y.Z, vX.Y.Z-rc.N; not CHANGELOG.md (changeset workflow owns that)", - "line": 761 + "line": 768 }, { "id": "RELEASE-NOTES.SOURCE.changesets", "klass": "RELEASE-NOTES", "value": ".changeset/*.md (frontmatter pr: + body bullets)", - "line": 777 + "line": 784 }, { "id": "RELEASE-NOTES.SOURCE.commits", "klass": "RELEASE-NOTES", "value": "git log .. --pretty=format:'%s%n%n%b' --no-merges", - "line": 776 + "line": 783 }, { "id": "RELEASE-NOTES.SOURCE.pr-bodies", "klass": "RELEASE-NOTES", "value": "gh pr view --json title,body for fixes lacking a changeset", - "line": 778 + "line": 785 }, { "id": "RELEASE-NOTES.SOURCE.precedence", "klass": "RELEASE-NOTES", "value": "changeset body > commit body > PR body > commit subject (prefer authored content over auto-generated)", - "line": 779 + "line": 786 }, { "id": "RELEASE-NOTES.STANDARD.bullet-shape", "klass": "RELEASE-NOTES", "value": "**Bold user-visible change** — explanation of what was broken or what's new, leading with symptom not implementation. Trailing (#NNN) PR ref.", - "line": 769 + "line": 776 }, { "id": "RELEASE-NOTES.STANDARD.footer.full-changelog", "klass": "RELEASE-NOTES", "value": "**Full Changelog**: https://github.com/open-gsd/gsd-core/compare/...", - "line": 773 + "line": 780 }, { "id": "RELEASE-NOTES.STANDARD.footer.hotfix", "klass": "RELEASE-NOTES", "value": "Install/upgrade: \\`npx @golem15/msd-core@latest\\`", - "line": 771 + "line": 778 }, { "id": "RELEASE-NOTES.STANDARD.footer.rc", "klass": "RELEASE-NOTES", "value": "Install for testing: \\`npx @golem15/msd-core@next\\` (per branch->dist-tag policy)", - "line": 772 + "line": 779 }, { "id": "RELEASE-NOTES.STANDARD.heading-level", "klass": "RELEASE-NOTES", "value": "## for category, ### for subgroup (area), - for bullet", - "line": 768 + "line": 775 }, { "id": "RELEASE-NOTES.STANDARD.intro", "klass": "RELEASE-NOTES", "value": "optional one-paragraph framing for RC/feature releases; omit for pure-fix hotfixes", - "line": 774 + "line": 781 }, { "id": "RELEASE-NOTES.STANDARD.subgroups", "klass": "RELEASE-NOTES", "value": "phase-planning-state | workstream | query-dispatch-cli | code-review | install | capture | docs | architecture | security", - "line": 770 + "line": 777 }, { "id": "RELEASE-NOTES.STANDARD.taxonomy", "klass": "RELEASE-NOTES", "value": "Keep-a-Changelog 1.1.0: Added | Changed | Deprecated | Removed | Fixed | Security | Documentation", - "line": 767 + "line": 774 }, { "id": "RELEASE-NOTES.TEMPLATE.hotfix", "klass": "RELEASE-NOTES", "value": "## Fixed\\n\\n### \\n- **** — . (#)\\n\\n---\\n\\nInstall/upgrade: \\`npx @golem15/msd-core@latest\\`\\n\\n**Full Changelog**: ", - "line": 794 + "line": 801 }, { "id": "RELEASE-NOTES.TEMPLATE.rc", "klass": "RELEASE-NOTES", "value": "\\n\\n## Added\\n### \\n- **** — . (#)\\n\\n## Changed\\n### Architecture\\n- **** — . (#)\\n\\n## Fixed\\n### \\n- **** — . (#)\\n\\n## Documentation\\n- **** — . (#)\\n\\n---\\n\\nThis is a release candidate. Install for testing:\\n\\`\\`\\`bash\\nnpx @golem15/msd-core@next\\n\\`\\`\\`\\n\\n**Full Changelog**: ", - "line": 795 + "line": 802 }, { "id": "RELEASE-NOTES.WORKFLOW.edit", "klass": "RELEASE-NOTES", "value": "gh release edit --notes-file ", - "line": 781 + "line": 788 }, { "id": "RELEASE-NOTES.WORKFLOW.idempotency", "klass": "RELEASE-NOTES", "value": "gh release edit overwrites body wholesale; safe to re-run after refining", - "line": 784 + "line": 791 }, { "id": "RELEASE-NOTES.WORKFLOW.token", "klass": "RELEASE-NOTES", "value": "must use .envrc GITHUB_TOKEN per RULESET.GH.AUTH.DEFAULT (this doc); never ambient gh auth", - "line": 783 + "line": 790 }, { "id": "RELEASE-NOTES.WORKFLOW.view", "klass": "RELEASE-NOTES", "value": "gh release view --json body --jq .body", - "line": 782 + "line": 789 }, { "id": "RULESET.ADR-HEADER", "klass": "RULESET", "value": "every docs/adr/NNNN-*.md must open with - **Status:** Accepted|Proposed|Superseded (by [ADR-NNNN](file.md))|Legacy + - **Date:** YYYY-MM-DD immediately after title", - "line": 676 + "line": 684 }, { "id": "RULESET.AGENT_SIZE_BUDGET", "klass": "RULESET", "value": "agent-size-budget (#1074; sibling of WORKFLOW_SIZE_BUDGET; BYTES not lines per #717/#683, rebased from lines in PR 3/3) = differential attribution size ratchet (PRIMARY anti-creep since #2724/ADR-2719 §4, same mechanism and same `Emitted-Drift-Ack-Growth:` commit trailer (ADR-3942, superseding ADR-2719 §3's fragment model) as WORKFLOW_SIZE_BUDGET, scoped to agents/msd-*.md) + loose tier hard caps (red lines, never raised on approach: XL<=57344 / LARGE<=49152 / DEFAULT<=24576); net-new agents are DEFAULT-tier (no separate new-file cap). Sizes are measured via the shared scripts/workflow-size.cjs measureMdFiles(dir,predicate) counter (tests/helpers/emitted-runtime.cjs's currentSizes() and the guard's own tier-cap checks both import it). A grown agent fails the differential guard — ack + justify, or extract LAZILY to msd-core/references/. DISTINCT from DEFECT.AGENT-FILE-SIZE-CAP-BREACH (a separate 45K-CHAR extraction-evidence threshold on msd-planner via planner-decomposition/reachability tests): that guard proves mode-sections were extracted; this one bounds total agent bytes. Two guards, two units (chars vs bytes), two purposes. The prior per-file baseline (tests/agent-size-baseline.json, `npm run size:baseline`) is REMOVED by #2724", - "line": 665 + "line": 673 }, { "id": "RULESET.ALLOWED-TOOLS-FRONTMATTER", "klass": "RULESET", "value": "command's allowed-tools must cover every tool the workflow calls (including Write for file creation); thin-wrapper pattern makes this easy to miss", - "line": 672 + "line": 680 }, { "id": "RULESET.ARGUMENTS-SANITIZE", "klass": "RULESET", "value": "any workflow step constructing .planning/.../{SLUG}.md path from user input ($ARGUMENTS, parsed remainder) must sanitize inline ([a-z0-9-] only, reject ..//\\\\, max-length) — \"(already sanitized)\" must trace back to explicit guard; RESUME/fallback modes need own guards", - "line": 673 + "line": 681 }, { "id": "RULESET.AUDIT.search-source-not-generated", "klass": "RULESET", "value": "verify an invariant/validation EXISTS by searching the AUTHORED source (src/*.cts OR the scripts/gen-*.cjs generator), never the generated bin/lib/*.cjs (gitignored, ADR-457); gen-time checks live in gen-*.cjs not the .cts it consumes → search BOTH before declaring absent; read generated .cjs only for output drift. Repro: grep src/*.cts for VALID_CONVERTER_NAMES → false \"5e ConverterName unenforced\"; actually enforced in gen-capability-registry.cjs. cf RULESET.TESTS.no-source-grep", - "line": 661 + "line": 669 }, { "id": "RULESET.CAPABILITY.cutover-self-gating", "klass": "RULESET", "value": "a phase-6 per-feature cutover moves the host's phase-context detection + mode/flag logic INTO the skill (self-gating, per ADR-894); the loop hook is intentionally COARSE — \"invoke skill X at point Y when config Z\" — and carries no detection/mode. WORKED EXAMPLE: plan-phase.md §5.6 UI gate (frontend-detection via ui-safety-gate.cjs + --auto/manual branch + --skip-ui bypass) must move into msd-ui-phase before its plan:pre hook can replace the inline call without behavior loss. Spike #1018 finding.", - "line": 434 + "line": 439 }, { "id": "RULESET.CAPABILITY.off-means-off", "klass": "RULESET", "value": "the host derives shared outputs from the ACTIVE hook set (via loop.render-hooks); a hook may ADD a labeled block or be COUNTED into a host-computed aggregate (e.g. a score denominator), but NEVER mutates host source — so a disabled capability yields the base output by construction, not by authoring discipline. Ratify in ADR-894; proven by spike #1018.", - "line": 432 + "line": 437 }, { "id": "RULESET.CAPABILITY.precedence-engine-single-owner", "klass": "RULESET", "value": "the config-key four-level precedence walk (loadConfig result → workstream config.json → root config.json → registry.configSchema default → absent) is owned solely by src/capability-activation.cts: raw-value primitive resolveConfigKey(dotKey, {config,cwd,registry}) and boolean wrapper _resolveActivationValue(dotKey,config,cwd,registry); loop-resolver.cts imports the engine (no duplicate); resolveConfigValues in loop-resolver.cts delegates to resolveConfigKey; resolveCapabilityRuntimeState does NOT return registry/config — callers import capability-registry.cjs and call loadConfig(cwd) directly. #3661 adds a THIRD export from the same engine, _resolvePointGate(pointFrom,point,config,cwd,registry): a step's optional pointFrom field names a dotted enum config key; the step is active for its own `point` only when that key resolves to a value === point (found:false or a type/value mismatch → false). loop-resolver.cts's isActive and capability-state.cts's processHooks both call it (ANDed with the existing when gate) so a capability can register the same logical step at more than one loop point with config selecting which registration is live — see capabilities/code-review/capability.json's execute:post/execute:wave:post pair for the reference shape. capability-validator.cjs's validateAgainstContract requires pointFrom to reference an enum cap.config key whose values include the declaring step's own point (mirrors the pre-existing when-must-be-a-cap.config-key check for `when`).", - "line": 438 + "line": 443 }, { "id": "RULESET.CAPABILITY.step-additive-gate-blocks", "klass": "RULESET", "value": "a `step` hook is purely additive (invoke skill + produce artifacts, NEVER halts the host); host-blocking preconditions are `gate`s (blocking:true, onError:halt); runtime/mode context (auto/chain vs manual) self-gates IN THE SKILL, not via `when` (config-only). §5.6 = plan:pre step (ui-phase; skill self-gates on frontend+pipeline, auto-fires only in pipelines) + a NEW plan:pre gate (frontend-and-no-UI-SPEC → halt, when:workflow.ui_safety_gate); the loop.render-hooks dispatch template handles steps AND gates. Resolves #1022.", - "line": 436 + "line": 441 }, { "id": "RULESET.CODERABBIT.GUARD.COMPLETE", "klass": "RULESET", "value": "required_checks_green && coderabbit_check_pass && graphQL(reviewThreads.unresolved_count)==0", - "line": 698 + "line": 706 }, { "id": "RULESET.CODERABBIT.GUARD.GRAPHQL", "klass": "RULESET", "value": "reviewThreads(first:100){nodes{id isResolved comments{nodes{author body path line originalLine url}}}}; use unresolved threads as authoritative, not badge text alone", - "line": 699 + "line": 707 }, { "id": "RULESET.CODERABBIT.GUARD.OPEN_PRS", "klass": "RULESET", "value": "gh pr list --repo golem15/msd-core --author @me --state open; repeat near end because open PR set can change mid-run", - "line": 697 + "line": 705 }, { "id": "RULESET.CODERABBIT.GUARD.RERUN", "klass": "RULESET", "value": "after every push wait for CodeRabbit completion, then re-query unresolved threads; CodeRabbit can add new findings after earlier threads were resolved", - "line": 700 + "line": 708 }, { "id": "RULESET.CODERABBIT.GUARD.RESOLVE", "klass": "RULESET", "value": "fix validated finding -> focused tests -> commit/push -> resolveReviewThread(threadId) -> wait CI/CodeRabbit -> final unresolved_count query", - "line": 701 + "line": 709 }, { "id": "RULESET.CODERABBIT.GUARD.SCOPE", "klass": "RULESET", "value": "if a new @me open PR appears during final list, include it in the same guard pass before declaring all-open-PRs complete", - "line": 702 + "line": 710 }, { "id": "RULESET.CONTENT-PATH-NORMALIZATION", "klass": "RULESET", "value": "filesystem paths substituted into markdown body text (@-references, workflow .md, agent .md, generated docs, command bodies) MUST be normalized to POSIX forward slashes via .replace(/\\\\/g,'/') at the production source BEFORE substitution; never push normalization to tests; cross-platform content is POSIX-only; applies to: computePathPrefix output, install-path rewrites, generated shim paths emitted into .md bodies; idempotent on POSIX so unconditional; mechanically enforced by local/normalize-path-in-content (eslint, src/**/*.cts; #1733)", - "line": 913 + "line": 920 }, { "id": "RULESET.CONTRIB.CLASSIFY.enhancement", "klass": "RULESET", "value": "requires approved-enhancement before implementation", - "line": 691 + "line": 699 }, { "id": "RULESET.CONTRIB.CLASSIFY.feature", "klass": "RULESET", "value": "requires approved-feature before implementation", - "line": 692 + "line": 700 }, { "id": "RULESET.CONTRIB.CLASSIFY.fix", "klass": "RULESET", "value": "requires confirmed-bug before implementation (legacy 'confirmed' label is back-compat only for duplicate-sweep exemption, not a valid implementation gate)", - "line": 690 + "line": 698 }, { "id": "RULESET.CONTRIB.GATE.ORDER", "klass": "RULESET", "value": "issue-first -> approval-label -> code -> PR-link -> changeset/no-changelog", - "line": 689 + "line": 697 }, { "id": "RULESET.CR-THREAD-RESOLVE", "klass": "RULESET", "value": "after adding // allow-test-rule: to silence lint, resolve existing inline CR threads via graphql resolveReviewThread mutation before merge — open threads mislead future reviewers; pattern: gh api graphql -f query='mutation { resolveReviewThread(input:{threadId:\"PRRT_...\"}) { thread { isResolved } } }'", - "line": 683 + "line": 691 }, { "id": "RULESET.EMITTED_ATTRIBUTION", "klass": "RULESET", "value": "the emitted-artifact family (ADR-2719, epic #2719) — POST-CUTOVER (#2724, Phase 4). Historically tests/fixtures/golden-install-parity/*.json (19 path→hash manifests) + tests/workflow-size-baseline.json + tests/agent-size-baseline.json were all committed, PURE FUNCTIONS of the source tree whose correct merge was ALWAYS \"recompute\" — 140 of 143 conflicted-file instances across the open PR queue were these files. #2724 DELETES all three, the golden test (tests/golden-install-parity.test.cjs), the generator (scripts/gen-golden-install-parity-zcode.cjs), `npm run gen:golden`, `UPDATE_GOLDEN`, the merge-driver bridge (scripts/git-merge-regen-driver.cjs, `npm run setup:merge-driver`, the .gitattributes merge=msd-regen block), and scripts/update-size-baseline.cjs (`npm run size:baseline`). The differential attribution check (tests/emitted-attribution.test.cjs + tests/emitted-provenance.test.cjs) is now the SOLE gate for emitted-artifact propagation AND size growth — no committed artifact, nothing to hand-merge, nothing to regenerate. `npm run regen:derived` still exists for what remains committed and derived: build, registry, ADR index, capability matrix, inventory manifest, manifest versions, and `tests/fixtures/install-tree/*.json` (now `npm run gen:install-tree`, folded into `regen:derived`). tests/fixtures/install-tree/*.json is DELIBERATELY EXCLUDED from the cutover (ADR-2719 §7): it conflicts on 0 of 7, its diffs are readable, and it preserves \"the installer stopped shipping X\" as a hard absolute failure — capturing it would convert that absolute into an attribution-free auto-resolve. The baseline the differential compares against is now published by `scripts/gen-emitted-baseline.cjs` on every push to `next` (cached, keyed on sha) and restored in PR lanes via `MSD_EMITTED_BASELINE`/`resolveBaseline()` (tests/helpers/emitted-baseline.cjs); a cache miss falls back to an in-job build via a throwaway `git worktree` (tests/helpers/emitted-runtime.cjs's `buildBaselineAtRef`). REMEDIATION IS PART OF THE GATE (#2778): the failure output names its own remedy, because a gate that states a requirement and withholds the means of satisfying it is a maintainer round-trip, not a gate — ADR-2719 §3's \"conspicuous declaration\" only works if the contributor can discover how to make it. Both failing branches name the commit trailer to add — `Emitted-Drift-Ack-Hash:` or `Emitted-Drift-Ack-Growth:` (ADR-3942) — print its exact grammar (` — `, key and reason split on the FIRST em dash), and repeat \"do NOT regenerate anything\" — post-#2724 there is nothing left to regenerate, and hunting for a deleted baseline is the predictable wrong guess. The two branches key on DIFFERENT, now STRUCTURALLY DISTINCT trailer key spaces (separate maps since ADR-3942, closing a latent defect where a growth key could satisfy a hash lookup by naming coincidence and vice versa) and each says which: the hash pass keys on the EMITTED PATH (always contains a `/`, `Emitted-Drift-Ack-Hash:`), the size ratchet keys on the BARE FILENAME (`Emitted-Drift-Ack-Growth:`; `currentSizes` writes `sizes[entry.name]` from readdirSync over `msd-core/workflows/` + `agents/`). A stale-ack failure additionally says to drop the trailer line (amending the commit) when removing its last entry, since a lingering unused trailer signals nothing; post-#2789 it also offers CORRECTING the reason to name the ripple actually made, which is the other honest resolution and the one a contributor usually wants. NOT ack-able and deliberately given no ack text: the `NEW_FILE_CAP` branch, whose remedy is extraction. Text is sourced from one frozen `REMEDIATION` export in tests/helpers/emitted-diff.cjs, whose example line is rendered via `renderAckTrailer` (`: — `, ADR-3942) so the taught grammar cannot drift from what `parseAckTrailers` actually accepts (a round-trip test feeds the printed line back through the parser); a key that is reserved (`__proto__`/`constructor`/`prototype`) or contains `<`, `>`, or whitespace is rejected loudly, and a doc example like ` — ` must never parse as a real declaration. Note the ADR's Consequences originally called the #2724 migration \"terminal\"; #2778 corrected that — it is terminal only for a PR that grows no shipped file. The ack was PR-lifetime data kept in permanent, shared, merge-path state, and each fix generated the next defect until ADR-3942 moved it off the tree entirely (see `### Emitted Artifact Provenance`): the single shared `tests/emitted-drift-ack.json` was a guaranteed merge-conflict cell (#2789; 5 of 6 conflicting PRs in one open queue collided on it and nothing else); #2914 replaced it with per-PR fragments under `tests/emitted-drift-acks/` — the `.changeset/` shape — ending the FILE conflict but not the KEY conflict, since two sources could never name the same path; #3078 found a fully-spent fragment left on `next` still walled off every key it owned (measured at the sweep: 45 fragments owning 403 paths, up from 13/272 at triage 19 days earlier) and added the post-merge-only `guard-no-ack-on-next` job plus a manual sweep; #3842's hand sweep handed three in-flight external PRs a `modify/delete` conflict each; #3823's hand-authored sweep, computed at branch time against a guard that evaluates at merge time, lost the race to a fragment merged mid-flight and left `next` red for 24 consecutive pushes; #3875's timed sweeper workflow automated the remedy but could not merge its own PRs (three independent, deterministic defects — bad conventional-title match, wrong CI-lane classification, no auto-merge path). ADR-3942 ends the chain: the escape hatch is now a commit trailer scoped to the PR's own commits, so there is no shared file, no shared key namespace, and nothing to sweep — the fragment directory, the next-lane guard job, the scheduled sweep workflow and the standalone ack linter are all DELETED (named by ROLE rather than by filename on purpose: a backticked path here asserts a LIVE repo path and `check-glossary-refs.cjs` fails on one that does not exist, while `lint-removed-but-needed.cjs` additionally fails on a deleted file's bare BASENAME appearing anywhere it scans — and this predicate's generated projection lands in docs/, which it does scan. ADR-3942 carries the exact paths; it sits under docs/adr/, which that guard exempts as a historical record). cf `RULESET.WORKFLOW_SIZE_BUDGET`, `RULESET.AGENT_SIZE_BUDGET`; see `### Emitted Artifact Provenance`", - "line": 666 + "line": 674 }, { "id": "RULESET.GENERATIVE-FIX", "klass": "RULESET", "value": "parallel implementations diverge silently when no parity test enforces equality at the test layer; for any new constant/array/parser shared between two parallel surfaces (two workflow surfaces, or a generated artifact and its hand-authored source), the same commit MUST add a parity assertion that fails when the two diverge; exemplar: tests/runtime-launcher-parity.test.cjs (asserts every workflow bash block uses the canonical msd_run launcher)", - "line": 911 + "line": 918 }, { "id": "RULESET.GH.AUTH.DEFAULT", "klass": "RULESET", "value": "source .envrc GITHUB_TOKEN before gh; exception=ambient allowed only when user explicitly says machine-only fallback", - "line": 696 + "line": 704 }, { "id": "RULESET.HARNESS.test-memory-guard", "klass": "RULESET", "value": "~/.claude/hooks/test-memory-guard.sh fires on every Bash PreToolUse; if argv[0]∈{node|vitest|jest|mocha|tsx|ts-node|tap|ava|playwright|cypress} OR matches (npm|pnpm|yarn|bun) (run )?(t|test|tests|vitest|jest); blocks via hookSpecificOutput.permissionDecision=deny when sum(RSS of running matching procs, excluding tsserver|*-mcp|claude|Electron|...) ≥ 4 GiB OR when argv[0] basename matches a running process's argv[0]. Exception: node --version|-v|--help|-h|-p|-e are trivial probes and skip the check. Designed for a 24 GB Mac where prior accidental fan-out exhausted RAM", - "line": 955 + "line": 962 }, { "id": "RULESET.MANIFEST-CANONICAL-KEY", "klass": "RULESET", "value": "docs/INVENTORY-MANIFEST.json has a single top-level key: families; ALL EIGHT families.* arrays (agents/commands/workflows/references/cli_modules/hooks flat, plus workflow_modes/workflow_steps nested — #2996, epic #1671 Phase 6.5) are canonical, consumed by test suites — tests/inventory-manifest-sync.test.cjs reads all eight, edit-phase/enh-2380/enh-2430 tests read commands+workflows; the six flat families are keyed by BARE BASENAME while the two nested families are keyed by // path, deliberately, because two workflows may each own a same-named step file and a basename key would silently drop one under a JSON-equality comparison; recursion is bounded at exactly one named subdirectory, never a general walk; the family tables live ONCE in scripts/gen-inventory-manifest.cjs and are IMPORTED by the test (the test formerly redeclared them, a DEFECT.GENERATIVE-FIX divergence that let a new family be verified by nobody while still reporting green); the old generated date field and the stale top-level workflows key are both gone; regen via node scripts/gen-inventory-manifest.cjs --write, AFTER build:lib; #3762 added the ROSTER half — tests/inventory-manifest-sync.test.cjs now also asserts every manifest entry has a hand-written row in docs/INVENTORY.md, via the pure matcher in tests/helpers/inventory-roster.cjs. Scope is the SIX FLAT families only, each searched inside its own `## ` section; workflow_steps/workflow_modes are DELIBERATELY exempt because docs/INVENTORY.md §\"Workflow Sub-Files\" is a shipped decision that they carry no hand-written per-file rows. Matching is whole-CELL-exact (never substring — the rostered host-integration-adapters/imperative-hook-bus.cjs must not satisfy the separate top-level hook-bus.cjs) and section-scoped (smart-entry.md and smart-entry.cjs are different families), EXCEPT commands, which match on the row's Source-column link to ../commands/msd/.md because the six ns-* namespace routers deliberately RENDER a name that is not their file stem (/msd-workflow ← ns-workflow.md) — DEFECT.DISPLAY-VALUE-AS-IDENTITY. Landing the gate required backfilling 32 pre-existing unrostered surfaces on next", - "line": 677 + "line": 685 }, { "id": "RULESET.PR-FLOW.docker-before-push", "klass": "RULESET", "value": "before ANY git push of any fix to any PR, run msd-test (docker on the remote, mirrors ubuntu CI) and confirm exit 0. macOS-local node --test is NOT a substitute — many failures are platform-specific (path separators, case sensitivity, locale, fs semantics). Watchdog with Monitor on the output log; never set a sleep/timer and walk away. Source: user feedback 2026-05-16 — \"we don't set a timer we actively watch and record results in real time as possible\". SUPERSEDED 2026-07-17: 'confirm exit 0' is a false-green trap — piping/backgrounding can report exit 0 on a failed suite; gate on the verdict-line outcome:\"passed\" for the exact HEAD sha instead. See CLAUDE.md's msd-test rule and the msd-test-is-ref-based-commit-first predicate for the current, correct gating contract.", - "line": 957 + "line": 964 }, { "id": "RULESET.PR-FLOW.templates-mandatory", "klass": "RULESET", "value": "every gh pr create|edit|gh issue create|edit MUST first invoke the gh-templates-first skill and Read (Read tool, not Bash cat — k321 read-tracking) the matching template in .github/. Apply ALL required sections; never write freeform bodies. Repo enforces this via msd-pr-template-policy GitHub Action which flags any non-templated body — the bot allows the PR to stay open only because authors are contributors-or-higher, but the warning is a real complaint that must be cured. Source: user feedback 2026-05-16 (multi-message escalation) — \"the whole reason i have that github action is because you fucking blow through and ignore using the templates\"", - "line": 959 + "line": 966 }, { "id": "RULESET.PR-SCOPE.one-concern-per-pr", "klass": "RULESET", "value": "split unrelated changes into separate PRs; cherry-pick doc changes to dedicated docs/ branch immediately, then force-push original to remove the commit", - "line": 679 + "line": 687 }, { "id": "RULESET.SHARED-HELPERS-LINT-VS-TEST", "klass": "RULESET", "value": "when a lint script and test suite both implement same constant (CANONICAL_TOOLS) or parser (parseFrontmatter, executionContextRefs), extract to scripts/*-helpers.cjs required by both — silent divergence otherwise", - "line": 674 + "line": 682 }, { "id": "RULESET.TESTS.CODERABBIT_FIX", "klass": "RULESET", "value": "prefer exported-function behavioral tests over source-grep; lint-no-source-grep rejects readFileSync source assertions without allow-test-rule", - "line": 703 + "line": 711 }, { "id": "RULESET.TESTS.boundary-coverage", "klass": "RULESET", "value": "tests MUST exercise inputs at and near the threshold/limit, not only trivial-fit and trivial-overflow; pick inputs where N ∈ {limit-1, limit, limit+1} and where pre-trim/pre-check accumulators ≈ effective limit; \"very small\" and \"very large\" inputs alone do not constitute edge-case coverage and routinely miss off-by-one + reservation-accounting bugs", - "line": 646 + "line": 654 }, { "id": "RULESET.TESTS.boundary-coverage.anti-pattern", "klass": "RULESET", "value": "test suites that pair budget:1_000_000 (trivially fits) with budget:1 (trivially overflows) and skip the boundary region; failure mode that shipped PR #3708 UNNEEDED_TRIM + FALSE_HARDFAIL regressions (commit 2df566ed, fixed bde1ae8f)", - "line": 649 + "line": 657 }, { "id": "RULESET.TESTS.boundary-coverage.fixtures", "klass": "RULESET", "value": "for any code with budget/limit/quota/threshold parameter, test suite MUST include: (a) input where SUT estimate == limit exactly, (b) input where estimate == limit - 1, (c) input where estimate == limit + 1, (d) input where any internal reserve/safety constant pushes baseline within reserve-distance of limit (catches early-pressure firing)", - "line": 648 + "line": 656 }, { "id": "RULESET.TESTS.clock-seam", "klass": "RULESET", "value": "concurrency logic must accept an optional {clock=Date} parameter; tests control time via t.mock.timers.enable(['Date']) + t.mock.timers.setTime(0) + t.mock.timers.tick(N); real OS scheduler races are not a permitted test pattern after ADR 456 (2026-05-28); real-race tests are deleted once deterministic seam tests cover the same logical path; clock.cjs realClock adds nowIso() (→ new Date(this.now()).toISOString()) and today() (→ nowIso().split('T')[0]) so all date-stamping in state.cjs routes through the seam; subprocess time-pin adapter: set MSD_TEST_MODE=1 + MSD_NOW_MS= in runMsdTools env to pin the date written by the SUT without touching real wall-clock (issue #474)", - "line": 653 + "line": 661 }, { "id": "RULESET.TESTS.coderabbit-fix-prefer", "klass": "RULESET", "value": "behavioral tests (call exported fn, capture JSON, assert typed fields) over source-grep", - "line": 644 + "line": 652 }, { "id": "RULESET.TESTS.delete-bad-tests", "klass": "RULESET", "value": "pass-always / vacuous-truth / source-grep / elapsed-time / real-race / permanent-allow-test-rule tests are DELETED and replaced with compliant tests in the same PR; not skipped, not commented out, not permanently exempted; replacement must cover the same logical path via typed-surface assertion or clock-seam pattern", - "line": 658 + "line": 666 }, { "id": "RULESET.TESTS.diagnostics", "klass": "RULESET", "value": "after JSON.parse, assert output shape (Array.isArray(output.phases)) with raw-output-prefix diagnostics before .map() — prevents opaque TypeErrors when CLI output shape changes", - "line": 645 + "line": 653 }, { "id": "RULESET.TESTS.escape-regex", "klass": "RULESET", "value": "new RegExp(\"prefix${var}\") must escapeRegex(var); phase-id.cjs exports escapeRegex (core.cjs re-export spine retired in epic #1267); phase IDs like 5.1 contain . which is metacharacter", - "line": 641 + "line": 649 }, { "id": "RULESET.TESTS.eslint-harness", "klass": "RULESET", "value": "ADR 452 (2026-05-28): ESLint flat config + typescript-eslint + eslint-plugin-n + eslint-plugin-no-only-tests + local plugin at eslint-rules/ (repo root, NOT scripts/eslint-rules/); replaces scripts/lint-*.cjs regex scanners (fully removed in #632); all three test-rigor rules now ship at error in tests/**/*.test.cjs scope: local/no-source-grep and local/no-magic-sleep-in-tests promoted by #3313, local/no-elapsed-assertion promoted by #3331 once #3314 delivered its ADR-456 §(a) precondition (epic #1885 was subsumed into epic #3053 and closed stale before this promotion landed)", - "line": 659 + "line": 667 }, { "id": "RULESET.TESTS.feedback-loop-convergence", "klass": "RULESET", "value": "when a feature's OUTPUT feeds back into its own INPUT (calibration, retry backoff, adaptive budgets, ratchets, any self-correcting signal), step-wise tests are NOT sufficient evidence of correctness: they assert `given X return Y` while the defect lives in the TRAJECTORY across iterations. Required: a closed-loop test that (a) drives the REAL end-to-end surface — not the pure core alone, since composition bugs live between surfaces — for N >= 2x the loop's window, (b) asserts convergence on the known-true value, (c) asserts the fixed point (an already-correct history must produce NO correction), and (d) asserts boundedness under an adversarial/oscillating history. Two defects shipped past a green ~26,800-test suite in epic #1952 for want of exactly this: calibration applied twice across two surfaces (factor^2, #2631) and calibration measured against its own corrected output so it oscillated to ~1.41 instead of converging on 2.0 (#2632). Every unit, boundary, property and round-trip test passed for both. HOW TO SPOT ONE (the detection tell, not a judgment call): the feature's own acceptance criterion carries a TEMPORAL QUANTIFIER — \"after N phases\", \"subsequent\", \"over time\", \"improves\", \"learns\", \"adapts\". That phrasing means the claim is about a TRAJECTORY, so a step-wise `given X return Y` test does not test the claim that was made. #1952's AC4 read \"After N phases, the error is computed and applied as a correction to SUBSEQUENT estimates\" — the tell was in plain sight and was still tested as a point. Survey of this repo (2026-07): estimation calibration is the ONLY true instance; size/mutation ratchets are exempt because they fail on both growth AND shrinkage (cannot self-satisfy), and retry ladders (node_repair_budget, plan_bounce_passes, provider_escalation) terminate rather than feed back. Test anchor: tests/estimate-loop-convergence.test.cjs", - "line": 647 + "line": 655 }, { "id": "RULESET.TESTS.guard-toplevel-readFileSync", "klass": "RULESET", "value": "module-level const src = readFileSync(...) throws before any test() registers — wrap in try/catch in test() or use lazy load", - "line": 643 + "line": 651 }, { "id": "RULESET.TESTS.mutation-runner", "klass": "RULESET", "value": "Stryker executes every shard through the OFFICIAL @stryker-mutator/tap-runner (testRunner:'tap'), never the built-in 'command' runner (#3915); 'command' is the one runner Stryker excludes from coverage analysis, which forced coverageAnalysis:'off' and made cost strictly linear in (mutants x whole-shard test time) — the frontmatter shard measured 1751s on run 33021042847 vs 212s for the next slowest. tap.testFiles is injected per shard via MUTATION_TEST_FILES (mutation.yml env <- matrix.tests <- scripts/mutation-matrix.cjs buildResult); resolveMutationTestFiles is the SINGLE fail-closed reader and existence-checks every entry, because the tap runner's findTestyLookingFiles resolves the list with glob() and a non-matching pattern yields an EMPTY list SILENTLY (a fast, confident, meaningless run). tap.forceBail is FALSE by measurement, not preference: 3 of 26 shard test files spawn subprocesses (config-schema.property, core-utils, feat-3881-yaml-parser-consequences) and bail fires on every KILLED mutant, so leaving it on kills processes mid-spawnSync and orphans their children; Stryker's separate disableBail still skips remaining FILES, which is most of the win. tap.nodeArgs and top-level buildCommand stay UNSET so no rebuild lands between mutation and test (ADR-457). Coverage granularity is per FILE, not per test (\"a test is always a test file\"), so the #2790 excludeTests bans on spawn-heavy integration files remain necessary and unchanged", - "line": 656 + "line": 664 }, { "id": "RULESET.TESTS.mutation-score", "klass": "RULESET", "value": "Stryker runs incremental (--since origin/next) on ubuntu-latest/Node24 CI leg; default threshold 80% killed/total; surviving mutants in scope block merge unless path is listed in stryker.config.mjs with documented reason; treat surviving mutant as a failing test specification", - "line": 655 + "line": 663 }, { "id": "RULESET.TESTS.mutation-score-denominator", "klass": "RULESET", "value": "the gated number is mutation-testing-metrics' mutationScore = totalDetected/totalValid, which counts NoCoverage in the denominator EXACTLY as Survived; both Stryker's own thresholds.break (core dist/src/reporters/mutation-test-report-helper.js) and scripts/check-mutation-score-ratchet.cjs read THAT field, which is what makes the #3915 coverageAnalysis 'off'->'perTest' switch score-neutral. NEVER gate on mutationScoreBasedOnCoveredCode — it EXCLUDES NoCoverage and inflates sharply under perTest (measured on a synthetic report: 8 killed/2 survived = 80 and 80; 8 killed/2 noCoverage = 80 and 100), so swapping to the better-sounding field would make every minScore floor trivially satisfiable and the gate decorative. Under the pre-#3915 coverageAnalysis:'off' the two fields were ALWAYS identical (noCoverage was structurally 0), which is why nothing had ever pinned the choice; tests/mutation-score-ratchet.test.cjs now pins it with a non-vacuity assertion that the two numbers genuinely diverge", - "line": 657 + "line": 665 }, { "id": "RULESET.TESTS.no-dead-regex-in-includes", "klass": "RULESET", "value": "src.includes(\"foo.*bar\") is always false — .* is regex metacharacter not wildcard; use new RegExp(...).test(src) or delete", - "line": 642 + "line": 650 }, { "id": "RULESET.TESTS.no-duplicate-fold-marker", "klass": "RULESET", "value": "local/no-duplicate-fold-marker ESLint AST rule (eslint-rules/no-duplicate-fold-marker.cjs, #3271) reports the 2nd and every later __foldDescribe(\"folded: ...\") call carrying a marker already seen in the SAME file, naming the first occurrence's line; error in tests/**/*.cjs. The key is the WHITESPACE-delimited token after folded:, NOT a [a-z0-9-]* slice — a slice truncates at \".\" and collides feat-443-effort-fast-mode.integration with feat-443-effort-fast-mode (two distinct suites coexisting in tests/model-resolver.test.cjs), and NOT the whole title, so a re-fold under a different batch label (\"B1 #1970\" vs \"B5 #1975\") is still caught. Deliberately silent on: a __foldDescribe title with no folded: prefix (the alias is reused for one ordinary describe in tests/review-default-reviewers-workflow.test.cjs), a plain describe(), a non-literal title, and the same marker in two DIFFERENT files (the defect class is intra-file).", - "line": 638 + "line": 646 }, { "id": "RULESET.TESTS.no-duplicate-fold-marker.why", "klass": "RULESET", "value": "consolidation epic #1969 folds are self-contained blocks, so a second verbatim copy parses, registers and PASSES twice — nothing reports it; #3271 found 25 such copies (~5,800 lines) in tests/install.test.cjs (18), tests/install-minimal-hooks.test.cjs (5) and tests/install-write-confinement.test.cjs (2), all from one stale-base re-application in 6d072435d (#1975 re-applying #1970's hunks, 2026-07-03). Ref DEFECT.GENERATIVE-FIX: the two copies drift apart silently when a contributor fixes one and leaves the other asserting the old behavior, with the suite still green.", - "line": 639 + "line": 647 }, { "id": "RULESET.TESTS.no-source-grep", "klass": "RULESET", "value": "local/no-source-grep ESLint AST rule (eslint-rules/no-source-grep.cjs) rejects readFileSync of a source .cjs/.js/.ts path bound to a var later hit with .includes()/.match()/.startsWith()/.endsWith()/.indexOf()/.search(); error in tests/**/*.test.cjs, warn in msd-core/bin/**/*.cjs + scripts/**/*.cjs (ADR 452 retired the old regex script, removed for good in #632)", - "line": 635 + "line": 643 }, { "id": "RULESET.TESTS.no-source-grep.exemption", "klass": "RULESET", "value": "// allow-test-rule: with one-line justification; reserved for tests where the file content IS the product surface (STATE.md, config.toml, hooks.json, agent .md). Migration to typed-IR parser tracked in #2974.", - "line": 636 + "line": 644 }, { "id": "RULESET.TESTS.no-source-grep.tmp-file-traps", "klass": "RULESET", "value": "reading tmp files written by the SUT in tests still trips lint; round-trip through CLI (e.g. frontmatter get) instead of readFileSync+.includes()", - "line": 637 + "line": 645 }, { "id": "RULESET.TESTS.no-timing-assertion", "klass": "RULESET", "value": "do not assert on wall-clock elapsed time (Date.now() delta, performance.now(), process.hrtime() comparison); such assertions test the host machine not the SUT and flake on loaded CI runners; enforcement: local/no-elapsed-assertion ESLint rule, error (promoted by #3331 once #3314 delivered the ADR-456 §(a) reachability rule + deterministic backfill precondition); canonical replacement: clock-seam pattern with node:test mock.timers", - "line": 652 + "line": 660 }, { "id": "RULESET.TESTS.property-based-testing", "klass": "RULESET", "value": "modules implementing parsing / transformation / budget-limit / bijective contracts must include at least one fast-check (fc) property test asserting a domain invariant; invariant categories: round-trip, monotonicity, boundary-containment, idempotency; property tests live in *.test.cjs alongside unit tests; CI signal: Stryker mutation score below 80% blocks merge", - "line": 654 + "line": 662 }, { "id": "RULESET.TRIAGE-EXISTING-WORK", "klass": "RULESET", "value": "before writing agent brief for confirmed bug, check (1) local branches git branch -a | grep , (2) untracked/modified files on that branch, (3) stash, (4) open PRs with matching head branch — recover existing work rather than re-implement", - "line": 681 + "line": 689 }, { "id": "RULESET.WORKFLOW.COVERAGE-METADATA", "klass": "RULESET", "value": "#1602 SUMMARY frontmatter `coverage:` block (list of {id,description,requirement?,verification:[{kind∈unit|integration|e2e|automated_ui|manual_procedural|other, ref, status∈pass|fail|unknown}],human_judgment:bool,rationale?}) is the per-deliverable RTM consumed DETERMINISTICALLY by verify-work extract_tests via `msd-tools uat classify-coverage --summary ` (src/coverage.cts → bin/lib/coverage.cjs). AUTHORING: execute-plan create_summary populates it from task results; every deliverable MUST be classified; fail-safe default = human_judgment:true + rationale. CLASSIFY CONTRACT: auto-pass (skip human) ONLY when human_judgment===false (strict boolean) AND verification non-empty AND every status==='pass' AND zero validation errors — else PRESENT to human. mode:legacy (no block) ⇒ byte-identical prose `## Accomplishments` fall-through; `coverage: []` ⇒ mode:coverage, zero entries (single-confirmation). Frozen IR: MODE/PRESENT_REASON/ERROR_CODE enums locked by tests/coverage-metadata-parser.test.cjs. extractFrontmatter CANNOT parse it (scalars-only `-` items) → dedicated parser, sibling of parseMustHavesBlock. Asymmetry by design: false-negative=redundant prompt (status quo); false-positive=shipped bug UAT existed to catch", - "line": 670 + "line": 678 }, { "id": "RULESET.WORKFLOW_EXECUTE_END_TO_END", "klass": "RULESET", "value": "standard for single-workflow commands is \"Execute end-to-end.\" (no bolded **Follow the X workflow** fragments); flag-dispatch routing uses \"execute the X workflow end-to-end.\" in routing bullets — convention verified live across ~20 commands/msd/*.md files; no ADR currently documents this specific phrasing rule (ADR-0002 covers the adjacent but distinct command-contract/@-ref-resolution seam, not this convention)", - "line": 669 + "line": 677 }, { "id": "RULESET.WORKFLOW_EXECUTION_CONTEXT", "klass": "RULESET", "value": "@-ref in commands/msd/*.md must resolve to an existing file on disk; regression test in tests/docs-update.test.cjs (folds former \\`bug-3135-capture-backlog-workflow\\`, consolidation epic #1969); INVENTORY.md row + INVENTORY-MANIFEST.json families.workflows must stay in sync; \"Invoked by\" attribution must move when a flag absorbs a micro-skill", - "line": 668 + "line": 676 }, { "id": "RULESET.WORKFLOW_FILE_NAMES", "klass": "RULESET", "value": "workflow files use hyphens; XML attributes must match (extract-learnings not extract_learnings); tests should pin exact hyphenated name", - "line": 667 + "line": 675 }, { "id": "RULESET.WORKFLOW_MARKDOWN.FENCES", "klass": "RULESET", "value": "preserve opening language fence when editing shell snippets in workflow markdown; malformed fence creates fresh CR threads (MD040)", - "line": 663 + "line": 671 }, { "id": "RULESET.WORKFLOW_SIZE_BUDGET", "klass": "RULESET", "value": "workflow size enforcement (#1074; BYTES not lines per #717; LF-normalized per #683) = differential attribution size ratchet (PRIMARY anti-creep since #2724/ADR-2719 §4: tests/emitted-attribution.test.cjs's real-tree test reports growth in any msd-core/workflows/*.md with its exact byte delta vs `next`, no committed snapshot, requires an `Emitted-Drift-Ack-Growth:` commit trailer on the PR's own commits (ADR-3942, superseding ADR-2719 §3's fragment model — key is the bare filename, reason follows ` — `)) + loose tier hard caps (outer red lines, NEVER raised on approach: XL<=98304 / LARGE<=61440 / DEFAULT<=40960) + discuss-phase<32000; a file that grew fails the differential guard — add an ack entry naming the file and reason, justify the growth in the PR (or extract LAZILY-loaded content; eager @-imports don't reduce loaded context); crossing a hard cap means EXTRACT, not bump. The prior per-file baseline (tests/workflow-size-baseline.json, `npm run size:baseline`) is REMOVED by #2724. Its new-file cap (ADR-1610 Decision point 3, un-baselined files <=32768, the Codex anchor) is REVIVED inside the differential's size ratchet itself (`NEW_FILE_CAP` in tests/helpers/emitted-diff.cjs) rather than lost: \"not yet baselined\" is exactly \"present in sizeCurrent, absent from sizeBaseline\", a signal the ratchet already computes for its own reasons. NOT ack-able — same as the tier hard caps, the fix is extraction. Narrower than the original: this check cannot see XL/LARGE tiering (tests/workflow-size-budget.test.cjs's classification, invisible to the pure differential module), so a legitimately large NEW file must extract rather than tier in, one release earlier than an existing file would need to — a disclosed, deliberate simplification", - "line": 664 + "line": 672 }, { "id": "SEAM.capability-activation-precedence-owner.enforced-by", "klass": "SEAM", "value": "test:tests/capability-precedence-parity.test.cjs", - "line": 440 + "line": 445 }, { "id": "SEAM.capability-activation-precedence-owner.owns", "klass": "SEAM", "value": "the config-key four-level precedence walk (loadConfig result → workstream config.json → root config.json → registry.configSchema default → absent), owned solely by src/capability-activation.cts", - "line": 439 + "line": 444 }, { "id": "SEAM.git-query-readonly-seam.enforced-by", "klass": "SEAM", "value": "test:tests/git-base-branch.test.cjs", - "line": 227 + "line": 230 }, { "id": "SEAM.git-query-readonly-seam.owns", "klass": "SEAM", "value": "bounded, never-throw git repository introspection — base-branch detection, worktree-info detection, phase change-set detection", - "line": 226 + "line": 229 }, { "id": "SEAM.package-identity.enforced-by", "klass": "SEAM", "value": "test:tests/package-identity.test.cjs", - "line": 304 + "line": 309 }, { "id": "SEAM.package-identity.owns", "klass": "SEAM", "value": "MSD's published-package coordinates (packageName, binName, repoSlug, changelogRawUrl, manualInstallCommand) — single seam so a repoint/rename is a one-line change", - "line": 303 + "line": 308 }, { "id": "SEAM.phase-locator-milestone-enum.enforced-by", @@ -1524,13 +1518,13 @@ "id": "SEAM.shellcmdproj-win-binary-resolution.enforced-by", "klass": "SEAM", "value": "lint-rule:no-private-binary-resolution", - "line": 922 + "line": 929 }, { "id": "SEAM.shellcmdproj-win-binary-resolution.owns", "klass": "SEAM", "value": "Windows binary resolution (resolveExecutableBinary, projectSpawnInvocation) — which file a declared command name actually names, and cmd.exe mediation", - "line": 921 + "line": 928 }, { "id": "SEAM.verification-isphasecomplete.enforced-by", @@ -1548,199 +1542,199 @@ "id": "SEAM.worktree-safety-policy.enforced-by", "klass": "SEAM", "value": "test:tests/worktree-safety.test.cjs", - "line": 731 + "line": 739 }, { "id": "SEAM.worktree-safety-policy.owns", "klass": "SEAM", "value": "Worktree Safety Policy Module — resolveWorktreeContext, parseWorktreePorcelain, planWorktreePrune, executeWorktreePrunePlan, W017 classification (see WORKTREE.SEAM.* above for full interface/invariant detail)", - "line": 730 + "line": 738 }, { "id": "SESSION.2026-05-05", "klass": "SESSION", "value": "[PRED.k320..k331 introduced; DEFECT.SOURCE-GREP-IN-NEW-TESTS, DEFECT.CHANGESET-PR-FIELD-DRIFT, DEFECT.PHASE-DIR-PREFIX-DRIFT, DEFECT.PROMPT-INJECTION-SCAN-COLLISION; ADR-0002 thin-wrapper pattern findings folded into RULESET.WORKFLOW_*]", - "line": 945 + "line": 952 }, { "id": "SESSION.2026-05-05.sdk-bridge", "klass": "SESSION", "value": "PR #3158 SDK Runtime Bridge — observability isolation rule; strict-mode dispatchMode reporting invariant; transport decision ordering (guard before event emission); folded into Dispatch Policy Module glossary", - "line": 946 + "line": 953 }, { "id": "SESSION.2026-05-09", "klass": "SESSION", "value": "[8-PR triage wave, 7 merged + 1 subsumed; META.RULE.* introduced; WAVE.LESSON.* captured; k320/k322/k323/k326/k331 evidence; AI Ops Memory predicate format established]", - "line": 947 + "line": 954 }, { "id": "SESSION.2026-05-10", "klass": "SESSION", "value": "[ai-ops memory consolidation; release-notes standard taxonomy + templates; RELEASE-NOTES.* predicates introduced]", - "line": 948 + "line": 955 }, { "id": "SESSION.2026-05-13", "klass": "SESSION", "value": "[Shell Command Projection Module expansion (#3465-#3468); ADR-0009 superseded; new exports for subprocess dispatch and platform file I/O; phase-gated migration plan; PR #3464 three-gate invariant CI+CR+unresolved=0; PR #3470 stash-include-untracked rebase pattern]", - "line": 949 + "line": 956 }, { "id": "SESSION.2026-05-14", "klass": "SESSION", "value": "[#3095/PR #3490 EXEC.CLASSIFY.* introduced (Anthropic/Copilot/Codex/Gemini [runtime removed #1928] cross-runtime rate-limit sentinel coverage); #3489/PR #3499 DEFECT.STATE-TRAMPLE.idempotency-oracle (STATE.md current_phase field is oracle for state.complete-phase); #3488/PR #3501 DAG resolver same-phase short-form depends_on (shortFormToId index added to sdk/src/query/phase.ts); #3491/PR #3502 DEFECT.NESTED-GIT-INIT (gitWorktreeInfoInternal helper); #3493/PR #3500 extractCurrentMilestone generic Phase Details continuation past planned-milestone siblings; #3503/PR #3504 DEFECT.PATH-SUBSTRING-CHECK (trailing-slash anchor for homedir checks); #3346/PR #3505 codex AoT TOML leaf-key via extractFlatHookEventName; #3506/PR #3507 label-scoped stale-bot sub-job pattern; multi-PR triage operational lessons folded into PROC.TRIAGE.*; #3508 DEFECT.AGENT-ISOLATION-SILENT-FAIL; msd-test image-missing auto-build (locally-built image via embedded heredoc Dockerfile); refined PRED.k322 threshold to 3 PRs/<10min]", - "line": 950 + "line": 957 }, { "id": "SESSION.2026-05-15", "klass": "SESSION", "value": "[#3537/PR #3538 DEFECT.PHASE-REGEX-FANOUT — phaseMarkdownRegexSource promoted to core.cjs and wired to 7 sites; parity-style regression test established as DEFECT.GENERATIVE-FIX exemplar; trek-e/msd-test-runner#1 filed for DEFECT.MSD-TEST-MIRROR-POISONED — chown-back-before-exec legacy gap (poisoned holodeck mirror unstuck via authorized docker chown to remote 1000:1000); RULESET.PR-FLOW.* codified from project CLAUDE.md load-bearing rule; first dispatch under run-tests-before-create held cleanly (PR #3520 worker stopped on Docker exit 12 infra failure, orchestrator opened PR after unblock); CONTEXT.md refactored from 882 lines of mixed prose+predicates into ~500 lines of pure-predicate format with chronological session log]", - "line": 951 + "line": 958 }, { "id": "SESSION.2026-05-15.parallel-fix-dispatch", "klass": "SESSION", "value": "[#3542/PR #3546 prohibit git stash family in executor agents (shared refs/stash across worktrees); #3541/PR #3547 non-TTY resolution for installer prompt-user actions (default remove for SDK build artifacts, keep for skills/msd-*/SKILL.md); #3545 filed for msd-test-summary concurrent /tmp output collision; new predicates DEFECT.HOOK-OVER-ENFORCEMENT.read-tool-tracking, DEFECT.MSD-TEST-CONCURRENT-OUTPUT-COLLISION, DEFECT.SUBAGENT-LONG-RUNNING-BG-STALL, DEFECT.AGENT-RETIRED-SLASH-SYNTAX-DRIFT, PROC.PARALLEL-FIX-DISPATCH; agent-trust-but-verify caught /msd-update retired-syntax comment slip in #3541 implementation before PR open]", - "line": 952 + "line": 959 }, { "id": "SESSION.2026-05-16", "klass": "SESSION", "value": "[multi-PR triage wave (#3577/3581/3640/3641/3642/3648/3649/3637/3639). Established global PreToolUse hook ~/.claude/hooks/test-memory-guard.sh denying new node/test spawns when sum(RSS of node|vitest|jest|...) >= 4 GiB on the 24 GB Mac OR when a same-runner process is already in argv[0] — hard deny via hookSpecificOutput.permissionDecision=deny. PR #3577 fix: revert config-ensure-section dispatch to CJS cmdConfigEnsureSection (SDK author wrote single-section semantics under a name whose legacy callers expect full-default config init); plus 3 SDK parity carve-outs (configNewProject defaults align with sdk/shared/config-defaults.manifest.json, return relative .planning/config.json path, drop quotes from Unknown config key, lead malformed-JSON error with \"Failed to read config.json:\"). PR #3649 fix: chunk node --test spawn at 28K argv ceiling (Windows CreateProcess lpCommandLine cap 32,767 was instantly aborting unchunked spawn of 546 paths). Chunking fix surfaced 14 pre-existing Windows-only test bugs (4010 pass / 14 fail; vs 0/0 before — entire suite was un-runnable on Windows). PRs #3639 + #3637 confirmed unable to stand alone (legitimately depend on Phase 6 scaffolding only present on feat/3575-enforcement-hardening) — user decision: cherry-pick into #3577 and close. Five other PRs each had ≤1 unresolved CR thread of the changeset-pr-number / null-vs-throw / implicit-Claude-runtime / docs-stale-guidance / hardcoded-tests-path family — all quick wins. New predicates: DEFECT.SDK-PORT-NAME-COLLISION, DEFECT.WINDOWS-ARGV-OVERFLOW, DEFECT.STACKED-PR-CANNOT-STAND-ALONE, DEFECT.CANARY-VERSION-LEAK, DEFECT.MSD-TEST-HOST-MID-RUN-DEATH, RULESET.HARNESS.test-memory-guard, RULESET.PR-FLOW.docker-before-push, RULESET.PR-FLOW.templates-mandatory]", - "line": 953 + "line": 960 }, { "id": "WAVE.LESSON.agent-narrative-unreliable", "klass": "WAVE", "value": "k095/k324 confirmed at scale: 5 of 8 agents terminated mid-monitor with stale claims requiring direct verification", - "line": 904 + "line": 911 }, { "id": "WAVE.LESSON.changelog-policy-violation-multiplier", "klass": "WAVE", "value": "brief contradicting CONTRIBUTING.md's changelog-fragment policy (\"CHANGELOG Entries — Drop a Fragment\" section) produced violations on 5 of 8 PRs (#3300, #3302, #3304, #3305, #3308); k326 + k320 capture", - "line": 901 + "line": 908 }, { "id": "WAVE.LESSON.cr-throttle-burst-correlation", "klass": "WAVE", "value": "8 PRs in <15min triggered k322 sustained-throttle on multiple PRs (#3306 worst case)", - "line": 902 + "line": 909 }, { "id": "WAVE.LESSON.k101-still-trips", "klass": "WAVE", "value": "even after CONTEXT.md k101 reinforcement, agent of record posted self-PR comment on close; k331 adds explicit close-time literal-instruction guard", - "line": 905 + "line": 912 }, { "id": "WAVE.LESSON.sibling-audit-overlap", "klass": "WAVE", "value": "k015-family parallel dispatch on #3297 + #3298 produced k323 add-backlog.md cross-PR overlap", - "line": 903 + "line": 910 }, { "id": "WORKSTREAM.INVARIANT.migrate-name", "klass": "WORKSTREAM", "value": "must normalize through canonical slug policy", - "line": 717 + "line": 725 }, { "id": "WORKSTREAM.INVARIANT.slug-contract", "klass": "WORKSTREAM", "value": "all .planning/workstreams/ must be addressable by set/get/status/complete", - "line": 718 + "line": 726 }, { "id": "WORKSTREAM.NAME.POLICY.cjs-module", "klass": "WORKSTREAM", "value": "msd-core/bin/lib/workstream-name-policy.cjs owns toWorkstreamSlug + active-name/path-segment validation", - "line": 735 + "line": 743 }, { "id": "WORKSTREAM.POINTER.SEAM.cjs-module", "klass": "WORKSTREAM", "value": "msd-core/bin/lib/active-workstream-store.cjs owns read/write self-heal for .planning/active-workstream", - "line": 736 + "line": 744 }, { "id": "WORKSTREAM.REGRESSION.test-anchor", "klass": "WORKSTREAM", "value": "tests/workstream.test.cjs::normalizes --migrate-name to a valid workstream slug", - "line": 719 + "line": 727 }, { "id": "WORKTREE.SEAM.caller-rule", "klass": "WORKTREE", "value": "verify.cjs must consume inspectWorktreeHealth for W017 classification; no ad-hoc porcelain parsing in callers", - "line": 727 + "line": 735 }, { "id": "WORKTREE.SEAM.current", "klass": "WORKTREE", "value": "Worktree Safety Policy Module", - "line": 711 + "line": 719 }, { "id": "WORKTREE.SEAM.decision-1", "klass": "WORKTREE", "value": "retain non-destructive default; destructive path only as explicit future opt-in scaffold", - "line": 715 + "line": 723 }, { "id": "WORKTREE.SEAM.default-prune-policy", "klass": "WORKTREE", "value": "metadata_prune_only (non-destructive)", - "line": 714 + "line": 722 }, { "id": "WORKTREE.SEAM.files", "klass": "WORKTREE", "value": "[msd-core/bin/lib/worktree-safety.cjs]", - "line": 712 + "line": 720 }, { "id": "WORKTREE.SEAM.interface", "klass": "WORKTREE", "value": "[resolveWorktreeContext, parseWorktreePorcelain, planWorktreePrune, executeWorktreePrunePlan, planWorktreeRecordAgent, cmdWorktreeRecordAgent]", - "line": 713 + "line": 721 }, { "id": "WORKTREE.SEAM.invariant", "klass": "WORKTREE", "value": "parser failure must degrade to metadata_prune_only and never escalate to destructive removal", - "line": 725 + "line": 733 }, { "id": "WORKTREE.SEAM.inventory-interface", "klass": "WORKTREE", "value": "[listLinkedWorktreePaths, inspectWorktreeHealth]", - "line": 726 + "line": 734 }, { "id": "WORKTREE.SEAM.inventory-snapshot", "klass": "WORKTREE", "value": "snapshotWorktreeInventory(repoRoot,{staleAfterMs,nowMs}) is canonical linked-worktree health snapshot for callers", - "line": 729 + "line": 737 }, { "id": "WORKTREE.SEAM.test-anchor-w017", "klass": "WORKTREE", "value": "tests/orphan-worktree-detection.test.cjs + tests/worktree-safety.test.cjs", - "line": 728 + "line": 736 }, { "id": "WORKTREE.SEAM.test-anchors", "klass": "WORKTREE", "value": "[resolveWorktreeContext:has_local_planning|linked_worktree|not_git_repo|main_worktree, planWorktreePrune:git_list_failed|worktrees_present|no_worktrees|parser_throw_fallback, executeWorktreePrunePlan:missing_plan|skip_passthrough|unsupported_action|metadata_prune_only]", - "line": 724 + "line": 732 }, { "id": "WORKTREE.SEAM.test-policy", "klass": "WORKTREE", "value": "cover all decision branches in policy module before changing prune behavior", - "line": 723 + "line": 731 } ], "duplicates": [] diff --git a/msd-core/bin/lib/capability-validator.cjs b/msd-core/bin/lib/capability-validator.cjs index c32892a6c..9d7a5f71b 100644 --- a/msd-core/bin/lib/capability-validator.cjs +++ b/msd-core/bin/lib/capability-validator.cjs @@ -900,7 +900,7 @@ const VALID_SANDBOX_TIERS = new Set(['none', 'codex-agent-sandbox']); const VALID_ARTIFACT_KIND_NAMES = new Set(['commands', 'agents', 'skills']); const VALID_ARTIFACT_NESTINGS = new Set(['flat', 'nested']); // #2871 Phase 2 — only `commands` and `skills` are trigger-bearing (a `/msd-` -// the USER types). `agents` and `kimi-agents` are a separate dispatch interface +// the USER types). `agents` is a separate dispatch interface // point (subagent invocation via `subagent_type`/named dispatch), never a trigger — // see 40-design.md's "agents are not trigger-bearing" correction. A narrower set // than VALID_ARTIFACT_KIND_NAMES on purpose: an artifact KIND can be agents; a @@ -1719,7 +1719,7 @@ function validateRuntimeBody(cap) { // promptFlag — optional; string or null (#2627, Phase 3). `null`/absent // means the host takes the executor prompt positionally (codex, opencode); - // a string names the flag that carries it (kimi/kimi-code: --prompt). + // a string names the flag that carries it (e.g. --prompt). if (oe.promptFlag !== undefined && oe.promptFlag !== null && typeof oe.promptFlag !== 'string') { errors.push( 'runtime.orchestratorExec.promptFlag must be a string or null (got: ' + JSON.stringify(oe.promptFlag) + ')', @@ -2459,10 +2459,9 @@ function validateReviewerBodyFields(cap) { * ADR-2782 D7 — probe.kind is a closed enum WIDER than existence, and every * probe that starts a process or a connection MUST be bounded. * - * `command-exists` alone is structurally insufficient: `kimi` is claimed by both - * Kimi Code CLI and the legacy Python kimi-cli (a separate first-party runtime - * capability in this repo), so an existence-only probe registers the wrong tool. - * The unbounded form of that probe was a live instance of this repo's named + * `command-exists` alone is structurally insufficient: one binary name can be + * claimed by more than one tool, so an existence-only probe may register the + * wrong tool. The unbounded form of such a probe was a live instance of this repo's named * Unbounded Subprocesses defect — it ran on EVERY /msd:review invocation * regardless of which flags were passed, so a binary waiting on a first-run auth * prompt hung every future review, including reviews that never asked for it. diff --git a/scripts/lint-retired-runtime-name.cjs b/scripts/lint-retired-runtime-name.cjs index 5f3d3bf3d..48d10e170 100644 --- a/scripts/lint-retired-runtime-name.cjs +++ b/scripts/lint-retired-runtime-name.cjs @@ -392,8 +392,8 @@ const RUNTIME_WORD_RE = new RegExp( // Positive evidence that the line is on the MODEL axis. Required, not merely // the absence of a runtime word: a reviewer demonstrated that "absence of a // veto word" is not evidence, with `The installer now offers Gemini 3.`, -// `MSD installs cleanly on Gemini 3, Kimi, and Codex.` and -// `Supported agents include Gemini 3, Kimi, and Cursor.` all exiting 0 — the +// `MSD installs cleanly on Gemini 3, Cline, and Codex.` and +// `Supported agents include Gemini 3, Cline, and Cursor.` all exiting 0 — the // exact laundering class this guard exists to catch. Every real model-axis // line in this repo names a model explicitly, so requiring it costs nothing // and inverts the failure direction from "silently allow" to "flag". diff --git a/scripts/release-tarball-smoke.cjs b/scripts/release-tarball-smoke.cjs index 750715b1f..1d8cb3057 100644 --- a/scripts/release-tarball-smoke.cjs +++ b/scripts/release-tarball-smoke.cjs @@ -408,7 +408,7 @@ function runSmoke({ lifecycleCommands = ['init', 'discuss-phase', 'plan-phase', 'execute-phase'], // claude and codex cover the two top-level config surfaces this scan knows // how to read (settings.json, and hooks.json + config.toml). Most other - // runtimes reuse one of those two shapes; Cline and Kimi do not (see + // runtimes reuse one of those two shapes; Cline does not (see // RUNTIME_CONFIG_FILES), so they are out of scope here rather than covered. entrypointRuntimes = ['claude', 'codex'], dryRun = false, diff --git a/scripts/run-tests.cjs b/scripts/run-tests.cjs index f0ffdafe1..60923e782 100644 --- a/scripts/run-tests.cjs +++ b/scripts/run-tests.cjs @@ -1736,7 +1736,7 @@ function main() { if (liveConfigGuardEnabled) { liveConfigRoots = resolveLiveConfigRoots(); // #2665 round 3: $MSD_HOME/.msd, and one native config.toml per non-registry - // config-home descriptor (Kimi CLI's and, since #2755, Kimi Code's), are live + // config-home descriptor, are live // write surfaces that are not runtime config ROOTS, so they are invisible to the // line above. Watched independently — and note the OR: the extras alone are // reason enough to snapshot, so an unbuilt tree that yields zero roots no diff --git a/src/install-profiles.cts b/src/install-profiles.cts index 9bb7ba49d..1a9ec38f4 100644 --- a/src/install-profiles.cts +++ b/src/install-profiles.cts @@ -489,8 +489,8 @@ function stageSkillsForProfile(srcDir: string, resolvedProfile: ResolvedProfile) * markers. It is still called, by `bin/install.js`'s `_stageAgents`, whose output * feeds the inline agent loop and `installCodexConfig`; both of those compose the * content themselves before writing, so the raw output never reaches disk. What - * changed in #2995 is that `agentsKind` and `kimiAgentsKind` no longer use it — - * they route through `stageAgentsForRuntimeWithConverter`, which composes. + * changed in #2995 is that `agentsKind` no longer uses it — + * it routes through `stageAgentsForRuntimeWithConverter`, which composes. * * The invariant to preserve: anything that takes this function's output and WRITES * it as a runtime artifact must call `composeWorkflow` on each file first, or it diff --git a/src/review-lane-descriptor.cts b/src/review-lane-descriptor.cts index 7bb33478e..bee630e90 100644 --- a/src/review-lane-descriptor.cts +++ b/src/review-lane-descriptor.cts @@ -282,8 +282,8 @@ const SPAWN_STDIN_STDOUT = { * * The `gemini` lane was retired by #4709: Google sunset Gemini CLI on 2026-06-18 (the same * sunset that removed the gemini RUNTIME in #1928/1.8.0), so the lane spawned a binary that no - * longer serves the free/Pro/Ultra tiers that are MSD's audience. The `qwen` and `kimi-code` - * lanes were retired together with their runtimes. + * longer serves the free/Pro/Ultra tiers that are MSD's audience. The `qwen` + * lane was retired together with its runtime. */ export const REVIEWER_LANES: ReadonlyArray = Object.freeze([ { diff --git a/tests/agent-descriptor-parity.install.test.cjs b/tests/agent-descriptor-parity.install.test.cjs index 646a39ae5..c1721127d 100644 --- a/tests/agent-descriptor-parity.install.test.cjs +++ b/tests/agent-descriptor-parity.install.test.cjs @@ -53,14 +53,10 @@ * * 3. Both scopes are exercised for every runtime that declares a * per-scope `agents` kind (claude, codex, opencode × global+local). The prior revision was global-only, which - * is exactly the class of gap that let two separate agents-drop - * regressions reach `next` undetected: cline-local (fixed alongside + * is exactly the class of gap that let an agents-drop + * regression reach `next` undetected: cline-local (fixed alongside * this rewrite — capabilities/cline/capability.json's `local` - * artifactLayout now declares an `agents` kind) and kimi-code-local - * (fixed the same way — its `local` artifactLayout previously declared - * no `agents` kind at all, so the deleted inline loop's implicit - * scope-gate was silently replaced with NO gate, dropping every - * kimi-code local install's agents/msd-*.md entirely). + * artifactLayout now declares an `agents` kind). * * H8 is mandatory, not optional: a parity harness never demonstrated failing * is decoration. It feeds the oracle a DELIBERATELY WRONG (but real, diff --git a/tests/agent-fragments-emission.install.test.cjs b/tests/agent-fragments-emission.install.test.cjs index 53f33bb65..3f79c508b 100644 --- a/tests/agent-fragments-emission.install.test.cjs +++ b/tests/agent-fragments-emission.install.test.cjs @@ -21,10 +21,9 @@ * * 1. bin/install.js's inline agent loop — non-descriptor runtimes * 2. stageAgentsForRuntimeWithConverter — the 9 descriptor runtimes - * 3. kimiAgentsKind's own readFileSync — kimi - * 4. agentsKind (converter: null) — claude(local), zcode + * 3. agentsKind (converter: null) — claude(local), zcode * - * Point 4 never reads content into JS at all: `stageAgentsForProfile` returns a + * Point 3 never reads content into JS at all: `stageAgentsForProfile` returns a * raw byte copy — or, under the DEFAULT `full` profile, the real unstaged * `agents/` directory itself — and `_copyStaged` copies bytes. * @@ -69,7 +68,7 @@ const MARKER_TOKEN = 'msd:section'; /** Path fragment identifying artifacts derived from the probe agent. Emitted * artifacts are named after the source agent on every runtime that emits them - * (`.md`, Codex's `.toml`, Kimi's `subagents/.{yaml,md}`), so this is the + * (`.md`, Codex's `.toml`), so this is the * precise derived-artifact filter. * * Scoping matters: several SHIPPED library files under `msd-core/bin/lib/` diff --git a/tests/capability-registry.test.cjs b/tests/capability-registry.test.cjs index 1e782efca..c7fcb14fd 100644 --- a/tests/capability-registry.test.cjs +++ b/tests/capability-registry.test.cjs @@ -3899,7 +3899,7 @@ describe('ADR-1016 phase 5a: validateConfigHome unit tests', () => { }); test('valid generic-agents-root with probe → no errors', () => { - const errors = validateConfigHome('test', { kind: 'generic-agents-root', name: 'agents', env: ['KIMI_CONFIG_DIR'], probe: ['~/.config/agents'] }); + const errors = validateConfigHome('test', { kind: 'generic-agents-root', name: 'agents', env: ['EXAMPLE_CONFIG_DIR'], probe: ['~/.config/agents'] }); assert.deepEqual(errors, []); }); @@ -4183,7 +4183,7 @@ describe('FIX 3: tightened runtime validator — ArtifactKind field type checks' describe('FIX 3: tightened runtime validator — probeExists optional string', () => { test('probeExists: "skills" is accepted', () => { const errors = validateConfigHome('test', { - kind: 'generic-agents-root', name: 'agents', env: ['KIMI_CONFIG_DIR'], + kind: 'generic-agents-root', name: 'agents', env: ['EXAMPLE_CONFIG_DIR'], probe: ['~/.config/agents', '~/.agents'], probeExists: 'skills', }); @@ -4214,7 +4214,7 @@ describe('FIX 3: tightened runtime validator — probeExists optional string', ( test('probeExists absent → no error (optional)', () => { const errors = validateConfigHome('test', { - kind: 'generic-agents-root', name: 'agents', env: ['KIMI_CONFIG_DIR'], + kind: 'generic-agents-root', name: 'agents', env: ['EXAMPLE_CONFIG_DIR'], probe: ['~/.config/agents'], }); const probeExistsErrors = errors.filter((e) => e.includes('probeExists')); diff --git a/tests/cli-exit.test.cjs b/tests/cli-exit.test.cjs index 725903150..6fc17b681 100644 --- a/tests/cli-exit.test.cjs +++ b/tests/cli-exit.test.cjs @@ -1252,7 +1252,7 @@ describe('#3906: terminateNow', () => { assert.deepEqual(JSON.parse(r.stdout), { reason: 'blocked-by-guard', detail: 'x' }); }); - test('HOOK_DENY writes the SAME payload to both stdout and stderr (Kimi reads exit-2 output from stderr)', () => { + test('HOOK_DENY writes the SAME payload to both stdout and stderr (some hosts read exit-2 output from stderr)', () => { const r = spawnTerminateNow([ `const c = require(${JSON.stringify(BUILT_CLI_EXIT_PATH)});`, `c.terminateNow('HOOK_DENY', { reason: 'blocked' });`, diff --git a/tests/codex-declarative-reference.test.cjs b/tests/codex-declarative-reference.test.cjs index d8417d951..f8b4177eb 100644 --- a/tests/codex-declarative-reference.test.cjs +++ b/tests/codex-declarative-reference.test.cjs @@ -168,7 +168,7 @@ test('upgrade 3 — the pre-move location is migrated (stale ~/.codex/skills/msd const oldDir = install._resolveMovedSkillsOldDir('codex', codexHome, 'global'); assert.equal(oldDir, path.join(codexHome, 'skills'), 'the pre-move location is $CODEX_HOME/skills'); // A runtime with no home override yields null (no false migration). - assert.equal(install._resolveMovedSkillsOldDir('kimi', path.join(os.homedir(), '.kimi'), 'global'), null); + assert.equal(install._resolveMovedSkillsOldDir('claude', path.join(os.homedir(), '.claude'), 'global'), null); const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-migrate-')); const skillsDir = path.join(tmp, 'skills'); diff --git a/tests/declarative-reference-antigravity.test.cjs b/tests/declarative-reference-antigravity.test.cjs index 704082ed3..249755b5d 100644 --- a/tests/declarative-reference-antigravity.test.cjs +++ b/tests/declarative-reference-antigravity.test.cjs @@ -93,7 +93,7 @@ test('a real Antigravity install emits a msd command/skill surface (invocable)', // --------------------------------------------------------------------------- // #2096 EoS/antigravity — AC3/AC5: fail-closed negotiation + validator -// acceptance + the folded descriptor (mirrors kimi/codex reference tests). +// acceptance + the folded descriptor (mirrors the codex reference tests). // --------------------------------------------------------------------------- // -- AC5: negotiation fails CLOSED on a corrupted descriptor ------------------ diff --git a/tests/effort-surface-axis.test.cjs b/tests/effort-surface-axis.test.cjs index a44d1d5ff..0144c5932 100644 --- a/tests/effort-surface-axis.test.cjs +++ b/tests/effort-surface-axis.test.cjs @@ -747,8 +747,8 @@ describe('#2615: the matrix documents the effortSurface axis', () => { const declared = registry.runtimes[id].runtime.hostIntegration.effortSurface; if (declared === undefined) { - // kimi-code declares no value: its mechanism (`/effort`) is interactive-only - // and neither `argv` nor `none` describes it. The matrix must say so rather + // A runtime may declare no value when its mechanism (e.g. an interactive-only + // `/effort`) is neither `argv` nor `none`. The matrix must say so rather // than invent a value. assert.match(documented, /not declared/i, `${id}: an absent descriptor value must be documented as absent, not guessed (#2615)`); diff --git a/tests/emitted-attribution.test.cjs b/tests/emitted-attribution.test.cjs index ba432009a..253ab17e5 100644 --- a/tests/emitted-attribution.test.cjs +++ b/tests/emitted-attribution.test.cjs @@ -202,8 +202,7 @@ test('a trailing-slash source entry matches by prefix, segment-aware', () => { assert.equal(sourceSatisfiedBy('a/b.md', new Set(['a/b.md'])), 'a/b.md'); assert.equal(sourceSatisfiedBy('a/b.md', new Set(['a/b.md.bak'])), null); - // No shipped provenance rule declares a prefix source any more (the Kimi root agent - // that did was removed with its runtime), so drive the REAL diffEmitted against a + // No shipped provenance rule declares a prefix source any more, so drive the REAL diffEmitted against a // synthetic rule by swapping attributeEmittedPath in a freshly loaded emitted-diff. const provPath = require.resolve('./helpers/emitted-provenance.cjs'); const diffPath = require.resolve('./helpers/emitted-diff.cjs'); @@ -1622,11 +1621,11 @@ test('formatReport truncation is exact at limit-1 / limit / limit+1', () => { // ─── Independence + purity ─────────────────────────────────────────────────── test('the differential covers every runtime present in either manifest', () => { - const baseline = { claude: { [WORKFLOW_KEY]: 'a' }, kimi: { [WORKFLOW_KEY]: 'a' } }; + const baseline = { claude: { [WORKFLOW_KEY]: 'a' }, cursor: { [WORKFLOW_KEY]: 'a' } }; const current = { claude: { [WORKFLOW_KEY]: 'b' }, opencode: { [WORKFLOW_KEY]: 'c' } }; const r = diffEmitted({ baseline, current, changedPaths: [] }); const seen = new Set([...r.unattributable, ...r.attributed, ...r.removed].map((x) => x.runtime)); - assert.deepEqual([...seen].sort(), ['claude', 'kimi', 'opencode'], + assert.deepEqual([...seen].sort(), ['claude', 'cursor', 'opencode'], 'a runtime present on only one side must still be evaluated'); }); diff --git a/tests/emitted-provenance.test.cjs b/tests/emitted-provenance.test.cjs index 34bb83020..68bd76a94 100644 --- a/tests/emitted-provenance.test.cjs +++ b/tests/emitted-provenance.test.cjs @@ -17,9 +17,9 @@ * The residual this does NOT close is false attribution — a rule can point at the * WRONG source and still be total. The spot-checks below pin the pairs where that * is most likely, and ADR-2719 designates the Phase 3 (#2723) dual-run as the - * mitigation for the rest. Three real instances of that class were caught while - * building this table (Copilot's `.agent.md` rename, Kimi's `agents/msd.md` - * root agent, and Copilot's `hooks/msd-session.json`), all of which passed totality + * mitigation for the rest. Two real instances of that class were caught while + * building this table (Copilot's `.agent.md` rename and Copilot's + * `hooks/msd-session.json`), both of which passed totality * while resolving to repo files that do not exist — which is why the * "every attributed source exists" test below is a first-class gate, not a nicety. * diff --git a/tests/golden-parity-single-source.test.cjs b/tests/golden-parity-single-source.test.cjs index 486eebeda..65a4f722b 100644 --- a/tests/golden-parity-single-source.test.cjs +++ b/tests/golden-parity-single-source.test.cjs @@ -81,7 +81,7 @@ test('install-shared.cjs exports the canonical buildParityManifest + exclusion c // ALL FIVE identifiers are guarded, not just buildParityManifest + VOLATILE_FILES: // the drift that shipped broken fixtures was a MISSING exclusion-constant entry // (#2100 = generator's HOOK_CONFIG_FILES copy lacked settings.local.json; #2095 = -// kimi's HOOK_CONFIG_RELATIVE_PATHS entry), so a re-declared HOOK_CONFIG_FILES / +// a runtime-specific HOOK_CONFIG_RELATIVE_PATHS entry), so a re-declared HOOK_CONFIG_FILES / // HOOK_CONFIG_RELATIVE_PATHS / EXCLUDED_PREFIXES is exactly the failure class this // guard exists to prevent — checking only two of four would leave that gap open. const FORBIDDEN_INLINE = [ @@ -277,8 +277,8 @@ describe('#1575 — surface path: no prune data-loss over pre-existing legacy ag test('runMinimalInstall resolves local config dirs from RUNTIME_META alone (#3031)', () => { // install-shared.cjs used to carry a SECOND, hand-maintained local-dir map - // beside RUNTIME_META. It drifted: four runtimes present in RUNTIME_META - // (hermes, kimi, kimi-code, zcode) were missing from it, so `scope: 'local'` + // beside RUNTIME_META. It drifted: several runtimes present in RUNTIME_META + // (e.g. hermes, zcode) were missing from it, so `scope: 'local'` // for any of them resolved `path.join(root, undefined)` and threw a bare // TypeError naming neither the runtime nor the map at fault. #3023 had // already hit this for `pi` and fixed it by adding one more entry, which diff --git a/tests/helpers-process-isolation.test.cjs b/tests/helpers-process-isolation.test.cjs index 7f38930d5..afe8ac77a 100644 --- a/tests/helpers-process-isolation.test.cjs +++ b/tests/helpers-process-isolation.test.cjs @@ -106,9 +106,9 @@ describe('withIsolatedProcessState', () => { // all of them, and they stay green. // // That is not hypothetical — it is how round 2 found MSD_HOME and -// KIMI_SHARE_DIR while this block was fully green. MSD_HOME belonged to no -// enumeration at all (it is MSD's own store root, not a runtime configHome); -// KIMI_SHARE_DIR sat inside a function body where nothing could enumerate it. +// a runtime-specific config-location var while this block was fully green. +// MSD_HOME belonged to no enumeration at all (it is MSD's own store root, not a +// runtime configHome); the other var sat inside a function body where nothing could enumerate it. // Round 3's fix was to make both enumerable rather than to add two assertions, // precisely because an assertion added per reviewer-named var is the // hand-maintained list wearing a test's clothes. @@ -250,7 +250,7 @@ describe('#2665: TEST_ENV_BASE config-location coverage', () => { // Named literally rather than derived from the family constant on purpose: a // test that reads WRITE_ESCAPE_PERMISSION_ENV_KEYS and asserts over it shrinks // its own expectation when the family is emptied — the enumeration-relative - // failure this suite already documents, and the one that let the kimi-code + // failure this suite already documents, and the one that let a // descriptor go unwatched. Naming it is what makes removal fail loudly. assert.strictEqual( TEST_ENV_BASE.MSD_ALLOW_SYMLINKED_DEST, diff --git a/tests/helpers.cjs b/tests/helpers.cjs index 88a3a0598..4c4b0cbb3 100644 --- a/tests/helpers.cjs +++ b/tests/helpers.cjs @@ -70,7 +70,7 @@ function builtLib() { // MSD_WORKSTREAM — planningDir() workstream segment (src/planning-workspace.cts) // // #2665 round 3: this list shrinks as sources become enumerable, and that direction -// is the point. KIMI_SHARE_DIR was NOT added here — it now derives from +// is the point. Descriptor-resolved vars were NOT added here — they now derive from // NON_REGISTRY_CONFIG_HOME_DESCRIPTORS, because hand-adding each var a reviewer // names is precisely what reopened this bug three times. const NON_REGISTRY_CONFIG_LOCATION_ENV_KEYS = [ @@ -133,8 +133,8 @@ function configLocationEnvKeys() { ...Object.values(runtimes).flatMap( (r) => r?.runtime?.configHome?.skillsHome?.env ?? [], ), - // 2. Descriptor-shaped config homes resolved OUTSIDE the registry (kimi's - // native config.toml home via KIMI_SHARE_DIR). Derived, not hand-listed. + // 2. Descriptor-shaped config homes resolved OUTSIDE the registry (a host's + // native config.toml home). Derived, not hand-listed. // Same skillsHome walk as rung 1 — a descriptor is a descriptor. ...NON_REGISTRY_CONFIG_HOME_DESCRIPTORS.flatMap((d) => [ ...(d?.env ?? []), diff --git a/tests/helpers/emitted-diff.cjs b/tests/helpers/emitted-diff.cjs index 777692656..0aad136cc 100644 --- a/tests/helpers/emitted-diff.cjs +++ b/tests/helpers/emitted-diff.cjs @@ -178,8 +178,8 @@ const REMEDIATION = Object.freeze({ /** * Does a changed repo path satisfy a provenance `sources` entry? * - * A trailing `/` marks a PREFIX (Phase 2's SOURCE_PREFIX_SUFFIX contract) — e.g. Kimi's - * root agent aggregates all of `agents/`. Prefix matching is SEGMENT-AWARE on purpose: + * A trailing `/` marks a PREFIX (Phase 2's SOURCE_PREFIX_SUFFIX contract) — e.g. an + * aggregate artifact built from all of `agents/`. Prefix matching is SEGMENT-AWARE on purpose: * a bare `startsWith('agents/')` would also accept `agentsfoo/x.md` under a source of * `agents`, and silently over-attribute. Exact entries compare exactly. */ diff --git a/tests/helpers/emitted-provenance.cjs b/tests/helpers/emitted-provenance.cjs index d60e1e570..96f7e1371 100644 --- a/tests/helpers/emitted-provenance.cjs +++ b/tests/helpers/emitted-provenance.cjs @@ -200,7 +200,7 @@ const RUNTIME_NOTE_FILTERED_FAMILIES = new Set( /** * A `sources` entry ending in `/` is a PREFIX, not a file: it means "any repo path * under this directory legitimately explains this emitted path". Used where an - * emitted artifact aggregates a whole directory (Kimi's root agent enumerates every + * emitted artifact aggregates a whole directory (a root agent enumerating every * staged agent). Phase 3 must honor the trailing slash when testing a changed-path * set against these sources; a plain string is an exact path. */ @@ -338,8 +338,8 @@ const PROVENANCE_RULES = [ // per-runtime `kind`) and why this wholesale reclassification was chosen instead. kind: 'derived', roots: ['agents'], - // Excludes `msd.md`: that is Kimi's ROOT agent, built from a code literal and - // NOT a repo agent file. Without the exclusion it matched here and resolved to + // Excludes `msd.md`: that is a ROOT agent built from a code literal, NOT a + // repo agent file. Without the exclusion it matched here and resolved to // `agents/msd.md`, which does not exist — a false attribution that still passed // totality, i.e. the exact residual ADR-2719 records. Every repo agent is // `msd-.md`, so excluding the bare `msd.md` is precise. @@ -441,8 +441,7 @@ const PROVENANCE_RULES = [ // Sources are scoped PER ROOT, not declared as one flat union. Each root has // exactly one writer besides the shared marker module, and a flat list would // attribute every root to all of them — `extensions/package.json` to the - // hooks-surface writer that never touches it, `.kimi/hooks/package.json` to - // the native-plugin writer, and so on. That matters because + // hooks-surface writer that never touches it, and so on. That matters because // `emitted-diff.cjs` accepts the FIRST satisfied source: a flat list // containing `bin/install.js` lets any change anywhere in that 13k-line file // authorise marker drift for every root — the blanket escape hatch this @@ -451,7 +450,6 @@ const PROVENANCE_RULES = [ // hooks/ (shared bundle) -> bin/install.js (installSharedHooksBundle) // hooks/ (#2717 runtimes) -> src/runtime-hooks-surface.cts + bin/install.js // (the codex copy block calls the exported helper) - // .kimi/hooks/ -> bin/install.js (the kimi hooks-root bundle) // plugins/, extensions/ -> src/install-engine.cts // (_installNativePluginIfDeclared) // @@ -467,7 +465,7 @@ const PROVENANCE_RULES = [ if (root === 'plugins' || root === 'extensions') { return [COMMONJS_MARKER_SRC, INSTALL_ENGINE_SRC]; } - // Both Kimi products install the shared bundle into their own native hook + // Some products install the shared bundle into their own native hook // root rather than under the generic Agent-Skills configDir (#2755). if (root === '.kimi/hooks' || root === '.kimi-code/hooks') { return [COMMONJS_MARKER_SRC, INSTALLER_SRC]; @@ -561,7 +559,7 @@ const PROVENANCE_RULES = [ // real global subdirectory those files sit outside the walked configDir, // the rules matched nothing, and the totality guard's dead-rule arm fired // — exactly as designed. The files are still written, still covered by the - // existence/config suites (kimi-upgrades, codex-config, install suites); + // existence/config suites (codex-config, install suites); // they are simply no longer manifest members to attribute. // ── Synthesized: install-time / environment state, no repo content source ── @@ -569,10 +567,6 @@ const PROVENANCE_RULES = [ id: 'synthesized-install-metadata', kind: 'synthesized', roots: null, - // `.kimi/package.json` and `.kimi-code/package.json` are the same literal - // `{"type":"commonjs"}` CommonJS-mode marker as the root one, written into - // each Kimi product's separate hooks root (installSharedHooksBundle; the - // per-runtime root split is #2755). pattern: /^(\.msd-profile|package\.json|msd-core\/VERSION|msd-core\/\.msd-runtime)$/, sources: () => [], }, diff --git a/tests/helpers/install-shared.cjs b/tests/helpers/install-shared.cjs index 28b6469af..5b7a6c0e8 100644 --- a/tests/helpers/install-shared.cjs +++ b/tests/helpers/install-shared.cjs @@ -169,30 +169,16 @@ const VOLATILE_FILES = new Set([ // node-runner command as settings.json, so excluded for the same reason (#2086). const HOOK_CONFIG_FILES = new Set(['settings.json', 'settings.local.json', 'hooks.json']); -// Kimi's native config.toml (#2095 EoS/kimi Upgrade 1) embeds the same -// platform-varying node-runner command as the HOOK_CONFIG_FILES above (via the -// same buildHookCommand/projectManagedHookCommand machinery), so it needs the -// same exclusion — but it is NOT matched by basename like HOOK_CONFIG_FILES: -// Codex's OWN config.toml (installSurface 'codex-toml') is a stable, tracked -// top-level `config.toml` entry in its golden fixture (it only ever gets a -// platform-stable `[features] hooks = true` flag — the real hook commands -// live in Codex's separate hooks.json, already excluded above). Blanket- -// excluding the 'config.toml' basename would silently blind Codex's fixture -// to any future regression there — and it would blind kimi-code's too: since -// #3547 the harness installs into each runtime's REAL global subdirectory, so -// kimi-code's hooks config.toml sits at its configDir root (rel `config.toml`) -// and is legitimately manifest-visible. Tracking it is safe now: the -// install-tree fixture carries paths only, and the ADR-2719 differential -// compares base-vs-current on the same machine, so the platform-varying -// node-runner command embedded in the TOML never crosses platforms inside a -// gate (that was a golden-content-era hazard, and the goldens are gone). -// Kimi CLI's config.toml (KIMI_SHARE_DIR root ~/.kimi) lives OUTSIDE its MSD -// configDir (~/.config/agents) and never enters the walk. The pre-#3547 -// relative-path exclusions ('.kimi/config.toml', '.kimi-code/config.toml') -// existed only for the collapsed shape — where the walked root was the HOME -// itself and those HOME-level siblings were inside it; with no walker rooting -// at HOME anymore they matched nothing and were removed (#3547). kimi-code -// resolves its own root since #2755. +// Native config.toml files are deliberately NOT excluded by basename like the +// HOOK_CONFIG_FILES above: Codex's OWN config.toml (installSurface 'codex-toml') is +// a stable, tracked top-level `config.toml` entry in its golden fixture (it only +// ever gets a platform-stable `[features] hooks = true` flag — the real hook +// commands live in Codex's separate hooks.json, already excluded above). +// Blanket-excluding the 'config.toml' basename would silently blind Codex's +// fixture to any future regression there. Relative-path exclusions existed only +// for the pre-#3547 collapsed shape, where the walked root was the HOME itself +// and HOME-level siblings were inside it; with no walker rooting at HOME anymore +// they matched nothing and were removed (#3547). const HOOK_CONFIG_RELATIVE_PATHS = new Set(); // Path prefixes excluded from the parity manifest. `msd-core/bin/lib/` holds the @@ -637,7 +623,7 @@ function runMinimalInstall({ runtime, scope, extraArgs = [], installScript = INS cwd = root; // #3031: local scope reads RUNTIME_META.localDir — the SAME table the // global branch above reads — instead of a second hand-maintained map. - // That duplicate map was missing four runtimes (hermes, kimi, kimi-code, + // That duplicate map was missing several runtimes (e.g. hermes, // zcode), so `scope: 'local'` for any of them resolved // `path.join(root, undefined)` and threw a bare TypeError naming neither // the runtime nor the map at fault. #3023 fixed exactly this for `pi` by diff --git a/tests/hooks-crash-policy.test.cjs b/tests/hooks-crash-policy.test.cjs index 0e1ab045f..98ece773e 100644 --- a/tests/hooks-crash-policy.test.cjs +++ b/tests/hooks-crash-policy.test.cjs @@ -30,8 +30,7 @@ * * Crash trigger: malformed JSON on stdin ('{not json'). Every one of the 9 * hooks that declares an ON_CRASH policy parses its stdin payload as the - * FIRST statement inside its outer try — `JSON.parse(input)` (or the Kimi- - * normalized `normalizeKimiPayload(JSON.parse(input))`) — so a syntax error + * FIRST statement inside its outer try — `JSON.parse(input)` — so a syntax error * there throws before any applicability logic runs and is a real, hook- * authored crash, not a synthetic fault injected by this suite. */ diff --git a/tests/host-integration.test.cjs b/tests/host-integration.test.cjs index e1d74c4a5..ab9ce201b 100644 --- a/tests/host-integration.test.cjs +++ b/tests/host-integration.test.cjs @@ -1587,24 +1587,21 @@ describe('resolveOrchestratorExec — the 4 shipped orchestrator-worktree descri assert.equal(result.cwd, CWD); }); - // #2627: `args` carries --print because kimi's working mode is otherwise the + // #2627: `args` carries --print because some hosts' working mode is otherwise the // interactive TUI — an orchestrator spawning a TUI hangs forever rather than // returning a completed plan. - test('kimi: --print --work-dir (headless flag leads)', () => { - const result = resolveOrchestratorExec({ command: 'kimi', args: ['--print'], cwdFlag: '--work-dir' }, CWD); + test('--print --work-dir (headless flag leads)', () => { + const result = resolveOrchestratorExec({ command: 'hostcli', args: ['--print'], cwdFlag: '--work-dir' }, CWD); assert.equal(result.ok, true); - assert.equal(result.command, 'kimi'); + assert.equal(result.command, 'hostcli'); assert.deepEqual(result.args, ['--print', '--work-dir', CWD]); assert.equal(result.cwd, CWD); }); - // #2627: the binary is `kimi`, NOT `kimi-code` — Moonshot's TypeScript Kimi - // Code installs its binary as `kimi`; `kimi-code` is only the npm package and - // config-home name, so spawning it is an immediate ENOENT. - test('kimi-code: command is "kimi"; cwdFlag:null appends NO flag, cwd still returned (process-cwd case)', () => { - const result = resolveOrchestratorExec({ command: 'kimi', args: [], cwdFlag: null }, CWD); + test('cwdFlag:null appends NO flag, cwd still returned (process-cwd case)', () => { + const result = resolveOrchestratorExec({ command: 'hostcli', args: [], cwdFlag: null }, CWD); assert.equal(result.ok, true); - assert.equal(result.command, 'kimi'); + assert.equal(result.command, 'hostcli'); assert.deepEqual(result.args, []); assert.equal(result.cwd, CWD); }); @@ -1630,17 +1627,17 @@ describe('resolveOrchestratorExec — prompt passing (#2627, Phase 3)', () => { assert.deepEqual(result.args, ['run', '--dir', CWD, PROMPT]); }); - test('promptFlag string → [flag, prompt] appended (kimi shape)', () => { + test('promptFlag string → [flag, prompt] appended (flag-carried prompt shape)', () => { const result = resolveOrchestratorExec( - { command: 'kimi', args: ['--print'], cwdFlag: '--work-dir', promptFlag: '--prompt' }, CWD, PROMPT, + { command: 'hostcli', args: ['--print'], cwdFlag: '--work-dir', promptFlag: '--prompt' }, CWD, PROMPT, ); assert.equal(result.ok, true); assert.deepEqual(result.args, ['--print', '--work-dir', CWD, '--prompt', PROMPT]); }); - test('promptFlag string + cwdFlag null → prompt flag only, cwd via process cwd (kimi-code shape)', () => { + test('promptFlag string + cwdFlag null → prompt flag only, cwd via process cwd (process-cwd shape)', () => { const result = resolveOrchestratorExec( - { command: 'kimi', args: [], cwdFlag: null, promptFlag: '--prompt' }, CWD, PROMPT, + { command: 'hostcli', args: [], cwdFlag: null, promptFlag: '--prompt' }, CWD, PROMPT, ); assert.equal(result.ok, true); assert.deepEqual(result.args, ['--prompt', PROMPT]); @@ -2045,7 +2042,7 @@ describe('#2584 orchestratorExec — validator', () => { test('cwdFlag: null is valid (no error)', () => { const cap = shippedCodexCapabilityWithoutOrchestratorExec(); - cap.runtime.orchestratorExec = { command: 'kimi-code', args: [], cwdFlag: null }; + cap.runtime.orchestratorExec = { command: 'hostcli', args: [], cwdFlag: null }; const errors = validateCapability(cap, 'codex'); const oeErrors = errors.filter((e) => e.includes('orchestratorExec')); assert.deepEqual(oeErrors, []); @@ -2227,10 +2224,10 @@ describe('#3714 resolveOrchestratorExec — modelFlag/model seam (mechanical, RE }); // MATRIX row 7 [CONTROL]: a host with no modelFlag is byte-identical to - // today whether or not a model is passed — kimi-code/opencode never get a + // today whether or not a model is passed — opencode never gets a // 5th positional token nor a flag pair injected. test('row 7 CONTROL: a host descriptor with NO modelFlag key resolves identically whether or not a model is passed', () => { - const descriptor = { command: 'kimi', args: ['--print'], cwdFlag: '--work-dir', promptFlag: '--prompt' }; + const descriptor = { command: 'hostcli', args: ['--print'], cwdFlag: '--work-dir', promptFlag: '--prompt' }; const withoutModelArg = resolveOrchestratorExec(descriptor, CWD, PROMPT); const withModelArg = resolveOrchestratorExec(descriptor, CWD, PROMPT, 'some-model'); assert.deepEqual(withModelArg, withoutModelArg, diff --git a/tests/host-runtime-detection.test.cjs b/tests/host-runtime-detection.test.cjs index 2268b3fde..065d67c59 100644 --- a/tests/host-runtime-detection.test.cjs +++ b/tests/host-runtime-detection.test.cjs @@ -249,9 +249,9 @@ describe('resolveReportedRuntime: precedence', () => { test('invalid MSD_RUNTIME falls through to config', (t) => { const tmpDir = withProject(t); - writeConfig(tmpDir, JSON.stringify({ runtime: 'kimi' })); + writeConfig(tmpDir, JSON.stringify({ runtime: 'cursor' })); const result = resolveReportedRuntime(tmpDir, { env: { MSD_RUNTIME: ' ' } }); - assert.strictEqual(result, 'kimi'); + assert.strictEqual(result, 'cursor'); }); test('explicit config runtime outranks detection (#2517 preserved)', (t) => { @@ -307,10 +307,10 @@ describe('resolveReportedRuntime: precedence', () => { test('crlf config json is unaffected', (t) => { const tmpDir = withProject(t); - const crlfConfig = ['{', ' "runtime": "kimi"', '}'].join('\r\n'); + const crlfConfig = ['{', ' "runtime": "cursor"', '}'].join('\r\n'); writeConfig(tmpDir, crlfConfig); const result = resolveReportedRuntime(tmpDir, { env: { CODEX_SANDBOX: 'seatbelt' } }); - assert.strictEqual(result, 'kimi'); + assert.strictEqual(result, 'cursor'); }); // Deliberately NOT a "windows-shaped CODEX_HOME" test computed with @@ -447,8 +447,8 @@ describe('runtime-slash: frozen contract (#3245 extraction must not move it)', ( writeConfig(tmpDir, JSON.stringify({ runtime: 'opencode' })); assert.strictEqual(resolveRuntime(tmpDir), 'opencode'); - process.env.MSD_RUNTIME = 'kimi'; - assert.strictEqual(resolveRuntime(tmpDir), 'kimi'); + process.env.MSD_RUNTIME = 'cursor'; + assert.strictEqual(resolveRuntime(tmpDir), 'cursor'); }); test('resolveRuntime ignores a codex session signal', (t) => { @@ -644,11 +644,11 @@ describe('resolveReportedRuntime: install-marker rung (#4717)', () => { test('explicit config runtime still outranks the marker (#4717 ladder rung 1)', (t) => { const tmpDir = withProject4717(t); - writeConfig4717(tmpDir, 'kimi'); + writeConfig4717(tmpDir, 'cursor'); slash._setInstallRuntimeMarkerForTests('codex'); t.after(() => slash._resetInstallRuntimeMarkerCacheForTests()); const result = resolveReportedRuntime(tmpDir, { env: {} }); - assert.strictEqual(result, 'kimi'); + assert.strictEqual(result, 'cursor'); }); test('no marker: host detection unchanged (dev/source trees, pre-#2297 installs)', (t) => { diff --git a/tests/lint-retired-runtime-name.test.cjs b/tests/lint-retired-runtime-name.test.cjs index 187b10491..17c66046c 100644 --- a/tests/lint-retired-runtime-name.test.cjs +++ b/tests/lint-retired-runtime-name.test.cjs @@ -647,13 +647,13 @@ describe('lint-retired-runtime-name — a version number never launders a claim' ['a version ending the sentence', 'docs/guides/b1.md', `The installer now offers ${RETIRED_NAME} 3.`], ['a version inside a runtime list', 'docs/guides/b2.md', - `MSD installs cleanly on ${RETIRED_NAME} 3, Kimi, and Codex.`], + `MSD installs cleanly on ${RETIRED_NAME} 3, Cline, and Codex.`], ['a version in parentheses', 'docs/guides/b3.md', `Pick your coding tool (${RETIRED_NAME} 3) during setup.`], ['a "plugins" list — "plugin" is not a runtime word', 'docs/guides/c3.md', `Supported plugins: Claude Code, Codex, ${RETIRED_NAME} 3.`], ['an "agents" list — "agent" is deliberately not a runtime word', 'docs/guides/c4.md', - `Supported agents include ${RETIRED_NAME} 3, Kimi, and Cursor.`], + `Supported agents include ${RETIRED_NAME} 3, Cline, and Cursor.`], ]; for (const [label, relPath, body] of CASES) { diff --git a/tests/live-config-guard.test.cjs b/tests/live-config-guard.test.cjs index 730900eb0..206f77306 100644 --- a/tests/live-config-guard.test.cjs +++ b/tests/live-config-guard.test.cjs @@ -496,7 +496,7 @@ describe('#2665: guard watches non-root write surfaces', () => { // Every descriptor in the array must contribute a target. Calling one // named resolver instead would cover one of today's two entries and silently // miss tomorrow's — the same partial-enumeration defect that put - // KIMI_SHARE_DIR outside the scrub set, one layer over. + // a config-location var outside the scrub set, one layer over. // // SCOPE BOUNDARY (per round-2 Nit 7, and it bites here): this asserts one // target PER DESCRIPTOR and nothing about whether one target per descriptor diff --git a/tests/msd-agent-isolation-guard.test.cjs b/tests/msd-agent-isolation-guard.test.cjs index 27e81ed88..2672ff35e 100644 --- a/tests/msd-agent-isolation-guard.test.cjs +++ b/tests/msd-agent-isolation-guard.test.cjs @@ -183,7 +183,7 @@ describe('msd-agent-isolation-guard.js: applicability matrix (#3045)', () => { assert.equal(out.decision, 'block'); assert.match(out.reason, /harness-worktree/); assert.match(out.reason, /isolation="worktree"/); - assert.equal(r.stderr, out.reason, 'stderr must carry the same reason (Kimi reads stderr on exit 2)'); + assert.equal(r.stderr, out.reason, 'stderr must carry the same reason (some hosts read stderr on exit 2)'); }); test('row 2: isolation="worktree" present -> allow', () => { diff --git a/tests/plan-review-convergence.test.cjs b/tests/plan-review-convergence.test.cjs index 7b4a8dba4..6b6fca582 100644 --- a/tests/plan-review-convergence.test.cjs +++ b/tests/plan-review-convergence.test.cjs @@ -211,7 +211,7 @@ describe('plan-review-convergence: --agy/--antigravity reviewer whitelist (#2293 // #2800: the runtime contract used to be a hand-written grep-accumulation // block — one `grep -q '\-\-'` line per recognized flag. Absence meant // the flag was silently dropped, and that whitelist drifted three separate - // times (--coderabbit, then --qwen/--cursor/--kimi-code, then the unanchored + // times (--coderabbit, then --qwen/--cursor, then the unanchored // --agy pattern matching inside --antigravity). The fix derives the whitelist // structurally from the declared lane roster via `msd_run review-lane flags`, // so --agy/--antigravity (and every other lane flag) are guaranteed reachable diff --git a/tests/portability-vocab-drift.test.cjs b/tests/portability-vocab-drift.test.cjs index 643b9e035..6be8e6846 100644 --- a/tests/portability-vocab-drift.test.cjs +++ b/tests/portability-vocab-drift.test.cjs @@ -49,7 +49,7 @@ const INSTALL_JS_PATH_HELPERS = [ // #2088 (ADR-1239 upgrade 3): resolves the skills-install dir honoring a // skills-kind `home` override (e.g. Codex → $HOME/.agents/skills). '_resolveSkillsRootDir', - // #3664: shared kind-destination resolver (skills/agents/kimi-agents kinds). + // #3664: shared kind-destination resolver (skills/agents kinds). '_kindDestDir', ]; diff --git a/tests/resolve-dispatch-type.test.cjs b/tests/resolve-dispatch-type.test.cjs index 841416ae7..66aebef3f 100644 --- a/tests/resolve-dispatch-type.test.cjs +++ b/tests/resolve-dispatch-type.test.cjs @@ -17,7 +17,7 @@ describe('resolveDispatchType (pure function, #2508 Phase 4 Option A)', () => { }); }); - describe('built-in-only runtimes (namedDispatch: false, e.g. kimi-code)', () => { + describe('built-in-only runtimes (namedDispatch: false, e.g. hosts with only built-in agents)', () => { test('maps -planner / -roadmapper / -selector / -spec suffixes to plan', () => { assert.equal(resolveDispatchType('msd-planner', { namedDispatch: false }), 'plan'); assert.equal(resolveDispatchType('msd-roadmapper', { namedDispatch: false }), 'plan'); diff --git a/tests/review-lane-descriptor.test.cjs b/tests/review-lane-descriptor.test.cjs index bb80ec29c..5968c33f6 100644 --- a/tests/review-lane-descriptor.test.cjs +++ b/tests/review-lane-descriptor.test.cjs @@ -107,9 +107,9 @@ describe('reviewer lane parity — the shipped repo', () => { describe('reviewer lane parity — descriptor vs roster', () => { test('a roster slug with no descriptor entry is a violation', () => { - const r = check({ roster: [...KNOWN_REVIEWER_SLUGS, 'kimi_code'] }); + const r = check({ roster: [...KNOWN_REVIEWER_SLUGS, 'unknown_lane'] }); assert.deepStrictEqual(reasons(r), [ - `${PARITY_VIOLATION.ROSTER_SLUG_UNDECLARED}:kimi_code`, + `${PARITY_VIOLATION.ROSTER_SLUG_UNDECLARED}:unknown_lane`, ]); }); diff --git a/tests/review-lane-runner.test.cjs b/tests/review-lane-runner.test.cjs index 98929b74d..7bdd3a94f 100644 --- a/tests/review-lane-runner.test.cjs +++ b/tests/review-lane-runner.test.cjs @@ -170,7 +170,7 @@ describe('runner — probe (ADR-2782 D7)', () => { }); test('a capability probe that times out reports unavailable, never hangs', async () => { - // The original probe (closed PR #2776) was an unbounded `kimi --help | grep` that ran on EVERY + // The original probe (closed PR #2776) was an unbounded ` --help | grep` that ran on EVERY // review regardless of flags — a live instance of the named Unbounded Subprocesses defect. const p = capabilityPlan(); const r = await probeLane(p, deps({ diff --git a/tests/reviewer-config-federation.test.cjs b/tests/reviewer-config-federation.test.cjs index 186c2f01a..4edf9495f 100644 --- a/tests/reviewer-config-federation.test.cjs +++ b/tests/reviewer-config-federation.test.cjs @@ -387,7 +387,7 @@ describe('exclusivity gate sees dynamic patterns (#2797)', () => { // // Two independent defects, per .msd/bug/fix-3691-reviewer-prompt-budget/10-diagnosis.md: // (1) every `transport: spawn` lane (claude, coderabbit, antigravity, cursor, gemini, -// codex, kimi-code, opencode, qwen) declares `promptBudgetKey: null`, so +// codex, opencode, qwen) declares `promptBudgetKey: null`, so // `budgetFor` (msd-core/bin/msd-tools.cjs) returns null for them unconditionally; // (2) `review.max_prompt_tokens` is documented and in validKeys but // `config-defaults.manifest.json` has no `review` section, so the resolved diff --git a/tests/reviewer-docs-parity.test.cjs b/tests/reviewer-docs-parity.test.cjs index 182924b49..ba41f7980 100644 --- a/tests/reviewer-docs-parity.test.cjs +++ b/tests/reviewer-docs-parity.test.cjs @@ -413,17 +413,17 @@ describe('reviewer docs parity — HTML comment stripping (CodeQL js/incomplete- test('aNestedCommentCannotSmuggleAFlagPastTheStrip', () => { // `` + `-| `--opencode` | d |-->`: one pass strips the self-contained // `` in the middle, which joins the leftover `` closer. A + // `` closer. A // single-pass strip leaves that whole new span — row included — untouched in the output, so // the row reads as documented. Regressed exactly here: verified against the pre-fix // single-pass implementation, only `TABLE_ROW_MISSING` fired and `DOC_FLAG_MISSING` did not. const joinLine = '-| `--opencode` | d |-->'; const doc = ['### `/msd-review`', '', ...NON_VICTIM_ROWS, '', joinLine, ''].join('\n'); const r = checkReviewerDocsParity({ descriptor: REVIEWER_LANES, docs: { d: doc } }); - const kimi = r.violations.filter((v) => v.subject === '--opencode'); + const flagViolations = r.violations.filter((v) => v.subject === '--opencode'); assert.ok( - kimi.some((v) => v.reason === DOCS_PARITY_VIOLATION.DOC_FLAG_MISSING) - || kimi.some((v) => v.reason === DOCS_PARITY_VIOLATION.TABLE_ROW_MISSING), + flagViolations.some((v) => v.reason === DOCS_PARITY_VIOLATION.DOC_FLAG_MISSING) + || flagViolations.some((v) => v.reason === DOCS_PARITY_VIOLATION.TABLE_ROW_MISSING), '--opencode must still be reported missing once the join-trick comment is fully stripped', ); }); @@ -439,8 +439,8 @@ describe('reviewer docs parity — HTML comment stripping (CodeQL js/incomplete- '', ].join('\n'); const r = checkReviewerDocsParity({ descriptor: REVIEWER_LANES, docs: { d: doc } }); - const kimi = r.violations.filter((v) => v.subject === '--opencode'); - assert.ok(kimi.length > 0, '--opencode sits inside an open comment and must be reported missing'); + const flagViolations = r.violations.filter((v) => v.subject === '--opencode'); + assert.ok(flagViolations.length > 0, '--opencode sits inside an open comment and must be reported missing'); }); test('aCommentClosedOnALaterLineResumesCorrectly', () => { @@ -458,16 +458,16 @@ describe('reviewer docs parity — HTML comment stripping (CodeQL js/incomplete- '', ].join('\n'); const r = checkReviewerDocsParity({ descriptor: REVIEWER_LANES, docs: { d: doc } }); - const kimi = r.violations.filter((v) => v.subject === '--opencode'); - assert.deepStrictEqual(kimi, [], 'content after a real comment close must not be treated as commented out'); + const flagViolations = r.violations.filter((v) => v.subject === '--opencode'); + assert.deepStrictEqual(flagViolations, [], 'content after a real comment close must not be treated as commented out'); }); test('multipleIndependentCommentsOnOneLineAreAllStripped', () => { const line = 'A B '; const doc = ['### `/msd-review`', '', ...NON_VICTIM_ROWS, '', line, ''].join('\n'); const r = checkReviewerDocsParity({ descriptor: REVIEWER_LANES, docs: { d: doc } }); - const kimi = r.violations.filter((v) => v.subject === '--opencode'); - assert.ok(kimi.length > 0, 'both independent comment spans on one line must be stripped'); + const flagViolations = r.violations.filter((v) => v.subject === '--opencode'); + assert.ok(flagViolations.length > 0, 'both independent comment spans on one line must be stripped'); }); }); diff --git a/tests/runtime-artifact-layout-descriptor-drive.test.cjs b/tests/runtime-artifact-layout-descriptor-drive.test.cjs index 507542f63..3af8a9727 100644 --- a/tests/runtime-artifact-layout-descriptor-drive.test.cjs +++ b/tests/runtime-artifact-layout-descriptor-drive.test.cjs @@ -19,7 +19,7 @@ * the old switch's scope-agnostic behaviour. * * For runtimes that had explicit scope branches in the old switch - * (claude: distinct local=commands+agents; cline: local=[]; kimi: local=[]), + * (claude: distinct local=commands+agents; cline: local=[]), * the STEP-0 golden matches the descriptor exactly and is left unchanged. * * Unknown runtime case: diff --git a/tests/runtime-artifact-layout-surface.test.cjs b/tests/runtime-artifact-layout-surface.test.cjs index 75cb143c5..5e97531d8 100644 --- a/tests/runtime-artifact-layout-surface.test.cjs +++ b/tests/runtime-artifact-layout-surface.test.cjs @@ -1381,7 +1381,7 @@ describe('skills-kind destination parity: installer vs surface-apply (#2911)', ( return; // runtime/scope combination not supported } const skillsKind = layout.kinds.find((k) => k.kind === 'skills'); - if (!skillsKind) return; // e.g. cline/kimi at local scope: no skills kind + if (!skillsKind) return; // e.g. cline at local scope: no skills kind if (typeof skillsKind.home === 'string' && skillsKind.home !== '') { discriminatingRuntimes++; diff --git a/tests/runtime-flags.test.cjs b/tests/runtime-flags.test.cjs index b9e70b72a..3ec3249af 100644 --- a/tests/runtime-flags.test.cjs +++ b/tests/runtime-flags.test.cjs @@ -24,11 +24,10 @@ const EXPECTED_FLAGS = [ const NON_INSTALLABLE_RUNTIMES = new Set(['vscode']); test('runtimeFlags: every known non-claude runtime sets exactly its own flag true', () => { - // Convert a flag name (camelCase, e.g. isKimiCode) to the runtime id it - // represents (kimi-code). Strip the 'is' prefix, then split camelCase + // Convert a flag name (camelCase, e.g. isFooBar) to the runtime id it + // represents (foo-bar). Strip the 'is' prefix, then split camelCase // boundaries into hyphen-separated lowercase words. The simpler - // slice(2).toLowerCase() worked while every runtime id was a single word; - // kimi-code is the first hyphenated id. + // slice(2).toLowerCase() only works while every runtime id is a single word. const flagToId = (flag) => flag.slice(2) .replace(/[A-Z]/g, (m, i) => (i > 0 ? '-' : '') + m.toLowerCase()); const idToFlag = (id) => 'is' + id.charAt(0).toUpperCase() + id.slice(1).replace(/-([a-z])/g, (_, c) => c.toUpperCase()); @@ -66,7 +65,7 @@ test('runtimeFlags drift guard: covers every registry runtime except claude and // flag by design and must not trip this guard. // // #2454: the flag-name → runtime-id conversion must fold PascalCase boundaries - // back to kebab-case (isKimiCode → kimi-code, not 'kimicode'). + // back to kebab-case (isFooBar → foo-bar, not 'foobar'). const flagToId = (flag) => flag.slice(2) .replace(/[A-Z]/g, (m, i) => (i > 0 ? '-' : '') + m.toLowerCase()); const registryNonClaude = Object.keys(registry.runtimes) diff --git a/tests/runtime-homes-descriptor-drive.test.cjs b/tests/runtime-homes-descriptor-drive.test.cjs index c41dd9c6d..dfb3e5bed 100644 --- a/tests/runtime-homes-descriptor-drive.test.cjs +++ b/tests/runtime-homes-descriptor-drive.test.cjs @@ -78,8 +78,6 @@ function restoreEnvKeys(saved) { // ── STEP 0: golden scenarios captured from old switch BEFORE edits ──────────── // GOLDEN DEFAULTS (no env vars set, no existsSync probe hits). -// kimi is NOT included here because it depends on real filesystem probing — -// its probe-miss/hit scenarios are covered separately via injected existsSync. // antigravity default also depends on probing; the default assumes NO dirs exist. const GOLDEN_DEFAULTS = { claude: path.join(HOME, '.claude'), @@ -304,11 +302,11 @@ describe('#3023 review finding 1: whitespace-only env override falls back to def { kind: 'generic-agents-root', name: 'agents', - env: ['KIMI_CONFIG_DIR'], + env: ['EXAMPLE_CONFIG_DIR'], probe: ['~/.config/agents', '~/.agents'], probeExists: 'skills', }, - { env: { KIMI_CONFIG_DIR: ' ' }, home: '/home/u', existsSync: () => false }, + { env: { EXAMPLE_CONFIG_DIR: ' ' }, home: '/home/u', existsSync: () => false }, ); assert.strictEqual(result, path.join('/home/u', '.config', 'agents')); }); @@ -676,7 +674,7 @@ describe('descriptor-driven equivalence: explicitDir short-circuit', () => { test('explicitDir absolute path returned as-is (any runtime)', () => { assert.strictEqual(String(getGlobalConfigDir('claude', '/tmp/explicit')).replace(/\\/g, '/'), '/tmp/explicit'); assert.strictEqual(String(getGlobalConfigDir('opencode', '/tmp/explicit')).replace(/\\/g, '/'), '/tmp/explicit'); - assert.strictEqual(String(getGlobalConfigDir('kimi', '/tmp/explicit')).replace(/\\/g, '/'), '/tmp/explicit'); + assert.strictEqual(String(getGlobalConfigDir('cursor', '/tmp/explicit')).replace(/\\/g, '/'), '/tmp/explicit'); assert.strictEqual(String(getGlobalConfigDir('grok', '/tmp/explicit')).replace(/\\/g, '/'), '/tmp/explicit'); }); @@ -769,7 +767,6 @@ describe('descriptor-driven parity: 13 non-probe registry runtimes × no-env-var // This is the hardest assertion: it drives getGlobalConfigDir() (which calls // the registry internally) and compares against GOLDEN_DEFAULTS captured from // the old switch. Any discrepancy means a regression. - // kimi is excluded because its default depends on real filesystem probing. // antigravity is excluded because it also depends on real fs probing — a machine // with ~/.gemini/antigravity-ide or ~/.gemini/antigravity-cli (but not // ~/.gemini/antigravity) gets a different result. Probe scenarios are covered in diff --git a/tests/runtime-label-policy.test.cjs b/tests/runtime-label-policy.test.cjs index f0fa031f3..1ca207a5c 100644 --- a/tests/runtime-label-policy.test.cjs +++ b/tests/runtime-label-policy.test.cjs @@ -21,8 +21,8 @@ * snapshot here. * * Voice: these SHORT UI labels are intentionally distinct from the descriptor - * `title` (the long product name). Two prior-chain inconsistencies are resolved - * by the canonical map: kimi → 'Kimi CLI'; cline → 'Cline'. + * `title` (the long product name). One prior-chain inconsistency is resolved + * by the canonical map: cline → 'Cline'. * * ADR-1239 Phase B (#1679). Behavioral tests only: assert on returned values. */ diff --git a/tests/slug-derivation-drift-guard.test.cjs b/tests/slug-derivation-drift-guard.test.cjs index 888cf31a5..27d8ec1f7 100644 --- a/tests/slug-derivation-drift-guard.test.cjs +++ b/tests/slug-derivation-drift-guard.test.cjs @@ -99,8 +99,7 @@ describe('findSlugDerivationDrift — T2: the canonical owner (src/core-utils.ct }); }); -// ─── T3-T5: the sanctioned sites are exempted BY the allowlist -// (normalizeKimiSkillName left with the retired kimi runtime — prune-runtimes) ────────── +// ─── T3-T5: the sanctioned sites are exempted BY the allowlist ────────── describe('findSlugDerivationDrift — T3-T5: sanctioned sites are exempted BY the allowlist, not by accident', () => { const sanctioned = [ diff --git a/tests/zcode-agent-mcp-grants.install.test.cjs b/tests/zcode-agent-mcp-grants.install.test.cjs index 9c93e2fb2..6b4aaa5e2 100644 --- a/tests/zcode-agent-mcp-grants.install.test.cjs +++ b/tests/zcode-agent-mcp-grants.install.test.cjs @@ -12,8 +12,7 @@ * `tools:` frontmatter as a REQUIRED MCP server and throws CONFIGURATION_ERROR on * spawn when it is not connected. Claude Code treats the same grants as an optional * allowlist, which is why the MSD sources carry them. The install must therefore - * filter `mcp__*` entries out of the tools list for ZCode — the same outcome Kimi - * already gets via its own conversion path — while leaving Claude Code's verbatim + * filter `mcp__*` entries out of the tools list for ZCode while leaving Claude Code's verbatim * copy untouched. * * Every row spawns a REAL installer and asserts on the parsed frontmatter of what