Skip to content

Commit 4048405

Browse files
Merge remote-tracking branch 'origin/master' into codex/commitlint-hook
2 parents b499d2a + 495ba7d commit 4048405

13 files changed

Lines changed: 474 additions & 82 deletions

File tree

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ homepage = "https://github.com/ScriptedAlchemy/tracedecay"
99
readme = "README.md"
1010
keywords = ["code-intelligence", "knowledge-graph", "mcp", "tree-sitter", "claude"]
1111
categories = ["development-tools", "command-line-utilities"]
12+
1213
# Explicit whitelist so `cargo package`/`cargo publish` ship everything the
1314
# build needs — including the PREBUILT dashboard dist bundles, which are
1415
# gitignored (an `exclude`-style package can never pick them up). Run
@@ -40,6 +41,9 @@ include = [
4041
"/dashboard/hermes-wrapper/src/**",
4142
]
4243

44+
[workspace]
45+
exclude = [".worktrees", ".codex-worktrees"]
46+
4347
[features]
4448
default = ["full", "token-counting"]
4549

src/git.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,9 @@ pub(crate) fn git_capture_at(repo_root: &Path, args: &[&str]) -> GitCaptureAtRes
171171

172172
fn git_command_at(repo_root: &Path, args: &[&str]) -> Command {
173173
let mut command = Command::new(git_program());
174+
command.env_remove("GIT_DIR");
175+
command.env_remove("GIT_WORK_TREE");
176+
command.env_remove("GIT_COMMON_DIR");
174177
command.arg("-C").arg(repo_root).args(args);
175178
command
176179
}
@@ -266,6 +269,22 @@ mod tests {
266269
);
267270
}
268271

