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
36 changes: 35 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,40 @@

All notable changes to the Toolpath workspace are documented here.

## Bare `path resume` — one picker from any session to any harness — 2026-08-03

`path resume` with no argument now opens a cross-harness session
picker: every session from every installed harness (Claude Code,
Gemini CLI, Codex, Copilot, opencode, Cursor, Pi), ranked with the
current project's sessions first — pick one, pick a target harness,
and you're back in the conversation. Previously resume always required
an explicit input (URL, file, or cache id); the local "resume that
session I ran earlier, maybe in a different harness" loop needed a
`share`-shaped detour or a manual `p import`.

- **path-cli** (0.17.0): the bare picker is recency-first — it
hydrates only the newest 100 sessions across every harness, so it
opens in milliseconds regardless of history size. Codex candidates
rank stat-only by mtime; each hydrates from the listing cache when
its stamp matches (instant, message count included) or via one
streaming read that is then cached, so counts appear everywhere and
only new/changed sessions ever pay a read. A tail row ("N older
sessions — load everything") runs the full sweep on demand, and
`--project` always sweeps fully since its matches may be old.
(`toolpath-codex` 0.6.3 also gains an O(1) head+tail `peek_metadata`
for count-free peeking.) It reuses `share`'s aggregation and picker rows,
then flows into the existing harness-picker → project → exec
pipeline. New bare-mode flags:
`--from <harness>` narrows the session picker to one harness
(the resume target is still `--harness` / the harness picker) and
`--project <path>` narrows it to one project directory, mirroring
`share --project`. Cache policy mirrors `share`: derived docs are
written through to the cache and recorded in the sync manifest by
default, with a freshness fast path that skips re-deriving unchanged
sessions (`--force` re-derives, `--no-cache` stays in-memory).
Closes #110. The session picker sits behind a new
`cmd_resume::SessionPicker` seam (`FixedPicker` test double),
mirroring `ExecStrategy`, and `ResumeArgs` now implements `Default`.
## Warm picker opens are effectively instant — 2026-08-04

The session picker (`path share`, and bare `path resume` once #154
Expand Down Expand Up @@ -36,7 +70,6 @@ the others before anything appeared.
chain-index cache is single-threaded); everything else fans out.
Row concatenation keeps the old provider order, so ranking
tie-breaks are unchanged.

## Codex session listing no longer parses every byte — 2026-08-04

Listing Codex sessions read every rollout file end to end through
Expand All @@ -53,6 +86,7 @@ upcoming bare `resume` picker — into a minute-plus silent stall.
method: a first prompt buried past the head budget reports as
`None`, and `line_count` counts non-empty lines rather than
successfully parsed ones.

## Projected Claude sessions are resumable again — 2026-07-30

Two fixes found by live-resuming a projected session against the real
Expand Down
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ cargo run -p path-cli -- share --url https://my-pathbase.example
# Resume a Toolpath document into your coding agent of choice (interactive harness picker)
cargo run -p path-cli -- resume <pathbase-url-or-shorthand-or-file-or-cache-id>
cargo run -p path-cli -- resume <input> --harness claude -C /path/to/project
cargo run -p path-cli -- resume # bare: pick any local session from any harness

# Plumbing: export toolpath documents into external formats. <ref> is a
# cache id or a file path.
Expand Down Expand Up @@ -283,7 +284,7 @@ Build the site after changes: `cd site && pnpm run build` (should produce 11 pag
- Interactive session selection: `path p import <provider>` (claude / gemini / pi / codex / opencode) auto-launches a fuzzy picker when stdin and stderr are TTYs and no `--session` was given. Backend: external `fzf` if on `$PATH`, otherwise the embedded skim picker (default-feature `embedded-picker`, defined in `crates/path-cli/src/skim_picker.rs`). Multi-select (TAB) produces a `Graph` document; single-select produces a `Path`. The picker uses `path show <provider> --…` as its `--preview` command. When neither backend can run (no TTY, or `--no-default-features` AND no `fzf`), it falls back to most-recent (with `--project`) or prints the manual recipe (without). `path p list <provider> --format tsv` is the documented machine-readable surface — column 1 is the project (for claude/gemini/pi) or session id (for codex/opencode), and the trailing column carries `first_user_message` so consumers can fuzzy-match by topic.
- Conversation metadata title field: `toolpath-claude::ConversationMetadata`, `toolpath-gemini::ConversationMetadata`, and `toolpath-pi::SessionMeta` all expose `first_user_message: Option<String>` — the first non-empty user-prompt text. Populated cheaply during the metadata pass (single-pass for Claude/Gemini; one extra short read for Pi). Used by the picker UI but useful for any "list sessions by topic" surface.
- `path share` is the one-shot equivalent of `path p import <harness> | path p export pathbase`. It probes installed agent harnesses (claude/gemini/codex/opencode/pi), aggregates their sessions into a single fzf picker, and ranks rows whose project (claude/gemini/pi) or recorded cwd (codex/opencode) canonicalizes to the current directory at the top. `--harness` narrows the picker to one provider; `--harness X --session Y` (and `--project P` for keyed providers) skips the picker entirely. Pathbase flags (`--url`, `--anon`, `--repo`, `--slug`, `--public`) match `path export pathbase`. By default the derived doc is written to the cache like `import` does; pass `--no-cache` to skip. When the manifest shows the picked session unchanged since its last sync (`sync::fresh_cache_id`: stamps match, doc present; the freshness stat targets that one artifact directly, no sibling enumeration), share uploads the cached doc directly instead of re-deriving. The cache ingests maximally (thinking always included), and uploads carry the same full derivation as local projection (`resume`, `p export <harness>`) — there is no egress stripping.
- `path resume <input>` is the inverse of `path share`. It accepts a Pathbase URL, an `owner/repo/slug` shorthand, a local toolpath JSON file, or a cache id; resolves it (caching URL fetches under `~/.toolpath/documents/` unless `--no-cache`); validates that the document is a single agent-bearing `Path`; then opens an `fzf` harness picker (skipped with `--harness X`). The picker pre-selects the source harness inferred from `path.meta.source` (`claude-code`/`gemini-cli`/`codex`/`opencode`/`pi`) when it's installed. After picking, `path resume` projects the session into the harness's on-disk layout under the chosen working directory (default: shell cwd; override with `-C, --cwd P`) and `execvp`'s the harness's resume command (`claude -r <id>` / `gemini --resume <id>` / `codex resume <id>` / `opencode --session <id>` / `pi --session <id>`). On Windows it spawns and waits, propagating the exit code. The exec is mockable via `cmd_resume::ExecStrategy` — production uses `RealExec`; integration tests use `RecordingExec` to capture the recipe without launching a real harness.
- `path resume <input>` is the inverse of `path share`. It accepts a Pathbase URL, an `owner/repo/slug` shorthand, a local toolpath JSON file, or a cache id; resolves it (caching URL fetches under `~/.toolpath/documents/` unless `--no-cache`); validates that the document is a single agent-bearing `Path`; then opens an `fzf` harness picker (skipped with `--harness X`). The picker pre-selects the source harness inferred from `path.meta.source` (`claude-code`/`gemini-cli`/`codex`/`opencode`/`pi`) when it's installed. After picking, `path resume` projects the session into the harness's on-disk layout under the chosen working directory (default: shell cwd; override with `-C, --cwd P`) and `execvp`'s the harness's resume command (`claude -r <id>` / `gemini --resume <id>` / `codex resume <id>` / `opencode --session <id>` / `pi --session <id>`). On Windows it spawns and waits, propagating the exit code. The exec is mockable via `cmd_resume::ExecStrategy` — production uses `RealExec`; integration tests use `RecordingExec` to capture the recipe without launching a real harness. **Bare mode**: `path resume` with no `<input>` opens a cross-harness *session* picker (reusing `share`'s aggregation and picker rows, cwd-ranked), derives the picked session, then flows into the same harness-picker → project → exec pipeline. `--from <harness>` narrows the session picker to one harness (the resume *target* is still `--harness`/the harness picker); `--project <path>` narrows it to one project directory (mirrors `share --project`); `-C` is both the ranking anchor and the projection/exec cwd. Cache policy mirrors `share`: write-through by default (cache + sync-manifest record), with a `sync::fresh_cache_id` fast path that loads the cached doc when the source is unchanged; `--force` re-derives, `--no-cache` derives in-memory only; `--url` is inert in bare mode. Without a TTY, bare mode bails: `no input provided and no TTY for interactive selection; pass an <input> (URL, file, or cache id), or rerun in a terminal`. The session picker is mockable via `cmd_resume::SessionPicker` (`FixedPicker` in tests), mirroring `ExecStrategy`.
- `path query` does not load the whole cache into memory when it can avoid it. `crates/path-cli/src/query/plan.rs` parses the jaq filter into jaq's own AST (`jaq_core::load::parse::Term`) and classifies it into a `Plan`: `PerFileStream` (`.[] | g` element-wise work — run per document, print as you go), `Decompose { reduce }` (algebraic aggregations — run the whole filter per file, concatenate the per-file outputs, then run a derived combine: `map`→`add` (array concat), top-N `sort_by(k)|.[:N]`→`add | sort_by(k)|.[:N]`, `length`→`add` over exact integer counts), or `Slurp` (the always-correct whole-array fallback). Recognition is conservative — a non-distributive prefix like `unique`/`group_by` slurps, and so do scalar `add` (float sums re-associate across per-file partials), `min`/`max` (`[] | min == null` poisons the merge), and any unrecognized tail — so **the planner never changes an answer** — `crates/path-cli/src/query/filter.rs` tests assert streamed output equals slurp byte-for-byte. `filter::execute` compiles the filter once (jaq's compiled `Filter` is fully owned, so it's reused across files) and drives the plan; `mod.rs::stream_files` yields one document's wrapped steps at a time. `TOOLPATH_QUERY_EXPLAIN=1` prints the chosen plan to stderr. No user-facing flag — it's automatic. Tie-break caveat: a streamed top-N matches slurp's *ranking*, but boundary ties may resolve to different specific rows.
- Cache sync: `path p cache sync [types…]` (`crates/path-cli/src/artifact.rs`: `ArtifactType` + `ArtifactRef` + the stamp helpers; `sync/engine.rs`: manifest + ingestion loop, no UI — it reports through a `SyncObserver` trait, `&mut ()` for a silent sync; `sync/sources.rs`: an `ArtifactSource` trait — enumerate / stamp / derive — with one impl per provider, so the engine never matches on artifact type; `cmd_cache.rs`: the stderr progress line + summary) incrementally ingests artifacts into the cache — no args syncs every artifact type. Change detection is **stat-level**: each artifact is enumerated as an `ArtifactRef` whose fingerprint is the source file's mtime + size (claude: the *whole session chain* — max segment mtime + summed segment sizes via `claude_chain_stamp`, because Claude Code rotates to a new file on continuation while the chain keeps its oldest segment's id, so appends land in the newest file, not the head; the chain comes from the same cached index `list_conversations` builds; codex: rollout file, id from the stem's trailing UUID; pi: session file, id from a one-line header peek; copilot: `session-state/<id>/events.jsonl`, pure read-dir + stat) or the DB row's updated-at (opencode: header-only `SELECT time_updated`; cursor: composer headers' `lastUpdatedAt`, bubble-less drafts skipped, workspace-less composers *included* unlike `share`). Gemini enumerates via `PathResolver::list_session_entries` (`toolpath-gemini` 0.6.1), whose identity peek is bounded to the first 4 KiB of a main file. Deciding "nothing changed" reads no session bodies — a no-op sync is milliseconds. Changed/new artifacts derive through the same provider managers (each source calls the `derive_*_session_with` helpers in `derive.rs`). Manifest at `~/.toolpath/manifest.json`: artifact type → artifact id → `{path?, cache_id, modified?, size?, synced_at}`; atomic temp+rename writes, `0600`, checkpointed every 10 writes (interruption-safe: a killed run keeps nearly everything it derived, and derives run newest-first so partial progress covers the sessions that matter most); writers serialize on an advisory lock (`manifest.json.lock`) and every write is a locked read-merge-save — checkpoints merge only the records the run wrote — so concurrent invocations (query auto-syncs, imports) union their records instead of clobbering each other. Pending work reports progress on stderr (`\r`-updating `<type> done/total` on a TTY, a plain line every 25 items otherwise; no-op syncs stay silent). Sync always writes the cache with force — refresh semantics — and never deletes: artifacts removed upstream keep their cache docs and manifest records (archive, not mirror). Derivation failures warn and tally, they don't abort. A record's `cache_id` is *optional*: a record without one is "known, not materialized" — created when `p cache rm` evicts a doc (rm downgrades the record; the next sync re-materializes it, and sync also verifies the doc file actually exists before skipping, so even out-of-band deletions self-heal). Claude derives leave `DeriveConfig.project_path` unset so `path.base` comes from the session's own recorded cwd rather than the lossy slug. `path query` runs this sync implicitly before reading, scoped to its flags (`--source X` → that type; `--id`s → their prefixes; bare query → all types; `--input`-only → none), quiet unless something was ingested, degrading to the cache as-is if sync fails; `--no-sync` opts out. `p import` and `share` record what they write: every session derive carries a provenance `ArtifactRef` (stamped *before* the source is read, in `DerivedDoc.provenance`), and the cache-write sites call `sync::record_artifact` so the next sync sees those artifacts as unchanged instead of re-deriving them. Every import flow — explicit `--session`, picker multi-select, `--all`, and the most-recent fallbacks — loops the per-session helpers, so every session write is recorded; there is no bulk `derive_project` path in the CLI anymore, and `p import pi --all` now emits one Path per session like every other provider (it used to emit a single combined Graph). `--no-cache` paths record nothing: the manifest describes the cache.
- Claude Code plugin: `.claude-plugin/marketplace.json` (marketplace `toolpath`) + `plugins/claude-code/` (plugin `path`, so commands are `/path:share` and `/path:query`). The plugin does **not** commit binaries — both commands invoke the CLI through `plugins/claude-code/scripts/ensure-path.sh`, which prefers an existing Toolpath `path` on PATH (identity-checked via `--help`), else `~/.local/bin/path`, else `~/.toolpath/bin/path`, else downloads the latest GitHub release (sha256-verified, same logic as `scripts/install.sh`) and installs globally to `~/.local/bin` — falling back to `~/.toolpath/bin` when a foreign binary named `path` claims the name. Two hard-won constraints baked into the command docs: slash-command inline `!` context commands and model-issued Bash must not contain `$PWD`/variables (Claude Code's permission checker rejects commands it can't statically analyze — hence the `sessions` and `current-session` helper modes, the latter reading `$CLAUDE_CODE_SESSION_ID` so no-arg `/path:share` shares exactly the running session), and `--project` must always be an absolute path (path-cli does not canonicalize relative `--project` values; `.` silently matches nothing). Tests: `scripts/test-plugin.sh` (manifest consistency + offline bootstrap tests against a stubbed curl/release), wired in as the `plugin` quality gate; plugin shell scripts are shellchecked. Dev loop: `claude --plugin-dir ./plugins/claude-code`. Future harness integrations go under `plugins/<harness>/` (only Claude Code plugins are marketplace entries; other harnesses distribute their own way). Version bumps: keep `plugins/claude-code/.claude-plugin/plugin.json` and the matching entry in `.claude-plugin/marketplace.json` in lockstep (test-plugin.sh asserts this); the binary is unpinned (latest release) with `MIN_VERSION` in ensure-path.sh naming the oldest CLI the command docs support.
Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,15 @@ toolpath-convo = { version = "0.11.1", path = "crates/toolpath-convo" }
toolpath-git = { version = "0.6.0", path = "crates/toolpath-git" }
toolpath-claude = { version = "0.12.2", path = "crates/toolpath-claude", default-features = false }
toolpath-gemini = { version = "0.6.1", path = "crates/toolpath-gemini", default-features = false }
toolpath-codex = { version = "0.6.2", path = "crates/toolpath-codex" }
toolpath-codex = { version = "0.6.3", path = "crates/toolpath-codex" }
toolpath-copilot = { version = "0.1.0", path = "crates/toolpath-copilot" }
toolpath-opencode = { version = "0.5.0", path = "crates/toolpath-opencode" }
toolpath-cursor = { version = "0.2.0", path = "crates/toolpath-cursor" }
toolpath-github = { version = "0.6.0", path = "crates/toolpath-github" }
toolpath-dot = { version = "0.5.0", path = "crates/toolpath-dot" }
toolpath-md = { version = "0.7.0", path = "crates/toolpath-md" }
toolpath-pi = { version = "0.6.1", path = "crates/toolpath-pi" }
path-cli = { version = "0.16.3", path = "crates/path-cli" }
path-cli = { version = "0.17.0", path = "crates/path-cli" }
pathbase-client = { version = "0.2.0", path = "crates/pathbase-client" }

reqwest = { version = "0.13", default-features = false, features = ["blocking", "json", "rustls"] }
Expand Down
2 changes: 1 addition & 1 deletion crates/path-cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "path-cli"
version = "0.16.3"
version = "0.17.0"
edition.workspace = true
license.workspace = true
repository = "https://github.com/empathic/toolpath"
Expand Down
Loading
Loading