diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 46b9178..64c245a 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.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" }, diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a91acb..c0ade52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Cargo.lock b/Cargo.lock index 5edf7e9..b11cc3d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2516,7 +2516,7 @@ dependencies = [ [[package]] name = "task-journal-cli" -version = "0.31.1" +version = "0.31.2" dependencies = [ "anyhow", "assert_cmd", @@ -2540,7 +2540,7 @@ dependencies = [ [[package]] name = "task-journal-core" -version = "0.31.1" +version = "0.31.2" dependencies = [ "anyhow", "chrono", @@ -2565,7 +2565,7 @@ dependencies = [ [[package]] name = "task-journal-mcp" -version = "0.31.1" +version = "0.31.2" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index caeb0f6..17857aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ members = [ ] [workspace.package] -version = "0.31.1" +version = "0.31.2" edition = "2021" rust-version = "1.88" license = "MIT" @@ -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"] } diff --git a/INSTALL.md b/INSTALL.md index 9dabf7a..3446875 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -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 | @@ -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 diff --git a/README.md b/README.md index 03d5e02..cc3c521 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json index 116ff10..677c788 100644 --- a/plugin/.claude-plugin/plugin.json +++ b/plugin/.claude-plugin/plugin.json @@ -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" diff --git a/plugin/hooks/hooks.json b/plugin/hooks/hooks.json index b614c24..fbc71ee 100644 --- a/plugin/hooks/hooks.json +++ b/plugin/hooks/hooks.json @@ -1,4 +1,4 @@ { - "modules": ["./register.ts"], + "modules": ["./register.tsx"], "hooks": {} } diff --git a/plugin/hooks/journal.test.ts b/plugin/hooks/journal.test.ts index 4f7f389..e6c1ad8 100644 --- a/plugin/hooks/journal.test.ts +++ b/plugin/hooks/journal.test.ts @@ -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', () => { diff --git a/plugin/hooks/journal.ts b/plugin/hooks/journal.ts index 4677cae..9e17d0a 100644 --- a/plugin/hooks/journal.ts +++ b/plugin/hooks/journal.ts @@ -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(', ')}]` : '' diff --git a/plugin/hooks/register.test.ts b/plugin/hooks/register.test.ts new file mode 100644 index 0000000..c6e2a99 --- /dev/null +++ b/plugin/hooks/register.test.ts @@ -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 { + 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') + }) +}) diff --git a/plugin/hooks/register.ts b/plugin/hooks/register.tsx similarity index 93% rename from plugin/hooks/register.ts rename to plugin/hooks/register.tsx index 97025bf..c2ea6e3 100644 --- a/plugin/hooks/register.ts +++ b/plugin/hooks/register.tsx @@ -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); @@ -121,6 +121,7 @@ async function refresh($: EngineInterface): Promise { 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 { @@ -137,7 +138,7 @@ async function refresh($: EngineInterface): Promise { 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 @@ -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 ( + + {text} + {below} + + ) + }) + on('prompt.compose', async ($, e, next) => { const composed = await next(e) if (mod.isOff) return composed diff --git a/plugin/package.json b/plugin/package.json index 40b3672..241a347 100644 --- a/plugin/package.json +++ b/plugin/package.json @@ -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",