chore: clear dead test and allowlist leftovers of dropped runtimes
Some checks failed
Tests / PR mergeability (push) Successful in 18s
Tests / Base branch health (push) Successful in 9s
Tests / Detect test scope (push) Successful in 16s
Tests / lint-tests (push) Failing after 1m43s
Tests / plugin-validate (push) Successful in 58s
Tests / test (ubuntu-latest, 24, shard 1/3) (push) Failing after 19s
Tests / test (ubuntu-latest, 24, shard 2/3) (push) Failing after 20s
Tests / test (ubuntu-latest, 24, shard 3/3) (push) Failing after 20s
Tests / test (ubuntu-latest, 24) (push) Failing after 18s
Tests / test (inert CI) (push) Has been skipped
Tests / QA loop walk (smell ratchet) (push) Failing after 19s
Tests / Coverage gate (merged shards) (push) Has been skipped
Tests / Publish emitted-baseline artifact (push) Has been skipped
Duplicate auto-close sweep / sweep (push) Successful in 19s
CI timeout budget report / report (push) Failing after 14s
Close Draft PRs (sweep) / Sweep open draft PRs (push) Successful in 9s
Dismiss Unauthorized PR Approvals / dismiss-unauthorized-approval (push) Successful in 9s
Tests / conformance test (macos-latest, 24) (push) Has been cancelled
Tests / conformance test (windows-latest, 24, shard 1/3) (push) Has been cancelled
Tests / conformance test (windows-latest, 24, shard 2/3) (push) Has been cancelled
Tests / conformance test (windows-latest, 24, shard 3/3) (push) Has been cancelled
Tests / Required tests (push) Has been cancelled

This commit is contained in:
Jakub Zych
2026-10-06 20:30:09 +02:00
parent 792139b5ed
commit fe3ed06691
83 changed files with 308 additions and 1997 deletions

View File

@@ -39,32 +39,9 @@ Spawned by `/msd:map-codebase --query`, which has already confirmed `intel.enabl
## Project Scope
<!-- Layout detection: only meaningful when analysing the MSD framework's own repo (#3290). -->
**Runtime layout detection (MSD framework repo only):** if `package.json` `"name"` equals `"@golem15/msd-core"`, this project IS the MSD framework — detect the runtime root to choose canonical paths:
```bash
if [[ "$(jq -r '.name // ""' package.json 2>/dev/null)" == "@golem15/msd-core" ]]; then
ls -d .kilo 2>/dev/null && echo "kilo" || (ls -d .claude/msd-core 2>/dev/null && echo "claude") || echo "unknown"
fi
```
For all other projects, skip this step and go to Step 1.
Use the detected root (when applicable) to resolve canonical paths:
| Source type | Standard `.claude` layout | `.kilo` layout |
|-------------|--------------------------|----------------|
| Agent files | `agents/*.md` | `.kilo/agents/*.md` |
| Command files | `commands/msd/*.md` | `.kilo/command/*.md` |
| CLI tooling | `msd-core/bin/` | `.kilo/msd-core/bin/` |
| Workflow files | `msd-core/workflows/` | `.kilo/msd-core/workflows/` |
| Reference docs | `msd-core/references/` | `.kilo/msd-core/references/` |
| Hook files | `hooks/*.js` | `.kilo/hooks/*.js` |
When analyzing this project, use ONLY the canonical source locations matching the detected layout — do not fall back to standard layout paths if `.kilo` is detected (those paths will be empty, producing semantically empty intel).
EXCLUDE from counts/analysis: `.planning/` (planning docs, not project code); `node_modules/`, `dist/`, `build/`, `.git/`.
**Count accuracy:** when reporting component counts (stack.json, arch-decisions.json), always derive counts by running Glob on the layout-resolved canonical locations, never from memory or CLAUDE.md. E.g. standard: `Glob("agents/*.md")`; kilo: `Glob(".kilo/agents/*.md")`.
**Count accuracy:** when reporting component counts (stack.json, arch-decisions.json), always derive counts by running Glob on the canonical source locations (agents, commands, hooks, workflows), never from memory or CLAUDE.md. E.g. `Glob("agents/*.md")`.
## Forbidden Files
NEVER read or include in output: `.env` files (except `.env.example`/`.env.template`); `*.key`, `*.pem`, `*.pfx`, `*.p12`; files with `credential`/`secret` in their name; `*.keystore`, `*.jks`; `id_rsa`, `id_ed25519`; `node_modules/`, `.git/`, `dist/`, `build/` directories. If encountered, skip silently — do NOT include contents.

View File

@@ -57,40 +57,14 @@ The /msd:map-codebase --query command has already confirmed that intel.enabled i
## Project Scope
<!-- Layout detection: only meaningful when analysing the MSD framework's own repo (#3290). -->
**Runtime layout detection (MSD framework repo only):** If `package.json` `"name"` equals `"@golem15/msd-core"`, this project IS the MSD framework. In that case, detect the runtime root to choose canonical paths:
```bash
# Only run layout detection when analysing the MSD framework repo itself.
if [[ "$(jq -r '.name // ""' package.json 2>/dev/null)" == "@golem15/msd-core" ]]; then
ls -d .kilo 2>/dev/null && echo "kilo" || (ls -d .claude/msd-core 2>/dev/null && echo "claude") || echo "unknown"
fi
```
For all other projects, skip this step and proceed directly to Step 1.
Use the detected root (when applicable) to resolve all canonical paths below:
| Source type | Standard `.claude` layout | `.kilo` layout |
|-------------|--------------------------|----------------|
| Agent files | `agents/*.md` | `.kilo/agents/*.md` |
| Command files | `commands/msd/*.md` | `.kilo/command/*.md` |
| CLI tooling | `msd-core/bin/` | `.kilo/msd-core/bin/` |
| Workflow files | `msd-core/workflows/` | `.kilo/msd-core/workflows/` |
| Reference docs | `msd-core/references/` | `.kilo/msd-core/references/` |
| Hook files | `hooks/*.js` | `.kilo/hooks/*.js` |
When analyzing this project, use ONLY the canonical source locations matching the detected layout. Do not fall back to the standard layout paths if the `.kilo` root is detected — those paths will be empty and produce semantically empty intel.
EXCLUDE from counts and analysis:
- `.planning/` -- Planning docs, not project code
- `node_modules/`, `dist/`, `build/`, `.git/`
**Count accuracy:** When reporting component counts in stack.json or arch-decisions.json, always derive
counts by running Glob on the layout-resolved canonical locations above, not from memory or CLAUDE.md.
Example (standard layout): `Glob("agents/*.md")`. Example (kilo): `Glob(".kilo/agents/*.md")`.
counts by running Glob on the canonical source locations (agents, commands, hooks, workflows), not from memory or CLAUDE.md.
Example: `Glob("agents/*.md")`.
## Forbidden Files

View File

@@ -98,7 +98,7 @@ Reviewer capabilities declare a cross-AI **reviewer lane** — one external CLI
model endpoint `/msd-review` hands a plan to (ADR-2782 D3). They are not install
targets: they emit no skills, agents, hooks or surface files, so their
extension-point and hook-kind cells are `—`. A host that is *also* a reviewer
(Claude, Codex, Cursor, OpenCode, Qwen, Antigravity) keeps one manifest and
(Claude, Codex, Cursor, OpenCode, Antigravity) keeps one manifest and
appears under **runtime** above, carrying its lane alongside its runtime body;
only lanes that MSD never installs into appear here.

View File

@@ -66,14 +66,9 @@ const PATH_RETURNING_FNS = [
'getGlobalSkillDisplayPath',
'resolveSkillsBaseFromDescriptor',
'resolveConfigHomeFromDescriptor',
'resolveKimiGlobalDir',
// #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',
'getConfigDirFromHome',
'resolveKiloConfigPath',
'resolveOpencodeConfigPath',
'computePathPrefix',
'expandHome',

View File

@@ -13,7 +13,7 @@
* The canonical defect is computePathPrefix returning `${resolvedTarget}/`
* verbatim on Windows (PR #1622) — backslashes leaked into `@~/.claude/...`
* markdown content, breaking cross-platform substring checks and producing
* malformed @-references in Windsurf workflow files.
* malformed @-references in installed workflow files.
*
* References:
* DEFECT.WINDOWS-PATH-LEAK-IN-MARKDOWN-CONTENT (CONTEXT.md)

View File

@@ -838,48 +838,25 @@ function validateTaskContentResolverFields(cap) {
return errors;
}
// ADR-857 phase 5e: Closed ConverterName enum — complete set used across 16 runtime descriptors,
// ADR-857 phase 5e: Closed ConverterName enum — complete set used across the runtime descriptors,
// all exported by bin/install.js (commands/skills) and src/runtime-artifact-conversion.cts (agents).
// Any ArtifactKind with a non-null converter must use one of these.
const VALID_CONVERTER_NAMES = new Set([
// commands / skills converters (pre-existing)
'convertClaudeCommandToAntigravitySkill',
'convertClaudeCommandToAugmentSkill',
'convertClaudeCommandToClineSkill',
'convertClaudeCommandToClaudeSkill',
'convertClaudeCommandToCodebuddyCommand',
'convertClaudeCommandToCodebuddySkill',
'convertClaudeCommandToCodexSkill',
'convertClaudeCommandToCopilotSkill',
'convertClaudeCommandToCursorSkill',
'convertClaudeCommandToKiloSkill',
'convertClaudeCommandToKimiSkill',
'convertClaudeCommandToKimiCodeSkill',
'convertClaudeCommandToOpencodeSkill',
'convertClaudeCommandToTraeSkill',
'convertClaudeCommandToWindsurfSkill',
'convertClaudeCommandToWindsurfWorkflow',
// agent converters (#1173 — descriptor-driven agent conversion wiring)
'convertClaudeAgentToCopilotAgent',
'convertClaudeAgentToAntigravityAgent',
'convertClaudeAgentToCursorAgent',
'convertClaudeAgentToWindsurfAgent',
'convertClaudeAgentToAugmentAgent',
'convertClaudeAgentToTraeAgent',
'convertClaudeAgentToCodebuddyAgent',
'convertClaudeAgentToClineAgent',
'convertClaudeAgentToCodexAgent',
// ADR-1239 / #2092 Phase B Upgrade 1 — native .qwen/agents/*.md subagent projection.
'convertClaudeAgentToQwenAgent',
// #3384 — ZCode agents are Claude-shaped but its dispatcher treats mcp__* tools
// grants as required MCP servers; this converter strips them at install time.
'convertClaudeAgentToZcodeAgent',
// #2875 Part 2 (the agents-bypass closure) — data-driven Hermes branding
// converter (reads hostBehaviors.brandingRewrites rather than a hardcode),
// and the kilo/opencode agent converters (shared with those runtimes'
// commands-kind entries, options-bag signature `(content, {isAgent, modelOverride})`).
'convertClaudeAgentToHermesAgent',
'convertClaudeToKiloFrontmatter',
// opencode agent converter (shared with the runtime's commands-kind entries,
// options-bag signature `(content, {isAgent, modelOverride})`).
'convertClaudeToOpencodeFrontmatter',
]);
@@ -893,7 +870,7 @@ const VALID_HOOKS_SURFACES = new Set(['settings-json', 'codex-hooks-json', 'curs
const VALID_HOOK_EVENTS = new Set(['claude', 'gemini']);
// extensionEvents — the plugin/extension-system event dialect (ADR-1239 amendment / #1943).
// DISTINCT from hookEvents (managed-hook dialect): extensionEvents describes the
// plugin-owned event subset imperative hosts expose (opencode / pi); 'none' = the
// plugin-owned event subset imperative hosts expose (opencode); 'none' = the
// host exposes no extension surface (engine owns the bus, e.g. VS Code).
const VALID_EXTENSION_EVENTS = new Set(['opencode', 'none']);
const VALID_SANDBOX_TIERS = new Set(['none', 'codex-agent-sandbox']);
@@ -910,8 +887,8 @@ const VALID_TRIGGER_PRECEDENCE_KINDS = new Set(['commands', 'skills']);
// descriptor omits the axis (see validateRuntimeBody's required-with-default
// handling below). Not invented: `['skills', 'commands']` is the ordering every
// in-tree runtime that emits both kinds (from the same trigger stems) wants —
// claude's local/global collision and the same-scope collision (codebuddy, kilo,
// opencode, zcode) all resolve to skills winning. claude's shipped descriptor
// claude's local/global collision and the same-scope collision (opencode,
// zcode) all resolve to skills winning. claude's shipped descriptor
// declares this SAME value explicitly, and
// tests/runtime-artifact-layout-trigger-surface.test.cjs asserts the two agree
// (a parity assertion — two surfaces reading one rule must not silently drift).
@@ -922,8 +899,7 @@ const FEATURE_FIELDS_FORBIDDEN_ON_RUNTIME = ['skills', 'agents', 'steps', 'contr
const VALID_INSTALL_SURFACES = new Set(['settings-json', 'codex-toml', 'cursor-hooks-json', 'profile-marker-only', 'none']);
// 'antigravity' added #2096 Phase B Upgrade 1 — settings.json permissions.allow writer.
const VALID_PERMISSION_WRITERS = new Set(['opencode', 'antigravity']);
// SubagentStart added #2092 Phase B Upgrade 2 (qwen-only today — see
// capabilities/qwen/capability.json's extendedHookEvents).
// SubagentStart added #2092 Phase B Upgrade 2.
const VALID_EXTENDED_HOOK_EVENTS = new Set(['SubagentStop', 'Stop', 'PreCompact', 'FileChanged', 'BeforeAgent', 'AfterAgent', 'BeforeModel', 'SubagentStart']);
// ADR-1239 Phase A: hostIntegration axes (MUST stay parity-identical to HOST_INTEGRATION_AXES in src/host-integration.cts)
@@ -1078,7 +1054,7 @@ const INSTALL_SURFACE_TO_ALLOWED_HOOKS_SURFACES = new Map([
// Gemini agent-events require hookEvents='gemini'; Claude-family events require hookEvents='claude'.
const GEMINI_AGENT_EVENTS = new Set(['BeforeAgent', 'AfterAgent', 'BeforeModel']);
// SubagentStart added #2092 Phase B Upgrade 2 — Claude hook-event dialect
// counterpart of SubagentStop (qwen-only today).
// counterpart of SubagentStop.
const CLAUDE_FAMILY_EVENTS = new Set(['SubagentStop', 'Stop', 'PreCompact', 'FileChanged', 'SubagentStart']);
/**

View File

@@ -59,7 +59,7 @@ const MSD_SKILL_DIR_PREFIX = 'msd-';
/**
* File extensions eligible for the stale-skill-path scan.
* SKILL.md is the only file in a codex/cursor/kilo/etc skill directory that
* SKILL.md is the only file in a codex/cursor/etc skill directory that
* embeds an @-import path to the MSD runtime config tree.
*/
const SKILL_MD_EXTENSIONS = new Set(['.md']);

View File

@@ -5,8 +5,7 @@
*
* Runtimes `codex` and `opencode` bake the resolved
* model ID into each agent's static config at install time (bin/install.js
* ~5667-5767 for codex, ~10008-10026 for opencode, and the adjacent kilo
* branch added by #2093). Their task/spawn_agent interfaces do not accept
* ~5667-5767 for codex, ~10008-10026 for opencode). Their task/spawn_agent interfaces do not accept
* an inline `model` parameter, so editing `model_overrides` in
* `.planning/config.json` or `~/.msd/defaults.json` has NO effect until the
* user re-runs `msd install <runtime>` (or `msd update`). The failure is
@@ -26,8 +25,7 @@ const os = require('os');
* Runtimes whose agent config is static frontmatter/TOML baked at install time.
* MUST stay in sync with the bake paths in bin/install.js. The parity test in
* tests/stale-bake-guard.test.cjs asserts this matches the runtimes that
* actually emit a baked model: line id #2256 (opencode), #49/#2256 (codex),
* and #2093 (kilo).
* actually emit a baked model: line id #2256 (opencode) and #49/#2256 (codex).
*/
const STATIC_FRONTMATTER_RUNTIMES = Object.freeze(['codex', 'opencode']);
@@ -87,7 +85,7 @@ function resolveRuntimeFromConfig(config) {
/**
* Resolve the install root for a runtime's agent files, honoring the same env
* vars the installer does (CODEX_HOME, OPENCODE_CONFIG_DIR, KILO_CONFIG_DIR).
* vars the installer does (CODEX_HOME, OPENCODE_CONFIG_DIR).
* Returns the absolute directory or `null` for unsupported runtimes.
*/
function resolveAgentDir(runtime, { env = process.env, homedir = os.homedir } = {}) {

View File

@@ -193,7 +193,7 @@ Reviewer capabilities declare a cross-AI **reviewer lane** — one external CLI
model endpoint \`/msd-review\` hands a plan to (ADR-2782 D3). They are not install
targets: they emit no skills, agents, hooks or surface files, so their
extension-point and hook-kind cells are \`—\`. A host that is *also* a reviewer
(Claude, Codex, Cursor, OpenCode, Qwen, Antigravity) keeps one manifest and
(Claude, Codex, Cursor, OpenCode, Antigravity) keeps one manifest and
appears under **runtime** above, carrying its lane alongside its runtime body;
only lanes that MSD never installs into appear here.

View File

@@ -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, Cline, and Codex.` and
// `Supported agents include Gemini 3, Cline, and Cursor.` all exiting 0 — the
// `MSD installs cleanly on Gemini 3, Cursor, and Codex.` and
// `Supported agents include Gemini 3, OpenCode, 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".

View File

@@ -116,13 +116,6 @@
* SHARED `transliterateForSlug` primitive, so this is not an independent
* re-derivation of the transliteration step — only of the collapse/trim
* shape it deliberately keeps un-consolidated.
* - `src/runtime-artifact-conversion.cts` `normalizeKimiSkillName` — a Kimi
* runtime skill-name normalizer in a completely different domain (CLI
* skill invocation names, never a `.planning/` phase/plan/milestone
* slug); its negated class (`[^a-z0-9-]`) deliberately PRESERVES
* hyphens (a skill name may already contain them), the opposite of the
* slug seam's contract. Shaped like the re-derivation textually; not one
* by domain.
* - `scripts/generate-package-identity.cjs` `slugifyPackageName` —
* npm-scope-name-to-cache-filename prep. Runs PRE-BUILD (`npm run
* generate:identity`, step 1 of `npm run build`, before `build:lib`
@@ -209,7 +202,6 @@ const TOP_LEVEL_FUNCTION_RE = /^(?:export\s+)?function\s+([A-Za-z0-9_]+)\s*\(/;
const FUNCTION_SCOPED_EXEMPTIONS = new Map([
[path.join('src', 'core-utils.cts'), new Set(['generateSlugInternal'])],
[path.join('src', 'gsd2-import.cts'), new Set(['slugify'])],
[path.join('src', 'runtime-artifact-conversion.cts'), new Set(['normalizeKimiSkillName'])],
[path.join('scripts', 'generate-package-identity.cjs'), new Set(['slugifyPackageName'])],
]);

View File

@@ -150,8 +150,8 @@ const MSD_OWNED_NESTED = [
* them, for the same reason TEST_ENV_BASE derives its keys: a hand-list is only
* ever as complete as its author's recall, and this one was measurably not.
*
* Round 5's adversarial review found the hand-list missing Kilo's SINGULAR
* `command/`, `workflows/`, and Hermes' `skills/msd` -- the last being a whole
* Round 5's adversarial review found the hand-list missing a SINGULAR
* `command/` dir, `workflows/`, and a `skills/msd` dir -- the last being a whole
* directory whose name carries no `msd-` prefix, so no prefix rule reaches it.
* A capability that declares a new destSubpath now extends this set in the same
* commit that declares it.

View File

@@ -408,8 +408,8 @@ 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 does not (see
// RUNTIME_CONFIG_FILES), so they are out of scope here rather than covered.
// runtimes reuse one of those two shapes; any that do not are out of scope
// here rather than covered.
entrypointRuntimes = ['claude', 'codex'],
dryRun = false,
npmEnv = undefined,

View File

@@ -126,7 +126,7 @@ const migration: InstallerMigration = {
destructive: true,
plan: (ctx: MigrationPlanContext): MigrationAction[] => {
// Defense in depth ahead of the framework's own runtimes/scope filters:
// a claude/kimi/etc. skills/ or agents/ directory is a live install
// a claude/etc. skills/ or agents/ directory is a live install
// surface, never a retirement target, and so is antigravity's LOCAL
// .agents/skills layout.
if (ctx.runtime !== 'antigravity') return [];

View File

@@ -54,9 +54,7 @@
* 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 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).
* regression reach `next` undetected.
*
* H8 is mandatory, not optional: a parity harness never demonstrated failing
* is decoration. It feeds the oracle a DELIBERATELY WRONG (but real,
@@ -556,7 +554,7 @@ test('agent-descriptor-parity: J8 — opencode resolves model overrides through
* "runtime"` capability in the REAL capability-registry that declares an
* `agents` kind (either scope), the REAL production entry point
* (`installRuntimeArtifacts` — which internally routes combinedFamilyInstall
* runtimes like kilo/opencode through `installOpencodeFamilyAgents`, #2875
* runtimes like opencode through `installOpencodeFamilyAgents`, #2875
* Part 2 Task A) actually materializes agents/ on disk. If any runtime were
* still silently depending on the deleted inline loop, this call would write
* nothing to agents/ for it (the deleted code was the ONLY thing that used to

View File

@@ -106,7 +106,7 @@ function scopesForRuntime(registry, runtime) {
if (layout) {
for (const scope of ['global', 'local']) {
for (const entry of layout[scope] || []) {
if (entry.kind === 'agents' || entry.kind === 'kimi-agents') scopes.add(scope);
if (entry.kind === 'agents') scopes.add(scope);
}
}
}

View File

@@ -216,13 +216,13 @@ describe('SPAWN: spawn type consistency', () => {
}
});
test('execute-phase has Copilot sequential fallback in runtime_compatibility', () => {
test('execute-phase has a sequential inline fallback in runtime_compatibility', () => {
const content = fs.readFileSync(
path.join(WORKFLOWS_DIR, 'execute-phase.md'), 'utf-8'
);
assert.ok(
content.includes('sequential inline execution'),
'execute-phase must document sequential inline execution as Copilot fallback'
'execute-phase must document sequential inline execution as a fallback'
);
assert.ok(
content.includes('spot-check'),

View File

@@ -221,7 +221,7 @@ test('every installable runtime accepts a configured MCP grant without crashing
// only 6 have deep per-runtime assertions elsewhere in this file. This locks
// in that the other runtimes' own converters don't choke or mangle output
// when a canonical grant is appended into their frontmatter dialect.
// scope: 'global' — universally supported (cline is global-only; local
// scope: 'global' — universally supported (local
// support varies per runtime, global does not). Search from `install.home`,
// not `install.configDir`: a nested-home runtime (e.g. antigravity) places
// agents in a sibling directory outside its own configDir subtree.

View File

@@ -19,11 +19,11 @@
* UPGRADE 2 — MCP companion config: `configureAntigravityMcpConfig` writes
* a standalone `mcp_config.json` (antigravity.google/docs/cli/gcli-migration)
* registering the `msd` MCP server (`bin/msd-mcp-server.js`, the SAME
* companion OpenCode/Kilo document/wire), non-destructively preserving any
* companion OpenCode documents/wires), non-destructively preserving any
* other user-configured `mcpServers` entries. Uninstall removes only the
* `msd` entry.
*
* Both writers are NOT MSD_TEST_MODE-gated (mirrors Kilo's dispatch, not
* Both writers are NOT MSD_TEST_MODE-gated (unlike
* OpenCode's) — runMinimalInstall strips MSD_TEST_MODE from the spawned
* installer's env, so both fire during a "minimal" install too.
*/

View File

@@ -3926,16 +3926,16 @@ describe('ADR-1016 phase 5a: validateConfigHome unit tests', () => {
test('skillsHome with valid kind → no errors', () => {
const errors = validateConfigHome('test', {
kind: 'xdg', name: 'kilo', env: [],
skillsHome: { kind: 'dot-home', name: '.kilo', env: [] },
kind: 'xdg', name: 'opencode', env: [],
skillsHome: { kind: 'dot-home', name: '.opencode', env: [] },
});
assert.deepEqual(errors, []);
});
test('skillsHome with bad kind → error', () => {
const errors = validateConfigHome('test', {
kind: 'xdg', name: 'kilo', env: [],
skillsHome: { kind: 'exotic', name: '.kilo', env: [] },
kind: 'xdg', name: 'opencode', env: [],
skillsHome: { kind: 'exotic', name: '.opencode', env: [] },
});
assert.ok(errors.some((e) => e.includes('skillsHome') && e.includes('kind')));
});
@@ -4164,12 +4164,12 @@ describe('FIX 3: tightened runtime validator — ArtifactKind field type checks'
assert.deepEqual(converterErrors, [], 'converter: null must be accepted, got: ' + JSON.stringify(converterErrors));
});
test('ArtifactKind with converter: "convertClaudeCommandToKiloSkill" (string) → no error', () => {
test('ArtifactKind with converter: "convertClaudeCommandToOpencodeSkill" (string) → no error', () => {
const cap = makeRuntimeCap({
id: 'test-rt',
runtime: {
artifactLayout: {
global: [{ kind: 'skills', destSubpath: 'skills', prefix: 'msd-', nesting: 'flat', recursive: true, converter: 'convertClaudeCommandToKiloSkill' }],
global: [{ kind: 'skills', destSubpath: 'skills', prefix: 'msd-', nesting: 'flat', recursive: true, converter: 'convertClaudeCommandToOpencodeSkill' }],
local: [],
},
},
@@ -4294,57 +4294,26 @@ describe('ADR-1016 phase 5a: closed-vocab set exports', () => {
// ─── 25. ADR-857 phase 5e: closed ConverterName enum (Part B) ─────────────────
describe('ADR-857 phase 5e: VALID_CONVERTER_NAMES closed enum', () => {
// #2875 Part 2 (the agents-bypass closure): 3 converters added —
// convertClaudeAgentToHermesAgent (data-driven Hermes branding converter,
// reads hostBehaviors.brandingRewrites) and convertClaudeToKiloFrontmatter /
// convertClaudeToOpencodeFrontmatter (kilo/opencode's agents-kind
// converters — shared by name with those runtimes' commands-kind entries,
// options-bag signature `(content, {isAgent, modelOverride})`). All three
// are genuinely new agent converters (not renamed/leftover), so the agent
// count grows from 11 to 14; the 16 command/skill/workflow converters are
// unchanged.
test('VALID_CONVERTER_NAMES has exactly 30 entries (16 command/skill/workflow + 14 agent converters)', () => {
test('VALID_CONVERTER_NAMES has exactly 10 entries (5 command/skill + 5 agent converters)', () => {
assert.ok(VALID_CONVERTER_NAMES instanceof Set, 'VALID_CONVERTER_NAMES must be a Set');
assert.strictEqual(VALID_CONVERTER_NAMES.size, 30, 'VALID_CONVERTER_NAMES must have exactly 30 entries, got: ' + VALID_CONVERTER_NAMES.size);
assert.strictEqual(VALID_CONVERTER_NAMES.size, 10, 'VALID_CONVERTER_NAMES must have exactly 10 entries, got: ' + VALID_CONVERTER_NAMES.size);
});
test('VALID_CONVERTER_NAMES contains all expected converter names', () => {
const expected = [
// command/skill converters (pre-existing)
'convertClaudeCommandToAntigravitySkill',
'convertClaudeCommandToAugmentSkill',
'convertClaudeCommandToClineSkill',
'convertClaudeCommandToClaudeSkill',
'convertClaudeCommandToCodebuddyCommand',
'convertClaudeCommandToCodebuddySkill',
'convertClaudeCommandToCodexSkill',
'convertClaudeCommandToCopilotSkill',
'convertClaudeCommandToCursorSkill',
'convertClaudeCommandToKiloSkill',
'convertClaudeCommandToKimiSkill',
'convertClaudeCommandToOpencodeSkill',
'convertClaudeCommandToTraeSkill',
'convertClaudeCommandToWindsurfSkill',
'convertClaudeCommandToWindsurfWorkflow',
// agent converters (#1173 — descriptor-driven agent conversion wiring)
'convertClaudeAgentToCopilotAgent',
'convertClaudeAgentToAntigravityAgent',
'convertClaudeAgentToCursorAgent',
'convertClaudeAgentToWindsurfAgent',
'convertClaudeAgentToAugmentAgent',
'convertClaudeAgentToTraeAgent',
'convertClaudeAgentToCodebuddyAgent',
'convertClaudeAgentToClineAgent',
'convertClaudeAgentToCodexAgent',
// ADR-1239 / #2092 Phase B Upgrade 1 — native .qwen/agents/*.md subagent projection.
'convertClaudeAgentToQwenAgent',
// #3384 — ZCode agent converter (strips mcp__* grants at install time).
'convertClaudeAgentToZcodeAgent',
// #2875 Part 2 (the agents-bypass closure) — data-driven Hermes branding
// converter, and the kilo/opencode agent converters (shared name with
// those runtimes' commands-kind entries).
'convertClaudeAgentToHermesAgent',
'convertClaudeToKiloFrontmatter',
// opencode agent converter (shared name with the runtime's commands-kind entries).
'convertClaudeToOpencodeFrontmatter',
];
for (const name of expected) {
@@ -4379,8 +4348,8 @@ describe('ADR-857 phase 5e: validateArtifactKindEntry — ConverterName enum (FA
});
// Valid known name must be accepted
test('ACCEPTED: converter "convertClaudeCommandToKiloSkill" is a known ConverterName', () => {
const entry = makeArtifactEntry({ converter: 'convertClaudeCommandToKiloSkill' });
test('ACCEPTED: converter "convertClaudeCommandToOpencodeSkill" is a known ConverterName', () => {
const entry = makeArtifactEntry({ converter: 'convertClaudeCommandToOpencodeSkill' });
const errors = validateArtifactKindEntry('test-cap', entry, 'artifactLayout.global[0]');
const converterErrors = errors.filter((e) => e.includes('converter'));
assert.deepEqual(converterErrors, [], 'Known converter name must be accepted, got: ' + JSON.stringify(converterErrors));
@@ -4666,7 +4635,7 @@ describe('ADR-857 phase 5f: cross-field consistency gate rejection tests (DEFECT
);
});
test('REJECTS: permissionWriter not null and not in {opencode,kilo} → throws validation error', () => {
test('REJECTS: permissionWriter not null and not in {opencode,antigravity} → throws validation error', () => {
const cap = makeValidRuntimeCap({ runtime: { permissionWriter: 'notarealwriter' } });
const errors = validateRuntimeBody(cap);
assert.ok(
@@ -4800,7 +4769,7 @@ describe('ADR-857 phase 5f: cross-field consistency gate rejection tests (DEFECT
test('VALID_EXTENDED_HOOK_EVENTS covers all 8 known extended events', () => {
assert.ok(VALID_EXTENDED_HOOK_EVENTS instanceof Set, 'Must be a Set');
assert.strictEqual(VALID_EXTENDED_HOOK_EVENTS.size, 8, 'Must cover 8 extended hook events');
// SubagentStart added #2092 Phase B Upgrade 2 (qwen-only today).
// SubagentStart added #2092 Phase B Upgrade 2.
for (const ev of ['SubagentStop', 'Stop', 'PreCompact', 'FileChanged', 'BeforeAgent', 'AfterAgent', 'BeforeModel', 'SubagentStart']) {
assert.ok(VALID_EXTENDED_HOOK_EVENTS.has(ev), 'Must include event "' + ev + '"');
}

View File

@@ -25,8 +25,7 @@ const SCRIPT_REL = path.join('scripts', 'check-glossary-refs.cjs');
// The real bin/install.js allRuntimes array, mirrored here so fixtures can
// build both a matching and a deliberately-drifted CONTEXT.md against it.
const REAL_RUNTIMES = [
'claude', 'antigravity', 'augment', 'cline', 'codebuddy', 'codex', 'copilot',
'cursor', 'hermes', 'kimi', 'kilo', 'opencode', 'pi', 'qwen', 'trae', 'windsurf', 'zcode',
'claude', 'antigravity', 'codex', 'cursor', 'opencode', 'zcode',
];
function allRuntimesSentence(count, members) {
@@ -80,7 +79,7 @@ test('a clean CONTEXT.md whose refs resolve and whose allRuntimes matches passes
'',
'See `src/real-module.cts` for details.',
'',
`${allRuntimesSentence(17, REAL_RUNTIMES)}.`,
`${allRuntimesSentence(6, REAL_RUNTIMES)}.`,
'',
].join('\n');
const root = makeRepo(t, { contextBody: context });
@@ -96,7 +95,7 @@ test('a reference to a nonexistent tracked file fails --check and names the toke
'',
'See `src/does-not-exist.cts` for details.',
'',
`${allRuntimesSentence(17, REAL_RUNTIMES)}.`,
`${allRuntimesSentence(6, REAL_RUNTIMES)}.`,
'',
].join('\n');
const root = makeRepo(t, { contextBody: context });
@@ -107,24 +106,24 @@ test('a reference to a nonexistent tracked file fails --check and names the toke
});
test('allRuntimes count and membership drift is caught', (t) => {
// Mirrors the real-world case: CONTEXT.md says 15 while bin/install.js has 17.
const claimed15 = REAL_RUNTIMES.filter((r) => r !== 'pi' && r !== 'zcode');
assert.equal(claimed15.length, 15);
// Mirrors the real-world case: CONTEXT.md says 4 while bin/install.js has 6.
const claimed4 = REAL_RUNTIMES.filter((r) => r !== 'cursor' && r !== 'zcode');
assert.equal(claimed4.length, 4);
const context = [
'# Context',
'',
'See `src/real-module.cts` for details.',
'',
`${allRuntimesSentence(15, claimed15)}.`,
`${allRuntimesSentence(4, claimed4)}.`,
'',
].join('\n');
const root = makeRepo(t, { contextBody: context }); // bin/install.js defaults to the real 17
const root = makeRepo(t, { contextBody: context }); // bin/install.js defaults to the real 6
const res = run(root, ['--check']);
assert.equal(res.status, 1);
assert.match(res.stderr, /claims 15 values/);
assert.match(res.stderr, /has 17/);
assert.match(res.stderr, /pi/);
assert.match(res.stderr, /claims 4 values/);
assert.match(res.stderr, /has 6/);
assert.match(res.stderr, /cursor/);
assert.match(res.stderr, /zcode/);
});
@@ -134,7 +133,7 @@ test('a reference to a nonexistent msd-core/bin/lib/*.cjs path is skipped (gener
'',
'Generated router lives at `msd-core/bin/lib/does-not-exist.cjs`.',
'',
`${allRuntimesSentence(17, REAL_RUNTIMES)}.`,
`${allRuntimesSentence(6, REAL_RUNTIMES)}.`,
'',
].join('\n');
const root = makeRepo(t, { contextBody: context });
@@ -154,7 +153,7 @@ test('an intentionally-absent documented path is skipped, not asserted missing',
'',
'The escape hatch is a committed acknowledgment (`tests/emitted-drift-ack.json`).',
'',
`${allRuntimesSentence(17, REAL_RUNTIMES)}.`,
`${allRuntimesSentence(6, REAL_RUNTIMES)}.`,
'',
].join('\n');
const root = makeRepo(t, { contextBody: context });
@@ -175,7 +174,7 @@ test('a sibling tests/ path that is merely missing is still caught', (t) => {
'',
'See `tests/emitted-drift-ack-typo.json` for details.',
'',
`${allRuntimesSentence(17, REAL_RUNTIMES)}.`,
`${allRuntimesSentence(6, REAL_RUNTIMES)}.`,
'',
].join('\n');
const root = makeRepo(t, { contextBody: context });
@@ -191,7 +190,7 @@ test('a ~/-rooted path and a bare filename are both skipped', (t) => {
'',
'See `~/.claude/x.md` and `core.cjs` for details.',
'',
`${allRuntimesSentence(17, REAL_RUNTIMES)}.`,
`${allRuntimesSentence(6, REAL_RUNTIMES)}.`,
'',
].join('\n');
const root = makeRepo(t, { contextBody: context });
@@ -211,7 +210,7 @@ test('a `..`-traversal token cannot escape ROOT into a filesystem-existence prob
'',
'Escape attempt: `src/../../../../../../etc/passwd` and `src/../../etc/hosts`.',
'',
`${allRuntimesSentence(17, REAL_RUNTIMES)}.`,
`${allRuntimesSentence(6, REAL_RUNTIMES)}.`,
'',
].join('\n');
const root = makeRepo(t, { contextBody: context });
@@ -255,7 +254,7 @@ test('a broken tracked reference after a RULESET predicate with inner backticks
'',
'Coverage lives in `src/does-not-exist.cts`.',
'',
`${allRuntimesSentence(17, REAL_RUNTIMES)}.`,
`${allRuntimesSentence(6, REAL_RUNTIMES)}.`,
'',
].join('\n');
const root = makeRepo(t, { contextBody: context });
@@ -279,7 +278,7 @@ test('tracked paths inside an outer-wrapped predicate span are extracted (#3604)
'',
'`WORKTREE.SEAM.demo-anchor=src/real-module.cts + tests/missing-anchor.test.cjs`',
'',
`${allRuntimesSentence(17, REAL_RUNTIMES)}.`,
`${allRuntimesSentence(6, REAL_RUNTIMES)}.`,
'',
].join('\n');
const root = makeRepo(t, { contextBody: context });
@@ -307,7 +306,7 @@ test('glob and NNNN template mentions are not drift (#3604)', (t) => {
'A fresh ADR starts from `docs/adr/NNNN.md`.',
'Retired scanners were `scripts/lint-*.cjs`.',
'',
`${allRuntimesSentence(17, REAL_RUNTIMES)}.`,
`${allRuntimesSentence(6, REAL_RUNTIMES)}.`,
'',
].join('\n');
const root = makeRepo(t, { contextBody: context });
@@ -335,7 +334,7 @@ test('retired-path exemptions are exact, not a blanket hole (#3604)', (t) => {
'`RULESET.TESTS.harness=local plugin at eslint-rules/ (repo root, NOT scripts/eslint-rules/)`',
'A typo sibling `tests/golden-install-parity-typo.test.cjs` is real drift.',
'',
`${allRuntimesSentence(17, REAL_RUNTIMES)}.`,
`${allRuntimesSentence(6, REAL_RUNTIMES)}.`,
'',
].join('\n');
const root = makeRepo(t, { contextBody: context });

View File

@@ -41,7 +41,7 @@ describe('generate-claude-md', () => {
const content = fs.readFileSync(claudePath, 'utf-8');
assert.ok(content.includes('## MSD Workflow Enforcement'));
// #3584: generated CLAUDE.md must emit the runtime-routable hyphen-form
// (Claude/Cursor/OpenCode/Kilo etc.); the legacy colon form is no longer
// (Claude/Cursor/OpenCode etc.); the legacy colon form is no longer
// dispatched by current skill installs.
assert.ok(content.includes('/msd-quick'));
assert.ok(content.includes('/msd-debug'));

View File

@@ -128,7 +128,7 @@ describe('detectWorkflowBackend', () => {
});
test('non-Claude runtime -> inline (criterion 6: no change to non-Claude loop)', () => {
for (const runtimeId of ['codex', 'cursor', 'opencode', 'copilot', ' Windsurf'.trim()]) {
for (const runtimeId of ['codex', 'cursor', 'opencode', 'zcode', 'antigravity']) {
const r = detectWorkflowBackend({
runtimeId,
hostIntegration: CAPABLE_HOST,

View File

@@ -219,8 +219,8 @@ process.env.MSD_TEST_MODE = '1';
* --codex --global`.
*
* Fix: re-wire copyCommandsAsCodexSkills() back into the install dispatch path
* so the same skill-shape that Claude / Copilot / Antigravity / Cursor /
* Windsurf / Augment / Trae installs produce is also produced for Codex.
* so the same skill-shape that Claude / Antigravity / Cursor
* installs produce is also produced for Codex.
*/
const { describe, test, beforeEach, afterEach } = require('node:test');
@@ -1966,7 +1966,7 @@ process.env.MSD_TEST_MODE = '1';
* the right one per its HookHandlerConfig schema
* (codex-rs/config/src/hook_config.rs: commandWindows / command_windows alias).
*
* Note: UserPromptSubmit is NOT wired (same rationale as Qwen #788 — the
* Note: UserPromptSubmit is NOT wired (same rationale as #788 — the
* msd-prompt-guard handler exits unless tool_name is Write|Edit, so it would be
* a silent no-op for the UserPromptSubmit payload shape).
*

View File

@@ -648,7 +648,7 @@ description: An unknown agent with no declared tools
});
test('gates a provider-namespaced anthropic/claude-* model from the runtime-resolver path (#2310 review)', () => {
// Catalog assigns anthropic/claude-* to opencode/hermes/kilo. A mixed-runtime config
// Catalog assigns anthropic/claude-* to opencode. A mixed-runtime config
// (runtime: opencode) + Codex install resolves those; they must NOT reach the .toml.
const runtimeResolver = { runtime: 'opencode', resolve: () => ({ model: 'anthropic/claude-opus-4-8' }) };
const result = generateCodexAgentToml('msd-executor', sampleAgent, null, runtimeResolver);

View File

@@ -5,7 +5,7 @@
*
* `installSharedHooksBundle` used to write `{"type":"commonjs"}` over
* `<configRoot>/package.json` unconditionally — no existence check, no merge,
* no backup. On OpenCode and Kilo that file is documented, user-writable
* no backup. On OpenCode that file is documented, user-writable
* territory (it is where local-plugin npm dependencies are declared), so every
* install and every `/msd-update` destroyed the user's `name`, `type`,
* `dependencies`, and `scripts`.
@@ -522,14 +522,12 @@ describe('#2544 regression: install must not clobber the config-root package.jso
// `staging zero hook scripts` tests above are the ones that pin a real
// staged-nothing gate, on the #2717 writers.
//
// ZCode, not Windsurf. Windsurf was the original choice because
// hostBehaviors.skipSharedHooksInstall kept it out of the shared bundle —
// but #2717 then began staging cursor/windsurf/codex .js hooks via dedicated
// paths and writing the marker beside them, so for those three MSD now DOES
// fill hooks/ and the marker is correct. ZCode is the durable choice: per
// #1821 it has hooksSurface:'none' AND no plugin surface to spawn hooks, so
// MSD stages no .js there by either route. (Measured on this tree: zcode
// stages 0 .js hooks and gets no marker; windsurf stages 2 and gets one.)
// ZCode: cursor/codex stage .js hooks via dedicated paths (#2717) and write
// the marker beside them, so for those MSD now DOES fill hooks/ and the
// marker is correct. ZCode is the durable choice: per #1821 it has
// hooksSurface:'none' AND no plugin surface to spawn hooks, so MSD stages
// no .js there by either route. (Measured on this tree: zcode stages 0 .js
// hooks and gets no marker.)
const userHooks = path.join(root, 'hooks');
fs.mkdirSync(userHooks, { recursive: true });
fs.writeFileSync(path.join(userHooks, 'my-hook.js'), '// user-authored\n');

View File

@@ -230,7 +230,7 @@ test('an EPERM from statSync (Windows equivalent of EACCES on a parent directory
]);
});
test('a selfExecutable + interpreterCandidates entry checks both the execute bit and the interpreter (#4249 CodeRabbit review — Cline\'s hybrid `env node` shebang)', (t) => {
test('a selfExecutable + interpreterCandidates entry checks both the execute bit and the interpreter (#4249 CodeRabbit review — hybrid `env node` shebang)', (t) => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'configured-entrypoint-self-exec-'));
t.after(() => helpers.cleanup(root));
const okScript = path.join(root, 'ok.js');
@@ -239,8 +239,8 @@ test('a selfExecutable + interpreterCandidates entry checks both the execute bit
fs.writeFileSync(notExecScript, '#!/usr/bin/env node\n');
const makeEntry = (scriptPath) => ({
runtime: 'cline',
configPath: path.join(root, '.clinerules', 'hooks', 'PreToolUse'),
runtime: 'cursor',
configPath: path.join(root, '.cursor', 'hooks', 'PreToolUse'),
scriptPath,
interpreterCandidates: ['node'],
selfExecutable: true,

View File

@@ -90,7 +90,7 @@ Planner body
// inside backtick spans, before punctuation) survived and triggered the
// post-install audit "Found N unreplaced .claude path reference(s)".
//
// Fix: add three bare-form rewrites (mirroring augment/windsurf/trae) using
// Fix: add three bare-form rewrites using
// (?![\w-]) to avoid corrupting .claude-plugin / .claudeignore.
//
// TDD proof: these assertions FAIL before the fix and PASS after.

View File

@@ -107,7 +107,7 @@ describe('msd-cursor-subagent-start.js: isolation guard applicability (#3045)',
let linkedWorktree; // real `git worktree add` of harnessProject, NOT under Cursor's managed
// worktree root — a hand-made worktree the user opened themselves.
// #3045 finding 3: this is no longer treated as isolation proof.
let noneProject; // config.json { runtime: 'windsurf' } -> resolves to 'none'
let noneProject; // config.json { runtime: 'zcode' } -> resolves to 'none'
let orchestratorEnvProject; // exercised via MSD_RUNTIME=codex -> 'orchestrator-worktree'
let noMsdDir; // no .planning at all
let unreadableConfigProject; // config.json is a directory (EISDIR)
@@ -126,7 +126,7 @@ describe('msd-cursor-subagent-start.js: isolation guard applicability (#3045)',
git(['worktree', 'add', linkedPath, '-b', 'agent-msd-cs-iso-test'], harnessProject);
linkedWorktree = linkedPath;
noneProject = makeGitProject('msd-cs-none-', JSON.stringify({ runtime: 'windsurf' }));
noneProject = makeGitProject('msd-cs-none-', JSON.stringify({ runtime: 'zcode' }));
orchestratorEnvProject = makeGitProject('msd-cs-orch-', JSON.stringify({}));
noMsdDir = createTempDir('msd-cs-nomsd-');
@@ -184,7 +184,7 @@ describe('msd-cursor-subagent-start.js: isolation guard applicability (#3045)',
assert.equal(out.permission, undefined);
});
test('resolved mode none (config.json runtime=windsurf) -> allow', () => {
test('resolved mode none (config.json runtime=zcode) -> allow', () => {
const r = runHook(subagentPayload([noneProject]));
assert.equal(r.status, 0, `stdout: ${r.stdout}`);
const out = JSON.parse(r.stdout);

View File

@@ -14,10 +14,9 @@
* block was previously empty. ZCode's golden install tree has ZERO hook
* files (verified), so folding this onto `hostBehaviors.skipSharedHooksInstall`
* is byte-parity — the same fold already done for
* windsurf/copilot/cursor/cline/kilo/trae (#2089/#2090/#2093/#2094/#2099/#2100).
* cursor (#2089/#2090/#2093/#2094/#2099/#2100).
*
* This test is the reference-host dogfood mirroring
* tests/declarative-reference-windsurf.test.cjs: it (1) classifies ZCode's
* This test is the reference-host dogfood: it (1) classifies ZCode's
* profile via profileOf, (2) confirms the public declarative adapter
* classifies it as declarative, (3) round-trips a real install proving a
* msd surface is emitted, (4) proves negotiation fails CLOSED on a corrupted
@@ -120,7 +119,7 @@ test('a real ZCode install emits an invocable msd skill/command/agent surface',
// ---------------------------------------------------------------------------
// #2101 EoS/zcode — fail-closed negotiation + validator acceptance +
// the folded descriptor (mirrors codebuddy/windsurf/augment reference tests).
// the folded descriptor.
// ---------------------------------------------------------------------------
test('negotiateHostCapabilities never throws for zcode, even fully corrupted', () => {
@@ -204,8 +203,8 @@ test('#4002 negative space: the project-local shim fallback survives the zcode r
// AC-style proof: the 2 still-`undocumented` dispatch sub-axes (nested/
// maxDepth) must degrade to the most-restrictive KNOWN value, never their
// optimistic value. Unlike augment (3 undocumented sub-axes) or antigravity
// (4), zcode documents namedDispatch/background/subagentToolkit/
// optimistic value. Unlike antigravity
// (4 undocumented sub-axes), zcode documents namedDispatch/background/subagentToolkit/
// backgroundDispatch, leaving only nested + maxDepth undocumented. Real
// values confirmed via:
// node -e "const {negotiateHostCapabilities}=require('./msd-core/bin/lib/host-integration.cjs');

View File

@@ -1,434 +0,0 @@
'use strict';
/**
* emitted-caps.test.cjs — the per-runtime emitted-byte cap decision
* (issue #2931, epic #1671, Phase 4). Exercises `tests/helpers/emitted-caps.cjs`
* per `.msd/phase/chore-2931-emitted-byte-caps/50-test-matrix.md` section A.
*
* Assertion discipline: every check compares typed structured values
* (`REASON` enum members, numeric fields) — never rendered prose
* (CONTRIBUTING.md, "Prohibited: Raw Text Matching on Test Outputs").
*/
const test = require('node:test');
const assert = require('node:assert/strict');
const fc = require('./helpers/fast-check-setup.cjs');
const { REASON, EMITTED_CAPS, evaluateEmittedCaps } = require('./helpers/emitted-caps.cjs');
const WINDSURF_PATTERN = 'workflows/*.md';
const WINDSURF_CAP = 12000;
// ─── A1-A8: happy path + boundaries + independence ───────────────────────────
test('passesRuntimeWithNoDeclaredCap', () => {
// `windsurf` must also report SOME sizes here: EMITTED_CAPS declares a
// windsurf cap, and a runtime the cap table names but `sizes` never
// mentions is REASON.UNKNOWN_RUNTIME (a distinct, more specific error —
// see A10/`errorsOnCapRuleForUnknownRuntime`), not the "no declared cap"
// path this test targets. Giving windsurf a compliant artifact keeps that
// orthogonal path out of this fixture while still proving `claude` (which
// truly has no cap entry) is recorded unmeasured and passes.
const r = evaluateEmittedCaps({
sizes: {
claude: { 'workflows/plan-phase.md': 999999 },
windsurf: { 'workflows/satisfies-the-rule.md': 100 },
},
capTable: EMITTED_CAPS,
});
assert.equal(r.errors.length, 0);
assert.equal(r.violations.length, 0);
assert.equal(r.unmeasured.length, 1);
assert.equal(r.unmeasured[0].runtime, 'claude');
assert.equal(r.compliant.length, 1);
assert.equal(r.compliant[0].runtime, 'windsurf');
assert.ok(r.ok);
});
test('passesArtifactUnderCap', () => {
const r = evaluateEmittedCaps({
sizes: { windsurf: { 'workflows/a.md': 100 } },
});
assert.equal(r.violations.length, 0);
assert.equal(r.compliant.length, 1);
assert.equal(r.compliant[0].bytes, 100);
assert.equal(r.compliant[0].cap, WINDSURF_CAP);
assert.ok(r.ok);
});
test('passesAtCapMinusOne', () => {
const r = evaluateEmittedCaps({ sizes: { windsurf: { 'workflows/a.md': WINDSURF_CAP - 1 } } });
assert.equal(r.violations.length, 0);
assert.equal(r.compliant.length, 1);
assert.ok(r.ok);
});
test('passesAtExactlyCap', () => {
const r = evaluateEmittedCaps({ sizes: { windsurf: { 'workflows/a.md': WINDSURF_CAP } } });
assert.equal(r.violations.length, 0, 'inclusive <= means the cap itself passes');
assert.equal(r.compliant.length, 1);
assert.equal(r.compliant[0].bytes, WINDSURF_CAP);
assert.ok(r.ok);
});
test('failsAtCapPlusOne', () => {
const r = evaluateEmittedCaps({ sizes: { windsurf: { 'workflows/a.md': WINDSURF_CAP + 1 } } });
assert.equal(r.compliant.length, 0);
assert.equal(r.violations.length, 1);
const v = r.violations[0];
assert.equal(v.runtime, 'windsurf');
assert.equal(v.rel, 'workflows/a.md');
assert.equal(v.bytes, WINDSURF_CAP + 1);
assert.equal(v.cap, WINDSURF_CAP);
assert.equal(v.delta, 1);
assert.equal(v.reason, REASON.CAP_EXCEEDED);
assert.ok(!r.ok);
});
test('passesZeroByteArtifact', () => {
const r = evaluateEmittedCaps({ sizes: { windsurf: { 'workflows/empty.md': 0 } } });
assert.equal(r.violations.length, 0, 'empty is not oversize');
assert.equal(r.compliant.length, 1);
assert.equal(r.compliant[0].bytes, 0);
assert.ok(r.ok);
});
test('ignoresPathMatchingNoCapRule', () => {
// The sole windsurf rule (`workflows/*.md`) must ALSO match something in
// this fixture, or it is a dead rule across the whole run (A13/A14 — a
// deliberate hard error, see `errorsOnDeadCapRuleMatchingNothing`) and
// this test would be asserting two different failure modes at once. Give
// it a compliant match so the ONLY thing under test is: a path the rule
// doesn't match is ignored (unmeasured), not that the rule is dead.
const r = evaluateEmittedCaps({
sizes: {
windsurf: {
'skills/msd-add-tests/SKILL.md': 999999,
'workflows/satisfies-the-rule.md': 100,
},
},
});
assert.equal(r.violations.length, 0);
assert.equal(r.unmeasured.length, 1);
assert.equal(r.unmeasured[0].rel, 'skills/msd-add-tests/SKILL.md');
assert.equal(r.compliant.length, 1);
assert.equal(r.deadRules.length, 0);
assert.ok(r.ok);
});
test('failsOnlyOffendingRuntimeForSharedRelPath', () => {
const capTable = {
windsurf: [{ pattern: WINDSURF_PATTERN, maxBytes: 100, note: 'test' }],
otherRuntime: [{ pattern: WINDSURF_PATTERN, maxBytes: 100, note: 'test' }],
};
const r = evaluateEmittedCaps({
sizes: {
windsurf: { 'workflows/shared.md': 200 }, // over
otherRuntime: { 'workflows/shared.md': 50 }, // under
},
capTable,
});
assert.equal(r.violations.length, 1);
assert.equal(r.violations[0].runtime, 'windsurf');
assert.equal(r.compliant.length, 1);
assert.equal(r.compliant[0].runtime, 'otherRuntime');
assert.ok(!r.ok);
});
// ─── A9-A20: negative / hostile ──────────────────────────────────────────────
test('errorsOnDeadCapRuleMatchingNothing', () => {
const capTable = { windsurf: [{ pattern: WINDSURF_PATTERN, maxBytes: WINDSURF_CAP, note: 'x' }] };
const r = evaluateEmittedCaps({
sizes: { windsurf: { 'skills/other.md': 10 } }, // never matches the workflows/*.md rule
capTable,
});
assert.equal(r.deadRules.length, 1);
assert.equal(r.deadRules[0].runtime, 'windsurf');
assert.equal(r.deadRules[0].pattern, WINDSURF_PATTERN);
assert.equal(r.deadRules[0].reason, REASON.DEAD_RULE);
assert.ok(!r.ok, 'a cap guarding nothing is a hard error');
});
test('errorsOnCapRuleForUnknownRuntime', () => {
const capTable = { 'ghost-runtime': [{ pattern: '*.md', maxBytes: 100, note: 'x' }] };
const r = evaluateEmittedCaps({
sizes: { claude: { 'a.md': 10 } },
capTable,
});
assert.equal(r.errors.length, 1);
assert.equal(r.errors[0].reason, REASON.UNKNOWN_RUNTIME);
assert.equal(r.errors[0].runtime, 'ghost-runtime');
assert.ok(!r.ok);
});
test('errorsWhenRuntimeProducedNoArtifacts', () => {
const r = evaluateEmittedCaps({ sizes: { windsurf: {} } });
assert.equal(r.errors.length, 1);
assert.equal(r.errors[0].reason, REASON.NO_ARTIFACTS);
assert.equal(r.errors[0].runtime, 'windsurf');
assert.ok(!r.ok, 'never read "nothing to check" as "pass"');
});
test('errorsOnMissingSizesMap', () => {
for (const bad of [null, undefined]) {
const r = evaluateEmittedCaps({ sizes: bad });
assert.equal(r.errors.length, 1, `${bad} must be rejected`);
assert.equal(r.errors[0].reason, REASON.INVALID_SIZES);
assert.ok(!r.ok);
}
});
test('errorsOnNonObjectSizesMap', () => {
for (const bad of [0, 'str', [], true]) {
const r = evaluateEmittedCaps({ sizes: bad });
assert.equal(r.errors.length, 1, `${JSON.stringify(bad)} must be rejected`);
assert.equal(r.errors[0].reason, REASON.INVALID_SIZES);
assert.equal(r.errors[0].receivedType, Array.isArray(bad) ? 'array' : typeof bad);
assert.ok(!r.ok);
}
});
test('errorsOnNonObjectCapTable', () => {
for (const bad of [null, [], 0, 'str']) {
const r = evaluateEmittedCaps({ sizes: { windsurf: { 'a.md': 1 } }, capTable: bad });
assert.equal(r.errors.length, 1, `${JSON.stringify(bad)} must be rejected`);
assert.equal(r.errors[0].reason, REASON.INVALID_CAP_TABLE);
assert.ok(!r.ok);
}
});
test('treatsZeroCapAsAlwaysViolating', () => {
const capTable = { windsurf: [{ pattern: WINDSURF_PATTERN, maxBytes: 0, note: 'zero cap' }] };
const violating = evaluateEmittedCaps({ sizes: { windsurf: { 'workflows/a.md': 1 } }, capTable });
assert.equal(violating.violations.length, 1);
assert.equal(violating.violations[0].cap, 0);
assert.equal(violating.violations[0].delta, 1);
assert.equal(violating.errors.length, 0, 'maxBytes: 0 is a LEGAL table entry');
const stillPasses = evaluateEmittedCaps({ sizes: { windsurf: { 'workflows/a.md': 0 } }, capTable });
assert.equal(stillPasses.violations.length, 0, 'a genuinely empty artifact still passes a zero cap');
assert.equal(stillPasses.compliant.length, 1);
});
test('errorsOnNonPositiveIntegerCap', () => {
for (const bad of [-1, NaN, Infinity, 1.5]) {
const capTable = { windsurf: [{ pattern: WINDSURF_PATTERN, maxBytes: bad, note: 'x' }] };
const r = evaluateEmittedCaps({ sizes: { windsurf: { 'skills/unrelated.md': 5 } }, capTable });
const err = r.errors.find((e) => e.reason === REASON.INVALID_CAP_VALUE);
assert.ok(err, `${bad} must be rejected at table validation`);
assert.ok(
Number.isNaN(bad) ? Number.isNaN(err.value) : err.value === bad,
'the raw offending value must be surfaced',
);
assert.equal(r.deadRules.length, 0, 'an invalid rule must never also be reported as merely dead');
assert.ok(!r.ok);
}
});
test('errorsOnStringCapValue', () => {
const capTable = { windsurf: [{ pattern: WINDSURF_PATTERN, maxBytes: '12000', note: 'x' }] };
const r = evaluateEmittedCaps({ sizes: { windsurf: { 'workflows/a.md': 5 } }, capTable });
const err = r.errors.find((e) => e.reason === REASON.INVALID_CAP_VALUE);
assert.ok(err);
assert.equal(err.value, '12000');
assert.equal(typeof err.value, 'string', 'no implicit coercion — the raw string is surfaced, not 12000');
assert.ok(!r.ok);
});
test('rejectsReservedKeysInSizesMap', () => {
// Genuine OWN properties named __proto__/constructor/prototype, built the
// way a real ingest (JSON.parse) would — not the object-literal special
// case that would set the prototype instead of a key.
for (const key of ['__proto__', 'constructor', 'prototype']) {
const topLevel = JSON.parse(JSON.stringify({ [key]: { 'a.md': 10 } }));
const runtimeLevel = JSON.parse(JSON.stringify({ windsurf: { [key]: 10 } }));
const rTop = evaluateEmittedCaps({ sizes: topLevel, capTable: {} });
const topErr = rTop.errors.find((e) => e.reason === REASON.RESERVED_KEY && e.scope === 'sizes-runtime');
assert.ok(topErr, `${key} as a runtime key must be rejected loudly`);
assert.equal(topErr.key, key);
assert.ok(!rTop.ok);
const rRel = evaluateEmittedCaps({ sizes: runtimeLevel, capTable: {} });
const relErr = rRel.errors.find((e) => e.reason === REASON.RESERVED_KEY && e.scope === 'sizes-rel');
assert.ok(relErr, `${key} as a rel key must be rejected loudly`);
assert.equal(relErr.key, key);
assert.equal(relErr.runtime, 'windsurf');
assert.ok(!rRel.ok);
}
// The brief's "in either map" also covers capTable's runtime keys. Must use
// the COMPUTED key form (`{ ['__proto__']: ... }`), matching the genuine
// OWN-property construction above: the literal object-initializer form
// `{ __proto__: ... }` is special-cased by the language to set the
// object's [[Prototype]] instead of creating an own property, so it would
// produce an object with ZERO own keys (nothing for JSON.stringify to
// serialize, and nothing for Object.keys(capTable) to ever see) — testing
// nothing at all rather than the hostile-key case this asserts on.
const capTableWithReservedRuntime = JSON.parse(
JSON.stringify({ ['__proto__']: [{ pattern: '*.md', maxBytes: 10, note: 'x' }] }),
);
const rCapTable = evaluateEmittedCaps({ sizes: { windsurf: { 'a.md': 1 } }, capTable: capTableWithReservedRuntime });
const capErr = rCapTable.errors.find((e) => e.reason === REASON.RESERVED_KEY && e.scope === 'capTable-runtime');
assert.ok(capErr, '__proto__ as a capTable runtime key must be rejected loudly');
assert.equal(capErr.key, '__proto__');
assert.ok(!rCapTable.ok);
});
test('rejectsTraversalInCapPattern', () => {
for (const pattern of ['../workflows/*.md', '/workflows/*.md']) {
const capTable = { windsurf: [{ pattern, maxBytes: WINDSURF_CAP, note: 'x' }] };
const r = evaluateEmittedCaps({ sizes: { windsurf: { 'workflows/a.md': 5 } }, capTable });
const err = r.errors.find((e) => e.reason === REASON.UNSAFE_PATTERN);
assert.ok(err, `"${pattern}" must be rejected`);
assert.equal(err.pattern, pattern);
assert.ok(!r.ok);
}
});
test('errorsOnRuntimeWithEmptyArtifactSet', () => {
const r = evaluateEmittedCaps({
sizes: { windsurf: {}, claude: { 'foo.md': 5 } },
});
const err = r.errors.find((e) => e.reason === REASON.NO_ARTIFACTS);
assert.ok(err, 'must not be excused just because another runtime has real content');
assert.equal(err.runtime, 'windsurf');
assert.equal(r.unmeasured.length, 1, 'the other runtime is still processed normally');
assert.equal(r.unmeasured[0].runtime, 'claude');
assert.ok(!r.ok);
});
// ─── A21-A23: shipping shape, idempotence, determinism ───────────────────────
test('usesShippingCallerShapeWithNoOptions', () => {
const sizes = { windsurf: { 'workflows/a.md': 100 } };
const withNoOptions = evaluateEmittedCaps({ sizes });
const withExplicitDefault = evaluateEmittedCaps({ sizes, capTable: EMITTED_CAPS });
assert.deepEqual(withNoOptions, withExplicitDefault);
});
test('isIdempotentAcrossRepeatedEvaluation', () => {
const sizes = { windsurf: { 'workflows/a.md': WINDSURF_CAP + 1 }, claude: { 'x.md': 5 } };
const capTable = { windsurf: [{ pattern: WINDSURF_PATTERN, maxBytes: WINDSURF_CAP, note: 'x' }] };
const sizesBefore = JSON.stringify(sizes);
const capTableBefore = JSON.stringify(capTable);
const r1 = evaluateEmittedCaps({ sizes, capTable });
const r2 = evaluateEmittedCaps({ sizes, capTable });
assert.deepEqual(r1, r2);
assert.equal(JSON.stringify(sizes), sizesBefore, 'sizes must not be mutated');
assert.equal(JSON.stringify(capTable), capTableBefore, 'capTable must not be mutated');
});
test('returnsViolationsInStableSortedOrder', () => {
const capTable = {
zeta: [{ pattern: '*.md', maxBytes: 1, note: 'x' }],
alpha: [{ pattern: '*.md', maxBytes: 1, note: 'x' }],
};
// Inserted deliberately out of sorted order so the assertion bites.
const r = evaluateEmittedCaps({
sizes: { zeta: { 'z.md': 99 }, alpha: { 'a.md': 99 } },
capTable,
});
assert.equal(r.violations.length, 2);
assert.deepEqual(r.violations.map((v) => v.runtime), ['alpha', 'zeta']);
});
// ─── A24-A25: fast-check property tests ──────────────────────────────────────
test('propertyUnderCapNeverViolates', () => {
fc.assert(
fc.property(
fc.nat({ max: 50000 }),
fc.nat({ max: 50000 }),
(bytes, maxBytes) => {
const capTable = { windsurf: [{ pattern: 'workflows/probe.md', maxBytes, note: 'x' }] };
const r = evaluateEmittedCaps({ sizes: { windsurf: { 'workflows/probe.md': bytes } }, capTable });
assert.equal(r.errors.length, 0);
for (const v of r.violations) {
assert.ok(v.bytes > v.cap, 'no artifact <= its cap may ever appear in violations');
}
if (bytes <= maxBytes) {
assert.equal(r.violations.length, 0);
assert.equal(r.compliant.length, 1);
} else {
assert.equal(r.violations.length, 1);
assert.equal(r.compliant.length, 0);
}
},
),
);
});
test('propertyEveryArtifactLandsInExactlyOneBucket', () => {
const runtimeArb = fc.constantFrom('windsurf', 'cursor', 'claude', 'trae', 'roo');
const relArb = fc
.tuple(
fc.constantFrom('workflows', 'agents', 'skills', 'commands'),
fc.constantFrom('alpha', 'beta', 'gamma', 'delta', 'epsilon'),
fc.constantFrom('md', 'yaml', 'toml'),
)
.map(([dir, name, ext]) => `${dir}/${name}.${ext}`);
fc.assert(
fc.property(
fc.array(
fc.record({ runtime: runtimeArb, rel: relArb, bytes: fc.nat({ max: 20000 }) }),
{ minLength: 0, maxLength: 25 },
),
fc.array(
fc.record({
runtime: runtimeArb,
patternKind: fc.constantFrom('exact', 'wildcard'),
dir: fc.constantFrom('workflows', 'agents', 'skills', 'commands'),
maxBytes: fc.nat({ max: 20000 }),
}),
{ minLength: 0, maxLength: 8 },
),
(entries, ruleSpecs) => {
const sizes = {};
const expectedKeys = new Set();
for (const { runtime, rel, bytes } of entries) {
sizes[runtime] = sizes[runtime] || {};
sizes[runtime][rel] = bytes;
expectedKeys.add(`${runtime}::${rel}`);
}
// Every runtime named in sizes must have at least one artifact, or
// evaluateEmittedCaps correctly reports NO_ARTIFACTS instead of
// conserving it — filtered out here since the property is about the
// WELL-FORMED subset (see the module's conservation-law comment).
for (const runtime of Object.keys(sizes)) {
if (Object.keys(sizes[runtime]).length === 0) delete sizes[runtime];
}
fc.pre(Object.keys(sizes).length > 0); // nothing to conserve this run
const capTable = {};
for (const { runtime, patternKind, dir, maxBytes } of ruleSpecs) {
capTable[runtime] = capTable[runtime] || [];
const pattern = patternKind === 'exact' ? `${dir}/fixed.md` : `${dir}/*.md`;
capTable[runtime].push({ pattern, maxBytes, note: 'property' });
}
// Only reference runtimes that are actually present in sizes, so this
// run never trips UNKNOWN_RUNTIME noise unrelated to the conservation
// law under test.
for (const runtime of Object.keys(capTable)) {
if (!Object.prototype.hasOwnProperty.call(sizes, runtime)) delete capTable[runtime];
}
const r = evaluateEmittedCaps({ sizes, capTable });
const seen = new Set();
for (const bucket of [r.violations, r.unmeasured, r.compliant]) {
for (const rec of bucket) {
const key = `${rec.runtime}::${rec.rel}`;
assert.ok(!seen.has(key), `${key} appeared in more than one bucket`);
seen.add(key);
}
}
assert.deepEqual([...seen].sort(), [...expectedKeys].sort());
},
),
);
});

View File

@@ -18,8 +18,7 @@
* 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. Two real instances of that class were caught while
* building this table (Copilot's `<name>.agent.md` rename and Copilot's
* `hooks/msd-session.json`), both of which passed totality
* building this table, 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.
*

View File

@@ -573,7 +573,7 @@ describe('#3177: matrix section extraction is bounded by its heading', () => {
assert.equal(matrixField(doc, 'claude', '__definitely_not_a_field__'), null);
assert.notEqual(
matrixField(doc, 'claude', 'effortSurface'),
matrixField(doc, 'kilo', 'effortSurface'),
matrixField(doc, 'antigravity', 'effortSurface'),
'two hosts with different values must not resolve to the same cell',
);
});

View File

@@ -227,7 +227,7 @@ describe('execute-phase completion reconciliation (#4217 — split A of #3754)',
assert.ok(content.includes('spot-check'), 'execute-phase must keep spot-check fallback vocabulary');
assert.ok(
content.includes('sequential inline execution'),
'execute-phase must keep the Copilot sequential inline fallback wording'
'execute-phase must keep the sequential inline fallback wording'
);
});

View File

@@ -7,9 +7,9 @@
*
* That made it the only reviewer seeing anything beyond the prompt file: the prompt is assembled
* once (PROJECT.md, the roadmap section, every PLAN file, CONTEXT.md, RESEARCH.md, REQUIREMENTS.md)
* before any lane runs, qwen receives only that prompt, and codex runs `--ephemeral`. Beyond the
* before any lane runs, the other lanes receive only that prompt, and codex runs `--ephemeral`. Beyond the
* measured injection cost, the asymmetry cuts at the workflow's premise — "independent review"
* meant something different for the claude lane than for the other two.
* meant something different for the claude lane than for the others.
*
* The fix is declared data, not a handler: the claude lane carries `invoke.env`, the resolver folds
* it into the plan, and the runner merges it over the inherited environment for that ONE child.
@@ -121,7 +121,7 @@ describe('#2483 the claude reviewer lane suppresses CLAUDE.md + auto-memory inje
'the claude lane must declare BOTH CLAUDE_CODE_DISABLE_CLAUDE_MDS=1 and ' +
'CLAUDE_CODE_DISABLE_AUTO_MEMORY=1 — CLAUDE.md loading and auto-memory are ' +
'independently-toggled mechanisms, and a lane missing either re-inherits that half of the ' +
'context, reintroducing the asymmetry against the prompt-fed qwen and codex lanes'
'context, reintroducing the asymmetry against the prompt-fed lanes'
);
});

View File

@@ -8,10 +8,9 @@
*
* The committed artifact is plain JSON (not a `.cjs` CommonJS module): a
* shipped runtime module is the wrong place for ~120 KB of arbitrary
* CONTEXT.md prose, and embedding it there tripped both
* tests/cline-install.test.cjs (leaked `.claude/hooks/...` path literals) and
* CONTEXT.md prose, and embedding it there tripped
* tests/package-name-single-source.test.cjs (hardcoded package-name
* literals) — both true positives against runtime-code content scanning.
* literals) — a true positive against runtime-code content scanning.
* docs/CONTEXT-INDEX.json mirrors docs/INVENTORY-MANIFEST.json's precedent:
* a committed, generated, `--check`-guarded JSON manifest that is not
* runtime code.

View File

@@ -278,11 +278,9 @@ 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: several runtimes present in RUNTIME_META
// (e.g. hermes, zcode) were missing from it, so `scope: 'local'`
// (e.g. 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
// left the divergence itself intact for the next runtime to rediscover.
// TypeError naming neither the runtime nor the map at fault.
//
// Same anti-divergence pattern as the buildParityManifest guard above: the
// duplicate is gone, and this asserts it does not come back.

View File

@@ -126,7 +126,7 @@ function configLocationEnvKeys() {
// 1. Every runtime descriptor the capability registry carries — including
// the nested skillsHome descriptor, which resolves independently of
// configHome (resolveSkillsBaseFromDescriptor) and can carry its own
// env array. Inert today (only kilo declares skillsHome, with env: []),
// env array. Inert today (no runtime declares skillsHome env),
// but walking configHome.env alone is the identical gap-shape this PR
// closed twice already, one field over. (#2665 round 4)
...Object.values(runtimes).flatMap((r) => r?.runtime?.configHome?.env ?? []),

View File

@@ -1,462 +0,0 @@
'use strict';
/**
* emitted-caps.cjs — the per-runtime emitted-byte cap decision (issue #2931,
* epic #1671, Phase 4). Sibling law to `emitted-diff.cjs`'s conservation law:
* same pure/IO split, same error-accumulation shape, same reserved-key guard.
*
* ── Why this module is pure ──────────────────────────────────────────────────
* No fs, no git, no clock, no process. The expensive part — spawning an
* install and measuring real emitted bytes — lives elsewhere; this module only
* decides, given a `{ [runtime]: { [rel]: bytes } }` map and a cap table,
* which artifacts violate, which are unmeasured, and which comply. Keeping the
* decision pure makes every boundary (`cap-1`/`cap`/`cap+1`) a millisecond
* table test and keeps the Stryker gate able to bite.
*
* ── Why the cap table is HARD-CODED here, not read from capability.json ─────
* ADR-2719's guiding principle (already load-bearing in `emitted-diff.cjs`:
* it never re-derives a byte, because asserting `emitted == transform(source)`
* is the tautology ADR-2264's Amendment rejected) applies here too, one level
* up: a cap that is DERIVED from the same descriptor it is meant to guard
* (`capabilities/<rt>/capability.json`) would silently follow any edit to that
* descriptor. Bump the descriptor, the guard bumps with it, and a real
* regression sails through unnoticed. `EMITTED_CAPS` is a second, independent
* source of truth, edited deliberately by a human who has to look Windsurf's
* actual 12,000-byte platform limit in the eye — exactly the friction a guard
* exists to provide.
*/
// ─── REASON enum ───────────────────────────────────────────────────────────
const REASON = Object.freeze({
CAP_EXCEEDED: 'cap_exceeded',
DEAD_RULE: 'dead_rule',
UNKNOWN_RUNTIME: 'unknown_runtime',
NO_ARTIFACTS: 'no_artifacts',
INVALID_SIZES: 'invalid_sizes',
INVALID_CAP_TABLE: 'invalid_cap_table',
INVALID_CAP_VALUE: 'invalid_cap_value',
RESERVED_KEY: 'reserved_key',
UNSAFE_PATTERN: 'unsafe_pattern',
});
/**
* Keys that can never legitimately name a runtime or an emitted path, and
* that also happen to be the JS-object footguns. Mirrors
* `emitted-diff.cjs`'s `RESERVED_ACK_KEYS`: rejected LOUDLY (REASON.RESERVED_KEY),
* never silently dropped.
*/
const RESERVED_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
/**
* The declared per-runtime emitted-byte cap table. Exactly one real entry
* today: Windsurf hard-caps a workspace workflow file at 12,000 bytes.
*
* Shape: `{ [runtime]: Array<{ pattern, maxBytes, note }> }`. `pattern` is a
* simple glob, evaluated against the emitted-relative path: `*` matches
* within one path segment (never crosses `/`), `**` matches across segments.
* Frozen two levels deep so a test cannot mutate the shipped table out from
* under a later assertion in the same run.
*/
const EMITTED_CAPS = Object.freeze({
windsurf: Object.freeze([
Object.freeze({
pattern: 'workflows/*.md',
maxBytes: 12000,
note: 'Windsurf hard-caps workspace workflow files at 12,000 bytes.',
}),
]),
});
// ─── Small predicates ────────────────────────────────────────────────────────
function isPlainObject(value) {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
function typeNameOf(value) {
if (value === null) return 'null';
if (Array.isArray(value)) return 'array';
return typeof value;
}
/** A legal byte count or cap value: a `number`, never a coerced string, never
* negative, NaN, Infinity, or fractional. `Number.isSafeInteger` alone
* already excludes NaN/Infinity/non-integers; `>= 0` excludes negatives. */
function isNonNegativeSafeInteger(value) {
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
}
/** `..` traversal or a leading `/` in a cap-table pattern is never legitimate
* — every emitted-relative path in this repo is already relative and
* segment-clean, so either shape can only be an authoring mistake or a
* hostile table entry. */
function isUnsafePattern(pattern) {
return pattern.includes('..') || pattern.startsWith('/');
}
/**
* Translate a `pattern` (already validated safe) into an anchored RegExp.
* `*` -> one path segment (`[^/]*`); `**` -> across segments (`.*`). Every
* other character is escaped, so a pattern is never accidentally read as a
* richer regex than the two glob tokens it declares.
*/
function compileGlobPattern(pattern) {
let out = '';
for (let i = 0; i < pattern.length; i += 1) {
const ch = pattern[i];
if (ch === '*' && pattern[i + 1] === '*') {
out += '.*';
i += 1;
} else if (ch === '*') {
out += '[^/]*';
} else if (/[.+^${}()|[\]\\]/.test(ch)) {
out += `\\${ch}`;
} else {
out += ch;
}
}
return new RegExp(`^${out}$`);
}
/** Stable comparator: runtime, then rel/pattern. Plain `<`/`>` on strings,
* not `localeCompare`, so ordering is locale-independent and reproducible. */
function byRuntimeThen(field) {
return (a, b) => {
if (a.runtime !== b.runtime) return a.runtime < b.runtime ? -1 : 1;
if (a[field] === b[field]) return 0;
return a[field] < b[field] ? -1 : 1;
};
}
// ─── The decision ────────────────────────────────────────────────────────────
/**
* Evaluate every measured emitted artifact against the declared cap table.
*
* ── The conservation law (by construction) ───────────────────────────────
* Every `(runtime, rel)` key in `sizes` whose byte value is a legal
* non-negative safe integer, and whose `runtime`/`rel` are not reserved
* keys, is placed into EXACTLY ONE of `violations`, `unmeasured`, or
* `compliant` — the single walk below assigns each such key to precisely one
* push. A key excluded by a RESERVED_KEY or INVALID_SIZES error is not a
* legitimate size measurement at all (its own error already names it), so it
* is not counted as a fourth bucket; the law is over the WELL-FORMED subset,
* exactly as `diffEmitted`'s conservation law is over the entries `parseAck`
* accepted.
*
* Internal bookkeeping (which rule matched which path, for dead-rule
* detection) is kept as index-based tracking, never as an object keyed by an
* external string — so a hostile `rel`/`runtime` value cannot pollute
* anything even transiently.
*
* @param {object} opts
* @param {object} opts.sizes { [runtime]: { [rel]: bytes } }
* @param {object} [opts.capTable] defaults to the shipped `EMITTED_CAPS`
* @returns {{
* violations: Array, unmeasured: Array, compliant: Array,
* deadRules: Array, errors: Array, ok: boolean
* }}
*/
function evaluateEmittedCaps({ sizes, capTable = EMITTED_CAPS } = {}) {
const errors = [];
if (sizes === null || sizes === undefined) {
errors.push({
reason: REASON.INVALID_SIZES,
receivedType: sizes === null ? 'null' : 'undefined',
message: `sizes is required, got ${sizes === null ? 'null' : 'undefined'}`,
});
} else if (!isPlainObject(sizes)) {
errors.push({
reason: REASON.INVALID_SIZES,
receivedType: typeNameOf(sizes),
message: `sizes must be a plain object keyed by runtime, got ${typeNameOf(sizes)}`,
});
}
if (capTable === null || capTable === undefined || !isPlainObject(capTable)) {
errors.push({
reason: REASON.INVALID_CAP_TABLE,
receivedType: typeNameOf(capTable),
message: `capTable must be a plain object keyed by runtime, got ${typeNameOf(capTable)}`,
});
}
if (errors.length) {
return { violations: [], unmeasured: [], compliant: [], deadRules: [], errors, ok: false };
}
const violations = [];
const unmeasured = [];
const compliant = [];
const deadRules = [];
// ── capTable validation, once, up front — never at compare time ──────────
// Maps `runtime -> Array<{ regex, maxBytes, note, pattern, matches: number }>`
// for runtimes that ARE known (present in `sizes`) and whose rules parsed
// cleanly. Keyed on `runtime`, but only ever assigned via `Map.set`, never
// bracket-property assignment — a hostile `__proto__` runtime name cannot
// pollute anything here either.
const compiledRules = new Map();
const knownRuntimeNoArtifacts = new Set();
for (const runtime of Object.keys(capTable)) {
if (RESERVED_KEYS.has(runtime)) {
errors.push({
reason: REASON.RESERVED_KEY,
scope: 'capTable-runtime',
key: runtime,
message: `capTable key "${runtime}" is reserved and can never be a real runtime name`,
});
continue;
}
if (!Object.prototype.hasOwnProperty.call(sizes, runtime)) {
// A cap declared for a runtime that never even reported sizes. Not
// installable/measured in this run at all — a hard error, distinct
// from "measured but produced zero files" (REASON.NO_ARTIFACTS below).
errors.push({
reason: REASON.UNKNOWN_RUNTIME,
runtime,
message: `capTable declares caps for "${runtime}", but sizes has no entry for it`,
});
continue;
}
const rules = capTable[runtime];
if (!Array.isArray(rules)) {
errors.push({
reason: REASON.INVALID_CAP_TABLE,
runtime,
message: `capTable.${runtime} must be an array of rules, got ${typeNameOf(rules)}`,
});
continue;
}
const parsed = [];
rules.forEach((rule, index) => {
if (!isPlainObject(rule)) {
errors.push({
reason: REASON.INVALID_CAP_TABLE,
runtime,
index,
message: `capTable.${runtime}[${index}] must be an object with pattern/maxBytes`,
});
return;
}
const { pattern, maxBytes, note } = rule;
let patternOk = true;
if (typeof pattern !== 'string' || pattern.length === 0) {
errors.push({
reason: REASON.INVALID_CAP_TABLE,
runtime,
index,
message: `capTable.${runtime}[${index}].pattern must be a non-empty string`,
});
patternOk = false;
} else if (isUnsafePattern(pattern)) {
errors.push({
reason: REASON.UNSAFE_PATTERN,
runtime,
pattern,
message: `capTable.${runtime}[${index}].pattern "${pattern}" contains a ".." traversal or a leading "/"`,
});
patternOk = false;
}
let capOk = true;
if (!isNonNegativeSafeInteger(maxBytes)) {
errors.push({
reason: REASON.INVALID_CAP_VALUE,
runtime,
pattern: typeof pattern === 'string' ? pattern : null,
value: maxBytes,
message:
`capTable.${runtime}[${index}].maxBytes must be a non-negative safe integer, `
+ `got ${JSON.stringify(maxBytes)} (${typeNameOf(maxBytes)})`,
});
capOk = false;
}
if (patternOk && capOk) {
parsed.push({
pattern,
regex: compileGlobPattern(pattern),
maxBytes,
note: typeof note === 'string' ? note : undefined,
matches: 0,
});
}
});
compiledRules.set(runtime, parsed);
}
// ── Walk sizes, sorted, and assign every well-formed key exactly once ────
for (const runtime of Object.keys(sizes).sort()) {
if (RESERVED_KEYS.has(runtime)) {
errors.push({
reason: REASON.RESERVED_KEY,
scope: 'sizes-runtime',
key: runtime,
message: `sizes key "${runtime}" is reserved and can never be a real runtime name`,
});
continue;
}
const artifacts = sizes[runtime];
if (!isPlainObject(artifacts)) {
errors.push({
reason: REASON.INVALID_SIZES,
runtime,
message: `sizes.${runtime} must be a plain object of { rel: bytes }, got ${typeNameOf(artifacts)}`,
});
continue;
}
const rels = Object.keys(artifacts);
if (rels.length === 0) {
errors.push({
reason: REASON.NO_ARTIFACTS,
runtime,
message: `sizes.${runtime} produced no artifacts — never read "nothing to check" as "pass"`,
});
knownRuntimeNoArtifacts.add(runtime);
continue;
}
const rules = compiledRules.get(runtime) || [];
for (const rel of rels.sort()) {
if (RESERVED_KEYS.has(rel)) {
errors.push({
reason: REASON.RESERVED_KEY,
scope: 'sizes-rel',
runtime,
key: rel,
message: `sizes.${runtime} key "${rel}" is reserved and can never be a real emitted path`,
});
continue;
}
const bytes = artifacts[rel];
if (!isNonNegativeSafeInteger(bytes)) {
errors.push({
reason: REASON.INVALID_SIZES,
runtime,
rel,
message:
`sizes.${runtime}["${rel}"] must be a non-negative safe integer, `
+ `got ${JSON.stringify(bytes)} (${typeNameOf(bytes)})`,
});
continue;
}
const rule = rules.find((r) => r.regex.test(rel));
if (!rule) {
unmeasured.push({ runtime, rel, bytes });
continue;
}
rule.matches += 1;
if (bytes <= rule.maxBytes) {
compliant.push({ runtime, rel, bytes, cap: rule.maxBytes });
} else {
violations.push({
runtime,
rel,
bytes,
cap: rule.maxBytes,
delta: bytes - rule.maxBytes,
reason: REASON.CAP_EXCEEDED,
note: rule.note,
});
}
}
}
// ── Dead rules: matched nothing across the WHOLE run ──────────────────────
// Skipped for runtimes already flagged UNKNOWN_RUNTIME or NO_ARTIFACTS —
// those are more specific, more actionable errors, and a rule for a
// runtime with zero measured artifacts trivially matches nothing for a
// reason this walk already named.
for (const [runtime, rules] of compiledRules) {
if (knownRuntimeNoArtifacts.has(runtime)) continue;
for (const rule of rules) {
if (rule.matches === 0) {
deadRules.push({
runtime,
pattern: rule.pattern,
cap: rule.maxBytes,
reason: REASON.DEAD_RULE,
message: `capTable.${runtime} rule "${rule.pattern}" matched zero emitted paths — a cap guarding nothing is rot`,
});
}
}
}
violations.sort(byRuntimeThen('rel'));
unmeasured.sort(byRuntimeThen('rel'));
compliant.sort(byRuntimeThen('rel'));
deadRules.sort(byRuntimeThen('pattern'));
const ok = errors.length === 0 && violations.length === 0 && deadRules.length === 0;
return { violations, unmeasured, compliant, deadRules, errors, ok };
}
// ─── Rendering ────────────────────────────────────────────────────────────────
/**
* Pure renderer. Tests assert on `evaluateEmittedCaps`'s structured result,
* never on this string (CONTRIBUTING.md, "Prohibited: Raw Text Matching on
* Test Outputs").
*
* The CAP_EXCEEDED message deliberately warns against the Goodhart's-Law
* escape hatch: moving bytes behind an EAGERLY `@`-imported reference file
* changes where the bytes are typed, not how many bytes load. An eager
* `@`-import is inlined at load time, so the cap would read green while the
* runtime still pays every byte — gaming the metric, not complying with it
* (this exact failure mode is recorded in CONTEXT.md
* `RULESET.WORKFLOW_SIZE_BUDGET`).
*/
function formatCapReport(result) {
const parts = [];
if (result.errors.length) {
parts.push(
`${result.errors.length} error(s):\n ${result.errors.map((e) => e.message).join('\n ')}`,
);
}
if (result.violations.length) {
const list = result.violations.map(
(v) => ` ${v.runtime}: ${v.rel} is ${v.bytes} bytes — exceeds the ${v.cap}-byte cap by ${v.delta}`
+ (v.note ? ` (${v.note})` : ''),
);
parts.push(
`${result.violations.length} emitted artifact(s) exceed their declared cap:\n${list.join('\n')}\n\n`
+ 'Moving these bytes behind an EAGERLY `@`-imported reference file is gaming this '
+ 'metric, not complying with it: an eager @-import is inlined before the cap is ever '
+ 'measured, so the runtime still pays every byte at load time (CONTEXT.md '
+ 'RULESET.WORKFLOW_SIZE_BUDGET). The fix is to reduce what actually loads.',
);
}
if (result.deadRules.length) {
const list = result.deadRules.map((r) => ` ${r.runtime}: "${r.pattern}" (cap ${r.cap}) matched nothing`);
parts.push(`${result.deadRules.length} cap rule(s) matched zero emitted paths — a cap guarding nothing is rot:\n${list.join('\n')}`);
}
return parts.join('\n\n');
}
module.exports = {
REASON,
EMITTED_CAPS,
evaluateEmittedCaps,
formatCapReport,
};

View File

@@ -59,8 +59,7 @@ const { MANIFEST_FAMILIES, runMinimalInstall, buildParityManifest } = require('.
const EXPECTED_MANIFEST_COUNT = MANIFEST_FAMILIES.length;
// ─── Emitted roots ────────────────────────────────────────────────────────────
// Longest-first: `skills/msd` (hermes category dir) must win over `skills` for
// `skills/msd/...`, and `.agents/skills` must win over `.agents`.
// Longest-first: `.agents/skills` must win over `.agents`.
const SKILLS_ROOTS = ['.agents/skills', 'skills'];
const HOOKS_ROOTS = ['hooks'];
@@ -68,16 +67,10 @@ const HOOKS_ROOTS = ['hooks'];
/** Source-of-truth command dir every skill/command surface converts from. */
const COMMANDS_SRC = 'commands/msd';
/** Installer source file that emits the Cline/AGENTS.md instruction bodies as
* code literals (buildClineRulesBody / buildClineAgentsMdBody /
* buildClinePreToolUseHook). */
const CLINE_BODY_SRC = 'src/runtime-hooks-surface.cts';
/**
* Installer source file that GENERATES the Windows-only `hooks/<name>.cmd` shim
* wrapping a Codex hook's `.js` script (#3426). Same physical file as
* CLINE_BODY_SRC — kept as its own named constant because the two constants
* attribute unrelated transform code that happens to live in one file:
* Transform code:
* buildCodexHookWindowsShimIR / ensureCodexHooksJsonSessionStart /
* ensureCodexHooksJsonEvent (verified via Memtrace: these are the ONLY writers
* of a `.cmd` file anywhere in the installer — no other runtime's hook surface
@@ -85,8 +78,7 @@ const CLINE_BODY_SRC = 'src/runtime-hooks-surface.cts';
*/
const HOOKS_WINDOWS_SHIM_SRC = 'src/runtime-hooks-surface.cts';
/** Installer source file that emits the Hermes skill-category DESCRIPTION.md
* (writeHermesCategoryDescription) as a code literal. */
/** Installer source file that emits shared code-literal artifacts. */
const INSTALLER_SRC = 'bin/install.js';
/** Module owning the #2544 `{"type":"commonjs"}` marker literal and the
@@ -97,9 +89,6 @@ const COMMONJS_MARKER_SRC = 'src/commonjs-marker.cts';
* beside it (_installNativePluginIfDeclared). */
const INSTALL_ENGINE_SRC = 'src/install-engine.cts';
/** Source file holding the Kimi root-agent literal (runtime-artifact-layout.cts:303). */
const KIMI_ROOT_AGENT_SRC = 'src/runtime-artifact-layout.cts';
/**
* Transform sources for the per-runtime agent-content pipeline (#2757).
*
@@ -187,7 +176,7 @@ const ZCODE_BODY_TRANSFORM_SRCS = [
'bin/install.js',
];
// #4482: every non-Copilot runtime filters audience-specific notes through the
// #4482: every runtime filters audience-specific notes through the
// conversion module and the published installer copy path.
const RUNTIME_NOTE_FILTER_TRANSFORM_SRCS = [
'src/runtime-artifact-conversion.cts',
@@ -282,7 +271,7 @@ const PROVENANCE_RULES = [
{
id: 'msd-core-verbatim',
// Markdown payloads pass through copyWithPathReplacement's audience
// filter for non-Copilot runtimes. Non-Markdown payloads remain byte-for-
// filter for every runtime. Non-Markdown payloads remain byte-for-
// byte copies, but one rule has one kind; the match-specific transform
// list below keeps the causal attribution precise.
kind: 'derived',
@@ -343,10 +332,7 @@ const PROVENANCE_RULES = [
// `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-<name>.md`, so excluding the bare `msd.md` is precise.
// `.agent.md` is likewise excluded — Copilot emits a RENAMED copy
// (`<name>.agent.md`) whose source is `agents/<name>.md`; matching it here
// resolved to a file that does not exist. Same false-attribution class.
pattern: /^(?!msd\.md$)(?!.*\.agent\.md$)[^/]+\.md$/,
pattern: /^(?!msd\.md$)[^/]+\.md$/,
sources: (m) => [`agents/${m[0]}`],
transforms: AGENT_TRANSFORM_SRCS,
},
@@ -381,11 +367,8 @@ const PROVENANCE_RULES = [
roots: HOOKS_ROOTS,
// Emitted from hooks/dist/, which scripts/build-hooks.js builds from hooks/.
// Attribute to the REPO source a PR actually edits, not the build artifact.
// Excludes Copilot's hook-registration JSON (next rule) — that is a code
// literal, not a built script, and attributing it here resolved to a
// nonexistent `hooks/msd-session.json`. `package.json` is excluded for the
// same reason (the #2544 `commonjs-marker` rule below): there is no
// `hooks/package.json` in the repo to attribute to.
// `package.json` is excluded (the #2544 `commonjs-marker` rule below):
// there is no `hooks/package.json` in the repo to attribute to.
//
// `.cmd` shims are a SEPARATE, Windows-only emission path folded into this
// SAME rule rather than a dedicated one (see the `transforms` doc above for
@@ -400,11 +383,11 @@ const PROVENANCE_RULES = [
// same source file. The wrapped `.js` file's NAME flows into the `.cmd`
// bytes; its CONTENT never does — see the `sources` comment below for why
// that rules out attributing to `hooks/<name>.js`.
pattern: /^(?!msd-session\.json$|package\.json$).+$/,
pattern: /^(?!package\.json$).+$/,
// `package.json` (the CommonJS marker) is excluded here and owned by the
// dedicated `commonjs-marker` rule below. #2717 attributed it inside THIS
// rule, routing it to HOOKS_WINDOWS_SHIM_SRC because at that point
// src/runtime-hooks-surface.cts was its only emitter (cursor/windsurf, plus
// src/runtime-hooks-surface.cts was its only emitter (cursor, plus
// the codex copy block calling the same exported helper). #2544 adds a
// second emitter — src/commonjs-marker.cts, via installSharedHooksBundle and
// _installNativePluginIfDeclared — and two roots this rule does not cover
@@ -418,8 +401,7 @@ const PROVENANCE_RULES = [
// source file), but its CONTENT never does, so attributing `sources` to
// `hooks/<name>.js` would be a false byte-provenance claim. Point `sources`
// at the same file as `transforms`, matching the `code-derived` convention
// used elsewhere in this table (copilot-hook-registration, cline-rules-
// code-derived, hermes-category-description). The redundancy between
// used elsewhere in this table. The redundancy between
// `sources` and `transforms` here is harmless — the mis-attribution was not.
// No `package.json` arm here: the pattern above excludes it, so the branch
// #2717 added for it is unreachable from this rule.
@@ -434,8 +416,8 @@ const PROVENANCE_RULES = [
roots: [...HOOKS_ROOTS, 'plugins', 'extensions'],
// #2544: a `{"type":"commonjs"}` module-type marker, written as a code
// literal so Node's ancestor walk resolves MSD's staged `.js` files as
// CommonJS under an ambient `"type": "module"`. Like the Copilot
// registration JSON above it is emitted, never built — there is no
// CommonJS under an ambient `"type": "module"`. It is emitted,
// never built — there is no
// `hooks/package.json` or `plugins/package.json` in the repo, so the
// built-script rule would attribute it to a path that does not exist.
// Sources are scoped PER ROOT, not declared as one flat union. Each root has
@@ -465,13 +447,8 @@ const PROVENANCE_RULES = [
if (root === 'plugins' || root === 'extensions') {
return [COMMONJS_MARKER_SRC, INSTALL_ENGINE_SRC];
}
// 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];
}
// 'hooks' — written by the shared bundle for most runtimes and by the
// #2717 dedicated paths for cursor/windsurf/codex.
// #2717 dedicated paths for cursor/codex.
return [COMMONJS_MARKER_SRC, INSTALLER_SRC, HOOKS_WINDOWS_SHIM_SRC];
},
},
@@ -533,8 +510,7 @@ const PROVENANCE_RULES = [
kind: 'descriptor',
roots: ['plugins', 'extensions'],
pattern: /^[^/]+\.(js|cjs|mjs)$/,
// Source is per-runtime (opencode -> .opencode/…, kilo -> .kilo/…,
// pi -> pi/msd.cjs), so attribution is a function of (rel, runtime).
// Source is per-runtime (opencode -> .opencode/…), so attribution is a function of (rel, runtime).
sources: (m, ctx) => {
const np = nativePluginDescriptor(ctx.runtime);
if (!np || !np.source) {
@@ -554,8 +530,7 @@ const PROVENANCE_RULES = [
// ^AGENTS\.md$) and `synthesized-msd-defaults` (^\.msd/defaults\.json$):
// both paths only appeared in a manifest while the harness collapsed
// configDir onto the sandbox HOME, walking HOME-level siblings
// (cline's ~/.agents/AGENTS.md, every non-Claude runtime's
// ~/.msd/defaults.json). With the harness installing into each runtime's
// (every non-Claude runtime's ~/.msd/defaults.json). With the harness installing into each runtime's
// 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
@@ -863,13 +838,11 @@ module.exports = {
EXPECTED_MANIFEST_COUNT,
PROVENANCE_RULES,
SKILLS_ROOTS,
KIMI_ROOT_AGENT_SRC,
AGENT_TRANSFORM_SRCS,
RUNTIME_NOTE_FILTER_TRANSFORM_SRCS,
SOURCE_PREFIX_SUFFIX,
HOOKS_ROOTS,
COMMANDS_SRC,
CLINE_BODY_SRC,
HOOKS_WINDOWS_SHIM_SRC,
INSTALLER_SRC,
stripSkillPrefix,

View File

@@ -163,7 +163,7 @@ const VOLATILE_FILES = new Set([
// varies by platform and cannot be normalized to a single sentinel reliably.
// Their content is asserted directly by the dedicated hook tests
// (install-minimal-hooks, sh-hook-paths, codex-config, etc.). Matched by basename.
// settings.json = Claude/Antigravity/Augment/etc. hook surface; hooks.json =
// settings.json = Claude/Antigravity hook surface; hooks.json =
// Codex/Cursor hook surface — both embed the platform-varying node-runner command.
// settings.local.json = Claude LOCAL hook surface (#338): same platform-varying
// node-runner command as settings.json, so excluded for the same reason (#2086).
@@ -235,8 +235,7 @@ const SEMVER_ISH_RE = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/;
* - `"version": "<ver>"` — JSON manifests (plugin/extension/capability-style)
* embedding the package version as a string field.
* - `version: "<ver>"` / `version: <ver>` — YAML frontmatter version fields (e.g.
* skill frontmatter's `yamlQuote(pkg.version)`, Hermes' category
* `DESCRIPTION.md`).
* skill frontmatter's `yamlQuote(pkg.version)`).
* - `@golem15/msd-core@<ver>` — pinned package-spec references.
* - a file whose ENTIRE trimmed content IS the version (`msd-core/VERSION`).
* Each pattern only matches when the version in the content EQUALS the supplied
@@ -605,7 +604,7 @@ function runMinimalInstall({ runtime, scope, extraArgs = [], installScript = INS
// includes rewritten on live installs, zero manifest/fixture diffs). The
// explicit flag stays: hermeticity-by-override is immune to ambient
// redirect envs (CI runners export XDG_CONFIG_HOME, which redefines the
// opencode/kilo XDG descriptors' resolution when no explicit dir wins).
// opencode XDG descriptor's resolution when no explicit dir wins).
const globalMeta = RUNTIME_META[runtime];
if (!globalMeta || !globalMeta.globalSuffix) {
// #3023 lesson: a silent `path.join(root, undefined)` here throws a
@@ -623,21 +622,18 @@ 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 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
// adding one more entry, which left the divergence itself in place; the
// table is now single-source so a new runtime cannot reintroduce it.
// `cline` keeps its ternary: its local artifacts land at the project root
// itself, which is a genuine exception rather than a directory name.
// That duplicate map was missing several runtimes (e.g. 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; the table is now single-source so a new runtime cannot
// reintroduce it.
const localMeta = RUNTIME_META[runtime];
if (runtime !== 'cline' && (!localMeta || !localMeta.localDir)) {
if (!localMeta || !localMeta.localDir) {
throw new Error(
`runMinimalInstall: no RUNTIME_META.localDir for runtime "${runtime}" — refusing to guess a local config dir (#3031)`,
);
}
configDir = runtime === 'cline' ? root : path.join(root, localMeta.localDir);
configDir = path.join(root, localMeta.localDir);
}
args.push(...extraArgs);
const result = runNode(args, {
@@ -680,7 +676,7 @@ function manifestSkillSet(manifest) {
const seg = key.split('/')[1].replace(/^msd-/, '').replace(/\.md$/, '');
out.add(seg);
} else if (key.startsWith('command/')) {
// OpenCode/Kilo: command/msd-<cmd>.md
// OpenCode: command/msd-<cmd>.md
const file = key.split('/')[1];
out.add(file.replace(/^msd-/, '').replace(/\.md$/, ''));
} else if (key.startsWith('commands/msd/')) {

View File

@@ -47,8 +47,8 @@ for (const routerStem of ROUTER_STEMS) {
/**
* Nested SKILL.md path for a concrete skill stem:
* <skillsRoot>/<prefix><router>/skills/<stem>/SKILL.md
* prefix is '' for runtimes that nest under a skills/msd parent dir (hermes),
* or 'msd-' for flat-prefixed runtimes (claude, cline, qwen, …).
* prefix is '' for runtimes that nest under a bare parent dir, or 'msd-' for
* flat-prefixed runtimes (claude, …).
*/
function nestedSkillPath(skillsRoot, prefix, stem) {
const router = CHILD_ROUTER[stem];

View File

@@ -2875,12 +2875,12 @@ describe('#2627 dispatch-isolation CLI route', () => {
test('raw output is the bare negotiated value for each isolation model', () => {
assert.equal(query('claude').trim(), 'harness-worktree');
assert.equal(query('codex').trim(), 'orchestrator-worktree');
assert.equal(query('pi').trim(), 'none');
assert.equal(query('zcode').trim(), 'none');
});
test('an undocumented isolation declaration degrades to none (fail-closed)', () => {
// cline declares isolation:"undocumented" — the corpus-wide sentinel.
assert.equal(query('cline').trim(), 'none');
// antigravity declares isolation:"undocumented" — the corpus-wide sentinel.
assert.equal(query('antigravity').trim(), 'none');
});
test('an unknown runtime degrades to none rather than erroring', () => {

View File

@@ -1,4 +1,4 @@
// docs-guard-exempt: docs/... substrings are external URL citations (qwenlm/code.claude.com) in comments, not repo paths.
// docs-guard-exempt: docs/... substrings are external URL citations (code.claude.com) in comments, not repo paths.
/**
* Installer Module — Sections 9–11 + 13.
*
@@ -651,18 +651,12 @@ describe('#1755: .sh hooks are copied and executable after install', () => {
// ─── #1821/#2305: hooks staged iff a surface consumes them ─────────────────────
//
// #1821 reported dead hook scripts staged for runtimes with hooksSurface:'none'.
// OpenCode, pi — and, corrected by #2305, Kilo — ALSO declare
// hooksSurface:'none', but each has a native plugin adapter that spawns the
// staged hooks/*.js scripts as subprocesses (OpenCode's #1914
// plugins/msd-core.js via OpenCode's event bus; pi's #2102 Stage 2 pi/msd.cjs
// → extensions/msd.js via pi.on(...) bridges; Kilo's plugins/msd-core.js,
// byte-identical to OpenCode's) — so for all three, the hooks are LIVE and
// OpenCode ALSO declares hooksSurface:'none', but has a native plugin adapter
// that spawns the staged hooks/*.js scripts as subprocesses (OpenCode's #1914
// plugins/msd-core.js via OpenCode's event bus) — so its hooks are LIVE and
// must keep being copied. ZCode has no plugin surface at all, so its staged
// hooks are genuinely dead: that is the case #1821's fix removes. (#1821
// originally excluded Kilo too, on the false premise that it had no plugin
// surface — #2305 reversed that: the skip flag silently no-opped every guard
// hook Kilo's plugin spawns.) These tests assert the split: ZCode gets no
// hooks; Kilo/OpenCode/pi (and Claude) do.
// hooks are genuinely dead: that is the case #1821's fix removes. These tests
// assert the split: ZCode gets no hooks; OpenCode (and Claude) do.
describe('#1821/#2305: ZCode receives no dead hook files; OpenCode/Claude keep their hooks', () => {
function msdHookFilesUnder(configDir, hooksDirName) {
@@ -686,7 +680,7 @@ describe('#1821/#2305: ZCode receives no dead hook files; OpenCode/Claude keep t
// Collect results while targetDir still exists — cleanup() below removes it.
const pluginRelPath = opts.pluginRelPath || path.join('plugins', 'msd-core.js');
// #3023: the shared hooks bundle's staged directory name is per-runtime
// (hostBehaviors.sharedHooksDirName; pi renames it to `msd-hooks/`) —
// (hostBehaviors.sharedHooksDirName) —
// resolve it the same way the installer does rather than hardcoding
// 'hooks', or every non-default runtime would look hookless.
const hooksDirName = resolveSharedHooksDirName(runtime);
@@ -1183,7 +1177,7 @@ process.env.MSD_TEST_MODE = '1';
* extendedHookEvents descriptor field, not hardcoded runtime-name checks.
*
* Before this change:
* - SubagentStop/Stop/PreCompact were wired only when (isQwen || runtime==='claude')
* - SubagentStop/Stop/PreCompact were wired only when (runtime==='claude')
* - FileChanged was wired only when (runtime === 'claude')
* - BeforeAgent/AfterAgent/BeforeModel were wired only when (isGemini)
*
@@ -1348,9 +1342,9 @@ describe('enh-1076 phase 5f-3: claude extendedHookEvents → SubagentStop/Stop/P
});
});
// ─── Suite 2: qwen shape (SubagentStop+Stop+PreCompact, no FileChanged) ───────
// ─── Suite 2: SubagentStop+Stop+PreCompact shape (no FileChanged) ─────────────
describe('enh-1076 phase 5f-3: qwen extendedHookEvents → SubagentStop/Stop/PreCompact only', () => {
describe('enh-1076 phase 5f-3: extendedHookEvents → SubagentStop/Stop/PreCompact only', () => {
let targetDir;
let settings;
@@ -1358,7 +1352,7 @@ describe('enh-1076 phase 5f-3: qwen extendedHookEvents → SubagentStop/Stop/Pre
targetDir = createStubTargetDir();
settings = { hooks: {} };
const opts = buildOpts(targetDir, {
runtime: 'qwen',
runtime: 'hypothetical-stop-shape',
extendedHookEvents: ['SubagentStop', 'Stop', 'PreCompact'],
});
applySettingsJsonHooks(settings, opts);
@@ -1380,7 +1374,7 @@ describe('enh-1076 phase 5f-3: qwen extendedHookEvents → SubagentStop/Stop/Pre
assert.strictEqual(
hasHooksFor(settings, 'FileChanged'),
false,
`FileChanged must NOT be wired for qwen shape; hooks keys: ${JSON.stringify(Object.keys(settings.hooks || {}))}`
`FileChanged must NOT be wired for the SubagentStop/Stop/PreCompact shape; hooks keys: ${JSON.stringify(Object.keys(settings.hooks || {}))}`
);
});
@@ -1491,7 +1485,7 @@ describe('enh-1076 phase 5f-3: arbitrary runtime with SubagentStop in descriptor
targetDir = createStubTargetDir();
settings = { hooks: {} };
const opts = buildOpts(targetDir, {
runtime: 'hypothetical', // NOT 'claude' or 'qwen' — would have been skipped before
runtime: 'hypothetical', // NOT 'claude' — would have been skipped before
extendedHookEvents: ['SubagentStop'],
});
applySettingsJsonHooks(settings, opts);
@@ -1753,7 +1747,7 @@ describe('enh-1077 phase 5f-2: gemini hookEvents dialect → AfterTool/BeforeToo
// ─── Suite 2: Claude-dialect runtimes use PostToolUse/PreToolUse ──────────────
//
// Registry runtimes with hookEvents='claude': claude, augment
// Registry runtimes with hookEvents='claude': claude
describe('enh-1077 phase 5f-2: claude hookEvents dialect → PostToolUse/PreToolUse', () => {
// ── claude ──
@@ -1837,7 +1831,7 @@ describe('enh-1077 phase 5f-2: registry-parity — hookEvents descriptor drives
}
// Runtimes that have settings.json surfaces and a hookEvents descriptor
const SETTINGS_JSON_RUNTIMES = ['claude', 'antigravity', 'augment', 'qwen', 'hermes', 'codebuddy'];
const SETTINGS_JSON_RUNTIMES = ['claude', 'antigravity'];
const failures = [];
@@ -1908,11 +1902,10 @@ describe('enh-1077 phase 5f-2: registry-parity — hookEvents descriptor drives
process.env.MSD_TEST_MODE = '1';
/**
* Enhancement #788: Expand Qwen Code hook-event coverage.
* Enhancement #788: Expand hook-event coverage.
*
* Qwen Code supports 15 hook events; msd previously registered only
* SessionStart and PostToolUse. This suite asserts that a Qwen install
* registers the 3 new high-value events:
* msd previously registered only SessionStart and PostToolUse. This suite
* asserts that an install registers the 3 new high-value events:
* - SubagentStop — subagent lifecycle finalisation (context tracking)
* - Stop — model stop / final-response hook (context tracking)
* - PreCompact — pre-compaction awareness (context tracking)
@@ -1923,11 +1916,6 @@ process.env.MSD_TEST_MODE = '1';
* Note: UserPromptSubmit is NOT wired — msd-prompt-guard exits unless
* tool_name is Write|Edit (PreToolUse shape), so it would be a no-op for
* the UserPromptSubmit payload. Deferred to a follow-on issue.
*
* Also asserts the inverse: Claude Code installs do NOT gain these events
* (strict isQwen scope guard).
*
* Source: https://qwenlm.github.io/qwen-code-docs/en/users/features/hooks/
*/
const { test, describe, beforeEach, afterEach } = require('node:test');
@@ -1977,8 +1965,7 @@ function stubHooksIntoTarget(targetDir) {
}
// ─── Suite 2: Claude install DOES get the context events (since #770) ───────
// Note: Prior to #770, these were Qwen-only events. #770 extended them to
// Claude Code. This suite is updated to match the new expected behavior.
// Note: #770 extended these events to Claude Code.
describe('enh-788 (updated by #770): Claude install registers context lifecycle events', () => {
let tmpDir;
@@ -2095,10 +2082,10 @@ process.env.MSD_TEST_MODE = '1';
* PreCompact / FileChanged).
*
* Claude Code now supports the same SubagentStop, Stop, and PreCompact events
* that were wired for Qwen Code in #788. This suite asserts:
* that were wired in #788. This suite asserts:
*
* 1. Claude Code installs register SubagentStop, Stop, and PreCompact, each
* wired to msd-context-monitor.js (same as Qwen).
* wired to msd-context-monitor.js.
* 2. Claude Code installs register a FileChanged hook for .planning/config.json
* wired to msd-config-reload.js (new hook; hot-reloads msd config).
* 3. All four registrations are idempotent (reinstall does not duplicate).
@@ -2683,9 +2670,7 @@ describe('#4087 regression: Codex install stages the hook helpers its hooks requ
// statusline hook, which Codex never installs, so every registered Codex
// event was a guaranteed silent no-op. The #4087 bug class this describe
// block guards (a staged hook requiring an unshipped hooks/lib/ helper)
// remains covered live via Windsurf's own guards — see the
// "#4087 review: Windsurf install..." describe block below, unaffected
// by this change.
// remains covered by the grammar-level rows below.
const hooksDir = installCodex(tmpDir);
const libDir = path.join(hooksDir, 'lib');
@@ -2724,8 +2709,8 @@ describe('#4087 regression: Codex install stages the hook helpers its hooks requ
// dependency closure is correctly empty. This row still proves the
// GRAMMAR holds (whatever IS required must be staged) — it is just that
// "whatever is required" is now the empty set for Codex specifically.
// The non-trivial case (closure size > 0) is covered live by the
// "#4087 review: Windsurf install..." describe block below.
// The non-trivial case (closure size > 0) is covered by the grammar-level
// rows below.
assert.strictEqual(required.size, 0,
'no staged Codex hook should require a ./lib/ helper post-#2586 — if this becomes non-zero, '
+ 'extend this row (do not just raise the bar back to ">0") so the new dependency stays proven');
@@ -2756,8 +2741,8 @@ describe('#4087 regression: Codex install stages the hook helpers its hooks requ
// #2586: Codex's closure is now legitimately empty (msd-context-monitor.js,
// the only staged Codex hook that ever required a helper, is no longer
// staged) — the deepStrictEqual below is still the real assertion and
// holds for the empty case too; the non-empty case remains covered live
// by the "#4087 review: Windsurf install..." describe block below.
// holds for the empty case too; the non-empty case remains covered by the
// grammar-level rows below.
assert.strictEqual(stagedLibs.length, 0, 'no helpers should be staged for Codex post-#2586');
// Derive the closure independently of the installer.

View File

@@ -1,4 +1,4 @@
// docs-guard-exempt: codebuddy.ai/docs/... is an external URL and docs/adr/58-...md is a comment citation; neither is read.
// docs-guard-exempt: docs/adr/58-...md is a comment citation; it is not read.
// allow-test-rule: source-text-is-the-product
// Reads .md/.json/.yml product files whose deployed text IS what the
// runtime loads — testing text content tests the deployed contract.
@@ -640,7 +640,7 @@ describe('installOpencodeFamilySkills — emits skills/<name>/SKILL.md (#784)',
!body.includes(`~/${defaultBase}/`),
`${skillName}: must not leak hardcoded ~/${defaultBase}/ — should use install target`,
);
// Regression guard for the prefix-overlap double-rewrite (e.g. kilo-alt-alt).
// Regression guard for the prefix-overlap double-rewrite (e.g. opencode-alt-alt).
assert.ok(
!new RegExp(`${escapeRegex(defaultBase)}-[^/\\s]*-`).test(body),
`${skillName}: must not contain a doubled config-dir suffix`,
@@ -668,7 +668,7 @@ describe('installOpencodeFamilySkills — emits skills/<name>/SKILL.md (#784)',
}
});
// ─── #2362: OpenCode/Kilo combined-family INSTALL path drops the capability
// ─── #2362: OpenCode combined-family INSTALL path drops the capability
// registry — an installed+registered+surfaced+active third-party capability
// skill never materializes ───────────────────────────────────────────────
//
@@ -1353,7 +1353,7 @@ describe('convertClaudeToAntigravityContent bare path replacement (#2418)', () =
// ────────────────────────────────────────────────────────────────────────
// Folded from tests/enh-789-codebuddy-commands.test.cjs — consolidation epic #1969 (B1 #1970)
// (codebuddy runtime retired; the Cursor #2644 guard that lived in the same file is kept)
// (the Cursor #2644 guard that lived in the same file is kept)
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
@@ -3186,9 +3186,7 @@ describe('#443 Source purity: agents/msd-planner.md has no effort: key', () => {
// relocation of pure rewrite-engine helpers out of hand-authored bin/install.js.
// - getDirName -> msd-core/bin/lib/runtime-name-policy.cjs
// - processAttribution -> msd-core/bin/lib/runtime-artifact-conversion.cjs
// getCommitAttribution stays in install.js (impure install-time config I/O); the
// convertClaudeToAugmentMarkdown duplicate dedup is deferred to Phase 2's cleanup
// (entangled converter cluster; not required to unblock Phase 2).
// getCommitAttribution stays in install.js (impure install-time config I/O).
// These tests exercise the REAL relocated functions at their new home (the
// generated .cjs). #2876 (epic #2866 Phase 7) retired install.js's re-export
// of both names — a repo-wide audit found zero production consumers of the
@@ -3390,10 +3388,10 @@ describe('_computePathPrefix', () => {
isGlobal: false,
isOpencode: false,
isWindowsHost: true,
resolvedTarget: 'C:\\Users\\runner\\AppData\\Local\\Temp\\msd-1615-windsurf',
resolvedTarget: 'C:\\Users\\runner\\AppData\\Local\\Temp\\msd-1615-fixture',
homeDir: 'C:\\Users\\runner',
});
assert.strictEqual(prefix, 'C:/Users/runner/AppData/Local/Temp/msd-1615-windsurf/');
assert.strictEqual(prefix, 'C:/Users/runner/AppData/Local/Temp/msd-1615-fixture/');
assert.ok(!prefix.includes('\\'), `prefix must not contain backslashes: ${prefix}`);
});
});
@@ -5281,7 +5279,7 @@ describe('#2873 C1-C6 — install-time shadow report (spawned installer wiring)'
//
// readMsdRuntimeProfileResolver probes for the project's
// .planning/config.json by walking up from the install's targetDir. A GLOBAL
// OpenCode/Kilo install (targetDir ~/.config/<runtime>) can never reach the
// OpenCode install (targetDir ~/.config/<runtime>) can never reach the
// consuming project, and ~/.msd/defaults.json never carries model_profile
// (writeNonClaudeDefaults writes only resolve_model_ids + runtime), so the
// resolver silently fell back to 'balanced' and baked e.g.
@@ -5295,7 +5293,6 @@ describe('#2873 C1-C6 — install-time shadow report (spawned installer wiring)'
// - a found config keeps the documented 'balanced' default (local installs).
// - a profile (or model_overrides pin) declared in ~/.msd/defaults.json is
// machine-level and still bakes on a global install.
// - Kilo mirrors OpenCode (static-frontmatter twin, #2093).
{
const { describe: __d3543, test: __t3543, beforeEach: __be3543, afterEach: __ae3543 } = require('node:test');
const { install: __install3543 } = require('../bin/install.js');

View File

@@ -175,7 +175,7 @@ describe('resolveScope', () => {
});
// Row 14
test('preserves opencode/kilo config-file precedence', () => {
test('preserves opencode config-file precedence', () => {
const filePath = '/home/x/custom/opencode-config.json';
const result = resolveScope(fixture({
id: 'global',

View File

@@ -2872,7 +2872,7 @@ describe('#2875: user-artifact-staging confinement (E1-E5)', () => {
);
// Run from a cwd that resolves the relative destDir straight back to
// configDir (mirrors the real cline-local call shape, where targetDir
// configDir (mirrors a local call shape where targetDir
// === process.cwd()) — the exact case that must NOT be treated as
// "correctly resolved" just because the coincidence lines up.
const previousCwd = process.cwd();

View File

@@ -103,7 +103,7 @@ describe('getGlobalConfigDir — all runtimes default paths', () => {
}
});
for (const runtime of allRuntimes.filter(runtime => runtime !== 'kimi')) {
for (const runtime of allRuntimes) {
test(`getGlobalConfigDir('${runtime}') returns expected home-relative path`, () => {
const expected = path.join(os.homedir(), RUNTIME_META[runtime].globalSuffix);
assert.strictEqual(getGlobalConfigDir(runtime), expected);
@@ -665,12 +665,6 @@ describe('antigravity local install writes to .agents/ canonical dir (#791)', ()
}
});
});
// ─── Section 6: Windsurf / devin-desktop alias (#792) ───────────────────────
// ─── Section N: Windsurf workflow slash-command install (#1615) ─────────────
// Reads deployed workflow .md files whose text IS the product surface the
// Windsurf runtime loads at startup (path references, command names).
// ─── Section N+1: #767 — disallowedTools injection for read-only agents ──────
//
// Verifies (installer-behavioral test — drives install() to a temp dir):
@@ -4915,7 +4909,7 @@ describe('bug #505: dead SDK verification subsystem removed from bin/install.js'
* Fix: the `.js` branch of the hook-copy loop now applies
* `content.replace(/msd:/gi, 'msd-')` when
* `shouldNormalizeHyphenNamespaceInAgentBody(runtime)` is true (covers
* claude, qwen, hermes).
* claude).
*
* Test plan:
* 1. Claude install to tmp prefix — installed .js hook files must contain
@@ -5432,7 +5426,7 @@ test('install-effort-resolver.cjs tier-defaults object has exactly the same keys
* the directory name as a namespace, so `commands/msd/update.md` became `/msd:update`.
*
* Fix: write each command as `msd-<stem>.md` directly in `commands/` (flat layout).
* This is the same approach used for OpenCode/Kilo (see `copyFlattenedCommands`).
* This is the same approach used for OpenCode (see `copyFlattenedCommands`).
*/
'use strict';

View File

@@ -22,7 +22,7 @@
* (`resolveRuntimeArtifactLayoutFromRegistry`) and `resolveTriggerSurface` —
* never `resolveScope`, which always consults the REAL
* `capability-registry.cjs`. So every test below uses a REAL registered
* runtime id (`claude`, `cursor`, `cline`, `windsurf`) for scope resolution,
* runtime id (`claude`, `cursor`, `codex`, `zcode`) for scope resolution,
* and reaches for `opts.registry` only when a row needs a layout shape the
* real registry does not currently ship (namespaced-by-dir commands, B2/B8).
* C7/C8 (the all-runtimes sweep) are asserted against the real registry only

View File

@@ -30,49 +30,16 @@ const SUPPORTED_RUNTIMES = installModule.allRuntimes;
const RUNTIME_INSTALL_CONTRACTS = {
claude: { surface: 'flat-skills', settings: true, hooksPackageJson: true },
antigravity: { surface: 'flat-skills', settings: true, hooksPackageJson: true },
augment: { surface: 'flat-skills', settings: true, hooksPackageJson: true },
cline: { surface: 'clinerules', settings: false, hooksPackageJson: false },
codebuddy: { surface: 'flat-skills', settings: true, hooksPackageJson: true },
// codex/cursor/windsurf: #2717 stages their .js hooks via dedicated paths
// codex/cursor: #2717 stages their .js hooks via dedicated paths
// (skipSharedHooksInstall / the !isCodex gate keep them out of the shared
// bundle) and writes the marker beside those scripts, so hooks/package.json
// is expected for all three. Measured on this tree: codex stages 3 .js hooks,
// cursor 6, windsurf 2 — each with the marker.
// is expected for both. Measured on this tree: codex stages 3 .js hooks,
// cursor 6 — each with the marker.
codex: { surface: 'flat-skills', settings: false, hooksPackageJson: true, codexConfig: true },
copilot: { surface: 'flat-skills', settings: false, hooksPackageJson: false, copilotInstructions: true },
cursor: { surface: 'flat-skills', settings: false, hooksPackageJson: true },
gemini: { surface: 'commands-msd', settings: true, hooksPackageJson: true },
hermes: { surface: 'hermes-skills', settings: true, hooksPackageJson: true },
kimi: { surface: 'kimi-skills-agents', settings: false, hooksPackageJson: false },
// #2454: Kimi Code (Node CLI) has NO custom named subagents (per official
// docs), so its install surface is skills-only (flat-skills), NOT
// kimi-skills-agents. The kimi-agents YAML layout is Python kimi-cli only.
'kimi-code': { surface: 'flat-skills', settings: false, hooksPackageJson: false },
// #2305: Kilo's native plugin spawns the staged guard hooks, so it receives
// the shared hooks bundle + the CommonJS package.json marker, like OpenCode.
// (#1821 excluded Kilo on the false premise that it had no plugin surface.)
kilo: { surface: 'flat-command', settings: false, hooksPackageJson: true },
// #2329: OpenCode discovers commands from the PLURAL `commands/` dir — the
// singular `command/` (still correct for Kilo) made all /msd-* commands
// invisible to OpenCode. commandDirName overrides the flat-command default.
// #2329: OpenCode discovers commands from the PLURAL `commands/` dir.
opencode: { surface: 'flat-command', settings: true, hooksPackageJson: true, commandDirName: 'commands' },
// #2102 Stage 1/2: pi is a PLUGIN-ONLY install (hostBehaviors.pluginOnlyInstall)
// for commands/agents/skills — NO commands/, agents/, or skills/ dir. pi's
// /msd command is registered programmatically by the native extension
// (extensions/msd.cjs) and dispatches via a bounded subprocess to
// msd-tools.cjs; it has no host-read markdown surface. Stage 2 (adversarial-
// review fix): pi's native extension DOES spawn the shared hooks/*.js bundle
// as bounded subprocesses (session_start/before_agent_start/session_before_
// compact bridges) and its /msd tokenizer requires hooks/lib/git-cmd.js, so
// `hostBehaviors.skipSharedHooksInstall` was removed — pi now receives
// hooks/ + hooks/lib/ + the {"type":"commonjs"} package.json marker, exactly
// like OpenCode and (since #2305) Kilo — architecturally identical:
// hooksSurface:'none' + a native plugin that spawns the staged hooks — NOT
// like ZCode (no plugin surface, where the same hooks are dead weight).
pi: { surface: 'plugin-only', settings: false, hooksPackageJson: true },
qwen: { surface: 'flat-skills', settings: true, hooksPackageJson: true },
trae: { surface: 'flat-skills', settings: false, hooksPackageJson: false },
windsurf: { surface: 'global-artifacts-noop', settings: false, hooksPackageJson: true },
// #1821: ZCode (hooksSurface:none, no plugin surface) no longer receives the
// dead hook scripts or the CommonJS package.json marker.
zcode: { surface: 'flat-skills', settings: false, hooksPackageJson: false },
@@ -237,40 +204,31 @@ function assertFreshInstallContract(runtime, targetDir) {
const contract = RUNTIME_INSTALL_CONTRACTS[runtime];
assert.ok(contract, `missing runtime install contract for ${runtime}`);
// #3023: the shared hooks bundle's staged directory name is per-runtime
// (hostBehaviors.sharedHooksDirName; pi renames it to `msd-hooks/` to avoid
// pi's own host-reserved `hooks/`) — resolve it the same way the installer
// (hostBehaviors.sharedHooksDirName) — resolve it the same way the installer
// does rather than hardcoding 'hooks', which is only the default.
const hooksDirName = installModule.resolveSharedHooksDirName(runtime);
if (contract.workflowPayload !== false) {
assert.equal(
fs.readFileSync(path.join(targetDir, 'msd-core', 'VERSION'), 'utf8'),
pkg.version,
`${runtime} should install the package VERSION`
);
assert.ok(
fs.existsSync(path.join(targetDir, 'msd-core', 'bin', 'msd-tools.cjs')),
`${runtime} should install the MSD tool payload`
);
assert.ok(
fs.existsSync(path.join(targetDir, 'msd-file-manifest.json')),
`${runtime} should write the install manifest`
);
assert.equal(
fs.readFileSync(path.join(targetDir, 'msd-core', 'VERSION'), 'utf8'),
pkg.version,
`${runtime} should install the package VERSION`
);
assert.ok(
fs.existsSync(path.join(targetDir, 'msd-core', 'bin', 'msd-tools.cjs')),
`${runtime} should install the MSD tool payload`
);
assert.ok(
fs.existsSync(path.join(targetDir, 'msd-file-manifest.json')),
`${runtime} should write the install manifest`
);
const manifest = JSON.parse(fs.readFileSync(path.join(targetDir, 'msd-file-manifest.json'), 'utf8'));
assert.equal(manifest.version, pkg.version, `${runtime} manifest should record the package version`);
assert.equal(manifest.mode, 'full', `${runtime} manifest should record a full install`);
assert.ok(
manifest.files['msd-core/VERSION'],
`${runtime} manifest should track the installed VERSION file`
);
} else {
assert.equal(
fs.existsSync(path.join(targetDir, 'msd-core')),
false,
`${runtime} should not install the MSD workflow payload`
);
}
const manifest = JSON.parse(fs.readFileSync(path.join(targetDir, 'msd-file-manifest.json'), 'utf8'));
assert.equal(manifest.version, pkg.version, `${runtime} manifest should record the package version`);
assert.equal(manifest.mode, 'full', `${runtime} manifest should record a full install`);
assert.ok(
manifest.files['msd-core/VERSION'],
`${runtime} manifest should track the installed VERSION file`
);
if (contract.surface === 'flat-skills') {
if (runtime === 'codex') {
@@ -295,131 +253,33 @@ function assertFreshInstallContract(runtime, targetDir) {
// the same flat-skills surface as the other runtimes.
assertHasMsdDirectory(targetDir, 'skills');
}
} else if (contract.surface === 'hermes-skills') {
// Hermes layout uses prefix: '' — skill dirs have bare stem names (no msd- prefix).
// Assert that the category dir contains at least one skill dir with SKILL.md.
const hermesMsdDir = path.join(targetDir, 'skills', 'msd');
const hermesSkillCount = fs.existsSync(hermesMsdDir)
? fs.readdirSync(hermesMsdDir, { withFileTypes: true })
.filter(e => e.isDirectory() && fs.existsSync(path.join(hermesMsdDir, e.name, 'SKILL.md')))
.length
: 0;
assert.ok(hermesSkillCount > 0, `skills/msd should contain generated MSD entries (got ${hermesSkillCount})`);
assert.ok(
fs.existsSync(path.join(targetDir, 'skills', 'msd', 'DESCRIPTION.md')),
'Hermes should install the nested MSD category description'
);
} else if (contract.surface === 'flat-command') {
// #2329: dir name is per-runtime (opencode='commands', kilo stays 'command').
// #2329: dir name is per-runtime (opencode='commands').
const commandDirName = contract.commandDirName || 'command';
assert.ok(
listDirNames(targetDir, commandDirName).some((name) => name.startsWith('msd-') && name.endsWith('.md')),
`${runtime} should install flattened command markdown files in ${commandDirName}/`
);
} else if (contract.surface === 'plugin-only') {
// #2102 Stage 1/2: pi — PLUGIN-ONLY install for commands/agents/skills
// (hostBehaviors.pluginOnlyInstall). pi's /msd command is registered
// programmatically by the native extension and dispatches via a bounded
// subprocess to msd-tools.cjs — pi has no host-read markdown surface, so
// NO commands/, agents/, or skills/ dir is written. The extension DOES
// spawn the shared hooks bundle as bounded subprocesses (Stage 2
// adversarial-review fix — hooksSurface:'none' no longer implies
// skipSharedHooksInstall for pi, mirroring OpenCode), so the bundle + the
// git-cmd.js tokenizer helper ARE part of the artifact surface now. #3023:
// that bundle is staged under `msd-hooks/` for pi (hooksDirName above), not
// the generic `hooks/` — pi reserves `hooks/` for its own deprecated
// extension location.
// #2470: the dest filename comes from pi's descriptor, and must satisfy
// pi's isExtensionFile() auto-discovery filter (.ts/.js only) — otherwise
// the file installs but pi never loads it and /msd never registers.
const piNativePlugin = JSON.parse(
fs.readFileSync(path.join(__dirname, '..', 'capabilities', 'pi', 'capability.json'), 'utf8')
).runtime.hostBehaviors.nativePlugin;
assert.ok(
piNativePlugin.file.endsWith('.ts') || piNativePlugin.file.endsWith('.js'),
`${runtime}'s extension "${piNativePlugin.file}" must end in .ts or .js for pi to discover it (#2470)`
);
assert.ok(
fs.existsSync(path.join(targetDir, piNativePlugin.dir, piNativePlugin.file)),
`${runtime} should install the native extension file at ${piNativePlugin.dir}/${piNativePlugin.file}`
);
assert.ok(
fs.existsSync(path.join(targetDir, hooksDirName, 'msd-ensure-canonical-path.js')),
`${runtime} should install the shared ${hooksDirName}/ bundle (spawned by the native extension's event bridges)`
);
assert.ok(
fs.existsSync(path.join(targetDir, hooksDirName, 'lib', 'git-cmd.js')),
`${runtime} should install ${hooksDirName}/lib/git-cmd.js (the /msd command tokenizer)`
);
assert.equal(
fs.existsSync(path.join(targetDir, 'commands')),
false,
`${runtime} should NOT install a commands/ dir (plugin-only, no host-read markdown surface)`
);
assert.equal(
fs.existsSync(path.join(targetDir, 'agents')),
false,
`${runtime} should NOT install an agents/ dir (plugin-only, no named-dispatch toolkit)`
);
assert.equal(
fs.existsSync(path.join(targetDir, 'skills')),
false,
`${runtime} should NOT install a skills/ dir (plugin-only)`
);
} else if (contract.surface === 'commands-msd') {
assert.ok(
listDirNames(targetDir, path.join('commands', 'msd')).length > 0,
`${runtime} should install commands/msd entries`
);
} else if (contract.surface === 'kimi-skills-agents') {
assertHasMsdDirectory(targetDir, 'skills');
assert.ok(
fs.existsSync(path.join(targetDir, 'agents', 'msd.yaml')),
'Kimi should install the root agent YAML'
);
assert.ok(
fs.existsSync(path.join(targetDir, 'agents', 'msd.md')),
'Kimi should install the root agent prompt'
);
assert.ok(
fs.existsSync(path.join(targetDir, 'agents', 'subagents', 'msd-executor.yaml')),
'Kimi should install MSD subagent YAML'
);
} else if (contract.surface === 'clinerules') {
// #787: Cline now uses the .clinerules/ directory form (rules at msd.md).
assert.match(
fs.readFileSync(path.join(targetDir, '.clinerules', 'msd.md'), 'utf8'),
/MSD workflows live in `msd-core\/workflows\/`/,
'Cline should install .clinerules/msd.md guidance'
);
} else if (contract.surface === 'global-artifacts-noop') {
assert.equal(
fs.existsSync(path.join(targetDir, 'skills')),
false,
`${runtime} should not install unsupported global skills artifacts`
);
assert.equal(
fs.existsSync(path.join(targetDir, 'workflows')),
false,
`${runtime} should not install unsupported global workflow artifacts`
);
}
if (contract.surface !== 'kimi-skills-agents' && contract.surface !== 'global-artifacts-noop' && contract.surface !== 'plugin-only') {
if (runtime === 'antigravity') {
// #3738: antigravity agents install under the sandboxed home's
// .gemini/config root (see the flat-skills branch above), not targetDir.
const agySandboxHomeForAgents = path.join(path.dirname(targetDir), 'home');
assert.ok(
listDirNames(path.join(agySandboxHomeForAgents, '.gemini', 'config'), 'agents').some((name) => name.startsWith('msd-')),
`${runtime} full install should install agents`
);
} else {
assert.ok(
listDirNames(targetDir, 'agents').some((name) => name.startsWith('msd-')),
`${runtime} full install should install agents`
);
}
if (runtime === 'antigravity') {
// #3738: antigravity agents install under the sandboxed home's
// .gemini/config root (see the flat-skills branch above), not targetDir.
const agySandboxHomeForAgents = path.join(path.dirname(targetDir), 'home');
assert.ok(
listDirNames(path.join(agySandboxHomeForAgents, '.gemini', 'config'), 'agents').some((name) => name.startsWith('msd-')),
`${runtime} full install should install agents`
);
} else {
assert.ok(
listDirNames(targetDir, 'agents').some((name) => name.startsWith('msd-')),
`${runtime} full install should install agents`
);
}
assert.equal(
@@ -428,9 +288,9 @@ function assertFreshInstallContract(runtime, targetDir) {
`${runtime} settings.json presence should match the runtime contract`
);
// #2544: the CommonJS marker lives in the shared hooks dir (the dir MSD
// fills with its own .js scripts — `msd-hooks/` for pi, `hooks/` for every
// other runtime, #3023), never at the config root — that file is user-owned
// territory on OpenCode/Kilo and was being clobbered on every install.
// fills with its own .js scripts — `hooks/` unless a runtime overrides it,
// #3023), never at the config root — that file is user-owned
// territory on OpenCode and was being clobbered on every install.
assert.equal(
fs.existsSync(path.join(targetDir, hooksDirName, 'package.json')),
contract.hooksPackageJson,
@@ -449,14 +309,6 @@ function assertFreshInstallContract(runtime, targetDir) {
'Codex should install config.toml with the MSD marker'
);
}
if (contract.copilotInstructions) {
assert.match(
fs.readFileSync(path.join(targetDir, 'copilot-instructions.md'), 'utf8'),
/MSD Configuration/,
'Copilot should install managed copilot instructions'
);
}
}
describe('installer migration install integration', { concurrency: false }, () => {
@@ -618,11 +470,7 @@ describe('installer migration install integration', { concurrency: false }, () =
assert.match(output, /Installing for /);
assert.match(output, /Installer migrations/);
assert.match(output, /removed\s+hooks\/statusline\.js/);
if (runtime === 'kimi') {
assert.match(output, /Generated Kimi root agent/);
} else {
assert.match(output, /Installed workflow assets/);
}
assert.match(output, /Installed workflow assets/);
assert.match(output, /Done!/);
assert.equal(fs.existsSync(path.join(targetDir, 'hooks/statusline.js')), false);

View File

@@ -428,29 +428,26 @@ test('OpenCode commands/ baseline is idempotent — a second run does not re-pla
}
});
test('OpenCode commands/ baseline migration is scoped to opencode and never plans for Kilo', () => {
test('OpenCode commands/ baseline migration is scoped to opencode and never plans for other runtimes', () => {
const configDir = createTempInstall();
try {
// Kilo's descriptor keeps the singular `command/` dir; a `commands/` (plural)
// directory here would be unrelated to Kilo's install surface. This proves the
// migration's `runtimes: ['opencode']` scoping keeps Kilo installs untouched.
// This proves the migration's `runtimes: ['opencode']` scoping keeps other
// runtimes' installs untouched.
writeFile(configDir, 'commands/msd-plan-phase.md', 'managed command\n');
writeFile(configDir, 'command/msd-plan-phase.md', 'kilo managed command\n');
writeManifest(configDir, {
'commands/msd-plan-phase.md': sha256('managed command\n'),
'command/msd-plan-phase.md': sha256('kilo managed command\n'),
});
const result = runInstallerMigrations({
configDir,
runtime: 'kilo',
runtime: 'claude',
scope: 'global',
migrations: [opencodeBaselineCommandsDirMigration],
baselineScan: true,
now: () => '2026-07-17T00:00:03.000Z',
});
// migrationMatchesContext filters this migration out entirely for kilo
// migrationMatchesContext filters this migration out entirely for claude
// (runtimes: ['opencode']) before plan() is ever invoked: it is not
// "pending", produces zero actions, is never applied, and no install-state
// file is written for this run at all.
@@ -458,7 +455,6 @@ test('OpenCode commands/ baseline migration is scoped to opencode and never plan
assert.deepEqual(result.appliedMigrationIds, []);
assert.equal(fs.existsSync(path.join(configDir, INSTALL_STATE_NAME)), false);
assert.equal(fs.readFileSync(path.join(configDir, 'commands/msd-plan-phase.md'), 'utf8'), 'managed command\n');
assert.equal(fs.readFileSync(path.join(configDir, 'command/msd-plan-phase.md'), 'utf8'), 'kilo managed command\n');
} finally {
cleanup(configDir);
}
@@ -1692,7 +1688,7 @@ test('shipped installer-migration checksums are locked to a committed baseline (
// kinds install under the .gemini/config home override and this migration
// converges upgraded installs. Global scope only — local .agents is live.
'2026-08-26-antigravity-retire-confighome-artifacts':
'sha256:0011d160a7fe07230a9d89de8ce39215e004fff1077961dc687781cb9fe103ba',
'sha256:9d19e13048455df7727960e68473cedd24f162918362b21e792ddf881a496e01',
};
const { DEFAULT_MIGRATIONS_DIR, migrationChecksum: computeChecksum } = require('../msd-core/bin/lib/installer-migrations.cjs');

View File

@@ -1829,285 +1829,3 @@ describe('#3258: no stale /msd-intel slash-command references in product source
});
}
// ────────────────────────────────────────────────────────────────────────
// Folded from tests/bug-2351-intel-kilo-layout.test.cjs — consolidation epic #1969 (B7 #1976)
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:bug-2351-intel-kilo-layout (consolidation epic #1969 B7 #1976)", () => {
/**
* Regression test for bug #2351
*
* msd-intel-updater used hardcoded canonical paths (`agents/*.md`,
* `commands/msd/*.md`, `hooks/*.js`, etc.) that assumed the standard
* `.claude/` runtime layout. Under a `.kilo` install, the runtime root is
* `.kilo/`, and the command directory is `command/` (not `commands/msd/`).
* Globs against the old paths returned no results, producing semantically
* empty intel files (`"entries": {}`).
*
* Fix: add runtime layout detection and a mapping table so the agent
* resolves paths against the correct root.
*/
'use strict';
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const AGENT_PATH = path.join(__dirname, '..', 'agents', 'msd-intel-updater.md');
describe('bug #2351: intel updater kilo layout support', () => {
let content;
test('agent file exists', () => {
assert.ok(fs.existsSync(AGENT_PATH), 'agents/msd-intel-updater.md must exist');
content = fs.readFileSync(AGENT_PATH, 'utf-8');
});
test('scope section includes layout detection step', () => {
content = content || fs.readFileSync(AGENT_PATH, 'utf-8');
const hasDetection =
content.includes('ls -d .kilo') ||
content.includes('Runtime layout detection') ||
content.includes('detected layout') ||
content.includes('layout detection');
assert.ok(
hasDetection,
'msd-intel-updater.md must instruct the agent to detect the runtime layout ' +
'(.kilo vs .claude) before resolving canonical paths (#2351)'
);
});
test('scope section maps .kilo/agents path', () => {
content = content || fs.readFileSync(AGENT_PATH, 'utf-8');
assert.ok(
content.includes('.kilo/agents'),
'scope section must include the .kilo/agents/*.md path so agent count is correct under kilo layout'
);
});
test('scope section maps .kilo/command path (not commands/msd)', () => {
content = content || fs.readFileSync(AGENT_PATH, 'utf-8');
assert.ok(
content.includes('.kilo/command'),
'scope section must include .kilo/command path — kilo uses "command/" not "commands/msd/"'
);
});
test('scope section maps .kilo/hooks path', () => {
content = content || fs.readFileSync(AGENT_PATH, 'utf-8');
assert.ok(
content.includes('.kilo/hooks'),
'scope section must include .kilo/hooks path for hook file counts'
);
});
test('scope section retains standard layout paths for .claude installs', () => {
content = content || fs.readFileSync(AGENT_PATH, 'utf-8');
assert.ok(
content.includes('agents/*.md') || content.includes('Standard `.claude` layout'),
'scope section must still document the standard .claude layout paths for non-kilo installs'
);
});
});
});
}
// ────────────────────────────────────────────────────────────────────────
// Folded from tests/bug-3290-intel-updater-layout-block.test.cjs — consolidation epic #1969 (B7 #1976)
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:bug-3290-intel-updater-layout-block (consolidation epic #1969 B7 #1976)", () => {
// allow-test-rule: source-text-is-the-product (see #3290)
// agents/msd-intel-updater.md IS the deployed agent instruction set. Asserting
// its text content tests the deployed behaviour contract, not internal implementation.
'use strict';
/**
* Regression tests for bug #3290.
*
* The "Runtime layout detection" block in msd-intel-updater.md ran
* unconditionally on every project analysed, emitting:
*
* Layout detection returned "unknown" — this project is not a MSD-system
* installation (no `.claude/msd-core/` or `.kilo/` runtime root).
*
* for every ordinary (non-MSD-framework) user project. The verdict was already
* ignored by Steps 2-6 on non-MSD projects. The block was dead-but-noisy.
*
* Fix: gate the runtime bash detection on a positive "is-this-the-framework-
* repo" check (package.json name === "@golem15/msd-core") so it runs ONLY when
* analysing the MSD framework's own repo, OR remove the block entirely if no
* downstream consumers exist.
*
* Group A — gating contract:
* The unconditional bash detection invocation must be absent OR wrapped in a
* framework-repo guard. A bare `ls -d .kilo ... || echo "unknown"` with no
* surrounding gate is the defect signature.
*
* Group B — no orphan consumers:
* Confirm no other agent, command, or workflow file reads/consumes the layout-
* detection verdict emitted by this block.
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const ROOT = path.join(__dirname, '..');
const AGENT_PATH = path.join(ROOT, 'agents', 'msd-intel-updater.md');
// ─── helpers ─────────────────────────────────────────────────────────────────
/** Walk a directory recursively and return absolute paths of all .md files. */
function walkMd(dir) {
const results = [];
if (!fs.existsSync(dir)) return results;
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const abs = path.join(dir, entry.name);
if (entry.isDirectory()) {
results.push(...walkMd(abs));
} else if (entry.isFile() && entry.name.endsWith('.md')) {
results.push(abs);
}
}
return results;
}
// ─── Group A — gating contract ───────────────────────────────────────────────
describe('bug #3290 — Group A: layout-detection block must be gated or absent', () => {
let content;
test('agent file exists', () => {
assert.ok(fs.existsSync(AGENT_PATH), 'agents/msd-intel-updater.md must exist');
content = fs.readFileSync(AGENT_PATH, 'utf-8');
});
test(
'bare unconditional detection invocation is absent — ' +
'the "ls -d .kilo ... || echo unknown" must not appear outside a framework-repo gate',
() => {
content = content || fs.readFileSync(AGENT_PATH, 'utf-8');
// The defect signature: the bash block runs unconditionally.
// We look for the exact shell one-liner that emits the verdict.
const bareDetectionPattern =
/ls -d \.kilo\b.*\|\|.*echo "?unknown"?/;
const hasBareDetection = bareDetectionPattern.test(content);
if (!hasBareDetection) {
// Block is fully removed — option B — pass.
return;
}
// Block is still present. Verify it is surrounded by a framework-repo gate.
// A valid gate checks package.json name or an equivalent positive signal
// that the current project IS the MSD framework's own repo.
const hasFrameworkGate =
content.includes('@golem15/msd-core') ||
content.includes('is-this-the-framework') ||
content.includes('framework repo') ||
content.includes('Only run') ||
/if.*package\.json.*msd-core/i.test(content) ||
/Only.*layout detection.*MSD framework/i.test(content) ||
/Only.*layout detection.*framework/i.test(content);
assert.ok(
hasFrameworkGate,
'agents/msd-intel-updater.md contains a bare unconditional layout-detection ' +
'bash block (`ls -d .kilo ... || echo unknown`) with no surrounding ' +
'framework-repo gate (#3290). ' +
'Either remove the block entirely, or wrap it in a check like:\n' +
' if [[ "$(jq -r \'.name // ""\' package.json 2>/dev/null)" == "@golem15/msd-core" ]]; then\n' +
' # ... detection block ...\n' +
' fi'
);
}
);
});
// ─── Group B — no orphan downstream consumers ────────────────────────────────
describe('bug #3290 — Group B: layout-detection verdict has no downstream consumers', () => {
const SOURCE_DIRS = [
path.join(ROOT, 'agents'),
path.join(ROOT, 'commands', 'msd'),
path.join(ROOT, 'msd-core', 'workflows'),
];
/**
* Lines that reference the three possible verdict values emitted by the
* detection block: "claude", "kilo", "unknown" — ONLY as the verdict output
* of the msd-intel-updater layout detection (not general runtime references).
*
* We look for the specific phrase "Layout detection returned" which is the
* sentinel the noisy output line uses.
*/
test('no file contains "Layout detection returned" (the noisy verdict phrase)', () => {
const matches = [];
for (const dir of SOURCE_DIRS) {
const files = walkMd(dir);
for (const file of files) {
const rel = path.relative(ROOT, file);
const src = fs.readFileSync(file, 'utf-8');
if (src.includes('Layout detection returned')) {
// Collect matching lines for the error message
const lines = src.split(/\r?\n/)
.map((l, i) => ({ line: l, n: i + 1 }))
.filter(({ line }) => line.includes('Layout detection returned'));
matches.push({ rel, lines });
}
}
}
assert.strictEqual(
matches.length,
0,
'Expected zero files to contain "Layout detection returned" (the noisy verdict ' +
'phrase from the msd-intel-updater layout-detection block). Found:\n' +
matches.map(({ rel, lines }) =>
` ${rel}:\n${lines.map(({ n, line }) => ` L${n}: ${line.trim()}`).join('\n')}`
).join('\n')
);
});
test('no agent or workflow instructs reading the layout-detection verdict output', () => {
// The verdict was: echo "kilo" | echo "claude" | echo "unknown"
// If any file references "Layout detection returned unknown" as an instruction
// to consume, that would be a consumer. We verify none exist outside of
// the producing file (msd-intel-updater.md).
const verdictConsumerPattern = /Layout detection returned.*(unknown|claude|kilo)/i;
const consumers = [];
for (const dir of SOURCE_DIRS) {
const files = walkMd(dir);
for (const file of files) {
// Exclude the producer itself — it defines the message, not consumes it
if (path.basename(file) === 'msd-intel-updater.md') continue;
const src = fs.readFileSync(file, 'utf-8');
if (verdictConsumerPattern.test(src)) {
consumers.push(path.relative(ROOT, file));
}
}
}
assert.deepStrictEqual(
consumers,
[],
'Expected no downstream consumer of the layout-detection verdict. Found:\n' +
consumers.map((f) => ` ${f}`).join('\n') +
'\nIf a consumer exists, use option A (gate) not option B (remove).'
);
});
});
});
}

View File

@@ -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, Cline, and Codex.`],
`MSD installs cleanly on ${RETIRED_NAME} 3, Cursor, 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, Cline, and Cursor.`],
`Supported agents include ${RETIRED_NAME} 3, OpenCode, and Cursor.`],
];
for (const [label, relPath, body] of CASES) {

View File

@@ -428,7 +428,7 @@ describe('model-catalog: AGENT_TO_PHASE_TYPE / AGENT_DEFAULT_TIERS (#3915)', ()
describe('model-catalog: RUNTIME_PROFILE_MAP filtering (#3915)', () => {
// The catalog's runtimeTierDefaults has runtimes whose opus/sonnet/haiku
// entries are ALL null (e.g. 'cline') as a deliberate "no defaults yet"
// entries are ALL null as a deliberate "no defaults yet"
// sentinel, and runtimes fully populated (e.g. 'claude'). This pair is
// the exact boundary the filter's `Object.keys(filtered).length > 0`
// check exists for.

View File

@@ -31,9 +31,9 @@ const OMIT_RULE_MARKER = "<!-- #2517 model-omit-on-inherit -->";
function statesOmitRule(content) {
// Canonical form: the marker block, which links the rule's single source of truth.
// Preferred for new files because it is unambiguous and greppable, and because it
// carries no literal `model=` token — the installed Hermes copy of a workflow is
// asserted to contain none outside string literals (delegate_task has no per-call
// model parameter at all), so the older phrasing cannot be used everywhere.
// carries no literal `model=` token — some installed workflow copies are
// asserted to contain none outside string literals, so the older phrasing cannot
// be used everywhere.
if (content.includes(OMIT_RULE_MARKER)) return true;
// Legacy form: the rule stated inline in the file's own words. All four files that
// predate the marker (plan-phase, execute-phase, scan, ship) match this branch, and

View File

@@ -1289,8 +1289,7 @@ describe('#3045 MAJOR — --harness-flag can now accept a bare CLI-flag value (C
// fabricated registry precondition), not a production defect — corrected
// here to prove the `--harness-flag=<value>` parser generalizes to any
// bare-CLI-flag-shaped value, not merely Cursor's specific '--worktree'
// string (which the sub-test above already pins). (Originally pinned on
// windsurf, retired in prune-runtimes.)
// string (which the sub-test above already pins).
assert.equal(
runtimes.zcode.runtime.harnessIsolationFlag,
undefined,

View File

@@ -92,7 +92,7 @@ const { formatMsdSlash, resolveRuntime } = require(
const { cleanup } = require('./helpers.cjs');
describe('formatMsdSlash — runtime-aware slash command formatter', () => {
describe('hyphen-form runtimes (claude, cursor, opencode, kilo, etc.)', () => {
describe('hyphen-form runtimes (claude, cursor, opencode, etc.)', () => {
test('emits /msd-<cmd> for claude', () => {
assert.strictEqual(formatMsdSlash('execute-phase', 'claude'), '/msd-execute-phase');
});
@@ -105,10 +105,6 @@ describe('formatMsdSlash — runtime-aware slash command formatter', () => {
assert.strictEqual(formatMsdSlash('discuss-phase', 'opencode'), '/msd-discuss-phase');
});
test('emits /msd-<cmd> for kilo', () => {
assert.strictEqual(formatMsdSlash('health', 'kilo'), '/msd-health');
});
test('unknown runtime defaults to hyphen form', () => {
assert.strictEqual(
formatMsdSlash('new-project', 'some-future-runtime'),

View File

@@ -17,7 +17,7 @@
* adapter's own error handling swallows spawn failures by design (a broken hook
* must never break a tool call). A MISSING hook script likewise never breaks
* the tool call, but since #2305 it warns loudly (once per hook file) — a
* silently-absent guard script was exactly how every Kilo guard no-opped.
* silently-absent guard script was exactly how every guard no-opped.
*/
const { test } = require('node:test');
@@ -333,7 +333,7 @@ test('tool.execute.after: Read content rewriting maps ~/.claude/msd-core paths',
test('missing hook script warns loudly but still allows (never breaks the tool call, #2305)', async (t) => {
// No hook stubs written at all → every runHook finds no file → allow, but
// each absent guard script must be warned about (once per hook file): a
// silently-missing guard is how #2305 no-opped every Kilo guard.
// silently-missing guard is how #2305 no-opped every guard.
const { mod } = buildInstalledLayout(t, {});
const warnings = [];
const realConsoleError = console.error;

View File

@@ -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 '\-\-<flag>'` line per recognized flag. Absence meant
// the flag was silently dropped, and that whitelist drifted three separate
// times (--coderabbit, then --qwen/--cursor, then the unanchored
// times (--coderabbit, then --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

View File

@@ -21,8 +21,8 @@ const ROOT = path.join(__dirname, '..');
// Product names that appear in install blocks as comment headers
const PRODUCTS = [
'Claude Code', 'Claude', 'OpenCode', 'Kilo', 'Codex', 'Copilot',
'Cursor', 'Windsurf', 'Antigravity', 'Trae', 'Cline', 'Augment',
'Claude Code', 'Claude', 'OpenCode', 'Codex',
'Cursor', 'Antigravity',
'Gemini', 'Gemini CLI',
];

View File

@@ -51,9 +51,6 @@ const RUNTIMES = [
'claude',
'codex',
'opencode',
'kilo',
'kimi',
'copilot',
'antigravity',
// 'gemini' intentionally excluded from this loop — #4709 AC#1 made it
// REFUSE (RetiredRuntimeError) on both surfaces instead of agreeing on a

View File

@@ -7,7 +7,7 @@
* (such as AGENTS.md) that would become an untracked source of truth running
* in parallel with the canonical CONTEXT.md and docs/adr/ records.
*
* Context: historically bin/install.js (the since-retired local Copilot install
* Context: historically bin/install.js (the since-retired local install
* path, issue #786) wrote an AGENTS.md to process.cwd() when run inside a repo
* checkout. If such a file is ever committed, editors and AI tools that auto-load
* repo-root instruction files will silently pick up an installer-generated
@@ -25,7 +25,7 @@ const ROOT = path.resolve(__dirname, '..');
test('repo-layout: root AGENTS.md is not git-tracked — no ad-hoc AI instruction file committed alongside CONTEXT.md', () => {
// An installer-generated AGENTS.md may exist on disk after a local install
// (historically the Copilot path, issue #786). What must NOT happen is
// (historically a retired install path, issue #786). What must NOT happen is
// committing it to git, where editors and AI tools would silently pick up
// the installer-generated stub instead of CONTEXT.md.
const r = runGit(['ls-files', '--error-unmatch', 'AGENTS.md'], {

View File

@@ -207,7 +207,7 @@ describe('reviewer lane parity — not-corruption (must NOT fire)', () => {
// (ADR-2782 D8). Instances take no part in the roster, the flag set, or uniqueness.
const withNewInstance = WORKFLOW_TEXT.replace(
'## Consensus Summary',
'## Qwen Review (qwen-turbo)\n\n{x}\n\n---\n\n## Consensus Summary',
'## Example Review (example-turbo)\n\n{x}\n\n---\n\n## Consensus Summary',
);
assert.deepStrictEqual(check({ workflowText: withNewInstance }).violations, []);
});
@@ -245,11 +245,11 @@ describe('reviewer lane parity — cross-platform and hostile input', () => {
const crlf = asCrlf(
WORKFLOW_TEXT.replace(
'<step name="invoke_reviewers">',
'<step name="invoke_reviewers">\n<!-- reviewer-lane: qwen -->',
'<step name="invoke_reviewers">\n<!-- reviewer-lane: example -->',
),
);
assert.deepStrictEqual(reasons(check({ workflowText: crlf })), [
`${PARITY_VIOLATION.BESPOKE_LEG_PRESENT}:qwen`,
`${PARITY_VIOLATION.BESPOKE_LEG_PRESENT}:example`,
]);
});

View File

@@ -4,7 +4,7 @@
* THE GOLDEN TABLE IS THE POINT OF THIS FILE. Phase 5b deleted ~640 lines of hand-authored per-CLI
* bash, and every one of those legs encoded a hard-won fix (#2494/#2605 empty output, #1698 Codex
* stdout teardown noise, #1936 OpenCode zero-output turns, #2073 Antigravity's three modes, #2176
* repo-root anchoring, #2589 no jq on stock Windows, #2794 Qwen's missing sidecar). Old and new
* repo-root anchoring, #2589 no jq on stock Windows). Old and new
* cannot literally run in parallel, so the golden table below IS the strangler-fig substitute: each
* row is the invocation the bash leg produced, and the resolver must reproduce it exactly.
*

View File

@@ -1079,10 +1079,10 @@ describe('#2605 local OpenAI-compatible lanes produce diagnosable output', () =>
});
});
// Folded from tests/fix-2794-review-qwen-empty-guard (qwen retired; cursor stands in).test.cjs (#3334/H3).
// Folded from tests/fix-2794-review-*-empty-guard.test.cjs (#3334/H3).
//
// #2794 — the qwen reviewer leg was the last one still sending stderr to /dev/null. Every other lane
// captured stderr to a `.err` sidecar and appended it to the stub (#2494/#2605); qwen wrote a bare
// #2794 — one reviewer leg was the last still sending stderr to /dev/null. Every other lane
// captured stderr to a `.err` sidecar and appended it to the stub (#2494/#2605); that leg wrote a bare
// "failed or returned empty output." with no diagnostic at all, so a missing binary, an auth prompt
// and a rate-limit were indistinguishable from each other AND from a clean empty review.
describe('#2794 cursor reviewer stderr capture', () => {

View File

@@ -5,7 +5,7 @@
*
* Root cause: `routeReviewLane`'s `deps.spawn` (msd-core/bin/msd-tools.cjs) mediate
* Windows `.cmd`/`.bat` shims through cmd.exe only when the binary name ALREADY
* carries the extension, but lane descriptors declare BARE names ('codex', 'kimi',
* carries the extension, but lane descriptors declare BARE names ('codex',
* 'agy') and nothing resolved them to the on-disk extensioned form — while
* `deps.hasBinary` scanned PATH WITH PATHEXT, so probes reported the lane
* available for a spawn that could never start.
@@ -14,10 +14,10 @@
* from msd-tools.cjs. This file exercises:
* - the resolver itself, portably, against staged fake-bin dirs (P1–P8);
* - the real CLI end-to-end on Windows: staged `codex.CMD` (primary invocation
* path, file-arg) and `kimi.CMD` (capability-probe path) invoked through
* `msd-tools review-lane invoke` with the staged dir first on PATH (W1–W2).
* path, file-arg) invoked through
* `msd-tools review-lane invoke` with the staged dir first on PATH (W1).
*
* The E2E cases stage an extensionless POSIX sh shim NEXT to each `.CMD` on
* The E2E case stages an extensionless POSIX sh shim NEXT to the `.CMD` on
* purpose: an `['', ...PATHEXT]` resolver resolves to it and re-opens the exact
* ENOENT (field-reported on Windows 11), so its presence pins the ordering.
*
@@ -151,8 +151,7 @@ describe('resolveSpawnBinary (#3275)', () => {
// ────────────────────────────────────────────────────────────────────────
// Windows-gated end-to-end: the REAL CLI seam (no injected deps), staged
// shims first on PATH, mirroring how `npm install -g` lays down CLIs.
// Pre-fix, W1 stubs with `[spawn error: ENOENT]` and W2 fails its
// capability probe — the two symptoms the issue reports.
// Pre-fix, W1 stubs with `[spawn error: ENOENT]`.
// ────────────────────────────────────────────────────────────────────────
describe('review-lane invoke on Windows (#3275)', () => {
const isWin = process.platform === 'win32';
@@ -226,43 +225,9 @@ describe('review-lane invoke on Windows (#3275)', () => {
}
});
test('W2: kimi-code lane passes its capability probe and runs through a staged kimi.CMD shim', { skip: !isWin }, () => {
const binDir = createTempDir('msd-3275-w2-bin-');
const runDir = createTempDir('msd-3275-w2-run-');
const repoRoot = createTempDir('msd-3275-w2-repo-');
try {
stageBin(binDir, {
kimi: '#!/bin/sh\nexit 1\n',
'kimi.CMD': [
'@echo off',
'if /I "%~1"=="--help" (',
' echo Usage: kimi [-m MODEL] [-p PROMPT] --output-format FORMAT',
' exit /b 0',
')',
'echo fake kimi-code review from the #3275 Windows shim',
'exit /b 0',
].join('\r\n'),
});
const { result } = invokeLane({ slug: 'kimi-code', binDir, runDir, repoRoot });
assert.equal(result.ok, true, `lane failed: ${result.reason} ${result.detail}`);
assert.equal(result.stubbed, false, `review was stubbed: ${JSON.stringify(result)}`);
const review = fs.readFileSync(path.join(runDir, 'msd-review-kimi-code.md'), 'utf8');
assert.ok(
review.includes('#3275 Windows shim'),
`review file should carry the shim's output, got: ${review}`,
);
} finally {
cleanup(binDir);
cleanup(runDir);
cleanup(repoRoot);
}
});
test('E2E wiring is aimed at the real CLI seam', () => {
// Guards the test itself: if TOOLS_PATH or the review-lane verb ever moves,
// W1/W2 would silently skip their assertions behind the win32 gate.
// W1 would silently skip their assertions behind the win32 gate.
assert.ok(fs.existsSync(TOOLS_PATH), `msd-tools.cjs not found at ${TOOLS_PATH}`);
});
});

View File

@@ -381,11 +381,11 @@ describe('#3301 write_reviews grades each lane against the plan coverage manifes
.filter((id) => id !== '07')
.map((id) => `## ${id}\n\ncovered\n`)
.join('\n');
const fx = buildCoverageFixture(ids, { qwen: body });
const fx = buildCoverageFixture(ids, { cursor: body });
t.after(() => cleanup(fx.root));
const res = runScript(shell, extractCoverageCheckBlock(), fx.root, fx.runDir, fx.env, REPO_ROOT);
assert.strictEqual(res.status, 0, `block exited ${res.status}: ${res.stderr}`);
const cov = readCoverageJson(fx.runDir, 'qwen');
const cov = readCoverageJson(fx.runDir, 'cursor');
assert.strictEqual(cov.complete, false);
assert.deepEqual(cov.missing_ids, ['07']);
assert.strictEqual(cov.total, 7);

View File

@@ -101,20 +101,20 @@ describe('reviewer config federation — provenance actually moved (#2797)', ()
});
test('a lane with no model flag and no host owns no MODEL or HOST key (#3691 narrows #2797)', () => {
// Absent-safe (ADR-2782 D4): qwen and coderabbit take neither a model
// Absent-safe (ADR-2782 D4): coderabbit takes neither a model
// argument nor a host. Under #2797 that meant "declares nothing" — the only
// way a lane owned a config key was via a model flag or a host. #3691 gave
// every CLI lane a `review.max_prompt_tokens_per_reviewer.<slug>` key, a
// third legitimate reason to own a key, so qwen/coderabbit now legitimately
// own their own budget key. `cursor` gained a real model flag (#3653) and
// third legitimate reason to own a key, so coderabbit now legitimately
// owns its own budget key. `cursor` gained a real model flag (#3653) and
// now legitimately owns `review.models.cursor` too, so it is excluded from
// this loop. The part of the #2797 invariant that still holds — a lane must
// never own a MODEL or HOST key it has no use for, or another lane's budget
// key — is what this asserts directly for the two lanes that still have
// key — is what this asserts directly for the lane that still has
// neither.
for (const [key, entry] of Object.entries(registry.configSchema || {})) {
const owner = entry && entry.owner;
if (!['qwen', 'coderabbit'].includes(owner)) continue;
if (owner !== 'coderabbit') continue;
assert.ok(
!key.startsWith('review.models.') && !key.endsWith('_host'),
`${owner} must not own a model or host key, but owns "${key}"`,
@@ -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, opencode, qwen) declares `promptBudgetKey: null`, so
// codex, opencode) 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

View File

@@ -983,7 +983,7 @@ describe('F. Lane scalars', () => {
// Behavioural parity, not just source equality: the same inputs must get the
// same verdict from both surfaces.
for (const slug of ['gemini', 'lm_studio', 'llama_cpp', '4o-mini', '2b-local', 'kimi-code']) {
for (const slug of ['gemini', 'lm_studio', 'llama_cpp', '4o-mini', '2b-local']) {
const lane = laneOverride((l) => { l.slug = slug; });
const accepted = validateReviewerBody({ id: 'x', reviewer: lane }).length === 0;
assert.equal(

View File

@@ -11,15 +11,14 @@
* 3. layout.runtime === runtime, layout.configDir === configDir, layout.scope === scope.
*
* SCOPE-FALL-THROUGH NOTE:
* The old switch() had no scope branches for 12 runtimes (cursor, codex,
* copilot, antigravity, windsurf, augment, trae, qwen, hermes, codebuddy, opencode,
* kilo), meaning scope='local' returned the same kinds as scope='global'. The 5a
* descriptors incorrectly set local:[] for those runtimes, causing 31 local-install
* test regressions. The 5b backfill sets local == global for these 12, restoring
* the old switch's scope-agnostic behaviour.
* The old switch() had no scope branches for several runtimes (cursor, codex,
* antigravity, opencode), meaning scope='local' returned the same kinds as
* scope='global'. The 5a descriptors incorrectly set local:[] for those runtimes,
* causing 31 local-install test regressions. The 5b backfill sets local == global
* for these, restoring 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=[]),
* (claude: distinct local=commands+agents),
* the STEP-0 golden matches the descriptor exactly and is left unchanged.
*
* Unknown runtime case:
@@ -44,7 +43,7 @@ const FAKE_DIR = '/tmp/fake-config-dir-dd';
// ── STEP-0 golden (captured from switch BEFORE edits) ────────────────────────
// Format: { kind, destSubpath, prefix } for each entry in kinds[].
// 'function' means we assert typeof kind.stage === 'function'.
// ADR-1235 step 1 (#1763): cursor, windsurf, augment, trae, codebuddy each gained
// ADR-1235 step 1 (#1763): cursor gained
// an `agents` kind (appended last). Goldens consciously updated post-cutover.
const GOLDEN = {

View File

@@ -308,10 +308,10 @@ describe('applySurface', () => {
assert.ok(files.includes('msd-help.md'), 'msd-help.md should be present after applySurface on missing dest');
});
test('Hermes profile shrink: stale MSD skill dirs are removed; user skills preserved', (t) => {
test('Empty-prefix profile shrink: stale MSD skill dirs are removed; user skills preserved', (t) => {
const { _syncMsdDir } = require('../msd-core/bin/lib/surface.cjs');
const base = createTempDir('msd-surface-hermes-shrink-');
const base = createTempDir('msd-surface-empty-prefix-shrink-');
t.after(() => cleanup(base));
const stagedDir = path.join(base, 'staged');
const destDir = path.join(base, 'dest');
@@ -332,8 +332,8 @@ describe('applySurface', () => {
['msd-planner', []],
]);
const hermesKind = { kind: 'skills', destSubpath: 'skills/msd', prefix: '', stage: () => stagedDir };
_syncMsdDir(stagedDir, destDir, hermesKind, manifest);
const emptyPrefixKind = { kind: 'skills', destSubpath: 'skills/msd', prefix: '', stage: () => stagedDir };
_syncMsdDir(stagedDir, destDir, emptyPrefixKind, manifest);
assert.ok(
fs.existsSync(path.join(destDir, 'msd-executor', 'SKILL.md')),
@@ -349,10 +349,10 @@ describe('applySurface', () => {
);
});
test('_syncMsdDir skills kind (hermes): preserves non-MSD user dir under skills/msd/ when kindPrefix is empty', (t) => {
test('_syncMsdDir skills kind (empty prefix): preserves non-MSD user dir under skills/msd/ when kindPrefix is empty', (t) => {
const { _syncMsdDir } = require('../msd-core/bin/lib/surface.cjs');
const base = createTempDir('msd-surface-hermes-');
const base = createTempDir('msd-surface-empty-prefix-');
t.after(() => cleanup(base));
const stagedDir = path.join(base, 'staged');
const destDir = path.join(base, 'dest');
@@ -366,10 +366,10 @@ describe('applySurface', () => {
fs.mkdirSync(userDir, { recursive: true });
fs.writeFileSync(path.join(userDir, 'SKILL.md'), '# user custom\n', 'utf8');
const hermesKind = { kind: 'skills', destSubpath: 'skills/msd', prefix: '', stage: () => stagedDir };
_syncMsdDir(stagedDir, destDir, hermesKind);
const emptyPrefixKind = { kind: 'skills', destSubpath: 'skills/msd', prefix: '', stage: () => stagedDir };
_syncMsdDir(stagedDir, destDir, emptyPrefixKind);
assert.ok(fs.existsSync(userDir), 'user-custom-skill dir must be preserved when kindPrefix is empty (Hermes)');
assert.ok(fs.existsSync(userDir), 'user-custom-skill dir must be preserved when kindPrefix is empty');
assert.ok(fs.existsSync(path.join(destDir, stem1, 'SKILL.md')), 'MSD help/SKILL.md must be copied');
});
@@ -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 at local scope: no skills kind
if (!skillsKind) return; // e.g. a local scope with no skills kind
if (typeof skillsKind.home === 'string' && skillsKind.home !== '') {
discriminatingRuntimes++;
@@ -1535,8 +1535,8 @@ describe('codex skills-kind destination: home override (#2911)', () => {
// installRuntimeArtifacts, and createRuntimeArtifactUninstallPlan — all three of
// which honor `skillsKindEntry.home ?? <install root>`. This writer originally did
// not, and would silently reproduce the #2911 duplicate-tree symptom the moment
// any combined-family runtime (opencode, kilo) gains a `home` override. Neither
// declares one today, so this test exercises the REAL production code path
// any combined-family runtime (opencode) gains a `home` override. It does not
// declare one today, so this test exercises the REAL production code path
// (installOpencodeFamilySkills, via the module-ref call convention documented at
// src/install-engine.cts:37-38) under a synthetic `home` override injected by
// monkeypatching resolveRuntimeArtifactLayout's shared module export — the same

View File

@@ -134,7 +134,7 @@ describe('writesSharedSettings — descriptor-driven equivalence', () => {
});
// ---------------------------------------------------------------------------
// Test 4: finishPermissionWriter — opencode/kilo are non-null, the rest null.
// Test 4: finishPermissionWriter — opencode/antigravity are non-null, the rest null.
// Spot-check the two non-null writers (stable curated values) plus the
// descriptor-derived null set.
// ---------------------------------------------------------------------------

View File

@@ -341,7 +341,7 @@ describe('neutralizeAgentReferences', () => {
test('uses different instruction file per runtime', () => {
const input = 'Read CLAUDE.md for instructions.';
assert.ok(neutralizeAgentReferences(input, 'GEMINI.md').includes('GEMINI.md'));
assert.ok(neutralizeAgentReferences(input, 'copilot-instructions.md').includes('copilot-instructions.md'));
assert.ok(neutralizeAgentReferences(input, 'OTHER-INSTRUCTIONS.md').includes('OTHER-INSTRUCTIONS.md'));
assert.ok(neutralizeAgentReferences(input, 'AGENTS.md').includes('AGENTS.md'));
});
@@ -564,7 +564,7 @@ describe('feat-1173: stageAgentsForRuntimeWithConverter', () => {
const calls = [];
const converter = (content) => {
calls.push(content);
return content.replace('~/.claude/', '~/.copilot/');
return content.replace('~/.claude/', '~/.opencode/');
};
const resolvedProfile = { name: 'full', skills: '*', agents: new Set() };
@@ -574,11 +574,11 @@ describe('feat-1173: stageAgentsForRuntimeWithConverter', () => {
const stagedFiles = fs.readdirSync(stagedDir).sort();
assert.deepStrictEqual(stagedFiles, ['msd-executor.md', 'msd-planner.md']);
// Converter replaced ~/.claude/ with ~/.copilot/ in all staged files
// Converter replaced ~/.claude/ with ~/.opencode/ in all staged files
for (const file of stagedFiles) {
const content = fs.readFileSync(path.join(stagedDir, file), 'utf8');
assert.ok(!content.includes('~/.claude/'), `${file}: converter must have replaced ~/.claude/`);
assert.ok(content.includes('~/.copilot/'), `${file}: converter must have injected ~/.copilot/`);
assert.ok(content.includes('~/.opencode/'), `${file}: converter must have injected ~/.opencode/`);
}
});
@@ -1894,7 +1894,7 @@ test('manager.md and autonomous.md no longer contain old "not claude" background
* description: [BETA] Offload plan phase to Claude Code's ultraplan…
*
* YAML 1.2 treats a leading `[` as the start of a flow sequence, so any
* downstream parser (gh-copilot, JetBrains' kit, etc.) fails with
* downstream parser (JetBrains' kit, etc.) fails with
* "Unexpected scalar at node end". The Antigravity
* skill+agent converters in `bin/install.js` re-emit the description
* unquoted; the Claude variant `yamlQuote(...)`s it. Bring the others

View File

@@ -159,8 +159,8 @@ describe('descriptor-driven equivalence: tilde expansion in env overrides', () =
//
// `expandTilde` used to hardcode `os.homedir()` and ignore the `home` that
// `resolveConfigHomeFromDescriptor` had already resolved from `opts.home`.
// Every configHome.env override (claude's CLAUDE_CONFIG_DIR, pi's
// PI_CODING_AGENT_DIR, antigravity, windsurf, ...) routes a tilde-prefixed
// Every configHome.env override (claude's CLAUDE_CONFIG_DIR,
// antigravity, ...) routes a tilde-prefixed
// value through this seam. A caller that injects a sandbox `home` — exactly
// what hermetic tests do to keep installs inside a temp dir — silently got
// the developer's REAL home directory back instead, both a correctness bug
@@ -206,7 +206,7 @@ describe('expandTilde honors an injected opts.home (regression)', () => {
// ── #3023 review finding 1: whitespace-only env override must fall back ──────
//
// expandTilde's old call sites gated on a bare `if (val)`, which is falsy
// only for `''`. A whitespace-only value (e.g. `PI_CODING_AGENT_DIR=' '`,
// only for `''`. A whitespace-only value (e.g. `CLAUDE_CONFIG_DIR=' '`,
// which a broken shell template can produce when a substitution is blank but
// still quoted) passed the truthy check and resolved to the literal
// three-space string instead of falling back to the descriptor default. The
@@ -214,7 +214,7 @@ describe('expandTilde honors an injected opts.home (regression)', () => {
// resolveConfigHomeFromDescriptor on `hasNonBlankOverride` (real string, at
// least one non-whitespace char) instead of bare truthiness — covering
// dot-home, dot-home-nested, all three xdg steps, and generic-agents-root
// alike (same class, same fix, not just pi's branch).
// alike (same class, same fix).
//
// Leading/trailing whitespace on an otherwise non-blank value is deliberately
// NOT trimmed (see hasNonBlankOverride's doc comment in runtime-homes.cts):
@@ -315,7 +315,7 @@ describe('#3023 review finding 1: whitespace-only env override falls back to def
// ── GOLDEN XDG SCENARIOS ──────────────────────────────────────────────────────
describe('descriptor-driven equivalence: xdg runtimes (opencode, kilo)', () => {
describe('descriptor-driven equivalence: xdg runtimes (opencode)', () => {
// opencode
test('opencode: OPENCODE_CONFIG (file-path) → dirname', () => {
const saved = clearAllEnvKeys();
@@ -810,8 +810,6 @@ describe('descriptor-driven parity: 13 non-probe registry runtimes × no-env-var
//
// Fix introduces msd-core/bin/lib/runtime-homes.cjs with first-class
// support for every supported runtime, including:
// - hermes: nested skills/msd/<skillName>/ layout (#2841)
// - cline: rules-based, returns null (no skills directory)
// - CLAUDE_CONFIG_DIR env var for Claude (was missing)
// - All other runtime-specific env vars
@@ -953,7 +951,7 @@ describe('bug #3126: runtime-homes getGlobalSkillDir', () => {
});
});
describe('getGlobalConfigDir — explicitDir override and opencode/kilo file-path precedence', () => {
describe('getGlobalConfigDir — explicitDir override and opencode file-path precedence', () => {
// ── explicitDir override ──────────────────────────────────────────────────
test('explicitDir absolute path is returned as-is (claude)', () => {
assert.strictEqual(String(getGlobalConfigDir('claude', '/tmp/x')).replace(/\\/g, '/'), '/tmp/x');
@@ -1107,5 +1105,3 @@ describe('bug #3126: buildAgentSkillsBlock resolves the agent-skills path per ru
});
});
}
// ── resolveKimiHooksTomlDir: per-runtime hooks root (#2755) ──────────────────

View File

@@ -7,7 +7,7 @@
*
* Because 1.7.0 (ADR-1016 / ADR-1239) makes runtimes pluggable data, the label
* table is CURATED (a runtime id → short label mapping that cannot be derived
* from the id alone, e.g. "Claude Code", "Qwen Code", "ZCode"). This test
* from the id alone, e.g. "Claude Code", "ZCode"). This test
* enforces the COVERAGE CONTRACT rather than a frozen per-runtime snapshot:
*
* - every runtime in the capability registry MUST resolve to a distinct,
@@ -21,8 +21,7 @@
* snapshot here.
*
* Voice: these SHORT UI labels are intentionally distinct from the descriptor
* `title` (the long product name). One prior-chain inconsistency is resolved
* by the canonical map: cline → 'Cline'.
* `title` (the long product name).
*
* ADR-1239 Phase B (#1679). Behavioral tests only: assert on returned values.
*/

View File

@@ -218,7 +218,7 @@ describe('#3651 workflow — review.models settable-set rule', () => {
// Lanes whose capability declares modelConfigKey: null — the workflow used
// to walk users into writing these keys, and config-set rejects them.
// `cursor` gained a real modelConfigKey (#3653) and is no longer keyless.
for (const keyless of ['qwen', 'coderabbit']) {
for (const keyless of ['coderabbit']) {
assert.ok(
!isValidConfigKey(`review.models.${keyless}`),
`review.models.${keyless} must not validate (lane declares no modelConfigKey)`
@@ -234,7 +234,7 @@ describe('#3651 workflow — review.models settable-set rule', () => {
const tmp = createTempProject();
t.after(() => cleanup(tmp));
runMsdTools(['config-ensure-section'], tmp);
const r = runMsdTools(['config-set', 'review.models.qwen', 'qwen-model'], tmp);
const r = runMsdTools(['config-set', 'review.models.coderabbit', 'coderabbit-model'], tmp);
assert.ok(
!r.success,
'config-set must reject a keyless lane — the exact error the old workflow steered users into'

View File

@@ -152,7 +152,7 @@ describe('skill-manifest', () => {
// bug-929: nested layout discovery
test('bug-929: discovers concrete skills nested under msd-ns-* routers', () => {
// Mirrors the on-disk shape that stageSkillsForRuntimeAsSkills emits for
// cline/qwen/hermes/augment/trae/antigravity when nested=true:
// antigravity when nested=true:
// <root>/msd-ns-workflow/SKILL.md — router (top-level)
// <root>/msd-ns-workflow/skills/plan/SKILL.md — concrete
// <root>/msd-ns-workflow/skills/execute/SKILL.md — concrete

View File

@@ -226,18 +226,16 @@ describe('slash-command namespace invariant (#3443)', () => {
/**
* Regression for #3677 — installed agent bodies leak `/msd:<cmd>` colon refs
* for Claude / Qwen / Hermes (unroutable since #2808).
* for Claude (unroutable since #2808).
*
* Root cause: `bin/install.js` agent install loop (around line 8350-8447)
* reads each agent .md, runs runtime-specific transforms via
* `convertClaudeAgentToXAgent()`, then writes the result. For:
* - Self-converting runtimes (Copilot/Codex/Cursor/Windsurf/Augment/Trae/
* Codebuddy/Cline/Antigravity/Opencode/Kilo): their converters handle
* - Self-converting runtimes (Codex/Cursor/Antigravity/Opencode): their converters handle
* namespace themselves.
* - Gemini: intentionally uses colon namespace.
* - Claude-default / Qwen / Hermes: register hyphen-form `name:` (#2808)
* but copy bodies verbatim (Qwen/Hermes do branding-only swaps; Claude
* does no namespace work). The retired `/msd:<cmd>` colon refs leak.
* - Claude-default: registers hyphen-form `name:` (#2808)
* but copies bodies verbatim (Claude does no namespace work). The retired `/msd:<cmd>` colon refs leak.
*
* Sibling fixes #3583 (SKILL.md, via #3629) and #3584 (runtime emissions, via
* #3606) covered the other two surfaces. This is the agent-body surface.

View File

@@ -7,7 +7,7 @@
"test-events-linux-node24.jsonl",
"manual: codex-config.test.cjs/codex-config-hooks.test.cjs re-measured locally (node --test --test-reporter=tap, max of 3 runs) after next split codex-config.test.cjs (#4139/#4540) — the old 127783ms entry predated the split and codex-config-hooks.test.cjs, which now holds the #3245/#4249 install()-pipeline blocks, had no entry at all (PR #4249)."
],
"file_count": 771,
"file_count": 657,
"timings": {
"active-workstream-store.test.cjs": 81,
"active-workstream-store.unit.test.cjs": 364,
@@ -54,8 +54,6 @@
"atomic-write-coverage.test.cjs": 47,
"audit-command-cutover.test.cjs": 6076,
"audit-fix-command.test.cjs": 179,
"augment-conversion.test.cjs": 77,
"augment-upgrades.test.cjs": 10336,
"autonomous-allowed-tools.test.cjs": 41,
"autonomous-converge.test.cjs": 524,
"autonomous-decomposition.test.cjs": 43,
@@ -111,12 +109,6 @@
"claude-skills-migration.test.cjs": 201,
"cleanup-branch-pruning.test.cjs": 190,
"cli-exit.test.cjs": 1316,
"cline-beforetool-upgrade.test.cjs": 72,
"cline-dispatch-degradation.test.cjs": 40,
"cline-imperative-reference.test.cjs": 76,
"cline-install.test.cjs": 3511,
"cline-model-override-upgrade.test.cjs": 51,
"cline-support.test.cjs": 82,
"clock-seam.test.cjs": 10986,
"close-phase-todos-padded-resolves.test.cjs": 798,
"close-phase-todos-stage-deletion.test.cjs": 32,
@@ -128,8 +120,6 @@
"code-review-summary-parser.test.cjs": 85,
"code-review.test.cjs": 5022,
"codebase-mapper-date-restamp.test.cjs": 138,
"codebuddy-install.test.cjs": 3763,
"codebuddy-upgrades.test.cjs": 2932,
"codex-config-hooks.test.cjs": 3454,
"codex-config.test.cjs": 189,
"codex-declarative-reference.test.cjs": 205,
@@ -164,8 +154,6 @@
"contributor-standards.test.cjs": 169,
"conventional-title.property.test.cjs": 229,
"conventional-title.test.cjs": 261,
"copilot-install.test.cjs": 55244,
"copilot-upgrades.test.cjs": 6069,
"core-utils.test.cjs": 270,
"coverage-metadata-parser.test.cjs": 10242,
"coverage-uat-routing.test.cjs": 2153,
@@ -187,10 +175,6 @@
"debugger-semantic-recall.test.cjs": 106,
"decisions.test.cjs": 13530,
"declarative-reference-antigravity.test.cjs": 1582,
"declarative-reference-augment.test.cjs": 2079,
"declarative-reference-codebuddy.test.cjs": 1824,
"declarative-reference-copilot.test.cjs": 1492,
"declarative-reference-windsurf.test.cjs": 2161,
"declarative-reference-zcode.test.cjs": 2119,
"defaults-json-fallback.test.cjs": 188,
"derive-progress.property.test.cjs": 396,
@@ -211,8 +195,6 @@
"effort-surface-axis.test.cjs": 5485,
"effort-sync-installed-runtime.test.cjs": 1763,
"emitted-attribution.test.cjs": 189330,
"emitted-caps-gate.test.cjs": 2351,
"emitted-caps.test.cjs": 231,
"emitted-provenance.test.cjs": 38803,
"emitted-sizes.test.cjs": 3269,
"ensure-runtime-build.test.cjs": 7800,
@@ -245,42 +227,6 @@
"federated-config-loadconfig.test.cjs": 620,
"federated-config.test.cjs": 261,
"few-shot-calibration.test.cjs": 87,
"fix-1700-spike-manifest-idea-scoping.test.cjs": 107,
"fix-1941-quick-worktree-stale-base.test.cjs": 68,
"fix-2068-resolve-execution-dynamic-routing.test.cjs": 5270,
"fix-2136-clock-local-today.test.cjs": 692,
"fix-2138-ship-note-lost-on-merge.test.cjs": 70,
"fix-2194-review-timeout-guidance.test.cjs": 49,
"fix-2196-debug-agent-handoff.test.cjs": 35,
"fix-2257-debug-nonterminal-resume.test.cjs": 76,
"fix-2284-hermes-agent-delegate-task-projection.test.cjs": 9484,
"fix-2285-claude-orchestration-wiring.test.cjs": 4486,
"fix-2287-deferred-items-reader.test.cjs": 2890,
"fix-2289-context-monitor-event-allowlist.test.cjs": 5841,
"fix-2297-resolve-model-ids-runtime-scoping.test.cjs": 354,
"fix-2337-add-todo-severity.test.cjs": 61,
"fix-2358-review-temp-path-scoping.test.cjs": 93,
"fix-2494-review-claude-gemini-empty-guard.test.cjs": 119,
"fix-2587-cursor-hook-workspace-roots.test.cjs": 4678,
"fix-2589-workflow-jq-dependency.test.cjs": 120,
"fix-2590-workflow-script-contract.test.cjs": 1990,
"fix-2598-opencode-background-dispatch.test.cjs": 150,
"fix-2603-kimi-code-host-matrix.test.cjs": 86,
"fix-2605-review-local-server-empty-guard.test.cjs": 168,
"fix-2608-commit-staging-failure.test.cjs": 9605,
"fix-2615-effortsurface-matrix-parity.test.cjs": 188,
"fix-2649-diagnose-issues-worktree-stale-base.test.cjs": 67,
"fix-2650-plan-phase-stall-detection.test.cjs": 44503,
"fix-2657-untrack-compiled-artifacts.test.cjs": 381,
"fix-2658-trae-runtime-detection-and-instruction-path.test.cjs": 2083,
"fix-2717-cursor-windsurf-codex-commonjs-marker.test.cjs": 9093,
"fix-2766-audit-uat-archived-and-table-shapes.test.cjs": 5786,
"fix-2794-review-qwen-empty-guard.test.cjs": 83,
"fix-2830-halted-plan-dependents.test.cjs": 11301,
"fix-2847-gap-closure-frontmatter.test.cjs": 62,
"fix-2855-phase-locator-workstream-archive-scope.test.cjs": 132,
"fix-3045-cursor-subagent-isolation.test.cjs": 14459,
"fix-3045-dispatch-isolation-resolver.test.cjs": 7355,
"fixture-builder.test.cjs": 324,
"forensics.test.cjs": 164,
"format-github-release-notes.test.cjs": 69,
@@ -329,9 +275,6 @@
"health-validation.test.cjs": 17036,
"helpers-cleanup.test.cjs": 110,
"helpers-process-isolation.test.cjs": 37,
"hermes-dispatch-upgrade.test.cjs": 33,
"hermes-imperative-reference.test.cjs": 75,
"hermes-skills-migration.test.cjs": 121,
"hook-bus.test.cjs": 50,
"hook-validation.test.cjs": 84,
"hooks-opt-in.test.cjs": 8614,
@@ -358,7 +301,6 @@
"installer-migration-authoring.test.cjs": 136,
"installer-migration-config-root-marker.test.cjs": 94,
"installer-migration-install.integration.test.cjs": 74165,
"installer-migration-pi-extension-ext.test.cjs": 61,
"installer-migration-prune-stale-pristine.test.cjs": 82,
"installer-migration-rename-msd-core.test.cjs": 265,
"installer-migration-report.test.cjs": 339,
@@ -370,53 +312,8 @@
"inventory-nested-families.test.cjs": 192,
"io.test.cjs": 2567,
"ios-scaffold-safety.test.cjs": 71,
"issue-1575-agent-descriptor-parity.test.cjs": 4530,
"issue-1855-marketplace-manifest.test.cjs": 309,
"issue-2045-third-party-skills-surface.test.cjs": 3282,
"issue-2517-runtime-aware-profiles.test.cjs": 2761,
"issue-2639-codex-toml-neutralization.test.cjs": 85,
"issue-2695-codex-hook-set.test.cjs": 27204,
"issue-2701-nul-corrupted-validators.test.cjs": 6250,
"issue-2762-plan-reviews-chunked.test.cjs": 37,
"issue-2765-brace-expansion-lockfile.test.cjs": 1918,
"issue-2771-advisor-subagent-type.test.cjs": 43,
"issue-2772-discuss-phase-text-inconsistencies.test.cjs": 74,
"issue-2828-flat-roadmap-total-phases.test.cjs": 445,
"issue-2834-codex-install-model-ordering.test.cjs": 30,
"issue-2927-reviewer-lane-overlay-invocation.test.cjs": 4610,
"issue-2939-dispatch-flatten-maxdepth.test.cjs": 62,
"issue-2940-codex-config-merge-trailing.test.cjs": 57,
"issue-2945-phase-complete-checkbox-rollback.test.cjs": 2489,
"issue-2949-phase-complete-stage3-sentinel.test.cjs": 2857,
"issue-2977-frontmatter-bom.test.cjs": 202,
"issue-429-comment-text-gate.test.cjs": 1204,
"issue-498-identity-drift-lint.test.cjs": 371,
"issue-498-package-identity.test.cjs": 127,
"issue-498-update-backup-runtime-dir.test.cjs": 30,
"issue-498-update-context.test.cjs": 608,
"issue-57-runtime-install-no-drift.test.cjs": 66,
"issue-607-cache-lineage.test.cjs": 90,
"issue-607-installer-dry-run.install.test.cjs": 959,
"issue-607-legacy-cleanup.test.cjs": 259,
"issue-69-surface-keeps-nested.test.cjs": 402,
"issue-766-plugin-manifest.test.cjs": 412,
"issue-787-cline-hooks-agents.test.cjs": 10063,
"issue-815-update-next-channel.test.cjs": 51,
"issue-844-manifest-version-sync.test.cjs": 269,
"issue-dedupe.test.cjs": 357,
"issue-version-gate.test.cjs": 286,
"kilo-imperative-reference.test.cjs": 118,
"kilo-upgrades.test.cjs": 15771,
"kimi-agent-converter.test.cjs": 10214,
"kimi-guard-normalization-parity.test.cjs": 43,
"kimi-guard-typed-payload-reads.test.cjs": 172,
"kimi-imperative-reference.test.cjs": 88,
"kimi-normalize-payload.property.test.cjs": 614,
"kimi-payload-field-shadowing.security.test.cjs": 1643,
"kimi-skill-converter.test.cjs": 34,
"kimi-tool-mapping.test.cjs": 35,
"kimi-upgrades.test.cjs": 28880,
"kimi-variant-disambiguation.test.cjs": 3560,
"learnings.test.cjs": 2925,
"lint-docs-command-form.test.cjs": 2986,
"lint-docs-required.test.cjs": 210,
@@ -527,9 +424,6 @@
"phase6-planning-capabilities.test.cjs": 72,
"phase6-review-capabilities.test.cjs": 55,
"phases-command-router.test.cjs": 1972,
"pi-extension-reachability.test.cjs": 1686,
"pi-imperative-reference.test.cjs": 65,
"pi-upgrades.test.cjs": 309,
"pick-flag.test.cjs": 4706,
"plan-bounce.test.cjs": 1658,
"plan-phase-drift-guard.test.cjs": 487,
@@ -586,9 +480,6 @@
"quick-commit-boundary.test.cjs": 73,
"quick-research.test.cjs": 4873,
"quick-session-management.test.cjs": 98,
"qwen-imperative-reference.test.cjs": 330,
"qwen-skills-migration.test.cjs": 1047,
"qwen-upgrades.test.cjs": 8100,
"reachability-check.test.cjs": 44,
"read-guard.test.cjs": 5618,
"read-injection-scanner.property.test.cjs": 22710,
@@ -719,8 +610,6 @@
"todos-done-rename-guard.test.cjs": 290,
"trace-correlation.test.cjs": 54,
"tracer-bullet.test.cjs": 2886,
"trae-imperative-reference.test.cjs": 121,
"trae-upgrades.test.cjs": 7465,
"transition-verification-gate.test.cjs": 51,
"trim-safety.test.cjs": 143,
"tsconfig-noemit.test.cjs": 7162,
@@ -755,9 +644,6 @@
"vscode-lm-tools.test.cjs": 2453,
"vscode-subagent-dispatch.test.cjs": 2682,
"windows-robustness.test.cjs": 238,
"windsurf-conversion.test.cjs": 203,
"windsurf-hooks-bridge.test.cjs": 4388,
"windsurf-install.test.cjs": 121,
"workflow-compat.test.cjs": 296,
"workflow-fragments-emission.install.test.cjs": 113902,
"workflow-fragments.property.test.cjs": 281,

View File

@@ -148,10 +148,10 @@ describe('resolveUpdateContext: runtime probing + env overrides', () => {
const fs = fakeFs({ [ver(custom)]: '1.41.0\n', [marker(custom)]: 'x' });
const r = resolveUpdateContext({
home: HOME, cwd: CWD, env: {}, fs,
preferredConfigDir: custom, preferredRuntime: 'kilo',
preferredConfigDir: custom, preferredRuntime: 'opencode',
});
assert.equal(r.scope, 'GLOBAL');
assert.equal(r.runtime, 'kilo');
assert.equal(r.runtime, 'opencode');
assert.ok(sameDir(r.msdDir, custom), `msdDir was ${r.msdDir}`);
assert.equal(r.installedVersion, '1.41.0');
});
@@ -269,7 +269,7 @@ describe('msd-tools update-context (CLI): emits the JSON contract', () => {
nodeFs.writeFileSync(path.join(tmp, 'msd-core', 'VERSION'), '1.42.0\n');
nodeFs.writeFileSync(path.join(tmp, 'msd-core', 'workflows', 'update.md'), 'x');
const r = runNode(
[MSD_TOOLS, 'update-context', '--config-dir', tmp, '--runtime', 'kilo', '--json'],
[MSD_TOOLS, 'update-context', '--config-dir', tmp, '--runtime', 'opencode', '--json'],
{ env: { ...process.env, MSD_TEST_MODE: '1' }, timeoutMs: PROBE_TIMEOUT_MS },
);
throwIfFailed(r, 'msd-tools update-context --json');
@@ -277,7 +277,7 @@ describe('msd-tools update-context (CLI): emits the JSON contract', () => {
assert.deepEqual(Object.keys(ctx).sort(), ['installedVersion', 'msdDir', 'runtime', 'scope']);
assert.equal(ctx.installedVersion, '1.42.0');
assert.equal(ctx.scope, 'GLOBAL');
assert.equal(ctx.runtime, 'kilo');
assert.equal(ctx.runtime, 'opencode');
} finally {
cleanup(tmp);
}
@@ -318,7 +318,7 @@ describe('resolveUpdateContext: parity with the old inline bash (adversarial-rev
const custom = '/opt/msd-partial';
const fs = fakeFs({ [ver(custom)]: '1.41.0\n' }); // marker absent
const r = resolveUpdateContext({
home: HOME, cwd: CWD, env: {}, fs, preferredConfigDir: custom, preferredRuntime: 'kilo',
home: HOME, cwd: CWD, env: {}, fs, preferredConfigDir: custom, preferredRuntime: 'opencode',
});
assert.equal(r.installedVersion, '0.0.0', 'VERSION-only fast path must not be trusted');
assert.equal(r.scope, 'GLOBAL');

View File

@@ -7,7 +7,7 @@
*
* Confinement rows (E1-E5) live in tests/install-write-confinement.test.cjs
* (this suite's own file-count ratchet keeps them there — see 50-test-matrix.md
* "Suites"). Call-site integration (Hermes/Claude dev-preferences migration,
* "Suites"). Call-site integration (Claude dev-preferences migration,
* the mainline msd-core copy, and C7 production-reachability) lives in
* tests/install-runtime-artifacts.test.cjs.
*

View File

@@ -290,7 +290,7 @@ test('noSectionMarkerLeaksIntoEmittedArtifacts', (t) => {
// #4570: assert against the real installed workflow bytes for every runtime,
// after composition and runtime conversion. The background parameter itself
// is runtime vocabulary (`run_in_background` becomes `background` on Hermes),
// is runtime vocabulary (it can be renamed per runtime),
// so parity is pinned on the shared gate and its two semantic branches.
for (const relativePath of [PLAN_PHASE_REL, CHUNKED_PLAN_REL, STALL_HELPERS_REL]) {
const installedPath = path.join(configDir, relativePath);

View File

@@ -43,12 +43,6 @@ const MODULE_OWNED_HOOKS = new Set([
'msd-cursor-stop.js',
'msd-cursor-subagent-start.js',
'msd-cursor-subagent-stop.js',
// Windsurf/Cascade blocking hooks — registered by writeWindsurfHooksJson via the
// WINDSURF_EVENT_SCRIPT_MAP indirection (src/runtime-hooks-surface.cts), never a
// literal buildHookCommand(..., '<hook-name>', ...) call this source-scan matches.
// Validated behaviorally by tests/windsurf-hooks-bridge.test.cjs.
'msd-windsurf-pre-write.js',
'msd-windsurf-pre-command.js',
// msd-check-update-worker.js is an implementation detail of msd-check-update.js
// (spawned internally via child_process.spawn), never itself registered as a
// hook entry point.