diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index fbad745..36e4af8 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -6,14 +6,14 @@ }, "metadata": { "description": "Task Journal — append-only reasoning chain memory for AI-coding tasks", - "version": "0.13.1" + "version": "0.14.0" }, "plugins": [ { "name": "task-journal", "source": "./plugin", "description": "Append-only journal of AI-coding task reasoning chains. Captures hypotheses, decisions, rejections, evidence — renders compact resume packs so an agent can pick up a 2-week-old task with full context.", - "version": "0.13.1", + "version": "0.14.0", "author": { "name": "Digital-Threads" }, diff --git a/CHANGELOG.md b/CHANGELOG.md index 5eb1a11..6b016f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.14.0] - 2026-06-12 + +### Changed (breaking default) +- **Self-tagging-first: realtime auto-capture is now OFF by default.** A fresh + `install-hooks` wires only the cheap, read-only SessionStart resume hook — no + per-message classifier, no `claude -p` spawned, nothing charged. The agent + records reasoning directly via the MCP tools (the bundled skill drives this); + that is the capture mechanism. This removes the whole class of failures the + per-chunk `claude -p` design caused: each classification booted a full Claude + Code instance (CLAUDE.md + every plugin + hooks + MCP + plugin-marketplace + `git pull`) just to label one line — a recursion fork bomb (fixed 0.13.1), a + `git pull origin HEAD` storm, a runaway pending backlog, and burned Agent SDK + credit. +- **Per-message auto-capture is opt-in** via `install-hooks --auto-capture`, + which wires the `UserPromptSubmit` / `PostToolUse` / `Stop` / `PreCompact` + ingest hooks (honoring `--backend`). The classifier backends + (`heuristic` / `agent-sdk` / `api` / `hybrid`) and the recursion guard remain + in the codebase, unchanged, for users who explicitly want them. +- `dream` offline backfill is unchanged — a manual, batched LLM pass you run on + demand. + ## [0.13.1] - 2026-06-12 ### Fixed diff --git a/Cargo.lock b/Cargo.lock index a54750e..b6ebd4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2166,7 +2166,7 @@ dependencies = [ [[package]] name = "task-journal-cli" -version = "0.13.1" +version = "0.14.0" dependencies = [ "anyhow", "assert_cmd", @@ -2189,7 +2189,7 @@ dependencies = [ [[package]] name = "task-journal-core" -version = "0.13.1" +version = "0.14.0" dependencies = [ "anyhow", "chrono", @@ -2213,7 +2213,7 @@ dependencies = [ [[package]] name = "task-journal-mcp" -version = "0.13.1" +version = "0.14.0" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 30d57a8..1addbc4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ members = [ ] [workspace.package] -version = "0.13.1" +version = "0.14.0" edition = "2021" rust-version = "1.88" license = "MIT" diff --git a/README.md b/README.md index 90da33d..0bec236 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ That's it. Restart Claude Code, start working, and the journal fills itself. ## How it works - **Self-tagging is the primary path (recommended).** You — the agent in the live session — record reasoning directly via the five MCP tools: open a task with a `goal`, append a typed `decision` / `finding` / `rejection` / `evidence` event at the moment of commitment, and `task_close` with a written `outcome`. This is free (it rides the interactive session), language-agnostic, and higher-fidelity than any after-the-fact classifier. The bundled `task-journal` skill drives this automatically. See [MCP tools](#mcp-tools). -- **Auto-capture is a best-effort backstop.** Claude Code hooks also run every prompt, tool call, and reply through a two-stage classifier that lands typed events on its own. Stage 1 is a fast in-process heuristic — pattern-matches obvious EN+RU phrasing for zero cost. Stage 2 falls back to an LLM only when the heuristic is uncertain. Without an LLM backend it **degrades to heuristic-only**: it reliably catches keyword-obvious lines but misses real reasoning — especially non-English prose. Treat it as a safety net under your explicit self-tagging, not the main capture mechanism. Hook returns in <100 ms — both stages run in a detached background worker, never blocking your session. +- **Auto-capture is an opt-in backstop — OFF by default (v0.14.0).** A fresh `install-hooks` wires only a cheap, read-only SessionStart resume hook: no per-message classifier runs, no `claude -p` is ever spawned, nothing is charged. Self-tagging is the capture mechanism. Opt in with `install-hooks --auto-capture` and Claude Code hooks also run every prompt, tool call, and reply through a two-stage classifier that lands typed events on its own: Stage 1 is a fast in-process heuristic (obvious EN+RU phrasing, zero cost); Stage 2 falls back to an LLM only when the heuristic is uncertain (and only if you pick `--backend agent-sdk` / `api`). Even opted in it is a safety net under your explicit self-tagging, not the main mechanism, and it misses real reasoning — especially non-English prose. - **Artifact extraction.** Each event scans its text for commit hashes, PR URLs, file paths, issue IDs, and branch names. Aggregated artifacts are how Task Journal links related tasks: when you start a new task touching the same issue or file, the prior task is surfaced automatically. - **Resume packs.** `task_pack` (MCP tool or CLI) renders a task into a compact Markdown briefing — Goal, Outcome, decisions, rejections, evidence, artifacts — that fits in a fresh agent's context window without dumping the raw event log. - **Auto-capture boundaries.** Beyond per-event capture, two extra hooks mark *reasoning boundaries* automatically. On `PreCompact`, Task Journal reads the transcript JSONL tail (entries newer than the active task's last event) and enqueues anything the synchronous hooks missed before the compact — then drops a marker decision so the post-compact agent sees a clear cut. A `/rewind`-prefixed prompt appends a single correction event so pack readers see where the user rolled back. No mass-rejection of prior events — the boundary is a sentinel, not a rewrite. @@ -166,7 +166,7 @@ task-journal pack tj-x9rz1f --mode full | `doctor` | Self-check the install | | `rebuild-state` | Rebuild SQLite from JSONL | | `migrate-project` | Re-key data when a project moves on disk | -| `install-hooks [--scope user\|project] [--backend hybrid\|agent-sdk\|api\|heuristic]` | Wire Claude Code auto-capture hooks | +| `install-hooks [--scope user\|project] [--auto-capture] [--backend hybrid\|agent-sdk\|api\|heuristic]` | Wire Claude Code hooks. Default: only the read-only SessionStart resume hook. `--auto-capture` adds the per-message classifier (spawns `claude -p` unless `--backend heuristic`). | ## MCP tools @@ -253,10 +253,15 @@ Wire the MCP server into Claude Code (`~/.claude/settings.json`): } ``` -Wire auto-capture hooks (one-shot): +Wire hooks (one-shot). Default installs only the read-only SessionStart resume +hook — no classifier, no `claude -p`: ```bash task-journal install-hooks --scope user + +# Opt in to per-message auto-capture (spawns `claude -p` per uncertain chunk +# unless you pin --backend heuristic): +task-journal install-hooks --scope user --auto-capture --backend heuristic ``` ### Updating diff --git a/crates/tj-cli/Cargo.toml b/crates/tj-cli/Cargo.toml index 53a44ff..4bdd764 100644 --- a/crates/tj-cli/Cargo.toml +++ b/crates/tj-cli/Cargo.toml @@ -16,7 +16,7 @@ name = "task-journal" path = "src/main.rs" [dependencies] -tj-core = { package = "task-journal-core", version = "0.13.1", path = "../tj-core" } +tj-core = { package = "task-journal-core", version = "0.14.0", path = "../tj-core" } 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 83c51cf..611250f 100644 --- a/crates/tj-cli/src/main.rs +++ b/crates/tj-cli/src/main.rs @@ -743,6 +743,14 @@ enum Commands { /// ANTHROPIC_API_KEY (see `ingest-hook --help` for the credit note). #[arg(long, default_value = "hybrid")] backend: String, + /// v0.14.0: opt in to realtime auto-capture. Without it, install-hooks + /// wires ONLY the cheap, read-only SessionStart resume hook — no + /// per-message classifier, no `claude -p`, no cost. Primary capture is + /// the agent self-tagging via the MCP tools. With `--auto-capture` the + /// per-message + PreCompact ingest hooks are installed too (they spawn + /// the classifier, honoring `--backend`). + #[arg(long)] + auto_capture: bool, }, /// Show local classifier and journal statistics. Stats, @@ -1392,6 +1400,7 @@ fn main() -> Result<()> { uninstall, backfill, backend, + auto_capture, } => { let settings_path = match scope.as_str() { "user" => { @@ -1506,20 +1515,25 @@ fn main() -> Result<()> { format!("task-journal ingest-hook --backend={backend} || true") }; let cmd = cmd_string.as_str(); - let entries = serde_json::json!({ - "UserPromptSubmit": [{ "matcher": "", "hooks": [{ "type": "command", "command": cmd }] }], - "PostToolUse": [{ "matcher": "", "hooks": [{ "type": "command", "command": cmd }] }], - "Stop": [{ "matcher": "", "hooks": [{ "type": "command", "command": cmd }] }], - // SessionStart drives the auto resume-pack injection: - // ingest-hook short-circuits on this kind, queries open - // tasks for the current project, and emits the - // additionalContext envelope Claude Code expects. - "SessionStart": [{ "matcher": "", "hooks": [{ "type": "command", "command": cmd }] }], - // PreCompact: drop a marker decision event on the most-recent - // open task so the post-compact agent sees a clear boundary - // in the journal between pre- and post-compaction reasoning. - "PreCompact": [{ "matcher": "", "hooks": [{ "type": "command", "command": cmd }] }], + // v0.14.0 — self-tagging-first. By DEFAULT only the cheap, + // read-only SessionStart hook is wired: ingest-hook short-circuits + // on that kind, injects the resume pack + a nudge, and spawns no + // model. The per-message hooks (UserPromptSubmit/PostToolUse/Stop) + // and PreCompact are what spawn the classifier (`claude -p`); they + // are opt-in via `--auto-capture`. Primary capture is the agent + // self-tagging through the MCP tools. + let mut entries = serde_json::json!({ + "SessionStart": [{ "matcher": "", "hooks": [{ "type": "command", "command": cmd }] }], }); + if auto_capture { + let obj = entries.as_object_mut().expect("entries is an object"); + for ev in ["UserPromptSubmit", "PostToolUse", "Stop", "PreCompact"] { + obj.insert( + ev.to_string(), + serde_json::json!([{ "matcher": "", "hooks": [{ "type": "command", "command": cmd }] }]), + ); + } + } hooks_obj.insert("hooks".into(), entries); } std::fs::write(&settings_path, serde_json::to_string_pretty(¤t)?)?; diff --git a/crates/tj-cli/tests/cli.rs b/crates/tj-cli/tests/cli.rs index 27d2071..aa64c9b 100644 --- a/crates/tj-cli/tests/cli.rs +++ b/crates/tj-cli/tests/cli.rs @@ -836,13 +836,42 @@ fn install_hooks_writes_to_settings_json() { let settings_path = dir.path().join(".claude").join("settings.json"); assert!(settings_path.exists()); let content = std::fs::read_to_string(&settings_path).unwrap(); - assert!(content.contains("UserPromptSubmit")); - assert!(content.contains("PostToolUse")); assert!(content.contains("task-journal ingest-hook")); assert!( content.contains("SessionStart"), "install-hooks must wire SessionStart so resume-pack injection works" ); + // v0.14.0 — self-tagging-first: the default install does NOT wire the + // per-message classifier hooks (those spawn `claude -p`); they are opt-in + // via `--auto-capture`. + assert!( + !content.contains("UserPromptSubmit"), + "default install must not wire the per-message classifier hooks" + ); + assert!(!content.contains("PostToolUse")); +} + +#[test] +fn install_hooks_auto_capture_wires_all_events() { + let dir = assert_fs::TempDir::new().unwrap(); + Command::cargo_bin("task-journal") + .unwrap() + .env("HOME", dir.path()) + .args(["install-hooks", "--scope", "user", "--auto-capture"]) + .assert() + .success(); + + let content = + std::fs::read_to_string(dir.path().join(".claude").join("settings.json")).unwrap(); + for ev in [ + "SessionStart", + "UserPromptSubmit", + "PostToolUse", + "Stop", + "PreCompact", + ] { + assert!(content.contains(ev), "--auto-capture must wire {ev}"); + } } #[test] @@ -875,7 +904,7 @@ fn install_hooks_is_idempotent_and_uninstall_works() { || after_install.contains("\"theme\": \"dark\""), "must preserve unrelated keys" ); - assert!(after_install.contains("UserPromptSubmit")); + assert!(after_install.contains("SessionStart")); Command::cargo_bin("task-journal") .unwrap() @@ -2891,13 +2920,13 @@ fn install_hooks_wires_precompact_event() { Command::cargo_bin("task-journal") .unwrap() .env("HOME", dir.path()) - .args(["install-hooks", "--scope", "user"]) + .args(["install-hooks", "--scope", "user", "--auto-capture"]) .assert() .success(); let s = std::fs::read_to_string(dir.path().join(".claude/settings.json")).unwrap(); assert!( s.contains("PreCompact"), - "settings.json must wire PreCompact: {s}" + "settings.json must wire PreCompact under --auto-capture: {s}" ); } diff --git a/crates/tj-mcp/Cargo.toml b/crates/tj-mcp/Cargo.toml index 6c88d59..b2cb97e 100644 --- a/crates/tj-mcp/Cargo.toml +++ b/crates/tj-mcp/Cargo.toml @@ -16,7 +16,7 @@ name = "task-journal-mcp" path = "src/main.rs" [dependencies] -tj-core = { package = "task-journal-core", version = "0.13.1", path = "../tj-core" } +tj-core = { package = "task-journal-core", version = "0.14.0", path = "../tj-core" } anyhow = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json index 33cdcea..bb2d5e3 100644 --- a/plugin/.claude-plugin/plugin.json +++ b/plugin/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "task-journal", - "version": "0.13.1", + "version": "0.14.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"