Skip to content

Commit b04b1d6

Browse files
bellmanGajae Code
authored andcommitted
feat: detect git rebase/merge/cherry-pick/bisect states (#89)
Add GitOperation enum to detect mid-operation git states from the branch header in git status --short --branch output. - Rebase: 'rebasing ...' in branch header - Merge: '[merge-in-progress]' tag - Cherry-pick: 'cherry-pick-in-progress' tag - Bisect: 'bisect-in-progress' tag Operation state appears in: - status text: 'rebase-in-progress, dirty · 3 files · ...' - status JSON: 'git_operation' field (null when no operation) - git_state headline includes operation prefix Generated with https://github.com/Yeachan-Heo/gajae-code Co-authored-by: Gajae Code <dev@gajae-code.com>
1 parent 934bf28 commit b04b1d6

2 files changed

Lines changed: 72 additions & 8 deletions

File tree

ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1760,7 +1760,7 @@ Original filing (2026-04-13): user requested a `-acp` parameter to support ACP p
17601760

17611761
**Source.** Jobdori dogfood 2026-04-17 against `/tmp/claude-md-injection/inner/work` on main HEAD `82bd8bb` in response to Clawhip pinpoint nudge at `1494691430096961767`. Second (and higher-severity) member of the "discovery-overreach" cluster after #85. Different axis from the #80–#84 / #86–#87 truth-audit cluster: here the discovery surface is reaching into state it should not, and the consumed state feeds directly into the agent's system prompt — the highest-trust context surface in the entire runtime.
17621762

1763-
89. **`claw` is blind to mid-operation git states (rebase-in-progress, merge-in-progress, cherry-pick-in-progress, bisect-in-progress) — `doctor` returns `Workspace: ok` on a workspace that is literally paused on a conflict** — dogfooded 2026-04-17 on main HEAD `9882f07` from `/tmp/git-state-probe`. A branch rebase that halted on a conflict leaves the workspace in the `rebase-merge` state with conflict files in the index and `HEAD` detached on the rebase's intermediate commit. `claw`'s workspace surface reports this as a plain dirty workspace on "branch detached HEAD," with no signal that the lane is mid-operation and cannot safely accept new work.
1763+
89. **DONE — `claw` is blind to mid-operation git states (rebase-in-progress, merge-in-progress, cherry-pick-in-progress, bisect-in-progress) — `doctor` returns `Workspace: ok` on a workspace that is literally paused on a conflict** — dogfooded 2026-04-17 on main HEAD `9882f07` from `/tmp/git-state-probe`. A branch rebase that halted on a conflict leaves the workspace in the `rebase-merge` state with conflict files in the index and `HEAD` detached on the rebase's intermediate commit. `claw`'s workspace surface reports this as a plain dirty workspace on "branch detached HEAD," with no signal that the lane is mid-operation and cannot safely accept new work.
17641764

17651765
**Concrete repro.**
17661766
```

rust/crates/rusty-claude-cli/src/main.rs

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5729,6 +5729,31 @@ struct GitWorkspaceSummary {
57295729
unstaged_files: usize,
57305730
untracked_files: usize,
57315731
conflicted_files: usize,
5732+
/// #89: detected mid-operation git state (rebase, merge, cherry-pick, bisect)
5733+
operation: GitOperation,
5734+
}
5735+
5736+
/// #89: mid-operation git states detected from branch header in `git status --short --branch`.
5737+
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
5738+
enum GitOperation {
5739+
#[default]
5740+
None,
5741+
Rebase,
5742+
Merge,
5743+
CherryPick,
5744+
Bisect,
5745+
}
5746+
5747+
impl GitOperation {
5748+
fn as_str(self) -> &'static str {
5749+
match self {
5750+
Self::None => "",
5751+
Self::Rebase => "rebase-in-progress",
5752+
Self::Merge => "merge-in-progress",
5753+
Self::CherryPick => "cherry-pick-in-progress",
5754+
Self::Bisect => "bisect-in-progress",
5755+
}
5756+
}
57325757
}
57335758

57345759
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -5806,8 +5831,18 @@ impl GitWorkspaceSummary {
58065831
}
58075832

58085833
fn headline(self) -> String {
5834+
// #89: prefix with operation state when mid-operation
5835+
let op_prefix = if self.operation != GitOperation::None {
5836+
format!("{}, ", self.operation.as_str())
5837+
} else {
5838+
String::new()
5839+
};
58095840
if self.is_clean() {
5810-
"clean".to_string()
5841+
if self.operation != GitOperation::None {
5842+
format!("{op_prefix}clean")
5843+
} else {
5844+
"clean".to_string()
5845+
}
58115846
} else {
58125847
let mut details = Vec::new();
58135848
if self.staged_files > 0 {
@@ -5823,7 +5858,7 @@ impl GitWorkspaceSummary {
58235858
details.push(format!("{} conflicted", self.conflicted_files));
58245859
}
58255860
format!(
5826-
"dirty · {} files · {}",
5861+
"{op_prefix}dirty · {} files · {}",
58275862
self.changed_files,
58285863
details.join(", ")
58295864
)
@@ -6129,7 +6164,26 @@ fn parse_git_workspace_summary(status: Option<&str>) -> GitWorkspaceSummary {
61296164
};
61306165

61316166
for line in status.lines() {
6132-
if line.starts_with("## ") || line.trim().is_empty() {
6167+
if line.starts_with("## ") {
6168+
// #89: detect mid-operation states from branch header
6169+
// git status --short --branch shows:
6170+
// "## HEAD (no branch, rebasing feature-branch)"
6171+
// "## main [merge-in-progress]"
6172+
// "## HEAD (no branch, cherry-pick-in-progress)"
6173+
// "## main (no branch, bisect-in-progress)"
6174+
let header = line.to_ascii_lowercase();
6175+
if header.contains("rebasing") {
6176+
summary.operation = GitOperation::Rebase;
6177+
} else if header.contains("merge-in-progress") {
6178+
summary.operation = GitOperation::Merge;
6179+
} else if header.contains("cherry-pick-in-progress") {
6180+
summary.operation = GitOperation::CherryPick;
6181+
} else if header.contains("bisect-in-progress") {
6182+
summary.operation = GitOperation::Bisect;
6183+
}
6184+
continue;
6185+
}
6186+
if line.trim().is_empty() {
61336187
continue;
61346188
}
61356189

@@ -9433,6 +9487,12 @@ fn status_json_value(
94339487
"changed_files": context.git_summary.changed_files,
94349488
"is_clean": context.git_summary.changed_files == 0,
94359489
"staged_files": context.git_summary.staged_files,
9490+
// #89: mid-operation git state (rebase, merge, cherry-pick, bisect)
9491+
"git_operation": if context.git_summary.operation != GitOperation::None {
9492+
Some(context.git_summary.operation.as_str())
9493+
} else {
9494+
None::<&str>
9495+
},
94369496

94379497
"unstaged_files": context.git_summary.unstaged_files,
94389498
"untracked_files": context.git_summary.untracked_files,
@@ -13907,10 +13967,11 @@ mod tests {
1390713967
run_resume_command, short_tool_id, slash_command_completion_candidates_with_sessions,
1390813968
split_error_hint, status_context, status_json_value, summarize_tool_payload_for_markdown,
1390913969
try_resolve_bare_skill_prompt, validate_no_args, write_mcp_server_fixture, CliAction,
13910-
CliOutputFormat, CliToolExecutor, GitWorkspaceSummary, InternalPromptProgressEvent,
13911-
InternalPromptProgressState, LiveCli, LocalHelpTopic, PermissionModeProvenance,
13912-
PromptHistoryEntry, SessionLifecycleKind, SessionLifecycleSummary, SlashCommand,
13913-
StatusUsage, TmuxPaneSnapshot, DEFAULT_MODEL, LATEST_SESSION_REFERENCE, STUB_COMMANDS,
13970+
CliOutputFormat, CliToolExecutor, GitOperation, GitWorkspaceSummary,
13971+
InternalPromptProgressEvent, InternalPromptProgressState, LiveCli, LocalHelpTopic,
13972+
PermissionModeProvenance, PromptHistoryEntry, SessionLifecycleKind,
13973+
SessionLifecycleSummary, SlashCommand, StatusUsage, TmuxPaneSnapshot, DEFAULT_MODEL,
13974+
LATEST_SESSION_REFERENCE, STUB_COMMANDS,
1391413975
};
1391513976
use api::{ApiError, MessageResponse, OutputContentBlock, Usage};
1391613977
use plugins::{
@@ -17250,6 +17311,7 @@ mod tests {
1725017311
unstaged_files: 1,
1725117312
untracked_files: 1,
1725217313
conflicted_files: 0,
17314+
operation: GitOperation::None,
1725317315
},
1725417316
branch_freshness: test_branch_freshness(),
1725517317
stale_base_state: super::BaseCommitState::NoExpectedBase,
@@ -17621,6 +17683,7 @@ mod tests {
1762117683
unstaged_files: 1,
1762217684
untracked_files: 0,
1762317685
conflicted_files: 0,
17686+
operation: GitOperation::None,
1762417687
};
1762517688

1762617689
let preflight = format_commit_preflight_report(Some("feature/ux"), summary);
@@ -17744,6 +17807,7 @@ UU conflicted.rs",
1774417807
unstaged_files: 2,
1774517808
untracked_files: 1,
1774617809
conflicted_files: 1,
17810+
operation: GitOperation::None,
1774717811
}
1774817812
);
1774917813
assert_eq!(

0 commit comments

Comments
 (0)