feat(cli): detect superseded skill directories on startup (swamp-club#326)#1375
Conversation
…#326) When the CLI binary is upgraded but `swamp repo upgrade` is not run, superseded skill directories remain in the repo, causing AI agents to see stale skill descriptions with overlapping triggers. This adds a lightweight startup check that detects superseded skill directories across all enrolled tools and emits a deferred warning prompting the user to run `swamp repo upgrade`. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Adversarial Review
Critical / High
No critical or high severity findings.
Medium
-
src/cli/mod.ts:868—SKILL_DIRSlookup usesAiTooltype but the map isRecord<string, string>, silently skipping"none"When
marker.toolscontains"none"(which should betools: []per the type docs, but the union type still includes it for read-side compat), the code hitsSKILL_DIRS["none"]which returnsundefined, so theif (!dir) continueskips it harmlessly. This is correct behavior, but the intent is implicit — it works by accident of"none"not being a key in the map. Not a bug, but fragile if someone later adds a"none"mapping toSKILL_DIRS. -
src/cli/mod.ts:863-865—resolvePrimaryToolfallback whenmarker?.toolsis an empty arrayIf
marker.toolsis[](the normalized form oftool: "none"),marker?.tools?.lengthevaluates to0(falsy), so the code falls through toresolvePrimaryTool(marker)which returnsmarker?.tools?.[0] ?? "claude". Withtools: [],tools?.[0]isundefined, so it falls back to"claude"— scanning the claude skill dir even though the user explicitly opted out of tool integration. This is a minor inconsistency: an explicitly-no-tools repo would still get superseded-skill warnings for.claude/skills/. In practice this is harmless noise, but it's semantically incorrect.
Low
-
src/domain/repo/repo_service_test.ts:2508-2510— test for nonexistent directory relies onDeno.statthrowing for each entry rather than the parent being absentdetectSupersededSkills("/tmp/nonexistent-dir-326")works because everyDeno.stat(join("/tmp/nonexistent-dir-326", name))throwsNotFound. This is fine, but the test doesn't verify the behavior it implies (that a missing parent dir returns empty) — it just happens to work because child paths inside a nonexistent parent also don't exist. IfdetectSupersededSkillswere ever changed toDeno.readDirthe parent first, this test would break. Very minor — the current implementation makes this a non-issue.
Verdict
PASS — Clean, well-scoped change. The new detectSupersededSkills function is straightforward, the error handling follows the established non-fatal pattern, and the deferred-warning integration is correct. The Set-based dedup across multiple tools is a nice touch. The medium findings are cosmetic edge cases in uncommon configurations, not correctness issues.
There was a problem hiding this comment.
Code Review
Clean, well-scoped PR. The new startup check follows existing patterns (checkForMissingPulledExtensions), the domain logic lives in the right place, and the tests cover the key cases.
Blocking Issues
None.
Suggestions
- Minor —
SUPERSEDED_SKILLSexport widening: The type annotationexport const SUPERSEDED_SKILLS: readonly string[]widens the inferred tuple type. This works fine since the constant is only iterated, butas constwithsatisfies readonly string[]would preserve the literal types if they ever become useful downstream. Purely cosmetic — no action needed.
What looks good
- DDD:
detectSupersededSkillsis a stateless domain service function onrepo_service.ts, orchestrated by the CLI layer inmod.ts. Clean separation. - Import boundary: The CLI imports from
../domain/repo/paths, which is the established pattern (matchesresolve_extension_files.ts,commands/audit.ts, etc.). Thelibswamp/mod.tsrestriction applies only to libswamp internals — no violation here. - Test coverage: Three tests covering empty dir, detection of superseded dirs (with a non-superseded control), and nonexistent directory. The
withTempDirhelper already has the Windows EBUSY catch. - Error handling: Both
detectSupersededSkills(per-entry catch) andcheckForSupersededSkills(outer catch) are intentionally non-fatal, matching the deferred-warning design. - LogTape formatting: Warning message uses bare interpolation (
logger.warn\${warning.error}``) per project convention. - Design doc:
design/repo.mdupdated with the feature description.
Summary
swamp-extension-model) across all enrolled tools and emits a warning prompting the user to runswamp repo upgradeDeferredWarninginfrastructure alongsidecheckForMissingPulledExtensions, so the warning appears after logging initializes and never blocks startupclaude) for repos without an explicittoolsarray in their markerTest Plan
deno check— passesdeno lint— passesdeno fmt --check— passesdeno run test— 5877 tests pass~/code/swamp-ubiquity— warning fires with stale dirs, disappears afterswamp repo upgrade🤖 Generated with Claude Code