Repository navigation
Ask whether a rebase hand-over would be taken before replaying, so a refused one leaves no conflict standing - #247
Merged
Merged
Conversation
…lows A rebase handed over to a ticket the machine refuses has no successor, so the replay puts the tree back and reports the conflict instead of leaving it standing (FS-005-dispatch.12); the refusal names what to change (FS-005-dispatch.6). The tests pinning it are red until #245 lands.
`ephor rebase --dispatch` replayed under the leaving disposition and only then learned that the hand-over was refused, so a conflict stood in the tree with no ticket and nobody coming. It now asks the hand-over as a dry run first: taken, the conflict is left for the ticket; refused, the replay puts the tree back, still reports the conflict, and the refusal is the exit. A recipe's own opening move is likewise made only after the machine has answered. The undeclared-state refusal, shared now by dispatch and the sweep, names what to change: the recipe's state or the root's states.yaml. Closes #245
…root asks first The ask `ephor rebase --dispatch` makes before its replay is a dry run, and a dry run vetted only a work root already on disk. Into a fresh root a recipe whose state the shipped machine lacks still replayed with the conflict left standing and only then refused (§FS-005-dispatch.12). The dry run now vets the machine the real dispatch would install, and the refusal for a proposed root names the work configuration's states rather than a states.yaml that is not there (§FS-005-dispatch.6).
vjovanov
force-pushed
the
fix/issue-245
branch
from
October 22, 2026 15:36
64da471 to
970f97d
Compare
vjovanov
marked this pull request as ready for review
October 22, 2026 15:36
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.
Closes #245
ephor rebase --dispatch(and therebaseaction that runs it) now asks whether the hand-over would be taken before it replays the branch. When the ticket would be refused, the replay puts the tree back on the commit it started from, the conflict is still reported, and the refusal is the exit and says what to change. Before this change, the replay left the conflict standing first, and only then found that no ticket could be written. That left a mid-rebase tree that nobody was coming to resolve.Before, with a configured
rebaserecipe whosestate: rebaseis not declared by the machine already in the checkout'spanta/states.yaml(from the issue's job log, paths shortened):The checkout was left mid-rebase and no ticket was written.
After, on 64da471, the same setup:
REPORTwhen one was asked for, and the report says the tree was restored;states.yamlis untouched;When the hand-over would be taken, nothing changes: the conflict is left standing for the ticket, exactly as before.
What was actually wrong
The issue's title misread the cause. The machine was not stale. The shipped
rebaserecipe starts infix, which every machine involved declares, and a fresh root accepts it. Thestate 'rebase'in the report came from a recipe configured on that site. The real defect was ordering.ephor rebase --dispatchreplayed the branch with the conflict left standing (Stopped::Leave), and only afterwards did the hand-over vet the recipe's state against the root's machine and refuse. So the conflict was left for a ticket that was never written. §FS-005-dispatch.12 already said that a replay nobody is waiting on puts the tree back on the commit it started from.The issue offered three remedies. Two of them, reconciling the root's machine and isolating the work in another root, were rejected: both cross §FS-005-dispatch.6, under which an existing root keeps its machine unchanged. The third, refusing with actionable instructions, moves no contract. It was taken, together with vetting the hand-over before the replay so that the replay's disposition is chosen correctly.
What changed
The hand-over is asked before the replay.
src/rebase.rsfirst runs a dry-run dispatch of the samerebaserecipe on a dispatcher of its own, which is then discarded so its notes never reach the real hand-over. The answer chooses the disposition of the one replay:Leavewhen the ticket would be taken,Restorewhen it would be refused. On refusal, the conflict is still printed andREPORTis still written, and the refusal becomes the command's exit. A refusal asked about before a replay that turns out clean is dropped, because a clean replay needs no hand-over (§FS-005-dispatch.12).A recipe's opening move waits for the machine. In
Dispatcher::dispatch, the machine vet (the open root's machine, or the proposed shipped one) now runs beforeopening(). A recipe withopens_with: rebasewhose state the machine refuses therefore makes no opening move, and the tree is untouched (§FS-005-dispatch.12).A dry run into a fresh root vets the machine it would install. For a root that does not exist yet, the dry run now vets
WorkRoot::proposed, as the real dispatch already did. Without this, a configured recipe whose state the shipped machine lacks still replayed underLeaveand refused afterwards: the same defect, reached on a second path. Visible side effect:work dispatch --dry-runon a fresh root now refuses where the real dispatch would, which is what §FS-005-dispatch.6 already promised of a dry run.One refusal, with a next step. The undeclared-state refusal now lives in one place,
WorkRoot::undeclared(src/work/runtime/plan.rs). Dispatch and the sweep (src/sweep.rs) both use it; the sweep used to carry its own copy. The refusal still names the recipe, the state and the machine, and now ends with what to change: give the recipe astatethe machine declares, or declare the state in<root>/states.yaml. For a proposed root, where nostates.yamlexists yet, it names the work configuration'sstatesinstead (§FS-005-dispatch.6).Spec. §FS-005-dispatch.12 gains the paragraph "Whether anybody is coming is asked before the replay, not learned after it". §FS-005-dispatch.6 gains the clause saying the refusal names what to change, and that the root's machine is never changed for the reader. Both code sites cite these points.
Known and left as is. A refusal that appears only in the real dispatch after a passing ask, such as the read-back of the machine after it is minted, would still follow a
Leavereplay. Review judged it a nit and could not reproduce it, and the comment insrc/rebase.rsnames the case.No changelog entry, per the repository's convention.
Verification
New and updated tests in
tests/integration/work_test.rs:a_fresh_root_takes_the_shipped_rebase_recipe_and_the_conflict_stands_for_it: the control. A fresh root takes the shipped recipe, exits 3, the tree is left mid-rebase, and the ticket is written.a_rebase_whose_hand_over_would_be_refused_puts_the_tree_back: the issue's case. The command exits non-zero with the refusal, the tree is not mid-rebase, HEAD is on its starting commit, no ticket is written, and REPORT is written.a_fresh_root_whose_machine_would_refuse_puts_the_tree_back: the second path from review. The proposed machine is vetted, the tree is put back, no work root is made, and the refusal names the configuration'sstates.a_refusal_for_an_undeclared_state_names_what_to_change: both next-step clauses, and the root's machine is left byte-for-byte unchanged.a_recipe_dispatch_the_machine_refuses_makes_no_opening_move:work dispatchwithopens_with: rebaseand an undeclared state leaves the tree untouched.The gate on 64da471, all exit 0:
pre-commit run --all-files(cargo test, grund check and grund fmt under CI's pinned grund 0.18.0, fissile, private words, attribution);python -m unittest discover -s tests/integration -p 'test_*.py'(113 tests);python scripts/check_boundary.pyandpython scripts/check_parity.py;cargo testwithTMPDIRpointed at a symlinked directory, as on macOS.AI workflow: `rhei`, 15 agent invocations across 1 model; 8 tasks completed, 8 in progress.
github-issues-agent-grounds-ephor-245-implement-e95896a2.ticketsupervising (visit 1) — cld, anthropic/claude-opus-5-5 — 1m00s — 807.0k in / 5.6k outgithub-issues-agent-grounds-ephor-245-implement-e95896a2.ticket.triageassessing (visit 1) — cld, anthropic/claude-opus-5-5 — 2m56s — 1.8M in / 14.3k outgithub-issues-agent-grounds-ephor-245-implement-e95896a2.ticket.triagereproducing (visit 1) — cld, anthropic/claude-opus-5-5 — 1m19s — 537.9k in / 7.4k outgithub-issues-agent-grounds-ephor-245-implement-e95896a2.ticketsupervising (visit 2) — cld, anthropic/claude-opus-5-5 — 1m25s — 684.2k in / 7.0k outgithub-issues-agent-grounds-ephor-245-implement-e95896a2.ticket.specifyspecify — cld, anthropic/claude-opus-5-5 — 8m30s — 1.7M in / 15.3k outgithub-issues-agent-grounds-ephor-245-implement-e95896a2.ticketsupervising (visit 3) — cld, anthropic/claude-opus-5-5 — 26.0s — 250.2k in / 2.8k outgithub-issues-agent-grounds-ephor-245-implement-e95896a2.ticket.implementimplement — cld, anthropic/claude-opus-5-5 — 19m12s — 2.2M in / 14.8k outgithub-issues-agent-grounds-ephor-245-implement-e95896a2.ticketsupervising (visit 4) — cld, anthropic/claude-opus-5-5 — 52.0s — 262.9k in / 3.0k outgithub-issues-agent-grounds-ephor-245-implement-e95896a2.ticket.review-1review — cld, anthropic/claude-opus-5-5 — 4m47s — 1.4M in / 11.3k outgithub-issues-agent-grounds-ephor-245-implement-e95896a2.ticketsupervising (visit 5) — cld, anthropic/claude-opus-5-5 — 24.0s — 264.3k in / 2.3k outgithub-issues-agent-grounds-ephor-245-implement-e95896a2.ticket.fix-1fix — cld, anthropic/claude-opus-5-5 — 13m12s — 826.9k in / 7.0k outgithub-issues-agent-grounds-ephor-245-implement-e95896a2.ticketsupervising (visit 6) — cld, anthropic/claude-opus-5-5 — 28.0s — 267.2k in / 2.3k outgithub-issues-agent-grounds-ephor-245-implement-e95896a2.ticket.review-2review — cld, anthropic/claude-opus-5-5 — 2m03s — 1.2M in / 6.2k outgithub-issues-agent-grounds-ephor-245-implement-e95896a2.ticketsupervising (visit 7) — cld, anthropic/claude-opus-5-5 — 25.0s — 272.3k in / 2.4k outgithub-issues-agent-grounds-ephor-245-implement-e95896a2.ticketsupervising (visit 8) — cld, anthropic/claude-opus-5-5 — 27.0s — 275.6k in / 2.4k out