Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ homepage = "https://github.com/ScriptedAlchemy/tracedecay"
readme = "README.md"
keywords = ["code-intelligence", "knowledge-graph", "mcp", "tree-sitter", "claude"]
categories = ["development-tools", "command-line-utilities"]

[workspace]
exclude = [".worktrees", ".codex-worktrees"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep package include in the package table

Placing [workspace] here changes the scope of the subsequent include = [...], so Cargo interprets it as the unsupported workspace.include key and the package loses its explicit whitelist. As a result, cargo package omits the gitignored dashboard dist assets that the whitelist deliberately ships, leaving published builds to attempt an npm rebuild or fail when npm/assets are unavailable; move the workspace table below the package-specific keys.

Useful? React with 👍 / 👎.


# Explicit whitelist so `cargo package`/`cargo publish` ship everything the
# build needs — including the PREBUILT dashboard dist bundles, which are
# gitignored (an `exclude`-style package can never pick them up). Run
Expand Down
19 changes: 19 additions & 0 deletions src/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,9 @@ pub(crate) fn git_capture_at(repo_root: &Path, args: &[&str]) -> GitCaptureAtRes

fn git_command_at(repo_root: &Path, args: &[&str]) -> Command {
let mut command = Command::new(git_program());
command.env_remove("GIT_DIR");
command.env_remove("GIT_WORK_TREE");
command.env_remove("GIT_COMMON_DIR");
command.arg("-C").arg(repo_root).args(args);
command
}
Expand Down Expand Up @@ -266,6 +269,22 @@ mod tests {
);
}

#[test]
fn git_at_command_clears_repository_selection_overrides() {
let command = git_command_at(Path::new("/problematic/project/root"), &["status"]);

for key in ["GIT_DIR", "GIT_WORK_TREE", "GIT_COMMON_DIR"] {
assert_eq!(
command
.get_envs()
.find(|(candidate, _)| *candidate == OsStr::new(key))
.map(|(_, value)| value),
Some(None),
"git -C must resolve the supplied root rather than inherited {key}"
);
}
}

#[cfg(unix)]
#[test]
fn git_capture_deadline_kills_and_reaps_child() {
Expand Down
42 changes: 30 additions & 12 deletions src/sessions/cline_like.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ use serde_json::{Map, Value};

use crate::sessions::SessionMessageRecord;
use crate::sessions::shared::{
StoredCursor, TranscriptLocation, TranscriptLocationMetadataKeys, append_location_metadata,
append_tool_calls_metadata, append_usage_metadata, content_storage_text_and_tools,
path_belongs_to_project, title_from_messages,
ProjectMembership, ProjectRootMatcherCache, StoredCursor, TranscriptLocation,
TranscriptLocationMetadataKeys, append_location_metadata, append_tool_calls_metadata,
append_usage_metadata, content_storage_text_and_tools, title_from_messages,
};
use crate::sessions::source::{
ParsedTranscript, SessionDraft, TranscriptSource, read_changed_with_companion,
Expand All @@ -46,6 +46,7 @@ pub struct ClineLikeSource {
provider: &'static str,
storage_roots: Vec<PathBuf>,
user_registered_roots: Option<Vec<PathBuf>>,
project_matchers: ProjectRootMatcherCache,
}

impl ClineLikeSource {
Expand Down Expand Up @@ -78,6 +79,7 @@ impl ClineLikeSource {
.join("User/globalStorage/saoudrizwan.claude-dev/tasks"),
],
user_registered_roots: None,
project_matchers: ProjectRootMatcherCache::default(),
}
}

Expand All @@ -89,6 +91,7 @@ impl ClineLikeSource {
.join("User/globalStorage/rooveterinaryinc.roo-cline/tasks"),
],
user_registered_roots: None,
project_matchers: ProjectRootMatcherCache::default(),
}
}

