Field Kit
Saving state while the tree was dirty
A record of state written over an unclean working tree describes a tree that does not exist, so unclean has to be a blocker in the record rather than a footnote nobody writes.
What happened
On 2026-09-20, a save routine was about to write a continuation file while uncommitted changes sat in the working directory, which would have produced a record describing a tree nobody could reproduce later.
3 prompts, in order
- Audit a save for dirty-tree blockers A saved continuation file exists and nobody knows whether the tree was clean when it was written.
- Add a dirty-tree gate before saving The audit shows the save writes without reading the working tree first.
- Prove the gate refuses a dirty tree The gate is in place and nothing has yet forced it to produce a blocker.
1Audit a save for dirty-tree blockers
- Run it when
- A saved continuation file exists and nobody knows whether the tree was clean when it was written.
- It asks you for
- The saved file in full. The status captured at save time. The current status with filenames.
- You get back
- A quoted tree statement or a recorded absence. A list of paths claimed done but still uncommitted. A verdict.
- It refuses if
- No status output with filenames arrives. The audit stops and names the missing input.
<stance> The saved file outlives the session. A record written while files sit uncommitted describes a working tree that exists nowhere. Readers trust every line. Detail is not evidence of a clean tree, and a save that reads well can still rest on a snapshot nobody can reproduce. </stance> <inputs> Ask for these. Wait for each answer. Do not guess a missing one. 1. The saved continuation or state file, pasted in full. 2. The working tree status recorded at save time, if the file or the tool captured it. 3. The current working tree status, with filenames. 4. The branch name. Also whether anything was committed after the save. If item 2 is missing, say so and continue. Mark every later conclusion as unverified. </inputs> <method> 1. Scan the saved file for any statement about the working tree. Quote it exactly. If no such statement exists, record that absence as the finding. 2. List every file path the saved file claims as changed, done or ready. 3. Compare that list to the current status. Name each path that is still uncommitted. 4. Decide whether the saved file acknowledges any uncommitted path as a blocker. Answer yes or no. Quote the line behind your answer. 5. If no status was captured at save time, do not infer one from the current status. Commits may have landed since. Say the question stays open. Name the one input that would close it. 6. Rank the gaps. A path described as done while still uncommitted ranks above a path the save never mentions. 7. If the saved file names a blocker but omits filenames, treat that as a partial pass. A blocker without paths cannot be acted on. </method> <deliver> - Tree statement found: the quoted line, or none. - Paths claimed done and still uncommitted. - Verdict: the save acknowledges the dirty tree, or it does not. - Unverified items, each with the input that would settle it. </deliver> <refuse> Do not rewrite the saved file in this pass. Do not call the record sound because it is detailed. Do not treat a passing build or a green test run as proof the tree was clean. If the user supplies no status output at all, stop after step 1. Report that the audit needs filenames. Say which ones. Do not infer committed state from the wording of the save. </refuse>
2Add a dirty-tree gate before saving
- Run it when
- The audit shows the save writes without reading the working tree first.
- It asks you for
- The save code or prompt. The exact write point. The available status command. Whether commits are permitted.
- You get back
- A diff placing the gate ahead of every write path. Three named outcomes. A blocker entry format with an example.
- It refuses if
- The write path is not shown, or no status command is named. The work stops there.
<stance> A pre-save gate has one job. It must refuse. Code that detects uncommitted changes and then logs a warning before saving anyway is a comment, not a gate, and the record it produces is as wrong as one written with no check at all. The rule under repair is plain. A dirty tree at save time is a blocker, recorded in the file or resolved by committing first. </stance> <inputs> Ask for all four. Wait for each. Do not design against an assumed status command. 1. The code or prompt that performs the save. 2. The exact point where it writes to disk. 3. The command or API available for reading working tree status. 4. Whether the agent may commit, or may only report. </inputs> <method> 1. Find the point where the save writes the file. Name it. If two write paths exist, name both. The gate goes in front of each. 2. Read the status before any content is composed. Capture filenames, not a boolean. 3. Branch on three outcomes, not two. Clean. Dirty with permission to commit. Dirty without permission. 4. For clean, save as normal. 5. For dirty with permission, offer to commit first. Wait for an answer. Do not commit without one. 6. For dirty without permission, write the save and include a blocker entry naming every uncommitted path. The save must not describe those paths as done. 7. Make the blocker entry a required field. A reader must reach it before the task list. 8. If the status read fails or returns nothing, treat the tree as dirty. An unreadable tree is not a clean tree. 9. If the save already has a warning-only check, replace it. Do not leave both in place. </method> <deliver> - The gate, as a diff against the save path. - The three outcomes, with the exact text each one writes. - The blocker entry format, with one filled example. - Failure handling for an unreadable status. </deliver> <refuse> Do not log a warning and save anyway. Do not reduce the status to a yes or no flag. The filenames are the payload. Do not commit on the user's behalf without an explicit answer. If the save path is not shown, stop and ask for it. Do not write a gate against a status command you were never given. Name the missing input and stop. </refuse>
3Prove the gate refuses a dirty tree
- Run it when
- The gate is in place and nothing has yet forced it to produce a blocker.
- It asks you for
- The gate code. A throwaway file path. The save command. Whether commits are permitted in this run.
- You get back
- A clean-tree baseline save. A dirty-tree save with the blocker section quoted. Pass or fail per step.
- It refuses if
- The clean run and the dirty run produce identical files. The result is reported as a fail.
<stance> Prove the gate by making it say no. A check that has never produced a blocker entry is untested code sitting on the only path that matters. Trees are clean most of the time, which is how a silent gate goes unnoticed. The test is small. It has to fail on purpose. </stance> <inputs> Ask for each. Wait. Do not run anything against work the user cannot discard. 1. The gate code as it now stands. 2. A throwaway file path that is safe to edit. 3. The command that triggers a save. 4. Whether commits are permitted in this run. </inputs> <method> 1. Run the save with a clean tree. Keep the saved file. This is the baseline. 2. Make one edit to the throwaway path. Leave it uncommitted. 3. Run the save again. Do not clean the tree first. 4. Open the saved file. Search for the throwaway path by name in the blocker section. 5. If the path appears, the gate reads filenames and writes them through. Record a pass. 6. If the file saved with no blocker entry, the gate does not run before the write. Record a fail. Locate the write point again. 7. If the save refuses and offers to commit, confirm it waits for an answer. A refusal that proceeds anyway is a fail. 8. Break the status read deliberately, for example by pointing it at a directory outside any repository. Run the save. Confirm it treats the unreadable result as dirty. 9. Diff the clean save against the dirty save. The only difference should be the blocker entry. 10. Commit or discard the throwaway edit when the run ends. State which you did. </method> <deliver> - Baseline save from a clean tree. - Dirty-tree save, with the blocker section quoted verbatim. - Pass or fail per step, each with its evidence line. - Behaviour when the status read is unreadable. </deliver> <refuse> Do not declare the gate working from reading it. Only a dirty-tree run settles that. Do not use a real work-in-progress file as the dirty file. Do not clean the tree between steps 2 and 3. If no save command is supplied, stop and ask for it. If the dirty run and the clean run produce identical files, report a fail. Stop there and do not suggest a reason without evidence. </refuse>
How you know it worked
Make one uncommitted edit to a throwaway file. Run the save. Open the saved file. The throwaway path appears by name in a blocker entry, or the save stops and offers to commit first. An empty blocker section alongside a dirty tree means the gate never ran before the write.