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