Expand All @@ -101,6 +104,7 @@ impl ClineLikeSource {
home.join(".kilocode/cli/global/tasks"),
],
user_registered_roots: None,
project_matchers: ProjectRootMatcherCache::default(),
}
}

Expand Down Expand Up @@ -137,15 +141,15 @@ impl TranscriptSource for ClineLikeSource {
let metadata = read_task_metadata(task_dir)?;
let location_cwd = if let Some(roots) = &self.user_registered_roots {
let paths = metadata_project_paths(&metadata);
if paths
.iter()
.any(|path| roots.iter().any(|root| path_belongs_to_project(path, root)))
{
if paths.iter().any(|path| {
self.project_matchers.membership_against_roots(path, roots)
!= ProjectMembership::NoMatch
}) {
return None;
}
paths.into_iter().next()?
} else {
metadata_project_location(&metadata, project_root)?
metadata_project_location(&metadata, project_root, &self.project_matchers)?
};

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

fn metadata_project_location(metadata: &Value, project_root: &Path) -> Option<PathBuf> {
metadata_project_paths(metadata)
.into_iter()
.find(|path| path_belongs_to_project(path, project_root))
fn metadata_project_location(
metadata: &Value,
project_root: &Path,
project_matchers: &ProjectRootMatcherCache,
) -> Option<PathBuf> {
let mut matched = None;
for path in metadata_project_paths(metadata) {
match project_matchers.membership(&path, project_root) {
ProjectMembership::Match => {
if matched.is_none() {
matched = Some(path);
}
}
ProjectMembership::NoMatch => {}
ProjectMembership::Unknown => return None,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Let a definitive Cline path match override unknown paths

When Cline metadata contains both a path that definitively belongs to project_root and another stale or network-backed path whose Git lookup times out, this branch returns None and discards the already-recorded match. Such multi-path metadata previously used any matching path, so a consistently inaccessible auxiliary path can now prevent a valid task from ever being ingested; accumulate Unknown and defer only when no path produced Match.

Useful? React with 👍 / 👎.

};
}
matched
}

fn metadata_project_paths(value: &Value) -> Vec<PathBuf> {
Expand Down
53 changes: 30 additions & 23 deletions src/sessions/cursor_composer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ use libsql::{Builder, OpenFlags};
use serde_json::{Value, json};

use crate::global_db::{GlobalDb, ParseOffset};
use crate::sessions::shared::path_belongs_to_project;
use crate::sessions::shared::{ProjectMembership, ProjectRootMatcherCache};
use crate::sessions::{SessionMessageRecord, SessionRecord};

/// `SQLITE_OPEN_URI` — not exposed by libsql's [`OpenFlags`], so we OR the raw
Expand Down Expand Up @@ -84,6 +84,7 @@ impl CursorComposerSweepOutcome {
pub struct CursorComposerSource {
state_db_path: PathBuf,
chats_dir: PathBuf,
project_matchers: ProjectRootMatcherCache,
}

impl CursorComposerSource {
Expand All @@ -104,6 +105,7 @@ impl CursorComposerSource {
.join("globalStorage")
.join("state.vscdb"),
chats_dir: home.join(".cursor").join("chats"),
project_matchers: ProjectRootMatcherCache::default(),
}
}

Expand Down Expand Up @@ -209,20 +211,23 @@ impl CursorComposerSource {
.or_insert_with(|| project.path.clone());
}
let selected_project = match project_root {
Some(root) if path_belongs_to_project(Path::new(&project.path), root) => {
ComposerProject {
Some(root) => match self
.project_matchers
.membership(Path::new(&project.path), root)
{
ProjectMembership::Match => ComposerProject {
path: project.path.clone(),
}
}
Some(_) => continue,
None if registered_roots
.iter()
.any(|root| path_belongs_to_project(Path::new(&project.path), root)) =>
},
ProjectMembership::NoMatch | ProjectMembership::Unknown => continue,
},
None => match self
.project_matchers
.membership_against_roots(Path::new(&project.path), registered_roots)
{
continue;
}
None => ComposerProject {
path: "user".to_string(),
ProjectMembership::NoMatch => ComposerProject {
path: "user".to_string(),
},
ProjectMembership::Match | ProjectMembership::Unknown => continue,
},
};
// Own this session for JSONL dedupe regardless of the per-pass cap.
Expand Down Expand Up @@ -322,18 +327,20 @@ impl CursorComposerSource {
let ws_hash = ws_entry.file_name().to_string_lossy().to_string();
// Scope by ws-hash -> project mapping harvested from the envelopes.
let project_path = match (workspace_paths.get(&ws_hash), project_root) {
(Some(path), Some(root)) if path_belongs_to_project(Path::new(path), root) => {
path.clone()
(Some(path), Some(root)) => {
match self.project_matchers.membership(Path::new(path), root) {
ProjectMembership::Match => path.clone(),
ProjectMembership::NoMatch | ProjectMembership::Unknown => continue,
}
}
(Some(_), Some(_)) | (None, _) => continue,
(Some(path), None)
if registered_roots
.iter()
.any(|root| path_belongs_to_project(Path::new(path), root)) =>
(None, _) => continue,
(Some(path), None) => match self
.project_matchers
.membership_against_roots(Path::new(path), registered_roots)
{
continue;
}
(Some(_), None) => "user".to_string(),
ProjectMembership::NoMatch => "user".to_string(),
ProjectMembership::Match | ProjectMembership::Unknown => continue,
},
};
let Ok(agent_entries) = std::fs::read_dir(ws_entry.path()) else {
continue;
Expand Down
49 changes: 33 additions & 16 deletions src/sessions/kiro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ use serde_json::Value;

use crate::sessions::SessionMessageRecord;
use crate::sessions::shared::{
StoredCursor, TranscriptIngestStats, TranscriptLocation, TranscriptLocationMetadataKeys,
append_location_metadata, append_tool_calls_metadata, append_usage_metadata,
content_storage_text_and_tools, path_belongs_to_project, title_from_messages,
ProjectMembership, ProjectRootMatcherCache, StoredCursor, TranscriptIngestStats,
TranscriptLocation, TranscriptLocationMetadataKeys, append_location_metadata,
append_tool_calls_metadata, append_usage_metadata, content_storage_text_and_tools,
title_from_messages,
};
use crate::sessions::source::{
ParsedTranscript, SessionDraft, TranscriptSource, collect_files_with_ext, read_changed_file,
Expand All @@ -49,6 +50,7 @@ pub struct KiroSource {
agent_dir: PathBuf,
workspace_storage_dir: PathBuf,
user_registered_roots: Option<Vec<PathBuf>>,
project_matchers: ProjectRootMatcherCache,
}

impl KiroSource {
Expand All @@ -66,6 +68,7 @@ impl KiroSource {
agent_dir: data_dir.join("User/globalStorage/kiro.kiroagent"),
workspace_storage_dir: data_dir.join("User/workspaceStorage"),
user_registered_roots: None,
project_matchers: ProjectRootMatcherCache::default(),
}
}

Expand All @@ -86,23 +89,27 @@ impl TranscriptSource for KiroSource {
let mut out = collect_user_workspace_session_files(
&self.agent_dir.join("workspace-sessions"),
registered_roots,
&self.project_matchers,
);
out.extend(collect_user_agent_storage_files(
&self.agent_dir,
&self.workspace_storage_dir,
registered_roots,
&self.project_matchers,
));
return out;
}
let mut out = Vec::new();
out.extend(collect_workspace_session_files(
&self.agent_dir.join("workspace-sessions"),
project_root,
&self.project_matchers,
));
out.extend(collect_agent_storage_files(
&self.agent_dir,
&self.workspace_storage_dir,
project_root,
&self.project_matchers,
));
out
}
Expand All @@ -116,13 +123,18 @@ impl TranscriptSource for KiroSource {
) -> Option<ParsedTranscript> {
let location_cwd = transcript_location_path(path, &self.workspace_storage_dir)?;
if let Some(roots) = &self.user_registered_roots {
if roots
.iter()
.any(|root| path_belongs_to_project(&location_cwd, root))
if self
.project_matchers
.membership_against_roots(&location_cwd, roots)
!= ProjectMembership::NoMatch
{
return None;
}
} else if !path_belongs_to_project(&location_cwd, project_root) {
} else if self
.project_matchers
.membership(&location_cwd, project_root)
!= ProjectMembership::Match
{
return None;
}

Expand Down Expand Up @@ -187,6 +199,7 @@ impl TranscriptSource for KiroSource {
fn collect_user_workspace_session_files(
sessions_root: &Path,
registered_roots: &[PathBuf],
project_matchers: &ProjectRootMatcherCache,
) -> Vec<PathBuf> {
let Ok(entries) = std::fs::read_dir(sessions_root) else {
return Vec::new();
Expand All @@ -200,9 +213,8 @@ fn collect_user_workspace_session_files(
}
let workspace =
decode_workspace_sessions_dir(entry.file_name().to_string_lossy().as_ref())?;
if registered_roots
.iter()
.any(|root| path_belongs_to_project(&workspace, root))
if project_matchers.membership_against_roots(&workspace, registered_roots)
!= ProjectMembership::NoMatch
{
return None;
}
Expand Down Expand Up @@ -273,7 +285,11 @@ fn empty_changed_transcript(
}
}

fn collect_workspace_session_files(sessions_root: &Path, project_root: &Path) -> Vec<PathBuf> {
fn collect_workspace_session_files(
sessions_root: &Path,
project_root: &Path,
project_matchers: &ProjectRootMatcherCache,
) -> Vec<PathBuf> {
let Ok(entries) = std::fs::read_dir(sessions_root) else {
return Vec::new();
};
Expand All @@ -288,7 +304,7 @@ fn collect_workspace_session_files(sessions_root: &Path, project_root: &Path) ->
else {
continue;
};
if !path_belongs_to_project(&workspace, project_root) {
if project_matchers.membership(&workspace, project_root) != ProjectMembership::Match {
continue;
}
let Ok(session_entries) = std::fs::read_dir(&encoded_dir) else {
Expand All @@ -308,6 +324,7 @@ fn collect_agent_storage_files(
agent_dir: &Path,
workspace_storage_dir: &Path,
project_root: &Path,
project_matchers: &ProjectRootMatcherCache,
) -> Vec<PathBuf> {
let mut workspace_dirs: Vec<(u64, PathBuf, PathBuf)> = Vec::new();
let Ok(entries) = std::fs::read_dir(agent_dir) else {
Expand All @@ -326,7 +343,7 @@ fn collect_agent_storage_files(
let Some(workspace) = workspace_path_from_hash(workspace_storage_dir, &name) else {
continue;
};
if !path_belongs_to_project(&workspace, project_root) {
if project_matchers.membership(&workspace, project_root) != ProjectMembership::Match {
continue;
}
let mtime = entry
Expand Down Expand Up @@ -356,6 +373,7 @@ fn collect_user_agent_storage_files(
agent_dir: &Path,
workspace_storage_dir: &Path,
registered_roots: &[PathBuf],
project_matchers: &ProjectRootMatcherCache,
) -> Vec<PathBuf> {
let Ok(entries) = std::fs::read_dir(agent_dir) else {
return Vec::new();
Expand All @@ -374,9 +392,8 @@ fn collect_user_agent_storage_files(
return None;
}
let workspace = workspace_path_from_hash(workspace_storage_dir, &name)?;
if registered_roots
.iter()
.any(|root| path_belongs_to_project(&workspace, root))
if project_matchers.membership_against_roots(&workspace, registered_roots)
!= ProjectMembership::NoMatch
{
return None;
}
Expand Down
Loading
Loading