Skip to content

Commit e29039c

Browse files
authored
Merge pull request #37 from Digital-Threads/feat/consolidate-claude-p-backend
feat(consolidate): claude -p backend — works without an API key (0.20.0)
2 parents 657c5e2 + fa78fda commit e29039c

9 files changed

Lines changed: 91 additions & 23 deletions

File tree

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.20.0] - 2026-06-13
11+
12+
### Added
13+
- **`consolidate` now works without an API key.** It picks a backend
14+
automatically: the direct Haiku API when `ANTHROPIC_API_KEY` is set
15+
(~1c/run), otherwise the local **`claude -p`** binary — your existing Claude
16+
subscription login, **no API key needed** (post-2026-06-15 it bills as extra
17+
usage and boots the environment per call, so it's pricier, but it requires no
18+
key). With neither available it still skips cleanly, writing nothing.
19+
`TJ_CONSOLIDATE_BACKEND=none` force-disables it.
20+
21+
### Internal
22+
- `consolidate::summarize` (backend selection) + `consolidate_via_cli` reusing
23+
the classifier's `run_claude_json` / `ClaudeBinaryStdinRunner` (recursion
24+
guard intact).
25+
1026
## [0.19.0] - 2026-06-13
1127

1228
### Added

Cargo.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ members = [
77
]
88

99
[workspace.package]
10-
version = "0.19.0"
10+
version = "0.20.0"
1111
edition = "2021"
1212
rust-version = "1.88"
1313
license = "MIT"

crates/tj-cli/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ default = ["embed"]
2323
embed = ["tj-core/embed"]
2424

2525
[dependencies]
26-
tj-core = { package = "task-journal-core", version = "0.19.0", path = "../tj-core", default-features = false }
26+
tj-core = { package = "task-journal-core", version = "0.20.0", path = "../tj-core", default-features = false }
2727
anyhow = { workspace = true }
2828
clap = { workspace = true }
2929
tracing = { workspace = true }

