Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.4.0] — 2026-05-11

### Added — Phase 4: Dream safety net

- **Dream snapshot harness.** New top-level `cc-janitor dream` subapp:
`history`, `diff <pair_id>`, `doctor`, `rollback <pair_id> --apply`,
`prune --older-than-days N --apply`. Snapshots are stored under
`~/.cc-janitor/backups/dream/<pair_id>-{pre,post}/`; pairs recorded
to `~/.cc-janitor/dream-snapshots.jsonl`.
- **Watcher `--dream` mode.** `cc-janitor watch start --dream` polls
every `~/.claude/projects/*/memory/.consolidate-lock`. On lock-appears
writes a raw mirror pre-snapshot; on lock-gone writes the post-snapshot
and records the pair.
- **`cc-janitor dream doctor` (10 checks).** Stale `.consolidate-lock`
detection, `autoDreamEnabled` state, server-gate inference hint,
last-dream timestamp, backup dir health + disk usage, MEMORY.md cap,
memory file count, cross-file duplicate summary, settings-audit
toggle warning.
- **`cc-janitor stats sleep-hygiene`.** Four keyword/regex/dup metrics
(MEMORY.md line count, relative-date density, cross-file duplicate
count, contradicting-feedback pairs).
- **`cc-janitor backups tar-compact --kind dream`** + new scheduler
template `dream-tar-compact`. Raw mirrors compacted to `.tar.gz`
after 7 days, tarballs purged after 30 days (thresholds configurable
via `~/.cc-janitor/config.toml`).
- **`~/.cc-janitor/config.toml`** (optional). User-tunable thresholds
for dream-doctor, snapshot retention, hygiene regex extras.
- **8th TUI tab: `Dream`.** Snapshot list pane + diff viewer pane.
- **Settings audit hook.** Caches `autoDreamEnabled` value at
`~/.cc-janitor/state/autodream-last-seen.json`; on flip, writes an
audit-log entry (`cmd=settings-observe`) and surfaces a WARN row in
`cc-janitor dream doctor` ("settings autoDream toggled").
- **i18n `[dream]` and `[sleep_hygiene]` tables** for English and
Russian.
- **Three new cookbook recipes** covering snapshot-after-Dream
diff/rollback, stale-lock diagnosis, and scheduled snapshot setup.

### Fixed

- Closes verified user pain in upstream Claude Code Issues #47959
(silent Auto Dream memory deletion), #50694 (stale
`.consolidate-lock` silently disables Auto Dream), #38493
(missing `.dream-log.md`), #38461 (server-gate inference for
Auto Dream flag).

## [0.3.3] — 2026-05-11

### Added
Expand Down
28 changes: 25 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,33 @@ cc-janitor never silently destroys data:

cc-janitor is designed to be invoked by both you (TUI / CLI) and Claude Code itself (CLI), but only on your explicit request. See [docs/CC_USAGE.md](docs/CC_USAGE.md) for the reference Claude Code reads when deciding whether a subcommand is safe to call.

## Phase 4: Dream safety net

**Dream safety net** — snapshot before Auto Dream, diff after, rollback if
needed. Closes upstream Issues #47959, #50694, #38493, #38461.

```bash
# Opt-in: poll lock files and snapshot around every Auto Dream cycle
CC_JANITOR_USER_CONFIRMED=1 cc-janitor watch start --dream

# Review what each Dream cycle changed
cc-janitor dream history
cc-janitor dream diff <pair_id>

# 10 health checks covering stale locks, autoDream flag, disk, hygiene
cc-janitor dream doctor

# Roll back if Dream rewrote something you wanted
CC_JANITOR_USER_CONFIRMED=1 cc-janitor dream rollback <pair_id> --apply
```

## Roadmap

- [x] **Phase 1** (this release): sessions / permissions / context inspector / CLI / TUI / safety primitives
- [ ] **Phase 2**: memory editor, reinject hook, hook debugger with simulation, scheduler (cron / Task Scheduler)
- [ ] **Phase 3**: monorepo nested .claude/ discovery, auto-reinject watcher, stats dashboard, export/import config
- [x] **Phase 1**: sessions / permissions / context inspector / CLI / TUI / safety primitives
- [x] **Phase 2**: memory editor, reinject hook, hook debugger with simulation, scheduler (cron / Task Scheduler)
- [x] **Phase 3**: monorepo nested .claude/ discovery, auto-reinject watcher, stats dashboard, export/import config
- [x] **Phase 4**: Dream safety net — snapshot/diff/doctor/rollback, sleep-hygiene metrics, settings audit hook
- [ ] **Phase 5**: cross-platform hook fixers, `dream fix-stale-lock`, deeper TUI for Dream pair review

## Contributing

