feat(933): add item_type + epic CRDT registers + migrate epic mechanism

Replaces the YAML-only `type: epic` / `epic: <id>` front-matter fields with
typed CRDT registers on PipelineItemCrdt. The epic-mechanism MCP tools
(`tool_list_epics`, `tool_show_epic`), the epic-context injection in agent
spawn, and the type-classifier helpers (`item_type_from_id`, `is_bug_item`,
`is_refactor_item`) now all read from the CRDT.

Schema:
- PipelineItemCrdt: `item_type: LwwRegisterCrdt<String>` and
  `epic: LwwRegisterCrdt<String>` registers.
- WorkItem: typed `item_type()` and `epic()` accessors returning `Option<&str>`.
- crdt_state::set_item_type(story_id, Option<&str>) and
  crdt_state::set_epic(story_id, Option<&str>) typed setters.

Write paths populate the new registers:
- create_story_file / create_bug_file / create_spike_file /
  create_refactor_file / create_epic_file — each calls set_item_type after
  write_story_content.
- tool_update_story intercepts `epic` and `type` fields and routes them to
  the typed setters (same pattern as qa / depends_on).

Read paths migrated off yaml_legacy:
- http/mcp/story_tools/epic.rs: tool_list_epics + tool_show_epic.
- agents/lifecycle.rs::item_type_from_id (numeric-only IDs).
- agents/pool/start/spawn.rs epic-context injection.
- http/workflow/bug_ops/bug.rs::is_bug_item, refactor.rs::is_refactor_item.
- http/workflow/pipeline.rs::load_pipeline_state — review_hold/qa/epic_id
  all come from the CRDT now; only merge_failure is still YAML (sweep in
  929 stage 10).

All `yaml_residue(...)` wraps for item_type / epic are removed; the
remaining residue marker doc no longer references 933.

cargo fmt --check, clippy --all-targets -- -D warnings, and the 2857-test
suite all pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Timmy
2026-05-12 19:58:43 +01:00
parent aadbb1b2af
commit 7d7ab85994
16 changed files with 200 additions and 109 deletions
+11 -14
View File
@@ -95,18 +95,16 @@ pub fn load_pipeline_state(ctx: &AppContext) -> Result<PipelineState, String> {
let sid = &item.story_id.0;
let agent = agent_map.get(sid).cloned();
// Enrich with content-derived metadata (merge_failure, review_hold, qa, epic_id).
let (merge_failure, review_hold, qa, epic_id) = crate::db::read_content(sid)
// Stories 929/932/933: review_hold, qa_mode, epic_id come from typed
// CRDT registers. merge_failure remains in YAML for now (tracked by
// 929 stage 10's sweep).
let view = crate::crdt_state::read_item(sid);
let review_hold = view.as_ref().map(|v| v.review_hold()).filter(|b| *b);
let qa = view.as_ref().and_then(|v| v.qa_mode().map(str::to_string));
let epic_id = view.as_ref().and_then(|v| v.epic().map(str::to_string));
let merge_failure = crate::db::read_content(sid)
.and_then(|c| parse_front_matter(&c).ok())
.map(|meta| {
(
meta.merge_failure,
meta.review_hold,
meta.qa.map(|m| m.as_str().to_string()),
meta.epic,
)
})
.unwrap_or((None, None, None, None));
.and_then(|meta| meta.merge_failure);
let story = UpcomingStory {
story_id: sid.clone(),
@@ -211,9 +209,8 @@ pub fn load_upcoming_stories(_ctx: &AppContext) -> Result<Vec<UpcomingStory>, St
.filter(|item| matches!(item.stage, Stage::Backlog))
.map(|item| {
let sid = &item.story_id.0;
let epic_id = crate::db::read_content(sid)
.and_then(|c| parse_front_matter(&c).ok())
.and_then(|meta| meta.epic);
let epic_id =
crate::crdt_state::read_item(sid).and_then(|v| v.epic().map(str::to_string));
UpcomingStory {
story_id: item.story_id.0.clone(),
name: if item.name.is_empty() {