Releases: hugoforte/rig
Release list
rig 3.27.0
A work's record can be corrected after the fact (#235)
A work's record can be corrected after the fact
Tickets: #145, #198, #178, #146
Direction
Every fact rig writes into a work's record gets a way to be corrected in place, with the views of it (the context doc's header, the generated AGENTS.md and the PR body) rewritten from the record rather than hand-edited. Nothing new is stored except what a correction needs; nothing is deleted that a reader would want a year later.
- The title (#145):
rig save --title "...". The title is prose, like the context doc, so it rides on the command that already commits hand-made prose. It rewriteswork.json, the context doc's# <id> — <title>heading and the generated AGENTS.md. Never the branch (renaming it breaks the stack) and never the id (it names a folder and a record across every root). The heading is rewritten only by this flag, never by every save, so a heading edited by hand is not silently reverted. - Tickets (#198):
rig ticket <new> --replaces <old>andrig ticket --remove <key>. Both rewrite the record and theTickets:line, and neither tells the tracker anything. A key is corrected wherever the record holds it: the work's tickets and any stage's own--key. A transferred issue has a new number whichever list named it, so one command covers both, and--replaceskeeps the new key in the place the old one held. A key the record does not hold is refused.- The shared-ticket close (#154) is not what remove is for. Remove says the record was wrong about a ticket. A work that did part of a ticket was right to name it; what it wants is a ticket the work refers to without delivering, which
rig closecomments on and never closes. Removing works as a stopgap, at the cost of the close comment and the header naming the ticket. The real answer is its own change, left for Hugo to ticket.
- The shared-ticket close (#154) is not what remove is for. Remove says the record was wrong about a ticket. A work that did part of a ticket was right to name it; what it wants is a ticket the work refers to without delivering, which
- The PR body (#178):
rig pr --refresh. Rewrites each repo's open work-branch PR from the current record, title and body, through the same rendererrig propens it with, so the two cannot drift. Idempotent: an unchanged PR is reported as up to date and not edited. With no open PR it says so and opens nothing.rig nextoffers it when an open PR's title or body differs from whatrig prwould render now, the way it offersrig plan --refresh. A newghadapter method,editPr, with its in-memory fake;prForBranchcarriestitleandbodyso the comparison costs no extra call. - Stages (#146):
rig stage <branch> --dropped "reason"andrig stage <branch> --replaced-by <branch>. Recorded on the stage asdroppedAt+reason, orreplacedAt+replacedBy, never deleted, likeabandonedAtat the work level.--replaced-bynames another declared stage of this work.rig stage,rig nextand the stage table (PR body and rollout plan, one renderer) show it as dropped or replaced, never as "not started", andrig nextnever offers it as the next stage. A stage that has landed cannot be dropped. - Stages 3 and 4 share one renderer. The stage table is
stageTable, used byrig pr,rig pr --refresh,rig next's staleness check and the rollout plan, so dropping a stage makesrig nextoffer the refresh.
Not in this work: renaming the branch, renaming the id, and a "referred to, not delivered" ticket kind.
Planned stages
feat/save-title-corrects-the-work-title— #145,rig save --title.feat/ticket-replaces-and-remove— #198,rig ticket --replaces/--remove.feat/pr-refresh-rewrites-the-open-pr— #178,rig pr --refresh, theeditPradapter, therig nextoffer.feat/a-stage-can-be-dropped-or-replaced— #146,rig stage --dropped/--replaced-by.
DESIGN decisions 123–126, one per stage, in that order.
Stages
| Order | Stage | Delivers | Repos | PR | State |
|---|---|---|---|---|---|
| 1 | feat/save-title-corrects-the-work-title |
rig save --title corrects the work's title | rig | #230 | landed |
| 2 | feat/ticket-replaces-and-remove |
rig ticket --replaces and --remove correct the record's tickets | rig | #231 | landed |
| 3 | feat/pr-refresh-rewrites-the-open-pr |
rig pr --refresh rewrites each open PR from the record, and rig next offers it | rig | #232 | landed |
| 4 | feat/a-stage-can-be-dropped-or-replaced |
a declared stage can be dropped or marked replaced, and says so | rig | #233 | landed |
Context doc: https://github.com/hugoforte/rig-data/blob/main/work/record-fidelity/context.md
Full changelog: v3.26.1...v3.27.0
rig 3.26.1
A work always lands in the data root that holds it (#227)
A work always lands in the data root that holds it
Tickets: #104, #170, #107, #154
Direction
Four fixes to one question, which data root a command is in, each its own stage stacked on the work branch.
- #104,
rig updatewith nothing selected.updategathers its location the waydoctordoes: catch the refusal and fall back to the tool checkout, so the tool, every configured root and the freshness cache still move.updatedoes not say the refusal itself. It ends in the doctor checks, which already say it word for word and count it, so saying it inupdatetoo would say it twice. - #170, placement by org.
repoAtCwdnames a repoorg/repofrom its remote, androotsCataloguingmatchescatalog/<org>/<repo>.mdwhen the name carries an org; a bare name (a folder with no origin,--repos) matches any org as today. Standing in a configured data root's own checkout places the command in that root, asked after--reposand before the repo at the cwd. The Jira-key placement is left out unless it stays small. - #154 item 6 only (part of #154, never closes it).
describecarries the upstream the branch's config names even when its ref is not here.prepareDataRootandupdateCheckoutfetch when one is configured but missing before concluding "no upstream", and doctor stops telling such a root to push to a private repo. - #107, the work folder's marker.
regeneratewrites.rig/dataonly when the root in hand is the one root that holds the work's record. With more than one root configured,doctorwarns about a work folder with no marker, and about one whose marker names a root that does not hold its record. Built last: it editsbin/doctor.mjs, which open PR #196 also changes.
Why not more: the Jira-key placement (#170's third point) and #154 items 2 to 5 belong to other works or later.
Planned stages
| Stage branch | Delivers | Ticket |
|---|---|---|
fix/update-without-a-current-root |
rig update brings everything forward when no root is selected |
#104 |
fix/place-a-work-by-org-and-root-checkout |
placement matches the org, and a data root's own checkout places itself | #170 |
fix/fetch-a-missing-upstream |
a data root whose upstream ref is missing fetches before saying "no upstream" (#154 item 6) | — |
fix/the-marker-names-the-root-that-holds-the-record |
the marker is written only where the record is, and doctor reports folders without one | #107 |
Stages
| Order | Stage | Delivers | Repos | PR | State |
|---|---|---|---|---|---|
| 1 | fix/update-without-a-current-root |
rig update brings the tool and every root forward when no data root is selected | rig | #203 | landed |
| 2 | fix/place-a-work-by-org-and-root-checkout |
placement matches the repo's org, and a data root's own checkout places the command in that root | rig | #206 | landed |
| 3 | fix/fetch-a-missing-upstream |
a data root whose configured upstream ref is missing locally fetches before saying no upstream (part of #154, item 6) | rig | #210 | landed |
| 4 | fix/the-marker-names-the-root-that-holds-the-record |
the work folder's marker is written only where the record is, and doctor reports a folder without one | rig | #213 | landed |
Context doc: https://github.com/hugoforte/rig-data/blob/main/work/root-placement/context.md
Full changelog: v3.26.0...v3.26.1
rig 3.26.0
rig tells the truth when a staged work lands (#226)
rig tells the truth when a staged work lands
Tickets: #192, #200, #181, #193
Direction
Four stages, stacked on the work branch, one ticket each. Each one makes a rig reader say what is true at the moment a staged work lands.
- #192, unpushed means not on the remote (
fix/unpushed-means-not-on-the-remote).aheadis measured against@{u}, and a work branch's upstream is its base, so it counts commits not on main.worktrees.state()gainsunpushed: the commits on HEAD that no remote branch holds (rev-list HEAD --not --remotes=origin). For a pushed branch that is the distance fromorigin/<branch>; for one never pushed it is everything over its base.rig next's push and PR offers andworkState's "unpushed" blocker read it.ahead/behindstay the distance from the base, which is whatstatusandlistprint.bin/checkouts.mjsmeasures the data root and the tool, whose upstream is their own branch, so it is left alone. - #200, a worktree left on a landed stage (
fix/a-worktree-left-on-a-landed-stage).state()names the branch the worktree is on. Once every stage is in,rig nextnames each repo still on a stage branch and gives the command that moves it to the work branch;rig prsays the same beside the PR it opens. rig only names the command: it never moves a branch behind you. - #181, a close fetches the merged PR's head (
fix/close-fetches-the-merged-pr-head).dropMergedtakes the PR number, and when the mirror lacks the PR's head commit it fetchesrefs/pull/<n>/headfirst. A failed fetch keeps the copy with the fetch error as the reason. - #193, a squash-merged stage (
fix/say-when-a-stage-was-squashed). A stage whose PR merged but whose tip is not an ancestor of the work branch was squashed. When the squash commit's tree equals the stage tip's tree,rig nextoffersgit rebase --onto origin/<work> <stage tip> <next stage>with real SHAs. Otherwise it says the stage was squashed and offers no command.
Not in scope: changing what upstream rig attach gives a work branch. That would change what git pull and git status say in every worktree, and #192 is fixed without it.
Stages
| Order | Stage | Delivers | Repos | PR | State |
|---|---|---|---|---|---|
| 1 | fix/unpushed-means-not-on-the-remote |
rig next counts unpushed commits against the remote, not the base | rig | #205 | landed |
| 2 | fix/a-worktree-left-on-a-landed-stage |
rig next and rig pr name the switch back to the work branch once every stage is in | rig | #211 | landed |
| 3 | fix/close-fetches-the-merged-pr-head |
rig close fetches a merged PR's head before deciding a branch has commits the PR did not | rig | #212 | landed |
| 4 | fix/say-when-a-stage-was-squashed |
rig next names a squash-merged stage and the rebase that replays the next stage onto the work branch | rig | #215 | landed |
Context doc: https://github.com/hugoforte/rig-data/blob/main/work/stack-landing/context.md
Full changelog: v3.25.0...v3.26.0
rig 3.25.0
Add "Say it once" to what rig believes (#195)
Adds principle 10 to docs/philosophy.md:
A copy is a promise to keep two things in step, and nothing keeps it. [...] Deleting a copy beats checking it, and checking beats trusting it.
Three steps in a row came back to it:
- The adversarial review of #190 found the org doc described seven ways across the tool.
- #191 added a test holding the six lists of the data root's contents together.
- #194 deleted the list of lesson homes in
rig next's offer instead of checking it.
A lesson from the rig work for #185, carried here through the philosophy home that #194 added. Docs only, so the docs/ branch asks for no release.
Small fixes: help never acts, impact, catalog, demo, new --type (#225)
Small fixes: help never acts, impact, catalog, demo, new --type
Direction
Four stacked stages on the rig repo, each small and reviewed on its own, in this order:
fix/help-never-acts(#199):--help/-hon any command prints that command's usage and exits 0 without acting. The per-command usage is cut out of the onerig helptext, so there is one source. A flag a command's usage does not name is refused with that usage, exit non-zero, before anything runs — if that check proves wide, ship--helpalone and report the rest.fix/impact-catalog-demo(#147):rig impactsays "keeps happening" only for a count above one;rig catalog --verbosereads a bare-stringtalks_toitem asbuildGraphdoes;rig demo(andlist/dashif they share it) reads records throughreadRecords/sayUnreadable, so one unreadablework.jsonis named, not a stack trace.fix/new-type-prefixes(#134):rig new --typeis checked against the prefixesbin/release.mjsexports, and the refusal names them.perfjoinsPREFIX_BUMPSas a patch, by Hugo's decision: a performance change is visible to users, so it releases.fix/test-leak-and-engines(part of #154, items 4 and 5): a failedbillingInstallremoves its temp dir;enginesrequires the Node the suite needs.
Why not more: #154's other items belong to other works, and moving the test helpers out of test/ is deliberately left for later.
Stages
| Order | Stage | Delivers | Repos | PR | State |
|---|---|---|---|---|---|
| 1 | fix/help-never-acts |
help on any command prints its usage and never acts; an unknown flag is refused | rig | #204 | landed |
| 2 | fix/impact-catalog-demo |
impact counts what it saw, catalog reads a bare talks_to item, demo names an unreadable record | rig | #207 | landed |
| 3 | fix/new-type-prefixes |
rig new refuses a --type the release check would refuse, and names the list | rig | #209 | landed |
| 4 | fix/test-leak-and-engines |
a failed billingInstall leaves no temp dir; engines asks for the Node the suite needs | rig | #214 | landed |
Context doc: https://github.com/hugoforte/rig-data/blob/main/work/small-fixes/context.md
Full changelog: v3.24.0...v3.25.0
rig 3.24.0
Check that every list of a data root's contents names all of it (#191)
rig lists what a data root holds in six places: AGENTS.md's three roots, DESIGN.md §3 and §9, the README diagram, the README rig init writes into a new data root, and the rig skill. When #190 added orgs/, four of them were missed, and only an adversarial review caught it.
test/data-root-lists.test.mjs now checks each one. Every list is found by an anchor in the text around it and must name the catalogue, the org docs, the work records and rig.json, as paths or as prose depending on the list. A list the test can't find fails, rather than passing on nothing, so rewording one means pointing the test at it again in the same PR.
Checked against the README as it was before #190: the diagram test fails.
Test only, so the test/ branch asks for no release.
A lesson from the rig work that shipped #184.
rig-learn grows the org doc, and rig's own philosophy, from what a work taught (#194)
rig-learn grows the org doc, and rig's own philosophy, from what a work taught
Tickets: #185
Direction
Agreed with Hugo, 2026-09-28.
- The org doc is the one place rig-learn writes prose that every session reads, and the exception is stated. rig-learn's rule is "never add a rule to an
AGENTS.md", and the org doc is inlined into every generated one. It is allowed because it is the org speaking about itself rather than rig inventing a rule, and because every review corrects it: a line a work showed wrong is retired then and there. The rule it borrows from checks still holds: when a proposed org doc edit could be a check, the check is offered first, in the repo or the catalogue, and only a belief nothing can check goes in the doc. rig-learn and DESIGN.md both say so. - In the review, proposed edits plus one optional line. The agent reads the story against each org doc the work touches and proposes concrete edits as ordinary lessons, under an Org doc section: retire a "What hurts now" line the work resolved, reword a belief the work had to bend, add what it taught. They can be in the TL;DR and ride on "go". Then one optional line, below "Say go…", for what only the user knows: "Did this work confirm, contradict or add to anything in the org doc? A bare go skips it." It is asked only where a doc exists; #184's one-sentence question still covers an org without one, and the two are never both asked for the same org. Corrected before appended to, like the catalogue.
docs/philosophy.mdis for beliefs about rig the tool, never about the repos a work used. A lesson about rig that changes a belief, rather than reporting a bug, goes there instead of the tracker. When rig is attached, the edit goes in the work's rig PR like any repo lesson. When it is not, it becomes an issue on rig titled "Philosophy: …" carrying the proposed wording, and the page is edited from it later. Either way the tracker's rule applies: rig alone, no private names. A dropped principle is struck through with the friction that killed it.- The public/private split in an org doc is just prose. rig-learn reads the doc whole and the agent applies it; rig does not model repo visibility. Product areas (#188) are where that would go.
- No code change is expected.
rig statusalready names each org's doc. What changes is the skill, the philosophy page's "How this page grows", AGENTS.md's lesson-review paragraph (rule 4 lists the homes) and DESIGN.md.
Not done here: anything rig next offers, product areas, and machine-checking a belief (that is step 6 of #187).
Context doc: https://github.com/hugoforte/rig-data/blob/main/work/org-doc-grows/context.md
Full changelog: v3.23.0...v3.24.0
rig 3.23.0
Add a timeline of how rig grew (#182)
Adds docs/timeline.md: rig's first ten days in six phases, each named by the capacity it added, with command, module and test counts at every major tag. Linked from the README's "Going deeper" list.
Docs-only branch, so no version bump.
Add a philosophy page: what rig's frictions taught (#183)
Adds docs/philosophy.md, linked from the README beside the timeline in place of the separate link to DESIGN.md's principles.
The timeline says how rig grew. This page says what those frictions taught, so someone deciding whether to use rig can read it in five minutes, and the next decision can be checked against it.
- One idea. What runs out first is the human's attention, and most of it goes either side of the implementation: deciding what to do next before it, and checking it got done after. rig spends less of it, and none on itself.
- Nine principles, each citing the friction or decision behind it: friction is the roadmap; trivial to start, deep to master; guide where you are; attention is the budget; facts in code, judgement in prompts; prove it or mark it, and say when; everything accretes; keep it refactorable; strong convictions, loosely held. The last two are DESIGN.md's principles, given one line each with a link to the full text there.
- What rig asks of an org. The same questions the page answers about rig: what you're trying to accomplish, what hurts now, what you believe, whose call it is.
- How it grows. By hand when a friction changes a belief, and from rig-learn at close. A dropped principle is struck through with the friction that killed it.
Docs only. No behaviour changes.
Unlink the symbolic HEAD in the gitfs test rather than rmSync it (#189)
On Node 25 on Windows, fs.rmSync returns without error on a dangling symbolic link and leaves it there. The test "a HEAD that is a symbolic link names the ref it links to" removes its first link that way, so the second symlinkSync fails with EEXIST. It fails on main on Windows with Node 25. CI runs Node 20 and 22, whose rmSync removes the link, so CI stayed green.
unlinkSync removes the link itself. Full suite: 1039 pass, 0 fail, 1 skipped, on Windows with Node 25.1.
Test only. No behaviour changes.
An org can say what it is trying to do, and every work reads it (#190)
An org can say what it is trying to do, and every work reads it
Tickets: #184
Direction
Agreed with Hugo, 2026-09-27.
- It lives at
<data root>/orgs/<org>.md. It is a new top-level folder besidecatalog/andwork/, because the doc is about the org and not about any repo. The org's name is its key inrig.json'sorgs, the same namecatalog/<org>/already uses. Product areas can later go inorgs/<org>/without touching the catalogue. The frontmatter holds onlyorg:. Anything else would be state that git already answers, such as when the doc was last changed. - A work reads the orgs of the repos attached to it. That is each distinct org among the work's repos, in the order they were attached. A work with no repos reads nothing. The next
rig attachregenerates the file, so there is no gap to cover by also reading the ticket's org. - The generated
AGENTS.mdinlines each doc in full. Each org gets a## <org>section, with the doc's own headings nested one level below it and the file's path named as the only copy. The doc is meant to be short, and the point is that every session starts knowing it. A link is something agents skip. An org with no doc adds nothing to the file: no heading and no "none yet" line. A doc with nothing under its frontmatter counts as none. The doc's headings move so its shallowest lands under the org's, and a fence it leaves open is closed. A changed doc reaches a work on that work's next mutating command, like the catalogue lines beside it. rig statusnames each org's doc, or says there is none and where it would go. rig-learn reads this line to decide whether to ask the question. It also works once the work is closed and its folder is gone.- #184 handles an org with no doc; #185 handles an org that has one. In #184, rig-learn asks one optional question, "what is this org trying to accomplish?", and only when the org has no doc. It asks in one line right after "Say go", naming every org with no doc, so there is no extra round-trip; a bare go skips it. An answer is written as a doc with only "What we're trying to accomplish" filled in. The other three headings are left out rather than stubbed, following the same rule as the context doc, and are written by hand until #185. The four headings are named exactly once, in AGENTS.md. A skip stores nothing, so the next review in that org asks again. Confirming, contradicting or adding to an existing doc, and the philosophy page as a home, belong to #185.
- The new-work prompt names the stated problem when a doc exists, once the repos are attached, because only then are the work's orgs known. It reads "What hurts now" and "What we're trying to accomplish" and says which one the work addresses, or says out loud that it addresses none. It never refuses a work on these grounds. Nothing changes when there is no doc.
- rig-data is not attached. rig-learn writes the doc into the data root the same way it writes catalogue corrections, and
rig save --learnedcommits it. dotfiles is untouched. - In the same PR: the "What rig asks of an org" section of
docs/philosophy.mdmoves to the present tense, CONTEXT.md defines org doc, DESIGN.md §3 showsorgs/<org>.md, §7.4 shows the new part of the generated file, and a new decision records all of this.
Not done here: product areas, anything rig next, rig init or rig attach would offer, and any check in rig doctor.
Context doc: https://github.com/hugoforte/rig-data/blob/main/work/org-doc/context.md
Full changelog: v3.22.0...v3.23.0
rig 3.22.0
DESIGN.md can cite a scenario's steps as its enforcement (#180)
DESIGN.md can cite a scenario's steps as its enforcement
Direction
test/design.test.mjs resolves a journey's steps as <scenario> › <step> besides plain test(...) titles, so DESIGN.md's "Enforced by" column can name the end-to-end test that actually proves a decision. Step titles are unique only within their journey, hence the prefix. Decision 106 cites the restore journey. Came out of the rig-restore lesson review, 2026-09-25; Hugo said go.
Context doc: https://github.com/hugoforte/rig-data/blob/main/work/design-cites-scenario-steps/context.md
Full changelog: v3.21.0...v3.22.0
rig 3.21.0
rig-learn leads with a TL;DR the user can answer with go (#177)
rig-learn presents lessons under four headings
Direction
The rig-learn skill's step 3 opens with a TL;DR of two or three recommended actions the user can approve with one word ("go", or the numbers), then one line per lesson under Repos we touched, Catalogue and Rig in general, each saying what changes, where, and why that home in a clause; then Dropped. Revised 2026-09-25 after a trial run on rig-restore read as far too verbose: Hugo's first cut had four headings including a separate "why" section, which is now folded into each line. The three homes and machine-check-first are unchanged, so AGENTS.md and DESIGN.md decision 105 still describe it.
Context doc: https://github.com/hugoforte/rig-data/blob/main/work/rig-learn-sections/context.md
🤖 Generated with Claude Code
Full changelog: v3.20.0...v3.21.0
rig 3.20.0
Cut redundant tests and fix two that tested nothing (#174)
An audit of the suite: which tests repeat another's case, and which don't test what they claim.
Two broken tests, fixed
pr.test.mjs— "the Direction … is lifted verbatim" wrote into a context doc and read the same file back.rig prnever ran on a written Direction. It now opens a PR on a second work and asserts the body. Checked by breakingprBody: the test fails.installation-update.test.mjs— the "no git on PATH" tests built PATH from'C:\Windows\System32'in a plain JS string, which reads asC:WindowsSystem32. The environment is now one helper,onPath, with the path escaped and Windows-only. The duplicate doctor run is folded into the node-only doctor test next to it.
Cut: ~20 tests that repeat another test's input and assertions
| Removed | Still covered by |
|---|---|
package — real npm install -g (51s locally) |
install — install.sh / install.ps1 do the same link + rig help |
release — verdict / notes CLI spawns (3) |
release-e2e runs the same commands |
release-e2e — "naming no bump" |
release unit test of the same case |
stage-cut — re-declare refused |
stages-e2e "and a stage is not declared twice" |
next — measured zero; draft entry alone |
same input, superset assertions, in the same file |
dash — window bounds merged |
--since test in the same file |
jira — createIssue markdown |
the two full-argv createIssue tests (rationale comment moved) |
doctor — unclosed work "whichever root" |
"a work folder that is missing is said once"; the multi-root case is in dataroots |
version — two dataMajor({}) tests; "stamp is applyMigrations" |
merged into one; migration 2's test stamps 1.4.0 → 2.0.0 |
attach — mirror-ref test |
folded into the doctor-behind test as a precondition |
smoke — dash with no payload |
the acceptance test runs the same dash (filename assertion moved) |
helpers — statusLine |
phase (plus one assertion for the no-repo-facts call path) |
installation-freshness — non-number interval (20s poll) |
roots unit test of the same normalisation |
Also:
skills.test.mjsno longer pins the exact list of skill folders, which broke on every new skill. It requires therigentry point, and checks that every skill rig names inbin/and in the entry point is a folder it ships.- Removes unused imports and fixture exports, and the
statusLinere-export frombin/rig.mjs.
No removed title is cited in DESIGN.md's decision log.
Result
18 fewer tests than main, and no change in run time. Measured back to back on one machine: main 53.2s / 56.0s, this branch 53.7s / 61.5s. CI: 58s on the last main push, 52s here. The difference between two runs of the same code is larger than the difference between the two.
node --test runs files in parallel, so wall time is the slowest file, not the sum. The 51s npm install -g test ran beside install.test.mjs and the scenarios, which take about as long. Cutting it saves CPU time without ending the run sooner. An earlier "144s → 114s" in this description was measured on a loaded machine and was wrong.
The case for this PR is correctness and upkeep, not speed.
Two tests that claimed more than they asserted
demo.test.mjs— "a merged PR is reported with the stretches it spent" only checked that the section heading rendered. It now asserts each stretch, including the two with no review to measure from. Checked by swapping one stretch's endpoints inbin/demo.mjs: the test fails.stages-e2e.test.mjs— stacked-stage test claimed "the chain orders them, not the array", but its stages are declared in chain order, so it could not tell. The claim is dropped; the out-of-order test after it is the one that proves it.
Looked at and kept
scenarios.test.mjs: the scenarios look like repeats ofdataroots.test.mjs, but they aren't.datarootssaves and restores the machine file between steps or fabricates the legacy state. The scenarios reach those states the way a machine does: an unnamed firstinit, then a rename, then a split. That sequence is the reason the file exists.stages-e2eagainststage-cut: one file cuts its branches by hand with git, the other has rig cut them with--cut. They cover two routes to the same discovery, and later tests build on each.
Not in this PR
- Fixture speed:
stacked()inworktrees-fixture.mjsbuilds a fresh remote per test; copying one prepared tree, as #164 did forcheckouts-*, would speed those files up.
Restore a work's worktrees on a second machine in one command (#176)
Restore a work's worktrees on a second machine in one command
Direction
One command, rig restore [<id>], rebuilds a work's folder from its record and changes nothing
in the record. It is not an attach (issue comment 2, point 3): attach decides a base, stamps
attachedAt and pushes a repo entry; a restore has all of that already and writes none of it.
work.json is byte-identical afterwards and there is no data-root commit. It is in MUTATING
only for prepareDataRoot's fast-forward, so a second machine restores from the newest records.
Per recorded repo whose worktree is missing (present ones are left alone, so it is idempotent):
- Pick the top of the repo's stack. Fetch the mirror, read
chain()over the declared stages,
order withstageOrder, and take the highest branch this repo carries on the remote or in the
mirror; failing any stage, the work branch. This is what puts infra-tim on its stage branch
rather than an unpushed, empty work branch. - Never recreate a branch. A new
trees().checkOutchecks out a branch the remote has, or the
copy the mirror kept, exactly ascut()does — and answers "absent" wherecut()would cut a
new branch from the remote HEAD. An absent repo is skipped and named with its PR state
(prForBranch): "no branch on the remote — PR #n CLOSED" or "never pushed, and no PR". - Name the branches built on top that rig does not know. One
gh pr list --base <top>per
restored repo, followed up the chain: the open PRs stacked on it that are not declared stages
are named, in order, withrig stage <branch>as the way to record them. Reported, not checked
out — which branch of an unrecorded stack is "the" work is the user's call. - Repeat attach's per-repo preparation, extracted out of
attachReposo both share it:
user.emailfromidentityFor,copySecrets, and the catalogue'ssetupprinted (--setup
runs it). Catalogue drafting stays attach-only — a restored repo is already catalogued. - Regenerate the work folder —
AGENTS.md,CLAUDE.md,.rig/id,.rig/data— with
regenerate, which never touches the record.
Around it:
-
rig nextoffersrig restore <id>first when a missing repo could still come back: its
PR is neitherMERGEDnorCLOSED. A repo whose branch went with a closed PR is not offered
forever; restore has already said why it cannot come back. -
rig doctornames the remedy on "work folder missing" and "worktree is gone". -
loadWork, asked for an id the root in hand does not hold, names the root that does
(--data <name>), sorig restore <id>on a machine with several roots says where to look. -
rig-handoffplaces the handoff withrig status --work <id>when the folder is missing,
and its continue prompt putsrig restore <id>before thecd. -
rig attach <repo>on a recorded repo whose worktree is gone restores that one repo through
the same routine instead of saying "nothing to do" (#99).attachedAtis left alone: it records
when the repo joined the work.
Not doing: recording anything, or picking between the branches of a forked stack.
Agreed with Hugo 2026-09-25: name rig restore; unrecorded stack reported by default, --tip to
check it out; #99 folded in.
Context doc: https://github.com/hugoforte/rig-data/blob/main/work/rig-restore/context.md
Handoff by URL (#171)
rig status prints a handoff line when the work has a handoff.md. It gives the file's URL on the data root's remote, or this machine's path, marked as such, when there is no remote. The rig-handoff continue prompt is built from that line and rig restore <id>, with no cd. Nothing in it belongs to the machine that wrote it.
Fixes #112
Fixes #99
Fixes #171
Full changelog: v3.19.0...v3.20.0
rig 3.19.0
A lesson loop at close: rig-learn offers the catalogue's check field and its prose (#173)
A lesson loop at close: rig-learn offers the catalogue's check field and its prose
Tickets: #172
Direction
A lesson review is a gate recorded on the work, like the ticket decision, not a prompt inside rig close. AGENTS.md already explains why: no rig command waits for a human, and close deletes the worktrees a repo lesson would have to be committed in.
rig-learnskill, shipped inskills/besiderigandrig-handoff. It reads the whole story (context.md,handoff.md, PR review threads, CI failures and re-runs, the commit log) and offers each lesson a home:- the catalogue: a
checkcommand when a machine can verify it, else prose; - an attached repo: a test or CI step first, else its docs, landing in the work's last PR, or a small follow-up PR;
- rig itself: an issue in rig's tracker when the lesson is about the tool.
- the catalogue: a
- Order of preference is
/harness-learn's: a machine check over prose, and never a new rule. - Prefer correcting a wrong sentence to appending a new one; check the entry first.
- Route each lesson to the data root its reader can see; preferences about how the user works go to agent memory, not the catalogue.
- Scale by reach: a one-repo, one-PR work gets one question; a staged, multi-repo work gets the full walk.
- The gate:
rig save --learnedrecords it, including a review that found nothing worth keeping. It may be passed after the close and is refused on an abandoned work.rig nextoffers the skill once a PR is open, above the close offer;rig closenames an unreviewed work and never refuses.
Not chosen: a prompt inside rig close (it tears the worktrees down and exits).
Context doc: https://github.com/hugoforte/rig-data/blob/main/work/rig-learn/context.md
Fixes #172
Full changelog: v3.18.0...v3.19.0