From fa78fda8240d0b345c93fe936313d2da711a38d0 Mon Sep 17 00:00:00 2001 From: Mher Shahinyan Date: Sat, 13 Jun 2026 13:22:46 +0400 Subject: [PATCH] =?UTF-8?q?feat(consolidate):=20claude=20-p=20backend=20?= =?UTF-8?q?=E2=80=94=20works=20without=20an=20API=20key=20(0.20.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 0.19.0 consolidate was API-key-only, which left it unusable for anyone on a Claude subscription without an ANTHROPIC_API_KEY (bd tj-09r8xz1kwa). consolidate::summarize now selects a backend: direct Haiku API when ANTHROPIC_API_KEY is set (cheapest, ~1c/run), else the local `claude -p` binary via run_claude_json + ClaudeBinaryStdinRunner (subscription auth, no key; recursion guard already set by base_claude_command), else Ok(None) so the caller skips cleanly. TJ_CONSOLIDATE_BACKEND=none force-disables. Still manual-only, one call per run, never on a hook → cannot spend automatically. No backend => writes nothing, no heuristic fallback. Tests: existing 5 core + 2 CLI green (mock test still exercises the API path; skip test forces TJ_CONSOLIDATE_BACKEND=none so it's deterministic even where `claude` is on PATH). Clean on default and --no-default-features. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 16 ++++++++ Cargo.lock | 6 +-- Cargo.toml | 2 +- crates/tj-cli/Cargo.toml | 2 +- crates/tj-cli/src/main.rs | 18 +++++---- crates/tj-cli/tests/cli.rs | 3 ++ crates/tj-core/src/consolidate.rs | 63 +++++++++++++++++++++++++++---- crates/tj-mcp/Cargo.toml | 2 +- plugin/.claude-plugin/plugin.json | 2 +- 9 files changed, 91 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a281257..8cb14a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.20.0] - 2026-06-13 + +### Added +- **`consolidate` now works without an API key.** It picks a backend + automatically: the direct Haiku API when `ANTHROPIC_API_KEY` is set + (~1c/run), otherwise the local **`claude -p`** binary — your existing Claude + subscription login, **no API key needed** (post-2026-06-15 it bills as extra + usage and boots the environment per call, so it's pricier, but it requires no + key). With neither available it still skips cleanly, writing nothing. + `TJ_CONSOLIDATE_BACKEND=none` force-disables it. + +### Internal +- `consolidate::summarize` (backend selection) + `consolidate_via_cli` reusing + the classifier's `run_claude_json` / `ClaudeBinaryStdinRunner` (recursion + guard intact). + ## [0.19.0] - 2026-06-13 ### Added diff --git a/Cargo.lock b/Cargo.lock index 529d049..ccd4b02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2572,7 +2572,7 @@ dependencies = [ [[package]] name = "task-journal-cli" -version = "0.19.0" +version = "0.20.0" dependencies = [ "anyhow", "assert_cmd", @@ -2596,7 +2596,7 @@ dependencies = [ [[package]] name = "task-journal-core" -version = "0.19.0" +version = "0.20.0" dependencies = [ "anyhow", "chrono", @@ -2621,7 +2621,7 @@ dependencies = [ [[package]] name = "task-journal-mcp" -version = "0.19.0" +version = "0.20.0" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index a4ae95d..c9190bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ members = [ ] [workspace.package] -version = "0.19.0" +version = "0.20.0" edition = "2021" rust-version = "1.88" license = "MIT" diff --git a/crates/tj-cli/Cargo.toml b/crates/tj-cli/Cargo.toml index 709658c..494c6a8 100644 --- a/crates/tj-cli/Cargo.toml +++ b/crates/tj-cli/Cargo.toml @@ -23,7 +23,7 @@ default = ["embed"] embed = ["tj-core/embed"] [dependencies] -tj-core = { package = "task-journal-core", version = "0.19.0", path = "../tj-core", default-features = false } +tj-core = { package = "task-journal-core", version = "0.20.0", path = "../tj-core", default-features = false } anyhow = { workspace = true } clap = { workspace = true } tracing = { workspace = true } diff --git a/crates/tj-cli/src/main.rs b/crates/tj-cli/src/main.rs index c1965ea..ddbfb72 100644 --- a/crates/tj-cli/src/main.rs +++ b/crates/tj-cli/src/main.rs @@ -3942,19 +3942,21 @@ fn run_consolidate(max_facts: usize) -> anyhow::Result<()> { let texts: Vec = sources.iter().map(|(_, t)| t.clone()).collect(); let source_ids: Vec = sources.iter().map(|(id, _)| id.clone()).collect(); - let consolidator = match tj_core::consolidate::Consolidator::from_env(max_facts) { - Ok(c) => c, - Err(e) => { - println!("skipped: {e}. Set ANTHROPIC_API_KEY to enable consolidation (~1c/run)."); + let (backend, facts) = match tj_core::consolidate::summarize(&texts, max_facts)? { + Some(x) => x, + None => { + println!( + "skipped: no consolidation backend. Either set ANTHROPIC_API_KEY \ +(direct Haiku API, ~1c/run) or install Claude Code so `claude` is on PATH \ +(uses your subscription login, no API key needed)." + ); return Ok(()); } }; eprintln!( - "consolidating {} high-signal event(s) via {} …", - texts.len(), - consolidator.model + "consolidating {} high-signal event(s) via {backend} …", + texts.len() ); - let facts = consolidator.consolidate(&texts)?; if facts.is_empty() { println!("no durable facts found"); return Ok(()); diff --git a/crates/tj-cli/tests/cli.rs b/crates/tj-cli/tests/cli.rs index 3d4ab96..829319c 100644 --- a/crates/tj-cli/tests/cli.rs +++ b/crates/tj-cli/tests/cli.rs @@ -5355,6 +5355,9 @@ fn consolidate_skips_without_api_key_and_spends_nothing() { .current_dir(proj.path()) .env("XDG_DATA_HOME", xdg.path()) .env_remove("ANTHROPIC_API_KEY") + // Force the no-backend path so the test is deterministic even where + // `claude` is on PATH (which would otherwise be tried). + .env("TJ_CONSOLIDATE_BACKEND", "none") .args(["consolidate"]) .assert() .success() diff --git a/crates/tj-core/src/consolidate.rs b/crates/tj-core/src/consolidate.rs index c1935f2..c4b2d36 100644 --- a/crates/tj-core/src/consolidate.rs +++ b/crates/tj-core/src/consolidate.rs @@ -1,14 +1,17 @@ //! Memory consolidation (Pillar C): distil a project's recurring decisions and -//! constraints into a handful of durable semantic/procedural facts via a direct -//! Anthropic Haiku API call. +//! constraints into a handful of durable semantic/procedural facts with a +//! single LLM call. //! -//! Direct API, not `claude -p`: post-2026-06-15 both bill as extra usage, but -//! `claude -p` also boots the whole user environment (~tens of k tokens) on -//! every call, while the direct API sends only our ~7k-token prompt — roughly -//! 1c per run versus 5-10c. This is a MANUAL command (one call per run, only -//! when the user asks), so it never resembles the per-prompt classifier burn. -//! No `ANTHROPIC_API_KEY` → the caller skips cleanly; we never fall back to a +//! Two backends, picked by [`summarize`]: the **direct Anthropic Haiku API** +//! when `ANTHROPIC_API_KEY` is set (cheapest — only our ~7k-token prompt, +//! ~1c/run), otherwise the local **`claude -p`** binary (subscription auth, no +//! API key needed, but it boots the whole environment per call so it's +//! pricier). With neither, the caller skips cleanly — we never fall back to a //! heuristic, which would manufacture low-trust "facts". +//! +//! Either way this is a MANUAL command: one call per run, only when the user +//! asks, never wired to a hook — so it never resembles the per-prompt +//! classifier burn. use anyhow::{anyhow, Context}; use serde::{Deserialize, Serialize}; @@ -91,6 +94,50 @@ impl Consolidator { } } +/// Run whichever summarisation backend is available and return its label plus +/// the facts it produced. Order: (1) `ANTHROPIC_API_KEY` set → direct Haiku API +/// (cheapest, ~1c/run); (2) else `claude` on PATH → local `claude -p` +/// (subscription auth, no API key, heavier per-call boot); (3) else `Ok(None)`, +/// so the caller skips with a message — never a heuristic. +/// `TJ_CONSOLIDATE_BACKEND=none` forces the no-backend path (disable / tests). +pub fn summarize( + events: &[String], + max_facts: usize, +) -> anyhow::Result)>> { + if std::env::var("TJ_CONSOLIDATE_BACKEND").as_deref() == Ok("none") { + return Ok(None); + } + if std::env::var("ANTHROPIC_API_KEY").is_ok() { + let c = Consolidator::from_env(max_facts)?; + return Ok(Some(("haiku-api", c.consolidate(events)?))); + } + if crate::classifier::agent_sdk::claude_on_path() { + return Ok(Some(("claude -p", consolidate_via_cli(events, max_facts)?))); + } + Ok(None) +} + +/// Summarise via the local `claude -p` binary (subscription auth). Reuses the +/// classifier's command plumbing — including the recursion guard set by +/// `base_claude_command` — and unwraps the `--output-format json` envelope. +fn consolidate_via_cli( + events: &[String], + max_facts: usize, +) -> anyhow::Result> { + if events.is_empty() { + return Ok(Vec::new()); + } + let prompt = build_prompt(events, max_facts); + let model = std::env::var("TJ_CONSOLIDATE_MODEL") + .unwrap_or_else(|_| crate::classifier::agent_sdk::DEFAULT_MODEL.to_string()); + let text = crate::classifier::agent_sdk::run_claude_json( + &crate::classifier::agent_sdk::ClaudeBinaryStdinRunner, + &model, + &prompt, + )?; + Ok(parse_facts(&text)) +} + /// The summarisation prompt. Deliberately strict: durable-only, fixed line /// format, "output nothing" escape hatch so the model doesn't pad. pub fn build_prompt(events: &[String], max_facts: usize) -> String { diff --git a/crates/tj-mcp/Cargo.toml b/crates/tj-mcp/Cargo.toml index fc41df8..46cadbe 100644 --- a/crates/tj-mcp/Cargo.toml +++ b/crates/tj-mcp/Cargo.toml @@ -17,7 +17,7 @@ path = "src/main.rs" [dependencies] # Lean: the MCP server doesn't embed yet, so it skips the model2vec backend. -tj-core = { package = "task-journal-core", version = "0.19.0", path = "../tj-core", default-features = false } +tj-core = { package = "task-journal-core", version = "0.20.0", path = "../tj-core", default-features = false } anyhow = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json index d3fd32b..2a66a31 100644 --- a/plugin/.claude-plugin/plugin.json +++ b/plugin/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "task-journal", - "version": "0.19.0", + "version": "0.20.0", "description": "Append-only journal of AI-coding task reasoning chains: hypotheses, decisions, rejections, evidence. Renders compact resume packs so an agent can pick up a 2-week-old task with full context.", "author": { "name": "Mher Shahinyan"