Skip to content

Commit ae1c99a

Browse files
Shahinyanmclaude
andcommitted
release: v0.2.6 — backfill onboarding + project boundary normalization
Bundles two follow-ups to the v0.2.5 SessionStart resume-pack: 1. install-hooks --backfill: re-execs `task-journal backfill` against the current directory after writing settings.json. Replaces the manual two-step (install-hooks → cd repo → backfill) with a single command for first-time users. 2. project_hash root walk-up: subdirs now hash to their git/.task- journal root instead of being treated as separate projects. Previously, opening Claude Code in repo/src/ silently produced an empty journal because the hash didn't match repo/. Worktrees handled via .git-as-file detection. Three new tests cover the git root, the .task-journal/ override, and the worktree case. CHANGELOG entry covers all three feature lines (SessionStart, --backfill, project_root) under [0.2.6]. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 755b883 commit ae1c99a

10 files changed

Lines changed: 146 additions & 13 deletions

File tree

.claude-plugin/marketplace.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66
},
77
"metadata": {
88
"description": "Task Journal — append-only reasoning chain memory for AI-coding tasks",
9-
"version": "0.2.5"
9+
"version": "0.2.6"
1010
},
1111
"plugins": [
1212
{
1313
"name": "task-journal",
1414
"source": "./plugin",
1515
"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.",
16-
"version": "0.2.5",
16+
"version": "0.2.6",
1717
"author": {
1818
"name": "Digital-Threads"
1919
},

CHANGELOG.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10-
## [0.2.5] - 2026-05-07
10+
## [0.2.6] - 2026-05-07
11+
12+
Three additive features that close the "auto-memory" loop end-to-end:
13+
the journal can now (1) surface itself at session start, (2) seed
14+
itself from existing Claude Code history at install time, and (3)
15+
recognize a project regardless of which subdir you launch from.
16+
17+
### Added
18+
- **SessionStart resume-pack injection**. `task-journal ingest-hook
19+
--kind=SessionStart` now opens the project's journal, renders the
20+
three most-recent open tasks in compact mode, and writes a
21+
`hookSpecificOutput.additionalContext` envelope to stdout. Claude
22+
Code merges that into the system prompt so a new session starts
23+
with the journal's state already in context — no manual
24+
`task_pack` call needed. Empty stdout when there are no open
25+
tasks, so fresh projects don't get noise.
26+
`install-hooks` automatically wires the `SessionStart` event
27+
alongside the existing three.
28+
- **`install-hooks --backfill`**. After writing the hook entries,
29+
re-execs `task-journal backfill` against the current directory so
30+
first-time users get an auto-populated journal from their existing
31+
Claude Code history. Onboarding becomes one command.
32+
- **Project-root normalization in `project_hash`**. `repo/`,
33+
`repo/src/`, and `repo/src/foo/bar/` now hash to the same project
34+
by walking up to the first `.git` (file or directory, so worktrees
35+
work) or `.task-journal/` marker. Without this, opening Claude
36+
Code in a subdir gave an empty journal and silently broke
37+
continuity. `.task-journal/` is the explicit override for
38+
intentional sub-projects.
1139

1240
DX improvement: ship classifier-wrapper config through `install-hooks`
1341
no more manual `bashrc` / `settings.json` edits to use `aimux`, `direnv`,

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.2.5"
10+
version = "0.2.6"
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
@@ -16,7 +16,7 @@ name = "task-journal"
1616
path = "src/main.rs"
1717

1818
[dependencies]
19-
tj-core = { package = "task-journal-core", version = "0.2.5", path = "../tj-core" }
19+
tj-core = { package = "task-journal-core", version = "0.2.6", path = "../tj-core" }
2020
anyhow = { workspace = true }
2121
clap = { workspace = true }
2222
tracing = { workspace = true }

crates/tj-cli/src/main.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,11 @@ enum Commands {
655655
/// Default: classifier uses `claude -p`.
656656
#[arg(long)]
657657
classifier_command: Option<String>,
658+
/// After installing hooks, retro-import existing Claude Code session
659+
/// history for the current project. Equivalent to running
660+
/// `task-journal backfill` afterwards. Onboarding shortcut.
661+
#[arg(long)]
662+
backfill: bool,
658663
},
659664
/// Show local classifier and journal statistics.
660665
Stats,
@@ -950,6 +955,7 @@ fn main() -> Result<()> {
950955
scope,
951956
uninstall,
952957
classifier_command,
958+
backfill,
953959
} => {
954960
let settings_path = match scope.as_str() {
955961
"user" => {
@@ -1026,6 +1032,24 @@ fn main() -> Result<()> {
10261032
}
10271033
std::fs::write(&settings_path, serde_json::to_string_pretty(&current)?)?;
10281034
println!("{}", settings_path.display());
1035+
1036+
// Onboarding convenience: retro-import existing Claude Code history
1037+
// so the journal isn't empty on day one. Always operates on the
1038+
// current working directory; install-hooks scope is independent.
1039+
// We re-exec ourselves rather than refactoring the (~150-line)
1040+
// backfill body — keeps the pipe simple and the output identical
1041+
// to a manual `task-journal backfill`.
1042+
if !uninstall && backfill {
1043+
let exe =
1044+
std::env::current_exe().context("locate task-journal binary for backfill")?;
1045+
let status = std::process::Command::new(&exe)
1046+
.arg("backfill")
1047+
.status()
1048+
.with_context(|| format!("spawn `{} backfill`", exe.display()))?;
1049+
if !status.success() {
1050+
eprintln!("backfill exited with {status}");
1051+
}
1052+
}
10291053
}
10301054
Commands::Stats => {
10311055
let metrics_dir = tj_core::paths::metrics_dir()?;

crates/tj-core/src/project_hash.rs

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,41 @@
11
use anyhow::Context;
22
use sha2::{Digest, Sha256};
3-
use std::path::Path;
3+
use std::path::{Path, PathBuf};
4+
5+
/// Walk up from `start` to a project boundary so that running
6+
/// `task-journal` from `repo/`, `repo/src/`, and `repo/src/foo/bar/`
7+
/// all hash to the same project. Without this normalization, opening
8+
/// Claude Code in a subdir gave an empty journal — broke the
9+
/// "auto-memory" promise.
10+
///
11+
/// Boundary markers, priority order:
12+
/// 1. `.task-journal/` directory — explicit opt-in for sub-projects
13+
/// that intentionally want a separate journal from their parent.
14+
/// 2. `.git` (file or directory) — covers normal checkouts and
15+
/// worktrees alike (a worktree's root holds a `.git` *file*
16+
/// pointing at the real gitdir, but its presence still marks the
17+
/// boundary).
18+
///
19+
/// Falls back to `start` if no marker is found, preserving prior
20+
/// behaviour for non-git scratch directories.
21+
fn project_root(start: &Path) -> PathBuf {
22+
let mut cur = start;
23+
loop {
24+
if cur.join(".task-journal").is_dir() || cur.join(".git").exists() {
25+
return cur.to_path_buf();
26+
}
27+
match cur.parent() {
28+
Some(p) => cur = p,
29+
None => return start.to_path_buf(),
30+
}
31+
}
32+
}
433

534
pub fn from_path(p: impl AsRef<Path>) -> anyhow::Result<String> {
635
let canonical = dunce::canonicalize(p.as_ref())
736
.with_context(|| format!("canonicalize {:?}", p.as_ref()))?;
8-
let bytes = canonical.as_os_str().as_encoded_bytes();
37+
let root = project_root(&canonical);
38+
let bytes = root.as_os_str().as_encoded_bytes();
939
let mut h = Sha256::new();
1040
h.update(bytes);
1141
let digest = h.finalize();
@@ -36,4 +66,55 @@ mod tests {
3666
let b = from_path(d2.path()).unwrap();
3767
assert_ne!(a, b);
3868
}
69+
70+
#[test]
71+
fn subdir_under_git_root_hashes_to_root() {
72+
// repo/ with .git inside; repo/src/foo/ should normalise to repo/.
73+
let repo = TempDir::new().unwrap();
74+
std::fs::create_dir(repo.path().join(".git")).unwrap();
75+
let sub = repo.path().join("src").join("foo");
76+
std::fs::create_dir_all(&sub).unwrap();
77+
78+
let root_hash = from_path(repo.path()).unwrap();
79+
let sub_hash = from_path(&sub).unwrap();
80+
assert_eq!(
81+
root_hash, sub_hash,
82+
"subdir of a git repo must hash to the repo root, not the subdir"
83+
);
84+
}
85+
86+
#[test]
87+
fn dot_task_journal_marker_overrides_git_boundary() {
88+
// repo/.git + repo/sub/.task-journal/. Then sub is its own project
89+
// (explicit opt-out of the parent's journal).
90+
let repo = TempDir::new().unwrap();
91+
std::fs::create_dir(repo.path().join(".git")).unwrap();
92+
let sub = repo.path().join("sub");
93+
std::fs::create_dir(&sub).unwrap();
94+
std::fs::create_dir(sub.join(".task-journal")).unwrap();
95+
96+
let root_hash = from_path(repo.path()).unwrap();
97+
let sub_hash = from_path(&sub).unwrap();
98+
assert_ne!(
99+
root_hash, sub_hash,
100+
"subdir with .task-journal/ marker must NOT inherit parent's project hash"
101+
);
102+
}
103+
104+
#[test]
105+
fn dot_git_file_in_worktree_root_is_a_boundary() {
106+
// Worktrees have a `.git` *file* (not a dir) at their root.
107+
// We must still treat that as a boundary.
108+
let wt = TempDir::new().unwrap();
109+
std::fs::write(wt.path().join(".git"), "gitdir: /elsewhere\n").unwrap();
110+
let sub = wt.path().join("inner");
111+
std::fs::create_dir(&sub).unwrap();
112+
113+
let wt_hash = from_path(wt.path()).unwrap();
114+
let sub_hash = from_path(&sub).unwrap();
115+
assert_eq!(
116+
wt_hash, sub_hash,
117+
"worktree subdir must normalise to worktree root via .git file"
118+
);
119+
}
39120
}

crates/tj-mcp/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ name = "task-journal-mcp"
1616
path = "src/main.rs"
1717

1818
[dependencies]
19-
tj-core = { package = "task-journal-core", version = "0.2.5", path = "../tj-core" }
19+
tj-core = { package = "task-journal-core", version = "0.2.6", path = "../tj-core" }
2020
anyhow = { workspace = true }
2121
tokio = { workspace = true }
2222
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.2.5",
3+
"version": "0.2.6",
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"

plugin/package.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.2.5",
3+
"version": "0.2.6",
44
"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.",
55
"author": {
66
"name": "Mher Shahinyan",

0 commit comments

Comments
 (0)