Skip to content

refactor(skills): collapse installed-skill walkers into one inventory module - #317

Merged
BlackHole1 merged 2 commits into
mainfrom
refactor/skills-installed-inventory
Jul 27, 2026
Merged

refactor(skills): collapse installed-skill walkers into one inventory module#317
BlackHole1 merged 2 commits into
mainfrom
refactor/skills-installed-inventory

Conversation

@BlackHole1

Copy link
Copy Markdown
Member

Summary

The skills area's central fact — "skill X from package P at version V, present on agents A,B" — was re-derived by five independent filesystem walkers with divergent merge rules:

Walker Rule it invented
installed-managed-skills.ts (shared by check-update / recommend / uninstall) host-first-wins by name, across all kinds
update.ts:359 private same-name clone registry-only first-wins + a hostNames union that nothing ever read
sync.ts collector max-semver per (package, skill), canonical wins ties
info-inventory.ts host scanner identity-key grouping (kind+name+package) with a shadow-host second pass
auto-sync.ts / legacy-gpt-image-2-cleanup.ts copy-pasted directory scans

Concrete symptom: check-update and sync upload could report two different versions for the same skill, and check-update/uninstall disagreed with update about whether a shadowed registry skill was installed at all. None of these divergences were pinned by any test.

This PR lands skills/installed-skills.ts as the single owner of that fact:

  • readInstalledSkills(env, settingsFilePath) walks the canonical registry root and every available host once, returning one row per skill identity (kind + packageName + name) with a per-agent copy breakdown (state: managed | unmanaged | unparseable).
  • The row hides the unified rules: installed = at least one copy with parseable .oo-metadata.json; installed version = highest semver across copies (ties keep the canonical-then-agent-order copy); rows ordered bundled (embedded order) → registry → local, names sorted within each kind; metadata-less/unparseable copies attach to a same-name installed row instead of forming rows.
  • update, check-update, sync, info/list, recommend, and package uninstall now project from the shared rows. Deliberately outside the seam: locate (documented SKILL.md-presence contract), auto-sync's repair loop, and self-uninstall's physical-path walk — the first two now merely reuse the exported directory-scan primitive.

Behavior changes (all in previously untested divergent-copy states)

  1. check-update, sync upload, list/info, and recommend now report one installed version — the highest copy. check-update previously reported whichever host copy agent declaration order found first; list/info's top-level version previously took the first non-null host copy. Per-host repair logic (update, check-update status) still inspects each copy and is unchanged.
  2. A same-name bundled/local host copy no longer hides a registry skill from check-update and package uninstall (both now agree with update, which already ignored non-registry copies).
  3. Minor: the inventory read is tolerant of metadata-file IO errors (they surface as unparseable copies, as skills list already did) instead of aborting the whole command.

The skills list/info --json field shape, ordering guarantees, shadow-host semantics, and summary counts are unchanged.

Tests

  • New installed-skills.test.ts: table tests over a seeded tmpdir for precedence, max-semver + tie-break, identity separation, shadow attach/drop, canonical-root filtering, ordering, legacy metadata, and the selectors.
  • New CLI pins: check-update divergent-copy version, check-update + uninstall bundled-shadow fixes, sync upload divergent-copy version.
  • resolveHostControlState unit tests migrated to the copy-state input shape; the 901-line list.cli.test.ts snapshot suite passes unchanged.

Docs

docs/commands.md / docs/commands.zh-CN.md: documented currentVersion as the highest installed copy (same answer as sync upload).

All gates pass: lint:fix, ts-check, knip, bun run test (1680 pass / 0 fail).

… module

The area's central fact — "skill X from package P at version V, present on
agents A,B" — was re-derived by five independent filesystem walkers with
divergent precedence rules: the shared merge in installed-managed-skills.ts
(host-first-wins by name across kinds), a private same-name clone in update.ts
(registry-only, hostNames union that was never read), sync.ts's collector
(max-semver per package+skill), info-inventory.ts's host scanner (identity-key
grouping with a shadow-host second pass), plus copy-pasted directory scans in
auto-sync.ts and legacy-gpt-image-2-cleanup.ts.

Land skills/installed-skills.ts as the single owner of that fact:

- readInstalledSkills(env, settingsFilePath) walks the canonical registry
  root and every available host once and returns one row per skill identity
  (kind + packageName + name) with a per-agent copy breakdown.
- The row hides the unified rules: installed = at least one copy with
  parseable .oo-metadata.json; installed version = highest semver across
  copies (ties keep the canonical-then-agent-order copy); rows ordered
  bundled-in-embedded-order, then registry, then local, names sorted within
  each kind; metadata-less and unparseable copies attach to a same-name
  installed row as agent copies instead of forming rows.
- update, check-update, sync, info/list, recommend, and package uninstall now
  project from the shared rows; locate (documented SKILL.md-presence
  contract), auto-sync's repair loop, and self-uninstall's physical-path walk
  deliberately stay outside the seam.

Behavior fixes in previously untested divergent-copy states:

