Skip to main content
Glama
README.md
# handoff-mcp

An MCP server that gives AI coding agents persistent memory across sessions.

When you close a Claude Code session and start a new one, the new session has no idea what the previous one was doing. handoff-mcp solves this by saving session context — tasks, decisions, blockers, and file pointers — to a local `.handoff/` directory that the next session can load automatically.

## The Problem

AI coding sessions are stateless. Every new session starts from zero:

- **"What was I working on?"** — the agent doesn't know
- **"What decisions were made?"** — lost with the previous context window
- **"What's left to do?"** — you have to re-explain everything

This gets painful fast on multi-session projects.

## How It Works

```
Session 1                          Session 2
┌──────────────┐                   ┌──────────────┐
│ Working...   │   .handoff/       │ load_context │
│              │──────────────────>│  ↓ guidance  │
│ save_context │   tasks/          │ save_context │
│  - close     │   sessions/      │  (active)    │
│  - summary   │   config.toml    │  ↓ work...   │
│  - decisions │                   │ save_context │
│  - blockers  │                   │  (close)     │
└──────────────┘                   └──────────────┘
```

At session start, the agent calls `handoff_load_context` to pick up where things left off. If no active session exists, the response includes `session_guidance` prompting the agent to establish one via `handoff_save_context` with `session_status: "active"` — this creates a persistent `.active.json` that survives interruptions. At session end, the agent calls `handoff_save_context` (defaulting to `session_status: "closed"`) to close the session.

## Installation

### Codex CLI plugin (recommended for Codex users)

Install the Handoff binary, add the GitHub marketplace, install the Codex Task
Loop plugin, and register the Handoff MCP server:

```bash
npm install -g handoff-mcp-server
codex plugin marketplace add alphaelements/handoff-mcp
codex plugin add handoff-task-loop-codex@handoff-mcp-marketplace
codex mcp add handoff -- handoff-mcp
```

Confirm the plugin and MCP server, then begin a new Codex session:

```bash
handoff-mcp --version
codex plugin list
codex mcp get handoff
```

Use `$handoff-session-loop` only when you want to explicitly run ready Handoff
tasks through the Codex developer, tester, and reviewer loop. For regular
session continuity, use the Handoff MCP tools and this repository's
[`AGENTS.md`](AGENTS.md) guidance.

**Local checkout:** replace `alphaelements/handoff-mcp` with the absolute path
to this repository in the marketplace command. Reinstall the plugin after an
update and start a new Codex session so it reloads the bundled skill.

**Direct MCP alternative:** users who do not need the task loop can install the
binary and run only `codex mcp add handoff -- handoff-mcp`. This supplies the
Handoff tools but not `$handoff-session-loop`.

For a controlled non-interactive loop, permit Handoff MCP writes for that one
invocation; otherwise `codex exec` can cancel task-state changes:

```bash
codex exec -c 'mcp_servers.handoff.default_tools_approval_mode = "auto"' \
  --sandbox workspace-write '$handoff-session-loop <scoped task request>'
```

**Updating:** run `codex plugin marketplace upgrade handoff-mcp-marketplace`,
then reinstall with `codex plugin add handoff-task-loop-codex@handoff-mcp-marketplace`.
Update the binary separately with `npm install -g handoff-mcp-server@latest`
or `cargo install handoff-mcp --force`.

> **Claude Code compatibility:** Codex uses `$handoff-session-loop`; Claude
> Code uses `/session-loop` and `/research-loop`. The Codex plugin does not
> execute Claude commands, agents, or Workflow DSL.

### Claude Code Plugin (recommended)

The easiest way to install handoff-mcp is as a Claude Code plugin.

```bash
# 1. Install the binary (required — the plugin calls it)
npm install -g handoff-mcp-server
# or: cargo install handoff-mcp

# 2. Add the marketplace (GitHub repo)
/plugin marketplace add alphaelements/handoff-mcp

# 3. Install the plugin (MCP server + skills)
/plugin install handoff-mcp@handoff-mcp-marketplace

# 4. Apply the change
/reload-plugins
```

This registers the MCP server and all skills automatically — no manual
`.mcp.json` or skill file setup needed. The `handoff-mcp` plugin is enabled
on install, so no separate `/plugin enable` is needed. Run `/reload-plugins`
to pick up the change mid-session (a Claude Code restart also applies it).

> **Naming**: `handoff-mcp` is the *plugin* name; `handoff-mcp-marketplace`
> is the *marketplace* name (the `name` field in `.claude-plugin/marketplace.json`).
> Install commands always use `<plugin>@<marketplace>`.

#### Platform support

The npm package ships **prebuilt binaries** — no Rust toolchain, no compiler,
and no install scripts are needed. npm downloads only the binary matching your
platform.

| Platform | x64 | arm64 |
|---|---|---|
| Linux (glibc 2.35+) | ✅ | ✅ |
| macOS | ✅ | ✅ |
| Windows | ✅ | ✅ |

WSL works as a plain Linux install. For anything else — musl/Alpine, FreeBSD,
32-bit, or glibc older than 2.35 — install with `cargo install handoff-mcp`
instead, which builds from source.

If you already have a binary you built yourself, point the npm wrapper at it
with `HANDOFF_MCP_BINARY_PATH=/path/to/handoff-mcp`.

> **Note**: installing with `--omit=optional` skips the prebuilt binary and
> leaves the CLI unable to start. Reinstall without that flag.
>
> **Alpine / musl**: the Linux binaries are glibc-linked. npm may install one
> anyway, and it fails to exec with a confusing "not found" error even though
> the file is present. Use `cargo install handoff-mcp` on musl systems.

**Optional: task loop (automated TDD + research workflows)**

