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.31.1"
"version": "0.31.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.31.1",
"version": "0.31.2",
"author": {
"name": "Digital-Threads"
},
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.31.2] - 2026-10-05

### Changed
- The mod shows the session's task in a band above the prompt instead of the
status line under it. Claude Code draws a plugin's status line as one of its
pinned warnings, yellow with a `⚠`, so `⚠ task-journal: 📓 no task` read as an
error. The band shows only while the session has a task; without one the mod
draws nothing. It sits on top of another plugin's band rather than replacing it.

## [0.31.1] - 2026-10-04

### Fixed
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.

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

[workspace.package]
version = "0.31.1"
version = "0.31.2"
edition = "2021"
rust-version = "1.88"
license = "MIT"
Expand All @@ -20,7 +20,7 @@ categories = ["command-line-utilities", "development-tools"]
[workspace.dependencies]
# `version` must equal workspace.package.version: `cargo publish` uploads it as
# the requirement on core. Cargo can't inherit it; plugin_metadata.rs checks it.
tj-core = { package = "task-journal-core", version = "0.31.1", path = "crates/tj-core", default-features = false }
tj-core = { package = "task-journal-core", version = "0.31.2", path = "crates/tj-core", default-features = false }
anyhow = "1"
thiserror = "1"
serde = { version = "1", features = ["derive"] }
Expand Down
5 changes: 3 additions & 2 deletions INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ server) plus thin integrations for each agent:

| Client | What you install | What it gets |
|--------|------------------|--------------|
| Claude Code 2.1.287+ | the plugin (MCP server + skill + mod) | journal tools, the session's task kept in the system prompt, catch-up before compaction, status line |
| Claude Code 2.1.287+ | the plugin (MCP server + skill + mod) | journal tools, the session's task kept in the system prompt, catch-up before compaction, the task above the prompt |
| Claude Code, older | the plugin, optionally `install-hooks` | journal tools, resume packs on session start |
| Codex | `codex mcp add` + `install-hooks --client codex` | journal tools, resume packs, optional auto-capture |
| Any other MCP client | the MCP server | the seven journal tools |
Expand Down Expand Up @@ -52,7 +52,8 @@ The mod runs inside Claude Code and needs no setup. It:
compaction can't lose it, and asks the compaction summary to keep its id;
- gives every session its own active task, even with several sessions open in
one project;
- shows the task in the status line and a short toast when an entry is recorded;
- shows the session's task in a band above the prompt (nothing while there is
none) and a short toast when an entry is recorded;
- reminds the agent to log only after several prompts without an entry, instead
of on every prompt;
- right before a compaction, asks the model which decisions, rejections and
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ that tie it back to the code.

## How it works

