feat(1001): recover_half_written_items MCP tool

Adds db::recover, a discovery + recovery layer for pipeline items that
got half-written before the Stage 1 fix landed (content in content
store + SQLite shadow, no live CRDT entry). For each orphan, the
content body is re-anchored to a fresh non-tombstoned id and the old
id's content row is cleared.

Exposed as the recover_half_written_items MCP tool. dry_run defaults
to true so the caller can review what would change before mutating.

YAML front-matter parsing is hand-rolled and scoped to the three
fields the create_*_file path emits (name, type, depends_on). It
tolerates missing or malformed lines by falling back to safe
defaults; the orphan is recovered with the best metadata we can pull
from the body and the rest is left to the operator to fix up.

The discovery step is read-only and idempotent. Recovery is also
idempotent in the sense that once an orphan is lifted, the next
discovery pass won't see it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Timmy
2026-05-13 19:16:05 +01:00
parent c61f715878
commit cd411ba443
6 changed files with 452 additions and 1 deletions
+2 -1
View File
@@ -96,6 +96,7 @@ mod tests {
assert!(names.contains(&"status"));
assert!(names.contains(&"loc_file"));
assert!(names.contains(&"dump_crdt"));
assert!(names.contains(&"recover_half_written_items"));
assert!(names.contains(&"get_version"));
assert!(names.contains(&"remove_criterion"));
assert!(names.contains(&"mesh_status"));
@@ -106,7 +107,7 @@ mod tests {
assert!(names.contains(&"show_epic"));
assert!(names.contains(&"freeze_story"));
assert!(names.contains(&"unfreeze_story"));
assert_eq!(tools.len(), 74);
assert_eq!(tools.len(), 75);
}
#[test]
@@ -288,6 +288,20 @@ pub(super) fn system_tools() -> Vec<Value> {
"required": []
}
}),
json!({
"name": "recover_half_written_items",
"description": "Discover (and optionally recover) half-written pipeline items — rows whose content is in the SQLite shadow and content store but whose CRDT entry is absent or tombstoned. These are invisible to every CRDT-driven read path and can't be cleaned up by delete_story / purge_story. Recovery moves each orphan's content onto a fresh non-tombstoned id and reports the old→new mapping. Defaults to dry_run=true so you can see what would change before mutating.",
"inputSchema": {
"type": "object",
"properties": {
"dry_run": {
"type": "boolean",
"description": "When true (default), only discover half-written items and report them. When false, perform the recovery."
}
},
"required": []
}
}),
json!({
"name": "wizard_status",
"description": "Return the current setup wizard state: which step is active, and which are done/skipped/pending. Use this to inspect progress before calling wizard_generate, wizard_confirm, wizard_skip, or wizard_retry.",