```bash
/plugin install handoff-task-loop@handoff-mcp-marketplace
/plugin enable handoff-task-loop@handoff-mcp-marketplace
/reload-plugins
```

Adds `/session-loop` (parallel TDD implementation, adversarial testing, Opus
review) and `/research-loop` (multi-agent investigation, verification, spec
drafting). See [plugin-task-loop/README.md](plugin-task-loop/README.md).

**Optional: memory auto-injection hooks**

```bash
/plugin install handoff-mcp-hooks@handoff-mcp-marketplace
/plugin enable handoff-mcp-hooks@handoff-mcp-marketplace
/reload-plugins
```

This adds hooks that inject relevant project memories on every prompt and file
edit. Disable anytime with `/plugin disable handoff-mcp-hooks@handoff-mcp-marketplace` —
the MCP server and skills remain active.

> **Important:** The hooks require a `handoff` MCP server entry in the
> project's `.mcp.json`. Run `handoff-mcp setup --mcp-json` in the project
> directory to add it automatically, or add it manually:
>
> ```json
> {
>   "mcpServers": {
>     "handoff": {
>       "type": "stdio",
>       "command": "handoff-mcp",
>       "args": [],
>       "env": {}
>     }
>   }
> }
> ```
>
> Without this entry, the hooks will show "not connected" errors on every
> prompt. The plugin's built-in MCP server is accessible to tools but not
> to hooks — this is a Claude Code limitation.

> The `handoff-task-loop` and `handoff-mcp-hooks` plugins ship with
> `defaultEnabled: false`, so they need an explicit `/plugin enable` step after
> install. The main `handoff-mcp` plugin is `defaultEnabled: true` and skips it.

**Installing the local development version instead**

If you are hacking on handoff-mcp and want Claude Code to load your local
checkout rather than the published GitHub version, register the repository root
(the directory containing `.claude-plugin/marketplace.json`) as a local
marketplace:

```bash
# 1. Build the binary and sync skills + plugin caches
./scripts/install-local.sh

# 2. Register the repo root as a local marketplace (first time only).
#    Use the local path here — not the alphaelements/handoff-mcp shorthand.
/plugin marketplace add /absolute/path/to/handoff-mcp

# 3. Install and apply
/plugin install handoff-mcp@handoff-mcp-marketplace
/reload-plugins
```

After the first setup, re-run `./scripts/install-local.sh` whenever you change
the code, then restart Claude Code (or `/reload-plugins`) to load the rebuilt
version. Note that `install-local.sh` **only rebuilds the binary and refreshes
the plugin cache** — it does not register the marketplace or enable the plugin,
so steps 2 and 3 are a one-time bootstrap.

### Updating

If you installed handoff-mcp as a plugin, it updates along **two independent
paths**, and you need both. The plugin does not bundle the binary:
`plugin.json` registers the MCP server as `command: "handoff-mcp"`, which
Claude Code resolves on your `PATH`. So the marketplace ships the skills and
the plugin manifest, while npm or cargo ships the executable that actually
implements the MCP tools.

(Installed without the plugin, via `cargo install` or `npm install -g` alone?
Then only step 1 and the restart apply.)

Updating only the plugin leaves you on the old MCP tools. Updating only the
binary leaves you on the old skills.

```bash
# 1. Binary — this is what implements the MCP tools
npm install -g handoff-mcp-server@latest
# or: cargo install handoff-mcp --force

# 2. Marketplace catalog — fetch the new version list
/plugin marketplace update handoff-mcp-marketplace

# 3. Plugin — skills and manifest
/plugin update handoff-mcp@handoff-mcp-marketplace

# 4. Restart Claude Code
```

Update the optional plugins the same way if you installed them:

```bash
/plugin update handoff-task-loop@handoff-mcp-marketplace
/plugin update handoff-mcp-hooks@handoff-mcp-marketplace
```

Steps 1 and 3 update different things, and neither substitutes for the other.
`/plugin update` never touches the binary — it swaps the cached plugin directory,
which contains no executable at all — so no amount of restarting will give you
new MCP tools if you skipped step 1. Conversely, step 1 rewrites the file on disk
but the MCP server Claude Code already spawned keeps running the old image, so
you get the new tools only after step 4.

Step 4 is therefore not optional, and `/reload-plugins` is not a substitute: it
refreshes skills, not the MCP server process.

Do not expect step 1 to take effect on its own. An installer that overwrites a
*running* binary in place fails on Linux with `Text file busy`; installers that
replace the file instead (unlink, then create — what `install-local.sh` does)
succeed, and leave the already-running server executing the now-deleted old
image until it restarts. Either way, the new tools appear only after step 4.

Verify the update landed:

```bash
which handoff-mcp          # the binary Claude Code will actually run
handoff-mcp --version
claude plugin list         # plugin version, per marketplace
```

`claude plugin list` reports the version you actually have installed. Note that
`claude plugin details` reads the marketplace source instead, so it shows the
version on offer whether or not you have updated to it — don't use it to
confirm an update.

**Local development checkout**: `./scripts/install-local.sh` does both halves at
once (rebuilds the binary into `~/.local/bin` and refreshes the plugin cache).
Restart Claude Code afterwards.

**Troubleshooting**

- **Plugin or skills don't show up** — run `/reload-plugins`, or restart Claude
  Code. As a last resort, `rm -rf ~/.claude/plugins/cache` and reinstall.
- **`plugin not found`** — refresh the catalog with
  `/plugin marketplace update handoff-mcp-marketplace`, then reinstall.
- **MCP server won't start** — open `/plugin` → **Errors** tab, and confirm the
  binary is on your `PATH` (`which handoff-mcp`).
- **You updated, but the MCP tools still behave like the old version** — you
  almost certainly updated the plugin without updating the binary, or you
  updated the binary but did not restart Claude Code. Work through the four
  steps above in order.
- **An older `handoff-mcp` earlier on your `PATH` shadows the new one.** This is
  the most common cause of "I updated and nothing changed", because
  `npm install -g` and `cargo install` write to *different* directories. List
  every copy and see which one wins:

  ```bash
  type -a handoff-mcp                       # every match, in resolution order
  for d in ${PATH//:/ }; do
    [ -x "$d/handoff-mcp" ] && echo "$d -> $("$d/handoff-mcp" --version)"
  done
  ```

  The first line is the one Claude Code runs. Remove the stale copies (e.g.
  `cargo uninstall handoff-mcp`, or delete the old file), or put the directory
  holding the current binary earlier on your `PATH`.

- **`--version` says the right number but the behavior is old.** A version
  string only changes when a release bumps it, so two builds of the *same*
  version — a stale `cargo install` and a fresh one — report identically.
  Compare the file itself rather than the version:

  ```bash
  ls -l "$(which handoff-mcp)"   # check the mtime
  ```

  When in doubt, reinstall the binary and restart Claude Code.

- **Old versions pile up in the plugin cache.** Claude Code keeps each installed
  version in its own directory under
  `~/.claude/plugins/cache/handoff-mcp-marketplace/<plugin>/<version>/`. This is
  harmless — the active version is recorded in
  `~/.claude/plugins/installed_plugins.json` — but you can reclaim the space by
  deleting the directories for versions you no longer use.

### cargo

```bash
cargo install handoff-mcp
```

### npm

```bash
npm install -g handoff-mcp-server
```

Both install the same binary, by different routes. `cargo install` fetches the
crate from [crates.io](https://crates.io/crates/handoff-mcp) and compiles it,
so it needs a Rust toolchain and works on any target Rust supports.
`npm install` downloads a prebuilt binary for your platform and compiles
nothing — see [Platform support](#platform-support) for the list.

### Build from source

```bash
git clone https://github.com/alphaelements/handoff-mcp.git
cd handoff-mcp
cargo build --release
```

## Setup (non-plugin)

If you installed via cargo/npm (without the plugin), register handoff-mcp as an
MCP server in Claude Code manually:

**Option A** — CLI (recommended):

```bash
claude mcp add -s user handoff -- handoff-mcp
```

The `-s user` flag registers it globally (available in all projects). Verify with `claude mcp get handoff`.

**Option B** — Manual edit of `~/.claude.json`:

```json
{
  "mcpServers": {
    "handoff": {
      "type": "stdio",
      "command": "handoff-mcp",
      "args": []
    }
  }
}
```

### Enable automatic memory injection (optional)

If you installed via the plugin, use `handoff-mcp-hooks` instead (see above).

For non-plugin installs, run:

```bash
handoff-mcp setup
```

This installs Claude Code hooks into `~/.claude/settings.json` and adds a
`handoff` server entry to the project's `.mcp.json` (required for hooks to
connect). The command is interactive by default — use `-y` to skip prompts:

```bash
handoff-mcp setup -y           # non-interactive (auto-approve everything)
handoff-mcp setup --mcp-json   # only add .mcp.json entry (skip hooks)
handoff-mcp setup --check      # check if hooks and .mcp.json are configured
```

Restart Claude Code after running setup.

You can check the current status or remove the hooks:

```bash
handoff-mcp setup --check      # Show hook status
handoff-mcp setup --uninstall  # Remove handoff hooks
```

The hooks fire on every prompt and file edit, which adds a small overhead per
interaction. If you want to stop automatic injection, run
`handoff-mcp setup --uninstall` — the memory tools themselves remain available
for manual use, only the automatic hooks are removed.

See [Automatic injection via hooks](#automatic-injection-via-hooks) for the
manual configuration alternative.

## Quick Start

1. **Initialize** a project:

   The agent calls `handoff_init` with your project name. This creates a `.handoff/` directory:

   ```
   .handoff/
   ├── config.toml      # Project settings
   ├── sessions/        # Session history (TOML files)
   └── tasks/           # Task tree (directories + TOML files)
   ```

2. **Load context** at session start — the agent calls `handoff_load_context`. If `session_guidance` is returned, the agent establishes an active session via `handoff_save_context` with `session_status: "active"` before starting work.

3. **Work normally** — create tasks, track progress, make decisions. The active session persists on disk, so progress survives interruptions.

4. **Save context** at session end — the agent calls `handoff_save_context` to close the active session with handoff data (summary, decisions, blockers, references).

> Add `.handoff/` to your `.gitignore` — it contains local working state, not code.

## Tools

### Core Session Management

| Tool | Purpose |
|------|---------|
| `handoff_init` | Initialize `.handoff/` directory for a project |
| `handoff_load_context` | Load session context, tasks, and git state at session start |
| `handoff_save_context` | Save session state — establish an active session or close it with handoff data |
| `handoff_update_session` | Incrementally update active session (toggle checklist, add decisions/notes/pointers) |
| `handoff_list_sessions` | List all sessions (open/active/paused/closed) with summary info |
| `handoff_get_session` | Get full detail of a specific session by ID |
| `handoff_fork_session` | Fork a new session from an existing one with context inheritance |
| `handoff_merge_sessions` | Merge multiple sessions into one with conflict detection |

### Task Management

| Tool | Purpose |
|------|---------|
| `handoff_list_tasks` | List tasks with filters (status, assignee, milestone, priority, label) |
| `handoff_get_task` | Get full task details (notes, done_criteria, schedule, etc.) |
| `handoff_update_task` | Create, update, or move tasks; supports `notes_append` for safe incremental notes and `requirement_ids` for linking to requirement SubItems |
| `handoff_check_criterion` | Toggle a single done_criteria item by index |
| `handoff_log_time` | Log hours worked — adds to `actual_hours`, deducts from `remaining_hours` |
| `handoff_bulk_update_tasks` | Update multiple tasks in one call (status, schedule, assignee, priority) |

### Metrics & Scheduling

| Tool | Purpose |
|------|---------|
| `handoff_get_metrics` | Project metrics: completion %, effort, overdue, budget, milestones |
| `handoff_get_capacity` | Work capacity for a date range, respecting calendar and assignee config |
| `handoff_auto_schedule` | Auto-schedule tasks based on dependencies, estimates, and capacity |

### Configuration & Team

| Tool | Purpose |
|------|---------|
| `handoff_get_config` | Read project configuration (full TOML as JSON) |
| `handoff_update_config` | Update config: settings, calendar, assignees, effort budget, gantt view |
| `handoff_list_assignees` | List team members with task counts and effort stats |
| `handoff_add_assignee` | Add a team member (`[assignees.<key>]`) |
| `handoff_update_assignee` | Update a team member's fields (partial; null clears a field) |
| `handoff_remove_assignee` | Remove a team member and unassign them from every task |
| `handoff_list_milestones` | List milestones (`[milestones.*]`) |
| `handoff_add_milestone` | Add a milestone (date, color, description) |
| `handoff_update_milestone` | Update a milestone (partial) |
| `handoff_remove_milestone` | Remove a milestone |
| `handoff_update_calendar` | Patch the project `[calendar]` (work hours, closed days, `day_hours`, schedule_mode) |
| `handoff_update_labels` | Set the project-level label vocabulary |
| `handoff_start_project` | Set `started_at` and optionally shift all task dates to the project start |

These CRUD tools and the VSCode extension write the same `config.toml`, so the
GUI and the MCP server stay in full parity. All writes are atomic (temp-file +
rename) so a concurrent reader never sees a partially-written file.

### Cross-Project

| Tool | Purpose |
|------|---------|
| `handoff_dashboard` | Overview of all handoff-enabled projects |
| `handoff_import_context` | Bulk import tasks and session data from documents |
| `handoff_refer` | Send a cross-project referral (bug, improvement, request) |
| `handoff_list_referrals` | List incoming referrals from other projects (summaries only) |
| `handoff_get_referral` | Fetch one incoming referral in full — details, suggested tasks, done_criteria, context |
| `handoff_update_referral` | Update referral status (open → acknowledged → resolved) |

### Timer Coordination

| Tool | Purpose |
|------|---------|
| `handoff_timer_start` | Start tracking time for a task — delegates to VSCode extension if alive, otherwise starts MCP fallback timer |
| `handoff_timer_stop` | Stop the timer and log elapsed hours to `actual_hours` — delegates to VSCode if alive |
| `handoff_timer_get_time` | Get current timer state (elapsed, authority, projected total) without stopping |

### Project Memory

| Tool | Purpose |
|------|---------|
| `handoff_memory_save` | Save a durable project memory (lesson/rule/convention/gotcha); detects exact and near-duplicate memories and hands near-duplicates back for AI-driven merge |
| `handoff_memory_query` | Return the memories most relevant to the current prompt/file (BM25 + scope-path boost); with a `session_id`, suppresses repeats already injected this session |
| `handoff_memory_delete` | Delete a memory by id (full id or unique prefix) |
| `handoff_memory_cleanup` | Manual/CLI housekeeping: silently merge exact duplicates, return near-duplicate/stale recommendations, gc old injection sidecars |

For usage best practices (granularity, scope_paths, conflict handling, cleanup), see `skills/handoff-memory/SKILL.md`.
See [Project Memory](#project-memory-1) below for what it is and how to wire automatic injection.

### Document Management

| Tool | Purpose |
|------|---------|
| `handoff_doc_save` | Create or update a document (auto-splits into sections) |
| `handoff_doc_get` | Read a document — full, meta, or single section |
| `handoff_doc_list` | List/search documents with BM25 and filters |
| `handoff_doc_delete` | Delete a document; unlinks from tasks |
| `handoff_doc_reassemble` | Reconstruct original Markdown from sections, with drift detection |
| `handoff_doc_update_section` | Replace a single section by seq (optimistic locking) |
| `handoff_doc_tree` | Walk family tree (ancestors/descendants/related) |
| `handoff_doc_graph` | Visualize inter-document relationships with optional verification status |
| `handoff_doc_trace` | Trace a document's lineage or dependency chain |
| `handoff_doc_query` | Context injection — staged full/outline, hook-driven |
| `handoff_doc_verify` | Verification matrix: generate, check, check_all, skip, sync, set_refs, add_item, set_dev_stage, set_priority, backfill_stable_ids |
| `handoff_doc_verify_status` | Verification progress summary with optional per-section details |
| `handoff_doc_req_status` | Requirements progress across documents (by status / priority / category, coverage, per-task coverage) |
| `handoff_doc_req_list` | List individual requirements with filters (priority, dev_stage, category, tests, task), sorting and paging |
| `handoff_doc_req_import` | Turn a Markdown requirement tree into requirement items with stable IDs and priorities (dry-run first) |
| `handoff_doc_req_scan` | Suggest code/test references for requirements by scanning source files for requirement IDs |
| `handoff_doc_req_impact` | Find requirements affected by changed files (or the current git diff) |
| `handoff_doc_analyze` | Read-only heuristic scan (import step 1) |
| `handoff_doc_import` | Atomic bulk write after analysis (import step 3) |
| `handoff_trace_record` | Record one execution batch (pass/fail/blocked/…) against layer-document items, as a new file under `.handoff/runs/` |
| `handoff_trace_report` | V-model coverage/gap report across every layer document, task links, and recorded runs; also refreshes `.handoff/docs/_trace_report.json` for editor integrations |
| `handoff_trace_slice` | Neighborhood view around one task or item (up/down/both the requirement↔verification graph), for progressive-disclosure traversal |
| `handoff_trace_history` | Every recorded execution result for one item, newest first |
| `handoff_trace_baseline` | Snapshot the current trace graph's coverage, list/diff past baselines |
| `handoff_trace_delta` | Change-proposal (delta) lifecycle: create/list/apply/reject a pending bundle of ops for human review |
| `handoff_trace_ingest` | Parse `cargo test --format json` JSONL output and record each matched test's result via `trace_record` |
| `handoff_trace_scaffold` | Generate one verification-layer item per acceptance-criteria bullet of a source item |
| `handoff_trace_suspect` | List/clear/baseline the 3 suspect kinds a link/task/result falls into when its upstream definition changed |
| `handoff_trace_impact` | Read-only impact analysis for a proposed change — never writes anything |
| `handoff_trace_lint` | Read-only lint over the whole trace graph (structure, tailoring, drift, format rules) |
| `handoff_trace_matrix` | Read-only coverage/gap matrix for one or more layer documents |
| `handoff_trace_next` | Read-only prioritized worklist of V-model items/tasks still needing attention |
| `handoff_trace_test_run` | Test run definitions and dynamically-computed progress (create/record/progress/list) |
| `handoff_trace_propose` | Read-only suggestion of existing V-model items covering a task, plus a ready-to-review template for a new one |
| `handoff_trace_tasks` | Generate one task per V-model item still missing the task role it needs |
| `handoff_trace_update` | Bulk-mutation entry point over several op kinds (upsert_item, link, unlink, suspect clear, approval, …) in one call |

Documents live in `.handoff/docs/` as single `_doc.<slug>.md` files (YAML
frontmatter + body). Large Markdown is split into sections on save;
`handoff_doc_reassemble` reconstructs the original with drift detection, and
`handoff_doc_query` feeds staged (outline-first, then full-text) context to
the agent — the same mechanism that powers the hook-driven injection described
below. `handoff_doc_verify` provides a verification matrix for tracking
per-section review status, implementation/test references, and staleness
detection after spec changes.

**Requirements traceability.** Requirement items (sub-items of the verification
matrix) carry a stable ID (e.g. `C01-FR-101`), priority (P0–P3), development
stage (`not_started` → `in_progress` → `implemented` → `tested` → `verified`)
and code/test references. Link a task to requirements with
`handoff_update_task(task={id, requirement_ids: [...]})`; the link is recorded on
both sides (with an `implements`/`executes` role), and the development stage
follows the linked `implements` tasks' status automatically. Progress is
summarized in `.handoff/docs/_requirements_summary.json`, which the VS Code
extension reads.

**V-model layer documents.** Pass `layer` to `handoff_doc_save` (one of
`requirement`/`basic_spec`/`detailed_spec`/`acceptance`/`system_test`/
`unit_test`) to turn a document's Markdown body into the source of truth for
its requirement/verification items — every save re-parses ID-prefixed
headings (`REQ-001`, `SPEC-012`, `AT-001`, …) with `refines`/`verifies`/
`priority`/`method`/`test` attributes into the verification matrix. Record
execution results with `handoff_trace_record`, then get a full
coverage/gap report (`handoff_trace_report`) or a neighborhood slice around
one task/item (`handoff_trace_slice`). See the `handoff-docs` skill's
"V-model Layer Documents" section for templates and the layer-skip
(inline-verification) workflow.

## V-model Traceability (optional)

handoff-mcp can track requirements/design/test coverage as a V-model: six
built-in layers (`req`/`spec`/`design`/`acceptance`/`system_test`/`unit_test`),
linked by `refines`/`verifies`, with lint, suspect/reverify tracking, and
baseline diffs. This is entirely separate from the simpler `doc_req_import`
freeform-SubItem workflow described above — **the two are mutually exclusive
on a single document** (see `handoff-trace` skill §15 if you need to migrate
one to the other).

To get started:

1. Add a `[trace]` section to `.handoff/config.toml`:
   ```toml
   [trace]
   profile = "standard"
   ```
2. Save a document with `handoff_doc_save(layer="req", trace_profile="standard", body="...")`
   — the body's headings (e.g. `### REQ-001 <title>`) become SubItems automatically.
3. Use `handoff_trace_lint` to check structural issues, `handoff_trace_matrix`
   for a coverage table, `handoff_trace_next` for a prioritized worklist, and
   `handoff_trace_report`/`handoff_trace_slice` for derivation graphs.

Full guide: **`skills/handoff-trace/SKILL.md`** — every layer, profile, body
notation rule, lint rule, and the `trace_*` tool family (17 tools) live there.

### Task Data Model

Tasks are stored as a directory tree with status encoded in filenames:

```
tasks/
├── t1-implement-auth/
│   ├── _task.done.json
│   ├── t1.1-design-schema/
│   │   └── _task.done.json
│   └── t1.2-write-handlers/
│       └── _task.in_progress.json
└── t2-deploy-staging/
    └── _task.blocked.json
```

Statuses: `todo` | `in_progress` | `review` | `done` | `blocked` | `skipped`

Each task can have:
- **Assignee** — team member key (matches `[assignees.<key>]` in config.toml)
- **Priority** — `low` / `medium` / `high`
- **Labels** — free-form tags
- **Done criteria** — checklist items (all must be checked before `done` transition)
- **Links** — URLs to issues, MRs, or docs
- **Notes** — markdown description
- **Schedule** — `start_date`, `due_date`, `estimate_hours`, `actual_hours`, `remaining_hours`, `milestone`, `pinned`
- **Dependencies** — task IDs this task depends on (circular deps rejected)

### Session Context

When saving context, the agent can record:

- **Summary** — one-line description of what happened
- **Decisions** — what was decided and why, with confidence levels (`confirmed` / `estimated` / `unverified`)
- **Blockers** — what's preventing progress
- **Checklist** — items for the next session
- **Handoff notes** — categorized as `caution`, `context`, or `suggestion`
- **References** — links to files, issues, MRs, wiki pages, or URLs
- **Context pointers** — specific files and line ranges the next session should look at
- **Git state** — current branch, recent commits, and dirty files (captured automatically)

### Dashboard

`handoff_dashboard` scans directories for projects with `.handoff/` and shows a summary:

```
## my-project (3 tasks)
  - [in_progress] Implement auth (high)
  - [todo] Add tests (medium)
  - [blocked] Deploy staging (medium)

## other-project (1 task)
  - [review] Update README (low)
```

## Configuration

`.handoff/config.toml`:

```toml
[project]
name = "my-project"
description = "Project description"

[settings]
history_limit = 20            # Max closed sessions to keep
done_task_limit = 10          # Max completed tasks to show
auto_git_summary = true       # Capture git state automatically
require_estimate_hours = true # Require estimate_hours on leaf tasks (default true)
multi_session = true          # Allow multiple active sessions (default true for new projects)
ai_estimate_multiplier = 0.2  # Multiplier turning human estimates into AI-effort hours
timer_provider = "auto"       # "auto" | "vscode" | "mcp" | "off"
timer_authority_ttl_secs = 30 # Heartbeat freshness TTL for authority.json
timer_idle_timeout_minutes = 10 # Idle pause threshold for MCP fallback timer

[dashboard]
scan_dirs = ["~/pro/"]     # Directories to scan for dashboard

[calendar]
work_hours_per_day = 8
closed_weekdays = ["sat", "sun"]
closed_dates = ["2026-12-25"]
open_dates = []
schedule_mode = "auto"     # "auto" or "manual"
overwork_limit_percent = 150

[calendar.day_hours]
fri = 4                    # Per-weekday hour overrides

[effort_budget]
total_hours = 500          # Total project effort cap

[assignees.alice]
display_name = "Alice Chen"
color = "#4A90D9"
work_hours_per_day = 8
closed_weekdays = [1, 2]   # Per-assignee overrides

[assignees.bob]
display_name = "Bob Martinez"
color = "#E74C3C"
work_hours_per_day = 6

[gantt_view]
sort = "start"             # start, id, id-desc, status
zoom = "week"              # day, week, month
mode = "compare"           # plan, actual, compare

[trace]
profile = "standard"
# See `handoff-trace` skill for layers/done_guard/id_prefixes/custom profiles.
```

All configuration sections can be updated via `handoff_update_config` with dot-notation keys (e.g., `"calendar.work_hours_per_day": 7`).

### Estimates and AI effort

handoff-mcp distinguishes the **raw human-effort estimate** you record on a task
from the **AI-effort hours** used in scheduling and metrics:

- **`require_estimate_hours`** (default `true`) — `handoff_update_task` rejects
  creating or updating a *leaf* task in `in_progress` / `review` / `done` without
  `schedule.estimate_hours > 0`. Parent tasks (with children) and tasks in `todo` /
  `blocked` / `skipped` are exempt, and an estimate already on the task satisfies
  the requirement. Set to `false` to opt out.
- **`ai_estimate_multiplier`** (default `0.2`) — the factor applied to raw
  estimates to model how long the work takes when an AI agent does it. Always
  record the *raw human-effort* estimate; the multiplier is applied at
  aggregation time by `handoff_get_metrics` (`total_adjusted_estimate_hours` and
  per-milestone `adjusted_estimate_hours`) and `handoff_get_capacity`. Raw values
  are never overwritten.

## Project Memory

Sessions answer *"what was I doing last time?"*. **Memory** answers a longer-lived
question: *"what should every session in this project always know?"* — durable
lessons, rules, conventions, and gotchas that outlive any one session.

> **Note:** The memory tools (`handoff_memory_save`, `handoff_memory_query`, etc.) can always be
> called directly by the agent. For **automatic** injection — where relevant
> memories are surfaced on every prompt without the agent asking — you need to
> configure Claude Code hooks. See [Automatic injection via hooks](#automatic-injection-via-hooks).

Memories live in `.handoff/memory/` (one JSON file per memory, plus per-session
`injected/` sidecars). A built-in multilingual similarity engine (Japanese /
English, dictionary-free) ranks relevance and detects duplicates, all in-memory
and sub-millisecond.

### Using it directly

The agent can call the memory tools at any time:

- `handoff_memory_save` — record a memory. An exact duplicate is reported (not
  rewritten); a near-duplicate comes back as a `conflict` with both bodies so the
  agent can merge them (`merge_into=<id>`, `absorb_ids=[…]`) or save separately
  with `force=true`. **handoff-mcp never merges for you** — it surfaces both
  bodies and lets the agent decide.
- `handoff_memory_query` — fetch the memories most relevant to some text and/or files.
- `handoff_memory_delete` / `handoff_memory_cleanup` — prune and de-duplicate the store.

### Automatic injection via hooks

MCP is request/response — the server cannot push a memory into the agent's
context on its own. **Claude Code hooks** close that gap: they fire regardless of
what the agent intends, call `handoff_memory_query`, and inject the matching memories as
`additionalContext`. A per-session diff (keyed on the hook `session_id`) ensures
the same memory is **not injected twice in one session** — and an *edited* memory
(new content hash) is re-injected.

The same hooks also call `handoff_doc_query`, so relevant documents (saved via
`handoff_doc_save`) are staged into context alongside memories — outline first,
then full text as relevance/budget allows.

| Event | Calls | Effect |
|-------|-------|--------|
| `UserPromptSubmit` | `handoff_memory_query` (prompt text), `handoff_doc_query` (prompt text) | Inject memories and documents relevant to the prompt |
| `PreToolUse` (`Edit\|Write\|MultiEdit`) | `handoff_memory_query` (file path), `handoff_doc_query` (file path) | Inject memories and documents scoped to the file being edited |

`handoff_memory_cleanup` (merge exact duplicates, gc old sidecars) is not wired
to a hook — call it manually or from a CLI/cron job when you want housekeeping.

> The bundled `plugin-hooks/hooks/hooks.json` (used by the "Handoff MCP — Memory
> & Document Hooks" plugin) already wires both `handoff_memory_query` and
> `handoff_doc_query` on `UserPromptSubmit` and `PreToolUse`; the JSON examples
> below show the equivalent hand-written config for non-plugin setups.

> **Wire hooks in your *user/global* settings, not in the repo.** Hooks are a
> personal workflow choice; the handoff-mcp repo does not ship a `.claude/`
> hooks config, and you should not commit one into a shared project. Put the
> config in `~/.claude/settings.json` (global) or your own
> `.claude/settings.local.json` (git-ignored).

**Native `mcp_tool` hook (preferred).** Recent Claude Code versions can call an
MCP tool from a hook directly, with no wrapper script. In
`~/.claude/settings.json`:

```json
{
  "hooks": {
    "UserPromptSubmit": [
      { "hooks": [ {
        "type": "mcp_tool", "server": "handoff", "tool": "handoff_memory_query",
        "input": { "project_dir": "${cwd}",
                   "session_id": "${session_id}", "text": "${prompt}" }
      } ] }
    ],
    "PreToolUse": [
      { "matcher": "Edit|Write|MultiEdit", "hooks": [ {
        "type": "mcp_tool", "server": "handoff", "tool": "handoff_memory_query",
        "input": { "project_dir": "${cwd}",
                   "session_id": "${session_id}", "tool_name": "${tool_name}",
                   "text": "${tool_input.file_path}",
                   "file_paths": ["${tool_input.file_path}"] }
      } ] }
    ]
  }
}
```

(`server` must match the name you registered handoff-mcp under — `handoff` in the
[Setup](#setup) examples.)

**Wrapper script fallback.** If your Claude Code version doesn't support the
`mcp_tool` hook type, use the bundled `command` wrapper
[`scripts/handoff-memory-hook.py`](scripts/handoff-memory-hook.py). It reads the
hook JSON on stdin, calls the server over JSON-RPC, and emits
`additionalContext` — the memory tools return their payload as a JSON *string* so
both paths parse it identically. Point all three hooks at it:

```json
{
  "hooks": {
    "UserPromptSubmit": [
      { "hooks": [ { "type": "command",
        "command": "/path/to/handoff-mcp/scripts/handoff-memory-hook.py" } ] }
    ],
    "PreToolUse": [
      { "matcher": "Edit|Write|MultiEdit", "hooks": [ { "type": "command",
        "command": "/path/to/handoff-mcp/scripts/handoff-memory-hook.py" } ] }
    ]
  }
}
```

The script resolves the `handoff-mcp` binary from `PATH` (override with
`HANDOFF_MCP_BIN`) and **fails safe**: on any error it prints nothing and exits
0, so a memory miss is silent and never blocks your prompt.

### Upgrading from a version with a `SessionStart` cleanup hook

Versions before this fix had `handoff-mcp setup` (and the
`handoff-mcp-hooks` plugin) install a **synchronous `SessionStart` hook**
that ran `handoff_memory_cleanup` on every session start. Under many
parallel sub-agents (e.g. `/research-loop`), that hook could pile up heavy
cleanup calls on the single-threaded server and hang your editor.

The `SessionStart` cleanup hook has been removed entirely — `memory_cleanup`
is still available, but only via manual/CLI invocation, never auto-fired.
If you already ran `handoff-mcp setup` before this change, migrate with
**one** of the following:

- **Re-run setup** (recommended): `handoff-mcp setup`. It now detects and
  automatically strips the legacy `SessionStart` cleanup hook while leaving
  your other handoff hooks untouched. Use `handoff-mcp setup --check` first
  if you want to confirm whether the legacy hook is present before touching
  anything.
- **Manual edit**: open `~/.claude/settings.json` and delete the
  `hooks.SessionStart` entry whose `tool` is `handoff_memory_cleanup` (remove
  the whole `SessionStart` key if that was its only entry).
- **Plugin users**: `/plugin update handoff-mcp-hooks@handoff-mcp-marketplace`
  to pick up the new `hooks.json`, then restart Claude Code. This one ships
  inside the plugin, so no binary update is needed.

Restart Claude Code after any of the above for the change to take effect.

### Optional workflow observer (Claude Code)

For workflow performance investigations, `scripts/claude-workflow-observer.js`
normalizes Claude Code lifecycle hooks into an append-only JSONL file. It is
**not enabled by this repository or by plugin frontmatter**: copy the command
paths from [`plugin-hooks/observer/hooks.json`](plugin-hooks/observer/hooks.json)
into your own `~/.claude/settings.json` or managed settings, replacing both
`/ABSOLUTE/PATH/TO/...` placeholders. Keep the log outside the repository, for
example `~/.local/state/handoff-mcp/claude-workflow.jsonl`.

The observer records `agent_started`, `phase_changed`, `tool_started`,
`tool_finished`, and `agent_finished`. It uses `session_id` as the fallback
agent/run correlation key and preserves a supplied `agent_id`, parent session,
task, phase, and `tool_use_id` where the hook runtime supplies them. It does
not record prompt text, tool input, command arguments, or command output; Bash
events retain only the executable name to support duration aggregates.

Set these environment variables in the hook command only when the workflow
wrapper can supply stable values:

| Variable | Purpose |
|-------|---------|
| `HANDOFF_OBSERVER_LOG` | Absolute JSONL destination; if omitted, no event is written. |
| `HANDOFF_WORKFLOW_RUN_ID` | Parent workflow-run correlation ID. |
| `HANDOFF_WORKFLOW_TASK_ID` | Handoff task identifier. |
| `HANDOFF_WORKFLOW_PHASE` | Workflow phase (for example `implement`). |

The hook always exits successfully, including a malformed event or unwritable
log, and it does not print hook output. This makes it observational only; it
cannot enforce command policy. Aggregate a completed or still-running run with:

```bash
node /ABSOLUTE/PATH/TO/claude-workflow-observer.js summarize \
  ~/.local/state/handoff-mcp/claude-workflow.jsonl
```

The summary reports `status: "running"` until an explicit `SessionEnd` or
`SubagentStop` arrives, even if a native journal contains only a started/result
pair. It also reports turn/tool counts, tool wait, estimated model wait,
per-executable duration, and the longest command. Estimated model wait is wall
time not covered by paired hook-visible tools, so it includes streaming and any
other uninstrumented interval; it is not a token-level OTel replacement.

Claude Code's OpenTelemetry export remains complementary: it can provide
process-level traces, but the command hook is the source for this normalized
per-agent lifecycle stream. The hook input varies by Claude Code version:
`session_id` and `tool_use_id` are usable fallback correlation keys, while an
independent workflow run, task, and phase must be supplied by the wrapper.

### Memory settings

All under `[settings]` in `.handoff/config.toml`, all with safe defaults
(existing projects need no change), all settable via `handoff_update_config`:

| Key | Default | Meaning |
|-----|---------|---------|
| `memory_enabled` | `true` | Master switch. When `false`, all four memory tools return a benign empty result and write nothing |
| `memory_dup_threshold` | `0.72` | Jaccard similarity at/above which a save is a near-duplicate conflict and cleanup groups a cluster |
| `memory_query_min_score` | `0.5` | BM25 relevance floor for `handoff_memory_query` results |
| `memory_query_limit` | `5` | Max memories returned per query |
| `memory_stale_days` | `60` | Days without a reference before a memory is flagged stale |
| `memory_injected_gc_days` | `14` | Age at which per-session injection sidecars are garbage-collected |

## CLI API

Since v0.15.0, every MCP tool is also callable directly from the shell:

```bash
handoff-mcp <group> <action> [--key value ...]
```

All output is JSON on stdout, suitable for scripting and programmatic use
(e.g. `child_process.execFile` from a VSCode extension).

**Examples:**

```bash
# Memory operations
handoff-mcp memory save --text "Always use atomic_write" --kind lesson --tags safety,io
handoff-mcp memory query --text "atomic" --limit 5
handoff-mcp memory delete --id m-20260630-...

# Task management
handoff-mcp task list --status-filter todo
handoff-mcp task update --id t1 --title "New task" --status todo --estimate-hours 2
handoff-mcp task log-time --task-id t1 --hours 0.5

# Session and metrics
handoff-mcp session load
handoff-mcp metrics
handoff-mcp dashboard
```

**Available groups:** `init`, `task`, `session`, `config`, `memory`,
`referral`, `assignee`, `milestone`, `calendar`, `labels`, `project`,
`metrics`, `capacity`, `schedule`, `dashboard`, `timer`.

Run `handoff-mcp --help` to see all groups, or `handoff-mcp <group> --help`
for actions within a group. See the
[CLI API Reference](https://github.com/alphaelements/handoff-mcp/wiki/CLI-API-Reference)
on the wiki for the full command list.

## MCP Resources

| URI | Description |
|-----|-------------|
| `handoff://sessions` | Active session data (JSON) |
| `handoff://config` | Project configuration (TOML) |

## Recommended CLAUDE.md Setup

Copy the template from [`templates/claude-md-section.md`](templates/claude-md-section.md) into your project's `CLAUDE.md`. It declares that the project uses handoff-mcp and points to the `handoff` skill for the full procedure. Plugin users get the skill automatically; non-plugin users should copy `skills/` to `~/.claude/skills/`.

## Skills

This repository includes skill files that make handoff behavior automatic in Claude Code:

| Skill | Purpose |
|-------|---------|
| `handoff` | Core session lifecycle, task management, metrics, scheduling |
| `handoff-load` | Quick session-start procedure |
| `handoff-docs` | Document management — save, search, verify, import, family tree |
| `handoff-trace` | V-model traceability — layers, profiles, lint, suspect/reverify, baselines |
| `handoff-memory` | Memory CRUD, conflict handling, cleanup |
| `handoff-refer` | Cross-project referrals |
| `handoff-import` | Bulk import from documents |

**Plugin users**: all skills are included automatically.

**Manual setup**: copy the skills to your user skills directory:

```bash
cp -r skills/* ~/.claude/skills/
```

## Compatibility

- **Claude Code** — fully supported (stdio transport)
- **Other MCP clients** — any client supporting the MCP stdio transport

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md) for local development gates, including
the performance budget harness for changes to the storage/handler layers.

## License

MIT