Skip to content

feat(state): keep long-run skill state on disk so compaction stops losing it - #76

Merged
pgebheim merged 1 commit into
mainfrom
feat/skill-state
Sep 11, 2026
Merged

pgebheim merged 1 commit into
mainfrom
feat/skill-state

Conversation

@pgebheim

@pgebheim pgebheim commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Summary

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. The agent then re-derives them from git and gh, or guesses.

rig-epic already worked around this with a hand-rolled .rig/epics/*.json — its own heading called it "replaces any external memory" — unvalidated, unschema'd, and used by exactly one skill. This generalizes that idea into a primitive, following the SKILL.state pattern (arXiv 2608.26263).

What's here

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. Four rules, all enforced outside the model:

  • Patches, not replacement. Deep merge, null deletes, arrays replace wholesale. runId/skill/createdAt are set at init and unpatchable.
  • Unknown keys are rejected, nested included, with the known keys listed back. This is what stops the state drifting into a memory blob.
  • Size budgets — 500 chars/string, 50 items/array, 8 KB/document. A rejection says summarize, which is what keeps the state a state rather than a transcript by another name.
  • Guards encode 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.

A rejected patch leaves the state untouched, prints the reason on stderr and the unchanged state on stdout — rollback-and-retry with no extra read.

rig-task, rig-epic, and rig-review read and patch it. rig-epic's epic file moves to .rig/state/<integration-branch>.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 exactly as before.

What this does not do

It doesn't 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.

Test plan

  • bun test scripts/ tooling/ — 161 pass, 46 new. Covers merge semantics, every rejection class, all eight guards, and the budgets.
  • tsc --noEmit clean on the new files.
  • End-to-end epic lifecycle in a scratch dir: the finish-with-unmerged-child rejection fires; a full two-child epic's resumable state is 940 bytes.
  • install.sh --target claude-code delivers rig-state.ts with no test files leaked.
  • New prose passes scripts/check-style.ts against templates/STYLE.md.

Not yet measured: whether this actually improves a real run. The honest test is a two-arm A/B — same ticket, main vs this branch, claude -p --autocompact 100k to force real compaction mid-run — scored on whether the resumed session re-runs work or re-asks a question already answered. Worth gating on the journal first: if the model skips the patch instructions, the mechanism is dead weight.

Architecture

New script (scripts/rig-state.ts) and one new state directory (.rig/state/). No new dependencies. It replaces rig-epic's ad-hoc .rig/epics/ rather than adding a second mechanism beside it. Skill prose follows the <SCRIPTS> resolution convention #71 established.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MBaTgFSa8va6PTaFfuSRsz

…sing it

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/<integration-branch>.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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MBaTgFSa8va6PTaFfuSRsz
@pgebheim
pgebheim merged commit 17af953 into main Sep 11, 2026
2 checks passed
@pgebheim
pgebheim deleted the feat/skill-state branch September 11, 2026 17:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant