From 2dc503380d3304455aad88d637cbf5d27a1a8840 Mon Sep 17 00:00:00 2001 From: Paul Gebheim <86010+pgebheim@users.noreply.github.com> Date: Tue, 1 Sep 2026 22:25:50 +0000 Subject: [PATCH] feat(state): keep long-run skill state on disk so compaction stops losing it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A rig skill outlives its own transcript. `/rig-task` spans seven steps and four delegated agents; `/rig-epic run` loops that over every child. Between those steps the host agent's only memory is the conversation, so a compaction mid-run drops the acceptance criteria, the failing assertion, and which review findings are already fixed — and the agent re-derives them from git and gh, or guesses. `rig-epic` already worked around this with a hand-rolled `.rig/epics/*.json` ("replaces any external memory") — unvalidated, unschema'd, and used nowhere else. This generalizes that one good idea into a primitive. `scripts/rig-state.ts` keeps one small structured document per run under `.rig/state/`, with an append-only journal beside it for humans debugging a failed run. Updates are patches, merged and validated outside the model: unknown keys, wrong types, undeclared phases, and impossible states are rejected with the reason, leaving the state untouched for a retry. Size budgets (500 chars per string, 50 items per array, 8 KB per document) are what keep the state a state instead of a transcript by another name. Guards make a document assert the gates the skills already describe in prose: rig-task can't reach `pr-open` with a red suite or an open P1, rig-epic can't reach `finish` with an unmerged child, rig-review can't call a round `clean` while a finding is open. rig-task, rig-epic, and rig-review now read and patch it; rig-epic's epic file moves to `.rig/state/.json`, so there's one mechanism rather than two. rig-doctor checks `.rig/state/` is gitignored and reports stale runs. Every state call is optional — with the script absent the skills run as before. This does not shorten the host agent's context; rig ships markdown procedures and Claude Code and pi own their own loops. What changes is the cost of losing the transcript. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01MBaTgFSa8va6PTaFfuSRsz --- README.md | 11 +- docs/state.md | 134 ++++++++ scripts/rig-state.test.ts | 297 +++++++++++++++++ scripts/rig-state.ts | 651 +++++++++++++++++++++++++++++++++++++ skills/rig-doctor/SKILL.md | 11 +- skills/rig-epic/SKILL.md | 129 +++++--- skills/rig-plan/SKILL.md | 4 +- skills/rig-review/SKILL.md | 32 +- skills/rig-task/SKILL.md | 73 ++++- 9 files changed, 1285 insertions(+), 57 deletions(-) create mode 100644 docs/state.md create mode 100644 scripts/rig-state.test.ts create mode 100644 scripts/rig-state.ts diff --git a/README.md b/README.md index 078d2f0..fb28188 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ read at runtime from a small **project profile** you fill in during onboarding. | **Agents** (`agents/`) | `rig-debugger`, `rig-reviewer`, `rig-architect`, `rig-qa`, `rig-coder` | Copied into `/.claude/agents/` | | **pi adapter** (`pi/`) | Per-target persona frontmatter (`pi/agents/*.yml`, assembled onto the shared bodies) + the `/rig` dispatcher prompt | Copied into `/.pi/{agents,prompts}/` — see [`docs/pi.md`](docs/pi.md) | | **Support docs** (`templates/`) | starter `REVIEWER.md` (+ `REVIEWER.scope-template.md`), `STYLE.md`, `label-mapping.md` | Copied into `/.claude/` (only if absent) | -| **Scripts** (`scripts/`) | `setup-worktree.sh`, `remove-worktree.sh`, `mint-gh-app-token.sh`, `scope-reviewer.ts`, `set-session-name.sh` (Claude-only) | Copied into `/.claude/scripts/` | +| **Scripts** (`scripts/`) | `setup-worktree.sh`, `remove-worktree.sh`, `mint-gh-app-token.sh`, `scope-reviewer.ts`, `rig-state.ts`, `set-session-name.sh` (Claude-only) | Copied into `/.claude/scripts/` | | **CI templates** (`ci/`) | `security-scan`, `label-pr`, `image-build`, `slack-notify`, `test-gate`, `test-e2e`, and the **AI review-bot bundle** (`review-bot-gate`, `auto-review-fix`, `pr-review-labels`) | Copied into `/.github/workflows/` (see `ci/README.md`) | | **Onboarding** (`skills/rig-onboard/`) | The agent-driven installer skill | Run once against a target project | @@ -89,6 +89,15 @@ reference (the origin project's own values). colocated with the code, mirroring nested `AGENTS.md`). `rig-review` resolves the scoped files a diff touches and asserts them as P1s, so a subsystem's hard-won invariants stop getting re-discovered in PR review. +- **Long runs keep state on disk, not in the transcript.** `/rig-task` spans + seven steps and four delegated agents. `/rig-epic run` loops that over every + child. When the transcript compacts mid-run, the acceptance criteria, the + failing assertion, and which review findings are already fixed go with it. So + every long skill keeps one small structured document per run under + `.rig/state/`, and `scripts/rig-state.ts` validates each update outside the + model. It rejects unknown keys, wrong types, and impossible states. A size + budget stops the document drifting back into a transcript. See + [`docs/state.md`](docs/state.md). - **Agent output is written for humans.** Agents produce a lot of prose — PR bodies, ticket descriptions, review findings, plans, hand-backs — and left alone they write it badly: buried conclusions, passive voice, hedge stacks, diff --git a/docs/state.md b/docs/state.md new file mode 100644 index 0000000..4b83016 --- /dev/null +++ b/docs/state.md @@ -0,0 +1,134 @@ +# Run state (`rig-state`) + +A rig skill is a long procedure. `/rig-task` spans seven steps and four +delegated agents; `/rig-epic run` loops that over every child. The host agent's +only memory of what happened between those steps is its transcript. When that +transcript compacts mid-run, three things go with it: the acceptance criteria, +which test went red and why, and which review findings are already fixed. The +agent then re-derives them from `git` and `gh`, or guesses. + +`rig-state` replaces that memory with one small structured document per run, +kept on disk and validated outside the model: + +``` +.rig/state/.json the current state — what the next step reads +.rig/state/.jsonl an append-only journal of accepted patches +``` + +The state is the working memory. The journal is the audit trail, there so a +human can debug a failed run. **Never feed the journal to a model.** That +reintroduces the history the state replaces. + +Both are throwaway coordination state, rebuildable from the tracker and the +repo, and **gitignored — never committed**. `/rig-doctor` checks that. + +## The contract + +Each step of a skill does three things, in this order: + +1. Read the state (`rig-state show `) instead of scrolling back. +2. Do the step's work. +3. Persist what a *later* step needs, as a patch — not a summary of what + happened. + +That third rule is the one that matters. Record facts and decisions the next +step reads: the branch name, the failing assertion, the reason you rejected an +approach. Don't record narration. If a later step never reads it, it doesn't go +in the state. + +## Commands + +`` resolves from the repo root: `.claude/scripts/` first, then +`.rig/scripts/`, first hit wins — the same rule `/rig-worktree` uses. Run these +with `bun` (or your TypeScript runner). + +| Command | Does | +|---|---| +| `rig-state.ts init [--json '{…}']` | Start a run at the skill's first phase. Fails if the run exists. | +| `rig-state.ts patch --json '{…}' [--step