Skip to content

Add untracked local file detection#52

Open
eljulians wants to merge 5 commits into
masterfrom
untracked-local-detectino
Open

Add untracked local file detection#52
eljulians wants to merge 5 commits into
masterfrom
untracked-local-detectino

Conversation

@eljulians
Copy link
Copy Markdown
Owner

SUMMARY
Detect files in install target directories not covered by any Skillfile entry. Surfaces untracked files as warnings in validate, as errors with validate --strict, and as an opt-in section in status --untracked. Includes type-safe UntrackedKind enum, false-positive fixes for flat-deployed and local directory entries, and post-review cleanup.

RATIONALE
Users who create skills by hand (e.g. .claude/skills/docker/) but forget to declare them in the Skillfile leave teammates without access after cloning. Ghost files also linger after skillfile remove. This feature closes the "source of truth" gap: the Skillfile now reports what it does and does not manage. The --strict flag enables CI enforcement for teams that require all skills to be declared.

CHANGES

  • crates/deploy/src/paths.rs: add UntrackedFile struct with typed UntrackedKind enum (File/Directory) and suffix() method, covered_paths() (private), find_untracked(), DirScanner, ScanSpec, for_each_local_adapter() helper, is_local_dir helper that checks the filesystem mirroring the deploy code's req.source.is_dir() pattern. Scans all local install targets (not just the first). Lenient mode: extra files inside managed nested dirs are not flagged. entry_covered_paths checks is_dir_entry(entry) || is_local_dir(entry, repo_root) to detect directory entries regardless of source type. For flat-mode dir entries, calls adapter.installed_dir_files() to cover the individual deployed file paths instead of the parent directory. DirScanner::into_sorted uses sort_unstable_by to match codebase convention. 17 new unit tests plus 2 false-positive regression tests (flat_agent_dir_entry_no_false_positives, local_dir_skill_not_false_positive).
  • crates/cli/src/commands/validate.rs: add print_untracked_warnings presentation function (replaces removed check_untracked middle-man that mixed validation with formatting). cmd_validate calls find_untracked directly. Wire --strict flag. Warnings print to stderr, --strict promotes to errors and exits non-zero. Summary line appends (N untracked). 3 new unit tests.
  • crates/cli/src/commands/status.rs: add --untracked flag and print_untracked(). Appends an "Untracked:" section when flag is set. Uses f.kind.suffix() instead of inline bool check.
  • crates/cli/src/main.rs: add --strict to Validate, --untracked to Status clap definitions.
  • tests/cli.rs: 4 new functional tests covering validate warnings, validate --strict failure, status --untracked output, and the negative case (no flag hides section).
  • roadmap.md: replace old untracked detection entry with full adversarial-reviewed spec. Remove completed "dirty change tracking" and "strip doc comments" entries from the idea pool.

SUMMARY
Detect files in install target directories not covered by any Skillfile
entry. Surfaces untracked files as warnings in `validate`, as errors
with `validate --strict`, and as an opt-in section in `status --untracked`.

RATIONALE
Users who create skills by hand (e.g. `.claude/skills/docker/`) but
forget to declare them in the Skillfile leave teammates without access
after cloning. Ghost files also linger after `skillfile remove`. This
feature closes the "source of truth" gap: the Skillfile now reports
what it does and does not manage. The `--strict` flag enables CI
enforcement for teams that require all skills to be declared.

CHANGES
- `crates/deploy/src/paths.rs`: add `UntrackedFile` struct,
  `covered_paths()`, `find_untracked()`, `DirScanner`, `ScanSpec`,
  `for_each_local_adapter()` helper. Scans all local install targets
  (not just the first). Lenient mode: extra files inside managed nested
  dirs are not flagged. 17 new unit tests.
- `crates/cli/src/commands/validate.rs`: add `check_untracked()`, wire
  `--strict` flag. Warnings print to stderr, `--strict` promotes to
  errors and exits non-zero. Summary line appends `(N untracked)`.
  3 new unit tests.
- `crates/cli/src/commands/status.rs`: add `--untracked` flag and
  `print_untracked()`. Appends an "Untracked:" section when flag is set.
- `crates/cli/src/main.rs`: add `--strict` to `Validate`, `--untracked`
  to `Status` clap definitions.
- `tests/cli.rs`: 4 new functional tests covering `validate` warnings,
  `validate --strict` failure, `status --untracked` output, and the
  negative case (no flag hides section).
- `roadmap.md`: replace old untracked detection entry with full
  adversarial-reviewed spec. Remove completed "dirty change tracking"
  and "strip doc comments" entries from the idea pool.
**SUMMARY**
Post-review cleanup from adversarial review of the untracked local
detection feature. Two minor fixes in `crates/deploy/src/paths.rs`.

**RATIONALE**
`covered_paths` has no external consumer outside the file where it is
defined, so `pub` visibility was unnecessarily broad. `sort_by` was
replaced with `sort_unstable_by` to match the codebase convention
since path uniqueness makes sort stability irrelevant.

**CHANGES**
- `covered_paths` visibility reduced from `pub` to private
- `DirScanner::into_sorted` switched from `sort_by` to
  `sort_unstable_by`
**SUMMARY**
Replaced the bare `is_dir: bool` on `UntrackedFile` with a typed
`UntrackedKind` enum and eliminated the `check_untracked` middle-man
function in `validate.rs`.

