Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changes/unreleased/Added-20260602-092301.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
kind: Added
body: 'integration rebuild: Add --no-rerere, --reset-resolution-file, and --from-scratch flags to clear cached resolution state, and log resolver/regenerator/rerere invocations so silent replays of stale resolutions are visible.'
time: 2026-06-02T09:23:01.662647-04:00
3 changes: 3 additions & 0 deletions .changes/unreleased/Changed-20260602-101431.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
kind: Changed
body: 'integration rebuild: Halt with a resolver-failure error when the configured auto-resolver produces corrupt or unusable output (parse error, non-zero exit, iteration cap). Previously the rebuild fell through to accept-incoming, which routinely dropped integration-side declarations.'
time: 2026-06-02T10:14:31.132085-04:00
13 changes: 13 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,16 @@ cover.html

# Test configuration (contains environment-specific values)
internal/forge/forgetest/testconfig.yaml

# Transient state from 'gs integration rebuild'. Held only for the
# life of the rebuild and removed on exit; should never be committed.
# Was previously swept up by AmendCommitAll's 'git add -A' and baked
# into the integration branch's merge commit on every rebuild.
.spice_rebuild.lock

# Per-repo resolver state for 'gs integration rebuild'. Carries the
# current_merge pointer (rewritten before each resolver call) and any
# Q&A history the user has answered. The resolver-recipe docs already
# instruct users to gitignore it; same AmendCommitAll path was also
# baking it into integration's merge commits.
.integration_resolution.json
72 changes: 52 additions & 20 deletions doc/includes/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,9 +317,11 @@ Before merging, the stack is checked for branches
whose base PR was already merged on the forge.
Use --no-branch-check to skip this validation.

Before each merge, waits for CI checks to pass.
Use --build-timeout to configure the maximum wait
before failing if checks are not ready.
Before each merge, waits for merge readiness:
the forge must observe the pushed head
and report that the CR is ready to merge.
Use --ready-timeout to configure the maximum wait
before failing if merge readiness is not reached.

By default, a branch failure skips that branch's upstack descendants,
but independent sibling branches continue.
Expand All @@ -328,12 +330,12 @@ Use --fail-fast to stop the queue after the first branch failure.
**Flags**

