handoff-mcp-server 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Cargo.toml ADDED
@@ -0,0 +1,24 @@
1
+ [package]
2
+ name = "handoff-mcp"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ description = "MCP server that gives AI coding agents persistent memory across sessions"
6
+ license = "MIT"
7
+ repository = "https://github.com/alphaelements/handoff-mcp"
8
+ homepage = "https://github.com/alphaelements/handoff-mcp"
9
+ readme = "README.md"
10
+ keywords = ["mcp", "ai", "claude", "handoff", "context"]
11
+ categories = ["command-line-utilities", "development-tools"]
12
+ rust-version = "1.70"
13
+ exclude = ["lefthook.yml", "tmp/", "wiki/", "docs/", ".claude/", ".vscode/"]
14
+
15
+ [dependencies]
16
+ serde = { version = "1", features = ["derive"] }
17
+ serde_json = "1"
18
+ toml = "0.8"
19
+ chrono = { version = "0.4", features = ["serde"] }
20
+ anyhow = "1"
21
+ thiserror = "2"
22
+
23
+ [dev-dependencies]
24
+ tempfile = "3.27.0"
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AlphaElements Co., Ltd.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,223 @@
1
+ # handoff-mcp
2
+
3
+ An MCP server that gives AI coding agents persistent memory across sessions.
4
+
5
+ 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.
6
+
7
+ ## The Problem
8
+
9
+ AI coding sessions are stateless. Every new session starts from zero:
10
+
11
+ - **"What was I working on?"** — the agent doesn't know
12
+ - **"What decisions were made?"** — lost with the previous context window
13
+ - **"What's left to do?"** — you have to re-explain everything
14
+
15
+ This gets painful fast on multi-session projects.
16
+
17
+ ## How It Works
18
+
19
+ ```
20
+ Session 1 Session 2
21
+ ┌──────────────┐ ┌──────────────┐
22
+ │ Working... │ .handoff/ │ Loading... │
23
+ │ │──────────────────>│ │
24
+ │ save_context │ tasks/ │ load_context │
25
+ │ - summary │ sessions/ │ - tasks │
26
+ │ - decisions │ config.toml │ - decisions │
27
+ │ - blockers │ │ - blockers │
28
+ │ - tasks │ │ - git state │
29
+ └──────────────┘ └──────────────┘
30
+ ```
31
+
32
+ At session end, the agent calls `handoff_save_context` to persist what matters. At session start, it calls `handoff_load_context` to pick up where things left off.
33
+
34
+ ## Installation
35
+
36
+ ### npm (recommended)
37
+
38
+ ```bash
39
+ npm install -g handoff-mcp-server
40
+ ```
41
+
42
+ ### Build from source
43
+
44
+ ```bash
45
+ git clone https://github.com/alphaelements/handoff-mcp.git
46
+ cd handoff-mcp
47
+ cargo build --release
48
+ ```
49
+
50
+ ## Setup
51
+
52
+ Add to your Claude Code MCP configuration:
53
+
54
+ **Global** (`~/.claude/.mcp.json`) — available in all projects:
55
+
56
+ ```json
57
+ {
58
+ "handoff": {
59
+ "command": "handoff-mcp",
60
+ "args": []
61
+ }
62
+ }
63
+ ```
64
+
65
+ **Per-project** (`.mcp.json` in project root):
66
+
67
+ ```json
68
+ {
69
+ "handoff": {
70
+ "command": "handoff-mcp",
71
+ "args": []
72
+ }
73
+ }
74
+ ```
75
+
76
+ ## Quick Start
77
+
78
+ 1. **Initialize** a project:
79
+
80
+ The agent calls `handoff_init` with your project name. This creates a `.handoff/` directory:
81
+
82
+ ```
83
+ .handoff/
84
+ ├── config.toml # Project settings
85
+ ├── sessions/ # Session history (TOML files)
86
+ └── tasks/ # Task tree (directories + TOML files)
87
+ ```
88
+
89
+ 2. **Work normally** — create tasks, track progress, make decisions.
90
+
91
+ 3. **Save context** at session end — the agent captures a summary, decisions, blockers, and references.
92
+
93
+ 4. **Load context** at next session start — the agent reads back everything and resumes.
94
+
95
+ > Add `.handoff/` to your `.gitignore` — it contains local working state, not code.
96
+
97
+ ## Tools
98
+
99
+ | Tool | Purpose |
100
+ |------|---------|
101
+ | `handoff_init` | Initialize `.handoff/` directory for a project |
102
+ | `handoff_load_context` | Load session context, tasks, and git state at session start |
103
+ | `handoff_save_context` | Save session summary, decisions, blockers, and references |
104
+ | `handoff_list_tasks` | List tasks with optional status filter |
105
+ | `handoff_update_task` | Create, update, or move tasks in a hierarchical tree |
106
+ | `handoff_get_config` | Read project configuration |
107
+ | `handoff_update_config` | Update project configuration |
108
+ | `handoff_dashboard` | Overview of all handoff-enabled projects |
109
+
110
+ ### Task Management
111
+
112
+ Tasks are stored as a directory tree, supporting hierarchical structures:
113
+
114
+ ```
115
+ tasks/
116
+ ├── 01-todo--implement-auth/
117
+ │ ├── task.toml
118
+ │ ├── 01.1-done--design-schema/
119
+ │ │ └── task.toml
120
+ │ └── 01.2-in_progress--write-handlers/
121
+ │ └── task.toml
122
+ └── 02-blocked--deploy-staging/
123
+ └── task.toml
124
+ ```
125
+
126
+ Statuses: `todo` | `in_progress` | `review` | `done` | `blocked` | `skipped`
127
+
128
+ Each task can have:
129
+ - Priority (`low` / `medium` / `high`)
130
+ - Labels
131
+ - Done criteria (checklist items)
132
+ - Links to issues, MRs, or docs
133
+ - Notes
134
+
135
+ ### Session Context
136
+
137
+ When saving context, the agent can record:
138
+
139
+ - **Summary** — one-line description of what happened
140
+ - **Decisions** — what was decided and why, with confidence levels (`confirmed` / `estimated` / `unverified`)
141
+ - **Blockers** — what's preventing progress
142
+ - **Checklist** — items for the next session
143
+ - **Handoff notes** — categorized as `caution`, `context`, or `suggestion`
144
+ - **References** — links to files, issues, MRs, wiki pages, or URLs
145
+ - **Context pointers** — specific files and line ranges the next session should look at
146
+ - **Git state** — current branch, recent commits, and dirty files (captured automatically)
147
+
148
+ ### Dashboard
149
+
150
+ `handoff_dashboard` scans directories for projects with `.handoff/` and shows a summary:
151
+
152
+ ```
153
+ ## my-project (3 tasks)
154
+ - [in_progress] Implement auth (high)
155
+ - [todo] Add tests (medium)
156
+ - [blocked] Deploy staging (medium)
157
+
158
+ ## other-project (1 task)
159
+ - [review] Update README (low)
160
+ ```
161
+
162
+ ## Configuration
163
+
164
+ `.handoff/config.toml`:
165
+
166
+ ```toml
167
+ [project]
168
+ name = "my-project"
169
+ description = "Project description"
170
+
171
+ [settings]
172
+ history_limit = 20 # Max closed sessions to keep
173
+ done_task_limit = 10 # Max completed tasks to show
174
+ auto_git_summary = true # Capture git state automatically
175
+
176
+ [dashboard]
177
+ scan_dirs = ["~/pro/"] # Directories to scan for dashboard
178
+ ```
179
+
180
+ ## MCP Resources
181
+
182
+ | URI | Description |
183
+ |-----|-------------|
184
+ | `handoff://sessions` | Active session data (JSON) |
185
+ | `handoff://config` | Project configuration (TOML) |
186
+
187
+ ## Recommended CLAUDE.md Setup
188
+
189
+ Add the following to your project's `CLAUDE.md` so the agent uses handoff consistently:
190
+
191
+ ```markdown
192
+ ## Session Handoff
193
+
194
+ This project uses handoff-mcp for session continuity.
195
+
196
+ - **Session start**: Call `handoff_load_context` to load previous session state.
197
+ If not initialized, call `handoff_init` with the project name.
198
+ - **Session end**: Call `handoff_save_context` with a summary, decisions, and blockers.
199
+ - **During work**: Use `handoff_update_task` to track progress.
200
+ Mark tasks `in_progress` when starting, `done` when complete.
201
+ - **Decisions**: Record decisions with confidence levels as they are made,
202
+ not just at session end. Use `confirmed` for verified facts, `estimated`
203
+ for reasonable assumptions, `unverified` for unknowns.
204
+ ```
205
+
206
+ ## Skill File (Optional)
207
+
208
+ This repository includes a skill file at [`skills/handoff/SKILL.md`](skills/handoff/SKILL.md) that makes handoff behavior automatic in Claude Code. Copy it to your user skills directory:
209
+
210
+ ```bash
211
+ cp -r skills/handoff ~/.claude/skills/
212
+ ```
213
+
214
+ This teaches the agent to automatically load context at session start, track tasks during work, and save context at session end.
215
+
216
+ ## Compatibility
217
+
218
+ - **Claude Code** — fully supported (stdio transport)
219
+ - **Other MCP clients** — any client supporting the MCP stdio transport
220
+
221
+ ## License
222
+
223
+ MIT
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const { execFileSync } = require("child_process");
5
+ const path = require("path");
6
+ const fs = require("fs");
7
+
8
+ const binary = path.join(__dirname, "handoff-mcp-bin");
9
+
10
+ if (!fs.existsSync(binary)) {
11
+ console.error(
12
+ "handoff-mcp binary not found. Try reinstalling: npm install -g handoff-mcp-server"
13
+ );
14
+ process.exit(1);
15
+ }
16
+
17
+ try {
18
+ execFileSync(binary, process.argv.slice(2), { stdio: "inherit" });
19
+ } catch (e) {
20
+ if (e.status != null) process.exit(e.status);
21
+ process.exit(1);
22
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "handoff-mcp-server",
3
+ "version": "0.1.0",
4
+ "description": "MCP server that gives AI coding agents persistent memory across sessions",
5
+ "license": "MIT",
6
+ "author": "AlphaElements <66808803+alphaelements@users.noreply.github.com>",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/alphaelements/handoff-mcp.git"
10
+ },
11
+ "homepage": "https://github.com/alphaelements/handoff-mcp",
12
+ "keywords": [
13
+ "mcp",
14
+ "ai",
15
+ "claude",
16
+ "handoff",
17
+ "context",
18
+ "session"
19
+ ],
20
+ "bin": {
21
+ "handoff-mcp": "./bin/handoff-mcp.js"
22
+ },
23
+ "scripts": {
24
+ "postinstall": "node scripts/postinstall.js"
25
+ },
26
+ "files": [
27
+ "bin/",
28
+ "scripts/",
29
+ "src/",
30
+ "tests/",
31
+ "Cargo.toml",
32
+ "Cargo.lock",
33
+ "LICENSE",
34
+ "README.md"
35
+ ],
36
+ "os": [
37
+ "linux",
38
+ "darwin"
39
+ ],
40
+ "engines": {
41
+ "node": ">=16"
42
+ }
43
+ }
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const { execSync } = require("child_process");
5
+ const fs = require("fs");
6
+ const path = require("path");
7
+
8
+ const ROOT = path.resolve(__dirname, "..");
9
+ const BIN_DIR = path.join(ROOT, "bin");
10
+ const BINARY = path.join(BIN_DIR, "handoff-mcp-bin");
11
+
12
+ if (fs.existsSync(BINARY)) {
13
+ process.exit(0);
14
+ }
15
+
16
+ try {
17
+ execSync("cargo --version", { stdio: "ignore" });
18
+ } catch {
19
+ console.error(
20
+ "Error: Rust toolchain not found.\n" +
21
+ "handoff-mcp-server requires Rust to build from source.\n" +
22
+ "Install Rust: https://rustup.rs/"
23
+ );
24
+ process.exit(1);
25
+ }
26
+
27
+ console.log("Building handoff-mcp from source...");
28
+ try {
29
+ execSync("cargo build --release", { cwd: ROOT, stdio: "inherit" });
30
+ } catch {
31
+ console.error("Error: cargo build failed.");
32
+ process.exit(1);
33
+ }
34
+
35
+ const built = path.join(ROOT, "target", "release", "handoff-mcp");
36
+ if (!fs.existsSync(built)) {
37
+ console.error("Error: binary not found after build.");
38
+ process.exit(1);
39
+ }
40
+
41
+ fs.mkdirSync(BIN_DIR, { recursive: true });
42
+ fs.copyFileSync(built, BINARY);
43
+ fs.chmodSync(BINARY, 0o755);
44
+ console.log("handoff-mcp installed successfully.");
package/src/lib.rs ADDED
@@ -0,0 +1,2 @@
1
+ pub mod mcp;
2
+ pub mod storage;
package/src/main.rs ADDED
@@ -0,0 +1,29 @@
1
+ use std::io::{self, BufRead, Write};
2
+
3
+ use handoff_mcp::mcp::protocol::process_line;
4
+
5
+ fn main() {
6
+ eprintln!("handoff-mcp v{}", env!("CARGO_PKG_VERSION"));
7
+
8
+ let stdin = io::stdin();
9
+ let mut stdout = io::stdout();
10
+
11
+ for line in stdin.lock().lines() {
12
+ let line = match line {
13
+ Ok(l) => l,
14
+ Err(e) => {
15
+ eprintln!("stdin read error: {e}");
16
+ break;
17
+ }
18
+ };
19
+
20
+ if let Some(response) = process_line(&line) {
21
+ if writeln!(stdout, "{response}").is_err() {
22
+ break;
23
+ }
24
+ if stdout.flush().is_err() {
25
+ break;
26
+ }
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,115 @@
1
+ use anyhow::{Context, Result};
2
+ use serde_json::Value;
3
+
4
+ use super::resolve_project_dir;
5
+ use crate::storage::config::{read_config, write_config};
6
+ use crate::storage::ensure_handoff_exists;
7
+
8
+ pub fn handle_get(arguments: &Value) -> Result<String> {
9
+ let project_dir = resolve_project_dir(arguments)?;
10
+
11
+ let handoff = ensure_handoff_exists(&project_dir)?;
12
+ let config = read_config(&handoff.join("config.toml"))?;
13
+ let result = serde_json::to_value(&config).context("Failed to serialize config")?;
14
+ serde_json::to_string_pretty(&result).context("Failed to format config")
15
+ }
16
+
17
+ pub fn handle_update(arguments: &Value) -> Result<String> {
18
+ let project_dir = resolve_project_dir(arguments)?;
19
+
20
+ let handoff = ensure_handoff_exists(&project_dir)?;
21
+ let config_path = handoff.join("config.toml");
22
+ let mut config = read_config(&config_path)?;
23
+
24
+ let updates = arguments
25
+ .get("updates")
26
+ .ok_or_else(|| anyhow::anyhow!("'updates' parameter is required"))?;
27
+
28
+ let updates = updates
29
+ .as_object()
30
+ .ok_or_else(|| anyhow::anyhow!("'updates' must be an object"))?;
31
+
32
+ let mut applied = Vec::new();
33
+
34
+ for (key, value) in updates {
35
+ match key.as_str() {
36
+ "settings.history_limit" => {
37
+ if let Some(n) = value.as_u64() {
38
+ config.settings.history_limit = n as u32;
39
+ applied.push(format!("settings.history_limit = {n}"));
40
+ }
41
+ }
42
+ "settings.done_task_limit" => {
43
+ if let Some(n) = value.as_u64() {
44
+ config.settings.done_task_limit = n as u32;
45
+ applied.push(format!("settings.done_task_limit = {n}"));
46
+ }
47
+ }
48
+ "settings.auto_git_summary" => {
49
+ if let Some(b) = value.as_bool() {
50
+ config.settings.auto_git_summary = b;
51
+ applied.push(format!("settings.auto_git_summary = {b}"));
52
+ }
53
+ }
54
+ "settings.context_files" => {
55
+ if let Some(arr) = value.as_array() {
56
+ config.settings.context_files = arr
57
+ .iter()
58
+ .filter_map(|v| v.as_str().map(String::from))
59
+ .collect();
60
+ applied.push(format!(
61
+ "settings.context_files = {:?}",
62
+ config.settings.context_files
63
+ ));
64
+ }
65
+ }
66
+ "dashboard.scan_dirs" => {
67
+ if let Some(arr) = value.as_array() {
68
+ config.dashboard.scan_dirs = arr
69
+ .iter()
70
+ .filter_map(|v| v.as_str().map(String::from))
71
+ .collect();
72
+ applied.push(format!(
73
+ "dashboard.scan_dirs = {:?}",
74
+ config.dashboard.scan_dirs
75
+ ));
76
+ }
77
+ }
78
+ "dashboard.exclude_patterns" => {
79
+ if let Some(arr) = value.as_array() {
80
+ config.dashboard.exclude_patterns = arr
81
+ .iter()
82
+ .filter_map(|v| v.as_str().map(String::from))
83
+ .collect();
84
+ applied.push(format!(
85
+ "dashboard.exclude_patterns = {:?}",
86
+ config.dashboard.exclude_patterns
87
+ ));
88
+ }
89
+ }
90
+ "project.name" => {
91
+ if let Some(s) = value.as_str() {
92
+ config.project.name = s.to_string();
93
+ applied.push(format!("project.name = {s}"));
94
+ }
95
+ }
96
+ "project.description" => {
97
+ if let Some(s) = value.as_str() {
98
+ config.project.description = Some(s.to_string());
99
+ applied.push(format!("project.description = {s}"));
100
+ }
101
+ }
102
+ other => {
103
+ applied.push(format!("{other}: unknown key (skipped)"));
104
+ }
105
+ }
106
+ }
107
+
108
+ write_config(&config_path, &config)?;
109
+
110
+ if applied.is_empty() {
111
+ Ok("No updates applied".to_string())
112
+ } else {
113
+ Ok(format!("Updated config:\n{}", applied.join("\n")))
114
+ }
115
+ }
@@ -0,0 +1,109 @@
1
+ use std::path::Path;
2
+
3
+ use anyhow::{Context, Result};
4
+ use serde_json::Value;
5
+
6
+ use crate::storage::config::read_config;
7
+ use crate::storage::sessions::read_active_sessions;
8
+ use crate::storage::tasks::build_task_index;
9
+
10
+ pub fn handle(arguments: &Value) -> Result<String> {
11
+ let scan_dirs: Vec<String> = arguments
12
+ .get("scan_dirs")
13
+ .and_then(|v| v.as_array())
14
+ .map(|arr| {
15
+ arr.iter()
16
+ .filter_map(|v| v.as_str().map(String::from))
17
+ .collect()
18
+ })
19
+ .unwrap_or_else(|| vec!["~/pro/".to_string()]);
20
+
21
+ let mut projects = Vec::new();
22
+ let mut total_active = 0u32;
23
+ let mut total_blocked = 0u32;
24
+
25
+ for scan_dir in &scan_dirs {
26
+ let expanded = expand_tilde(scan_dir);
27
+ let expanded_path = Path::new(&expanded);
28
+
29
+ if !expanded_path.exists() {
30
+ continue;
31
+ }
32
+
33
+ let entries = match std::fs::read_dir(expanded_path) {
34
+ Ok(e) => e,
35
+ Err(_) => continue,
36
+ };
37
+
38
+ for entry in entries.flatten() {
39
+ if !entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) {
40
+ continue;
41
+ }
42
+
43
+ let project_path = entry.path();
44
+ let handoff_dir = project_path.join(".handoff");
45
+
46
+ if !handoff_dir.join("config.toml").exists() {
47
+ continue;
48
+ }
49
+
50
+ if let Ok(info) = collect_project_info(&project_path) {
51
+ total_active += info["active_tasks"].as_u64().unwrap_or(0) as u32;
52
+ total_blocked += info["blocked_tasks"].as_u64().unwrap_or(0) as u32;
53
+ projects.push(info);
54
+ }
55
+ }
56
+ }
57
+
58
+ let result = serde_json::json!({
59
+ "projects": projects,
60
+ "total_active_tasks": total_active,
61
+ "total_blocked": total_blocked,
62
+ });
63
+
64
+ serde_json::to_string_pretty(&result).context("Failed to serialize dashboard")
65
+ }
66
+
67
+ fn collect_project_info(project_path: &Path) -> Result<Value> {
68
+ let handoff_dir = project_path.join(".handoff");
69
+ let config = read_config(&handoff_dir.join("config.toml"))?;
70
+
71
+ let sessions = read_active_sessions(&handoff_dir.join("sessions"))?;
72
+
73
+ let (_, summary) =
74
+ build_task_index(&handoff_dir.join("tasks"), config.settings.done_task_limit)?;
75
+
76
+ let last_session_ended = sessions.last().and_then(|s| s.ended_at.clone());
77
+
78
+ let branch = sessions.last().and_then(|s| s.branch.clone());
79
+
80
+ let active_tasks = *summary.by_status.get("in_progress").unwrap_or(&0)
81
+ + *summary.by_status.get("todo").unwrap_or(&0)
82
+ + *summary.by_status.get("review").unwrap_or(&0);
83
+
84
+ let blocked_tasks = *summary.by_status.get("blocked").unwrap_or(&0);
85
+
86
+ let blockers: Vec<String> = sessions
87
+ .iter()
88
+ .flat_map(|s| s.blockers.iter().cloned())
89
+ .collect();
90
+
91
+ Ok(serde_json::json!({
92
+ "name": config.project.name,
93
+ "path": project_path.to_string_lossy(),
94
+ "last_session_ended": last_session_ended,
95
+ "branch": branch,
96
+ "active_tasks": active_tasks,
97
+ "blocked_tasks": blocked_tasks,
98
+ "blockers": blockers,
99
+ }))
100
+ }
101
+
102
+ fn expand_tilde(path: &str) -> String {
103
+ if let Some(rest) = path.strip_prefix("~/") {
104
+ if let Ok(home) = std::env::var("HOME") {
105
+ return format!("{home}/{rest}");
106
+ }
107
+ }
108
+ path.to_string()
109
+ }
@@ -0,0 +1,28 @@
1
+ use anyhow::Result;
2
+ use serde_json::Value;
3
+
4
+ use super::resolve_project_dir;
5
+ use crate::storage;
6
+
7
+ pub fn handle(arguments: &Value) -> Result<String> {
8
+ let project_dir = resolve_project_dir(arguments)?;
9
+
10
+ let project_name = arguments
11
+ .get("project_name")
12
+ .and_then(|v| v.as_str())
13
+ .ok_or_else(|| anyhow::anyhow!("project_name is required"))?;
14
+
15
+ let description = arguments
16
+ .get("description")
17
+ .and_then(|v| v.as_str())
18
+ .unwrap_or("");
19
+
20
+ storage::init_handoff(&project_dir, project_name, description)?;
21
+
22
+ Ok(format!(
23
+ "Initialized handoff tracking for '{}' at {}/.handoff/\n\
24
+ Created: config.toml, sessions/, tasks/",
25
+ project_name,
26
+ project_dir.display()
27
+ ))
28
+ }