Skip to content

feat(doctor): detect drift and remove stale rules files #166

Description

@markcallen

Summary

Three related improvements to make `ballast doctor` a reliable integrity check for installed rules:

  1. Self-identification marker — embed a machine-readable fingerprint in every Ballast-managed rules file so the tool can verify ownership and content without guessing.
  2. Drift detection — `ballast doctor` should compare each installed rules file against the canonical content Ballast would generate today, and report any that have been modified.
  3. Stale-file removal — `ballast doctor --fix` (or a new `ballast prune` sub-command) should delete rules files that are no longer part of the currently configured targets/agents.

Motivation

Installed rules files can diverge from the source over time:

  • A user edits a file manually (or a merge conflict leaves noise in it).
  • A target or agent is removed from `.rulesrc.json` but the old file is never cleaned up.
  • A Ballast upgrade changes a rule, but the on-disk copy is stale.

Without a way to detect these states, `ballast doctor` gives a false "no action needed" result even when the project is actually out of sync.


Proposed changes

1. Self-identification marker

Every Ballast-managed rules file already starts with a human-readable `Created by Ballast` line (built in `getCreatedByBallastLine()`). Extend this to include a structured, machine-readable front-matter block that identifies:

  • The Ballast version that wrote the file.
  • The canonical rule ID (target + agent + language suffix, e.g. `typescript/linting`).
  • A content checksum (CRC-32 or SHA-256 of the rule body that follows).

Example header format for markdown rules files:

```

```

This marker must be:

  • Written by every `buildContent` / `buildClaudeSkill` code path in `build.ts`.
  • Ignored (or stripped) when computing the checksum so the marker itself is not self-referential.
  • Parseable by the doctor without requiring a full build — a single-line regex is sufficient.

2. `ballast doctor` drift detection

Add a new section to `DoctorReport`:

```ts
interface RuleFileStatus {
/** Absolute path to the installed file. /
path: string;
/
* Canonical rule ID extracted from the self-identification marker. /
ruleId: string | null;
/
* Whether the file is still part of the current config. /
active: boolean;
/
* Whether the on-disk content matches the canonical checksum. /
drifted: boolean;
/
* Whether the file has no Ballast marker (unowned / manual file). */
unowned: boolean;
}

interface DoctorReport {
// ... existing fields ...
ruleFiles: RuleFileStatus[];
}
```

`runDoctor()` should:

  1. Enumerate all files under the rules destination directories (`.claude/rules/`, `.cursor/rules/`, `.codex/rules/`, `.gemini/rules/`).
  2. For each file, parse the self-identification marker.
  3. Re-generate the canonical content for the rule ID using `buildContent()` and compare checksums.
  4. Add a recommendation for each drifted or stale file.

`formatDoctorReport` should output a Rules files section:

```
Rules files:

  • .claude/rules/typescript/typescript-linting.md [ok]
  • .claude/rules/typescript/typescript-logging.md [DRIFTED — run ballast install --refresh to restore]
  • .claude/rules/common/old-rule.md [STALE — no longer in config, run ballast doctor --fix to remove]
  • .claude/rules/manual-notes.md [unowned — not managed by Ballast, skipped]
    ```

3. `ballast doctor --fix` stale-file removal

When `--fix` is passed, in addition to upgrading CLI installs:

  • Delete any rules files whose `active: false` (stale, no longer in current config).
  • Do not automatically overwrite drifted files — require an explicit `ballast install --refresh` for that, to avoid silently clobbering user edits.
  • Print each deleted file path so the action is auditable.
  • Skip files with `unowned: true` (no Ballast marker) — never delete files Ballast did not write.

Acceptance criteria

  • Every rules file written by Ballast contains the self-identification marker with `id`, `version`, and `checksum` fields.
  • `ballast doctor` lists all rules files in the destination directories, categorised as `ok`, `drifted`, `stale`, or `unowned`.
  • `ballast doctor` adds a recommendation for each drifted or stale file.
  • `ballast doctor --fix` removes stale files (those no longer in the current config) and prints their paths.
  • `ballast doctor --fix` does not delete unowned files (files without a Ballast marker).
  • `ballast doctor --fix` does not overwrite drifted files; it recommends `ballast install --refresh` instead.
  • Existing tests pass; new tests cover marker parsing, drift detection, and stale-file removal.
  • Coverage remains ≥ 75%.

Out of scope

  • Changing the format of `.rulesrc.json`.
  • Auto-refreshing drifted files (that is the job of `ballast install --refresh`).

Governing PRD section

`PRD.md` — Doctor / health-check command (update or add acceptance criteria before implementation).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions