Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,20 @@ hand.
and `--no-sync` opts out. This is the piece that makes the cache
an implementation detail: a new user can run `path query` with no
setup and get their sessions.
- `--parent-dir <dir>` / `-d` on `p cache sync` and `path query`
restricts ingestion (and the query's reads) to artifacts under a
directory. Stat gate first, always: unchanged+cached artifacts skip
before any scope check; only artifacts that would cost a derive get
the constraint, with a one-line peek for codex and copilot (whose
cwd lives inside the session file) memoized into the manifest so it
happens at most once per artifact — and a derive never clobbers the
memoized peeked cwd. The copilot peek tolerates `session.start`
anywhere in the first lines and top-level cwd keys. Claude project
matching happens in slug space (its dir slugs are lossy), and pi
project scoping compares in its dir-encoded space so hyphenated
paths match. Out-of-scope artifacts are noted in the manifest
("known, not materialized") but not derived, tallied separately
(`N out of scope`), and never touch a materialized record's stamp.
- **`toolpath-codex`** (0.6.1, extends the bump below):
`session_id_from_stem` is public — the trailing UUID of a rollout
filename stem (or the whole stem when it has none), the same
Expand Down
6 changes: 4 additions & 2 deletions CLAUDE.md

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion crates/path-cli/src/cmd_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

use anyhow::Result;
use clap::Subcommand;
#[cfg(not(target_os = "emscripten"))]
use std::path::PathBuf;

use crate::cache::{list_cached, remove_cached};

Expand All @@ -22,6 +24,12 @@ pub enum CacheOp {
/// Artifact types to sync (default: every agent harness)
#[arg(value_enum)]
types: Vec<crate::artifact::ArtifactType>,

/// Only ingest artifacts living under this directory (subtree
/// match). Out-of-scope artifacts are noted in the manifest but
/// not derived.
#[arg(long, short = 'd')]
parent_dir: Option<PathBuf>,
},
}

Expand All @@ -30,7 +38,7 @@ pub fn run(op: CacheOp) -> Result<()> {
CacheOp::Ls => run_ls(),
CacheOp::Rm { id } => run_rm(&id),
#[cfg(not(target_os = "emscripten"))]
CacheOp::Sync { types } => crate::sync::run(types),
CacheOp::Sync { types, parent_dir } => crate::sync::run(types, parent_dir),
}
}

Expand Down
8 changes: 7 additions & 1 deletion crates/path-cli/src/cmd_query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ pub struct QueryArgs {
#[arg(long)]
project: Option<PathBuf>,

/// Keep only paths whose base lives under this directory (subtree
/// match), and scope the implicit sync to it likewise.
#[arg(long, short = 'd')]
parent_dir: Option<PathBuf>,

/// Keep only paths whose meta.kind matches this selector
/// (semver prefix, e.g. `agent-coding-session` or `…/v1.0`).
#[arg(long)]
Expand Down Expand Up @@ -103,6 +108,7 @@ pub fn run(args: QueryArgs, pretty: bool) -> Result<()> {
ids: args.ids,
inputs: args.input,
project: args.project,
parent_dir: args.parent_dir,
kind: args.kind,
};

Expand All @@ -123,7 +129,7 @@ fn sync_query_scope(args: &QueryArgs) {
return;
}
let bundle = crate::harness::HarnessBundle::from_environment();
match crate::sync::sync_bundle(&bundle, &types) {
match crate::sync::sync_bundle(&bundle, &types, args.parent_dir.as_deref()) {
Ok(outcomes) => {
for (t, o) in outcomes {
if o.new + o.updated + o.failed > 0 {
Expand Down
34 changes: 25 additions & 9 deletions crates/path-cli/src/query/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ pub struct Scope {
pub inputs: Vec<String>,
/// `--project`: keep only paths whose `base` resolves to this directory.
pub project: Option<PathBuf>,
/// `--parent-dir`: keep only paths whose `base` lives under this
/// directory (subtree match).
pub parent_dir: Option<PathBuf>,
/// `--kind`: keep only paths whose `meta.kind` matches this selector.
pub kind: Option<String>,
}
Expand Down Expand Up @@ -94,6 +97,7 @@ impl DocSource {
fn stream_files(scope: &Scope, emit: &mut dyn FnMut(Val) -> Result<()>) -> Result<()> {
let kind_sel = scope.kind.as_deref().map(kinds::parse_kind_selector);
let project = scope.project.as_deref().map(canonicalize_or_self);
let parent_dir = scope.parent_dir.as_deref().map(canonicalize_or_self);

for src in select_files(scope)? {
let graph = match read_source(&src) {
Expand All @@ -115,6 +119,7 @@ fn stream_files(scope: &Scope, emit: &mut dyn FnMut(Val) -> Result<()>) -> Resul
&graph,
kind_sel.as_ref(),
project.as_deref(),
parent_dir.as_deref(),
&mut steps,
);
drop(graph);
Expand Down Expand Up @@ -231,6 +236,7 @@ fn wrap_graph(
graph: &Graph,
kind_sel: Option<&KindSelector>,
project: Option<&FsPath>,
parent_dir: Option<&FsPath>,
out: &mut Vec<serde_json::Value>,
) {
for entry in &graph.paths {
Expand All @@ -249,6 +255,11 @@ fn wrap_graph(
{
continue;
}
if let Some(dir) = parent_dir
&& !path_matches_parent_dir(path, dir)
{
continue;
}

wrap_path(src, path, out);
}
Expand Down Expand Up @@ -307,13 +318,17 @@ fn path_context(path: &Path) -> serde_json::Value {
/// Whether a path's `base` resolves to `project` (a canonicalized directory).
/// Only `file://` bases can match; VCS bases (`github:…`) never do.
fn path_matches_project(path: &Path, project: &FsPath) -> bool {
let Some(base) = &path.path.base else {
return false;
};
let Some(fs) = base.uri.strip_prefix("file://") else {
return false;
};
canonicalize_or_self(FsPath::new(fs)) == project
base_fs_path(path).is_some_and(|p| p == project)
}

fn path_matches_parent_dir(path: &Path, parent_dir: &FsPath) -> bool {
base_fs_path(path).is_some_and(|p| p.starts_with(parent_dir))
}

fn base_fs_path(path: &Path) -> Option<PathBuf> {
let base = path.path.base.as_ref()?;
let fs = base.uri.strip_prefix("file://")?;
Some(canonicalize_or_self(FsPath::new(fs)))
}

fn canonicalize_or_self(p: &FsPath) -> PathBuf {
Expand Down Expand Up @@ -406,12 +421,12 @@ mod tests {
let graph = Graph::from_path(forked_path());
let mut out = Vec::new();
let sel = kinds::parse_kind_selector("agent-coding-session/v1.1.0");
wrap_graph(&doc_src("g"), &graph, Some(&sel), None, &mut out);
wrap_graph(&doc_src("g"), &graph, Some(&sel), None, None, &mut out);
assert_eq!(out.len(), 4, "matching kind keeps all steps");

out.clear();
let miss = kinds::parse_kind_selector("agent-coding-session/v2");
wrap_graph(&doc_src("g"), &graph, Some(&miss), None, &mut out);
wrap_graph(&doc_src("g"), &graph, Some(&miss), None, None, &mut out);
assert!(out.is_empty(), "non-matching kind drops the whole path");
}

Expand Down Expand Up @@ -447,6 +462,7 @@ mod tests {
ids: vec![],
inputs: vec!["/tmp/some.json".to_string(), "-".to_string()],
project: None,
parent_dir: None,
kind: None,
};
let files = select_files(&scope).unwrap();
Expand Down
Loading
Loading