CLAUDE.md@plugins/lifecycle-guard · git:20261031.6ec557e · 2026-10-31 · sha256 5e1e2e2d5803dfd4
CLAUDE.md@plugins/lifecycle-guard git:20261031.6ec557eA
Immutable. This exact content never changes and is served at /api/v1/blob/5e1e2e2d5803dfd4.
# lifecycle-guard — project guide (CLAUDE.md)
A Claude Code **plugin** that stops AI coding agents from declaring work "done" when
only the happy path is built. It forces a full-lifecycle spec up front, **blocks
completion** until a checklist is satisfied, runs an **independent fresh-context critic
on a different model**, and **learns** new checklist/style rules from your own
corrections over time.
> Thesis (the "Beyond the Happy Path" idea): an agent grading its own work in the same
> session says "done" too early. The fix is three layers that don't share the agent's
> blind spots — a deterministic gate, a different-model critic in fresh context, and a
> checklist that accumulates from real past misses.
- **Plugin root:** `~/claude-plugins/vimal-tools/plugins/lifecycle-guard/` (this dir).
- **Marketplace repo:** `~/claude-plugins/vimal-tools/` (`.claude-plugin/marketplace.json` lists this plugin).
- **Version:** `1.12.0` (see `.claude-plugin/plugin.json`).
- **Author:** Vimal Prakash. Runtime: Python 3 hooks + Markdown skill/agent/commands. No build step.
---
## The three mechanisms
1. **Spec-first workflow (the skill).** On a feature request, the agent writes
`.lifecycle/<slug>/spec.md` (actors, state machine, per-actor capabilities, failure
paths, cross-cutting) and a `tasks.md`, then implements against them. See
`skills/lifecycle-guard/SKILL.md`.
2. **Deterministic Stop gate (no LLM).** `hooks/on_stop.py` refuses to let the turn end
while an **active** `tasks.md` has open `- [ ]` items, and — even when all are ticked —
until a `review.md` with `VERDICT: PASS` (or `review: deferred`) exists. This is plain
Python/regex: it shares zero blind spots with any model and can't be rationalized past.
3. **Independent critic (fresh context, different model).** `agents/lifecycle-reviewer.md`
is a read-only skeptic (`model: sonnet`, tools `Read/Grep/Glob/Bash`) that reviews a
just-built feature against its spec + checklists and returns ranked findings. It runs
on a **different model** than the builder (builder is whatever the main session uses,
typically Opus) and never sees the build's own reasoning. The main agent fixes; the
critic never edits.
The role/model pairing is a dial: bigger-builder→smaller-critic (current) or
smaller-builder→bigger-critic both decouple blind spots. The deterministic layer is the
true-independence floor; the critic is the last mile for what tests can't encode.
---
## Repository layout
```
.claude-plugin/plugin.json name/version/description/author (version lives here)
agents/lifecycle-reviewer.md the critic subagent (model: sonnet, read-only)
commands/
audit.md /lifecycle-guard:audit [all] — review rules: keep/scope/reword/drop
bootstrap.md /lifecycle-guard:bootstrap — seed KB from ALL past sessions
learn.md /lifecycle-guard:learn [auto] — distill recent prompts/corrections
lg-status.md /lifecycle-guard:lg-status — show learned rules + pending tasks
review.md /lifecycle-guard:review [slug] — run the critic + write verdict
hooks/
hooks.json registers UserPromptSubmit / Stop / SessionEnd
lg_common.py shared helpers: data dir, config, seeding, jsonl logging
on_prompt.py UserPromptSubmit: log prompt, detect correction/feature, nudge
on_stop.py Stop: the definition-of-done gate (see above)
on_session_end.py SessionEnd: auto-run /learn in the background when enough piled up
scripts/digest.py prepares raw material for /learn (also --bootstrap/--mark/--stats/--audit/--rules/--context/--rule/--lint-rules/--footprint/--usage/--project/--prune-logs/--ingest-usage/--lint-spec)
skills/lifecycle-guard/
SKILL.md the full-lifecycle workflow the agent follows
references/review.md review rubric the critic/`/review` uses
references/spec-template.md spec sections + tasks template + portability test (read in §2 only)
seed/core.md universal checklist — SEED copied to the KB on first run
seed/style.md style/conventions seed
seed/domains/{auth,notifications,payments}.md domain checklists seed
tests/test_provenance.py stdlib unittest: hook origin + audit flags (temp LG_DATA_DIR)
tests/test_prompt_noise.py stdlib unittest: injected/pasted text never feeds learning
tests/test_rule_usage.py stdlib unittest: rule ids, Stop-hook usage ingest, audit freshness
tests/test_stop_gate.py stdlib unittest: every Stop gate case (boxes, review, status, guards)
tests/test_maintenance.py stdlib unittest: project identity + nested scopes, retention, dupes/size
tests/test_spec_lint.py stdlib unittest: --lint-spec flags generic requirement lines only
tests/test_context.py stdlib unittest: --context load (scope omit, ids = --rules, flags) + prose pointers
tests/test_budget.py stdlib unittest: CONTEXT BUDGET — caps every tier of text the plugin puts in context
tests/test_manifest.py stdlib unittest: manifests, frontmatter, hook wiring, SHA pins, CHANGELOG=version
evals/ behavioural golden set for `claude plugin eval` (fixture shop + 9 cases);
NOT in CI (paid sessions) — see evals/README.md
```
### Important: learned data lives OUTSIDE the plugin
So it survives plugin updates, the knowledge base is at **`~/.claude/lifecycle-guard/`**
(override with `LG_DATA_DIR`). `lg_common.ensure_data_dir()` seeds it from
`skills/lifecycle-guard/seed/` on first run and re-copies `scripts/digest.py` → `bin/digest.py`
on every run so updates propagate.
```
~/.claude/lifecycle-guard/
core.md universal lifecycle checklist (seeded, then grows via /learn)
style.md your stack/conventions/preferences (learned from prompts)
domains/*.md per-domain checklists (payments, auth, notifications, …); auto-created
prompts.jsonl every prompt you type (raw material for style learning)
corrections.jsonl prompts that look like "you missed X" (raw material for gap rules)
usage.jsonl which learned rules each review applied (caught | satisfied), from review.md
changelog.md every change /learn made (audit + revert reference)
backups/ file snapshots taken before each learn
bin/digest.py copy of scripts/digest.py (fixed path for commands)
config.json toggles + thresholds + last-learn timestamp
bootstrap/ chunked past-session material (created by digest --bootstrap)
```
`core.md`, `style.md`, and `domains/*.md` each carry a `## Learned` section; items there
are mandatory — each exists because something was missed before, formatted
`- [ ] [scope: <project>] <rule> _(learned YYYY-MM-DD @ <project>: <cause>)_ _(reviewed YYYY-MM-DD)_`.
`@ <project>` = origin (`digest.project_name`: the enclosing git repo's folder name, else the cwd's
name — folder names, not remotes, so labels stay stable); `[scope: X]` (optional) = applies where X is
the project or any enclosing folder below $HOME (`digest.project_scopes`), so an umbrella-folder
scope covers its nested repos; `digest.py --rules` marks the rest `N/A here` (skill + reviewer rely on
that, not prose matching); `_(reviewed …)_` (optional) = last audit keep.
Legacy rules without `@ project` still parse — `digest.py --audit` flags them `no-origin`. The
audit also flags `stale` (older than `review_after_days`, default 90), `looks-project-specific`
(unscoped but names a project from `prompts.jsonl` or a hostname) and `possible-duplicate` (word-set
Jaccard ≥ `duplicate_threshold`, default 0.5 — the real KB's max is ~0.17, so it only fires on true
re-writes; conceptual overlap is /audit's consolidate step), and warns when a file exceeds
`kb_budget_kb` (12). The audit header is one block ending in a blank line — `/lg-status` reads it
with `sed -n '1,/^$/p'`, so never insert a blank line inside it. `/lifecycle-guard:audit` turns
the report into keep / scope / reword / drop decisions (user-confirmed, backed up, changelogged).
**Usage:** a rule's id = first 8 hex of sha1(normalized text) (`digest.py --rules`); rewording gives a
new id, so history restarts (fail-safe: worst case one extra review). The reviewer reports
`RULES APPLIED`, the main agent copies it into review.md `## Rules applied`, and `on_stop.py` ingests it
(`digest.ingest_usage`, idempotent per project|feature|parsed-rules key — prose edits don't recount,
and a re-review listing identical rules+outcomes isn't new evidence; `ts` = review.md mtime; runs
before the loop guard, never affects the gate). Audit freshness = days since max(learned, reviewed, last applied).
---
## Per-feature artifacts (created in the CONSUMER project, not here)
When the plugin is active in some other repo, a feature produces `.lifecycle/<slug>/`:
- **`spec.md`** — goal, actors, entity state machine, per-actor capabilities, failure/edge
cases, cross-cutting (perms, tenant isolation, audit, notifications, reporting), checklist
coverage, acceptance criteria, out-of-scope, plus placement/reuse + business-rules sections.
- **`tasks.md`** — header `status: draft|active|done` + checkboxes grouped by area
(Backend / Customer UI / Admin UI / System-jobs-webhooks / Notifications / Tests / Ops).
`- [ ]` open, `- [x]` done+verified, `- [~] item — reason` deferred. The Stop gate reads this.
- **`review.md`** — the critic's verdict. Must contain `VERDICT: PASS` (or `review: deferred`)
before the Stop gate lets the feature be marked done.
Lifecycle of a feature: draft spec+tasks → user approves → `status: active` → implement,
ticking boxes → run critic → fix findings → write `review.md` PASS → `status: done`.
---
## Hooks — exact behavior
- **UserPromptSubmit → `on_prompt.py`** (never blocks): when `kb_over_budget` is set, adds a one-line
"/audit to merge rules" reminder at most once a week (merging is the one budget fix that needs a
person). Skips automated sessions entirely
(`CLAUDE_CODE_ENTRYPOINT=sdk-*`, i.e. `claude -p` / Agent SDK — tools, not the user; `digest.is_automated`,
opt in with `learn_from_automated`); otherwise appends the prompt (with its entrypoint) to
`prompts.jsonl`; if it matches the CORRECTION regex ("you missed / forgot / where is the /
not done / half-done…") it logs to `corrections.jsonl` and tells the agent to write the
lesson into the right checklist now; if it matches the FEATURE regex it reminds the agent
to run the lifecycle workflow. Gated by `feature_nudge`.
- **Stop → `on_stop.py`**: first ingests rule usage from `.lifecycle/*/review.md` (fail-safe), then the gate described above. Guards against infinite loops via
`stop_hook_active`; respects `stop_gate` config; only fires on active features.
- **SessionEnd → `on_session_end.py`**: prunes old logs, then runs `digest.maintain()` — **automatic KB
budget upkeep, no model**: trims `cause` stamps over 80 chars (backup + original in changelog; ids
unchanged) and records `kb_too_long` / `kb_over_budget` in config. Then, if `auto_learn` is on and
either enough new corrections/prompts accumulated since `last_learn_ts`, or `kb_too_long` is non-empty
(at most once a day, `last_compact_ts`), it spawns a
**headless** `claude -p /lifecycle-guard:learn auto` run (`LG_HEADLESS=1`, a `.learn.lock`
file prevents overlap, output to `learn.log`). It edits only files under the KB.
`LG_HEADLESS=1` makes hooks no-op so the background learner can't recurse.
---
## The learning loop
Raw material (`prompts.jsonl` + `corrections.jsonl`) → `scripts/digest.py` formats what's new
since the last learn → `/lifecycle-guard:learn` distills it into `## Learned` rules in
`core.md` / `domains/*.md` (or style rules in `style.md`), backs up first, logs to
`changelog.md`, then `digest.py --mark` records the timestamp. `--bootstrap` mines *all*
past sessions in `~/.claude/projects` into `bootstrap/` chunks for a one-time seed.
Two ways it grows: **in the moment** (a correction → the agent appends a generalized rule
immediately, prompted by `on_prompt.py`) and **in the background** (`on_session_end.py`).
---
## config.json toggles (in the KB, not the plugin)
```
auto_learn (true) run /learn in background at session end
auto_learn_min_corrections (2) threshold to trigger background learn
auto_learn_min_prompts (40) alternative threshold
stop_gate (true) block "done" while tasks.md has open boxes / no PASS
feature_nudge (true) inject the lifecycle reminder on feature-like prompts
last_learn_ts (0) bookkeeping
```
Defaults live in `lg_common.DEFAULT_CONFIG`; `config.json` overrides them.
---
## Working on this plugin
- **Edit the skill/agent/commands/hooks here;** they take effect on `/reload-plugins` (or a new
session). The knowledge base in `~/.claude/lifecycle-guard/` is user data — don't ship it in
the plugin; only `skills/lifecycle-guard/seed/` ships and seeds it.
- **Bump `version` in `.claude-plugin/plugin.json`** on a meaningful change (history: 1.2.0
entity-coverage → 1.3.0 entity cross-links on every page → 1.4.0 orient-before-building /
no-duplicate-surface → 1.5.0 business-logic reconciliation → 1.6.0 rule provenance, scoping + /audit → 1.6.1 learn only from the user's own words → 1.7.0 rule usage tracking → 1.8.0 gate tests + fixes, git-root identity, retention, dupes/size → 1.8.1 bin refresh before slash commands → 1.9.0 spec-slop lint + reviewer gate, SHA-pinned CI,
manifest test, PRIVACY.md → 1.10.0 behavioural evals + review routing fix → 1.12.0 context diet + budget guard (#1);
full history in the repo-root `CHANGELOG.md`).
- **Releases are automatic:** a version bump merged to `main` triggers `.github/workflows/release.yml`
(tests → `v<version>` release, notes = bump commit body + commits since last tag). CI
(`.github/workflows/test.yml`) runs the tests on every push/PR on Python 3.9 and 3.13.
- **Run the tests** before committing: `for t in plugins/lifecycle-guard/tests/test_*.py; do python3 $t; done`.
`digest.py` honours `LG_DATA_DIR` too, so tests never touch the real KB.
- **Hooks must stay fast and fail-safe** — each wraps `main()` in try/except and prints nothing
on error (a crashing hook must never break the user's turn). Keep them dependency-free (stdlib
only) and under the configured timeouts (10–15s).
- **`scripts/digest.py` is the source of truth**; it's copied to `~/.claude/lifecycle-guard/bin/`
on every hook run, so edit it here, not there. It also owns `clean_prompt()` (what the user
actually typed: drops `<task-notification>`/`<system-reminder>`/command blocks, `<pasted_content>`
and `[Image #N]` placeholders) and the single `CORRECTION` regex; the hooks import both via
`lg_common` (with a safe fallback so a broken import never disables the Stop gate). Old log
records are filtered at read time — never rewrite the user's jsonl files.
- **Changing seed files** only affects NEW installs (existing KBs already copied them). To push a
rule to existing users, it has to go through `/learn` or they re-seed.
## Gotchas
- **Measure prompt changes with the evals.** Before editing SKILL.md, the reviewer agent or a command
description, run the affected eval cases, change one thing, and rerun (`evals/README.md`). Always pass
`--judge-model sonnet`; the default judge misgrades long reviews. Routing is decided by command and agent
`description`s, so write them in the words users actually use ("is it done?"), not the mechanism.
- `--lint-spec` is advisory and lexical: it lints only requirement sections (actors, state, capabilities,
failure/edge, cross-cutting, acceptance, behaviour) when a spec has them, else the whole file; quoted
text, code fences and `N/A` lines are mentions, not requirements. It is line-based: multi-line backtick
spans aren't recognised, and "notify the user via" + "email" on the next line is still flagged. The reviewer's Spec-specificity gate is the judgement layer.
- **Context budget (#1 — never again).** Everything this plugin adds to a model's context is a cost paid
on every later turn. Tiers and caps (enforced by `tests/test_budget.py`; raise one only with a reason
in the commit and CHANGELOG):
| Tier | When it's paid | Cap |
|---|---|---|
| Model-visible descriptions (skill, agent, commands without `disable-model-invocation`) | every turn, every session | 1,450 B |
| One hook note (correction / feature nudge) | per matching prompt, stays in the transcript | 700 B |
| `SKILL.md` | per feature, then every later turn | 7,200 B |
| `references/spec-template.md` / `review.md` | only in the phase that needs it | 4,800 / 4,500 B |
| `--context` builder load (60-rule fixture, 1 domain) | per feature | 9,000 B |
Rules: load an index and fetch detail on demand (`--context` headlines + `--rule`); filter in
`digest.py`, not in the model; put phase-only detail in `references/`; send instructions just in time
(hooks) instead of up front; heavy, complete loads (`--full`) only in disposable subagent contexts;
user-only commands get `disable-model-invocation: true`; the knowledge base is budgeted at write time
(`rule_max_chars`, `kb_budget_kb`). Never tell an agent to read the KB files directly, except `/learn` and `/audit`, which edit them (auto-learn pays that full read on each run).
- CI actions are pinned to commit SHAs (`test_manifest` enforces it); bump them deliberately.
- The Stop gate keys off `status: active` in `tasks.md`. A `draft` or `done` feature won't block.
A feature with all boxes ticked but no `review.md` PASS **still blocks** — that's intentional.
- Deferring is first-class: `- [~] item — reason` satisfies the gate. Don't fake `- [x]`.
- The critic is read-only by design. If it could edit, it would stop being an independent check.
- Project identity is the git root's **folder name** (no subprocess; walks up for `.git`, stopping at
$HOME). Two repos with the same folder name share a label. Good enough for review and scoping;
don't build security decisions on it.
- Log retention: SessionEnd prunes `prompts.jsonl`/`corrections.jsonl` older than
`log_retention_days` (180) but never past `last_learn_ts`, and keeps unparseable lines.
`usage.jsonl` and `changelog.md` are never pruned.
- The Stop gate reads the `status:` **header line** and a filled-in `VERDICT: PASS` **line** (regexes
`STATUS` / `VERDICT_OK` in on_stop.py) — a phrase inside a task or the unedited `PASS | FAIL`
template must not count. `tests/test_stop_gate.py` pins every gate case.
- Everything the hooks log is the user's own prompt text — treat the KB as sensitive (it may
contain project details); it stays local under `~/.claude/`.