**RATIONALE**
Adversarial architectural review flagged two issues: (1) `is_dir: bool`
carried adapter-specific semantics (Nested vs Flat) erased into a bare
bool, with the `"/"` suffix logic duplicated across two CLI consumers.
(2) `check_untracked` mixed validation concerns with presentation
formatting by collecting strings into a `Vec<String>` that was then
iterated again for display.

`UntrackedKind::suffix()` centralizes the display logic, and
`cmd_validate` now calls `find_untracked` directly, passing the data
to a `print_untracked_warnings` presentation function.

**CHANGES**
- `UntrackedKind` enum (`File`/`Directory`) with `suffix()` method
  added to `crates/deploy/src/paths.rs`
- `UntrackedFile.is_dir` replaced with `UntrackedFile.kind`
- `check_untracked` removed from `validate.rs`, replaced with
  `print_untracked_warnings` that takes `&[UntrackedFile]` directly
- `cmd_validate` calls `find_untracked` directly instead of through
  a middle-man function
- `scan_adapter` iterator chain simplified: `map`+`filter`+`collect`
  replaced with `filter_map`+`then_some` (no intermediate `Vec`)
- Both CLI consumers (`validate.rs`, `status.rs`) now use
  `f.kind.suffix()` instead of inline `if f.is_dir { "/" } else { "" }`
**SUMMARY**
Fixed two bugs where `entry_covered_paths` computed wrong covered
paths for flat-deployed directory entries and local directory entries,
causing false positive untracked file reports.

**RATIONALE**
Adversarial QA review found that `entry_covered_paths` registered
`target_dir/name` for flat dir entries, but flat deploy places
individual files directly in `target_dir/`. Files like `a.md`, `b.md`
were missing from the covered set. Separately, `is_dir_entry()` only
inspects Github `path_in_repo` and returns false for Local entries, so
local directory skills got `target_dir/name.md` instead of the actual
`target_dir/name/` directory path. The deploy code already handled
this at `adapter.rs:150` with `is_dir_entry(entry) || source.is_dir()`
but `covered_paths` did not follow the same pattern.

**CHANGES**
- `entry_covered_paths` now checks `is_dir_entry(entry) ||
  is_local_dir(entry, repo_root)` to detect directory entries
  regardless of source type
- For flat-mode dir entries, calls `adapter.installed_dir_files()`
  to cover the individual deployed file paths instead of the
  parent directory
- Added `is_local_dir` helper that checks the filesystem, mirroring
  the deploy code's `req.source.is_dir()` pattern
- Added `github_agent_dir` test helper for agent directory entries
- Two regression tests: `flat_agent_dir_entry_no_false_positives`
  and `local_dir_skill_not_false_positive`
@eljulians eljulians force-pushed the untracked-local-detectino branch from 0ef8c9a to b0937b2 Compare March 31, 2026 16:11
**SUMMARY**
Six fixes from adversarial code review of the untracked local detection
feature. Fixes a false-positive bug for nested single-file skills,
improves public API ergonomics, help text discoverability, and code
clarity.

**RATIONALE**
Adversarial review found that `entry_covered_paths` only added the leaf
file (e.g. `browser/SKILL.md`) to the covered set for nested single-file
skills, but the scanner walks one level deep and sees the parent dir
(`browser/`). Every nested single-file skill would be flagged as
untracked. Additionally, `UntrackedFile` was a public struct without
`Debug`, the help text for `validate` and `status` didn't mention the
new flags, `find_untracked` ran unnecessary I/O when structural errors
already existed, and minor code quality issues.

**CHANGES**
- `crates/deploy/src/paths.rs`: add parent dir to covered set for nested
  single-file entries in `entry_covered_paths`, fixing false positives
- `crates/deploy/src/paths.rs`: add `#[derive(Debug)]` to
  `UntrackedFile`
- `crates/deploy/src/paths.rs`: move `scan_target_dir` into
  `DirScanner` as methods (`scan_target_dir` + `consider_entry`),
  split to satisfy the `excessive_nesting` clippy lint
- `crates/deploy/src/paths.rs`: simplify `relative_to` to use
  `unwrap_or` instead of `map_or_else`
- `crates/cli/src/commands/validate.rs`: move `find_untracked()` after
  the structural error check to avoid unnecessary I/O on error paths
- `crates/cli/src/main.rs`: update `validate` long help to describe
  untracked detection and `--strict`, add example
- `crates/cli/src/main.rs`: update `status` long help to describe
  `--untracked`, add example
- Update test assertions for nested covered paths (`browser/SKILL.md`
  and `browser/` instead of `browser.md`)
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 1, 2026

Codecov Report

❌ Patch coverage is 99.05303% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.41%. Comparing base (a95cb2c) to head (248d192).

Files with missing lines Patch % Lines
crates/deploy/src/paths.rs 99.06% 4 Missing ⚠️
crates/cli/src/commands/status.rs 96.29% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master      #52      +/-   ##
==========================================
+ Coverage   91.15%   91.41%   +0.25%     
==========================================
  Files          36       36              
  Lines       15509    16012     +503     
==========================================
+ Hits        14137    14637     +500     
- Misses       1372     1375       +3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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