Phase 2 of #3660 / ADR-3660. Routes both lifecycle verbs through the
Runtime Artifact Layout Module landed in Phase 1 (#3663):
- Add installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile)
and uninstallRuntimeArtifacts(runtime, configDir, scope) as the public
orchestrators. Both pre-prune stale gsd-* entries before staged copy;
installRuntimeArtifacts brackets the prune+copy with preserveUserArtifacts /
restoreUserArtifacts so user-owned content (e.g. gsd-dev-preferences) survives
wipe-and-replace for claude/qwen/hermes runtimes.
- Add applyRuntimeContentRewritesInPlace as the per-runtime path/branding
post-stage step (preserves byte-output equivalence with the legacy
copyCommandsAs* pipeline, including Qwen/Hermes branding rewrites).
- Add _copyStaged, _removeGsdEntries kind-aware filesystem helpers.
- Add _runLegacyInstallMigrations, _runLegacyUninstallCleanup as thin
dispatchers over existing ADR-0008 legacy migrations (Hermes flat->nested
per #2841, dev-preferences-as-skill per #2973). For Hermes, also clean up
the intermediate skills/gsd/gsd-*/ layout that pre-Phase-2 installs left
on disk.
- Delete the 9 copyCommandsAs*Skills functions (Codex / Cursor / Windsurf /
Trae / CodeBuddy / Copilot / Claude / Antigravity / Augment) and the
_copyCommandsAsSkillsViaConverter helper. All test entry points migrated
to call installRuntimeArtifacts directly through the unified seam.
- Collapse the 9-branch uninstall ladder to one uninstallRuntimeArtifacts
call plus preserved non-layout side-effects (Codex TOML, Copilot
instructions, hooks).
- Unify install dispatcher: a single _isSkillsRuntime gate routes all 11
skills runtimes through installRuntimeArtifacts for both full and core/
minimal profiles. Removes 11 per-runtime if-else branches (3 minimal-mode
shim branches + 8 dead after-the-gate branches).
Net delta on bin/install.js: 11,495 -> 11,174 (-321 LOC).
New tests:
- tests/install-uninstall-layout-loop.test.cjs (34 tests) - per-runtime
fixture assertions on install/uninstall/legacy-migration ordering.
- tests/install-hermes-regressions.test.cjs (6 tests) - covers the six
defects surfaced by iterative review: Hermes upgrade leaves stale dirs,
--hermes --profile=core fall-through, --qwen --profile=core fall-through,
minimal-mode dev-preferences migration skipped (Hermes/Qwen/Claude-global),
and ordering bug in _runLegacyInstallMigrations.
Existing tests (10,038 prior + 40 new) all green: 10,078/10,078 pass.
Refs #3664
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#2982): extend no-source-grep lint to catch var-binding readFileSync.includes()
The base lint (scripts/lint-no-source-grep.cjs) only catches
readFileSync(...).<text-method>() chained directly. The much more
common var-binding form escapes it:
const src = fs.readFileSync(p, 'utf8');
// 50 lines later
if (src.includes('foo')) {} // ← still grep, lint missed it
Scan of the test suite found ~141 files using this pattern.
Implementation built TDD per #2982 with structured-IR assertions:
scripts/lint-no-source-grep-extras.cjs
- detectVarBindingViolations(src) — pure detector, two passes:
pass 1 collects vars bound from readFileSync, pass 2 finds any
<var>.<includes|startsWith|endsWith|match|search>( on those vars.
- detectWrappedAssertOkMatch(src) — flags
assert.ok(<expr>.match(...)) which escapes the assert.match rule.
- VIOLATION enum exposes stable codes for tests to assert on.
scripts/lint-no-source-grep.cjs
- Wires the new detectors into the existing per-file check; one
additional violation row per file with the first 3 sample tokens.
tests/bug-2982-lint-var-binding.test.cjs
- 13 tests, all assertions on typed VIOLATION enum / structured
records. Covers all 5 text-match methods, multi-var, no-bind,
string literal (must NOT trigger), wrapped assert.ok(.match),
and assert.match (must NOT double-flag).
Migration backlog (#2974 expanded scope):
- 42 files annotated `// allow-test-rule: source-text-is-the-product`
(legitimate — they read .md/.json/.yml files whose deployed text
IS the product)
- 3 files annotated `// allow-test-rule: pending-migration-to-typed-ir [#2974]`
(read .cjs/.js source — clear migration debt)
- 95 files annotated `pending-migration-to-typed-ir [#2974]` with
`Per-file review may reclassify as source-text-is-the-product
during migration` (mixed — manual review under #2974)
After this lands the lint reports 0 violations on main; new
violations in PRs surface immediately.
Closes#2982
Refs #2974
* test(#2982): fix truncated test name per CR
The label ended with a bare '(' from a copy-paste mishap. Now reads
'does NOT flag .matchAll(...) — matchAll is not match, so
assert.ok(.matchAll(...)) is not flagged'.
* chore(#2982): add changeset fragment for PR #2985
* chore(#2982): add changeset fragment for PR #2985