fix(901): unblock_story works on CRDT-only stories post-865

Bug 901: `unblock_story` (and the chat `unblock` command) routed through
`parse_front_matter` and errored with "Missing front matter" on any
post-865 story (story content is now CRDT-only with no YAML on disk).

In `chat/commands/unblock.rs::unblock_by_story_id`:
  - Drop the early `parse_front_matter` gate.
  - Read story name and blocked state from the CRDT register API instead
    of parsed YAML (`crdt_state::read_item`, `pipeline_state::read_typed`).
  - Keep the legacy fallback cleanup, but gate it on the content actually
    starting with a `---` YAML block, so CRDT-only stories don't hit a
    parse error there either.
  - Remove the now-unused `parse_front_matter` import.

Surfaced a second sub-bug: even when the state-machine transition
fired (`Blocked + Unblock → Coding`), the CRDT `blocked` register was
never explicitly cleared. Pre-865 the YAML-strip content_transform
cleared it as a side effect; post-865 there is no YAML to strip.

  - Add `crdt_state::set_blocked(story_id, bool)` parallel to
    `set_retry_count`. Wired through `crdt_state::write` and the
    crate-level re-export.
  - `agents::lifecycle::transition_to_unblocked` now calls
    `set_blocked(story_id, false)` alongside `set_retry_count(0)` so
    the legacy register stays in sync with the typed stage.

Test: `unblock_command_works_on_crdt_only_story_no_yaml` seeds a CRDT
entry with no YAML on disk, runs unblock, asserts success + cleared
blocked + retry_count=0. All 10 existing unblock tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Timmy
2026-05-12 13:13:01 +01:00
parent cd12cb5e2c
commit 98d496b1ad
5 changed files with 159 additions and 67 deletions
+2 -1
View File
@@ -53,7 +53,8 @@ pub use types::{
};
pub use write::{
bump_retry_count, migrate_names_from_slugs, migrate_story_ids_to_numeric, name_from_story_id,
set_agent, set_depends_on, set_mergemaster_attempted, set_qa_mode, set_retry_count, write_item,
set_agent, set_blocked, set_depends_on, set_mergemaster_attempted, set_qa_mode,
set_retry_count, write_item,
};
#[cfg(test)]
+19
View File
@@ -285,6 +285,25 @@ pub fn set_retry_count(story_id: &str, count: i64) {
}
}
/// Set the `blocked` register on a story to the given value.
///
/// Pure metadata operation — the item's stage is not changed.
/// Use this alongside a state-machine transition out of `Blocked` /
/// `MergeFailure` to keep the legacy `blocked` register in sync with the
/// typed stage post-865 (where YAML side-effects no longer clear the
/// register on their own).
pub fn set_blocked(story_id: &str, blocked: bool) {
let Some(state_mutex) = get_crdt() else {
return;
};
let Ok(mut state) = state_mutex.lock() else {
return;
};
if let Some(&idx) = state.index.get(story_id) {
apply_and_persist(&mut state, |s| s.crdt.doc.items[idx].blocked.set(blocked));
}
}
/// Increment `retry_count` by 1 and return the new value.
///
/// Pure metadata operation — the item's stage is not changed.
+2 -2
View File
@@ -10,7 +10,7 @@ mod migrations;
mod tests;
pub use item::{
bump_retry_count, set_agent, set_depends_on, set_mergemaster_attempted, set_qa_mode,
set_retry_count, write_item,
bump_retry_count, set_agent, set_blocked, set_depends_on, set_mergemaster_attempted,
set_qa_mode, set_retry_count, write_item,
};
pub use migrations::{migrate_names_from_slugs, migrate_story_ids_to_numeric, name_from_story_id};