Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
},
"metadata": {
"description": "Task Journal — append-only reasoning chain memory for AI-coding tasks",
"version": "0.14.1"
"version": "0.14.2"
},
"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.14.1",
"version": "0.14.2",
"author": {
"name": "Digital-Threads"
},
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.14.2] - 2026-06-12

### Fixed
- **`install-hooks` no longer clobbers other plugins' hooks.** It used to
replace the entire `hooks` block in settings.json, wiping any co-located
third-party hooks (token-pilot, context-mode, …). It now **merges**: for each
event it touches, it strips only prior task-journal entries (idempotent
re-install) and appends its own, leaving foreign hooks and untouched events
intact. Makes re-running `install-hooks` safe on a multi-plugin setup —
needed to wire the 0.14.1 nudge without nuking everything else.

## [0.14.1] - 2026-06-12

### Added (reliability — no model, no cost)
Expand Down
6 changes: 3 additions & 3 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ members = [
]

[workspace.package]
version = "0.14.1"
version = "0.14.2"
edition = "2021"
rust-version = "1.88"
license = "MIT"
Expand Down
2 changes: 1 addition & 1 deletion crates/tj-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ name = "task-journal"
path = "src/main.rs"

[dependencies]
tj-core = { package = "task-journal-core", version = "0.14.1", path = "../tj-core" }
tj-core = { package = "task-journal-core", version = "0.14.2", path = "../tj-core" }
anyhow = { workspace = true }
clap = { workspace = true }
tracing = { workspace = true }
Expand Down
42 changes: 41 additions & 1 deletion crates/tj-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1554,7 +1554,47 @@ fn main() -> Result<()> {
);
}
}
hooks_obj.insert("hooks".into(), entries);
// MERGE our entries into the existing `hooks` block — touch ONLY
// task-journal hooks, never clobber other plugins' hooks. For each
// event we (a) strip any prior task-journal entry (idempotent
// re-install) then (b) append ours, leaving foreign hooks and
// untouched events intact.
let is_tj = |c: &str| {
c.contains("task-journal ingest-hook") || c.contains("task-journal nudge")
};
let hooks_block = hooks_obj
.entry("hooks".to_string())
.or_insert_with(|| serde_json::json!({}));
let hooks_block = hooks_block
.as_object_mut()
.ok_or_else(|| anyhow::anyhow!("settings `hooks` is not an object"))?;
for (event, our_arr) in entries.as_object().expect("entries is an object") {
let existing = hooks_block
.entry(event.clone())
.or_insert_with(|| serde_json::json!([]));
let existing = existing
.as_array_mut()
.ok_or_else(|| anyhow::anyhow!("hooks.{event} is not an array"))?;
for entry in existing.iter_mut() {
if let Some(inner) = entry.get_mut("hooks").and_then(|v| v.as_array_mut()) {
inner.retain(|h| {
h.get("command")
.and_then(|c| c.as_str())
.map(|c| !is_tj(c))
.unwrap_or(true)
});
}
}
existing.retain(|e| {
e.get("hooks")
.and_then(|v| v.as_array())
.map(|a| !a.is_empty())
.unwrap_or(true)
});
for our_entry in our_arr.as_array().expect("event entry is an array") {
existing.push(our_entry.clone());
}
}
}
std::fs::write(&settings_path, serde_json::to_string_pretty(&current)?)?;
println!("{}", settings_path.display());
Expand Down
54 changes: 54 additions & 0 deletions crates/tj-cli/tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -881,6 +881,60 @@ fn install_hooks_auto_capture_wires_all_events() {
}
}

#[test]
fn install_hooks_merges_and_preserves_third_party_hooks() {
let dir = assert_fs::TempDir::new().unwrap();
let claude_dir = dir.path().join(".claude");
std::fs::create_dir_all(&claude_dir).unwrap();
// Pre-existing foreign hooks (another plugin) on the same events we touch.
std::fs::write(
claude_dir.join("settings.json"),
serde_json::json!({
"hooks": {
"UserPromptSubmit": [{ "matcher": "", "hooks": [
{ "type": "command", "command": "other-plugin do-thing" }
]}],
"SessionStart": [{ "matcher": "", "hooks": [
{ "type": "command", "command": "other-plugin start" }
]}]
}
})
.to_string(),
)
.unwrap();

let run = || {
Command::cargo_bin("task-journal")
.unwrap()
.env("HOME", dir.path())
.args(["install-hooks", "--scope", "user"])
.assert()
.success();
};
run();
run(); // idempotent: second install must not duplicate task-journal entries

let content = std::fs::read_to_string(claude_dir.join("settings.json")).unwrap();
// Foreign hooks survive.
assert!(
content.contains("other-plugin do-thing"),
"must preserve a third-party UserPromptSubmit hook: {content}"
);
assert!(
content.contains("other-plugin start"),
"must preserve a third-party SessionStart hook"
);
// Ours got added.
assert!(content.contains("task-journal nudge"));
assert!(content.contains("task-journal ingest-hook"));
// Idempotent — exactly one nudge, not two.
assert_eq!(
content.matches("task-journal nudge").count(),
1,
"re-install must not duplicate the nudge hook: {content}"
);
}

#[test]
fn install_hooks_is_idempotent_and_uninstall_works() {
let dir = assert_fs::TempDir::new().unwrap();
Expand Down
2 changes: 1 addition & 1 deletion crates/tj-mcp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ name = "task-journal-mcp"
path = "src/main.rs"

[dependencies]
tj-core = { package = "task-journal-core", version = "0.14.1", path = "../tj-core" }
tj-core = { package = "task-journal-core", version = "0.14.2", path = "../tj-core" }
anyhow = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
Expand Down
2 changes: 1 addition & 1 deletion plugin/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "task-journal",
"version": "0.14.1",
"version": "0.14.2",
"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"
Expand Down
Loading