- check-update and sync upload now report the same installed version (the
  highest copy); check-update previously reported whichever host copy the
  agent declaration order found first.
- A same-name bundled/local host copy no longer hides a registry skill from
  check-update and package uninstall (both now agree with update, which
  already ignored non-registry copies).

The merge rules gain direct table tests in installed-skills.test.ts; new CLI
tests pin both fixes. The `skills list/info` JSON contract and documented
inventory ordering are unchanged.
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 12 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 149331c0-66a9-48e0-802d-72e4fb93afef

📥 Commits

Reviewing files that changed from the base of the PR and between c744a88 and 3e8cb60.

📒 Files selected for processing (9)
  • docs/commands.md
  • docs/commands.zh-CN.md
  • src/application/commands/skills/auto-sync.ts
  • src/application/commands/skills/check-update.ts
  • src/application/commands/skills/legacy-gpt-image-2-cleanup.ts
  • src/application/commands/skills/managed-skill-uninstall.ts
  • src/application/commands/skills/sync.ts
  • src/application/commands/skills/uninstall.ts
  • src/application/commands/skills/update.ts
✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch refactor/skills-installed-inventory

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/application/commands/skills/sync.ts (1)

1-1: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Handle canonical-only registry rows consistently

readInstalledSkills() can emit registry rows with canonical but no agents, and isInstalledRegistrySkill()/installedRegistrySkillNamesForPackage() still treat them as installed. That makes sync.ts upload them as active, while text uninstall throws if nothing is removable; the JSON path maps the same case to not-applicable. Either exclude these rows from package resolution/sync or make both uninstall paths treat them the same way.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/application/commands/skills/sync.ts` at line 1, Update
isInstalledRegistrySkill() and installedRegistrySkillNamesForPackage() so
registry rows containing canonical without agents are not treated as installed
or active during sync. Ensure sync.ts excludes these rows from package
resolution/upload, and keep text and JSON uninstall behavior consistent by
treating them as not-applicable when no removable agents exist.
🧹 Nitpick comments (4)
src/application/commands/skills/info-inventory.test.ts (1)

108-108: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Compose basePath with join() instead of a POSIX literal.

As per coding guidelines, "Never assume POSIX path separators in code, tests, snapshots, or assertions. Use node:path helpers such as join(), resolve(), and relative() for path construction."

♻️ Optional cleanup
-    const basePath = "/tmp/host/path";
+    const basePath = join(tmpdir(), "host", "path");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/application/commands/skills/info-inventory.test.ts` at line 108, Update
the basePath setup in the inventory test to construct the path with the
appropriate node:path join helper instead of a POSIX-style literal, preserving
the intended /tmp, host, and path segments.

Source: Coding guidelines

src/application/commands/skills/installed-skills.ts (1)

228-257: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant rowsByIdentity.set on every host copy.

set is only needed when the row was just created; re-setting an existing row each iteration is a no-op that obscures intent.

♻️ Optional cleanup
-        const row = rowsByIdentity.get(identityKey)
-            ?? createRow(copy.name, copy.metadata);
+        let row = rowsByIdentity.get(identityKey);
+
+        if (row === undefined) {
+            row = createRow(copy.name, copy.metadata);
+            rowsByIdentity.set(identityKey, row);
+        }
+
         const version = readMetadataVersion(copy.metadata);
-
-        rowsByIdentity.set(identityKey, row);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/application/commands/skills/installed-skills.ts` around lines 228 - 257,
Update the host-copy loop around rowsByIdentity and createRow so
rowsByIdentity.set is called only when a new row is created; preserve the
existing row lookup and agent append behavior for already-known identity keys.
src/application/commands/skills/check-update.ts (1)

128-137: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Hosts are resolved twice per invocation.

resolveAvailableManagedSkillHosts(context.env) runs here and again inside readInstalledSkills, duplicating host probing. Consider having readInstalledSkills accept pre-resolved hosts (or return them) so the handler's guard and the scan share one resolution.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/application/commands/skills/check-update.ts` around lines 128 - 137,
Update the check-update handler and readInstalledSkills flow so
resolveAvailableManagedSkillHosts(context.env) executes only once per
invocation. Pass the resolved hosts into readInstalledSkills, or otherwise reuse
a returned resolution, while preserving the empty-host guard before scanning
installed skills.
src/application/commands/skills/sync.cli.test.ts (1)

82-99: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate canonical-path setup logic.

This block recomputes storePaths/canonicalDirectory that seedRegistrySkill already derives internally, just to overwrite the canonical metadata with a diverged version. Consider extending the helper (e.g., an optional canonicalOverrideVersion param) or adding a small helper for "diverge canonical copy" so this path-resolution isn't duplicated per test.

As per coding guidelines, "In test files, extract repeated setup (mock, stub, or setup objects) into a local factory function at the bottom of the file."

