# consolidate-memory — project conventions

**v0.4.78.** A **Claude Code plugin**: **cross-project, verification-first memory** for agents — the layer beyond
Claude Code's built-in Auto Dream (per-project consolidation), adding a governed cross-project store +
verification against the live code. This repo is both the plugin and its marketplace —
end users install it with `/plugin marketplace add Zenetusken/consolidate-memory` +
`/plugin install consolidate-memory@zenetusken-plugins`; maintainers dogfood the same
way against this local checkout (`claude plugin marketplace add ./` — see below). See `README.md` for the
user-facing pitch and `plugins/consolidate-memory/skills/consolidate-memory/SKILL.md`
+ its `references/harness-map.md` for the full design.

**`AGENTS.md` is this repo's operating manual** — the authoritative tree, the full command inventory,
the CI job breakdown, and the dense reference. Read it when you work here. This file carries only what
must be enforced in EVERY session, so it stays short on purpose.

## The one gotcha that matters

**This ships as a Claude Code *plugin*, not a symlinked skill.** The skill lives at
`plugins/consolidate-memory/skills/consolidate-memory/` and `SKILL.md` invokes scripts
via **`${CLAUDE_PLUGIN_ROOT}`** — a variable that is **only set when the skill loads as
a plugin**. So the old "symlink `skill/` into `~/.claude/skills`" model is dead: a bare
user-skill copy would have an unset `${CLAUDE_PLUGIN_ROOT}` and every command would
break. Dogfood by registering this repo as a local marketplace and installing the plugin:
`claude plugin marketplace add ./` then `claude plugin install consolidate-memory@zenetusken-plugins`.
**Run `python3 tests/smoke.py` after any change to `scripts/`.**

How edits take effect (once installed as a local-marketplace plugin):

