mirror of
https://github.com/instructkr/claude-code.git
synced 2026-06-05 12:06:43 +00:00
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>
This commit is contained in:
@@ -1760,7 +1760,7 @@ Original filing (2026-04-13): user requested a `-acp` parameter to support ACP p
|
|||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
**Concrete repro.**
|
**Concrete repro.**
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -5729,6 +5729,31 @@ struct GitWorkspaceSummary {
|
|||||||
unstaged_files: usize,
|
unstaged_files: usize,
|
||||||
untracked_files: usize,
|
untracked_files: usize,
|
||||||
conflicted_files: usize,
|
conflicted_files: usize,
|
||||||
|
/// #89: detected mid-operation git state (rebase, merge, cherry-pick, bisect)
|
||||||
|
operation: GitOperation,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// #89: mid-operation git states detected from branch header in `git status --short --branch`.
|
||||||
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||||
|
enum GitOperation {
|
||||||
|
#[default]
|
||||||
|
None,
|
||||||
|
Rebase,
|
||||||
|
Merge,
|
||||||
|
CherryPick,
|
||||||
|
Bisect,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GitOperation {
|
||||||
|
fn as_str(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::None => "",
|
||||||
|
Self::Rebase => "rebase-in-progress",
|
||||||
|
Self::Merge => "merge-in-progress",
|
||||||
|
Self::CherryPick => "cherry-pick-in-progress",
|
||||||
|
Self::Bisect => "bisect-in-progress",
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -5806,8 +5831,18 @@ impl GitWorkspaceSummary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn headline(self) -> String {
|
fn headline(self) -> String {
|
||||||
|
// #89: prefix with operation state when mid-operation
|
||||||
|
let op_prefix = if self.operation != GitOperation::None {
|
||||||
|
format!("{}, ", self.operation.as_str())
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
if self.is_clean() {
|
if self.is_clean() {
|
||||||
"clean".to_string()
|
if self.operation != GitOperation::None {
|
||||||
|
format!("{op_prefix}clean")
|
||||||
|
} else {
|
||||||
|
"clean".to_string()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let mut details = Vec::new();
|
let mut details = Vec::new();
|
||||||
if self.staged_files > 0 {
|
if self.staged_files > 0 {
|
||||||
@@ -5823,7 +5858,7 @@ impl GitWorkspaceSummary {
|
|||||||
details.push(format!("{} conflicted", self.conflicted_files));
|
details.push(format!("{} conflicted", self.conflicted_files));
|
||||||
}
|
}
|
||||||
format!(
|
format!(
|
||||||
"dirty · {} files · {}",
|
"{op_prefix}dirty · {} files · {}",
|
||||||
self.changed_files,
|
self.changed_files,
|
||||||
details.join(", ")
|
details.join(", ")
|
||||||
)
|
)
|
||||||
@@ -6129,7 +6164,26 @@ fn parse_git_workspace_summary(status: Option<&str>) -> GitWorkspaceSummary {
|
|||||||
};
|
};
|
||||||
|
|
||||||
for line in status.lines() {
|
for line in status.lines() {
|
||||||
if line.starts_with("## ") || line.trim().is_empty() {
|
if line.starts_with("## ") {
|
||||||
|
// #89: detect mid-operation states from branch header
|
||||||
|
// git status --short --branch shows:
|
||||||
|
// "## HEAD (no branch, rebasing feature-branch)"
|
||||||
|
// "## main [merge-in-progress]"
|
||||||
|
// "## HEAD (no branch, cherry-pick-in-progress)"
|
||||||
|
// "## main (no branch, bisect-in-progress)"
|
||||||
|
let header = line.to_ascii_lowercase();
|
||||||
|
if header.contains("rebasing") {
|
||||||
|
summary.operation = GitOperation::Rebase;
|
||||||
|
} else if header.contains("merge-in-progress") {
|
||||||
|
summary.operation = GitOperation::Merge;
|
||||||
|
} else if header.contains("cherry-pick-in-progress") {
|
||||||
|
summary.operation = GitOperation::CherryPick;
|
||||||
|
} else if header.contains("bisect-in-progress") {
|
||||||
|
summary.operation = GitOperation::Bisect;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if line.trim().is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9433,6 +9487,12 @@ fn status_json_value(
|
|||||||
"changed_files": context.git_summary.changed_files,
|
"changed_files": context.git_summary.changed_files,
|
||||||
"is_clean": context.git_summary.changed_files == 0,
|
"is_clean": context.git_summary.changed_files == 0,
|
||||||
"staged_files": context.git_summary.staged_files,
|
"staged_files": context.git_summary.staged_files,
|
||||||
|
// #89: mid-operation git state (rebase, merge, cherry-pick, bisect)
|
||||||
|
"git_operation": if context.git_summary.operation != GitOperation::None {
|
||||||
|
Some(context.git_summary.operation.as_str())
|
||||||
|
} else {
|
||||||
|
None::<&str>
|
||||||
|
},
|
||||||
|
|
||||||
"unstaged_files": context.git_summary.unstaged_files,
|
"unstaged_files": context.git_summary.unstaged_files,
|
||||||
"untracked_files": context.git_summary.untracked_files,
|
"untracked_files": context.git_summary.untracked_files,
|
||||||
@@ -13907,10 +13967,11 @@ mod tests {
|
|||||||
run_resume_command, short_tool_id, slash_command_completion_candidates_with_sessions,
|
run_resume_command, short_tool_id, slash_command_completion_candidates_with_sessions,
|
||||||
split_error_hint, status_context, status_json_value, summarize_tool_payload_for_markdown,
|
split_error_hint, status_context, status_json_value, summarize_tool_payload_for_markdown,
|
||||||
try_resolve_bare_skill_prompt, validate_no_args, write_mcp_server_fixture, CliAction,
|
try_resolve_bare_skill_prompt, validate_no_args, write_mcp_server_fixture, CliAction,
|
||||||
CliOutputFormat, CliToolExecutor, GitWorkspaceSummary, InternalPromptProgressEvent,
|
CliOutputFormat, CliToolExecutor, GitOperation, GitWorkspaceSummary,
|
||||||
InternalPromptProgressState, LiveCli, LocalHelpTopic, PermissionModeProvenance,
|
InternalPromptProgressEvent, InternalPromptProgressState, LiveCli, LocalHelpTopic,
|
||||||
PromptHistoryEntry, SessionLifecycleKind, SessionLifecycleSummary, SlashCommand,
|
PermissionModeProvenance, PromptHistoryEntry, SessionLifecycleKind,
|
||||||
StatusUsage, TmuxPaneSnapshot, DEFAULT_MODEL, LATEST_SESSION_REFERENCE, STUB_COMMANDS,
|
SessionLifecycleSummary, SlashCommand, StatusUsage, TmuxPaneSnapshot, DEFAULT_MODEL,
|
||||||
|
LATEST_SESSION_REFERENCE, STUB_COMMANDS,
|
||||||
};
|
};
|
||||||
use api::{ApiError, MessageResponse, OutputContentBlock, Usage};
|
use api::{ApiError, MessageResponse, OutputContentBlock, Usage};
|
||||||
use plugins::{
|
use plugins::{
|
||||||
@@ -17250,6 +17311,7 @@ mod tests {
|
|||||||
unstaged_files: 1,
|
unstaged_files: 1,
|
||||||
untracked_files: 1,
|
untracked_files: 1,
|
||||||
conflicted_files: 0,
|
conflicted_files: 0,
|
||||||
|
operation: GitOperation::None,
|
||||||
},
|
},
|
||||||
branch_freshness: test_branch_freshness(),
|
branch_freshness: test_branch_freshness(),
|
||||||
stale_base_state: super::BaseCommitState::NoExpectedBase,
|
stale_base_state: super::BaseCommitState::NoExpectedBase,
|
||||||
@@ -17621,6 +17683,7 @@ mod tests {
|
|||||||
unstaged_files: 1,
|
unstaged_files: 1,
|
||||||
untracked_files: 0,
|
untracked_files: 0,
|
||||||
conflicted_files: 0,
|
conflicted_files: 0,
|
||||||
|
operation: GitOperation::None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let preflight = format_commit_preflight_report(Some("feature/ux"), summary);
|
let preflight = format_commit_preflight_report(Some("feature/ux"), summary);
|
||||||
@@ -17744,6 +17807,7 @@ UU conflicted.rs",
|
|||||||
unstaged_files: 2,
|
unstaged_files: 2,
|
||||||
untracked_files: 1,
|
untracked_files: 1,
|
||||||
conflicted_files: 1,
|
conflicted_files: 1,
|
||||||
|
operation: GitOperation::None,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
Reference in New Issue
Block a user