handoff-mcp-server 0.11.0 → 0.13.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 +42 -1
- package/Cargo.toml +3 -2
- package/README.md +163 -4
- package/package.json +1 -1
- package/scripts/cargo-env.sh +29 -0
- package/scripts/handoff-memory-hook.py +208 -0
- package/skills/handoff/SKILL.md +8 -1
- package/src/mcp/handlers/capacity.rs +21 -6
- package/src/mcp/handlers/config.rs +80 -0
- package/src/mcp/handlers/init.rs +1 -1
- package/src/mcp/handlers/memory.rs +906 -0
- package/src/mcp/handlers/metrics.rs +11 -0
- package/src/mcp/handlers/mod.rs +6 -0
- package/src/mcp/handlers/referrals.rs +20 -1
- package/src/mcp/handlers/update_task.rs +53 -4
- package/src/mcp/tools.rs +77 -0
- package/src/storage/config.rs +69 -0
- package/src/storage/memory/injected.rs +340 -0
- package/src/storage/memory/mod.rs +235 -0
- package/src/storage/memory/model.rs +96 -0
- package/src/storage/mod.rs +2 -0
- package/src/storage/referrals.rs +38 -0
- package/src/storage/tasks.rs +55 -0
package/src/storage/referrals.rs
CHANGED
|
@@ -160,6 +160,44 @@ pub fn change_referral_status(
|
|
|
160
160
|
Ok(())
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
/// Read a single referral's full data by ID, returning the data and its status.
|
|
164
|
+
/// Searches every status (open / acknowledged / resolved). Matches by exact ID
|
|
165
|
+
/// first, then falls back to a unique prefix match for convenience.
|
|
166
|
+
pub fn read_referral_by_id(
|
|
167
|
+
referrals_dir: &Path,
|
|
168
|
+
referral_id: &str,
|
|
169
|
+
) -> Result<Option<(ReferralData, String)>> {
|
|
170
|
+
if !referrals_dir.exists() {
|
|
171
|
+
return Ok(None);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let mut prefix_matches: Vec<(ReferralData, String)> = Vec::new();
|
|
175
|
+
|
|
176
|
+
for entry in std::fs::read_dir(referrals_dir)? {
|
|
177
|
+
let entry = entry?;
|
|
178
|
+
let name = entry.file_name().to_string_lossy().to_string();
|
|
179
|
+
if let Some(status) = parse_referral_status(&name) {
|
|
180
|
+
let content = std::fs::read_to_string(entry.path())?;
|
|
181
|
+
if let Ok(data) = serde_json::from_str::<ReferralData>(&content) {
|
|
182
|
+
if data.id == referral_id {
|
|
183
|
+
return Ok(Some((data, status)));
|
|
184
|
+
}
|
|
185
|
+
if data.id.starts_with(referral_id) {
|
|
186
|
+
prefix_matches.push((data, status));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
match prefix_matches.len() {
|
|
193
|
+
0 => Ok(None),
|
|
194
|
+
1 => Ok(Some(prefix_matches.into_iter().next().unwrap())),
|
|
195
|
+
n => anyhow::bail!(
|
|
196
|
+
"Ambiguous referral id prefix '{referral_id}' matches {n} referrals; use the full id"
|
|
197
|
+
),
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
163
201
|
pub fn find_referral_file(
|
|
164
202
|
referrals_dir: &Path,
|
|
165
203
|
referral_id: &str,
|
package/src/storage/tasks.rs
CHANGED
|
@@ -590,6 +590,61 @@ pub fn validate_skipped_transition(task_dir: &Path, data: &TaskData) -> Result<(
|
|
|
590
590
|
Ok(())
|
|
591
591
|
}
|
|
592
592
|
|
|
593
|
+
/// Returns true if the task directory contains at least one child task
|
|
594
|
+
/// (a non-`_`/`.`-prefixed subdirectory holding a task file).
|
|
595
|
+
pub fn task_has_children(task_dir: &Path) -> Result<bool> {
|
|
596
|
+
if !task_dir.exists() {
|
|
597
|
+
return Ok(false);
|
|
598
|
+
}
|
|
599
|
+
for entry in std::fs::read_dir(task_dir)? {
|
|
600
|
+
let entry = entry?;
|
|
601
|
+
if !entry.file_type()?.is_dir() {
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
let name = entry.file_name().to_string_lossy().to_string();
|
|
605
|
+
if name.starts_with('_') || name.starts_with('.') {
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
if find_task_file(&entry.path())?.is_some() {
|
|
609
|
+
return Ok(true);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
Ok(false)
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/// Whether a task in the given status requires an effort estimate.
|
|
616
|
+
/// Parent tasks (with children) and blocked/skipped tasks are exempt;
|
|
617
|
+
/// this only covers the status dimension.
|
|
618
|
+
pub fn status_requires_estimate(status: &str) -> bool {
|
|
619
|
+
matches!(status, "todo" | "in_progress" | "review" | "done")
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/// Validate that a leaf task carries an `estimate_hours` when the project
|
|
623
|
+
/// requires it. `has_children` lets the caller skip parent tasks.
|
|
624
|
+
/// Returns an error guiding the caller to add an estimate.
|
|
625
|
+
pub fn validate_estimate_required(
|
|
626
|
+
require_estimate_hours: bool,
|
|
627
|
+
status: &str,
|
|
628
|
+
has_children: bool,
|
|
629
|
+
schedule: Option<&Schedule>,
|
|
630
|
+
) -> Result<()> {
|
|
631
|
+
if !require_estimate_hours || has_children || !status_requires_estimate(status) {
|
|
632
|
+
return Ok(());
|
|
633
|
+
}
|
|
634
|
+
let has_estimate = schedule
|
|
635
|
+
.and_then(|s| s.estimate_hours)
|
|
636
|
+
.is_some_and(|h| h > 0.0);
|
|
637
|
+
if !has_estimate {
|
|
638
|
+
anyhow::bail!(
|
|
639
|
+
"Task requires an effort estimate: set schedule.estimate_hours (hours, > 0) \
|
|
640
|
+
when creating or updating a leaf task in status '{status}'. \
|
|
641
|
+
Estimate the human-effort hours for this task. \
|
|
642
|
+
To disable this requirement project-wide, set settings.require_estimate_hours = false."
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
Ok(())
|
|
646
|
+
}
|
|
647
|
+
|
|
593
648
|
fn check_children_terminal(task_dir: &Path, parent_id: &str) -> Result<()> {
|
|
594
649
|
if !task_dir.exists() {
|
|
595
650
|
return Ok(());
|