| You edit | Effect |
|---|---|
| `plugins/consolidate-memory/scripts/*.py` | live on next run (exec'd fresh) |
| `…/skills/consolidate-memory/SKILL.md` body | `/reload-plugins` or next session |
| `plugin.json` / `marketplace.json` | `claude plugin marketplace update` + `/reload-plugins` |
| a release lands on `main` | installed copy re-reads its version ONLY at CC startup — mid-session: `claude plugin marketplace update` → `claude plugin update consolidate-memory@zenetusken-plugins` → `/reload-plugins` (`plugin install` no-ops when already installed) |

When iterating on the published artifact, re-validate it:
`claude plugin validate ./plugins/consolidate-memory --strict`.

## Layout

The authoritative tree is **`AGENTS.md` § Layout** — it carries `docs/adr/` (24 ADRs) and the full
`tests/` inventory, which this file never did. The directives that live HERE:

- **`${CLAUDE_PLUGIN_ROOT}`** = `plugins/consolidate-memory/` — the plugin root; the skill lives at
  `plugins/consolidate-memory/skills/consolidate-memory/`.
- **`cm` is a MAINTAINER tool.** Symlink it on PATH (`ln -s "$(pwd)/cm" ~/.local/bin/cm`); end users
  open `~/.claude/projects/<slug>/dashboards/index.html`, never `cm`.
- **The global store** lives at `~/.claude/consolidate-memory/domains/<domain>/facts`; repo-root
  `memory/` is a GITIGNORED placeholder — only `memory/.gitkeep` ships.
- **`release.sh` and `security/` are LOCAL-only** — gitignored, never published.

## Conventions

- **Zero runtime dependencies.** Scripts are stdlib-only (uses 3.8+ stdlib; no pip
  installs); CI validates the full 3.8–3.13 range (3.8/3.9 pinned to `ubuntu-22.04` —
  actions/setup-python has no build for either on the current `ubuntu-latest`/24.04
  runner image). Keep it that way — it must run anywhere Claude Code does. (`TypedDict`
  and the type hints are stdlib + runtime-invisible;
  mypy is a dev-only maintainer tool, NOT a runtime dep — see the dev loop.)
- **The cycle record is the contract — now TYPED.** `memory_status.py --json` seeds it,
  the phases fill it, `render_dashboard.py` renders it. The shape is `TypedDict`s in
  `memory_status.py` (`CycleRecord` + nested, all `total=False`); a `validate_cycle_record`
  warns (stderr, never blocks) on a wrong-container-type key at runtime. (The former
  impossible-distill-count backstop was dropped in v0.4.21 — the counts are script-truth,
  and the scanner's output caps are its own constants.) Changing the
  schema means updating the seed, the renderer, the **TypedDicts**, and `SKILL.md`'s
  schema block together — a smoke test pins the SKILL block to `CycleRecord.__annotations__`,
  so they can't silently drift.
- **Model produces data, scripts produce presentation.** Don't hand-write report
  prose — emit a cycle record and render it, so output stays consistent.
- **Style:** match the existing scripts — imperative, explain *why*, type hints,
  small pure functions that the smoke tests can exercise.

## Safety (this repo is PUBLIC)

- **Never commit personal memory.** The global store lives at
  `~/.claude/consolidate-memory/domains/<domain>/facts` (legacy `~/.claude/memory/`:
  migration-only); repo-root `memory/` is just a gitignored placeholder. Only
  `memory/.gitkeep` belongs on the remote. Verify with
  `git ls-tree -r --name-only origin/main | grep memory`.
- **Keep the skill generic.** No hardcoded user paths, project names, or identities —
  use placeholders (`/home/you/project/foo`). It's meant to be reusable by anyone.
- **Secrets firewall at retrieval.** `extract_signals.py` omits credential-shaped
  turns before they reach context; don't weaken that.

## Dev loop

```
edit plugins/consolidate-memory/… → python3 tests/smoke.py → python3 tests/simulate_accumulation.py
→ mypy --config-file mypy.ini → ./cm <cmd> to spot-check → python3 tests/validate_manifests.py
→ python3 tests/docs_links.py → (before go-live) the local DevSecOps pentest harness
→ git commit && git push
```

⚠ `docs_links.py` is one of the four validators the release harness runs (see "Releasing"), and it is
the one most easily forgotten: run it after editing `README.md`, `CHANGELOG.md`, `AGENTS.md`, or any doc
a reader links to. `validate_manifests.py` is portable and takes no flags — the `--strict` variant
belongs only to the claude CLI (`claude plugin validate ./plugins/consolidate-memory --strict`, used
when iterating on the published artifact).

`mypy --config-file mypy.ini` is a **dev-only** contract check (catches cycle-record
drift on the producer side — a renamed/extra/wrong-typed key in a seed/demo literal). It
is NOT a runtime dep and NOT part of the dep-free `smoke.py` gate; the config is pragmatic
(checks both plugins' `scripts/` + dream-beta-tester `fixtures/` + `tests/`, not `--strict`), and must never disable the TypedDict checks
(`typeddict-item`/`typeddict-unknown-key` ARE the contract).

This tool dogfoods itself: once dev-installed as a plugin (local-marketplace add + `claude plugin install`), run `dream`
from this repo to consolidate its own development memory — written to its private store
at `~/.claude/projects/<slug>/memory/`, never to this repo. The `cm` CLI and the tests
invoke the scripts by explicit path, so they work without the plugin being installed. At an arc
boundary, `cm status` (any `memory_status.py` run) surfaces a **dream-timing advisory** — a no-nag
nudge when commits have accrued since the last dream — so you can catch a good consolidation boundary
*before* a compaction (advisory only; the skill never auto-fires).

## Releasing (auto-update cycle)

Installed plugins auto-update at Claude Code startup when the plugin's `version`
(`plugins/consolidate-memory/.claude-plugin/plugin.json`) changes on `main` (public
marketplace, no token needed). So a release = a bumped version landing on `main`. Keep
`version` ONLY in `plugin.json` (never also in `marketplace.json`).

**Versioning policy (pre-1.0; deterministic — decide IN ORDER):**
1. First **stable / committed-API** release → **major** (→ `1.0.0`).
2. **Breaks an existing install** — incompatible cycle-record schema, a removed/renamed
   script or CLI flag, a changed install/marketplace/manifest contract → **minor**
   (`0.N → 0.N+1.0`). (Pre-1.0, breaking changes ride a minor bump.)
3. Otherwise — additive feature, enhancement, fix, or docs that stays
   **backward-compatible** (legacy cycle records still render, existing installs keep
   working) → **patch** (`0.N.M → 0.N.M+1`). Full per-version precedent, and the version each
   rule was first exercised by: `CHANGELOG.md`.

**The release harness (local, gitignored `./release.sh`) is deterministic by
construction:** it reads the target version from the **top `## [X.Y.Z]` CHANGELOG
section** — the single source of truth you author + review during the cycle, NOT a bump
keyword — then computes the bump TYPE from the delta and enforces the policy. So author
the `## [X.Y.Z]` CHANGELOG entry first (using the policy above), then release in two
phases with a human merge between them (GitHub requires PRs to `main`):
- `./release.sh` — **dry-run**: prints current→target, the computed bump type, the tag,
  and the notes. No writes.
- `./release.sh --stage` — guards the clean tree, then **VERIFIES, never authors**: it
  asserts `plugin.json` already equals the CHANGELOG version (hand-bumped on your feature
  branch) and refuses otherwise, then runs the four validators
  (manifests + docs_links + smoke + sim). It edits no file, commits nothing, pushes
  nothing, and opens no PR — the bump rides your feature PR. (It does still `git fetch`
  on the way in, so it is not a read-only command. The TAG surface is guarded by
  `--finalize`, not here.)
- `./release.sh --finalize` — fetches, verifies `main`'s `plugin.json` equals the
  CHANGELOG version (the merge must have landed the bump), then **runs the same four
  validators** — on the working tree, which it names and compares against the commit being
  tagged, warning (not refusing) when they differ — so a red one stops the release here
  rather than shipping — tags `origin/main` (the pre-bumped path) or the release PR's
  merge commit if one exists, pushes the tag, and cuts the GH Release. Re-running reports
  already-done.
- **Pre-bump REQUIRED:** `plugin.json` must already equal the CHANGELOG version on your
  feature branch before `--stage` will pass — the bump rides your
  feature PR, so there are zero release PRs. There is no last-minute-bump fallback:
  `--stage` refuses when `plugin.json` is behind the CHANGELOG, and the remedy it
  prints is the hand-bump.
- `./release.sh --expect patch|minor|major [--stage|--finalize]` — also **asserts** the
  computed bump matches your intent (a second guard; aborts on mismatch).

It refuses a non-forward or multi-step version, an unfilled CHANGELOG stub, a dirty
tree (untracked files count — move session exports out first), or an existing tag —
and `--finalize` refuses when `main`'s version doesn't match the CHANGELOG or the
release PR isn't merged. (Deliberately structural: an earlier keyword-driven flow mis-shipped a
`minor` as a `patch`, so the version is tied to the reviewed CHANGELOG, never a release-time judgment.)

**Cycle closeout hygiene — `--finalize` ends the cycle, including the branch surface**
(2026-09-05, the maintainer's standing rule). A completed release leaves NO stale
branches or worktrees: after the tag + GH Release, the harness prunes every branch
merged into `origin/main` on BOTH sides (zero unique content, reflog-recoverable),
runs the worktree sweep, and reports the result (any surviving worktree is named,
never silently deleted). The sweep also runs on an already-done re-run, so a past
release gets it for free. Never leave the cycle's branches behind by hand.
