huskies: merge 492_story_remove_filesystem_pipeline_state_and_store_story_content_in_database

This commit is contained in:
dave
2026-04-08 03:03:59 +00:00
parent f43d30bdae
commit 8fd49d563e
27 changed files with 1663 additions and 1295 deletions
@@ -30,11 +30,14 @@ impl AgentPool {
let items = scan_stage_items(project_root, "1_backlog");
for story_id in &items {
// Only promote stories that explicitly declare dependencies.
let story_path = project_root
.join(".huskies/work/1_backlog")
.join(format!("{story_id}.md"));
let has_deps = std::fs::read_to_string(&story_path)
.ok()
// Try content store first, fall back to filesystem.
let contents = crate::db::read_content(story_id).or_else(|| {
let story_path = project_root
.join(".huskies/work/1_backlog")
.join(format!("{story_id}.md"));
std::fs::read_to_string(&story_path).ok()
});
let has_deps = contents
.and_then(|c| parse_front_matter(&c).ok())
.and_then(|m| m.depends_on)
.map(|d| !d.is_empty())
@@ -121,17 +124,29 @@ impl AgentPool {
"[auto-assign] Story '{story_id}' in 4_merge/ has no commits \
on feature branch. Writing merge_failure and blocking."
);
let story_path = project_root
.join(".huskies/work")
.join(stage_dir)
.join(format!("{story_id}.md"));
let empty_diff_reason = "Feature branch has no code changes — the coder agent \
did not produce any commits.";
let _ = crate::io::story_metadata::write_merge_failure(
&story_path,
empty_diff_reason,
);
let _ = crate::io::story_metadata::write_blocked(&story_path);
// Write merge_failure and blocked to content store.
if let Some(contents) = crate::db::read_content(story_id) {
let updated = crate::io::story_metadata::write_merge_failure_in_content(
&contents,
empty_diff_reason,
);
let blocked = crate::io::story_metadata::write_blocked_in_content(&updated);
crate::db::write_content(story_id, &blocked);
crate::db::write_item_with_content(story_id, stage_dir, &blocked);
} else {
// Fallback: filesystem.
let story_path = project_root
.join(".huskies/work")
.join(stage_dir)
.join(format!("{story_id}.md"));
let _ = crate::io::story_metadata::write_merge_failure(
&story_path,
empty_diff_reason,
);
let _ = crate::io::story_metadata::write_blocked(&story_path);
}
let _ = self.watcher_tx.send(crate::io::watcher::WatcherEvent::StoryBlocked {
story_id: story_id.to_string(),
reason: empty_diff_reason.to_string(),
+17 -8
View File
@@ -19,23 +19,32 @@ pub(in crate::agents::pool) fn is_agent_free(
}
pub(super) fn scan_stage_items(project_root: &Path, stage_dir: &str) -> Vec<String> {
let dir = project_root.join(".huskies").join("work").join(stage_dir);
if !dir.is_dir() {
return Vec::new();
use std::collections::BTreeSet;
let mut items = BTreeSet::new();
// Include CRDT items — the primary source of truth for pipeline state.
if let Some(all) = crate::crdt_state::read_all_items() {
for item in &all {
if item.stage == stage_dir {
items.insert(item.story_id.clone());
}
}
}
let mut items = Vec::new();
if let Ok(entries) = std::fs::read_dir(&dir) {
// Also include filesystem items (backwards compat / migration fallback).
let dir = project_root.join(".huskies").join("work").join(stage_dir);
if dir.is_dir() && let Ok(entries) = std::fs::read_dir(&dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) == Some("md")
&& let Some(stem) = path.file_stem().and_then(|s| s.to_str())
{
items.push(stem.to_string());
items.insert(stem.to_string());
}
}
}
items.sort();
items
items.into_iter().collect()
}
/// Return `true` if `story_id` has any active (pending/running) agent matching `stage`.
@@ -2,36 +2,45 @@
use std::path::Path;
/// Read story contents from DB content store first, fall back to filesystem.
fn read_story_contents(project_root: &Path, story_id: &str) -> Option<String> {
// Primary: in-memory content store (backed by SQLite).
if let Some(c) = crate::db::read_content(story_id) {
return Some(c);
}
// Fallback: scan filesystem stages.
for stage in &["1_backlog", "2_current", "3_qa", "4_merge", "5_done", "6_archived"] {
let path = project_root
.join(".huskies/work")
.join(stage)
.join(format!("{story_id}.md"));
if let Ok(c) = std::fs::read_to_string(&path) {
return Some(c);
}
}
None
}
/// Read the optional `agent:` field from the front matter of a story file.
///
/// Returns `Some(agent_name)` if the front matter specifies an agent, or `None`
/// if the field is absent or the file cannot be read / parsed.
pub(super) fn read_story_front_matter_agent(
project_root: &Path,
stage_dir: &str,
_stage_dir: &str,
story_id: &str,
) -> Option<String> {
use crate::io::story_metadata::parse_front_matter;
let path = project_root
.join(".huskies")
.join("work")
.join(stage_dir)
.join(format!("{story_id}.md"));
let contents = std::fs::read_to_string(path).ok()?;
let contents = read_story_contents(project_root, story_id)?;
parse_front_matter(&contents).ok()?.agent
}
/// Return `true` if the story file in the given stage has `review_hold: true` in its front matter.
pub(super) fn has_review_hold(project_root: &Path, stage_dir: &str, story_id: &str) -> bool {
pub(super) fn has_review_hold(project_root: &Path, _stage_dir: &str, story_id: &str) -> bool {
use crate::io::story_metadata::parse_front_matter;
let path = project_root
.join(".huskies")
.join("work")
.join(stage_dir)
.join(format!("{story_id}.md"));
let contents = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(_) => return false,
let contents = match read_story_contents(project_root, story_id) {
Some(c) => c,
None => return false,
};
parse_front_matter(&contents)
.ok()
@@ -40,16 +49,11 @@ pub(super) fn has_review_hold(project_root: &Path, stage_dir: &str, story_id: &s
}
/// Return `true` if the story file has `blocked: true` in its front matter.
pub(super) fn is_story_blocked(project_root: &Path, stage_dir: &str, story_id: &str) -> bool {
pub(super) fn is_story_blocked(project_root: &Path, _stage_dir: &str, story_id: &str) -> bool {
use crate::io::story_metadata::parse_front_matter;
let path = project_root
.join(".huskies")
.join("work")
.join(stage_dir)
.join(format!("{story_id}.md"));
let contents = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(_) => return false,
let contents = match read_story_contents(project_root, story_id) {
Some(c) => c,
None => return false,
};
parse_front_matter(&contents)
.ok()
@@ -81,16 +85,11 @@ pub(super) fn has_unmet_dependencies(
}
/// Return `true` if the story file has a `merge_failure` field in its front matter.
pub(super) fn has_merge_failure(project_root: &Path, stage_dir: &str, story_id: &str) -> bool {
pub(super) fn has_merge_failure(project_root: &Path, _stage_dir: &str, story_id: &str) -> bool {
use crate::io::story_metadata::parse_front_matter;
let path = project_root
.join(".huskies")
.join("work")
.join(stage_dir)
.join(format!("{story_id}.md"));
let contents = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(_) => return false,
let contents = match read_story_contents(project_root, story_id) {
Some(c) => c,
None => return false,
};
parse_front_matter(&contents)
.ok()