272+
#[test]
273+
fn git_at_command_clears_repository_selection_overrides() {
274+
let command = git_command_at(Path::new("/problematic/project/root"), &["status"]);
275+
276+
for key in ["GIT_DIR", "GIT_WORK_TREE", "GIT_COMMON_DIR"] {
277+
assert_eq!(
278+
command
279+
.get_envs()
280+
.find(|(candidate, _)| *candidate == OsStr::new(key))
281+
.map(|(_, value)| value),
282+
Some(None),
283+
"git -C must resolve the supplied root rather than inherited {key}"
284+
);
285+
}
286+
}
287+
269288
#[cfg(unix)]
270289
#[test]
271290
fn git_capture_deadline_kills_and_reaps_child() {

src/sessions/claude.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1379,7 +1379,7 @@ mod tests {
13791379
.ancestors()
13801380
.find(|ancestor| ancestor.file_name().is_some_and(|name| name == "repo"))
13811381
.unwrap_or(path);
1382-
if UNKNOWN_PATH_ATTEMPTS.fetch_add(1, Ordering::SeqCst) == 0 {
1382+
if UNKNOWN_PATH_ATTEMPTS.fetch_add(1, Ordering::SeqCst) == 1 {
13831383
return crate::worktree::GitRepoIdentityOutcome::Unknown;
13841384
}
13851385
crate::worktree::GitRepoIdentityOutcome::Resolved(crate::worktree::GitRepoIdentity {

src/sessions/cline_like.rs

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ use serde_json::{Map, Value};
2222

2323
use crate::sessions::SessionMessageRecord;
2424
use crate::sessions::shared::{
25-
StoredCursor, TranscriptLocation, TranscriptLocationMetadataKeys, append_location_metadata,
26-
append_tool_calls_metadata, append_usage_metadata, content_storage_text_and_tools,
27-
path_belongs_to_project, title_from_messages,
25+
ProjectMembership, ProjectRootMatcherCache, StoredCursor, TranscriptLocation,
26+
TranscriptLocationMetadataKeys, append_location_metadata, append_tool_calls_metadata,
27+
append_usage_metadata, content_storage_text_and_tools, title_from_messages,
2828
};
2929
use crate::sessions::source::{
3030
ParsedTranscript, SessionDraft, TranscriptSource, read_changed_with_companion,
@@ -46,6 +46,7 @@ pub struct ClineLikeSource {
4646
provider: &'static str,
4747
storage_roots: Vec<PathBuf>,
4848
user_registered_roots: Option<Vec<PathBuf>>,
49+
project_matchers: ProjectRootMatcherCache,
4950
}
5051

5152
impl ClineLikeSource {
@@ -78,6 +79,7 @@ impl ClineLikeSource {
7879
.join("User/globalStorage/saoudrizwan.claude-dev/tasks"),
7980
],
8081
user_registered_roots: None,
82+
project_matchers: ProjectRootMatcherCache::default(),
8183
}
8284
}
8385

@@ -89,6 +91,7 @@ impl ClineLikeSource {
8991
.join("User/globalStorage/rooveterinaryinc.roo-cline/tasks"),
9092
],
9193
user_registered_roots: None,
94+
project_matchers: ProjectRootMatcherCache::default(),
9295
}
9396
}
9497

@@ -101,6 +104,7 @@ impl ClineLikeSource {
101104
home.join(".kilocode/cli/global/tasks"),
102105
],
103106
user_registered_roots: None,
107+
project_matchers: ProjectRootMatcherCache::default(),
104108
}
105109
}
106110

@@ -137,15 +141,15 @@ impl TranscriptSource for ClineLikeSource {
137141
let metadata = read_task_metadata(task_dir)?;
138142
let location_cwd = if let Some(roots) = &self.user_registered_roots {
139143
let paths = metadata_project_paths(&metadata);
140-
if paths
141-
.iter()
142-
.any(|path| roots.iter().any(|root| path_belongs_to_project(path, root)))
143-
{
144+
if paths.iter().any(|path| {
145+
self.project_matchers.membership_against_roots(path, roots)
146+
!= ProjectMembership::NoMatch
147+
}) {
144148
return None;
145149
}
146150
paths.into_iter().next()?
147151
} else {
148-
metadata_project_location(&metadata, project_root)?
152+
metadata_project_location(&metadata, project_root, &self.project_matchers)?
149153
};
150154

151155
let document: Value = match serde_json::from_str(&changed.contents) {
@@ -310,10 +314,24 @@ fn read_task_metadata(task_dir: &Path) -> Option<Value> {
310314
None
311315
}
312316

313-
fn metadata_project_location(metadata: &Value, project_root: &Path) -> Option<PathBuf> {
314-
metadata_project_paths(metadata)
315-
.into_iter()
316-
.find(|path| path_belongs_to_project(path, project_root))
317+
fn metadata_project_location(
318+
metadata: &Value,
319+
project_root: &Path,
320+
project_matchers: &ProjectRootMatcherCache,
321+
) -> Option<PathBuf> {
322+
let mut matched = None;
323+
for path in metadata_project_paths(metadata) {
324+
match project_matchers.membership(&path, project_root) {
325+
ProjectMembership::Match => {
326+
if matched.is_none() {
327+
matched = Some(path);
328+
}
329+
}
330+
ProjectMembership::NoMatch => {}
331+
ProjectMembership::Unknown => return None,
332+
}
333+
}
334+
matched
317335
}
318336

319337
fn metadata_project_paths(value: &Value) -> Vec<PathBuf> {

src/sessions/codex.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1623,7 +1623,7 @@ mod source_matcher_cache_tests {
16231623
.ancestors()
16241624
.find(|ancestor| ancestor.file_name().is_some_and(|name| name == "repo"))
16251625
.unwrap_or(path);
1626-
if UNKNOWN_PATH_ATTEMPTS.fetch_add(1, Ordering::SeqCst) == 0 {
1626+
if UNKNOWN_PATH_ATTEMPTS.fetch_add(1, Ordering::SeqCst) == 1 {
16271627
return crate::worktree::GitRepoIdentityOutcome::Unknown;
16281628
}
16291629
crate::worktree::GitRepoIdentityOutcome::Resolved(crate::worktree::GitRepoIdentity {

src/sessions/cursor_composer.rs

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ use libsql::{Builder, OpenFlags};
4545
use serde_json::{Value, json};
4646

4747
use crate::global_db::{GlobalDb, ParseOffset};
48-
use crate::sessions::shared::path_belongs_to_project;
48+
use crate::sessions::shared::{ProjectMembership, ProjectRootMatcherCache};
4949
use crate::sessions::{SessionMessageRecord, SessionRecord};
5050

5151
/// `SQLITE_OPEN_URI` — not exposed by libsql's [`OpenFlags`], so we OR the raw
@@ -84,6 +84,7 @@ impl CursorComposerSweepOutcome {
8484
pub struct CursorComposerSource {
8585
state_db_path: PathBuf,
8686
chats_dir: PathBuf,
87+
project_matchers: ProjectRootMatcherCache,
8788
}
8889

8990
impl CursorComposerSource {
@@ -104,6 +105,7 @@ impl CursorComposerSource {
104105
.join("globalStorage")
105106
.join("state.vscdb"),
106107
chats_dir: home.join(".cursor").join("chats"),
108+
project_matchers: ProjectRootMatcherCache::default(),
107109
}
108110
}
109111

@@ -209,20 +211,23 @@ impl CursorComposerSource {
209211
.or_insert_with(|| project.path.clone());
210212
}
211213
let selected_project = match project_root {
212-
Some(root) if path_belongs_to_project(Path::new(&project.path), root) => {
213-
ComposerProject {
214+
Some(root) => match self
215+
.project_matchers
216+
.membership(Path::new(&project.path), root)
217+
{
218+
ProjectMembership::Match => ComposerProject {
214219
path: project.path.clone(),
215-
}
216-
}
217-
Some(_) => continue,
218-
None if registered_roots
219-
.iter()
220-
.any(|root| path_belongs_to_project(Path::new(&project.path), root)) =>
220+
},
221+
ProjectMembership::NoMatch | ProjectMembership::Unknown => continue,
222+
},
223+
None => match self
224+
.project_matchers
225+
.membership_against_roots(Path::new(&project.path), registered_roots)
221226
{
222-
continue;
223-
}
224-
None => ComposerProject {
225-
path: "user".to_string(),
227+
ProjectMembership::NoMatch => ComposerProject {
228+
path: "user".to_string(),
229+
},
230+
ProjectMembership::Match | ProjectMembership::Unknown => continue,
226231
},
227232
};
228233
// Own this session for JSONL dedupe regardless of the per-pass cap.
@@ -322,18 +327,20 @@ impl CursorComposerSource {
322327
let ws_hash = ws_entry.file_name().to_string_lossy().to_string();
323328
// Scope by ws-hash -> project mapping harvested from the envelopes.
324329
let project_path = match (workspace_paths.get(&ws_hash), project_root) {
325-
(Some(path), Some(root)) if path_belongs_to_project(Path::new(path), root) => {
326-
path.clone()
330+
(Some(path), Some(root)) => {
331+
match self.project_matchers.membership(Path::new(path), root) {
332+
ProjectMembership::Match => path.clone(),
333+
ProjectMembership::NoMatch | ProjectMembership::Unknown => continue,
334+
}
327335
}
328-
(Some(_), Some(_)) | (None, _) => continue,
329-
(Some(path), None)
330-
if registered_roots
331-
.iter()
332-
.any(|root| path_belongs_to_project(Path::new(path), root)) =>
336+
(None, _) => continue,
337+
(Some(path), None) => match self
338+
.project_matchers
339+
.membership_against_roots(Path::new(path), registered_roots)
333340
{
334-
continue;
335-
}
336-
(Some(_), None) => "user".to_string(),
341+
ProjectMembership::NoMatch => "user".to_string(),
342+
ProjectMembership::Match | ProjectMembership::Unknown => continue,
343+
},
337344
};
338345
let Ok(agent_entries) = std::fs::read_dir(ws_entry.path()) else {
339346
continue;

0 commit comments

Comments
 (0)