- **The mod (Claude Code 2.1.287+).** A plugin of function hooks that runs inside Claude Code and calls the `task-journal` CLI. It keeps the session's active task in the system prompt (a compaction can't drop it), gives each session its own active task, shows the task in the status line, reminds the agent to log only after several prompts without an entry, and — right before a compaction — asks the model what was never logged and records it as `suggested` events. It needs no setup. When it runs, the classic hooks skip what it replaces (the reminder, per-message classification and the transcript catch-ups) and keep the rest (resume packs, push-recall, `/rewind`). Codex and Claude Code older than 2.1.287 keep using the hooks below; there the mod simply doesn't load.
- **The mod (Claude Code 2.1.287+).** A plugin of function hooks that runs inside Claude Code and calls the `task-journal` CLI. It keeps the session's active task in the system prompt (a compaction can't drop it), gives each session its own active task, shows the session's task in a band above the prompt (nothing while it has none), reminds the agent to log only after several prompts without an entry, and — right before a compaction — asks the model what was never logged and records it as `suggested` events. It needs no setup. When it runs, the classic hooks skip what it replaces (the reminder, per-message classification and the transcript catch-ups) and keep the rest (resume packs, push-recall, `/rewind`). Codex and Claude Code older than 2.1.287 keep using the hooks below; there the mod simply doesn't load.
- **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 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.
- **Project chronicle (0.31).** The journal is also a map of the system: the project splits into **modules** — parts of the system by meaning, not folders or tickets — and every task belongs to one or more. Each module has a page: how it works now, the decisions, rejections and constraints of all its tasks, and its history, one line per task. A new task can start from that page; closing it adds its line. The journal suggests a module for a new task (from its words and files, no model call) and names what the chronicle is missing — no map yet, tasks without a module, a module whose state lags behind its tasks — at session start and in tool replies, so the agent keeps it up by itself. `/task-journal:map` (or the skill's steps in any client) maps a project and sorts its past tasks, with the user confirming both. A repository has one map: git worktrees share their main checkout's, and a module's history gathers tasks from every worktree, while each task stays in its own worktree's journal.
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.31.1",
"version": "0.31.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
2 changes: 1 addition & 1 deletion plugin/hooks/hooks.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"modules": ["./register.ts"],
"modules": ["./register.tsx"],
"hooks": {}
}
7 changes: 5 additions & 2 deletions plugin/hooks/journal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,11 @@ describe('texts', () => {

test('status line names the task and its counts', () => {
expect(statusText(s)).toBe('📓 tj-abc · 3 decisions · 1 rejected · 0 evidence')
expect(statusText(none)).toBe('📓 no task in this session · 3 open')
expect(statusText({ active: null, open_tasks: 0, archive: [] })).toBe('📓 no task')
})

test('no task in the session, no status line', () => {
expect(statusText(none)).toBe(null)
expect(statusText({ active: null, open_tasks: 0, archive: [] })).toBe(null)
})

test('the prompt section holds nothing that changes per entry', () => {
Expand Down
7 changes: 3 additions & 4 deletions plugin/hooks/journal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,10 @@ export function parseState(stdout: string): JournalState | null {
}
}

export function statusText(state: JournalState): string {
// The band above the prompt: the session's task, or nothing while it has none.
export function statusText(state: JournalState): string | null {
const a = state.active
if (a === null) {
return state.open_tasks > 0 ? `📓 no task in this session · ${state.open_tasks} open` : '📓 no task'
}
if (a === null) return null

const n = (k: string) => a.counts[k] ?? 0
const where = a.modules.length > 0 ? ` [${a.modules.join(', ')}]` : ''
Expand Down
51 changes: 51 additions & 0 deletions plugin/hooks/register.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { describe, expect, mock, test } from 'claude-code/testing'
import type { Engine } from 'claude-code/testing'
import type { On } from 'claude-code'

const ran = (stdout: string) => ({ exitCode: 0, stdout, stderr: '', isStdoutTruncated: false, isStderrTruncated: false })

const state = (active: unknown) => JSON.stringify({ schema: 'tj-state/1', open_tasks: 1, active })

const TASK = { task_id: 'tj-abc', title: 'Fix refresh', counts: { decision: 2 }, modules: ['auth'] }

// The engine beneath the mod: `task-journal state` answers `stdout`, and
// another plugin's band sits under the mod's line.
function session(on: On, stdout: string) {
on('session.start', (_$, e) => ({ cwd: e.cwd }))
on('session.id', () => ({ value: 'session-1' }))
on('process.run', () => ({ value: ran(stdout) }))
on('ui.render', { component: 'AbovePrompt' }, ($, e) => $.ui.resolve(e).Text({ children: 'other band' }))
mock.env(on, {})
}

// The band above the prompt as the terminal draws it, read as plain text.
async function band($: Engine): Promise<string> {
const props = { hasSurvey: false, isWorking: false } as never
const ui = await $.ui.mount({ plugin: 'task-journal', surface: 'terminal', component: 'AbovePrompt', props })
const textOf = (node: unknown): string => {
if (typeof node === 'string') return node
const children = (node as { children?: unknown[] } | null)?.children ?? []

return children.map(textOf).join('')
}

return textOf(await ui.drawn())
}

const START = { cwd: '/work', surface: 'terminal', isInteractive: true } as const

describe('band above the prompt', () => {
test('names the session task above the other band', async ($, on) => {
session(on, state(TASK))
await $.session.start(START)

expect(await band($)).toBe('📓 tj-abc [auth] · 2 decisions · 0 rejected · 0 evidence' + 'other band')
})

test('without a task the mod draws nothing of its own', async ($, on) => {
session(on, state(null))
await $.session.start(START)

expect(await band($)).toBe('other band')
})
})
24 changes: 22 additions & 2 deletions plugin/hooks/register.ts → plugin/hooks/register.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// cannot lose it, and asks the compaction summary to keep its id;
// - stamps the session id on the journal's MCP calls, so each session has
// its own active task;
// - shows the task in the status line and a toast on every entry;
// - shows the task in a band above the prompt and a toast on every entry;
// - reminds the agent to log only after N turns without an entry;
// - brings up a project-chronicle gap that opened during the session (the
// session start already showed the one it began with);
Expand Down Expand Up @@ -121,6 +121,7 @@ async function refresh($: EngineInterface): Promise<void> {
if (state === null) {
warnOnce($, result)
mod.state = null
$.ui.invalidate('ui.render')
if (result.kind === 'failed') {
mod.retryAt = (await $.clock.now()) + RETRY_AFTER_MS
} else {
Expand All @@ -137,7 +138,7 @@ async function refresh($: EngineInterface): Promise<void> {
mod.session = session
mod.state = state
mod.pinned = state.active?.task_id ?? null
$.ui.status(statusText(state))
$.ui.invalidate('ui.render')
}

// The session id can change under the mod: a /clear starts a new one with
Expand Down Expand Up @@ -235,6 +236,25 @@ export const register: Register = (on, options) => {
return next(e)
})

// A band above the prompt rather than `$.ui.status`: the engine draws that
// line as one of its pinned warnings, yellow with a ⚠, which reads as
// something being wrong. The band holds one tree for every plugin, so the
// line goes on top of what the hooks beneath draw (another mod's band).
on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
const text = mod.isOff || mod.state === null ? null : statusText(mod.state)
if (text === null || e.props.hasSurvey) return next(e)

const { Box, Text } = $.ui.resolve(e)
const below = await next(e)

return (
<Box flexDirection="column">
<Text dimColor>{text}</Text>
{below}
</Box>
)
})

on('prompt.compose', async ($, e, next) => {
const composed = await next(e)
if (mod.isOff) return composed
Expand Down
2 changes: 1 addition & 1 deletion plugin/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "task-journal",
"version": "0.31.1",
"version": "0.31.2",
"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.",
"author": {
"name": "Mher Shahinyan",
Expand Down
Loading