--- title: ".agents/ workspace hygiene" description: "Retention conventions for the .agents/ runtime workspace and how ao doctor keeps it clean." permalink: /agents-dir-hygiene last_reviewed: 2026-08-16 --- # `.agents/` workspace hygiene `.agents/` is the per-repository runtime workspace: gitignored local state written by the `ao` CLI and by skills while they work. It is not committed (except a small pinned config under `.agents/ao/`), it is not a deliverable, and it is never a source of authority — work belongs to the tracker, source and delivery history to Git, sessions to CASS, curated memory to CM, and rules to docs/ADRs. The preferred target layout from [ADR-0016](adr/ADR-0016-state-tiers.md): `ao/` (requested proof), `scratch/` (disposable work, `scratch/WRITER/DATE-SLUG/`), and `projections/` (named-consumer, manifest-stamped derived views). This target is not enforced as an exact closed set. A root outside it must have a declared compatibility or exact-path consumer contract; otherwise it is migration debt. Current declared exceptions include read-only `.agents/handoff/`, live `.agents/mto-handoff/`, and earlier skill-output roots retained by an explicit consumer contract in their owning skill. Two broad shapes of migration debt exist: - **Knowledge-shaped directories** — postmortems, pre-mortem checks, handoffs, retros, proofs: human-readable records a later session may re-read while they await promotion or expiry. - **Machine-artifact directories** — queue dirs, run scratch, staged outputs: consumed once by tooling and then debris. Left alone, the second class accumulates. The `workspace` subsystem of `ao doctor` detects specific registered failure modes and garbage-collects their debris safely; it does not currently flag every top-level root outside the preferred layout. Where this page and ADR-0016 disagree, the ADR wins. ## The ephemeral-dir contract Any writer that mints a queue- or run-shaped directory under `.agents/` MUST follow this contract: - **Mark abandonment by renaming, never by deleting.** A directory that is done or abandoned is renamed to `.stale-` with a UTC timestamp in `YYYYMMDDTHHMMSSZ` form, e.g. `land-queue-age-h433.19.stale-20260711T221032Z`. - **Retry attempts get `-retry` suffixes**, e.g. `land-queue-age-h433.22-native-retry2`. A retry directory may itself later be staled; both suffixes can appear on one name. - **GC is `ao doctor`'s job.** Writers never delete workspace directories — they only rename. Doctor collects stale and retry-chain directories whose newest content is older than the TTL. The GC TTL defaults to **14 days**, measured against the newest regular-file modification time inside the directory. Override it with `AO_DOCTOR_WS_TTL_DAYS` (a positive integer day count; invalid values fall back to the default). ## Canonical directory names Skills and sessions have historically spelled the same top-level directories several ways. Doctor normalizes toward one canonical name per concept: | Canonical | Known drift aliases | |---|---| | `postmortem` | `post-mortem`, `post-mortems` | | `pre-mortem-checks` | `pre-mortem`, `pre-mortems`, `premortem-checks` | | `ao/handoff` | `handoffs` | | `retro` | `retros` | | `proofs` | `proof` | | `tests` | `test` | This table is a projection of `workspaceCanonicalAliases` in `cli/internal/doctor/fix_workspace.go` — the Go table is the source of truth. If the two disagree, the Go table wins and this page is stale. `.agents/mto-handoff/` is not a handoff spelling alias. It is a distinct live recurrence protocol consumed at that exact path by `scripts/assay/consume-mto-recurrence.sh`; Doctor preserves it in place. ## How to clean up ```bash ao doctor # detect-only: report findings, change nothing ao doctor --fix # apply fixers (routes through mutate()) ao doctor --only fm-ws-empty-dirs # scope to one detector (repeatable) ao doctor --only workspace # or scope to the whole subsystem ``` The workspace detectors are: | Detector ID | Finds | Fix behavior | |---|---|---| | `fm-ws-stale-queue-dirs` | `.stale-*` / `-retry` directories past TTL | quarantine (rename) | | `fm-ws-empty-dirs` | empty top-level dirs (abandoned scaffolding) | quarantine (rename) | | `fm-ws-naming-drift` | drift-alias directory names (table above) | merge/rename to canonical | | `fm-ws-dual-store` | learnings split across `.agents/learnings` and `.agents/ao/learnings` | report-only; defers to `ao doctor --fix --only fm-knowledge-orphaned-flywheel-learnings` (moves top-level `*.md`/`*.jsonl` only) | | `fm-ws-nested-tree` | a `.agents` runtime tree nested under a repo subdirectory | report-only; manual review (may be an intentional nested project) | | `fm-ws-oversize` | unexpectedly large directories | report-only | Every mutating run writes receipts under `.doctor/runs/__/`: a `report.json` describing findings and applied fixes, an `actions.jsonl` journal of every mutation, and an `undo.sh` wrapper for reversing them (see the exact undo contract below). `.doctor/latest` is a symlink to the most recent run. ## Safety envelope - **Doctor has no delete operation by design.** What reads as "deletion" is an atomic rename into the run's `quarantine/` directory (`.doctor/runs//quarantine/workspace/`). Final disposal of quarantined content is always the human's call — for old runs, `ao doctor gc` requires explicit `--yes` and `--before `. - **Ambiguous merges are refused, not guessed.** When a drift-alias merge (or the knowledge subsystem's learnings consolidation) would collide with existing content and the right merge is not mechanically certain, the finding lands in a Skipped list with the reason, and nothing moves. - **What undo can and cannot reverse.** Detectors are pure reads; every disk write flows through the audited mutation path and lands as a journal line. FILE mutations are content-hashed and backed up verbatim before they change, and undo restores them from those backups. DIRECTORY renames (quarantines and drift merges) carry **no byte backup** — the renamed/quarantined tree itself *is* the preserved copy, byte for byte — and undo reverses them by replaying the rename backwards. A crash in the narrow window between a rename landing and its journal line being written leaves that one mutation invisible to undo; recovery is manual, from the run's `quarantine/` directory (the run receipts list what moved, and nothing is ever deleted). - **Concurrent external writers are bounded, not excluded.** Doctor's locks are per-process. A writer outside doctor racing a fix can slip content into a directory in the moment it is quarantined; that content is moved with the directory — journaled, restorable via undo, never destroyed. ## What doctor will NOT decide Disposal of large archives and eval payloads is a judgment call, not a mechanical one. `fm-ws-oversize` reports them — path, size, file count — and stops there. Doctor never quarantines, moves, or deletes content solely for being big; deciding what a large artifact is worth is the operator's job.