* `--method=METHOD` ([:material-wrench:{ .middle title="spice.merge.method" }](/cli/config.md#spicemergemethod)): Preferred merge method. One of 'merge', 'squash', and 'rebase'.
* `--build-timeout=30m` ([:material-wrench:{ .middle title="spice.merge.buildTimeout" }](/cli/config.md#spicemergebuildtimeout)): Max time to wait for CI checks before each merge. 0 means check once.
* `--ready-timeout=30m` ([:material-wrench:{ .middle title="spice.merge.readyTimeout" }](/cli/config.md#spicemergereadytimeout)): Max time to wait for merge readiness before each merge. 0 means check once.
* `--no-branch-check`: Skip stale base validation before merging.
* `--fail-fast`: Stop the merge queue after the first branch failure.
* `--branch=NAME`: Branch whose stack to merge

**Configuration**: [spice.merge.buildTimeout](/cli/config.md#spicemergebuildtimeout), [spice.merge.method](/cli/config.md#spicemergemethod)
**Configuration**: [spice.merge.method](/cli/config.md#spicemergemethod), [spice.merge.readyTimeout](/cli/config.md#spicemergereadytimeout)

### git-spice stack restack {#gs-stack-restack}

Expand Down Expand Up @@ -631,7 +633,7 @@ This command acts as a local merge queue:
it merges one Change Request,
waits for that merge to finish,
restacks and updates the next Change Request,
waits for its CI checks to pass,
waits for merge readiness on the updated Change Request,
and then repeats the process.

For a stack like this:
Expand All @@ -649,13 +651,15 @@ Before merging, the downstack is checked for branches
whose base PR was already merged on the forge.
Use --no-branch-check to skip this validation.

Before each merge, waits for CI checks to pass.
Use --build-timeout to configure the maximum wait
Before each merge, waits for merge readiness:
the forge must observe the pushed head
and report that the CR is ready to merge.
Use --ready-timeout to configure the maximum wait
(default: 30m, 0 means fail immediately if not ready).

Between merges, the command waits for each merge
to complete, restacks and updates the next PR,
waits for CI checks on the updated PR,
waits for merge readiness on the updated PR,
and syncs merged branch cleanup.

Use --no-wait for single branch merging
Expand All @@ -665,12 +669,12 @@ when you don't want to wait for the merge to propagate.
**Flags**

* `--method=METHOD` ([:material-wrench:{ .middle title="spice.merge.method" }](/cli/config.md#spicemergemethod)): Preferred merge method. One of 'merge', 'squash', and 'rebase'.
* `--build-timeout=30m` ([:material-wrench:{ .middle title="spice.merge.buildTimeout" }](/cli/config.md#spicemergebuildtimeout)): Max time to wait for CI checks before each merge. 0 means check once.
* `--ready-timeout=30m` ([:material-wrench:{ .middle title="spice.merge.readyTimeout" }](/cli/config.md#spicemergereadytimeout)): Max time to wait for merge readiness before each merge. 0 means check once.
* `--no-wait`: Skip polling for a single branch merge to propagate.
* `--no-branch-check`: Skip stale base validation before merging.
* `--branch=NAME`: Branch to start merging from

**Configuration**: [spice.merge.buildTimeout](/cli/config.md#spicemergebuildtimeout), [spice.merge.method](/cli/config.md#spicemergemethod)
**Configuration**: [spice.merge.method](/cli/config.md#spicemergemethod), [spice.merge.readyTimeout](/cli/config.md#spicemergereadytimeout)

### git-spice downstack edit {#gs-downstack-edit}

Expand Down Expand Up @@ -1140,16 +1144,18 @@ Use --branch to merge a different branch.
The branch must be based directly on trunk.
To merge a stacked branch, use 'gs downstack merge'.

Before merging, waits for CI checks to pass.
Use --build-timeout to configure the maximum wait.
Before merging, waits for merge readiness:
the forge must observe the pushed head
and report that the CR is ready to merge.
Use --ready-timeout to configure the maximum wait.

**Flags**

* `--method=METHOD` ([:material-wrench:{ .middle title="spice.merge.method" }](/cli/config.md#spicemergemethod)): Preferred merge method. One of 'merge', 'squash', and 'rebase'.
* `--build-timeout=30m` ([:material-wrench:{ .middle title="spice.merge.buildTimeout" }](/cli/config.md#spicemergebuildtimeout)): Max time to wait for CI checks before each merge. 0 means check once.
* `--ready-timeout=30m` ([:material-wrench:{ .middle title="spice.merge.readyTimeout" }](/cli/config.md#spicemergereadytimeout)): Max time to wait for merge readiness before each merge. 0 means check once.
* `--branch=NAME`: Branch to merge

**Configuration**: [spice.merge.buildTimeout](/cli/config.md#spicemergebuildtimeout), [spice.merge.method](/cli/config.md#spicemergemethod)
**Configuration**: [spice.merge.method](/cli/config.md#spicemergemethod), [spice.merge.readyTimeout](/cli/config.md#spicemergereadytimeout)

### git-spice branch submit {#gs-branch-submit}

Expand Down Expand Up @@ -1454,17 +1460,43 @@ configured resolver script is invoked to attempt automatic
resolution before surfacing conflicts. See the recipe for
details on the JSON protocol the script must implement.

Any conflicts that survive the merge drivers and the resolver
are auto-resolved by taking the incoming tip's version. Pass
--no-accept-incoming (or set spice.integration.acceptIncoming
=false) to disable that final fallback and surface conflicts
for manual resolution instead.
If the resolver produces corrupt or unusable output (script
exit failure, malformed JSON, missing markers) the rebuild
halts rather than falling through to accept-incoming. The
usual cause is a prompt, model, or script issue that needs
to be fixed, not glossed over by silently picking 'theirs'.

Conflicts that survive the merge drivers and a successful
resolver run are auto-resolved by taking the incoming tip's
version. Pass --no-accept-incoming (or set
spice.integration.acceptIncoming=false) to disable that
final fallback and surface conflicts for manual resolution
instead.

If a bad resolution was silently cached (in rerere, in the
resolution file, or in pending rebuild state) and is being
replayed on every rebuild, use --reset-rerere-cache to wipe
stale postimages (while still recording fresh ones),
--reset-resolution-file to wipe the Q&A history,
--reset-pending to drop any stale resume point, or
--from-scratch for all three.

--no-rerere is a stronger diagnostic mode: it disables
rerere recording too, so the rebuild produces nothing
cached for next time. Use it only when you suspect rerere
itself is misbehaving — otherwise --reset-rerere-cache is
what you want.

**Flags**

* `--push`: Also push the integration branch after rebuilding
* `--[no-]auto-resolve` ([:material-wrench:{ .middle title="spice.integration.autoResolve" }](/cli/config.md#spiceintegrationautoresolve)): Auto-resolve merge conflicts using the configured resolver script
* `--[no-]accept-incoming` ([:material-wrench:{ .middle title="spice.integration.acceptIncoming" }](/cli/config.md#spiceintegrationacceptincoming)): Final-stage fallback: take the incoming tip's version for any remaining conflicts so the rebuild completes without manual intervention
* `--no-rerere`: Disable rerere entirely (no replay AND no recording) for this rebuild. Diagnostic mode — prefer --reset-rerere-cache when you want fresh resolutions cached for next time.
* `--reset-rerere-cache`: Wipe the rerere cache (.git/rr-cache) before starting so stale cached resolutions are not replayed. Rerere stays enabled so the rebuild's fresh resolutions are recorded.
* `--reset-resolution-file`: Delete the resolution file (.integration_resolution.json) before starting so stale Q&A history is not carried into this rebuild.
* `--reset-pending`: Clear any pending rebuild state before starting. Use when a prior halted rebuild left stale state that should not be resumed.
* `--from-scratch`: Shorthand: implies --reset-rerere-cache, --reset-resolution-file, and --reset-pending. Use after a bad rebuild left stale state in any cache; rerere stays enabled so good resolutions get recorded.

**Configuration**: [spice.integration.acceptIncoming](/cli/config.md#spiceintegrationacceptincoming), [spice.integration.autoResolve](/cli/config.md#spiceintegrationautoresolve)

Expand Down
45 changes: 42 additions & 3 deletions doc/src/guide/integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,13 @@ Entries are pruned automatically when their branches are untracked
($$gs branch untrack$$), deleted ($$gs branch delete$$), or removed
by $$gs repo sync$$ after the underlying CR merges.

| Response | Behavior |
|-----------------------------------------------------|---------------------------------------------------------------------------------------|
| Empty (or `assumptions` only) | Assumptions logged at info level. Files staged, merge committed. |
| `questions` populated | Each question is asked interactively. Answers append to the resolution file. Resolver is re-invoked. |
| `unresolved_files` populated, no `questions` | Resolver structurally surrendered. The conflict surfaces for manual resolution; `--accept-incoming` is bypassed (the AI itself flagged the file as needing human judgement). |
| Non-zero exit code or invalid JSON | **Rebuild halts** with a resolver-failure error; pending state is saved so you can resume after fixing the underlying problem. `--accept-incoming` is also bypassed, since picking "theirs" wholesale would routinely drop integration-side API surface (methods, fields, getters that the tip branch never had). |

### Example: Claude Code resolver

The resolver is configured as a user-level preference — the config
Expand All @@ -199,7 +206,11 @@ and turn auto-resolve on by default:
```bash
git config --global spice.integration.resolver "$(cat <<'GITCONFIG'
#!/bin/sh
exec claude --print --max-turns 30 <<'PROMPT'
RAW=$(mktemp -t spice-resolver-raw.XXXXXX)
echo "spice resolver: raw Claude output -> $RAW" >&2
claude --print --max-turns 30 --output-format text 2>/dev/null <<'PROMPT' \
| tee "$RAW" \
| sed -n '/<resolution>/,/<\/resolution>/{/<resolution>/d;/<\/resolution>/d;p;}'
You are resolving merge conflicts on a throwaway integration branch.

CONTEXT
Expand All @@ -220,9 +231,27 @@ WHAT TO DO
- Do NOT run 'git add' or 'git commit'. After you exit, git-spice
stages every originally-conflicted path and commits the merge.

OUTPUT — emit exactly one JSON document on stdout, then exit:

DEFAULT TO ADDITIVE MERGES.
When each side adds an independent top-level declaration at the
conflict boundary — a method on a struct, an exported package
function, a struct field, an interface method, a config getter, a
type definition, an import, a struct literal field — that is almost
never a real conflict. Keep BOTH additions, in either order.
Dropping one side's method or getter to "resolve" the conflict
silently breaks the build of any caller that depends on it. Only
collapse two declarations into one when they have the same name
AND the same signature AND one body is strictly a superset of the
other; otherwise keep both. When in doubt, keep both and add an
assumption — it is much easier for the user to delete a stray
method than to rediscover one that was silently dropped.

OUTPUT — emit exactly one JSON document between <resolution> and
</resolution> tags. Only content between these tags is parsed; any
preface or trailing prose is dropped. Exit after emitting.

<resolution>
{"assumptions": [...], "questions": [...], "unresolved_files": [...]}
</resolution>

- All three keys are optional. Empty (or assumptions-only) means
"everything resolved cleanly"; git-spice will stage and commit.
Expand All @@ -237,6 +266,16 @@ GITCONFIG
git config --global spice.integration.autoResolve true
```

The `<resolution>...</resolution>` marker pattern (paired with the
`sed -n` extraction in the script) mirrors the
[$$spice.messageGenerator$$](../cli/config.md#spicemessagegenerator)
recipe. It is more robust than a strict-JSON contract: Claude
routinely emits a leading prose summary even when told not to, and
the marker lets us discard it without changing the resolver's JSON
parser. If the model omits the markers entirely, sed produces no
output — git-spice then logs an "Auto-resolve failed" warning and
falls through to whichever fallback you have configured.

For Claude Code to run unattended, pre-approve the tools it needs in
`~/.claude/settings.json`. The schema is a `permissions` object with
an `allow` array of tool patterns
Expand Down
Loading
Loading