handoff-mcp-server 0.4.0 → 0.6.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.lock +1 -1
- package/Cargo.toml +1 -1
- package/package.json +1 -1
- package/src/mcp/handlers/dashboard.rs +4 -2
- package/src/mcp/handlers/import_context.rs +10 -5
- package/src/mcp/handlers/load_context.rs +50 -21
- package/src/mcp/handlers/save_context.rs +106 -9
- package/src/mcp/resources.rs +3 -2
- package/src/mcp/router.rs +1 -1
- package/src/mcp/tools.rs +49 -25
- package/src/storage/sessions.rs +161 -33
- package/tests/storage_sessions.rs +189 -25
- package/tests/tool_import_context.rs +1 -1
- package/tests/tool_sessions.rs +638 -5
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "handoff-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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::
|
|
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
|
|
39
|
+
let target_session_id = arguments.get("session_id").and_then(|v| v.as_str());
|
|
38
40
|
|
|
39
|
-
let
|
|
41
|
+
let sessions = read_open_sessions(&sessions_dir)?;
|
|
40
42
|
|
|
41
|
-
let
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
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
|
|
62
|
-
|
|
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
|
|
66
|
-
|
|
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) =
|
|
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) =
|
|
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
|
}
|
|
@@ -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,
|
|
10
|
+
close_active_sessions, close_open_sessions, close_session_by_id, enforce_history_limit,
|
|
11
|
+
generate_session_id, write_open_session, SessionData,
|
|
11
12
|
};
|
|
12
13
|
|
|
13
14
|
pub fn handle(arguments: &Value) -> Result<String> {
|
|
@@ -22,13 +23,28 @@ pub fn handle(arguments: &Value) -> Result<String> {
|
|
|
22
23
|
.and_then(|v| v.as_str())
|
|
23
24
|
.ok_or_else(|| anyhow::anyhow!("'summary' is required"))?;
|
|
24
25
|
|
|
25
|
-
let
|
|
26
|
+
let close_id = arguments.get("close_session_id").and_then(|v| v.as_str());
|
|
27
|
+
|
|
28
|
+
let total_closed = if let Some(id) = close_id {
|
|
29
|
+
let closed = close_session_by_id(&sessions_dir, id)?;
|
|
30
|
+
if closed.is_some() {
|
|
31
|
+
1
|
|
32
|
+
} else {
|
|
33
|
+
0
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
let closed_active = close_active_sessions(&sessions_dir)?;
|
|
37
|
+
let closed_open = close_open_sessions(&sessions_dir)?;
|
|
38
|
+
closed_active.len() + closed_open.len()
|
|
39
|
+
};
|
|
26
40
|
|
|
27
41
|
let git_state = capture_git_state(&project_dir)?;
|
|
28
42
|
let now = Utc::now().to_rfc3339();
|
|
43
|
+
let session_id = generate_session_id();
|
|
29
44
|
|
|
30
45
|
let data = SessionData {
|
|
31
46
|
version: 2,
|
|
47
|
+
id: Some(session_id.clone()),
|
|
32
48
|
ended_at: Some(now),
|
|
33
49
|
summary: summary.to_string(),
|
|
34
50
|
branch: Some(git_state.branch),
|
|
@@ -43,7 +59,7 @@ pub fn handle(arguments: &Value) -> Result<String> {
|
|
|
43
59
|
environment: arguments.get("environment").cloned(),
|
|
44
60
|
};
|
|
45
61
|
|
|
46
|
-
let path =
|
|
62
|
+
let path = write_open_session(&sessions_dir, &data)?;
|
|
47
63
|
|
|
48
64
|
let history_limit = if config_path.exists() {
|
|
49
65
|
read_config(&config_path)
|
|
@@ -55,18 +71,23 @@ pub fn handle(arguments: &Value) -> Result<String> {
|
|
|
55
71
|
let removed = enforce_history_limit(&sessions_dir, history_limit)?;
|
|
56
72
|
|
|
57
73
|
let mut msg = format!(
|
|
58
|
-
"Session saved: {}\nFile: {}",
|
|
74
|
+
"Session saved: {}\nSession ID: {}\nFile: {}",
|
|
59
75
|
summary,
|
|
76
|
+
session_id,
|
|
60
77
|
path.file_name()
|
|
61
78
|
.map(|n| n.to_string_lossy().to_string())
|
|
62
79
|
.unwrap_or_default()
|
|
63
80
|
);
|
|
64
81
|
|
|
65
|
-
if
|
|
66
|
-
msg.push_str(&format!(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
82
|
+
if total_closed > 0 {
|
|
83
|
+
msg.push_str(&format!("\nClosed {} previous session(s)", total_closed));
|
|
84
|
+
}
|
|
85
|
+
if let Some(id) = close_id {
|
|
86
|
+
if total_closed == 0 {
|
|
87
|
+
msg.push_str(&format!(
|
|
88
|
+
"\nWarning: close_session_id '{id}' not found among active/open sessions"
|
|
89
|
+
));
|
|
90
|
+
}
|
|
70
91
|
}
|
|
71
92
|
if removed > 0 {
|
|
72
93
|
msg.push_str(&format!(
|
|
@@ -74,9 +95,85 @@ pub fn handle(arguments: &Value) -> Result<String> {
|
|
|
74
95
|
));
|
|
75
96
|
}
|
|
76
97
|
|
|
98
|
+
for w in collect_save_warnings(&data) {
|
|
99
|
+
msg.push_str(&format!("\n{w}"));
|
|
100
|
+
}
|
|
101
|
+
|
|
77
102
|
Ok(msg)
|
|
78
103
|
}
|
|
79
104
|
|
|
105
|
+
fn collect_save_warnings(data: &SessionData) -> Vec<String> {
|
|
106
|
+
let mut warnings = Vec::new();
|
|
107
|
+
|
|
108
|
+
if data.checklist.is_empty() {
|
|
109
|
+
warnings.push(
|
|
110
|
+
"Warning: No checklist items. Consider adding verification items for the next session."
|
|
111
|
+
.to_string(),
|
|
112
|
+
);
|
|
113
|
+
} else {
|
|
114
|
+
let unchecked: Vec<&str> = data
|
|
115
|
+
.checklist
|
|
116
|
+
.iter()
|
|
117
|
+
.filter_map(|item| {
|
|
118
|
+
let checked = item
|
|
119
|
+
.get("checked")
|
|
120
|
+
.and_then(|v| v.as_bool())
|
|
121
|
+
.unwrap_or(false);
|
|
122
|
+
if !checked {
|
|
123
|
+
item.get("item").and_then(|v| v.as_str())
|
|
124
|
+
} else {
|
|
125
|
+
None
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
.collect();
|
|
129
|
+
|
|
130
|
+
if !unchecked.is_empty() {
|
|
131
|
+
warnings.push(format!(
|
|
132
|
+
"Warning: {} unchecked checklist item(s) \u{2014} {}",
|
|
133
|
+
unchecked.len(),
|
|
134
|
+
unchecked.join(", ")
|
|
135
|
+
));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let has_suggestion = data.handoff_notes.iter().any(|note| {
|
|
140
|
+
note.get("category")
|
|
141
|
+
.and_then(|v| v.as_str())
|
|
142
|
+
.is_some_and(|c| c == "suggestion")
|
|
143
|
+
});
|
|
144
|
+
if !has_suggestion {
|
|
145
|
+
warnings.push(
|
|
146
|
+
"Warning: No 'suggestion' handoff_notes \u{2014} the next session won't know what to \
|
|
147
|
+
do first. Add at least one note with category 'suggestion' describing the recommended \
|
|
148
|
+
next action."
|
|
149
|
+
.to_string(),
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if data.context_pointers.is_empty() {
|
|
154
|
+
warnings.push(
|
|
155
|
+
"Warning: No context_pointers. The next session won't know which files to read first."
|
|
156
|
+
.to_string(),
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if data.decisions.is_empty() {
|
|
161
|
+
warnings.push(
|
|
162
|
+
"Warning: No decisions recorded. Consider documenting key decisions made during this session."
|
|
163
|
+
.to_string(),
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if data.references.is_empty() {
|
|
168
|
+
warnings.push(
|
|
169
|
+
"Warning: No references. Consider adding links to relevant docs, issues, or MRs."
|
|
170
|
+
.to_string(),
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
warnings
|
|
175
|
+
}
|
|
176
|
+
|
|
80
177
|
fn extract_array(val: &Value, key: &str) -> Vec<Value> {
|
|
81
178
|
val.get(key)
|
|
82
179
|
.and_then(|v| v.as_array())
|
package/src/mcp/resources.rs
CHANGED
|
@@ -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 =
|
|
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.
|
|
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\
|
package/src/mcp/tools.rs
CHANGED
|
@@ -35,6 +35,10 @@ pub fn all_tool_definitions() -> Vec<ToolDefinition> {
|
|
|
35
35
|
"project_dir": {
|
|
36
36
|
"type": "string",
|
|
37
37
|
"description": "Project directory path. Defaults to current working directory."
|
|
38
|
+
},
|
|
39
|
+
"session_id": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"description": "Session ID to activate and load. If omitted, activates all open sessions and returns the latest."
|
|
38
42
|
}
|
|
39
43
|
}
|
|
40
44
|
}),
|
|
@@ -53,16 +57,21 @@ pub fn all_tool_definitions() -> Vec<ToolDefinition> {
|
|
|
53
57
|
"type": "string",
|
|
54
58
|
"description": "One-line summary of this session"
|
|
55
59
|
},
|
|
60
|
+
"close_session_id": {
|
|
61
|
+
"type": "string",
|
|
62
|
+
"description": "Session ID to close. If omitted, all active and open sessions are closed (default behavior)."
|
|
63
|
+
},
|
|
56
64
|
"decisions": {
|
|
57
65
|
"type": "array",
|
|
58
66
|
"description": "Decisions made during this session",
|
|
59
67
|
"items": {
|
|
60
68
|
"type": "object",
|
|
61
69
|
"properties": {
|
|
62
|
-
"decision": { "type": "string" },
|
|
63
|
-
"reason": { "type": "string" },
|
|
70
|
+
"decision": { "type": "string", "description": "What was decided" },
|
|
71
|
+
"reason": { "type": "string", "description": "Why this decision was made" },
|
|
64
72
|
"confidence": {
|
|
65
73
|
"type": "string",
|
|
74
|
+
"description": "confirmed = verified by testing/evidence; estimated = reasoned but not verified; unverified = hypothesis needing validation",
|
|
66
75
|
"enum": ["confirmed", "estimated", "unverified"]
|
|
67
76
|
}
|
|
68
77
|
},
|
|
@@ -71,17 +80,20 @@ pub fn all_tool_definitions() -> Vec<ToolDefinition> {
|
|
|
71
80
|
},
|
|
72
81
|
"blockers": {
|
|
73
82
|
"type": "array",
|
|
83
|
+
"description": "Issues preventing progress. The next session should address these before starting new work.",
|
|
74
84
|
"items": { "type": "string" }
|
|
75
85
|
},
|
|
76
86
|
"checklist": {
|
|
77
87
|
"type": "array",
|
|
88
|
+
"description": "Verification items for the next session or user. Mark completed items as checked:true before saving.",
|
|
78
89
|
"items": {
|
|
79
90
|
"type": "object",
|
|
80
91
|
"properties": {
|
|
81
|
-
"item": { "type": "string" },
|
|
82
|
-
"checked": { "type": "boolean" },
|
|
92
|
+
"item": { "type": "string", "description": "What to verify or confirm" },
|
|
93
|
+
"checked": { "type": "boolean", "description": "true if already verified, false if pending" },
|
|
83
94
|
"owner": {
|
|
84
95
|
"type": "string",
|
|
96
|
+
"description": "user = requires human action; ai = the next AI session should handle this",
|
|
85
97
|
"enum": ["user", "ai"]
|
|
86
98
|
}
|
|
87
99
|
},
|
|
@@ -90,12 +102,14 @@ pub fn all_tool_definitions() -> Vec<ToolDefinition> {
|
|
|
90
102
|
},
|
|
91
103
|
"handoff_notes": {
|
|
92
104
|
"type": "array",
|
|
105
|
+
"description": "Notes for the next session. Include at least one 'suggestion' with a concrete next action.",
|
|
93
106
|
"items": {
|
|
94
107
|
"type": "object",
|
|
95
108
|
"properties": {
|
|
96
|
-
"note": { "type": "string" },
|
|
109
|
+
"note": { "type": "string", "description": "The note content. For suggestions: state what is ALREADY DONE, then describe the concrete next action." },
|
|
97
110
|
"category": {
|
|
98
111
|
"type": "string",
|
|
112
|
+
"description": "caution = risks/rules the next session must respect; context = background info for decisions; suggestion = concrete next action the next session should execute first (at least one required)",
|
|
99
113
|
"enum": ["caution", "context", "suggestion"]
|
|
100
114
|
}
|
|
101
115
|
},
|
|
@@ -104,28 +118,31 @@ pub fn all_tool_definitions() -> Vec<ToolDefinition> {
|
|
|
104
118
|
},
|
|
105
119
|
"references": {
|
|
106
120
|
"type": "array",
|
|
121
|
+
"description": "Links to related docs, issues, MRs, or external resources for reference (not active work files — use context_pointers for those).",
|
|
107
122
|
"items": {
|
|
108
123
|
"type": "object",
|
|
109
124
|
"properties": {
|
|
110
|
-
"label": { "type": "string" },
|
|
111
|
-
"uri": { "type": "string" },
|
|
125
|
+
"label": { "type": "string", "description": "Human-readable label for this reference" },
|
|
126
|
+
"uri": { "type": "string", "description": "Path, URL, or identifier" },
|
|
112
127
|
"type": {
|
|
113
128
|
"type": "string",
|
|
129
|
+
"description": "file = project file; issue = issue tracker; mr = merge/pull request; wiki = wiki page; doc = design document; url = external URL",
|
|
114
130
|
"enum": ["file", "issue", "mr", "wiki", "doc", "url"]
|
|
115
131
|
},
|
|
116
|
-
"notes": { "type": "string" }
|
|
132
|
+
"notes": { "type": "string", "description": "Additional context (e.g. 'see section 3 for root cause analysis')" }
|
|
117
133
|
},
|
|
118
134
|
"required": ["label", "uri"]
|
|
119
135
|
}
|
|
120
136
|
},
|
|
121
137
|
"context_pointers": {
|
|
122
138
|
"type": "array",
|
|
139
|
+
"description": "Files the next session should open first to resume work. Point to files that NEED WORK, not completed files. For completed files, use a 'context' handoff_note instead.",
|
|
123
140
|
"items": {
|
|
124
141
|
"type": "object",
|
|
125
142
|
"properties": {
|
|
126
|
-
"path": { "type": "string" },
|
|
127
|
-
"reason": { "type": "string" },
|
|
128
|
-
"lines": { "type": "string" }
|
|
143
|
+
"path": { "type": "string", "description": "File path relative to project root" },
|
|
144
|
+
"reason": { "type": "string", "description": "Why the next session should read this (e.g. 'resume implementation here', 'needs review')" },
|
|
145
|
+
"lines": { "type": "string", "description": "Line range to focus on (e.g. '42-78')" }
|
|
129
146
|
},
|
|
130
147
|
"required": ["path"]
|
|
131
148
|
}
|
|
@@ -366,13 +383,15 @@ pub fn all_tool_definitions() -> Vec<ToolDefinition> {
|
|
|
366
383
|
"summary": { "type": "string", "description": "One-line summary (required)" },
|
|
367
384
|
"decisions": {
|
|
368
385
|
"type": "array",
|
|
386
|
+
"description": "Decisions made during this session",
|
|
369
387
|
"items": {
|
|
370
388
|
"type": "object",
|
|
371
389
|
"properties": {
|
|
372
|
-
"decision": { "type": "string" },
|
|
373
|
-
"reason": { "type": "string" },
|
|
390
|
+
"decision": { "type": "string", "description": "What was decided" },
|
|
391
|
+
"reason": { "type": "string", "description": "Why this decision was made" },
|
|
374
392
|
"confidence": {
|
|
375
393
|
"type": "string",
|
|
394
|
+
"description": "confirmed = verified; estimated = reasoned but not verified; unverified = hypothesis",
|
|
376
395
|
"enum": ["confirmed", "estimated", "unverified"]
|
|
377
396
|
}
|
|
378
397
|
},
|
|
@@ -381,52 +400,57 @@ pub fn all_tool_definitions() -> Vec<ToolDefinition> {
|
|
|
381
400
|
},
|
|
382
401
|
"blockers": {
|
|
383
402
|
"type": "array",
|
|
403
|
+
"description": "Issues preventing progress",
|
|
384
404
|
"items": { "type": "string" }
|
|
385
405
|
},
|
|
386
406
|
"checklist": {
|
|
387
407
|
"type": "array",
|
|
408
|
+
"description": "Verification items for the next session or user",
|
|
388
409
|
"items": {
|
|
389
410
|
"type": "object",
|
|
390
411
|
"properties": {
|
|
391
|
-
"item": { "type": "string" },
|
|
392
|
-
"checked": { "type": "boolean" },
|
|
393
|
-
"owner": { "type": "string", "enum": ["user", "ai"] }
|
|
412
|
+
"item": { "type": "string", "description": "What to verify" },
|
|
413
|
+
"checked": { "type": "boolean", "description": "true if verified, false if pending" },
|
|
414
|
+
"owner": { "type": "string", "description": "user = human action; ai = next AI session", "enum": ["user", "ai"] }
|
|
394
415
|
},
|
|
395
416
|
"required": ["item"]
|
|
396
417
|
}
|
|
397
418
|
},
|
|
398
419
|
"handoff_notes": {
|
|
399
420
|
"type": "array",
|
|
421
|
+
"description": "Notes for the next session. Include at least one 'suggestion' with a concrete next action.",
|
|
400
422
|
"items": {
|
|
401
423
|
"type": "object",
|
|
402
424
|
"properties": {
|
|
403
|
-
"note": { "type": "string" },
|
|
404
|
-
"category": { "type": "string", "enum": ["caution", "context", "suggestion"] }
|
|
425
|
+
"note": { "type": "string", "description": "The note content. For suggestions: state what is done, then the next action." },
|
|
426
|
+
"category": { "type": "string", "description": "caution = risks/rules; context = background; suggestion = concrete next action (at least one required)", "enum": ["caution", "context", "suggestion"] }
|
|
405
427
|
},
|
|
406
428
|
"required": ["note"]
|
|
407
429
|
}
|
|
408
430
|
},
|
|
409
431
|
"references": {
|
|
410
432
|
"type": "array",
|
|
433
|
+
"description": "Links to related docs, issues, MRs (not active work files)",
|
|
411
434
|
"items": {
|
|
412
435
|
"type": "object",
|
|
413
436
|
"properties": {
|
|
414
|
-
"label": { "type": "string" },
|
|
415
|
-
"uri": { "type": "string" },
|
|
416
|
-
"type": { "type": "string", "enum": ["file", "issue", "mr", "wiki", "doc", "url"] },
|
|
417
|
-
"notes": { "type": "string" }
|
|
437
|
+
"label": { "type": "string", "description": "Human-readable label" },
|
|
438
|
+
"uri": { "type": "string", "description": "Path, URL, or identifier" },
|
|
439
|
+
"type": { "type": "string", "description": "file/issue/mr/wiki/doc/url", "enum": ["file", "issue", "mr", "wiki", "doc", "url"] },
|
|
440
|
+
"notes": { "type": "string", "description": "Additional context" }
|
|
418
441
|
},
|
|
419
442
|
"required": ["label", "uri"]
|
|
420
443
|
}
|
|
421
444
|
},
|
|
422
445
|
"context_pointers": {
|
|
423
446
|
"type": "array",
|
|
447
|
+
"description": "Files the next session should open first to resume work (not completed files)",
|
|
424
448
|
"items": {
|
|
425
449
|
"type": "object",
|
|
426
450
|
"properties": {
|
|
427
|
-
"path": { "type": "string" },
|
|
428
|
-
"reason": { "type": "string" },
|
|
429
|
-
"lines": { "type": "string" }
|
|
451
|
+
"path": { "type": "string", "description": "File path relative to project root" },
|
|
452
|
+
"reason": { "type": "string", "description": "Why to read this file" },
|
|
453
|
+
"lines": { "type": "string", "description": "Line range (e.g. '42-78')" }
|
|
430
454
|
},
|
|
431
455
|
"required": ["path"]
|
|
432
456
|
}
|