Expand Down
25 changes: 25 additions & 0 deletions docs/CC_USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,28 @@ invocation is recorded in `~/.cc-janitor/audit.log`.

Most mutating commands accept `--dry-run` to preview without applying.
Prefer dry-run when explaining what cc-janitor would do.

## Phase 4 — Auto Dream safety net (read-only commands safe for Claude)

```bash
cc-janitor dream history [--project P] [--json]
cc-janitor dream diff <pair_id> [--file F] [--json]
cc-janitor dream doctor [--json]
cc-janitor stats sleep-hygiene [--project P] [--json]
cc-janitor watch status [--json]
```

Mutating (require `CC_JANITOR_USER_CONFIRMED=1`, user must explicitly OK):

```bash
cc-janitor dream rollback <pair_id> --apply
cc-janitor dream prune --older-than-days N --apply
cc-janitor watch start --dream
cc-janitor backups tar-compact --kind dream
cc-janitor schedule add dream-tar-compact
```

If `cc-janitor dream doctor` shows a WARN row labelled
"settings autoDream toggled", advise the user to verify
`cc-janitor watch start --dream` is running before the next Dream
cycle, so memory edits will be snapshotted.
96 changes: 95 additions & 1 deletion docs/cookbook.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Cookbook

Ten task-oriented recipes for everyday cc-janitor use. Each recipe follows
Thirteen task-oriented recipes for everyday cc-janitor use. Each recipe follows
the same shape: **Problem → Command → Expected output → Next step.**

---
Expand Down Expand Up @@ -303,3 +303,97 @@ to `~/.cc-janitor/backups/import-<ts>/` before overwrite.
Or let cc-janitor write the file in the conventional location for you:

CC_JANITOR_USER_CONFIRMED=1 cc-janitor completions install bash

---

## 11. Auto Dream just rewrote my memory — how do I see what changed?

**Problem:** Anthropic's Auto Dream rewrote `~/.claude/projects/*/memory/`
during your last session. You want to know exactly which lines moved or
disappeared before deciding whether to keep the new version.

**Command:**

```bash
# Find the pair that wraps the most recent Dream cycle
cc-janitor dream history

# Diff pre vs post (unified diff, all files in the pair)
cc-janitor dream diff <pair_id>

# Narrow to a single file
cc-janitor dream diff <pair_id> --file MEMORY.md

# Regret it? Roll back to the pre-snapshot:
CC_JANITOR_USER_CONFIRMED=1 cc-janitor dream rollback <pair_id> --apply
```

**Expected output:** A coloured unified diff. `rollback --apply` restores
the pre-snapshot files in place and writes the displaced post-Dream copy
to `~/.cc-janitor/.trash/<ts>/dream-rollback-<pair_id>/`.

**Next step:** `cc-janitor stats sleep-hygiene` — surface the keyword,
duplicate, and stale-date counts that drove Dream to mutate so much.

---

## 12. Auto Dream is silently disabled — diagnose it

