Skip to content

Commit ca9cefd

Browse files
committed
feat(v0.5.0): artifacts auto-extract + linked_issue auto-link (Phase B+C)
Closes the auto-everything plan opened by Phase A (v0.4.1). The journal now scrapes commit hashes / PR URLs / ticket IDs / files / branches out of every event automatically, and prompts that mention a known ticket auto-link back to the prior task that handled it. Phase B — artifacts: - tj_core::artifacts module (regex extract + dedup + serde). - events_index.artifacts populated on every ingest_new_events. - db::task_artifacts aggregator. - Pack renders **Artifacts**: commits/PRs/issues/files/branches. - New `task-journal reclassify <id>` backfills v0.4.x events. Phase C — linked_issue / reopen: - db::find_tasks_by_linked_issues searches every task whose events reference a given ticket id. - auto_open_task_from_prompt extracts artifacts from the prompt and adds `linked:tj-old-id` to External when a prior task is found. stderr hint suggests reopen when the prior is closed. - New `task-journal reopen <id> [--reason ...]` flips a closed task back to open via [reopen] event. Schema migration v004 wipes pack cache so Artifacts block appears on next render for existing tasks. 13 new tests (9 unit + 4 integration). 53/53 tj-cli tests, 146/146 tj-core tests, clippy/fmt clean.
1 parent d65dd9f commit ca9cefd

15 files changed

Lines changed: 779 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.4.1"
9+
"version": "0.5.0"
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.4.1",
16+
"version": "0.5.0",
1717
"author": {
1818
"name": "Digital-Threads"
1919
},

CHANGELOG.md

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

88
## [Unreleased]
99

10+
## [0.5.0] - 2026-05-08
11+
12+
Auto-everything release. Phase B + C of the v0.5.0 plan land
13+
together: artifacts get scraped out of every event automatically,
14+
and prompts that mention a known ticket id auto-link back to the
15+
prior task that handled it.
16+
17+
### Added — Phase B (artifacts auto-extract)
18+
- New `tj_core::artifacts` module with `Artifacts` struct +
19+
regex-based `extract(text)`. Pulls commit hashes (7-40 hex), GitHub
20+
/ GitLab PR URLs, ticket IDs (FIN-868 etc), file paths, and branch
21+
names from any free-form text.
22+
- `events_index.artifacts` (added in v0.4.0 schema v003) is now
23+
populated on every `ingest_new_events` call. Per-event JSON keeps
24+
reclassify cheap.
25+
- `db::task_artifacts(conn, task_id)` aggregates and dedupes across
26+
every event of a task.
27+
- Pack output gets a new `**Artifacts**:` block listing commits, PRs,
28+
issues, files, branches when any are present.
29+
- New CLI `task-journal reclassify <task_id>` walks existing events
30+
and backfills `artifacts` for journals upgraded from v0.4.x.
31+
32+
### Added — Phase C (linked_issue / reopen)
33+
- `db::find_tasks_by_linked_issues(conn, issues)` searches every
34+
task whose events reference a given ticket id.
35+
- `auto_open_task_from_prompt` now extracts artifacts from the
36+
prompt; if any ticket id matches a prior task, the new task gets
37+
`linked:tj-old-id` appended to its External column. When the prior
38+
task is closed, a hint goes to stderr suggesting
39+
`task-journal reopen <id>` instead of fresh scope.
40+
- New CLI `task-journal reopen <task_id> [--reason "..."]` flips a
41+
closed task back to open (writes a `[reopen]` event whose lifecycle
42+
hook handles the status flip).
43+
44+
### Schema
45+
- Migration v004 wipes the pack cache so existing tasks pick up the
46+
new Artifacts block on next render. Events still need `reclassify`
47+
to backfill the `artifacts` column for old data.
48+
49+
### Tests
50+
- 9 new unit tests for `tj_core::artifacts` (commit / PR / issue /
51+
file / branch extraction + dedup + JSON round-trip).
52+
- 4 new integration tests in tj-cli covering pack rendering with
53+
artifacts, reclassify backfill, reopen lifecycle, and Phase C
54+
auto-link to closed task.
55+
1056
## [0.4.1] - 2026-05-08
1157

1258
v0.5.0 Phase A — auto-create tasks. Removes the manual

Cargo.lock

Lines changed: 4 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: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ members = [
77
]
88

99
[workspace.package]
10-
version = "0.4.1"
10+
version = "0.5.0"
1111
edition = "2021"
1212
rust-version = "1.88"
1313
license = "MIT"
@@ -39,6 +39,7 @@ ureq = { version = "2", features = ["json"] }
3939
ratatui = "0.29"
4040
crossterm = "0.28"
4141
fd-lock = "4"
42+
regex = "1"
4243

4344
# Test deps
4445
assert_fs = "1"

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.4.1", path = "../tj-core" }
19+
tj-core = { package = "task-journal-core", version = "0.5.0", path = "../tj-core" }
2020
anyhow = { workspace = true }
2121
clap = { workspace = true }
2222
tracing = { workspace = true }

crates/tj-cli/src/main.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,16 @@ enum Commands {
636636
#[arg(long)]
637637
outcome_tag: Option<String>,
638638
},
639+
/// Reopen a previously closed task (writes a `reopen` event and
640+
/// flips status back to `open`). Use when the same scope comes
641+
/// back, e.g. a regression on a shipped fix or a follow-up bug
642+
/// that belongs in the original chain rather than a new task.
643+
Reopen {
644+
task_id: String,
645+
/// One-line reason for reopening (regression, follow-up, etc).
646+
#[arg(long)]
647+
reason: Option<String>,
648+
},
639649
/// Set or update the goal of an existing task.
640650
Goal {
641651
task_id: String,
@@ -653,6 +663,11 @@ enum Commands {
653663
#[arg(long = "add")]
654664
add: String,
655665
},
666+
/// Re-run artifact extraction over every event of a task and
667+
/// refresh the pack cache. Use after upgrading from v0.4.x — older
668+
/// events were ingested before the artifact column was populated,
669+
/// so they have empty `artifacts` JSON until reclassify backfills.
670+
Reclassify { task_id: String },
656671
/// Full-text search across events (FTS5).
657672
Search {
658673
/// Query string.
@@ -1011,6 +1026,38 @@ fn main() -> Result<()> {
10111026
writer.flush_durable()?;
10121027
println!("{}", event.event_id);
10131028
}
1029+
Commands::Reopen { task_id, reason } => {
1030+
// The Reopen event itself flips tasks.status back to open
1031+
// when ingested (db::apply_lifecycle handles this). The CLI
1032+
// job is just to assert the task exists and write the event.
1033+
let cwd = std::env::current_dir()?;
1034+
let project_hash = tj_core::project_hash::from_path(&cwd)?;
1035+
let events_path = tj_core::paths::events_dir()?.join(format!("{project_hash}.jsonl"));
1036+
let state_path = tj_core::paths::state_dir()?.join(format!("{project_hash}.sqlite"));
1037+
let conn = tj_core::db::open(&state_path)?;
1038+
if events_path.exists() {
1039+
tj_core::db::ingest_new_events(&conn, &events_path, &project_hash)?;
1040+
}
1041+
if !tj_core::db::task_exists(&conn, &task_id)? {
1042+
anyhow::bail!("task not found: {task_id}");
1043+
}
1044+
drop(conn);
1045+
1046+
let mut event = tj_core::event::Event::new(
1047+
&task_id,
1048+
tj_core::event::EventType::Reopen,
1049+
tj_core::event::Author::User,
1050+
tj_core::event::Source::Cli,
1051+
reason.clone().unwrap_or_else(|| "(reopened)".into()),
1052+
);
1053+
if let Some(r) = reason {
1054+
event.meta = serde_json::json!({"reason": r});
1055+
}
1056+
let mut writer = tj_core::storage::JsonlWriter::open(&events_path)?;
1057+
writer.append(&event)?;
1058+
writer.flush_durable()?;
1059+
println!("{}", event.event_id);
1060+
}
10141061
Commands::Goal { task_id, text } => {
10151062
let cwd = std::env::current_dir()?;
10161063
let project_hash = tj_core::project_hash::from_path(&cwd)?;
@@ -1043,6 +1090,27 @@ fn main() -> Result<()> {
10431090
tj_core::db::add_task_external(&conn, &task_id, &add)?;
10441091
println!("ok");
10451092
}
1093+
Commands::Reclassify { task_id } => {
1094+
// Walk events_index for this task, re-run artifact extraction
1095+
// over each event's text (looked up via search_fts), and
1096+
// overwrite the artifacts column. Pack cache is wiped after
1097+
// so the next render picks up the new artifacts block. Used
1098+
// primarily to backfill v0.4.x events that were ingested
1099+
// before extraction existed.
1100+
let cwd = std::env::current_dir()?;
1101+
let project_hash = tj_core::project_hash::from_path(&cwd)?;
1102+
let events_path = tj_core::paths::events_dir()?.join(format!("{project_hash}.jsonl"));
1103+
let state_path = tj_core::paths::state_dir()?.join(format!("{project_hash}.sqlite"));
1104+
let conn = tj_core::db::open(&state_path)?;
1105+
if events_path.exists() {
1106+
tj_core::db::ingest_new_events(&conn, &events_path, &project_hash)?;
1107+
}
1108+
if !tj_core::db::task_exists(&conn, &task_id)? {
1109+
anyhow::bail!("task not found: {task_id}");
1110+
}
1111+
let count = tj_core::db::reclassify_task_artifacts(&conn, &task_id)?;
1112+
println!("reclassified {} events", count);
1113+
}
10461114
Commands::EventCorrect {
10471115
corrects,
10481116
task,
@@ -2002,6 +2070,31 @@ fn auto_open_task_from_prompt(
20022070
tj_core::db::set_task_goal(conn, &task_id, &goal)?;
20032071
}
20042072

2073+
// v0.5.0 Phase C: if the prompt mentions any tracked issue
2074+
// identifiers (FIN-868, JIRA-123…), search the journal for prior
2075+
// tasks that referenced the same issue. Auto-link them via the
2076+
// External column and warn on stderr when the prior is closed —
2077+
// user can reopen it instead of starting fresh.
2078+
let prompt_arts = tj_core::artifacts::extract(prompt);
2079+
if !prompt_arts.linked_issues.is_empty() {
2080+
let related = tj_core::db::find_tasks_by_linked_issues(conn, &prompt_arts.linked_issues)?;
2081+
for (other_id, status) in related {
2082+
if other_id == task_id {
2083+
continue;
2084+
}
2085+
let _ = tj_core::db::add_task_external(conn, &task_id, &format!("linked:{other_id}"));
2086+
if status == "closed" {
2087+
eprintln!(
2088+
"task-journal: prompt references issue(s) {}, which appear in closed task {} — \
2089+
run `task-journal reopen {}` if this is a continuation rather than new scope.",
2090+
prompt_arts.linked_issues.join(","),
2091+
other_id,
2092+
other_id
2093+
);
2094+
}
2095+
}
2096+
}
2097+
20052098
Ok(tj_core::classifier::TaskContext {
20062099
task_id,
20072100
title,

0 commit comments

Comments
 (0)