git-loom 0.20.0

A Git CLI tool that weaves together multiple feature branches into integration branches
# Spec 014: Continue and Abort (Conflict Recovery)

## Overview

When a loom command encounters a rebase conflict, it pauses the operation and
saves recovery state to disk. The user resolves the conflict with standard git
tools, then runs `loom continue` to finish or `loom abort` to cancel.

This replaces the previous behavior of auto-aborting on conflict and forcing
the user to re-run the command from scratch.

## In-Scope Commands

The following commands support resumable conflict handling:

- `update`
- `commit`
- `absorb`
- `drop commit` (not `drop branch`)
- Simple single-rebase `fold` paths:
  - `fold_files_into_commit` non-HEAD path
  - `fold_commit_into_commit`
  - `fold_commit_to_branch`
  - `fold_commit_to_unstaged` non-HEAD path (single rebase boundary only)

## Out-of-Scope Commands

The following commands retain the old hard-fail behavior: if they encounter a
conflict they abort immediately and leave the repository in its original state:

- `drop branch`
- `reword`
- `split`
- `fold` edit-and-continue paths
- `fold` multi-phase paths

## Saved State

When an in-scope command hits a conflict, it saves a state file:

```
.git/loom/state.json
```

The state file contains:

- `command`: The name of the interrupted command (e.g., `"update"`, `"commit"`)
- `rollback`: Saved references and patches for abort recovery:
  - `saved_head`: HEAD OID before the operation started
  - `saved_refs`: Snapshot of all branch ref OIDs before the operation
  - `delete_branches`: Branch names created during this operation (to delete on abort)
  - `saved_staged_patch`: Staged diff saved aside during the operation
  - `saved_worktree_patch`: Full working-tree diff saved before the rebase
- `context`: Command-specific resume data (serialized as JSON)

The `.git/loom/` directory is created lazily when state is first saved.

## Paused Operation Lifecycle

```
loom <command>
  → rebase starts
  → conflict encountered
  → state saved to .git/loom/state.json
  → spinner shows error, user sees conflict guidance
  → process exits successfully (exit code 0)

user resolves conflicts:
  git add <resolved files>

loom continue
  → loads state
  → runs git rebase --continue (if rebase still active)
    → if another conflict: stays paused, state preserved
    → if completed: runs post-rebase work for the interrupted command
  → deletes state on success

--- OR ---

loom abort
  → loads state
  → aborts active rebase (if any)
  → applies rollback (restore refs, staged patch, worktree patch)
  → deletes state
  → reports success
```

## Allowed Commands While Paused

When `.git/loom/state.json` exists, most commands are blocked with an error
naming the interrupted command and instructing the user to run `loom continue`
or `loom abort`.

**Allowed while paused:**

- `show`
- `trace`
- `continue`
- `abort`

**Blocked while paused:**

- `status`
- `update`
- `commit`
- `absorb`
- `drop`
- `fold`
- `branch`
- `push`
- `init`
- `reword`
- `split`

**Always exempt (never checked):**

- `Completions` — does not interact with the repo
- `InternalWriteTodo` — runs as a git subprocess during rebase

Error message format:

```
A loom <command> operation is paused due to a conflict.
Resolve the conflict and run `loom continue`, or run `loom abort` to cancel.
```

## `loom continue`

```bash
loom continue
```

1. Loads `.git/loom/state.json`. Errors if the file does not exist.
2. If a rebase is still in progress (`MERGE_HEAD` or `rebase-merge/` exists):
   - Runs `git rebase --continue`.
   - If `--continue` encounters another conflict: stays paused, keeps the state
     file, reports that the operation is still paused, exits successfully.
   - If `--continue` succeeds: moves to dispatch.
3. If no rebase is in progress: assumes the user already ran `git rebase
   --continue` manually and moves to dispatch.
4. Dispatches to the command-specific `after_continue` handler.
5. Deletes the state file only after dispatch succeeds.

## `loom abort`

```bash
loom abort
```

1. Loads `.git/loom/state.json`. Errors if the file does not exist.
2. Aborts the active rebase if one is in progress.
3. Applies shared rollback:
   - Hard-resets HEAD to `saved_head`
   - Restores all branch refs from `saved_refs`
   - Deletes branches listed in `delete_branches`
   - Re-applies `saved_staged_patch` (if non-empty)
   - Re-applies `saved_worktree_patch` (if non-empty)
4. Deletes the state file.
5. Reports success.

## Double-Conflict Behavior

If `loom continue` encounters another conflict after running `git rebase
--continue`, the state file is kept and the rebase remains paused. The user
resolves the new conflict and runs `loom continue` again. This can repeat as
many times as needed until the rebase completes.

## Missing or Corrupted State

- **`loom continue` with no state file**: Errors with
  `"No loom operation is in progress"`.
- **`loom abort` with no state file**: Errors with
  `"No loom operation is in progress"`.
- **Corrupted state file**: Both commands error with a descriptive parse
  failure message. The user must recover manually using `git rebase --abort`
  if a rebase is in progress.

## Command-Specific Resume Context

Each resumable command stores its own context in the `context` field. The
context is opaque JSON. The command discriminator (`command` field) determines
which `after_continue` handler is invoked during dispatch.

### `update` context

```json
{
  "branch_name": "<local branch name>",
  "upstream_name": "<upstream tracking branch name>",
  "skip_confirm": false
}
```

After continue: runs submodule update (if applicable), reports the upstream
commit info, and proposes removing gone-upstream branches.

### `commit` context

```json
{
  "branch_name": "<target feature branch name>",
  "saved_staged_patch": "<patch content>"
}
```

After continue: restores pre-existing staged changes, prints the success
message with the new commit hash on the target branch.

### `absorb` context

```json
{}
```

After continue: restores pre-existing staged changes and re-applies skipped
worktree changes (from `saved_worktree_patch`), prints the absorb success
message.

### `drop` context

```json
{
  "commit_hash": "<short hash of dropped commit>"
}
```

After continue: prints the drop success message.

### `fold` context

```json
{
  "fold_op": "<FoldOp discriminator>",
  ... (operation-specific fields)
}
```

After continue: prints the fold success message.

## Design Decisions

### State File in `.git/loom/`

Storing state under `.git/loom/` (rather than, e.g., the work tree root or
XDG config) keeps it:

- Scoped to the specific repository
- Out of the work tree (not visible to `git status`)
- Co-located with git's own recovery state (e.g., `rebase-merge/`)

### Exit Code 0 on Conflict Pause

When the operation is paused due to a conflict, loom exits with code 0. This
prevents CI pipelines or shell scripts from seeing a spurious failure while the
user is in the middle of conflict resolution. The conflict is surfaced visually
via the spinner error indicator and guidance message.

### State Deleted Only After Success

The state file is deleted only after `after_continue` succeeds. If
`after_continue` itself fails (e.g., a patch restore error), the state file
remains and the user can retry `loom continue`.

### Rollback Restores Pre-Existing State

The rollback on `loom abort` restores all branch refs and staged/worktree
patches to their state before the operation started. For `commit`, the
working-tree changes are preserved via mixed reset (the commit content returns
to the working directory as unstaged changes).