♻️ Example helper extension
 export async function seedRegistrySkill(options: {
     sandbox: Awaited<ReturnType<typeof createCliSandbox>>;
     skillName: string;
     packageName: string;
     version: string;
+    canonicalVersionOverride?: string;
 }): Promise<void> {
     ...
+    if (options.canonicalVersionOverride !== undefined) {
+        await writeFile(
+            resolveManagedSkillMetadataFilePath(canonicalDirectory),
+            renderSkillMetadataJson(createRegistrySkillMetadata({
+                packageName: options.packageName,
+                version: options.canonicalVersionOverride,
+            })),
+        );
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/application/commands/skills/sync.cli.test.ts` around lines 82 - 99,
Remove the duplicated storePaths and canonicalDirectory resolution from the test
setup around seedRegistrySkill. Extend seedRegistrySkill with an optional
canonicalOverrideVersion, or add a local helper at the bottom of the test file
that diverges the canonical copy using the paths it derives, then use it to
write the 0.3.0 metadata while preserving the existing test behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/application/commands/skills/update.ts`:
- Around line 361-364: Update the default update filter in
src/application/commands/skills/update.ts lines 361-364 to select registry
entries by inventory kind only, removing the skill-name bundled check. Apply the
same registry-only filter to the JSON path at
src/application/commands/skills/update.ts lines 701-702, preserving registry
rows even when their names match bundled skills.

---

Outside diff comments:
In `@src/application/commands/skills/sync.ts`:
- Line 1: Update isInstalledRegistrySkill() and
installedRegistrySkillNamesForPackage() so registry rows containing canonical
without agents are not treated as installed or active during sync. Ensure
sync.ts excludes these rows from package resolution/upload, and keep text and
JSON uninstall behavior consistent by treating them as not-applicable when no
removable agents exist.

---

Nitpick comments:
In `@src/application/commands/skills/check-update.ts`:
- Around line 128-137: Update the check-update handler and readInstalledSkills
flow so resolveAvailableManagedSkillHosts(context.env) executes only once per
invocation. Pass the resolved hosts into readInstalledSkills, or otherwise reuse
a returned resolution, while preserving the empty-host guard before scanning
installed skills.

In `@src/application/commands/skills/info-inventory.test.ts`:
- Line 108: Update the basePath setup in the inventory test to construct the
path with the appropriate node:path join helper instead of a POSIX-style
literal, preserving the intended /tmp, host, and path segments.

In `@src/application/commands/skills/installed-skills.ts`:
- Around line 228-257: Update the host-copy loop around rowsByIdentity and
createRow so rowsByIdentity.set is called only when a new row is created;
preserve the existing row lookup and agent append behavior for already-known
identity keys.

In `@src/application/commands/skills/sync.cli.test.ts`:
- Around line 82-99: Remove the duplicated storePaths and canonicalDirectory
resolution from the test setup around seedRegistrySkill. Extend
seedRegistrySkill with an optional canonicalOverrideVersion, or add a local
helper at the bottom of the test file that diverges the canonical copy using the
paths it derives, then use it to write the 0.3.0 metadata while preserving the
existing test behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 992881bb-6e8f-41ca-8065-c48f89d48f74

📥 Commits

Reviewing files that changed from the base of the PR and between a41bf88 and c744a88.

📒 Files selected for processing (19)
  • docs/commands.md
  • docs/commands.zh-CN.md
  • src/application/commands/skills/__tests__/helpers.ts
  • src/application/commands/skills/auto-sync.ts
  • src/application/commands/skills/check-update.cli.test.ts
  • src/application/commands/skills/check-update.ts
  • src/application/commands/skills/info-inventory.test.ts
  • src/application/commands/skills/info-inventory.ts
  • src/application/commands/skills/installed-managed-skills.ts
  • src/application/commands/skills/installed-skills.test.ts
  • src/application/commands/skills/installed-skills.ts
  • src/application/commands/skills/legacy-gpt-image-2-cleanup.ts
  • src/application/commands/skills/managed-skill-uninstall.ts
  • src/application/commands/skills/recommend/plan.ts
  • src/application/commands/skills/sync.cli.test.ts
  • src/application/commands/skills/sync.ts
  • src/application/commands/skills/uninstall.cli.test.ts
  • src/application/commands/skills/uninstall.ts
  • src/application/commands/skills/update.ts
💤 Files with no reviewable changes (1)
  • src/application/commands/skills/installed-managed-skills.ts

Comment thread src/application/commands/skills/update.ts
Resolves the predicted overlap with #316 (skill-directory-state): per-host
probes in check-update/update/uninstall/auto-sync keep main's
readSkillDirectoryState / isCurrentRegistryPublication implementations, the
data-source layer keeps this branch's readInstalledSkills projection, and the
private directory-scan copies deleted on both sides stay deleted (auto-sync
and legacy-gpt-image-2-cleanup now import the shared
readSkillsDirectoryEntries). info-inventory keeps this branch's row
projection; main's readHostScan semantics (not-directory and IO errors read
as unparseable) are already covered by the inventory module's copy states.
@BlackHole1
BlackHole1 merged commit afd0213 into main Jul 27, 2026
6 checks passed
@BlackHole1
BlackHole1 deleted the refactor/skills-installed-inventory branch July 27, 2026 10:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant