Skip to content

Repository files navigation

squire

Non-interactive, hunk-addressable git staging CLI for LLMs.

Splitting a commit with squire

git add -p is an interactive TUI that LLMs can't drive. squire exposes the same hunk-level staging through single commands with structured arguments, so an LLM (or script) can selectively stage, unstage, revert, and show hunks without any interactive prompts. It also provides branch cleanup analysis to identify merged, squash-merged, and stale branches, and a contextualized rebase playbook that guides an LLM through rebases and conflict resolution.

Every hunk gets a short, stable, content-based ID (first 8 hex chars of the hunk content's SHA-256 hash). Each line within a hunk also gets a short content hash (shortest unique prefix, min 2 hex chars). Use these IDs and line hashes to reference hunks and individual lines across commands.

Install

cargo install --path .

Requires Rust 1.85+ and git on your PATH.

Kiro CLI setup

Generate a steering file so the Kiro CLI knows how to use squire:

squire --llm-help > ~/.kiro/steering/squire.md

Usage

Default output is human-readable plain text. Global flags:

  • --json — structured JSON output
  • --short — compact one-line-per-hunk summary (ID, file, range, +/- counts)
  • --llm-help — print a comprehensive reference for LLM consumption, then exit

View hunks

squire diff                          # unstaged working tree changes (includes untracked)
squire diff --cached                 # staged changes
squire diff HEAD~1                   # working tree vs ref
squire diff HEAD~1 HEAD~2            # ref vs ref
squire diff -- src/main.rs           # filter by path
squire diff --json                   # output as JSON

Inspect a hunk

squire show abc12345                 # hunk from working tree or staged
squire show HEAD abc12345            # hunk from last commit (falls back to diff)
squire show HEAD~2 abc12345          # hunk from two commits ago
squire show df489e67                 # all hunks from commit df489e67

Stage, unstage, and revert hunks

squire stage abc12345 def67890       # stage specific hunks
squire stage abc12345:f3,a1          # stage specific lines by hash
squire stage abc12345:f3-7b          # stage a range of lines
squire unstage abc12345              # unstage specific hunks
squire revert abc12345               # discard changes from working tree
squire revert abc12345:f3,a1         # revert specific lines

Revert works on both unstaged and staged hunks. Staged hunks are unstaged and reverse-applied in one step.

Reword a commit message

squire reword HEAD -m "new message"            # reword HEAD
squire reword HEAD~2 -m "fix: corrected typo"  # reword older commit

For HEAD, delegates to git commit --amend -m. For older commits, uses a non-interactive rebase with reword. Requires a clean working tree for non-HEAD targets.

Drop hunks from a commit

squire drop HEAD abc12345                      # drop hunk from HEAD
squire drop HEAD~2 abc12345 def67890           # drop hunks from older commit

Inverse of amend: removes specific hunks from an existing commit. Find hunk IDs with squire diff <commit>~1 <commit> or squire log --json.

Any unstaged or staged changes are preserved across the operation.

Commit and amend

squire commit -m "feat: parser" abc12345       # stage + commit in one step
squire amend abc12345                          # amend into HEAD
squire amend -m "new msg" abc12345             # amend HEAD with new message
squire amend --commit HEAD~2 abc12345          # amend into an older commit

When --commit targets a non-HEAD commit, squire creates a fixup commit and runs an autosquash rebase to fold it in. The -m flag is only supported when amending HEAD.

Check status

squire status                        # plain text summary
squire status --json                 # structured output

Stage untracked files selectively

Untracked files always appear as new-file hunks in squire diff, so you can stage them with the same workflow as modified files.

# See all changes including new files
$ squire diff
--- b/src/new_module.rs ---
[a1b2c3d4] @@ -0,0 +1,40 @@
+... entire new file as a single hunk ...

# Stage the whole new file by hunk ID
$ squire stage a1b2c3d4
$ git commit -m "feat: add new_module"

Branch cleanup

squire cleanup                       # auto-detect master branch
squire cleanup --master main         # specify master branch
squire cleanup --json                # structured output for LLM

Analyzes local branches and classifies each as:

  • MERGED — fully merged via git ancestry
  • MERGED_EQUIVALENT — all commit patches are present in master (squash/cherry-pick merge); includes reworded cherry-picks
  • NEEDS_EVALUATION — some commits appear merged but others do not; each commit shows a best_match with message/diff similarity scores for LLM evaluation
  • UNMERGED — no matching commits found in master; commits with similar messages in master include best_match similarity scores

Commit history

squire log                           # last 10 commits, plain text
squire log -n 5                      # last 5 commits
squire log --short                   # one line per commit
squire log --json                    # structured output with hunks
squire log --max-hunk-lines 0        # no per-commit content cap

Lists recent commits with their hunks. Hunk IDs match what squire diff <sha>~1 <sha> would produce, so you can go straight from squire log to squire split, squire stage, or squire drop.

To keep squire log --json bounded for LLM context windows, per-commit hunk content is capped at 100 lines by default. Summary fields (id, file, old_range, new_range, header) are always kept; truncated bodies are replaced with a marker that points at squire show <sha> <id> so you can fetch the full body on demand. The commit SHA is required in the marker because squire show without a ref only searches the working tree. Pass --max-hunk-lines 0 to disable the cap.

Split a commit

squire split <commit>                # prepare to split a commit

Requires a clean working tree. Resets the target commit so its changes are unstaged, ready for selective re-staging with squire stage.

For HEAD, this is a simple mixed reset. For older commits, squire runs a non-interactive rebase that pauses at the target commit and resets it.

# Split the most recent commit into two
$ squire split abc1234
$ squire diff --json                 # see the unstaged changes
$ squire stage <id1> <id2>          # stage hunks for first commit
$ git commit -m "feat: part one"
$ squire stage <id3>                # stage remaining hunks
$ git commit -m "feat: part two"

# Split an older commit (rebase pauses at the commit)
$ squire split def5678
$ squire diff --json
$ squire stage <id> && git commit -m "first half"
$ squire stage <id> && git commit -m "second half"
$ GIT_EDITOR=true git rebase --continue  # replay remaining commits

Edit rebase todo (sequence editor)

GIT_SEQUENCE_EDITOR="squire seqedit edit:abc1234" git rebase -i HEAD~3
GIT_SEQUENCE_EDITOR="squire seqedit fixup:abc1 drop:def5" git rebase -i HEAD~5

squire seqedit rewrites a git rebase todo file, replacing sed/awk one-liners. It accepts one or more action:sha-prefix arguments followed by the todo file path (passed automatically by git).

Supported actions: pick, reword, edit, squash, fixup, drop.

Squash commits

squire squash HEAD~2 HEAD~1 HEAD             # fold last 2 commits into HEAD~2
squire squash abc1234 def5678                # fold def5678 into abc1234
squire squash -m "combined" abc1234 def5678  # squash with new message

Folds one or more source commits into a target commit. The first argument is the target (survives), the rest are folded in. The target's message is kept by default; use -m to replace it. Any unstaged or staged changes are preserved across the operation.

Stash specific hunks

squire stash abc12345                        # stash one hunk
squire stash -m "wip" abc12345              # stash with a message
squire stash abc12345:f3,a1                 # stash specific lines
squire stash abc12345 def67890              # stash multiple hunks

Removes the selected hunks from the working tree and saves them as a regular git stash entry. Other unstaged changes are preserved. Use git stash pop to restore — no special squire command needed.

Rebase

squire rebase                        # plain text playbook
squire rebase --onto origin/main     # override upstream
squire rebase --json                 # structured output

Prints a contextualized rebase playbook. Inspects the repo state and emits step-by-step instructions adapted to where you are: pre-rebase, mid-rebase with conflicts, or up-to-date. Creates a safety tag before the first rebase for easy recovery.

When mid-rebase with conflicts, the output includes:

  • current_commit — the SHA and message of the commit being replayed
  • ours_theirs — clarifies that during rebase, "ours" is the upstream and "theirs" is your commit (the opposite of merge)
  • step / total_steps — rebase progress (e.g. step 2 of 3)

Use --onto to override the upstream ref — for example, to rebase onto a different branch than the configured tracking branch.

How it works

squire wraps standard git primitives:

  • git diff and git diff --cached for working tree and index diffs
  • git show <sha> for commit diffs
  • git apply --cached to stage patches
  • git apply --cached --reverse to unstage patches
  • git apply --reverse to revert working tree changes

squire parses unified diff output, assigns content-hash IDs to each hunk, and reconstructs patches from selected hunks when staging or unstaging.

Design principle: don't replicate git

squire adds value where git's interface is interactive, unstructured, or hunk-unaware. It does not wrap git commands that already work fine non-interactively. For example:

  • Stage an entire file → git add <file> (no squire needed)
  • Restore a stash → git stash pop (no squire needed)
  • Resolve conflicts → git add + GIT_EDITOR=true git rebase --continue (no squire needed)
  • Abort a rebase → git rebase --abort (no squire needed)

squire detects and reports conflicts with structured output so an LLM or script can decide what to do, but it doesn't try to resolve them or wrap the recovery commands.

Conflict reporting

All rebase-based commands (amend --commit, drop, reword, squash, split) are atomic by default: if the rebase would leave a conflict, squire aborts the rebase, rolls back any intermediate commits it created (e.g. the fixup! commit amend creates), and resets HEAD to where it was before the command ran. The full pre-command state — HEAD, index, working tree, and untracked files — is restored exactly. You get a structured error naming the conflicting file(s) with rolled_back: true, but no half-finished rebase to clean up.

Use --pause-on-conflict to opt out of rollback. On conflict, the rebase is left paused so you can resolve the conflict by hand with git add + GIT_EDITOR=true git rebase --continue. The structured error reports rolled_back: false and gives continue/abort hints instead.

Rolled-back error (default):

{
  "conflict": true,
  "rolled_back": true,
  "conflicting_files": [
    { "file": "src/lib.rs", "status": "both_modified",
      "strategy": "non_trivial", "command": "show the diff and ask for guidance" }
  ],
  "current_commit": { "sha": "abc1234...", "message": "feat: parser" },
  "ours_theirs": {
    "ours": "upstream (origin/main)",
    "theirs": "your commit being replayed"
  },
  "hint": "amend could not complete cleanly and was rolled back; the working tree and history are unchanged. To retry and resolve by hand, re-run with `--pause-on-conflict` to leave the rebase paused on the conflicting commit. Alternative: perform a manual rebase ..."
}

Paused error (--pause-on-conflict):

{
  "conflict": true,
  "rolled_back": false,
  "conflicting_files": [ ... ],
  "current_commit": { ... },
  "ours_theirs": { ... },
  "hint": "Resolve conflicts, stage with `git add`, then run `GIT_EDITOR=true git rebase --continue`. To cancel and restore the pre-command state: `git rebase --abort` followed by `git reset --hard <pre-command HEAD>`."
}

Plain text output (rolled-back case):

Replaying: abc1234f feat: parser
Conflict during amend (rolled back, history unchanged):
  both_modified: src/lib.rs  → show the diff and ask for guidance
Note: "ours" = upstream (origin/main), "theirs" = your commit
amend could not complete cleanly and was rolled back ...

squire status reports conflicts when a rebase is paused (either from --pause-on-conflict or from an external git rebase):

{
  "branch": "HEAD",
  "rebase_in_progress": true,
  "conflicts": [
    { "file": "src/lib.rs", "status": "both_modified",
      "strategy": "non_trivial", "command": "show the diff and ask for guidance" }
  ],
  "staged": [],
  "unstaged": [],
  "staged_lines": { "added": 0, "removed": 0 },
  "unstaged_lines": { "added": 0, "removed": 0 }
}

The conflicts field is only present when there are unresolved conflicts. Possible status values: both_modified, both_added, deleted_by_us, deleted_by_them.

JSON output

Pass --json to get structured output from any command. diff and show return an array of hunk objects:

[
  {
    "id": "abc12345",
    "file": "src/main.rs",
    "old_file": "src/main.rs",
    "old_range": "10,5",
    "new_range": "10,7",
    "content": " ctx\n-old\n+new\n ...",
    "header": "fn main()",
    "line_hashes": ["e0", "90", "ca", "..."]
  }
]

The header field is present only when the @@ line includes a section header (for example, a function name).

stage, unstage, revert, and stash return a result object:

{ "staged": 2, "message": "Staged 2 hunk(s)" }

When line selectors are used, a new_hunks array is included with the residual hunks (id, file, ranges, header, line_hashes). This avoids needing to re-run squire diff after partial line operations. The array is omitted when empty.

status returns branch info, rebase state, hunks, and line counts. The staged and unstaged arrays contain the same hunk objects as diff. The conflicts array is present only during a paused rebase with unresolved conflicts:

{
  "branch": "main",
  "rebase_in_progress": false,
  "staged": [{ "id": "...", "file": "...", "..." : "..." }],
  "unstaged": [{ "id": "...", "file": "...", "..." : "..." }],
  "staged_lines": { "added": 3, "removed": 1 },
  "unstaged_lines": { "added": 5, "removed": 2 }
}

Development

cargo build
cargo test

License

MIT — see LICENSE.

About

Non-interactive git workflow CLI for LLMs and scripts

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages