SPEC-010: conformance audit — reverse sweep + folded recognition in global status - #84
SPEC-010: conformance audit — reverse sweep + folded recognition in global status#84nmccready-tars wants to merge 2 commits into
Conversation
Per-tool dirs (~/.claude et al.) accrete artifacts from many writers and sync-agents has no view of what it doesn't manage. SPEC-010 defines the ownership taxonomy (synced/folded/drifted/missing/conflict/ orphaned/foreign), the three precedence rules (.agents wins for its own artifacts; destination-only is hands-off; adoption is explicit), and a hard boundary: tool roots are application state, never enumerated. Phase 1 (audit) is scoped for implementation; Phase 2 (explicit adopt) and Phase 3 (Stow-style fold/drill materializer unifying local+global sync, with the never-fold-the-tool-root hazard) are open for discussion. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`global status` was forward-only: it classified every destination the .agents/ tree expects but had no view of what else occupies the managed subdirs, and a dir-level symlink that correctly resolves an artifact read as a conflict. Add the reverse sweep: enumerate each tool's managed artifact subdirs (never the tool root — that's application state) and classify unclaimed entries as [foreign] (hands-off, never touched) or [orphaned] (links into .agents/ with no claiming artifact). Teach the forward classifier [folded]: an expected path that resolves to the canonical artifact via an ancestor symlink is conformant, not a conflict. Close the report with a per-state count summary. Read-only throughout; covered by tests for all five SPEC-010 Phase 1 acceptance criteria. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| // ancestor symlink (a fold) already routes the destination to the | ||
| // canonical artifact even though destPath itself is not a link. A | ||
| // plain real file at destPath resolves to itself and can never equal | ||
| // a distinct wantTarget, so conflicts are not misclassified. |
There was a problem hiding this comment.
🟡 warning: foldedResolves uses filepath.EvalSymlinks on both destPath and wantTarget. If destPath is a regular file (not a symlink), EvalSymlinks will resolve it to its own path. If wantTarget also resolves to the same path (e.g., via a symlink chain), the function returns true, incorrectly classifying a conflict as folded. This violates AC-1 (a real dir at an expected artifact path should report not-a-symlink). The function should first check if destPath is a symlink before resolving, or ensure that a non-symlink file at destPath cannot match wantTarget.
| Detail: dest.SkipReason, | ||
| }) | ||
| case StrategySymlink: | ||
| expected[dest.Path] = true |
There was a problem hiding this comment.
🟡 warning: In computeStatus, the expected map only records dest.Path for StrategySymlink entries. However, for folded entries (where classifySymlinkDestination returns StateFolded), the destination path is still claimed by the artifact but is not added to expected. This means the reverse sweep in sweepUnmanaged will not skip these paths, potentially double-reporting them as foreign or orphaned. The expected map should include all claimed destinations, including those that resolve via folding.
| continue // absent dir: nothing to audit | ||
| } | ||
| for _, e := range entries { | ||
| full := filepath.Join(dir, e.Name()) |
There was a problem hiding this comment.
🔵 suggestion: In sweepUnmanaged, the check expected[full] || expected[filepath.Join(full, "SKILL.md")] only handles the case where a skill's SKILL.md is expected. However, the forward pass may also claim the skill directory itself (e.g., for folded skills). If a skill directory is claimed via a fold, it should be added to expected to prevent double-reporting. Consider adding the directory path to expected when a fold is detected.
| // is foreign and hands-off. | ||
| func classifyUnclaimed(toolID, path, agentsRootAbs string) StatusEntry { | ||
| row := StatusEntry{ | ||
| Tool: toolID, |
There was a problem hiding this comment.
🔵 suggestion: In classifyUnclaimed, the function uses filepath.Base(path) for ArtifactName. For entries in subdirectories like skills/foo/SKILL.md, this would set ArtifactName to SKILL.md instead of foo. While the sweep is one level deep, if a foreign directory contains a file, the entry's name might be misleading. Consider using the relative path from the managed subdir for clarity.
🤖 AI Code ReviewThe diff introduces a new audit sweep for SPEC-010 Phase 1. The implementation is generally sound, but there is a correctness bug in Findings: 0 critical · 2 warning · 2 suggestion (4 posted inline) model: |
What
Two commits:
SPEC-010 (
specs/) — Conformance audit + unified fold/drill sync. Defines the ownership taxonomy for per-tool dirs (synced/folded/drifted/missing/conflict/orphaned/foreign), three precedence rules (.agents/wins for its own artifacts; destination-only artifacts are hands-off; adoption is explicit-only), and a hard boundary: tool roots are application state (credentials, sessions, caches) and are never enumerated or mutated. Phase 2 (explicitadopt) and Phase 3 (Stow-style fold/drill materializer unifying local + global sync) are drafted as open discussion.Phase 1 implementation —
global statusgrows the reverse sweep and folded recognition:skills/ commands/ rules/ agents/ plans/ specs/ adrs/; Windsurf:global_workflows/; Cursor:rules/) and classify unclaimed entries as[foreign](untracked, hands-off) or[orphaned](symlink into~/.agents/with no claiming artifact)..claude/skills/<name>link) now reports[folded]instead of the false-conflict[not-a-symlink].Why
global statuswas forward-only — it could see whether.agents/artifacts were materialized, but had zero visibility into what else occupies the trees it fans into. On a machine with an organically-grown~/.claude, that means no way to size the mess, spot real conflicts, or distinguish "managed differently" from "not managed at all."Real-world run (before → after)
On a live
~/.claudewith a dir-levelrulessymlink, one dir-level skill link, one genuine shadow copy, and two untracked skills, the old classifier reported 5 false conflicts. Now:App state (
.credentials.json,sessions/,history.jsonl) never appears — the sweep is bounded to managed subdirs by construction.Tests
audit_test.gocovers all five SPEC-010 Phase 1 acceptance criteria: conflict still reported (AC-1), folded recognized + not double-reported (AC-2), foreign reported + untouched (AC-3), orphaned detected (AC-4), app state never swept (AC-5), plus the summary line. Existing status tests unchanged and green;go vetclean; fullinternal/...suite green.Docs
docs/commands/global-status.mdgains the[folded]row and an "Audit sweep" section documenting the new states and the app-state boundary.🤖 Generated with Claude Code