Skip to content

Commit d1213d3

Browse files
authored
runtime: Use worktree-aware Git state resolution for mutation scopes
Ensure OpenCode mutation-scope state is stored in the Git directory belonging to the tool's working tree, rather than assuming the main checkout's state location. Add linked-worktree coverage and update the Codex fixture notes to document the shared runtime resolver.
1 parent 672c399 commit d1213d3

2 files changed

Lines changed: 71 additions & 7 deletions

File tree

  • cli/src/services/hooks

‎cli/src/services/hooks/codex_mutation_scope/fixtures/NOTES.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ probe 11 : … → PreToolUse(Bash) → Interrupt → SessionEnd ← still no
251251
- **D15 — raw hook `cwd` is authoritative:** `PROVEN`. Every payload's `cwd` was
252252
the `codex exec -C` directory; running against a linked `git worktree` (probe
253253
10) reported the worktree path in `cwd`, and the write landed in the worktree,
254-
not the main checkout. `checkout::resolve_git_dir(cwd)` resolves the
254+
not the main checkout. `runtime::resolve_git_dir(cwd)` resolves the
255255
worktree-specific `.git/worktrees/<name>` directory. Codex exposes **no**
256256
worktree-lifecycle event (no `WorktreeRemove` equivalent among the 12 event
257257
names); worktree-scoped cleanup relies on the `SessionEnd` / `Interrupt` /

‎cli/src/services/hooks/opencode_mutation_scope/mod.rs‎

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ use std::path::{Path, PathBuf};
99
use anyhow::{anyhow, bail, Context, Result};
1010
use serde_json::{json, Map, Value};
1111

12-
use crate::services::checkout;
1312
use crate::services::hooks::{
1413
normalize_opencode_model_id, prefixed_diff_trace_session_id, OPENCODE_TOOL_NAME,
1514
};
15+
use crate::services::mutation_trace::runtime::resolve_git_dir;
1616
use crate::services::observability::traits::Logger;
1717

1818
use boundary_lock::{AdapterBoundaryLock, DEFAULT_BOUNDARY_LOCK_TIMEOUT};
@@ -339,7 +339,7 @@ pub(crate) fn run_opencode_mutation_scope_from_payload(
339339
stdin_payload: &str,
340340
logger: Option<&dyn Logger>,
341341
) -> Result<String> {
342-
let resolve_git_dir_fn = |cwd: &str| checkout::resolve_git_dir(Path::new(cwd));
342+
let resolve_git_dir_fn = |cwd: &str| resolve_git_dir(Path::new(cwd));
343343
let seam_fn = |repository_root: &Path, payload: &str, logger: Option<&dyn Logger>| {
344344
super::mutation_scope::run_mutation_scope_from_payload(repository_root, payload, logger)
345345
};
@@ -358,7 +358,7 @@ pub(crate) fn run_opencode_mutation_scope_from_payload_at_state_root(
358358
stdin_payload: &str,
359359
logger: Option<&dyn Logger>,
360360
) -> Result<String> {
361-
let resolve_git_dir_fn = |cwd: &str| checkout::resolve_git_dir(Path::new(cwd));
361+
let resolve_git_dir_fn = |cwd: &str| resolve_git_dir(Path::new(cwd));
362362
let seam_fn = |repository_root: &Path, payload: &str, logger: Option<&dyn Logger>| {
363363
super::mutation_scope::run_mutation_scope_from_payload_at_state_root(
364364
repository_root,
@@ -2126,7 +2126,7 @@ mod runtime_seam_tests {
21262126
use crate::services::agent_trace_storage::{
21272127
resolve_agent_trace_storage_at_state_root, AgentTraceStorageContext,
21282128
};
2129-
use crate::services::checkout::resolve_git_dir;
2129+
use crate::services::mutation_trace::runtime::resolve_git_dir;
21302130

21312131
use super::state::read_state;
21322132
use super::*;
@@ -2145,7 +2145,7 @@ mod runtime_seam_tests {
21452145
}
21462146

21472147
struct OpenCodeRepo {
2148-
_temp: tempfile::TempDir,
2148+
temp: tempfile::TempDir,
21492149
root: PathBuf,
21502150
state_root: PathBuf,
21512151
}
@@ -2182,7 +2182,7 @@ mod runtime_seam_tests {
21822182
.expect("state-root storage should initialize the repository DB");
21832183

21842184
Self {
2185-
_temp: temp,
2185+
temp,
21862186
root,
21872187
state_root,
21882188
}
@@ -2353,6 +2353,70 @@ mod runtime_seam_tests {
23532353
);
23542354
}
23552355

2356+
#[test]
2357+
fn linked_worktree_uses_its_worktree_specific_git_dir_for_mutation_scope_state() {
2358+
let repo = OpenCodeRepo::new("linked-worktree");
2359+
let linked_root = repo.temp.path().join("linked");
2360+
git(
2361+
&repo.root,
2362+
&[
2363+
"worktree",
2364+
"add",
2365+
"--quiet",
2366+
linked_root
2367+
.to_str()
2368+
.expect("linked worktree path should be UTF-8"),
2369+
],
2370+
);
2371+
2372+
let main_git_dir = resolve_git_dir(&repo.root).expect("main git dir should resolve");
2373+
let linked_git_dir = resolve_git_dir(&linked_root).expect("linked git dir should resolve");
2374+
assert_ne!(
2375+
main_git_dir, linked_git_dir,
2376+
"linked worktrees must use distinct Git state locations",
2377+
);
2378+
2379+
let before = json!({
2380+
"hook_event_name": "ToolExecuteBefore",
2381+
"session_id": "ses_linked",
2382+
"call_id": "call_linked",
2383+
"cwd": linked_root,
2384+
"tool_name": "write",
2385+
"model": "opencode/big-pickle",
2386+
})
2387+
.to_string();
2388+
run_opencode_mutation_scope_from_payload_at_state_root(&repo.state_root, &before, None)
2389+
.expect("linked-worktree Start");
2390+
2391+
let scope_id = read_state(&linked_git_dir)
2392+
.expect("linked adapter state should be readable")
2393+
.attempts[0]
2394+
.scope_id
2395+
.clone();
2396+
fs::write(linked_root.join("file.txt"), "one\nlinked\n")
2397+
.expect("linked worktree file should be writable");
2398+
2399+
let after = json!({
2400+
"hook_event_name": "ToolExecuteAfter",
2401+
"session_id": "ses_linked",
2402+
"call_id": "call_linked",
2403+
"cwd": linked_root,
2404+
"tool_name": "write",
2405+
})
2406+
.to_string();
2407+
run_opencode_mutation_scope_from_payload_at_state_root(&repo.state_root, &after, None)
2408+
.expect("linked-worktree Close");
2409+
2410+
assert!(read_state(&linked_git_dir)
2411+
.expect("linked adapter state should be readable")
2412+
.attempts
2413+
.is_empty());
2414+
assert_eq!(
2415+
repo.scope_status(&scope_id),
2416+
Some(("opencode".to_string(), "closed".to_string())),
2417+
);
2418+
}
2419+
23562420
#[test]
23572421
fn a_tool_error_abandons_the_scope_through_the_real_runtime() {
23582422
let repo = OpenCodeRepo::new("tool-error-abandon");

0 commit comments

Comments
 (0)