huskies: merge 1010
This commit is contained in:
@@ -1,15 +1,16 @@
|
||||
//! Filesystem watcher for `.huskies/project.toml` and `.huskies/agents.toml`.
|
||||
//! Filesystem watcher for `.huskies/project.toml`, `.huskies/agents.toml`,
|
||||
//! and `.huskies/worktrees/*/PLAN.md`.
|
||||
//!
|
||||
//! Watches config files for changes and broadcasts a [`WatcherEvent`] to all
|
||||
//! connected WebSocket clients so the frontend can reload the agent roster
|
||||
//! without a server restart.
|
||||
//!
|
||||
//! Work-item pipeline events (stage transitions) are driven by CRDT state
|
||||
//! changes via [`crate::crdt_state::subscribe`], not by filesystem events.
|
||||
//! without a server restart. Also watches worktree PLAN.md files and updates
|
||||
//! the typed [`crate::pipeline_state::PlanState`] in the CRDT whenever a
|
||||
//! PLAN.md is created, modified, or removed.
|
||||
//!
|
||||
//! # Debouncing
|
||||
//! Config-file events are buffered for 300 ms after the last activity to avoid
|
||||
//! duplicate broadcasts when an editor writes multiple events in quick succession.
|
||||
//! PLAN.md events are applied immediately without debouncing.
|
||||
//!
|
||||
//! # Submodules
|
||||
//! - [`events`] — [`WatcherEvent`] enum definition.
|
||||
@@ -28,6 +29,37 @@ use std::sync::mpsc;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
/// Extract the story ID from a path of the form
|
||||
/// `{git_root}/.huskies/worktrees/{story_id}/PLAN.md`.
|
||||
///
|
||||
/// Returns `Some(story_id)` when `path` is exactly a `PLAN.md` file one level
|
||||
/// inside the worktrees directory. Returns `None` for any other path.
|
||||
pub fn extract_story_id_from_plan_path(path: &Path, git_root: &Path) -> Option<String> {
|
||||
if path.file_name()? != "PLAN.md" {
|
||||
return None;
|
||||
}
|
||||
let parent = path.parent()?;
|
||||
let expected_worktrees = git_root.join(".huskies").join("worktrees");
|
||||
if parent.parent()? != expected_worktrees {
|
||||
return None;
|
||||
}
|
||||
Some(parent.file_name()?.to_str()?.to_string())
|
||||
}
|
||||
|
||||
/// Determine the [`PlanState`] for a PLAN.md file at `path`.
|
||||
///
|
||||
/// - File absent or unreadable → [`PlanState::Missing`]
|
||||
/// - File contains `<TBD>` → [`PlanState::Drafted`]
|
||||
/// - File exists with no `<TBD>` → [`PlanState::Confirmed`]
|
||||
pub fn plan_state_for_path(path: &Path) -> crate::pipeline_state::PlanState {
|
||||
use crate::pipeline_state::PlanState;
|
||||
match std::fs::read_to_string(path) {
|
||||
Ok(content) if content.contains("<TBD>") => PlanState::Drafted,
|
||||
Ok(_) => PlanState::Confirmed,
|
||||
Err(_) => PlanState::Missing,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return `true` if `path` is the root-level `.huskies/project.toml` or
|
||||
/// `.huskies/agents.toml`, i.e. `{git_root}/.huskies/{project,agents}.toml`.
|
||||
///
|
||||
@@ -100,8 +132,7 @@ pub fn start_watcher(git_root: PathBuf, event_tx: broadcast::Sender<WatcherEvent
|
||||
}
|
||||
};
|
||||
|
||||
// Watch config files for hot-reload. Work-item directories are NOT
|
||||
// watched — CRDT state transitions drive pipeline events now.
|
||||
// Watch config files for hot-reload.
|
||||
let huskies = git_root.join(".huskies");
|
||||
for config_file in [huskies.join("project.toml"), huskies.join("agents.toml")] {
|
||||
if config_file.exists()
|
||||
@@ -114,7 +145,15 @@ pub fn start_watcher(git_root: PathBuf, event_tx: broadcast::Sender<WatcherEvent
|
||||
}
|
||||
}
|
||||
|
||||
slog!("[watcher] watching config files for hot-reload");
|
||||
// Watch worktrees directory for PLAN.md changes.
|
||||
let worktrees_dir = huskies.join("worktrees");
|
||||
if worktrees_dir.exists()
|
||||
&& let Err(e) = watcher.watch(&worktrees_dir, RecursiveMode::Recursive)
|
||||
{
|
||||
slog!("[watcher] failed to watch worktrees dir: {e}");
|
||||
}
|
||||
|
||||
slog!("[watcher] watching config files and worktree PLAN.md files");
|
||||
|
||||
const DEBOUNCE: Duration = Duration::from_millis(300);
|
||||
|
||||
@@ -141,9 +180,17 @@ pub fn start_watcher(git_root: PathBuf, event_tx: broadcast::Sender<WatcherEvent
|
||||
slog!("[watcher] config change detected: {}", path.display());
|
||||
config_changed_pending = true;
|
||||
deadline = Some(Instant::now() + DEBOUNCE);
|
||||
} else if let Some(story_id) =
|
||||
extract_story_id_from_plan_path(&path, &git_root)
|
||||
{
|
||||
let plan_state = plan_state_for_path(&path);
|
||||
slog!(
|
||||
"[watcher] PLAN.md changed for '{}': {:?}",
|
||||
story_id,
|
||||
plan_state
|
||||
);
|
||||
crate::crdt_state::set_plan_state(&story_id, plan_state);
|
||||
}
|
||||
// Work-item file changes are intentionally ignored.
|
||||
// CRDT state transitions handle pipeline events.
|
||||
}
|
||||
}
|
||||
false
|
||||
|
||||
Reference in New Issue
Block a user