# Task 1899 — the account-dir reconcile reports and never moves

Design agreed 2026-07-22. Source task: `.tasks/1899-account-dir-reconcile-moves-live-state.md`.

## The problem this removes

`reconcileAccount` treats every account-root entry that is neither in the account's
`SCHEMA.md` `allowed-top-level` block nor in the hand-maintained
`PROTECTED_TOP_LEVEL` set as a stray, and moves it into `<account>/.quarantine/`.
The closed list has never matched what the platform and its agents write at an
account root, so every mismatch is a live feature or live data going dark with no
error anywhere. `PROTECTED_TOP_LEVEL` has been extended once per casualty
(`calendar-availability.json` was Task 1877). The sweep is what changes; the
allowed-list is not made complete here, because once nothing moves an undeclared
entry costs one log line rather than a broken feature.

## Units

### 1. `platform/services/claude-session-manager/src/account-dir-schema-reconcile.ts`

The move block, `quarantineDest`, and the `mkdirSync` / `renameSync` /
`appendFileSync` imports are deleted. `ReconcileCounts` loses `quarantined` and
stays the pure formatter's input, so `formatReconcileLine` still takes exactly
what it prints:

```
[fs-reconcile] stray-top-level=<n> over-deep=<n> bad-name=<n>
```

`reconcileAccount` returns a new `ReconcileResult`: the counts plus
`strayNames: string[]`. `PROTECTED_TOP_LEVEL` and `QUARANTINE_DIR` both stay.
They no longer protect anything from being moved, because nothing moves; they
keep known platform entries and existing quarantine directories out of the stray
count so the standing line stays readable and a clean account still reports zero.

### 2. `runReconcile` names the strays

One line per account that has any, emitted before the summary:

```
[fs-reconcile] op=strays account=<id> names=<a,b,c>
```

No cap on the name list. A count alone cannot distinguish six pieces of debris
from a live plugin config, and a truncated list would hide the entry an operator
is hunting for.

### 3. `platform/services/claude-session-manager/src/index.ts`

The call-site comment states that the pass quarantines. It is rewritten to state
that it reports and never moves, and why: a closed allowed-list that lags what
the platform writes turns a metric into data loss.

### 4. `platform/scripts/quarantine-restore.mjs`

One-shot, run per install with `--accounts-root <dir>`, plus `--dry-run`. For
each `<account>/.quarantine/manifest.jsonl` it walks records newest-first and for
each `originalPath`:

| On disk | Action | Log |
|---|---|---|
| Target free at the account root | move it back | `op=restore account=<id> name=<n> result=moved` |
| Target occupied by a copy the platform rewrote | leave the quarantined copy alone | `result=skipped-live-copy` |
| Target occupied by a newer copy this same run restored | leave the quarantined copy alone | `result=skipped-superseded` |
| Source absent (already restored by hand) | nothing | `result=absent` |

Three further outcomes were added during code review, each because its absence
would have been a silent skip of exactly the kind this task exists to end:

| Condition | Log | Effect |
|---|---|---|
| A manifest line that will not parse, or lacks a string `originalPath` / `quarantinePath` | `line=<n> result=unparsable` | counted, run continues |
| A record whose source escapes `<account>/.quarantine/` or whose target escapes the account | `line=<n> result=refused-outside-account` | counted, exit code 1 |
| The rename fails, or the manifest will not open | `result=failed` / `result=manifest-unreadable` | counted, exit code 1 |

The containment check exists because `.quarantine` is in the write guard's
allowed-top-level set, so an account-scoped agent can append to its own manifest,
while the script runs as the platform user across every account. Without it a
crafted record moves one account's live data into another. Every record the
reconcile ever wrote was a bare basename plus a path under `.quarantine/`, so the
check refuses nothing genuine.

Newest-first is what makes the occupied case correct for a name quarantined more
than once: the most recent copy is considered first, and every older copy of that
name then finds the target occupied. The live copy wins because the platform
rewrote it after the move, which the four `wa-channel-bindings.json` records on
account `5e99bba5` prove happened.

A restored record is appended to `<account>/.quarantine/restored.jsonl` and the
manifest is left intact, so the history of what moved and what came back
survives. The script never deletes.

### 5. `platform/templates/account-schema/SCHEMA.md`

The paragraph telling the agent that `.quarantine/` holds entries the reconcile
moved out of the top level describes behaviour this task deletes. It is reworded
to say the directory is a historical store from the pre-1899 sweep that nothing
adds to any more. Re-filing its contents through the `data-manager` specialist
still stands.

## Tests

New and rewritten cases go in the existing
`src/__tests__/account-dir-schema-reconcile.test.ts`; several of its 18 cases
assert quarantine moves and are rewritten to assert no move. The restore script
gets `platform/scripts/__tests__/quarantine-restore.test.sh`, matching how every
other `platform/scripts/` test is written.

## Out of scope

- Completing the `account-owned-dirs` declarations, and extending declarations
  from dirs to files so `data-portal.json` has a declaration path. Task 1902.
- The portal index push. It is correct and was starved of its config.
- The standing portal audit going silent when a config disappears. Task 1901.
- `over-deep` and `bad-name`. Both already count without moving.
- Installer payload twins. The payload has no committed copy of any file touched
  here; `packages/create-maxy-code/scripts/bundle.js` copies `platform/` at
  bundle time.
- Publishing, installing on the laptop's three reconcile-carrying installs, and
  running the restore. All await the operator's publish command, because the
  currently-installed sweep would re-take anything restored before it.