crates/tj-cli/src/main.rs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3942,19 +3942,21 @@ fn run_consolidate(max_facts: usize) -> anyhow::Result<()> {
39423942
let texts: Vec<String> = sources.iter().map(|(_, t)| t.clone()).collect();
39433943
let source_ids: Vec<String> = sources.iter().map(|(id, _)| id.clone()).collect();
39443944

3945-
let consolidator = match tj_core::consolidate::Consolidator::from_env(max_facts) {
3946-
Ok(c) => c,
3947-
Err(e) => {
3948-
println!("skipped: {e}. Set ANTHROPIC_API_KEY to enable consolidation (~1c/run).");
3945+
let (backend, facts) = match tj_core::consolidate::summarize(&texts, max_facts)? {
3946+
Some(x) => x,
3947+
None => {
3948+
println!(
3949+
"skipped: no consolidation backend. Either set ANTHROPIC_API_KEY \
3950+
(direct Haiku API, ~1c/run) or install Claude Code so `claude` is on PATH \
3951+
(uses your subscription login, no API key needed)."
3952+
);
39493953
return Ok(());
39503954
}
39513955
};
39523956
eprintln!(
3953-
"consolidating {} high-signal event(s) via {} …",
3954-
texts.len(),
3955-
consolidator.model
3957+
"consolidating {} high-signal event(s) via {backend} …",
3958+
texts.len()
39563959
);
3957-
let facts = consolidator.consolidate(&texts)?;
39583960
if facts.is_empty() {
39593961
println!("no durable facts found");
39603962
return Ok(());

crates/tj-cli/tests/cli.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5355,6 +5355,9 @@ fn consolidate_skips_without_api_key_and_spends_nothing() {
53555355
.current_dir(proj.path())
53565356
.env("XDG_DATA_HOME", xdg.path())
53575357
.env_remove("ANTHROPIC_API_KEY")
5358+
// Force the no-backend path so the test is deterministic even where
5359+
// `claude` is on PATH (which would otherwise be tried).
5360+
.env("TJ_CONSOLIDATE_BACKEND", "none")
53585361
.args(["consolidate"])
53595362
.assert()
53605363
.success()

crates/tj-core/src/consolidate.rs

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
//! Memory consolidation (Pillar C): distil a project's recurring decisions and
2-
//! constraints into a handful of durable semantic/procedural facts via a direct
3-
//! Anthropic Haiku API call.
2+
//! constraints into a handful of durable semantic/procedural facts with a
3+
//! single LLM call.
44
//!
5-
//! Direct API, not `claude -p`: post-2026-06-15 both bill as extra usage, but
6-
//! `claude -p` also boots the whole user environment (~tens of k tokens) on
7-
//! every call, while the direct API sends only our ~7k-token prompt — roughly
8-
//! 1c per run versus 5-10c. This is a MANUAL command (one call per run, only
9-
//! when the user asks), so it never resembles the per-prompt classifier burn.
10-
//! No `ANTHROPIC_API_KEY` → the caller skips cleanly; we never fall back to a
5+
//! Two backends, picked by [`summarize`]: the **direct Anthropic Haiku API**
6+
//! when `ANTHROPIC_API_KEY` is set (cheapest — only our ~7k-token prompt,
7+
//! ~1c/run), otherwise the local **`claude -p`** binary (subscription auth, no
8+
//! API key needed, but it boots the whole environment per call so it's
9+
//! pricier). With neither, the caller skips cleanly — we never fall back to a
1110
//! heuristic, which would manufacture low-trust "facts".
11+
//!
12+
//! Either way this is a MANUAL command: one call per run, only when the user
13+
//! asks, never wired to a hook — so it never resembles the per-prompt
14+
//! classifier burn.
1215
1316
use anyhow::{anyhow, Context};
1417
use serde::{Deserialize, Serialize};
@@ -91,6 +94,50 @@ impl Consolidator {
9194
}
9295
}
9396

97+
/// Run whichever summarisation backend is available and return its label plus
98+
/// the facts it produced. Order: (1) `ANTHROPIC_API_KEY` set → direct Haiku API
99+
/// (cheapest, ~1c/run); (2) else `claude` on PATH → local `claude -p`
100+
/// (subscription auth, no API key, heavier per-call boot); (3) else `Ok(None)`,
101+
/// so the caller skips with a message — never a heuristic.
102+
/// `TJ_CONSOLIDATE_BACKEND=none` forces the no-backend path (disable / tests).
103+
pub fn summarize(
104+
events: &[String],
105+
max_facts: usize,
106+
) -> anyhow::Result<Option<(&'static str, Vec<ConsolidatedFact>)>> {
107+
if std::env::var("TJ_CONSOLIDATE_BACKEND").as_deref() == Ok("none") {
108+
return Ok(None);
109+
}
110+
if std::env::var("ANTHROPIC_API_KEY").is_ok() {
111+
let c = Consolidator::from_env(max_facts)?;
112+
return Ok(Some(("haiku-api", c.consolidate(events)?)));
113+
}
114+
if crate::classifier::agent_sdk::claude_on_path() {
115+
return Ok(Some(("claude -p", consolidate_via_cli(events, max_facts)?)));
116+
}
117+
Ok(None)
118+
}
119+
120+
/// Summarise via the local `claude -p` binary (subscription auth). Reuses the
121+
/// classifier's command plumbing — including the recursion guard set by
122+
/// `base_claude_command` — and unwraps the `--output-format json` envelope.
123+
fn consolidate_via_cli(
124+
events: &[String],
125+
max_facts: usize,
126+
) -> anyhow::Result<Vec<ConsolidatedFact>> {
127+
if events.is_empty() {
128+
return Ok(Vec::new());
129+
}
130+
let prompt = build_prompt(events, max_facts);
131+
let model = std::env::var("TJ_CONSOLIDATE_MODEL")
132+
.unwrap_or_else(|_| crate::classifier::agent_sdk::DEFAULT_MODEL.to_string());
133+
let text = crate::classifier::agent_sdk::run_claude_json(
134+
&crate::classifier::agent_sdk::ClaudeBinaryStdinRunner,
135+
&model,
136+
&prompt,
137+
)?;
138+
Ok(parse_facts(&text))
139+
}
140+
94141
/// The summarisation prompt. Deliberately strict: durable-only, fixed line
95142
/// format, "output nothing" escape hatch so the model doesn't pad.
96143
pub fn build_prompt(events: &[String], max_facts: usize) -> String {

crates/tj-mcp/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ path = "src/main.rs"
1717

1818
[dependencies]
1919
# Lean: the MCP server doesn't embed yet, so it skips the model2vec backend.
20-
tj-core = { package = "task-journal-core", version = "0.19.0", path = "../tj-core", default-features = false }
20+
tj-core = { package = "task-journal-core", version = "0.20.0", path = "../tj-core", default-features = false }
2121
anyhow = { workspace = true }
2222
tokio = { workspace = true }
2323
tracing = { workspace = true }

plugin/.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "task-journal",
3-
"version": "0.19.0",
3+
"version": "0.20.0",
44
"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.",
55
"author": {
66
"name": "Mher Shahinyan"

0 commit comments

Comments
 (0)