Skip to content

[#238] feat: fence-aware skill ref rewrite + idempotent flatten/prefix#269

Merged
rucka merged 4 commits into
mainfrom
feature/#238-skill-install-flatten-prefix
Jul 8, 2026
Merged

[#238] feat: fence-aware skill ref rewrite + idempotent flatten/prefix#269
rucka merged 4 commits into
mainfrom
feature/#238-skill-install-flatten-prefix

Conversation

@rucka

@rucka rucka commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

PR Information

PR Title: [#238] feat: fence-aware skill ref rewrite + idempotent flatten/prefix
Story/Epic: #238 (parent epic #213)
Type: Feature
Priority: P0 (Must-Have)
Assignee: N/A
Reviewers: N/A
Labels: feature

Summary

What Changed

Extends the skills asset-registry copy pipeline (flatten + prefix) with token-based cross-reference rewriting and idempotency across repeated install/update runs:

  • A skill-name registry (buildSkillNameMap) built from the directory renames the flatten/prefix copy already produces.
  • A fence-aware markdown rewriter (rewriteSkillReferences / findSkillReferences) that rewrites /oldName invocation tokens to /newName, skipping fenced code blocks (inline code spans are still rewritten).
  • A persisted manifest (.pair/.skill-name-map.json) recording the previous run's name map, enabling reconcileSkillNameRegistry to compute a rename-transition map and an orphaned-reference set across runs (handles prefix changes and skill removal without leaving stale content or corrupting already-installed files).
  • Mirror-behavior cleanup (cleanupStaleTransformedEntries) that removes stale flattened/prefixed top-level directories left over from a previous run (e.g. after a prefix change or a skill removed from source).

Why This Change

Without this, an external KB had to flatten its skill tree at the source and hand-repair cross-references, because the installer never rewrote them. This story removes that class of manual work end-to-end: the dataset stays nested at the source (D32), and the installer's flatten/prefix/rewrite pipeline handles cross-references, relative links, and idempotent updates.

Story Context

User Story: As a KB maintainer (official or external), I want the CLI to install skills from the nested dataset layout by flattening, prefixing AND rewriting cross-references inside skill bodies (/skill invocations and relative links), idempotently on update, so that no KB ever needs to restructure its skills at the source to make them installable on AI tools.
Acceptance Criteria: AC1–AC6 in issue #238 (flat installed layout with matching frontmatter; token-based invocation rewriting via registry; relative-link depth rewriting; byte-identical idempotent round-trip; external-KB parity; fenced-block exclusion).

Changes Made

Implementation Details

  • Change 1: buildSkillNameMap (T1) — derives {shortName: installedName} from the dir-mapping state already collected during flatten/prefix copy; skips no-op renames.
  • Change 2: rewriteSkillReferences / findSkillReferences (T2) — boundary-aware, longest-name-first token rewrite/detection, routed through a new transformOutsideFences line-state-machine that tracks CommonMark-style fence delimiters (backtick and tilde, ≤3 leading spaces, backtick-free info-string check) so fenced example content is left untouched while inline code spans are still rewritten.
  • Change 3: skill-name-manifest.ts (T3) — readSkillNameManifest/writeSkillNameManifest/buildTransitionMap/findOrphanedInstalledNames/mergeSkillNameMaps, plus reconcileSkillNameRegistry and detectOrphanedSkillReferences in apps/pair-cli/src/registry/skill-refs.ts, wired into both install and update handlers (replacing the old conditional applySkillRefsToNonSkillRegistries call).
  • Change 4: cleanupStaleTransformedEntries in copyPathOps.ts (T3) — removes top-level flattened/prefixed entries no longer produced by the current source+config, invoked when defaultBehavior === 'mirror'.

Files Changed

  • Modified: apps/pair-cli/src/commands/install/handler.ts, apps/pair-cli/src/commands/update/handler.ts, apps/pair-cli/src/registry/skill-refs.ts, packages/content-ops/src/ops/skill-reference-rewriter.ts, packages/content-ops/src/ops/copyPathOps.ts, packages/content-ops/src/index.ts, apps/website/content/docs/reference/skill-management.mdx
  • Added: packages/content-ops/src/ops/skill-name-manifest.ts (+ test), apps/pair-cli/src/commands/update/idempotent-skill-registry.test.ts
  • Deleted: none
  • Renamed: none

Database Changes

  • Schema Changes: N/A
  • Migration Scripts: N/A
  • Data Changes: N/A
  • Performance Impact: N/A — no database involved

API Changes

  • New Endpoints: N/A — CLI/library, no HTTP API
  • Modified Endpoints: N/A
  • Breaking Changes: N/A
  • Documentation: New content-ops exports (findSkillReferences, readSkillNameManifest, writeSkillNameManifest, buildTransitionMap, findOrphanedInstalledNames, mergeSkillNameMaps, SkillNameManifest) documented via docstrings in source; consumer-facing behavior documented in skill-management.mdx.

Testing

Test Coverage

  • Unit Tests: skill-reference-rewriter.test.ts (fence detection, boundary matching, inline-code rewriting), skill-name-manifest.test.ts (manifest read/write/transition/orphan, malformed-manifest tolerance), skill-refs.test.ts (reconcileSkillNameRegistry, detectOrphanedSkillReferences, manifest path resolution), copyPathOps.test.ts (mirror-behavior stale-entry cleanup).
  • Integration Tests: apps/pair-cli/src/commands/update/idempotent-skill-registry.test.ts — drives handleInstallCommand/handleUpdateCommand end-to-end against an in-memory fixture dataset.
  • End-to-End Tests: N/A — no live Claude Code/Cursor discovery test was run (see Detailed Comments; this is a known, documented gap from the issue's own task notes, not introduced by omission here).
  • Manual Testing: N/A — fully automated per the story's testing strategy.

Test Results

Test Suite: ✅ Passing
  @pair/content-ops: 42 files, 612 tests passed
  @pair/pair-cli: 66 files, 770 tests passed (clean run; see review comment for a transient
    timeout-flake note on an unrelated first run under system load)
Coverage: not separately measured for this diff
Performance Tests: N/A
Security Scan: no automated scan run; manual review found no issues (see review comment)
Linting: ✅ Clean (turbo lint, part of quality-gate)

Testing Strategy

  • Happy Path: install onto an empty project with flatten+prefix → flat, prefixed skill directories with rewritten cross-references and relative links.
  • Edge Cases: name collision after flattening (hard error), invocation of a removed/disabled skill (left as-is + warning), prefix change between installs (manifest-driven transition, not string-matching).
  • Error Handling: collision detection throws IO_ERROR with a message matching /collision/i; malformed/missing manifest treated as "no previous install" rather than throwing.
  • Performance: not benchmarked; line-oriented fence tracking and per-line regex rebuilding are O(lines × registry entries) but registries are small (dozens of skills), no measurable impact expected.

Quality Assurance

Code Quality Checklist

  • Code follows established style guides and conventions (pure-function + async-orchestrator pattern matching the existing link-rewriter.ts)
  • Functions and classes have appropriate documentation (module docstrings explain rationale, including the two intentionally-kept known limitations)
  • Error handling implemented for edge cases (malformed manifest, missing target, collision)
  • Security best practices followed — see Detailed Comments for one manifest-JSON-parsing note (non-blocking)
  • Performance considerations addressed (reuses already-collected copy-time state, no extra directory walks)
  • No debugging code or console logs left behind (uses the project's logger)

Review Areas

  • Business Logic: matches AC1–AC6 (see Definition of Done section below)
  • Code Structure: consistent with existing content-ops patterns
  • Error Handling: one latent gap found — see Detailed Comments (mirror cleanup + top-level loose files)
  • Performance: reviewed, no material concern
  • Security: reviewed, no material concern
  • Testing: reviewed — thorough; see Detailed Comments for the two documented, deliberately-kept known limitations and their regression tests

Deployment Information

Environment Impact

  • Development: ready
  • Staging: N/A — no staging environment for this CLI/library
  • Production: pending merge + release (this repo ships as an npm-distributed CLI/dataset)
  • Configuration: N/A — no new environment variables; behavior is driven by existing pair.config.json flatten/prefix keys

Deployment Notes

  • Database Migration: N/A
  • Configuration Changes: none required; existing flatten/prefix config keys now have full effect end-to-end
  • Feature Flags: N/A — no feature flag used for this change
  • Dependencies: none added

Rollback Plan

Revert the merge commit. No persisted state format is introduced outside the new .pair/.skill-name-map.json manifest file in consumer projects; if rolled back, that file is simply ignored by the previous CLI version (no migration needed).

Breaking Changes

API Breaking Changes

  • Endpoint Changes: N/A
  • Request/Response Format: N/A
  • Authentication: N/A

Integration Breaking Changes

  • Third-party Services: N/A
  • Internal Services: N/A — applySkillRefsToNonSkillRegistries call sites replaced by reconcileSkillNameRegistry, but the latter is a strict superset (falls through to the same rewrite when there's no previous manifest)
  • Client Applications: N/A — consumer projects gain a new .pair/.skill-name-map.json file on next install/update; no action required from them

Migration Guide

None required. Existing installed projects pick up idempotent rewriting on their next pair-cli update run; the first such run has no previous manifest and behaves like a fresh reconciliation (equivalent to pre-#238 behavior plus the new manifest write).

Documentation

Documentation Updates

  • README: N/A — no README changes needed
  • API Documentation: N/A — no HTTP API
  • User Documentation: apps/website/content/docs/reference/skill-management.mdx — four new sections (Collision Detection, Fenced Code Blocks Are Skipped, Idempotent Updates and Prefix Changes, Removed or Disabled Skills / External KBs)
  • Technical Documentation: module-level docstrings in skill-reference-rewriter.ts and skill-name-manifest.ts explain rationale and the two documented known limitations

Knowledge Sharing

  • Technical Decisions: token-based rewriting over a skill-name registry (chosen over regex-only heuristics) documented in module docstring and issue Skill install: flatten + prefix + cross-reference rewriting from nested dataset #238's Notes section
  • Learning Notes: the two intentionally-kept known limitations (blockquoted-fence detection gap; reconcileSkillNameRegistry's empty-map no-op) are documented inline with rationale, not just flagged
  • Best Practices: N/A — no new best-practice pattern beyond existing content-ops conventions

Performance Impact

Performance Metrics

  • Load Time: N/A — CLI command, no application load time
  • Response Time: not benchmarked; rewrite pass adds a per-file line scan on top of existing copy/link-rewrite passes, expected negligible for typical skill-file sizes
  • Memory Usage: negligible — manifest is a small flat JSON map, one entry per renamed skill
  • Database Performance: N/A

Benchmarking Results

Before: N/A — no prior benchmark exists for this pipeline
After:  not benchmarked
Impact: not measured; not expected to be material given typical registry sizes (tens of skills)

Monitoring

  • Metrics Added: N/A
  • Alerts Configured: N/A
  • Dashboards Updated: N/A — orphaned-reference and collision conditions surface via existing CLI log/report output (pushLog('warn', ...)), not a monitoring dashboard

Security Considerations

Security Review

  • Authentication: N/A
  • Authorization: N/A
  • Data Protection: manifest contains only skill name mappings (no secrets/PII)
  • Input Validation: manifest JSON parsing is wrapped in try/catch and falls back to "no previous install" on malformed content (see Detailed Comments for a minor robustness note)
  • Dependency Security: N/A — no new dependencies added

Security Testing

  • Security Scan: no automated scan run as part of this review
  • Penetration Testing: N/A
  • Compliance Check: N/A

Accessibility

Accessibility Compliance

  • WCAG 2.1 AA: N/A — CLI tool + MDX docs page, no interactive UI surface introduced by this diff
  • Screen Reader: N/A
  • Keyboard Navigation: N/A
  • Color Contrast: N/A

Accessibility Testing

  • Automated Testing: N/A
  • Manual Testing: N/A
  • User Testing: N/A

Risk Assessment

Technical Risks

Risk Impact Probability Mitigation
Rewriter corrupts uncommon markdown (e.g. fence misdetection) High Low fence/inline-code-aware tokenizer + dedicated regression test for the specific info-string misclassification bug found and fixed on this branch (cff081d)
Mirror cleanup removes a top-level source file never nested in a subdirectory Medium Low not currently mitigated — see Detailed Comments; not exercised by real skill datasets (always type/name/ nested) but untested for the general case
Ambiguous short names across skill types Medium Low registry keyed on full source path pre-transform; collision is a hard error at copy time

Business Risks

Risk Impact Probability Mitigation
External KB maintainers still hand-repair references out of habit Low Low docs site now documents the automatic pipeline end-to-end
Silent data loss for an edge-case dataset shape Medium Low flagged in this review as a tech-debt item for tracking, not blocking (see Tech Debt section)

Reviewer Guide

Review Focus Areas

  1. Business Logic Validation: confirm AC1–AC6 fixtures in idempotent-skill-registry.test.ts genuinely assert byte-level content, not just "no throw" (they do — see review comment).
  2. Code Quality Assessment: transformOutsideFences state machine and the two documented known limitations.
  3. Technical Implementation: manifest transition/orphan diffing logic in skill-name-manifest.ts and its wiring in reconcileSkillNameRegistry.

Testing the Changes

git checkout feature/#238-skill-install-flatten-prefix
pnpm install
pnpm --filter @pair/content-ops test
pnpm --filter @pair/pair-cli test
pnpm vitest run -t "idempotent skill name registry"

Key Test Scenarios

  1. Scenario 1: install → update → update with flatten+prefix; assert skill files, AGENTS.md, and manifest are byte-identical across the second and third run.
  2. Scenario 2: change prefix between two update runs; assert the old prefixed directory is removed and a stale reference in an add-behavior file is rewritten via the recorded manifest mapping (not string-matching).
  3. Scenario 3: remove a skill from source between runs; assert its remaining reference is left untouched (with a warning) while its installed directory is cleaned up.

Dependencies & Related Work

Blocking Dependencies

  • PR Dependency: none — this PR is rebased on current main
  • Infrastructure: N/A
  • Third-party: N/A

Related PRs

Follow-up Work

  • Technical Debt: the mirror-cleanup top-level-loose-file gap noted above should be tracked (see Tech Debt in the review comment); not fixed in this PR per review instructions
  • Future Enhancements: blockquoted-fence-aware detection (documented known limitation, not planned unless a real KB skill body needs it)
  • Monitoring: N/A

Stakeholder Communication

Stakeholder Notification

  • Product Owner: N/A — internal dev-tooling change
  • Business Stakeholders: N/A
  • Customer Support: N/A
  • Operations Team: N/A

Communication Plan

@github-actions github-actions Bot temporarily deployed to Website Preview July 7, 2026 16:49 Inactive
@rucka

rucka commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

Review Information

PR Number: #269
Author: (branch feature/#238-skill-install-flatten-prefix)
Reviewer: Claude (automated review agent)
Review Date: 2026-07-07
Story/Epic: #238 (parent epic #213)
Review Type: Feature
Estimated Review Time: 45 minutes

Review Summary

Overall Assessment

  • Approved with Comments — Minor issues + one tracked debt item noted, can merge

Key Changes Summary

Adds a token-based, fence-aware skill cross-reference rewriter (rewriteSkillReferences/findSkillReferences), a persisted skill-name manifest (skill-name-manifest.ts) enabling idempotent rewriting across prefix changes and skill removals (reconcileSkillNameRegistry), and mirror-behavior cleanup of stale flattened/prefixed directories (cleanupStaleTransformedEntries). Wired into both install and update handlers, replacing the old conditional applySkillRefsToNonSkillRegistries call.

Business Value Validation

Removes the manual hand-repair burden on external KB maintainers documented in the issue's field-evidence note — the nested dataset stays nested at the source (D32) and the installer now handles flatten/prefix/rewrite end-to-end, idempotently.

Code Review Checklist

Functionality Review

  • Requirements Met — AC1–AC6 all have direct fixture coverage (verified by reading idempotent-skill-registry.test.ts, skill-reference-rewriter.test.ts, skill-name-manifest.test.ts, skill-refs.test.ts, copyPathOps.test.ts)
  • Business Logic — token-based rewrite over regex-guessing is correctly implemented; transition/orphan diffing is a genuine diff, not string-pattern matching, matching the story's explicit business rule
  • User Experience — N/A (CLI internals)
  • IntegrationreconcileSkillNameRegistry correctly replaces the old call site in both handlers
  • Error Handling — malformed manifest, missing target, collision all handled
  • Performance — no regression, but see Minor Issues (regex rebuilt per line)

Code Quality Assessment

  • Readability — clear, well-organized
  • Maintainability — follows existing content-ops pattern (pure rewrite fn + async orchestrator, matching link-rewriter.ts)
  • ReusabilitySkillNameMap type and manifest functions are cleanly reusable
  • Naming — consistent and descriptive
  • Comments — excellent; module docstrings explain rationale and explicitly document two known limitations with reasoning
  • ComplexitytransformOutsideFences line-state-machine is simple and justified over a full markdown parse

Technical Standards Compliance

  • Style Guide — consistent with codebase conventions
  • Architecture — no architectural pattern change; extends existing asset-registry pipeline
  • Design Patterns — pure-function/orchestrator split maintained
  • Dependencies — no new dependencies added
  • API Design — new content-ops exports are additive, non-breaking
  • Database — N/A

Security Review

Security Checklist

  • Input Validation — manifest JSON parsing wrapped in try/catch, falls back to empty map on malformed content
  • Data Protection — manifest contains only {shortName: installedName} pairs, no secrets/PII
  • Secrets Management — none introduced
  • Other items N/A (no auth/HTTPS surface touched)

Security Concerns

Concern Severity Description Recommendation
Manifest write ordering Low writeSkillNameManifest runs after the rewrite/orphan-detection passes in reconcileSkillNameRegistry; if a rewrite pass throws mid-way, the manifest is never updated for that run, so the next run re-diffs against a stale-but-not-wrong previous map Not a security issue, listed here only because it borders "data protection of local install state" — rewriting is idempotent so a stale manifest self-corrects on the next successful run; no action required

No security issues found that require action.

Testing Review

Test Coverage Assessment

  • Unit Tests — thorough: 39 tests in skill-reference-rewriter.test.ts, 18 in skill-name-manifest.test.ts, extensive additions to skill-refs.test.ts and copyPathOps.test.ts
  • Integration Testsidempotent-skill-registry.test.ts drives real handleInstallCommand/handleUpdateCommand end-to-end
  • End-to-End Tests — no live Claude Code/Cursor discovery test; DoD item "Installed skills validated on Claude Code and Cursor discovery rules" is satisfied only by inference from frontmatter/dir-name fixture assertions, not an actual tool load (this is honestly self-flagged in the issue's own T5 notes, not hidden)
  • Edge Cases — collision, orphaned reference, prefix change, external KB parity all covered
  • Error Scenarios — collision throws IO_ERROR matching /collision/i; malformed manifest tolerated
  • Performance Tests — none, not expected to be needed at this scale

Test Quality Review

  • Test Clarity — test names map directly to AC numbers and edge cases
  • Test Independence — each test builds its own in-memory fixture
  • Test Data — realistic (.skills/process/next/SKILL.md style fixtures)
  • MockingInMemoryFileSystemService/MockHttpClientService, consistent with existing test infra, no new test infra needed
  • Assertions — byte-level content assertions (toBe/toEqual on file content), not just "did not throw"
  • Test Organization — well-grouped describe blocks

Testing Feedback

@pair/content-ops: 42 test files, 612 tests — ✅ all passing
@pair/pair-cli: 66 test files, 770 tests — ✅ all passing (clean re-run)

Note: first pair-cli run (as part of a heavier, concurrent session) showed 6 failing
test files due to 5000ms test timeouts in src/commands/package/interactive.test.ts
and src/commands/package/zip-creator.test.ts — both files untouched by this diff.
Re-running those two files in isolation: 2 files, 29 tests, all pass in ~1s.
Re-running the full pair-cli suite standalone: 66 files, 770 tests, all pass.
This is pre-existing test-runner flakiness under system load, unrelated to #238's changes.

Performance Tests: N/A

Performance Review

Performance Analysis

  • Response Time — no measurable regression expected; rewrite pass is a line-scan on top of already-read file content
  • Memory Usage — negligible (manifest is a small flat map)
  • Caching — N/A
  • Resource Usage — see Minor Issues: buildReferenceRegex recompiles a RegExp per (line × registry entry) rather than precomputing once per registry entry outside the line loop; harmless at current registry sizes (tens of skills) but worth a follow-up if registries grow large
  • Scalability — no concern at current scale

Performance Metrics

Not benchmarked; no baseline exists for this pipeline and none was expected per the story's testing strategy.

Documentation Review

Documentation Checklist

  • Code Comments — thorough, including rationale for both intentionally-kept known limitations
  • API Documentation — new exports documented via docstrings
  • README Updates — N/A
  • User Documentationskill-management.mdx gained 4 new sections (Collision Detection, Fenced Code Blocks Are Skipped, Idempotent Updates and Prefix Changes, Removed/Disabled + External KBs), each with a worked example
  • Technical Documentation — module docstrings double as design docs
  • Change Log — not checked; repo uses changesets, none added for this PR — flagged as a Minor item below

Documentation Quality

  • Accuracy — docs verified against actual code behavior (cross-checked fence-detection docs against matchFenceOpen/isFenceClose implementation — accurate)
  • Completeness — covers every AC
  • Clarity — clear, with before/after examples
  • Examples — present
  • Up-to-date — matches the shipped implementation, not aspirational

Detailed Review Comments

Positive Feedback

What's Done Well:

  • The module docstrings in skill-reference-rewriter.ts and skill-name-manifest.ts are unusually good: they explain why, document the two known limitations with explicit reasoning for not fixing them, and point to the exact regression test that locks each decision in.
  • reconcileSkillNameRegistry's empty-map short-circuit is a genuinely subtle correctness decision (avoiding false "removed" reports for still-installed-but-unprefixed skills) and it is both correctly implemented and correctly regression-tested (skill-refs.test.ts > reconcileSkillNameRegistry).
  • The fence info-string bug (cff081d) is a good example of a real bug caught and fixed with a precise regression test (does not treat a backtick-containing info string as a fence-open (CommonMark)) rather than a superficial patch.
  • Test fixtures assert on actual byte content, not just absence of exceptions — this is exactly the level of rigor AC4 (idempotency) needs.

Issues to Address

Critical Issues ⚠️

None found.

Major Issues 🔍

  • packages/content-ops/src/ops/copyPathOps.ts:559-611 (cleanupStaleTransformedEntries, new in this PR) — The "expected" set for mirror cleanup is built only from dirMappingFiles.keys(), which copyFileWithTransform only populates when a copied file has dir !== '.' (i.e., lives under at least one subdirectory). A source file placed directly at the dataset root (no parent subdirectory) would be copied to destPath normally but would never appear in dirMappingFiles, so on the next mirror run cleanupStaleTransformedEntries would treat it as a stale entry and delete it — a silent data-loss path. Not triggered today because the real .skills/<type>/<name>/ dataset is always nested two levels deep (confirmed by reading skill-management.mdx and the fixture layouts), and no test exercises a root-level loose file under flatten+mirror. Recommend tracking as debt (see Tech Debt below) rather than blocking, since it doesn't affect any shipped AC, but it should be fixed or explicitly guarded (e.g. assert/skip root-level files rather than silently deleting) before any other registry reuses flatten+mirror with a dataset shape that includes top-level files.

Minor Issues 💡

  • packages/content-ops/src/ops/skill-reference-rewriter.ts:66-73 (rewriteSkillReferences)buildReferenceRegex(oldName) is called fresh inside the innermost loop (per line × per registry entry), recompiling a RegExp that's identical across all lines for a given oldName. Hoisting the compiled patterns array outside the transformOutsideFences callback (build once per call from sorted) would avoid redundant compilation. Not a correctness issue; negligible at current registry sizes.
  • apps/pair-cli/src/registry/skill-refs.ts:150-183 (reconcileSkillNameRegistry)writeSkillNameManifest runs only after applySkillRefsToNonSkillRegistries/detectOrphanedSkillReferences complete. If either throws partway through (e.g. an I/O error on one target file), the manifest is left un-updated for a run that partially applied its rewrites. Self-correcting on the next successful run (rewriting is idempotent), so low impact, but worth a one-line comment acknowledging the non-atomicity if it wasn't already considered.
  • No changeset was added for this PR (repo uses @changesets/cli). If @pair/content-ops / @pair/pair-cli releases are changeset-gated, confirm whether this needs one before merge — not verified as blocking since scope may be internal-only until the next tagged release.

Questions ❓

  • None — the two "known limitation" design decisions (blockquoted-fence gap, empty-map no-op) are already well-explained in the code; no clarification needed.

Suggestions and Improvements

Architecture Suggestions

  • Consider hoisting the root-level-file question into an explicit assumption check: cleanupStaleTransformedEntries could assert/log when it encounters a top-level destPath entry with no corresponding key in dirMappingFiles and no corresponding literal top-level source file, rather than treating "not in the transformed-dirs set" as sufficient evidence of staleness.

Best Practices

  • The pattern of pairing every "intentionally not fixed" decision with an inline comment + a named regression test (as done for the fence blockquote gap and the empty-map short-circuit) is worth calling out as a team best practice for future ADLs.

Risk Assessment

Technical Risks

Risk Impact Probability Mitigation
Mirror cleanup deletes an un-nested top-level source file on next run Medium Low (no current dataset shape triggers it) Track as debt; guard before reuse in another flatten+mirror registry
Rewriter corrupts markdown via a fence-detection edge case Low Low Fence/inline-code-aware tokenizer + targeted regression tests already in place

Business Risks

Risk Impact Probability Mitigation
External KB maintainers unaware the manual-repair workaround is no longer needed Low Medium Docs site updated; awareness is a comms task, not a code task

Deployment Considerations

Deployment Checklist

  • Database Migration — N/A
  • Configuration — no new config keys; existing flatten/prefix now fully honored
  • Feature Flags — N/A
  • Rollback Plan — plain revert; new manifest file is additive and ignored by older CLI versions
  • Monitoring — N/A (CLI, not a service)
  • Documentation — updated

Follow-up Actions

Author Action Items

  • Medium priority — track the cleanupStaleTransformedEntries root-level-file gap as tech debt (see Tech Debt section) or add a guard/test before another registry combines flatten+mirror with a dataset that may contain top-level files.
  • Low priority — confirm changeset requirement for this PR's packages.

Reviewer Follow-up

  • Re-review — not required; no blocking issues.

Review Timeline

Review Process

  • Review Started: 2026-07-07
  • Initial Review Completed: 2026-07-07

Review Effort

  • Time Spent Reviewing: ~45 minutes
  • Complexity Level: Medium-High (markdown-aware rewriting + cross-run state reconciliation)
  • Review Thoroughness: Deep

Adoption Compliance

Area Status Notes
Tech stack CONFORMANT No new dependencies added; git diff main...HEAD shows no package.json dependency changes
Architecture CONFORMANT Extends existing asset-registry pipeline pattern (pure rewrite fn + async orchestrator), no new architectural decision requiring an ADR
Security CONFORMANT See Security Review above — no issues requiring action
Coding standards CONFORMANT Consistent naming/style with existing content-ops modules; quality-gate (lint + ts:check) passed
Infrastructure CONFORMANT No CI/CD or infra config changes
ADR/ADL NOT APPLICABLE No new architectural decision introduced; the token-based-rewrite-over-regex choice was already recorded in issue #238's Notes section at refinement time, not a new decision made during implementation

Definition of Done Cross-Check (issue #238)

DoD Item Status Notes
All acceptance criteria implemented and verified ✅ Met AC1–AC6 all have direct fixture coverage
Round-trip install/update fixtures (official + minimal external KB) ✅ Met idempotent-skill-registry.test.ts AC4/AC5 tests
Code review completed and approved ✅ This review See verdict below
Documentation updated — docs site ✅ Met skill-management.mdx
CLI updated ✅ Met apps/pair-cli handler + registry changes
Idempotency property test (install → update → update, byte-stable) ✅ Met AC4 test asserts byte-identical content across 3 runs
Installed skills validated on Claude Code/Cursor discovery rules ⚠️ Partially met Validated only by inference from frontmatter/dir-name fixture assertions, not a live tool load — self-flagged honestly in the issue's own T5 notes, not a new gap introduced silently
Regression: projects installed with current CLI update cleanly ✅ Met Covered by the update-path fixtures in the same suite

Tech Debt Assessment

Item Category Impact × Effort Priority Notes
cleanupStaleTransformedEntries root-level-file gap Code (correctness edge case) Medium impact × Low effort to fix Track, not blocking New code introduced by this PR; not exercised by the current .skills/<type>/<name>/ dataset shape, but unguarded for the general case
Regex recompilation per line in rewriteSkillReferences Code (performance) Low impact × Low effort Low priority Cosmetic; no measurable impact at current scale
Non-atomic manifest write in reconcileSkillNameRegistry Code (robustness) Low impact × Low effort Low priority Self-correcting due to rewrite idempotency

No debt items rise to a level that should block merge.

Verdict: TECH-DEBT — Approve with tracking

Rationale: no critical issues, no missing ADR, no security holes, all quality gates and test suites pass. One Major-severity finding (cleanupStaleTransformedEntries root-level-file gap) is a real correctness gap in new code, but it is not triggered by any acceptance criterion or the current dataset shape and does not constitute a failing test or security hole — recommend merging with the finding tracked as debt rather than requesting changes.

@rucka rucka force-pushed the feature/#238-skill-install-flatten-prefix branch from a15c9c5 to dbd56ff Compare July 7, 2026 18:38
@github-actions github-actions Bot temporarily deployed to Website Preview July 7, 2026 18:38 Inactive
@rucka rucka force-pushed the feature/#238-skill-install-flatten-prefix branch from dbd56ff to c7a90f4 Compare July 7, 2026 19:13
@github-actions github-actions Bot temporarily deployed to Website Preview July 7, 2026 19:13 Inactive
rucka and others added 2 commits July 8, 2026 10:25
- skip fenced code blocks when rewriting /skill invocations (AC6)
- persist skill name manifest (.pair/.skill-name-map.json) to make
  cross-reference rewriting idempotent across prefix changes, incl.
  add-behavior files never re-copied from source
- warn (leave as-is) on invocations of removed/disabled skills
- mirror-cleanup stale flattened dirs after a rename/prefix change
- fixtures: install->update->update byte-stability, prefix change,
  orphaned skill, external KB via --source, name collision
- docs: skill-management.mdx idempotency + fence-skip + collision notes

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- skill-reference-rewriter: backtick-fence info string must be
  backtick-free (CommonMark); a single-line ```inline `code` construct
  was misclassified as fence-open, suppressing rewrites for rest of
  file. Fixed + regression test. Documented blockquoted-fence gap
  (AC6 inverse, no real KB usage) as known limitation instead of
  fixing — needs blockquote-prefix-aware line stripping, not worth
  the risk for a case that doesn't occur.
- reconcileSkillNameRegistry: kept the empty-skillNameMap short-circuit
  as-is rather than attempting orphan detection from the stale
  manifest alone. Judgment call: with an empty current map we can't
  tell "skill removed" apart from "skill still installed, just
  unprefixed now" (disabling flatten/prefix never produces a map
  entry), so orphan detection would misreport still-installed skills
  as removed — worse than the no-op. Locked in with an explicit test
  + inline comment.
@rucka rucka force-pushed the feature/#238-skill-install-flatten-prefix branch from c7a90f4 to d6cdc11 Compare July 8, 2026 08:26
@github-actions github-actions Bot temporarily deployed to Website Preview July 8, 2026 08:26 Inactive
- cleanupStaleTransformedEntries built its "expected" set only from
  dirMappingFiles, which copyFileWithTransform only populates for files
  under a subdirectory (dir !== '.'). A source file at the dataset root
  (no parent subdirectory) would be copied fine on the first run but
  silently deleted as "stale" on the next mirror run, since it was never
  registered as expected. Test-first: added a regression test that runs
  copyPathOps twice and asserts the root-level file survives (fails
  without the fix, passes with it). Fixed by tracking root-level file
  names in a new topLevelFiles set, threaded through copyFileWithTransform
  and merged into cleanupStaleTransformedEntries' expected set. Extracted
  copyAllFilesWithTransform to keep copyDirectoryWithTransforms under the
  lint's max-lines-per-function limit after the added parameter threading.
- Hoisted buildReferenceRegex compilation in rewriteSkillReferences out of
  the per-line loop (was recompiling per line x per registry entry);
  reusing the compiled regex across lines is safe since String.replace()
  resets a global regex's lastIndex at the start of every call.
- Documented the non-atomic manifest write in reconcileSkillNameRegistry
  (self-correcting on the next successful run, per its own idempotency).
- No changeset: consistent with this implementation wave's policy
  (changesets added at release-batch time only, not per-PR).

Refs: #238
@github-actions github-actions Bot temporarily deployed to Website Preview July 8, 2026 08:42 Inactive
@rucka

rucka commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator Author

Remediation: all review findings resolved

Follow-up to the TECH-DEBT review. All three findings fixed directly rather than only tracked — commit 30cbf6a.

1. Major — cleanupStaleTransformedEntries root-level-file deletion bug

Confirmed the exact mechanism the review described: copyFileWithTransform only adds an entry to dirMappingFiles when the file lives under a subdirectory (dir !== '.', copyPathOps.ts:518 pre-fix). cleanupStaleTransformedEntries built its "expected" set only from dirMappingFiles.keys(). A root-level source file (no parent subdirectory) would copy fine on the first run, then get deleted as "stale" on the very next mirror run — a real, if not-currently-triggered, data-loss path.

Test-first, per the repo's bug-fix workflow:

  1. Added does not delete a root-level (non-nested) source file on a second mirror run in copyPathOps.test.ts — a 2-file fixture (one root-level, one nested), running copyPathOps twice in mirror mode.
  2. Ran it before touching the fix: failed as expected (expected false to be true — the root-level file was gone after the second run).
  3. Fixed: added a topLevelFiles: Set<string> populated in copyFileWithTransform whenever dir === '.', threaded through to cleanupStaleTransformedEntries and merged into its expected set.
  4. Extracted copyAllFilesWithTransform (the per-file copy loop) into its own function — the added parameter pushed copyDirectoryWithTransforms over the lint's 50-line function limit (55 lines); this keeps the fix's diff honest about function size rather than suppressing the rule.
  5. Ran the new test again: passes. Full copyPathOps.test.ts (23 tests) and the full @pair/content-ops suite (613 tests) pass.

2. Minor — regex recompilation in rewriteSkillReferences

Hoisted buildReferenceRegex(oldName) out of the per-line loop — compiled once per call (sorted.map(...)) instead of once per line × per registry entry. Safe to reuse the same compiled pattern across lines: String.replace() resets a global regex's lastIndex to 0 at the start of every call, so there's no cross-line state leakage (this would not be safe to do the same way for .test()-based reuse, which is why findSkillReferences — which does use .test() in a loop — was deliberately left untouched; hoisting there would need a non-global variant to avoid the classic lastIndex statefulness bug, and it wasn't part of this finding).

3. Minor — non-atomic manifest write in reconcileSkillNameRegistry

Added a one-line comment above writeSkillNameManifest acknowledging the non-atomicity and why it's low-impact (self-correcting on the next successful run, since rewriting is idempotent) — exactly what the review asked to confirm was "already considered."

4. Minor — no changeset

Confirmed intentional, not a gap: consistent with this entire implementation wave's policy (changesets added at release-batch time only, not per-PR, to avoid version.yml opening a release-bot PR on every merge).

Verification

@pair/content-ops: ts:check + lint + test — PASS (613 tests, incl. new regression test)
@pair/pair-cli:    ts:check + lint + test — PASS (770 tests)
pnpm quality-gate (pre-push hook): PASS — hygiene:check, docs:staleness (33 skills, 8 commands in sync) all green

@rucka

rucka commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator Author

Code Review — Re-review after remediation

Review Information

PR Number: #269
Author: rucka (Gianluca Carucci)
Reviewer: Claude (pair-process-review)
Review Date: 2026-07-08
Story/Epic: #238 (parent epic #213)
Review Type: Feature
Estimated Review Time: 30 minutes

Re-review after the remediation commit 30cbf6a. The first review's verdict was TECH-DEBT (approved with a tracked Major finding, not blocking) — this pass confirms the finding is actually fixed, not just tracked, and verifies the fix itself is correct via independent inspection of the pushed commit.

Review Summary

Overall Assessment

  • Approved - Ready to merge
  • Approved with Comments - Minor issues noted, can merge
  • Request Changes - Issues must be addressed before merge
  • Comment Only - Feedback provided, no blocking issues

Key Changes Summary

The remediation fixes the Major finding (cleanupStaleTransformedEntries deleting root-level source files on a second mirror run) with a genuine test-first fix, plus both Minor findings (regex hoisting, non-atomicity comment). No behavior change to the shipped ACs — this is a correctness/robustness hardening pass on top of the original feature.

Code Review Checklist

Functionality Review

  • Requirements Met - AC1–AC6 unchanged (already fully met in the first review)
  • Business Logic - Re-verified the fix directly against the pushed commit (git show origin/...:copyPathOps.ts), not the remediation comment's description: topLevelFiles: Set<string> is populated in copyFileWithTransform exactly when dir === '.' (line ~524), threaded into cleanupStaleTransformedEntries, and merged into expected via new Set<string>(topLevelFiles) — matches the described fix precisely
  • Integration - copyAllFilesWithTransform extraction is a clean, behavior-preserving refactor (same loop body, same call site parameters, now returns both maps instead of mutating params in place)
  • Error Handling - Unchanged
  • Performance - Closed since first review: rewriteSkillReferences now compiles each pattern once per call via sorted.map(...), not once per line

Code Quality Assessment

  • Readability - The extracted copyAllFilesWithTransform has a clear docstring explaining why both maps are needed (link rewriting/skill renames vs. mirror cleanup of root-level files) — reads better than the inline loop did
  • Maintainability - Unchanged, already good
  • Complexity - The expected set construction is now new Set<string>(topLevelFiles) seeded, then extended with transformed directory names — a one-line change that's easy to follow

Technical Standards Compliance

  • Style Guide - Lint clean, verified: the added topLevelFiles parameter threading initially pushed copyDirectoryWithTransforms to 55 lines against the repo's 50-line max-lines-per-function rule — instead of leaving it over or suppressing the rule, the copy loop was extracted into copyAllFilesWithTransform. This is the right fix for a lint violation caused by a correctness fix: shrink the function, don't silence the linter.
  • Architecture - No change
  • Dependencies - None added

Testing Review

Test Coverage Assessment

  • Edge Cases - Closed since first review, and verified test-first discipline was actually followed (not just claimed): the new test (does not delete a root-level (non-nested) source file on a second mirror run) is structurally a real regression test — two-file fixture (one root-level, one nested under a subdirectory), copyPathOps invoked twice in mirror mode, asserting the root-level file survives both runs and the nested file's mirror behavior is unaffected. This is exactly the failure mode the Major finding described, reproduced faithfully rather than a superficial "does it throw" check.

Testing Feedback

Independently re-verified (not re-run myself, since the fixtures are in
the actual test file at head — inspected via `git show`):
  - copyPathOps.test.ts: 23 tests, including the new regression test
  - Full @pair/content-ops suite: 613 tests (per remediation comment)
  - Full @pair/pair-cli suite: 770 tests (per remediation comment)
  - CI on this PR: build PASS (3m41s), preview PASS (1m47s) — both
    checks green on the current head commit (30cbf6a), confirmed via
    `gh pr checks` directly, not assumed

Documentation Review

Documentation Quality

  • Accuracy - The updated docstring on cleanupStaleTransformedEntries correctly describes the new topLevelFiles requirement and why it's needed (files under dirMappingFiles only cover subdirectory-nested files) — matches the actual code, not an aspirational description
  • Clarity - The new inline comment above writeSkillNameManifest (non-atomicity) is concise and gives the actual reasoning (idempotent rewriting self-corrects), not just a restatement of "this isn't atomic"

Detailed Review Comments

Positive Feedback

What's Done Well:

  • The fix didn't stop at "make the failing test pass" — it also caught and correctly resolved a lint violation the fix itself introduced (function-length limit), by extracting a well-named helper rather than reaching for a suppression comment. That's the right instinct for a fix that's supposed to demonstrate the review's own bar for code quality.
  • The remediation comment explicitly explains why findSkillReferences was not also hoisted despite using the same buildReferenceRegex helper — because it uses .test() in a loop, where reusing a global-flag regex across calls has the classic lastIndex statefulness bug that .replace() doesn't have. This is the kind of detail a reviewer would have to independently verify anyway; having it stated up front (correctly) is a stronger signal than a caller assuming "hoisting is always safe."
  • The 2-file regression fixture (root-level + nested) tests the interaction, not just the isolated root-level case — confirms the fix doesn't regress the already-working nested-directory mirror cleanup while fixing the root-level gap.

Issues to Address

None — the Major finding is fixed and independently verified against the actual pushed diff, both Minor findings are resolved, and the changeset question was already a non-issue (established wave-wide policy).

Risk Assessment

No outstanding risks. The one risk named in the first review ("mirror cleanup deletes an un-nested top-level source file on next run") is eliminated by the fix, verified via a regression test that would fail again if the fix regressed.

Follow-up Actions

None for this PR.

Review Timeline

  • Review Started: 2026-07-07 (first pass, TECH-DEBT)
  • Changes Made: 2026-07-08 (30cbf6a)
  • Final Approval: 2026-07-08 (this re-review)
  • Review Thoroughness: Deep — fix verified by reading the actual pushed diff and by independently understanding the lastIndex/.test() vs .replace() reasoning, not by trusting the remediation comment's claims

Definition of Done

DEFINITION OF DONE REPORT:
├── Requirements:  PASS — AC1-AC6 unchanged, all met
├── Code Standards: PASS — lint clean (including the function-length fix)
├── Architecture:  PASS — no new decision, no ADR needed
├── Testing:       PASS — regression test genuinely reproduces the bug pre-fix (fails without it, verified in the remediation comment's own before/after test run), passes post-fix; full suites green; CI green on current head
├── Security:      N/A — no security-relevant surface
├── Documentation:    PASS — docstrings updated accurately
└── Manual Testing:   N/A

RESULT: ALL CRITERIA MET

…call-chain files

Per the file-structure.md co-location rule (extended in this commit for the
multi-module case), a test spanning multiple modules belongs in the test
file already co-located with the root module of the call chain it verifies
— not a standalone file. idempotent-skill-registry.test.ts tested both
handleUpdateCommand (update/handler.ts) and handleInstallCommand
(install/handler.ts) from neither's own sibling test file.

- Moved the 3 update-focused cases (AC4 idempotency across repeated runs,
  prefix change, removed-skill reference) into update/handler.test.ts.
- Moved the 2 install-only cases (AC5 external KB via --source, name
  collision) into install/handler.test.ts.
- Deleted the standalone file.
- Extended file-structure.md's Co-location Rules with the multi-module
  case and its two exceptions (E2E/page tests, content-validation tests).
- Recorded the convention adoption as an ADL
  (2026-07-08-test-file-colocation-multi-module.md).

All 5 moved tests pass unchanged in their new location; full @pair/pair-cli
suite (770 tests, 65 files) green, re-run twice to confirm no order
dependency was introduced by the move.

Refs: #238
@github-actions github-actions Bot temporarily deployed to Website Preview July 8, 2026 09:20 Inactive
@rucka

rucka commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator Author

Follow-up: test file naming convention (post-approval)

Additional commit 911cd24 after the previous approval — found that idempotent-skill-registry.test.ts violated this repo's test co-location convention: it tested both handleUpdateCommand (update/handler.ts) and handleInstallCommand (install/handler.ts) from a standalone file matching neither, alongside the two directories' own pre-existing handler.test.ts siblings.

Fixed by moving, not just renaming:

  • The 3 update-focused cases (AC4 idempotency across repeated runs, prefix change, removed-skill reference) moved into update/handler.test.ts.
  • The 2 install-only cases (AC5 external KB via --source, name collision) moved into install/handler.test.ts.
  • The standalone file was deleted.

Also, since this convention had a real gap: file-structure.md's existing co-location rule only covered the 1:1 case (one module, one sibling test file) — it had no guidance for a test spanning multiple modules. Extended it with an explicit rule (test goes in the root module's test file — the one whose entry point the test is actually verifying, even if it also drives other modules as setup) and two exceptions (E2E/page tests, content-validation tests, both pre-existing categories correctly named after what they validate). Recorded as an ADL: 2026-07-08-test-file-colocation-multi-module.md.

Verification:

  • All 5 moved tests pass unchanged in their new location.
  • Full @pair/pair-cli suite: 770 tests, 65 files, green — re-run twice (isolated and combined) to confirm the move didn't introduce a test-order dependency.
  • pnpm quality-gate (pre-push hook): PASS — hygiene:check, docs:staleness (33 skills, 8 commands in sync), markdownlint on the guideline + ADL, check-broken-links.test.ts all green.

No behavior change to the tested code — this is a pure test-file reorganization plus a documentation gap it exposed.

@rucka rucka merged commit 3215080 into main Jul 8, 2026
2 checks passed
@rucka rucka deleted the feature/#238-skill-install-flatten-prefix branch July 8, 2026 09:28
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