handoff-mcp-server 0.4.0 → 0.6.1

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.lock CHANGED
@@ -144,7 +144,7 @@ dependencies = [
144
144
 
145
145
  [[package]]
146
146
  name = "handoff-mcp"
147
- version = "0.4.0"
147
+ version = "0.6.1"
148
148
  dependencies = [
149
149
  "anyhow",
150
150
  "chrono",
package/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "handoff-mcp"
3
- version = "0.4.0"
3
+ version = "0.6.1"
4
4
  edition = "2021"
5
5
  description = "MCP server that gives AI coding agents persistent memory across sessions"
6
6
  license = "MIT"
Binary file
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "handoff-mcp-server",
3
- "version": "0.4.0",
3
+ "version": "0.6.1",
4
4
  "description": "MCP server that gives AI coding agents persistent memory across sessions",
5
5
  "license": "MIT",
6
6
  "author": "AlphaElements <66808803+alphaelements@users.noreply.github.com>",
@@ -6,7 +6,7 @@ use serde_json::Value;
6
6
  use crate::storage::config::read_config;
7
7
  use crate::storage::expand_tilde;
8
8
  use crate::storage::referrals::read_referral_summaries;
9
- use crate::storage::sessions::read_active_sessions;
9
+ use crate::storage::sessions::{read_active_sessions, read_open_sessions};
10
10
  use crate::storage::tasks::build_task_index;
11
11
 
12
12
  pub fn handle(arguments: &Value) -> Result<String> {
@@ -70,7 +70,9 @@ fn collect_project_info(project_path: &Path) -> Result<Value> {
70
70
  let handoff_dir = project_path.join(".handoff");
71
71
  let config = read_config(&handoff_dir.join("config.toml"))?;
72
72
 
73
- let sessions = read_active_sessions(&handoff_dir.join("sessions"))?;
73
+ let sessions_dir = handoff_dir.join("sessions");
74
+ let mut sessions = read_open_sessions(&sessions_dir)?;
75
+ sessions.extend(read_active_sessions(&sessions_dir)?);
74
76
 
75
77
  let (_, summary) =
76
78
  build_task_index(&handoff_dir.join("tasks"), config.settings.done_task_limit)?;
@@ -7,7 +7,8 @@ use crate::storage::config::read_config;
7
7
  use crate::storage::ensure_handoff_exists;
8
8
  use crate::storage::git::capture_git_state;
9
9
  use crate::storage::sessions::{
10
- close_active_sessions, enforce_history_limit, write_active_session, SessionData,
10
+ close_active_sessions, close_open_sessions, enforce_history_limit, write_open_session,
11
+ SessionData,
11
12
  };
12
13
  use crate::storage::tasks::*;
13
14
 
@@ -58,7 +59,8 @@ pub fn handle(arguments: &Value) -> Result<String> {
58
59
  anyhow::anyhow!("'session.summary' is required when session is provided")
59
60
  })?;
60
61
 
61
- let closed = close_active_sessions(&sessions_dir)?;
62
+ close_active_sessions(&sessions_dir)?;
63
+ let closed = close_open_sessions(&sessions_dir)?;
62
64
  let git_state = capture_git_state(&project_dir)?;
63
65
  let now = Utc::now().to_rfc3339();
64
66
 
@@ -89,6 +91,7 @@ pub fn handle(arguments: &Value) -> Result<String> {
89
91
 
90
92
  let data = SessionData {
91
93
  version: 2,
94
+ id: None,
92
95
  ended_at: Some(now),
93
96
  summary: summary.to_string(),
94
97
  branch: Some(git_state.branch),
@@ -103,7 +106,7 @@ pub fn handle(arguments: &Value) -> Result<String> {
103
106
  environment: Some(environment),
104
107
  };
105
108
 
106
- write_active_session(&sessions_dir, &data)?;
109
+ write_open_session(&sessions_dir, &data)?;
107
110
 
108
111
  let history_limit = if config_path.exists() {
109
112
  read_config(&config_path)
@@ -118,12 +121,14 @@ pub fn handle(arguments: &Value) -> Result<String> {
118
121
  session_saved = true;
119
122
  } else if let Some(raw_notes) = arguments.get("raw_notes").and_then(|v| v.as_str()) {
120
123
  if !raw_notes.is_empty() {
121
- let closed = close_active_sessions(&sessions_dir)?;
124
+ close_active_sessions(&sessions_dir)?;
125
+ let closed = close_open_sessions(&sessions_dir)?;
122
126
  let git_state = capture_git_state(&project_dir)?;
123
127
  let now = Utc::now().to_rfc3339();
124
128
 
125
129
  let data = SessionData {
126
130
  version: 2,
131
+ id: None,
127
132
  ended_at: Some(now),
128
133
  summary: format!("[import] {source_description}"),
129
134
  branch: Some(git_state.branch),
@@ -146,7 +151,7 @@ pub fn handle(arguments: &Value) -> Result<String> {
146
151
  })),
147
152
  };
148
153
 
149
- write_active_session(&sessions_dir, &data)?;
154
+ write_open_session(&sessions_dir, &data)?;
150
155
 
151
156
  let history_limit = if config_path.exists() {
152
157
  read_config(&config_path)
@@ -4,7 +4,9 @@ use serde_json::Value;
4
4
  use super::resolve_project_dir;
5
5
  use crate::storage::config::read_config;
6
6
  use crate::storage::referrals::read_referral_summaries;
7
- use crate::storage::sessions::read_active_sessions;
7
+ use crate::storage::sessions::{
8
+ activate_open_sessions, activate_session_by_id, read_open_sessions,
9
+ };
8
10
  use crate::storage::tasks::build_task_index;
9
11
  use crate::storage::{ensure_handoff_exists, handoff_dir};
10
12
 
@@ -34,23 +36,21 @@ pub fn handle(arguments: &Value) -> Result<String> {
34
36
  anyhow::bail!("config.toml not found");
35
37
  };
36
38
 
37
- let sessions = read_active_sessions(&sessions_dir)?;
39
+ let target_session_id = arguments.get("session_id").and_then(|v| v.as_str());
38
40
 
39
- let (task_tree, task_summary) = build_task_index(&tasks_dir, config.settings.done_task_limit)?;
41
+ let sessions = read_open_sessions(&sessions_dir)?;
40
42
 
41
- let last_session = sessions.last().map(|s| {
42
- serde_json::json!({
43
- "ended_at": s.ended_at,
44
- "summary": s.summary,
45
- "branch": s.branch,
46
- "commit": s.commit,
47
- })
48
- });
43
+ let selected_session = if let Some(sid) = target_session_id {
44
+ activate_session_by_id(&sessions_dir, sid)?;
45
+ sessions
46
+ .into_iter()
47
+ .find(|s| s.id.as_deref().is_some_and(|id| id == sid))
48
+ } else {
49
+ activate_open_sessions(&sessions_dir)?;
50
+ sessions.into_iter().last()
51
+ };
49
52
 
50
- let active_sessions: Vec<Value> = sessions
51
- .iter()
52
- .map(|s| serde_json::to_value(s).unwrap_or_default())
53
- .collect();
53
+ let (task_tree, task_summary) = build_task_index(&tasks_dir, config.settings.done_task_limit)?;
54
54
 
55
55
  let mut result = serde_json::json!({
56
56
  "project": config.project.name,
@@ -58,12 +58,26 @@ pub fn handle(arguments: &Value) -> Result<String> {
58
58
  "task_summary": task_summary,
59
59
  });
60
60
 
61
- if let Some(ls) = last_session {
62
- result["last_session"] = ls;
61
+ if selected_session.is_none() {
62
+ if let Some(sid) = target_session_id {
63
+ result["warning"] =
64
+ serde_json::json!(format!("session_id '{sid}' not found among open sessions"));
65
+ }
63
66
  }
64
67
 
65
- if !active_sessions.is_empty() {
66
- let latest = &active_sessions[active_sessions.len() - 1];
68
+ if let Some(ref session) = selected_session {
69
+ result["last_session"] = serde_json::json!({
70
+ "ended_at": session.ended_at,
71
+ "summary": session.summary,
72
+ "branch": session.branch,
73
+ "commit": session.commit,
74
+ });
75
+
76
+ if let Some(ref id) = session.id {
77
+ result["session_id"] = serde_json::json!(id);
78
+ }
79
+
80
+ let session_val = serde_json::to_value(session).unwrap_or_default();
67
81
 
68
82
  for key in [
69
83
  "decisions",
@@ -73,20 +87,35 @@ pub fn handle(arguments: &Value) -> Result<String> {
73
87
  "references",
74
88
  "context_pointers",
75
89
  ] {
76
- if let Some(val) = latest.get(key) {
90
+ if let Some(val) = session_val.get(key) {
77
91
  if val.as_array().is_some_and(|a| !a.is_empty()) {
78
92
  result[key] = val.clone();
79
93
  }
80
94
  }
81
95
  }
82
96
 
83
- if let Some(env) = latest.get("environment") {
97
+ if let Some(env) = session_val.get("environment") {
84
98
  if !env.is_null() {
85
99
  result["environment"] = env.clone();
86
100
  }
87
101
  }
88
102
  }
89
103
 
104
+ if let Some(notes) = result.get("handoff_notes").and_then(|v| v.as_array()) {
105
+ let suggestions: Vec<&str> = notes
106
+ .iter()
107
+ .filter(|n| {
108
+ n.get("category")
109
+ .and_then(|c| c.as_str())
110
+ .is_some_and(|c| c == "suggestion")
111
+ })
112
+ .filter_map(|n| n.get("note").and_then(|v| v.as_str()))
113
+ .collect();
114
+ if !suggestions.is_empty() {
115
+ result["next_actions"] = serde_json::json!(suggestions);
116
+ }
117
+ }
118
+
90
119
  if !config.settings.context_files.is_empty() {
91
120
  result["suggested_reads"] = serde_json::to_value(&config.settings.context_files)?;
92
121
  }
@@ -93,10 +93,165 @@ pub fn handle(arguments: &Value) -> Result<String> {
93
93
  target_dir.to_string_lossy().to_string()
94
94
  };
95
95
 
96
- Ok(format!(
96
+ let mut msg = format!(
97
97
  "Referral sent: {id}\n From: {}\n To: {target_name}\n Type: {referral_type}\n Summary: {summary}",
98
98
  source_config.project.name
99
- ))
99
+ );
100
+
101
+ for w in collect_refer_warnings(&data, &source_project_dir, &target_dir) {
102
+ msg.push_str(&format!("\n{w}"));
103
+ }
104
+
105
+ Ok(msg)
106
+ }
107
+
108
+ fn collect_refer_warnings(
109
+ data: &ReferralData,
110
+ source_dir: &Path,
111
+ target_dir: &Path,
112
+ ) -> Vec<String> {
113
+ let mut warnings = Vec::new();
114
+
115
+ if data.details.is_none() {
116
+ warnings.push(
117
+ "Warning: No details. The target project won't know what specifically to do. \
118
+ Add a 'details' field describing the change, its impact, and what needs updating."
119
+ .to_string(),
120
+ );
121
+ }
122
+
123
+ if data.tasks.is_empty() {
124
+ warnings.push(
125
+ "Warning: No tasks. Consider adding suggested tasks with done_criteria \
126
+ so the target project has actionable items to work from."
127
+ .to_string(),
128
+ );
129
+ }
130
+
131
+ if let Some(ref ctx) = data.context {
132
+ let refs = collect_refs_from_value(ctx);
133
+ if refs.is_empty() {
134
+ warnings.push(
135
+ "Warning: context has no spec/doc references. Add 'spec_docs' with wiki paths, \
136
+ MR URLs, or file paths so the target can find the authoritative specification."
137
+ .to_string(),
138
+ );
139
+ } else {
140
+ for r in &refs {
141
+ if r.starts_with("http://") || r.starts_with("https://") {
142
+ continue;
143
+ }
144
+ let clean = r.split(" — ").next().unwrap_or(r).trim();
145
+ let clean = clean.split('#').next().unwrap_or(clean).trim();
146
+ let p = Path::new(clean);
147
+ if p.is_absolute() {
148
+ if !p.exists() {
149
+ warnings.push(format!(
150
+ "Warning: spec reference path does not exist: {clean}"
151
+ ));
152
+ }
153
+ } else {
154
+ let in_source = source_dir.join(clean);
155
+ let in_target = target_dir.join(clean);
156
+ if !in_source.exists() && !in_target.exists() {
157
+ warnings.push(format!(
158
+ "Warning: spec reference path does not exist \
159
+ in source or target project: {clean}"
160
+ ));
161
+ } else if !in_target.exists() {
162
+ warnings.push(format!(
163
+ "Warning: spec reference '{clean}' exists in source project \
164
+ but not in target project. Use an absolute path or ensure \
165
+ the target has this file."
166
+ ));
167
+ }
168
+ }
169
+ }
170
+ }
171
+ } else {
172
+ warnings.push(
173
+ "Warning: No context. Add a 'context' field with 'spec_docs' referencing the \
174
+ authoritative specification (wiki paths, MR URLs, source file paths)."
175
+ .to_string(),
176
+ );
177
+ }
178
+
179
+ if data.priority.is_none() {
180
+ warnings.push(
181
+ "Warning: No priority. Set 'priority' (low/medium/high) so the target project \
182
+ can triage this referral appropriately."
183
+ .to_string(),
184
+ );
185
+ }
186
+
187
+ for (i, task) in data.tasks.iter().enumerate() {
188
+ let has_criteria = task
189
+ .get("done_criteria")
190
+ .and_then(|v| v.as_array())
191
+ .is_some_and(|a| !a.is_empty());
192
+ if !has_criteria {
193
+ let title = task
194
+ .get("title")
195
+ .and_then(|v| v.as_str())
196
+ .unwrap_or("(untitled)");
197
+ warnings.push(format!(
198
+ "Warning: Task #{} '{}' has no done_criteria. \
199
+ Add criteria so the target knows when the task is complete.",
200
+ i + 1,
201
+ title
202
+ ));
203
+ }
204
+ }
205
+
206
+ warnings
207
+ }
208
+
209
+ fn is_ref_string(s: &str) -> bool {
210
+ s.starts_with("http://")
211
+ || s.starts_with("https://")
212
+ || s.starts_with('/')
213
+ || s.starts_with("wiki/")
214
+ || s.starts_with("docs/")
215
+ || s.starts_with("src/")
216
+ || s.ends_with(".md")
217
+ || s.ends_with(".rs")
218
+ || s.ends_with(".ts")
219
+ || s.ends_with(".toml")
220
+ }
221
+
222
+ fn collect_refs_from_value(val: &Value) -> Vec<String> {
223
+ let mut refs = Vec::new();
224
+ match val {
225
+ Value::String(s) => {
226
+ if is_ref_string(s) {
227
+ refs.push(s.clone());
228
+ }
229
+ }
230
+ Value::Array(arr) => {
231
+ for item in arr {
232
+ refs.extend(collect_refs_from_value(item));
233
+ }
234
+ }
235
+ Value::Object(map) => {
236
+ for (key, v) in map {
237
+ if key == "spec_docs"
238
+ || key == "source_wiki"
239
+ || key == "source_data_model"
240
+ || key.contains("spec")
241
+ || key.contains("doc")
242
+ || key.contains("wiki")
243
+ {
244
+ refs.extend(collect_refs_from_value(v));
245
+ } else if let Value::String(s) = v {
246
+ if is_ref_string(s) {
247
+ refs.push(s.clone());
248
+ }
249
+ }
250
+ }
251
+ }
252
+ _ => {}
253
+ }
254
+ refs
100
255
  }
101
256
 
102
257
  fn resolve_target(arguments: &Value, scan_dirs: &[String]) -> Result<PathBuf> {
@@ -1,3 +1,5 @@
1
+ use std::path::Path;
2
+
1
3
  use anyhow::Result;
2
4
  use chrono::Utc;
3
5
  use serde_json::Value;
@@ -7,7 +9,8 @@ use crate::storage::config::read_config;
7
9
  use crate::storage::ensure_handoff_exists;
8
10
  use crate::storage::git::capture_git_state;
9
11
  use crate::storage::sessions::{
10
- close_active_sessions, enforce_history_limit, write_active_session, SessionData,
12
+ close_active_sessions, close_open_sessions, close_session_by_id, enforce_history_limit,
13
+ generate_session_id, write_open_session, SessionData,
11
14
  };
12
15
 
13
16
  pub fn handle(arguments: &Value) -> Result<String> {
@@ -22,13 +25,28 @@ pub fn handle(arguments: &Value) -> Result<String> {
22
25
  .and_then(|v| v.as_str())
23
26
  .ok_or_else(|| anyhow::anyhow!("'summary' is required"))?;
24
27
 
25
- let closed = close_active_sessions(&sessions_dir)?;
28
+ let close_id = arguments.get("close_session_id").and_then(|v| v.as_str());
29
+
30
+ let total_closed = if let Some(id) = close_id {
31
+ let closed = close_session_by_id(&sessions_dir, id)?;
32
+ if closed.is_some() {
33
+ 1
34
+ } else {
35
+ 0
36
+ }
37
+ } else {
38
+ let closed_active = close_active_sessions(&sessions_dir)?;
39
+ let closed_open = close_open_sessions(&sessions_dir)?;
40
+ closed_active.len() + closed_open.len()
41
+ };
26
42
 
27
43
  let git_state = capture_git_state(&project_dir)?;
28
44
  let now = Utc::now().to_rfc3339();
45
+ let session_id = generate_session_id();
29
46
 
30
47
  let data = SessionData {
31
48
  version: 2,
49
+ id: Some(session_id.clone()),
32
50
  ended_at: Some(now),
33
51
  summary: summary.to_string(),
34
52
  branch: Some(git_state.branch),
@@ -43,7 +61,7 @@ pub fn handle(arguments: &Value) -> Result<String> {
43
61
  environment: arguments.get("environment").cloned(),
44
62
  };
45
63
 
46
- let path = write_active_session(&sessions_dir, &data)?;
64
+ let path = write_open_session(&sessions_dir, &data)?;
47
65
 
48
66
  let history_limit = if config_path.exists() {
49
67
  read_config(&config_path)
@@ -55,18 +73,23 @@ pub fn handle(arguments: &Value) -> Result<String> {
55
73
  let removed = enforce_history_limit(&sessions_dir, history_limit)?;
56
74
 
57
75
  let mut msg = format!(
58
- "Session saved: {}\nFile: {}",
76
+ "Session saved: {}\nSession ID: {}\nFile: {}",
59
77
  summary,
78
+ session_id,
60
79
  path.file_name()
61
80
  .map(|n| n.to_string_lossy().to_string())
62
81
  .unwrap_or_default()
63
82
  );
64
83
 
65
- if !closed.is_empty() {
66
- msg.push_str(&format!(
67
- "\nClosed {} previous active session(s)",
68
- closed.len()
69
- ));
84
+ if total_closed > 0 {
85
+ msg.push_str(&format!("\nClosed {} previous session(s)", total_closed));
86
+ }
87
+ if let Some(id) = close_id {
88
+ if total_closed == 0 {
89
+ msg.push_str(&format!(
90
+ "\nWarning: close_session_id '{id}' not found among active/open sessions"
91
+ ));
92
+ }
70
93
  }
71
94
  if removed > 0 {
72
95
  msg.push_str(&format!(
@@ -74,9 +97,130 @@ pub fn handle(arguments: &Value) -> Result<String> {
74
97
  ));
75
98
  }
76
99
 
100
+ for w in collect_save_warnings(&data, &project_dir) {
101
+ msg.push_str(&format!("\n{w}"));
102
+ }
103
+
77
104
  Ok(msg)
78
105
  }
79
106
 
107
+ fn collect_save_warnings(data: &SessionData, project_dir: &Path) -> Vec<String> {
108
+ let mut warnings = Vec::new();
109
+
110
+ if data.checklist.is_empty() {
111
+ warnings.push(
112
+ "Warning: No checklist items. Consider adding verification items for the next session."
113
+ .to_string(),
114
+ );
115
+ } else {
116
+ let unchecked: Vec<&str> = data
117
+ .checklist
118
+ .iter()
119
+ .filter_map(|item| {
120
+ let checked = item
121
+ .get("checked")
122
+ .and_then(|v| v.as_bool())
123
+ .unwrap_or(false);
124
+ if !checked {
125
+ item.get("item").and_then(|v| v.as_str())
126
+ } else {
127
+ None
128
+ }
129
+ })
130
+ .collect();
131
+
132
+ if !unchecked.is_empty() {
133
+ warnings.push(format!(
134
+ "Warning: {} unchecked checklist item(s) \u{2014} {}",
135
+ unchecked.len(),
136
+ unchecked.join(", ")
137
+ ));
138
+ }
139
+ }
140
+
141
+ let has_suggestion = data.handoff_notes.iter().any(|note| {
142
+ note.get("category")
143
+ .and_then(|v| v.as_str())
144
+ .is_some_and(|c| c == "suggestion")
145
+ });
146
+ if !has_suggestion {
147
+ warnings.push(
148
+ "Warning: No 'suggestion' handoff_notes \u{2014} the next session won't know what to \
149
+ do first. Add at least one note with category 'suggestion' describing the recommended \
150
+ next action."
151
+ .to_string(),
152
+ );
153
+ }
154
+
155
+ if data.context_pointers.is_empty() {
156
+ warnings.push(
157
+ "Warning: No context_pointers. The next session won't know which files to read first."
158
+ .to_string(),
159
+ );
160
+ }
161
+
162
+ if data.decisions.is_empty() {
163
+ warnings.push(
164
+ "Warning: No decisions recorded. Consider documenting key decisions made during this session."
165
+ .to_string(),
166
+ );
167
+ }
168
+
169
+ if data.references.is_empty() {
170
+ warnings.push(
171
+ "Warning: No references. Consider adding links to relevant docs, issues, or MRs."
172
+ .to_string(),
173
+ );
174
+ } else {
175
+ for r in &data.references {
176
+ let uri = r.get("uri").and_then(|v| v.as_str()).unwrap_or("");
177
+ if uri.is_empty() {
178
+ continue;
179
+ }
180
+ if uri.starts_with("http://") || uri.starts_with("https://") {
181
+ continue;
182
+ }
183
+ if uri.starts_with("ref-") {
184
+ continue;
185
+ }
186
+ let resolved = if Path::new(uri).is_absolute() {
187
+ std::path::PathBuf::from(uri)
188
+ } else {
189
+ project_dir.join(uri)
190
+ };
191
+ let check_path = resolved
192
+ .to_string_lossy()
193
+ .split('#')
194
+ .next()
195
+ .unwrap_or("")
196
+ .to_string();
197
+ if !Path::new(&check_path).exists() {
198
+ let label = r.get("label").and_then(|v| v.as_str()).unwrap_or("");
199
+ warnings.push(format!(
200
+ "Warning: reference '{label}' path does not exist: {uri}"
201
+ ));
202
+ }
203
+ }
204
+ }
205
+
206
+ for cp in &data.context_pointers {
207
+ let p = cp.get("path").and_then(|v| v.as_str()).unwrap_or("");
208
+ if p.is_empty() {
209
+ continue;
210
+ }
211
+ let resolved = if Path::new(p).is_absolute() {
212
+ std::path::PathBuf::from(p)
213
+ } else {
214
+ project_dir.join(p)
215
+ };
216
+ if !resolved.exists() {
217
+ warnings.push(format!("Warning: context_pointer path does not exist: {p}"));
218
+ }
219
+ }
220
+
221
+ warnings
222
+ }
223
+
80
224
  fn extract_array(val: &Value, key: &str) -> Vec<Value> {
81
225
  val.get(key)
82
226
  .and_then(|v| v.as_array())
@@ -2,7 +2,7 @@ use anyhow::{Context, Result};
2
2
  use serde_json::{json, Value};
3
3
 
4
4
  use crate::storage::config::read_config;
5
- use crate::storage::sessions::read_active_sessions;
5
+ use crate::storage::sessions::{read_active_sessions, read_open_sessions};
6
6
  use crate::storage::{ensure_handoff_exists, handoff_dir};
7
7
 
8
8
  pub fn handle_resource_read(uri: &str) -> Result<Value> {
@@ -24,7 +24,8 @@ pub fn handle_resource_read(uri: &str) -> Result<Value> {
24
24
 
25
25
  fn read_sessions_resource(handoff: &std::path::Path) -> Result<Value> {
26
26
  let sessions_dir = handoff.join("sessions");
27
- let sessions = read_active_sessions(&sessions_dir)?;
27
+ let mut sessions = read_open_sessions(&sessions_dir)?;
28
+ sessions.extend(read_active_sessions(&sessions_dir)?);
28
29
 
29
30
  let contents: Vec<Value> = sessions
30
31
  .iter()
package/src/mcp/router.rs CHANGED
@@ -48,7 +48,7 @@ fn handle_initialize() -> JsonRpcResponse {
48
48
  ## Session Start\n\
49
49
  1. Call handoff_load_context (no args needed — uses cwd)\n\
50
50
  2. If it returns \"not initialized\", call handoff_init with the project name\n\
51
- 3. Review returned tasks, decisions, blockers, and git state before proceeding\n\n\
51
+ 3. Check the `next_actions` array first these are the previous session's recommended next steps. Do not re-verify work the previous session already completed\n\n\
52
52
  ## Session End\n\
53
53
  1. Call handoff_save_context with:\n\
54
54
  - summary: one-line description of what was accomplished\n\