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
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"name": "backlog",
"source": "./",
"description": "Persistent, cross-session task management for Claude Code. Agent coordination, doc-driven development, exposed as MCP tools and skills.",
"version": "2.4.2",
"version": "2.4.3",
"author": {
"name": "BacklogHQ"
},
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "backlog",
"version": "2.4.2",
"version": "2.4.3",
"description": "Persistent, cross-session task management for Claude Code. Tasks survive sessions so work started by one agent can be picked up by another.",
"author": {
"name": "BacklogHQ"
Expand Down
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,24 @@

## [Unreleased]

## 2.4.3 (2026-05-30)

### Added

- **Multi-writer text-index support** — backlog now opens its termlog text index in multi-writer mode via agentdb 2.3 + termlog 0.2. The parent Claude Code process and every spawned subagent / experimental agent-team member can read AND write to the same backlog data directory concurrently without crashing on the prior single-writer `.lock`. Each writer owns its own per-agent termlog files (`manifest-<agentId>.json`, `seg-<agentId>-NNNNNN.seg`, `docids-<agentId>.snap/.log`) and the reader view is the federated union of every writer's commits. `Collection.refresh()` (called automatically before every read and write by the engine's `sync()` helper) picks up other agents' new commits — including BM25 segments — without an expensive rebuild.
- **One-time auto-migration of legacy single-writer text index** — on first launch after upgrade, agentdb detects the legacy `text/manifest.json` and runs `TermLog.migrateLegacy()` once to convert the layout to per-agent files (assigning the migrating agent slot 0). A `console.warn` records the conversion. Existing tasks remain searchable; no manual migration step.
- **Tool-description guardrails for agents** — every mutation tool (`task_done`, `task_delete`, `task_start`, `task_stop`, `task_duplicate`, `task_modify`, `task_annotate`, `task_denotate`, `task_purge`, `task_doc_write`, `task_doc_delete`) now appends a `VERIFY_ID_NOTE` instructing the caller to confirm the UUID via `task_list` / `task_info` before mutating, never relying on IDs from prior sessions or memory. `task_add` / `task_log` / `task_modify` and the `description` field's schema docstring now explicitly call out the 500-char cap and direct agents to `task_doc_write` for longer content (acceptance criteria, requirements, technical context).
- **Skill / agent guidance** — `skills/implement/SKILL.md`, `skills/handoff/SKILL.md`, `skills/refine/SKILL.md`, `skills/plan/SKILL.md`, and `agents/task-planner.md` updated with explicit "use UUIDs from the live lookup, not memory" and "descriptions are one-line summaries; longer content goes in a doc" guidance.

### Changed

- **Bumped `@backloghq/agentdb` from `^2.2.1` to `^2.3.0`** — pulls in multi-writer text-index support (`AgentDBOptions.agentId` plumbed through to `TermLog`, federated reader view, cross-agent tombstoning, per-write flush in multi-writer mode, cheap `Collection.refresh()`). Requires `@backloghq/termlog ^0.2.0`. Backlog already passes a per-agent `BACKLOG_AGENT_ID` (auto-derived when unset), so no app-level wiring change is required.
- **Bumped all other deps to latest** — `zod` 4.3.6 → 4.4.3; dev deps `@types/node` 25.5.2 → 25.9.1, `@vitest/coverage-v8` 4.1.4 → 4.1.7, `eslint` 10.2.0 → 10.4.1, `typescript` ~6.0.2 → ^6.0.3, `typescript-eslint` 8.58.1 → 8.60.0, `vitest` 4.1.4 → 4.1.7. `npm audit fix` resolves 7 transitive vulns (6 moderate, 1 high). Build + lint + 193 tests all pass unchanged.

### Fixed

- **Multi-writer crash class eliminated** — the `IndexLockedError: Index is locked by process <pid>` startup failure that took down the backlog MCP server in any process that wasn't the first to open the data directory (e.g. spawned experimental-agent-team subagents while the parent was running) no longer occurs. Validated end-to-end with 3 concurrent writers (parent + 2 subagents): all see each other's tasks via attribute filters and BM25 text search, including cross-agent updates and deletes.

## 2.4.2 (2026-05-07)

### Changed
Expand Down
4 changes: 2 additions & 2 deletions agents/task-planner.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ You are a task planner. Your job is to break down a goal into a well-structured
- Independent where possible, with explicit dependencies where not

3. **Create tasks** — use `task_add` for each task with:
- A clear, actionable description
- A clear, actionable description (max 500 chars — one-line summary, not a full spec)
- `project` for logical grouping
- `priority`: H for blockers/critical path, M for core work, L for nice-to-have
- `tags` for categorization
- `depends` for ordering constraints (use UUIDs from previously created tasks)
- `scheduled` if the task shouldn't start until a certain date

4. **Write specs for complex tasks** — use `task_doc_write` for any task that needs detailed requirements, acceptance criteria, or technical context.
4. **Write specs for complex tasks** — use `task_doc_write` for any task that needs detailed requirements, acceptance criteria, or technical context. The description field is for the one-line summary; anything longer belongs in the attached doc.

5. **Present the plan** — show tasks in dependency order with IDs, descriptions, and priorities. Highlight which tasks can be started immediately (not blocked).

Expand Down
2 changes: 1 addition & 1 deletion dist/engine/task-schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const SCHEMA_DEF = {
textSearch: true,
fields: {
id: { type: "autoIncrement", description: "Stable short numeric ID for humans (1, 2, 3...). UUIDs are the primary key; this is for CLI/display." },
description: { type: "string", required: true, maxLength: 500, searchable: true, description: "Free-text task description. First line shown in listings; can be multi-line." },
description: { type: "string", required: true, maxLength: 500, searchable: true, description: "Free-text task description (max 500 chars). One-line summary of what needs doing — used in listings, BM25 search, and standups. For acceptance criteria, requirements, technical context, or anything longer than a sentence or two, keep this short and attach a markdown spec via task_doc_write." },
status: { type: "enum", values: ["pending", "completed", "deleted", "recurring"], default: "pending", description: "Lifecycle state. 'recurring' is a template that generates pending instances on its `recur` schedule." },
priority: { type: "enum", values: ["H", "M", "L"], description: "H=high/urgent, M=medium/normal, L=low/backlog. Affects urgency score and default sort order." },
project: { type: "string", pattern: /^[a-zA-Z0-9_-]+$/, description: "Project name for grouping (alphanumeric, hyphens, underscores). E.g. 'backend', 'auth-refactor'." },
Expand Down
6 changes: 6 additions & 0 deletions dist/schemas.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,9 @@ export declare const docOutput: z.ZodObject<{
content: z.ZodString;
}, z.core.$strip>;
export declare const LOCAL_NOTE = " No authentication required. No rate limits. All operations are local to the project's data directory.";
/**
* Reminder appended to every mutation tool's description: agents must verify
* task UUIDs against the live store before mutating. Carried as a constant so
* the wording stays consistent across task_done, task_modify, task_delete, etc.
*/
export declare const VERIFY_ID_NOTE = " Before passing a numeric ID or UUID to this tool, confirm it exists in the current backlog state via task_list or task_info \u2014 do not rely on IDs from prior sessions, memory, or the user's recall, since tasks may have been deleted, completed, or never existed.";
6 changes: 6 additions & 0 deletions dist/schemas.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,9 @@ export const countOutput = z.object({ count: z.number() });
export const stringArrayOutput = z.object({ items: z.array(z.string()) });
export const docOutput = z.object({ content: z.string() });
export const LOCAL_NOTE = " No authentication required. No rate limits. All operations are local to the project's data directory.";
/**
* Reminder appended to every mutation tool's description: agents must verify
* task UUIDs against the live store before mutating. Carried as a constant so
* the wording stays consistent across task_done, task_modify, task_delete, etc.
*/
export const VERIFY_ID_NOTE = " Before passing a numeric ID or UUID to this tool, confirm it exists in the current backlog state via task_list or task_info — do not rely on IDs from prior sessions, memory, or the user's recall, since tasks may have been deleted, completed, or never existed.";
6 changes: 3 additions & 3 deletions dist/tools/docs.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as z from "zod";
import { safe } from "../helpers.js";
import { confirmationOutput, docOutput, LOCAL_NOTE } from "../schemas.js";
import { confirmationOutput, docOutput, LOCAL_NOTE, VERIFY_ID_NOTE } from "../schemas.js";
import { writeDoc, readDoc, deleteDoc } from "../engine/index.js";
export function registerDocTools(server, config) {
server.registerTool("task_doc_write", {
Expand All @@ -9,7 +9,7 @@ export function registerDocTools(server, config) {
"Each task has at most one document; writing replaces any existing doc content. Errors if the task is not found. " +
"Automatically adds +doc tag and has_doc:true to the task, so docs are discoverable via task_list filter:'+doc'. " +
"This operation can be reversed with task_undo (restores previous doc state). " +
"For short notes, use task_annotate instead. To read the doc, use task_doc_read. To remove it, use task_doc_delete." + LOCAL_NOTE,
"For short notes, use task_annotate instead. To read the doc, use task_doc_read. To remove it, use task_doc_delete." + VERIFY_ID_NOTE + LOCAL_NOTE,
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
outputSchema: confirmationOutput,
inputSchema: z.object({
Expand Down Expand Up @@ -50,7 +50,7 @@ export function registerDocTools(server, config) {
title: "Delete Task Doc",
description: "Remove the markdown document attached to a task. Returns a confirmation on success. Errors if the task is not found. " +
"Clears the +doc tag and has_doc field. The document content is permanently deleted and cannot be recovered. " +
"Calling on a task with no document is a no-op. To update a document instead of removing it, use task_doc_write." + LOCAL_NOTE,
"Calling on a task with no document is a no-op. To update a document instead of removing it, use task_doc_write." + VERIFY_ID_NOTE + LOCAL_NOTE,
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
outputSchema: confirmationOutput,
inputSchema: z.object({
Expand Down
19 changes: 11 additions & 8 deletions dist/tools/lifecycle.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import * as z from "zod";
import { safe, parseTags } from "../helpers.js";
import { confirmationOutput, LOCAL_NOTE } from "../schemas.js";
import { confirmationOutput, LOCAL_NOTE, VERIFY_ID_NOTE } from "../schemas.js";
import { addTask, taskCommand, logTask, duplicateTask } from "../engine/index.js";
export function registerLifecycleTools(server, config) {
server.registerTool("task_add", {
title: "Add Task",
description: "Create a new pending task and return its UUID. Only 'description' is required; all other fields are optional. " +
"The task gets a stable numeric ID and UUID for cross-session references. Returns a confirmation with the UUID on success. " +
"Description is a SHORT SUMMARY (≤500 chars) — one-line statement of what needs doing. " +
"For acceptance criteria, technical context, requirements, or anything longer than a sentence or two, " +
"create the task with a short description and then call task_doc_write to attach a markdown spec. " +
"Errors if description is empty or exceeds 500 chars, project name has invalid characters, or dates are unparseable. " +
"This operation can be reversed with task_undo. " +
"To record already-completed work, use task_log instead. To copy an existing task, use task_duplicate." + LOCAL_NOTE,
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
outputSchema: confirmationOutput,
inputSchema: z.object({
description: z.string().describe("Task description (required, max 500 chars). Brief summary of what needs to be done."),
description: z.string().describe("Task description (required, max 500 chars). One-line summary of what needs doing. For longer content (acceptance criteria, requirements, context) keep this short and use task_doc_write to attach a markdown spec afterwards."),
project: z.string().optional().describe("Project name for grouping (alphanumeric, hyphens, underscores). E.g. 'backend', 'auth-refactor'"),
tags: z.string().optional().describe("Tags as comma-separated list or JSON array. E.g. 'bug,urgent' or '[\"bug\",\"urgent\"]'. Used for filtering with +tag/-tag syntax."),
priority: z.enum(["H", "M", "L"]).optional().describe("Priority: H (high), M (medium), L (low). Affects urgency score and sort order."),
Expand Down Expand Up @@ -67,7 +70,7 @@ export function registerLifecycleTools(server, config) {
"The task remains in the database and appears in completed task queries (filter: 'status:completed'). " +
"If the task is a recurring instance, completing it triggers generation of the next instance. " +
"This operation can be reversed with task_undo. Calling task_done on an already-completed task returns an error. " +
"To record work that was already done without creating a pending task first, use task_log instead." + LOCAL_NOTE,
"To record work that was already done without creating a pending task first, use task_log instead." + VERIFY_ID_NOTE + LOCAL_NOTE,
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
outputSchema: confirmationOutput,
inputSchema: z.object({
Expand All @@ -85,7 +88,7 @@ export function registerLifecycleTools(server, config) {
description: "Soft-delete a task by setting its status to 'deleted'. Returns a confirmation message on success. " +
"Errors if the task is not found. The task remains in the database and can be restored with task_undo. " +
"Deleted tasks are excluded from default queries but visible with filter 'status:deleted'. " +
"To permanently erase a deleted task, use task_purge. For bulk cleanup, use task_archive instead." + LOCAL_NOTE,
"To permanently erase a deleted task, use task_purge. For bulk cleanup, use task_archive instead." + VERIFY_ID_NOTE + LOCAL_NOTE,
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
outputSchema: confirmationOutput,
inputSchema: z.object({
Expand All @@ -104,7 +107,7 @@ export function registerLifecycleTools(server, config) {
"Returns a confirmation on success. Errors if the task is not found or is not in pending status. " +
"Started tasks appear in +ACTIVE queries and get a higher urgency score. " +
"Starting an already-active task is idempotent (no error). This operation can be reversed with task_undo. " +
"Use task_stop when pausing work, or task_done when finished. Multiple tasks can be active simultaneously." + LOCAL_NOTE,
"Use task_stop when pausing work, or task_done when finished. Multiple tasks can be active simultaneously." + VERIFY_ID_NOTE + LOCAL_NOTE,
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
outputSchema: confirmationOutput,
inputSchema: z.object({
Expand All @@ -122,7 +125,7 @@ export function registerLifecycleTools(server, config) {
description: "Stop actively working on a task by clearing the start timestamp. " +
"Returns a confirmation on success. Errors if the task is not found or is not currently active. " +
"The task returns to pending status and no longer appears in +ACTIVE queries. " +
"This operation can be reversed with task_undo. To finish a task instead of pausing, use task_done." + LOCAL_NOTE,
"This operation can be reversed with task_undo. To finish a task instead of pausing, use task_done." + VERIFY_ID_NOTE + LOCAL_NOTE,
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
outputSchema: confirmationOutput,
inputSchema: z.object({
Expand All @@ -145,7 +148,7 @@ export function registerLifecycleTools(server, config) {
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
outputSchema: confirmationOutput,
inputSchema: z.object({
description: z.string().describe("Description of the completed work (required, max 500 chars)."),
description: z.string().describe("Description of the completed work (required, max 500 chars). Keep to a one-line summary; for longer notes attach a markdown doc via task_doc_write afterwards."),
project: z.string().optional().describe("Project name for grouping."),
tags: z.string().optional().describe("Tags as comma-separated list. E.g. 'done,reviewed'"),
priority: z.enum(["H", "M", "L"]).optional().describe("Priority: H (high), M (medium), L (low)."),
Expand Down Expand Up @@ -179,7 +182,7 @@ export function registerLifecycleTools(server, config) {
description: "Create a new pending task by copying an existing one, optionally overriding specific fields. Returns a confirmation with the new UUID. " +
"The copy gets a new UUID and ID; start/end timestamps and status are reset to pending. " +
"Errors if the source task is not found, or if overridden fields fail validation (invalid project name, bad date, etc). " +
"This operation can be reversed with task_undo. For creating from scratch, use task_add instead." + LOCAL_NOTE,
"This operation can be reversed with task_undo. For creating from scratch, use task_add instead." + VERIFY_ID_NOTE + LOCAL_NOTE,
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
outputSchema: confirmationOutput,
inputSchema: z.object({
Expand Down
Loading
Loading