Summary
Three related improvements to make `ballast doctor` a reliable integrity check for installed rules:
- Self-identification marker — embed a machine-readable fingerprint in every Ballast-managed rules file so the tool can verify ownership and content without guessing.
- 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.
- 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:
- Enumerate all files under the rules destination directories (`.claude/rules/`, `.cursor/rules/`, `.codex/rules/`, `.gemini/rules/`).
- For each file, parse the self-identification marker.
- Re-generate the canonical content for the rule ID using `buildContent()` and compare checksums.
- 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
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).
Summary
Three related improvements to make `ballast doctor` a reliable integrity check for installed rules:
Motivation
Installed rules files can diverge from the source over time:
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:
Example header format for markdown rules files:
```
```
This marker must be:
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:
`formatDoctorReport` should output a Rules files section:
```
Rules files:
```
3. `ballast doctor --fix` stale-file removal
When `--fix` is passed, in addition to upgrading CLI installs:
Acceptance criteria
Out of scope
Governing PRD section
`PRD.md` — Doctor / health-check command (update or add acceptance criteria before implementation).