feat(cli): add hydrate stage discard - #90
Merged
Merged
Conversation
Abandoning a staged changeset meant deleting .hydrate/stage.json by hand. clear is not that verb — it STAGES removal of every top-level node, which is nearly the opposite and a dangerous thing to reach for by mistake. The discarded work exists nowhere else: nothing was committed, so there is no server copy. Two things follow. The full operation list prints before the delete, through the renderer diff already uses, because counts are not a record — a node's description is the user's authored prompt, and '2 nodes' does not tell you what you lost. And the outgoing stage is parked in .hydrate/stage.discarded.json, a single slot, with --json echoing the deltas so an agent can re-stage without reading the file. No confirmation prompt: this CLI is driven non-interactively and a prompt would break piping, so the mitigation is recoverability rather than friction. An empty stage is not an error — it exits 0 and says so, matching status, diff and commit. The verb takes exactly one file. .hydrate/ also holds the binding and a large pulled index, so a wider blast radius would unbind the working copy or throw away a snapshot the user would have to re-fetch; write_state_file takes a file NAME rather than a path so a caller cannot reach outside the state directory. status's noun renderer is now shared rather than copied, so the three verbs that describe one stage cannot drift on what it contains. A unit test proving park() works did NOT prove discard() calls it — deleting the call left every unit test passing. The end-to-end test that runs the real binary catches it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A review found the report was printed before the work. With a read-only .hydrate, stdout carried 'Discarded 1 staged operation' and 'Recoverable from ...' while the stage was untouched and no recovery file existed — a fabricated past-tense success on the documented machine channel. An agent reading it would author on top of a stage it believed empty and commit both. Now: the op listing goes to stderr FIRST, because it is the record of what is about to be destroyed and has to survive a failure part-way through. Then the park and the clear. Only then the verdict. An integration test drives a read-only .hydrate and asserts stdout claims nothing and the stage survives. The JSON also echoed the raw deltas, putting node UUIDs into output an author consumes — the one thing every sibling verb avoids, and something this change set argues for elsewhere. It now reports ops by dotted path through the same projection diff uses; the recovery FILE remains the verbatim source for re-staging, which is its job. write_state_file's doc claimed a caller could not reach outside the state directory. Path::join provides no such thing: the review demonstrated '../escaped.json' landing beside .hydrate and an absolute path replacing the base outright. The check is now implemented and tested rather than asserted. Its doc block had also been inserted between atomic_write's doc and atomic_write, describing a function it was no longer attached to; both are back where they belong. stage discard is now listed in the guide, next to clear, saying which is which. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Abandoning a staged changeset meant deleting
.hydrate/stage.jsonby hand.clearis not that verb — it stages removal of every top-level node, which is nearly the opposite and a dangerous thing to reach for by mistake.Why it prints the operations
The discarded work exists nowhere else — nothing was committed, so there is no server copy. A node's
descriptionis the user's authored prompt for code generation, and2 nodesdoes not tell you what you lost. The listing goes through the rendererdiffalready uses, so what scrolls past is a real record.Why there is no prompt
This CLI is driven non-interactively and a prompt would break piping. So the mitigation is recoverability, not friction: the outgoing stage is parked in
.hydrate/stage.discarded.json(single slot, overwritten each time), and--jsonechoes the deltas so an agent can re-stage without reading the file. The park happens before the delete, and a park failure aborts it.Blast radius
Exactly one file.
.hydrate/also holds the binding and a 100KB+ pulled index, so a wider reach would unbind the working copy or throw away a snapshot the user would have to re-fetch.write_state_filetakes a file name, not a path, so a caller cannot escape the state directory. Pinned by a test that plants a binding, an index, and a sibling file and asserts all three survive.A test that didn't test anything
The unit test proving
park()works did not provediscard()calls it — deleting theparkcall left every unit test passing, because the test invokedparkdirectly. The end-to-end test that runs the real binary in a real working copy catches it; verified by re-applying the mutation.status's noun renderer is now shared rather than copied, so the three verbs describing one stage can't drift on what it contains.415 unit + 9 integration tests.
fmt,clippy -D warningsclean. Driven against a real bound project, both the populated and empty paths.