**Problem:** You enabled `autoDreamEnabled` in `~/.claude/settings.json`
but Dream never runs. Most common cause: a leftover `.consolidate-lock`
file from a crashed previous run (upstream Issue #50694).

**Command:**

```bash
cc-janitor dream doctor
```

**Expected output:** Ten check rows. Look for `FAIL` on `stale_lock`. If
present, manually `rm` the listed lock file(s) and rerun
`cc-janitor dream doctor`. Look for `WARN` on `settings autoDream
toggled` — that means the flag flipped since the last check; verify
backups are configured.

**Next step:** `CC_JANITOR_USER_CONFIRMED=1 cc-janitor watch start --dream`
to enable lock-file polling + snapshots, so next time you can diff before
deciding.

---

## 13. Set up scheduled snapshot-around-Dream so I never lose memory again

**Problem:** You want guaranteed pre/post snapshots around every Auto
Dream cycle, with disk usage capped automatically.

**Command:**

```bash
# Start the watcher (polls ~/.claude/projects/*/memory/.consolidate-lock)
CC_JANITOR_USER_CONFIRMED=1 cc-janitor watch start --dream
cc-janitor watch status

# Compact 7-day-old raw snapshot dirs to .tar.gz, purge 30-day-old tars
cc-janitor backups tar-compact --kind dream

# Schedule the compaction nightly via OS-native scheduler
CC_JANITOR_USER_CONFIRMED=1 cc-janitor schedule add dream-tar-compact
cc-janitor schedule list
```

**Expected output:** A `dream-tar-compact` entry in the scheduler list
(cron / Task Scheduler) running nightly. Snapshots accumulate under
`~/.cc-janitor/backups/dream/` and compact themselves.

**Next step:** Override defaults via `~/.cc-janitor/config.toml`:

```toml
[dream_doctor]
disk_warning_mb = 1024
memory_md_line_threshold = 180
memory_file_count_threshold = 200

[backups]
dream_compact_after_days = 14
dream_purge_after_days = 60
```
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "cc-janitor"
version = "0.3.3"
version = "0.4.0"
description = "Tidy up your Claude Code environment — sessions, permissions, context, hooks, schedule."
readme = "README.md"
requires-python = ">=3.11"
Expand Down
4 changes: 3 additions & 1 deletion src/cc_janitor/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from .commands.config import config_app
from .commands.context import context_app
from .commands.doctor import doctor as _doctor
from .commands.dream import dream_app
from .commands.hooks import hooks_app
from .commands.install_hooks import install_hooks as _install_hooks
from .commands.memory import memory_app
Expand All @@ -20,7 +21,7 @@
from .commands.undo import undo as _undo
from .commands.watch import watch_app

__VERSION__ = "0.3.3"
__VERSION__ = "0.4.0"

app = typer.Typer(no_args_is_help=False, help="cc-janitor — Tidy Claude Code")

Expand Down Expand Up @@ -51,6 +52,7 @@ def root(
app.add_typer(completions_app, name="completions")
app.add_typer(config_app, name="config")
app.add_typer(context_app, name="context")
app.add_typer(dream_app, name="dream")
app.add_typer(hooks_app, name="hooks")
app.add_typer(memory_app, name="memory")
app.add_typer(monorepo_app, name="monorepo")
Expand Down
83 changes: 83 additions & 0 deletions src/cc_janitor/cli/commands/backups.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
from __future__ import annotations

import shutil
import tarfile
from datetime import UTC, datetime, timedelta
from pathlib import Path

import typer

Expand Down Expand Up @@ -103,3 +105,84 @@ def prune_cmd(
shutil.rmtree(bucket, ignore_errors=True)
changed["deleted_buckets"] = [b.name for b in targets]
typer.echo(f"Deleted {len(targets)} backup bucket(s).")


@backups_app.command("tar-compact")
def tar_compact_cmd(
kind: str = typer.Option(
"dream",
"--kind",
help="Backup subtree to compact (currently only `dream`).",
),
older_than_days: int = typer.Option(
7,
"--older-than-days",
help="Tar pairs whose pre/post mirrors are all older than N days.",
),
apply: bool = typer.Option(
False,
"--apply",
help="Actually create tar.gz and remove raw mirrors (default: dry-run).",
),
) -> None:
"""Tar-compact aged pre/post mirror pairs into ``<pair_id>.tar.gz``.

Groups ``<pair_id>-pre`` / ``<pair_id>-post`` directories under the chosen
backup subtree, archives them as ``<pair_id>.tar.gz`` with ``pre/`` and
``post/`` prefixes, and removes the raw mirrors. Drives the weekly
``dream-tar-compact`` scheduler template.
"""
root: Path = get_paths().home / "backups" / kind
if not root.exists():
typer.echo("Nothing to compact.")
return

cutoff = datetime.now(UTC).timestamp() - older_than_days * 86400
pair_dirs: dict[str, list[Path]] = {}
for d in root.iterdir():
if not d.is_dir():
continue
name = d.name
if name.endswith("-pre"):
pair_dirs.setdefault(name[:-4], []).append(d)
elif name.endswith("-post"):
pair_dirs.setdefault(name[:-5], []).append(d)
old_pairs: dict[str, list[Path]] = {
pid: dirs
for pid, dirs in pair_dirs.items()
if dirs and all(d.stat().st_mtime < cutoff for d in dirs)
}

if not apply:
typer.echo(
f"[dry-run] Would tar-compact {len(old_pairs)} pair(s) "
f"older than {older_than_days}d under {root}."
)
for pid in sorted(old_pairs):
typer.echo(f" - {pid}")
return

try:
require_confirmed()
except NotConfirmedError as e:
typer.echo(str(e), err=True)
raise typer.Exit(code=2) from e

with audit_action(
"backups tar-compact",
[f"--kind={kind}", f"--older-than-days={older_than_days}"],
) as changed:
archived: list[str] = []
for pid, dirs in old_pairs.items():
archive_path = root / f"{pid}.tar.gz"
with tarfile.open(archive_path, "w:gz") as tf:
for d in dirs:
arc = "pre" if d.name.endswith("-pre") else "post"
for f in d.rglob("*"):
if f.is_file():
tf.add(f, arcname=f"{arc}/{f.relative_to(d)}")
for d in dirs:
shutil.rmtree(d, ignore_errors=True)
archived.append(pid)
changed["archived"] = archived
typer.echo(f"Compacted {len(old_pairs)} pair(s).")
Loading
Loading