Repository navigation
Offer a git staging control only where the server proved a slot - #162
Merged
Merged
Conversation
PR #221 shipped no staging capability on purpose: a proposal id plus a snapshot revision does not pin a worktree, so a staging confirmation digest would have had to commit to the HEAD object id and the index state, and no projection carried either. That was the single contract gap behind the last open item of #128, and both halves of the ticket reached it independently. bext-stack/automonique PR #225 earned that field set from a mutation-free preflight of the repository. `ReviewCapabilities` now carries `staging` and `conflict_resolutions`, each entry naming the commit HEAD resolved to and a digest over the whole index, plus the two digests every confirmed lane needs. The four staging actions moved into `requires_confirmation`, so their unconfirmed spelling no longer encodes on either side. This commit is the bump and the mechanical repair that keeps the tree green: `ReviewCapabilities::new` grew an eighth argument, so every test helper that mints a capability response now passes an explicitly empty `ReviewGitStagingCapabilities`. Empty is the honest value for a helper that is exercising another family — it says the server proved no staging, which is exactly what those tests assume. The lockfile diff is limited to the two `automonique-*` source lines. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nf2PAqdKZxRuZSYGq3NE6o
The combined staged/unstaged/untracked/conflicted view has shipped since PR #159. Only the mutations were missing, and for one structural reason: `ReviewCapabilities` carried no staging capability, so a stage could only have gone out unconfirmed and uncorrelated, and an ambiguous outcome would have been reconcilable by nothing but an uncorrelated key search. That is exactly what the confirmed lane exists to avoid. The capability landed, so this consumes it, following `delivery.rs` rather than inventing a second shape: the two are the same problem, one read-side projection reporting what the server proved plus which fence is missing when it proved nothing. Absence is the withholding. The three staging kinds share one capability type but not one grant: an operator installs `index_write`, `commit` and `conflict_resolution` independently. A deployment granting stage and unstage but not commit therefore advertises two entries, and the commit proposal keeps its observation badge and grows no button. A disabled button with a tooltip would claim the grant is present and merely unavailable, which is the opposite of what the list says. `conflict_resolutions` lists one entry per admissible side, because the side decides which blob lands and a digest cannot commit to a choice not yet made. A file with both stage 2 and stage 3 recorded renders two controls; a delete/modify pair renders one. Three things this had to touch beyond the obvious: `review_action`'s confirmation guard now asks `requires_confirmation()` instead of restating it as a local `matches!` on the rerun. The confirmed set grew from one member to eight with this protocol revision, and the local spelling had already gone stale for `merge_pull_request`, which was minting previews the transport could no longer encode. That constructor now takes its capability like the rerun does, so it refuses honestly instead of failing at the wire. `custody.rs` did not understand these four families. It would have returned `UnsupportedAction` at `prepare` and the dispatch would have died at the fence, which is precisely what PR #161 found for the batch delivery after PR #160 asserted the format already knew "these actions". It knew the two it named. The disk enum now spells each staging kind separately, because a recovered plan that could be re-read as another kind would let a narrower grant replay as a wider one, and a conflict resolution persists its side because the side is part of the effect's identity. Receipts are polled through `GetReviewReceipt` with the correlation digest, which `lookup_review_action` already routes for any confirmed preview: re-sending the action would be pointless anyway, since the write invalidated the confirmation it was minted with. A stale worktree withdraws the control locally rather than reaching the daemon. The server mints the confirmation over the commit HEAD resolved to, the whole index and every named path, so comparing the digest against a fresher capability document *is* the head-and-index comparison; the observation is exposed on the projection so a reader can see the fence rather than trust that one exists. Hunk granularity is not built and not faked. Proposals list file ids and `ReviewAnchor`'s hunk id serves comments only, so file-level staging is all the contract can express. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nf2PAqdKZxRuZSYGq3NE6o
Amends SDUC-495 for capability-fenced file-level staging, unstaging, committing and conflict resolution, and adds SDTEST-1867..1873 across the core and UI inventories. The use case now states the two things a reader has to know to review this surface: a control exists only where a slot exists for that exact proposal, so a withheld grant is an absent control rather than a disabled one; and `conflict_resolutions` lists one entry per side git recorded, so a file holding both renders two. SDTEST-1848 is retired rather than deleted. It pinned the reason staging had no control at all, and that reason is gone: PR #225 closed the contract gap it named. SDTEST-1867 replaces it and fails closed the other way round, demonstrating that a deployment granting stage and unstage but not commit renders no commit control while both siblings go end to end. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nf2PAqdKZxRuZSYGq3NE6o
A staging proposal names files, so its control belongs on the file card next to the observation badge, not in the footer row where the check rerun and the batch send live. Those two act on the review as a whole; these do not. Same widgets, same sizing, same disabled gate as both siblings, so the placement is the only divergence and it is recorded where the next reader will look for it. Two smaller corrections in the same pass: the control row claims its bottom margin only when it holds a control, so a card with none does not push its preview down by an empty row's spacing; and the lane-level "is there anything a control could apply to" check now walks proposals once instead of every file against every proposal on a render path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nf2PAqdKZxRuZSYGq3NE6o
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 the last open item of #128: the combined review can now stage, unstage, commit and resolve conflicts, at file level.
The read side has shipped since PR #159 —
ReviewFilealready carriedworktree,conflict, the hunks and the preview metadata, so a partially staged file appeared on both sides and an unresolved conflict was listed only as a conflict. Only the mutations were missing, and #159 and #160 reached the same conclusion independently from opposite directions:ReviewCapabilitiesadvertised nothing that could fence a staging write, so a stage could only have gone out unconfirmed and uncorrelated, and an ambiguous outcome would have been reconcilable by nothing but an uncorrelated key search. That is precisely what the confirmed lane exists to avoid.bext-stack/automoniquePR #225 built the missing fence, and this consumes it.What arrives on the wire
Two lists on
ReviewCapabilities. Each entry names the commitHEADthe server's preflight resolved to and a digest over the whole index it read, because a worktree is shared substrate: another process running as the daemon uid can move either between the advertisement and the action, and the snapshot revision only tracks what the projection observed. The four actions moved intorequires_confirmation, so their unconfirmed spelling no longer encodes on either side.Absence is the withholding
The three staging kinds share one capability type but not one grant — an operator installs index writes, committing and conflict resolution independently. A deployment that grants stage and unstage but not commit therefore advertises two entries, and the commit proposal keeps its observation badge and grows no button. Not a disabled one with a tooltip: a disabled control claims the grant is present and merely unavailable, which is the opposite of what the list says.
SDTEST-1867 demonstrates that rather than asserting it. One run: the withheld commit answers
falseat the projection, refuses at the preview constructor, and stays visible as an observation; both granted siblings go end to end to a confirmed preview carrying the server's own digests verbatim, still admitted by a re-read of the capability. Then the commit grant is installed and the third control appears from unchanged projection code. Mutation-checked — forcingadvertises_stagingtotruefails it on the intended line.conflict_resolutionslists one entry per side git actually recorded, because the side decides which blob lands and is therefore inside the confirmation digest. A file holding both stage 2 and stage 3 renders two buttons; a delete/modify pair renders one (SDTEST-1869, which proves the two previews carry different confirmations — that is what makes them unswappable).Withdrawing locally rather than being refused
The server mints the confirmation over the commit
HEAD, the whole index and every named path, so comparing that digest against a fresher capability document is the head-and-index comparison; restating a subset of it separately would add nothing. Every staging preview therefore reportsrequires_capability_revalidation, and a moved worktree stops matching before anything reaches the daemon (SDTEST-1868). The observation is exposed on the projection and rendered, so a reader can see the fence rather than trust that one exists.Receipts are polled through
GetReviewReceiptwith the correlation digest —lookup_review_actionalready routes that for any confirmed preview. Re-sending the action would be pointless anyway: the write invalidated the confirmation it was minted with.Two things this had to touch beyond the obvious
The confirmation guard was a local restatement of the contract.
review_actiontestedmatches!(action, RerunCheck { .. })rather than askingrequires_confirmation(). The confirmed set grew from one member to eight with this protocol revision, and the stale spelling had already gone wrong formerge_pull_request, which was minting previews the transport could no longer encode. That constructor now takes its capability the way the rerun does, so it refuses honestly instead of failing at the wire. The server still ships the merge slot empty, so nothing changes in production.custody.rsdid not understand these four families.preparewould have returnedUnsupportedActionand the dispatch would have died at the fence — the same defect PR #161 found for the batch delivery after PR #160 asserted the format already knew "these actions". It knew the two it named. The disk enum now spells each staging kind separately, because a recovered plan re-readable as another kind would let a narrower grant replay as a wider one, and a conflict resolution persists its side because the side is part of the effect's identity. SDTEST-1870/1871 drive the whole store rather than only the enum.Hunk granularity: not built, not faked
ReviewProposallists file ids, a staging action names a proposal, andReviewAnchor's hunk id exists for comments only. File-level staging is all the contract can express, so that is what ships. The #128 item ticks for the file half; the hunk half needs a protocol addition — a hunk-scoped proposal or action, plus a hunk identity stable enough to fence a write on — and should be tracked separately.Coverage
SDTEST-1867..1873, rows in
docs/testing/tests-core.mdandtests-ui-and-app.md, plus a## Change logentry amending SDUC-495. SDTEST-1848 is retired rather than deleted: it pinned the reason staging had no control, and that reason is gone.Verification
cargo test --workspace --exclude shelldeck-ssh,cargo clippy --workspace --no-deps -- -D warnings,cargo fmt --all -- --check. The lockfile diff is the twoautomonique-*source lines and nothing else.🤖 Generated with Claude Code
https://claude.ai/code/session_01Nf2PAqdKZxRuZSYGq3NE6o