huskies: rename project from storkit to huskies
Rename all references from storkit to huskies across the codebase: - .storkit/ directory → .huskies/ - Binary name, Cargo package name, Docker image references - Server code, frontend code, config files, scripts - Fix script/test to build frontend before cargo clippy/test so merge worktrees have frontend/dist available for RustEmbed Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "storkit"
|
||||
name = "huskies"
|
||||
version = "0.8.8"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
|
||||
@@ -17,7 +17,7 @@ pub struct LogEntry {
|
||||
/// Writes agent events to a persistent log file (JSONL format).
|
||||
///
|
||||
/// Each agent session gets its own log file at:
|
||||
/// `.storkit/logs/{story_id}/{agent_name}-{session_id}.log`
|
||||
/// `.huskies/logs/{story_id}/{agent_name}-{session_id}.log`
|
||||
pub struct AgentLogWriter {
|
||||
file: File,
|
||||
}
|
||||
@@ -72,7 +72,7 @@ impl AgentLogWriter {
|
||||
/// Return the log directory for a story.
|
||||
fn log_dir(project_root: &Path, story_id: &str) -> PathBuf {
|
||||
project_root
|
||||
.join(".storkit")
|
||||
.join(".huskies")
|
||||
.join("logs")
|
||||
.join(story_id)
|
||||
}
|
||||
@@ -110,7 +110,7 @@ pub fn read_log(path: &Path) -> Result<Vec<LogEntry>, String> {
|
||||
|
||||
/// Find the most recent log file for a given story+agent combination.
|
||||
///
|
||||
/// Scans `.storkit/logs/{story_id}/` for files matching `{agent_name}-*.log`
|
||||
/// Scans `.huskies/logs/{story_id}/` for files matching `{agent_name}-*.log`
|
||||
/// and returns the one with the most recent modification time.
|
||||
pub fn find_latest_log(
|
||||
project_root: &Path,
|
||||
@@ -162,7 +162,7 @@ mod tests {
|
||||
AgentLogWriter::new(root, "42_story_foo", "coder-1", "sess-abc123").unwrap();
|
||||
|
||||
let expected_path = root
|
||||
.join(".storkit")
|
||||
.join(".huskies")
|
||||
.join("logs")
|
||||
.join("42_story_foo")
|
||||
.join("coder-1-sess-abc123.log");
|
||||
|
||||
@@ -18,12 +18,12 @@ pub(super) fn item_type_from_id(item_id: &str) -> &'static str {
|
||||
|
||||
/// Return the source directory path for a work item (always work/1_backlog/).
|
||||
fn item_source_dir(project_root: &Path, _item_id: &str) -> PathBuf {
|
||||
project_root.join(".storkit").join("work").join("1_backlog")
|
||||
project_root.join(".huskies").join("work").join("1_backlog")
|
||||
}
|
||||
|
||||
/// Return the done directory path for a work item (always work/5_done/).
|
||||
fn item_archive_dir(project_root: &Path, _item_id: &str) -> PathBuf {
|
||||
project_root.join(".storkit").join("work").join("5_done")
|
||||
project_root.join(".huskies").join("work").join("5_done")
|
||||
}
|
||||
|
||||
/// Move a work item (story, bug, or spike) from `work/1_backlog/` to `work/2_current/`.
|
||||
@@ -31,7 +31,7 @@ fn item_archive_dir(project_root: &Path, _item_id: &str) -> PathBuf {
|
||||
/// Idempotent: if the item is already in `2_current/`, returns Ok without committing.
|
||||
/// If the item is not found in `1_backlog/`, logs a warning and returns Ok.
|
||||
pub fn move_story_to_current(project_root: &Path, story_id: &str) -> Result<(), String> {
|
||||
let sk = project_root.join(".storkit").join("work");
|
||||
let sk = project_root.join(".huskies").join("work");
|
||||
let current_dir = sk.join("2_current");
|
||||
let current_path = current_dir.join(format!("{story_id}.md"));
|
||||
|
||||
@@ -103,7 +103,7 @@ pub fn feature_branch_has_unmerged_changes(project_root: &Path, story_id: &str)
|
||||
/// * If the story is already in `5_done/` or `6_archived/`, this is a no-op (idempotent).
|
||||
/// * If the story is not found in `2_current/`, `4_merge/`, `5_done/`, or `6_archived/`, an error is returned.
|
||||
pub fn move_story_to_done(project_root: &Path, story_id: &str) -> Result<(), String> {
|
||||
let sk = project_root.join(".storkit").join("work");
|
||||
let sk = project_root.join(".huskies").join("work");
|
||||
let current_path = sk.join("2_current").join(format!("{story_id}.md"));
|
||||
let merge_path = sk.join("4_merge").join(format!("{story_id}.md"));
|
||||
let done_dir = sk.join("5_done");
|
||||
@@ -153,7 +153,7 @@ pub fn move_story_to_done(project_root: &Path, story_id: &str) -> Result<(), Str
|
||||
/// This stages a work item as ready for the mergemaster to pick up and merge into master.
|
||||
/// Idempotent: if already in `4_merge/`, returns Ok without committing.
|
||||
pub fn move_story_to_merge(project_root: &Path, story_id: &str) -> Result<(), String> {
|
||||
let sk = project_root.join(".storkit").join("work");
|
||||
let sk = project_root.join(".huskies").join("work");
|
||||
let current_path = sk.join("2_current").join(format!("{story_id}.md"));
|
||||
let qa_path = sk.join("3_qa").join(format!("{story_id}.md"));
|
||||
let merge_dir = sk.join("4_merge");
|
||||
@@ -203,7 +203,7 @@ pub fn move_story_to_merge(project_root: &Path, story_id: &str) -> Result<(), St
|
||||
/// This stages a work item for QA review before merging to master.
|
||||
/// Idempotent: if already in `3_qa/`, returns Ok without committing.
|
||||
pub fn move_story_to_qa(project_root: &Path, story_id: &str) -> Result<(), String> {
|
||||
let sk = project_root.join(".storkit").join("work");
|
||||
let sk = project_root.join(".huskies").join("work");
|
||||
let current_path = sk.join("2_current").join(format!("{story_id}.md"));
|
||||
let qa_dir = sk.join("3_qa");
|
||||
let qa_path = qa_dir.join(format!("{story_id}.md"));
|
||||
@@ -246,7 +246,7 @@ pub fn reject_story_from_qa(
|
||||
story_id: &str,
|
||||
notes: &str,
|
||||
) -> Result<(), String> {
|
||||
let sk = project_root.join(".storkit").join("work");
|
||||
let sk = project_root.join(".huskies").join("work");
|
||||
let qa_path = sk.join("3_qa").join(format!("{story_id}.md"));
|
||||
let current_dir = sk.join("2_current");
|
||||
let current_path = current_dir.join(format!("{story_id}.md"));
|
||||
@@ -311,7 +311,7 @@ pub fn move_story_to_stage(
|
||||
)
|
||||
})?;
|
||||
|
||||
let sk = project_root.join(".storkit").join("work");
|
||||
let sk = project_root.join(".huskies").join("work");
|
||||
let target_dir = sk.join(target_dir_name);
|
||||
let target_path = target_dir.join(format!("{story_id}.md"));
|
||||
|
||||
@@ -362,7 +362,7 @@ pub fn move_story_to_stage(
|
||||
/// * If the bug is already in `5_done/`, this is a no-op (idempotent).
|
||||
/// * If the bug is not found anywhere, an error is returned.
|
||||
pub fn close_bug_to_archive(project_root: &Path, bug_id: &str) -> Result<(), String> {
|
||||
let sk = project_root.join(".storkit").join("work");
|
||||
let sk = project_root.join(".huskies").join("work");
|
||||
let current_path = sk.join("2_current").join(format!("{bug_id}.md"));
|
||||
let backlog_path = sk.join("1_backlog").join(format!("{bug_id}.md"));
|
||||
let archive_dir = item_archive_dir(project_root, bug_id);
|
||||
@@ -405,8 +405,8 @@ mod tests {
|
||||
use std::fs;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let backlog = root.join(".storkit/work/1_backlog");
|
||||
let current = root.join(".storkit/work/2_current");
|
||||
let backlog = root.join(".huskies/work/1_backlog");
|
||||
let current = root.join(".huskies/work/2_current");
|
||||
fs::create_dir_all(&backlog).unwrap();
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(backlog.join("10_story_foo.md"), "test").unwrap();
|
||||
@@ -422,7 +422,7 @@ mod tests {
|
||||
use std::fs;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let current = root.join(".storkit/work/2_current");
|
||||
let current = root.join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(current.join("11_story_foo.md"), "test").unwrap();
|
||||
|
||||
@@ -441,8 +441,8 @@ mod tests {
|
||||
use std::fs;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let backlog = root.join(".storkit/work/1_backlog");
|
||||
let current = root.join(".storkit/work/2_current");
|
||||
let backlog = root.join(".huskies/work/1_backlog");
|
||||
let current = root.join(".huskies/work/2_current");
|
||||
fs::create_dir_all(&backlog).unwrap();
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(backlog.join("1_bug_test.md"), "# Bug 1\n").unwrap();
|
||||
@@ -460,14 +460,14 @@ mod tests {
|
||||
use std::fs;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let current = root.join(".storkit/work/2_current");
|
||||
let current = root.join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(current.join("2_bug_test.md"), "# Bug 2\n").unwrap();
|
||||
|
||||
close_bug_to_archive(root, "2_bug_test").unwrap();
|
||||
|
||||
assert!(!current.join("2_bug_test.md").exists());
|
||||
assert!(root.join(".storkit/work/5_done/2_bug_test.md").exists());
|
||||
assert!(root.join(".huskies/work/5_done/2_bug_test.md").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -475,14 +475,14 @@ mod tests {
|
||||
use std::fs;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let backlog = root.join(".storkit/work/1_backlog");
|
||||
let backlog = root.join(".huskies/work/1_backlog");
|
||||
fs::create_dir_all(&backlog).unwrap();
|
||||
fs::write(backlog.join("3_bug_test.md"), "# Bug 3\n").unwrap();
|
||||
|
||||
close_bug_to_archive(root, "3_bug_test").unwrap();
|
||||
|
||||
assert!(!backlog.join("3_bug_test.md").exists());
|
||||
assert!(root.join(".storkit/work/5_done/3_bug_test.md").exists());
|
||||
assert!(root.join(".huskies/work/5_done/3_bug_test.md").exists());
|
||||
}
|
||||
|
||||
// ── item_type_from_id tests ────────────────────────────────────────────────
|
||||
@@ -502,14 +502,14 @@ mod tests {
|
||||
use std::fs;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let current = root.join(".storkit/work/2_current");
|
||||
let current = root.join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(current.join("20_story_foo.md"), "test").unwrap();
|
||||
|
||||
move_story_to_merge(root, "20_story_foo").unwrap();
|
||||
|
||||
assert!(!current.join("20_story_foo.md").exists());
|
||||
assert!(root.join(".storkit/work/4_merge/20_story_foo.md").exists());
|
||||
assert!(root.join(".huskies/work/4_merge/20_story_foo.md").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -517,14 +517,14 @@ mod tests {
|
||||
use std::fs;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let qa_dir = root.join(".storkit/work/3_qa");
|
||||
let qa_dir = root.join(".huskies/work/3_qa");
|
||||
fs::create_dir_all(&qa_dir).unwrap();
|
||||
fs::write(qa_dir.join("40_story_test.md"), "test").unwrap();
|
||||
|
||||
move_story_to_merge(root, "40_story_test").unwrap();
|
||||
|
||||
assert!(!qa_dir.join("40_story_test.md").exists());
|
||||
assert!(root.join(".storkit/work/4_merge/40_story_test.md").exists());
|
||||
assert!(root.join(".huskies/work/4_merge/40_story_test.md").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -532,7 +532,7 @@ mod tests {
|
||||
use std::fs;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let merge_dir = root.join(".storkit/work/4_merge");
|
||||
let merge_dir = root.join(".huskies/work/4_merge");
|
||||
fs::create_dir_all(&merge_dir).unwrap();
|
||||
fs::write(merge_dir.join("21_story_test.md"), "test").unwrap();
|
||||
|
||||
@@ -554,14 +554,14 @@ mod tests {
|
||||
use std::fs;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let current = root.join(".storkit/work/2_current");
|
||||
let current = root.join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(current.join("30_story_qa.md"), "test").unwrap();
|
||||
|
||||
move_story_to_qa(root, "30_story_qa").unwrap();
|
||||
|
||||
assert!(!current.join("30_story_qa.md").exists());
|
||||
assert!(root.join(".storkit/work/3_qa/30_story_qa.md").exists());
|
||||
assert!(root.join(".huskies/work/3_qa/30_story_qa.md").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -569,7 +569,7 @@ mod tests {
|
||||
use std::fs;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let qa_dir = root.join(".storkit/work/3_qa");
|
||||
let qa_dir = root.join(".huskies/work/3_qa");
|
||||
fs::create_dir_all(&qa_dir).unwrap();
|
||||
fs::write(qa_dir.join("31_story_test.md"), "test").unwrap();
|
||||
|
||||
@@ -591,14 +591,14 @@ mod tests {
|
||||
use std::fs;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let merge_dir = root.join(".storkit/work/4_merge");
|
||||
let merge_dir = root.join(".huskies/work/4_merge");
|
||||
fs::create_dir_all(&merge_dir).unwrap();
|
||||
fs::write(merge_dir.join("22_story_test.md"), "test").unwrap();
|
||||
|
||||
move_story_to_done(root, "22_story_test").unwrap();
|
||||
|
||||
assert!(!merge_dir.join("22_story_test.md").exists());
|
||||
assert!(root.join(".storkit/work/5_done/22_story_test.md").exists());
|
||||
assert!(root.join(".huskies/work/5_done/22_story_test.md").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -696,8 +696,8 @@ mod tests {
|
||||
use std::fs;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let qa_dir = root.join(".storkit/work/3_qa");
|
||||
let current_dir = root.join(".storkit/work/2_current");
|
||||
let qa_dir = root.join(".huskies/work/3_qa");
|
||||
let current_dir = root.join(".huskies/work/2_current");
|
||||
fs::create_dir_all(&qa_dir).unwrap();
|
||||
fs::create_dir_all(¤t_dir).unwrap();
|
||||
fs::write(
|
||||
@@ -728,7 +728,7 @@ mod tests {
|
||||
use std::fs;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let current_dir = root.join(".storkit/work/2_current");
|
||||
let current_dir = root.join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t_dir).unwrap();
|
||||
fs::write(current_dir.join("51_story_test.md"), "---\nname: Test\n---\n# Story\n").unwrap();
|
||||
|
||||
@@ -743,8 +743,8 @@ mod tests {
|
||||
use std::fs;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let backlog = root.join(".storkit/work/1_backlog");
|
||||
let current = root.join(".storkit/work/2_current");
|
||||
let backlog = root.join(".huskies/work/1_backlog");
|
||||
let current = root.join(".huskies/work/2_current");
|
||||
fs::create_dir_all(&backlog).unwrap();
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(backlog.join("60_story_move.md"), "test").unwrap();
|
||||
@@ -762,8 +762,8 @@ mod tests {
|
||||
use std::fs;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let current = root.join(".storkit/work/2_current");
|
||||
let backlog = root.join(".storkit/work/1_backlog");
|
||||
let current = root.join(".huskies/work/2_current");
|
||||
let backlog = root.join(".huskies/work/1_backlog");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::create_dir_all(&backlog).unwrap();
|
||||
fs::write(current.join("61_story_back.md"), "test").unwrap();
|
||||
@@ -781,7 +781,7 @@ mod tests {
|
||||
use std::fs;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let current = root.join(".storkit/work/2_current");
|
||||
let current = root.join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(current.join("62_story_idem.md"), "test").unwrap();
|
||||
|
||||
@@ -813,8 +813,8 @@ mod tests {
|
||||
use std::fs;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let qa_dir = root.join(".storkit/work/3_qa");
|
||||
let backlog = root.join(".storkit/work/1_backlog");
|
||||
let qa_dir = root.join(".huskies/work/3_qa");
|
||||
let backlog = root.join(".huskies/work/1_backlog");
|
||||
fs::create_dir_all(&qa_dir).unwrap();
|
||||
fs::create_dir_all(&backlog).unwrap();
|
||||
fs::write(qa_dir.join("63_story_qa.md"), "test").unwrap();
|
||||
|
||||
+28
-28
@@ -10,7 +10,7 @@ use super::gates::run_project_tests;
|
||||
|
||||
/// Global lock ensuring only one squash-merge runs at a time.
|
||||
///
|
||||
/// The merge pipeline uses a shared `.storkit/merge_workspace` directory and
|
||||
/// The merge pipeline uses a shared `.huskies/merge_workspace` directory and
|
||||
/// temporary `merge-queue/{story_id}` branches. If two merges run concurrently,
|
||||
/// the second call's initial cleanup destroys the first call's branch mid-flight,
|
||||
/// causing `git cherry-pick merge-queue/…` to fail with "bad revision".
|
||||
@@ -88,7 +88,7 @@ pub(crate) fn run_squash_merge(
|
||||
|
||||
let mut all_output = String::new();
|
||||
let merge_branch = format!("merge-queue/{story_id}");
|
||||
let merge_wt_path = project_root.join(".storkit").join("merge_workspace");
|
||||
let merge_wt_path = project_root.join(".huskies").join("merge_workspace");
|
||||
|
||||
// Ensure we start clean: remove any leftover merge workspace.
|
||||
cleanup_merge_workspace(project_root, &merge_wt_path, &merge_branch);
|
||||
@@ -189,7 +189,7 @@ pub(crate) fn run_squash_merge(
|
||||
|
||||
// ── Commit in the temporary worktree ──────────────────────────
|
||||
all_output.push_str("=== git commit ===\n");
|
||||
let commit_msg = format!("storkit: merge {story_id}");
|
||||
let commit_msg = format!("huskies: merge {story_id}");
|
||||
let commit = Command::new("git")
|
||||
.args(["commit", "-m", &commit_msg])
|
||||
.current_dir(&merge_wt_path)
|
||||
@@ -238,7 +238,7 @@ pub(crate) fn run_squash_merge(
|
||||
}
|
||||
|
||||
// ── Bug 226: Verify the commit contains real code changes ─────
|
||||
// If the merge only brought in .storkit/ files (pipeline file moves),
|
||||
// If the merge only brought in .huskies/ files (pipeline file moves),
|
||||
// there are no actual code changes to land on master. Abort.
|
||||
{
|
||||
let diff_check = Command::new("git")
|
||||
@@ -247,10 +247,10 @@ pub(crate) fn run_squash_merge(
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to check merge diff: {e}"))?;
|
||||
let changed_files = String::from_utf8_lossy(&diff_check.stdout);
|
||||
let has_code_changes = changed_files.lines().any(|f| !f.starts_with(".storkit/work/"));
|
||||
let has_code_changes = changed_files.lines().any(|f| !f.starts_with(".huskies/work/"));
|
||||
if !has_code_changes {
|
||||
all_output.push_str(
|
||||
"=== Merge commit contains only .storkit/ file moves, no code changes ===\n",
|
||||
"=== Merge commit contains only .huskies/ file moves, no code changes ===\n",
|
||||
);
|
||||
cleanup_merge_workspace(project_root, &merge_wt_path, &merge_branch);
|
||||
return Ok(SquashMergeResult {
|
||||
@@ -258,7 +258,7 @@ pub(crate) fn run_squash_merge(
|
||||
had_conflicts,
|
||||
conflicts_resolved,
|
||||
conflict_details: Some(
|
||||
"Feature branch has no code changes outside .storkit/ — only \
|
||||
"Feature branch has no code changes outside .huskies/ — only \
|
||||
pipeline file moves were found."
|
||||
.to_string(),
|
||||
),
|
||||
@@ -419,10 +419,10 @@ pub(crate) fn run_squash_merge(
|
||||
}
|
||||
|
||||
// Verify HEAD commit has actual code changes (not an empty cherry-pick).
|
||||
// Exclude .storkit/work/ (pipeline file moves) but keep .storkit/project.toml
|
||||
// Exclude .huskies/work/ (pipeline file moves) but keep .huskies/project.toml
|
||||
// and other config files which are legitimate deliverables.
|
||||
let diff_stat = Command::new("git")
|
||||
.args(["diff", "--stat", "HEAD~1..HEAD", "--", ".", ":(exclude).storkit/work"])
|
||||
.args(["diff", "--stat", "HEAD~1..HEAD", "--", ".", ":(exclude).huskies/work"])
|
||||
.current_dir(project_root)
|
||||
.output()
|
||||
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
|
||||
@@ -1047,7 +1047,7 @@ after\n";
|
||||
|
||||
// Verify no leftover merge workspace directory.
|
||||
assert!(
|
||||
!repo.join(".storkit/merge_workspace").exists(),
|
||||
!repo.join(".huskies/merge_workspace").exists(),
|
||||
"merge workspace should be cleaned up"
|
||||
);
|
||||
}
|
||||
@@ -1167,7 +1167,7 @@ after\n";
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
let sk_dir = repo.join(".storkit/work/4_merge");
|
||||
let sk_dir = repo.join(".huskies/work/4_merge");
|
||||
fs::create_dir_all(&sk_dir).unwrap();
|
||||
fs::write(sk_dir.join("diverge_test.md"), "---\nname: test\n---\n").unwrap();
|
||||
Command::new("git")
|
||||
@@ -1176,7 +1176,7 @@ after\n";
|
||||
.output()
|
||||
.unwrap();
|
||||
Command::new("git")
|
||||
.args(["commit", "-m", "storkit: queue diverge_test for merge"])
|
||||
.args(["commit", "-m", "huskies: queue diverge_test for merge"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
@@ -1218,7 +1218,7 @@ after\n";
|
||||
"merge-queue branch should be cleaned up, got: {branch_list}"
|
||||
);
|
||||
assert!(
|
||||
!repo.join(".storkit/merge_workspace").exists(),
|
||||
!repo.join(".huskies/merge_workspace").exists(),
|
||||
"merge workspace should be cleaned up"
|
||||
);
|
||||
}
|
||||
@@ -1270,13 +1270,13 @@ after\n";
|
||||
|
||||
// Cleanup should still happen.
|
||||
assert!(
|
||||
!repo.join(".storkit/merge_workspace").exists(),
|
||||
!repo.join(".huskies/merge_workspace").exists(),
|
||||
"merge workspace should be cleaned up"
|
||||
);
|
||||
}
|
||||
|
||||
/// Bug 226: Verifies that `run_squash_merge` fails when the feature branch
|
||||
/// only contains .storkit/ file moves with no real code changes.
|
||||
/// only contains .huskies/ file moves with no real code changes.
|
||||
#[tokio::test]
|
||||
async fn squash_merge_md_only_changes_fails() {
|
||||
use std::fs;
|
||||
@@ -1286,13 +1286,13 @@ after\n";
|
||||
let repo = tmp.path();
|
||||
init_git_repo(repo);
|
||||
|
||||
// Create a feature branch that only moves a .storkit/ file.
|
||||
// Create a feature branch that only moves a .huskies/ file.
|
||||
Command::new("git")
|
||||
.args(["checkout", "-b", "feature/story-md_only_test"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
let sk_dir = repo.join(".storkit/work/2_current");
|
||||
let sk_dir = repo.join(".huskies/work/2_current");
|
||||
fs::create_dir_all(&sk_dir).unwrap();
|
||||
fs::write(sk_dir.join("md_only_test.md"), "---\nname: Test\n---\n").unwrap();
|
||||
Command::new("git")
|
||||
@@ -1313,17 +1313,17 @@ after\n";
|
||||
|
||||
let result = run_squash_merge(repo, "feature/story-md_only_test", "md_only_test").unwrap();
|
||||
|
||||
// The squash merge will commit the .storkit/ file, but should fail because
|
||||
// there are no code changes outside .storkit/.
|
||||
// The squash merge will commit the .huskies/ file, but should fail because
|
||||
// there are no code changes outside .huskies/.
|
||||
assert!(
|
||||
!result.success,
|
||||
"merge with only .storkit/ changes must fail: {}",
|
||||
"merge with only .huskies/ changes must fail: {}",
|
||||
result.output
|
||||
);
|
||||
|
||||
// Cleanup should still happen.
|
||||
assert!(
|
||||
!repo.join(".storkit/merge_workspace").exists(),
|
||||
!repo.join(".huskies/merge_workspace").exists(),
|
||||
"merge workspace should be cleaned up"
|
||||
);
|
||||
}
|
||||
@@ -1444,7 +1444,7 @@ after\n";
|
||||
"merge-queue branch must be cleaned up"
|
||||
);
|
||||
assert!(
|
||||
!repo.join(".storkit/merge_workspace").exists(),
|
||||
!repo.join(".huskies/merge_workspace").exists(),
|
||||
"merge workspace must be cleaned up"
|
||||
);
|
||||
}
|
||||
@@ -1560,7 +1560,7 @@ after\n";
|
||||
|
||||
// Cleanup must still happen.
|
||||
assert!(
|
||||
!repo.join(".storkit/merge_workspace").exists(),
|
||||
!repo.join(".huskies/merge_workspace").exists(),
|
||||
"merge workspace must be cleaned up even on gate failure"
|
||||
);
|
||||
}
|
||||
@@ -1600,7 +1600,7 @@ after\n";
|
||||
.unwrap();
|
||||
|
||||
// Simulate a stale merge workspace left from a previous failed merge.
|
||||
let stale_ws = repo.join(".storkit/merge_workspace");
|
||||
let stale_ws = repo.join(".huskies/merge_workspace");
|
||||
fs::create_dir_all(&stale_ws).unwrap();
|
||||
fs::write(stale_ws.join("leftover.txt"), "stale").unwrap();
|
||||
|
||||
@@ -1620,7 +1620,7 @@ after\n";
|
||||
|
||||
// ── story 216: merge worktree uses project.toml component setup ───────────
|
||||
|
||||
/// When the project has `[[component]]` entries in `.storkit/project.toml`,
|
||||
/// When the project has `[[component]]` entries in `.huskies/project.toml`,
|
||||
/// `run_squash_merge` must run their setup commands in the merge worktree
|
||||
/// before quality gates — matching the behaviour of `create_worktree`.
|
||||
#[cfg(unix)]
|
||||
@@ -1633,9 +1633,9 @@ after\n";
|
||||
let repo = tmp.path();
|
||||
init_git_repo(repo);
|
||||
|
||||
// Add a .storkit/project.toml with a component whose setup writes a
|
||||
// Add a .huskies/project.toml with a component whose setup writes a
|
||||
// sentinel file so we can confirm the command ran.
|
||||
let sk_dir = repo.join(".storkit");
|
||||
let sk_dir = repo.join(".huskies");
|
||||
fs::create_dir_all(&sk_dir).unwrap();
|
||||
fs::write(
|
||||
sk_dir.join("project.toml"),
|
||||
@@ -1708,7 +1708,7 @@ after\n";
|
||||
let repo = tmp.path();
|
||||
init_git_repo(repo);
|
||||
|
||||
// No .storkit/project.toml — no component setup.
|
||||
// No .huskies/project.toml — no component setup.
|
||||
fs::write(repo.join("file.txt"), "initial").unwrap();
|
||||
Command::new("git")
|
||||
.args(["add", "."])
|
||||
|
||||
@@ -73,7 +73,7 @@ impl AgentPool {
|
||||
on feature branch. Writing merge_failure and blocking."
|
||||
);
|
||||
let story_path = project_root
|
||||
.join(".storkit/work")
|
||||
.join(".huskies/work")
|
||||
.join(stage_dir)
|
||||
.join(format!("{story_id}.md"));
|
||||
let empty_diff_reason = "Feature branch has no code changes — the coder agent \
|
||||
@@ -221,7 +221,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn auto_assign_picks_up_story_queued_in_current() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
let current = sk.join("work/2_current");
|
||||
std::fs::create_dir_all(¤t).unwrap();
|
||||
std::fs::write(
|
||||
@@ -260,7 +260,7 @@ mod tests {
|
||||
let root = tmp.path();
|
||||
|
||||
// Create project.toml with a QA agent.
|
||||
let sk = root.join(".storkit");
|
||||
let sk = root.join(".huskies");
|
||||
std::fs::create_dir_all(&sk).unwrap();
|
||||
std::fs::write(
|
||||
sk.join("project.toml"),
|
||||
@@ -269,7 +269,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
// Put a spike in 3_qa/ with review_hold: true.
|
||||
let qa_dir = root.join(".storkit/work/3_qa");
|
||||
let qa_dir = root.join(".huskies/work/3_qa");
|
||||
std::fs::create_dir_all(&qa_dir).unwrap();
|
||||
std::fs::write(
|
||||
qa_dir.join("20_spike_test.md"),
|
||||
@@ -298,7 +298,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn auto_assign_ignores_coder_preference_when_story_is_in_qa_stage() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
let qa_dir = sk.join("work/3_qa");
|
||||
std::fs::create_dir_all(&qa_dir).unwrap();
|
||||
std::fs::write(
|
||||
@@ -345,7 +345,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn auto_assign_respects_coder_preference_when_story_is_in_current_stage() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
let current_dir = sk.join("work/2_current");
|
||||
std::fs::create_dir_all(¤t_dir).unwrap();
|
||||
std::fs::write(
|
||||
@@ -392,7 +392,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn auto_assign_stage_mismatch_with_no_fallback_starts_no_agent() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
let qa_dir = sk.join("work/3_qa");
|
||||
std::fs::create_dir_all(&qa_dir).unwrap();
|
||||
// Only a coder agent is configured — no QA agent exists.
|
||||
@@ -431,7 +431,7 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path().to_path_buf();
|
||||
|
||||
let sk_dir = root.join(".storkit");
|
||||
let sk_dir = root.join(".huskies");
|
||||
// Two stories waiting in 2_current, one coder agent.
|
||||
fs::create_dir_all(sk_dir.join("work/2_current")).unwrap();
|
||||
fs::write(
|
||||
|
||||
@@ -18,7 +18,7 @@ impl AgentPool {
|
||||
/// (called immediately after) picks up the right next-stage agents.
|
||||
///
|
||||
/// Algorithm:
|
||||
/// 1. List all worktree directories under `{project_root}/.storkit/worktrees/`.
|
||||
/// 1. List all worktree directories under `{project_root}/.huskies/worktrees/`.
|
||||
/// 2. For each worktree, check whether its feature branch has commits ahead of the
|
||||
/// base branch (`master` / `main`).
|
||||
/// 3. If committed work is found AND the story is in `2_current/` or `3_qa/`:
|
||||
@@ -153,7 +153,7 @@ impl AgentPool {
|
||||
.unwrap_or_default()
|
||||
.default_qa_mode();
|
||||
let story_path = project_root
|
||||
.join(".storkit/work/2_current")
|
||||
.join(".huskies/work/2_current")
|
||||
.join(format!("{story_id}.md"));
|
||||
crate::io::story_metadata::resolve_qa_mode(&story_path, default_qa)
|
||||
}
|
||||
@@ -210,7 +210,7 @@ impl AgentPool {
|
||||
});
|
||||
} else {
|
||||
let story_path = project_root
|
||||
.join(".storkit/work/3_qa")
|
||||
.join(".huskies/work/3_qa")
|
||||
.join(format!("{story_id}.md"));
|
||||
if let Err(e) =
|
||||
crate::io::story_metadata::write_review_hold(&story_path)
|
||||
@@ -268,7 +268,7 @@ impl AgentPool {
|
||||
true
|
||||
} else {
|
||||
let story_path = project_root
|
||||
.join(".storkit/work/3_qa")
|
||||
.join(".huskies/work/3_qa")
|
||||
.join(format!("{story_id}.md"));
|
||||
let default_qa = crate::config::ProjectConfig::load(project_root)
|
||||
.unwrap_or_default()
|
||||
@@ -282,7 +282,7 @@ impl AgentPool {
|
||||
|
||||
if needs_human_review {
|
||||
let story_path = project_root
|
||||
.join(".storkit/work/3_qa")
|
||||
.join(".huskies/work/3_qa")
|
||||
.join(format!("{story_id}.md"));
|
||||
if let Err(e) =
|
||||
crate::io::story_metadata::write_review_hold(&story_path)
|
||||
@@ -416,13 +416,13 @@ mod tests {
|
||||
let root = tmp.path();
|
||||
|
||||
// Set up story in 2_current/.
|
||||
let current = root.join(".storkit/work/2_current");
|
||||
let current = root.join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(current.join("60_story_test.md"), "test").unwrap();
|
||||
|
||||
// Create a worktree directory that is a fresh git repo with no commits
|
||||
// ahead of its own base branch (simulates a worktree where no work was done).
|
||||
let wt_dir = root.join(".storkit/worktrees/60_story_test");
|
||||
let wt_dir = root.join(".huskies/worktrees/60_story_test");
|
||||
fs::create_dir_all(&wt_dir).unwrap();
|
||||
init_git_repo(&wt_dir);
|
||||
|
||||
@@ -447,7 +447,7 @@ mod tests {
|
||||
init_git_repo(root);
|
||||
|
||||
// Set up story in 2_current/ and commit it so the project root is clean.
|
||||
let current = root.join(".storkit/work/2_current");
|
||||
let current = root.join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(current.join("61_story_test.md"), "test").unwrap();
|
||||
Command::new("git")
|
||||
@@ -470,7 +470,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
// Create a real git worktree for the story.
|
||||
let wt_dir = root.join(".storkit/worktrees/61_story_test");
|
||||
let wt_dir = root.join(".huskies/worktrees/61_story_test");
|
||||
fs::create_dir_all(wt_dir.parent().unwrap()).unwrap();
|
||||
Command::new("git")
|
||||
.args([
|
||||
@@ -518,7 +518,7 @@ mod tests {
|
||||
// and the story stays in 2_current/. The important assertion is that
|
||||
// reconcile ran without panicking and the story is in a consistent state.
|
||||
let in_current = current.join("61_story_test.md").exists();
|
||||
let in_qa = root.join(".storkit/work/3_qa/61_story_test.md").exists();
|
||||
let in_qa = root.join(".huskies/work/3_qa/61_story_test.md").exists();
|
||||
assert!(
|
||||
in_current || in_qa,
|
||||
"story should be in 2_current/ or 3_qa/ after reconciliation"
|
||||
|
||||
@@ -19,7 +19,7 @@ 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(".storkit").join("work").join(stage_dir);
|
||||
let dir = project_root.join(".huskies").join("work").join(stage_dir);
|
||||
if !dir.is_dir() {
|
||||
return Vec::new();
|
||||
}
|
||||
@@ -169,7 +169,7 @@ mod tests {
|
||||
fn scan_stage_items_returns_sorted_story_ids() {
|
||||
use std::fs;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let stage_dir = tmp.path().join(".storkit").join("work").join("2_current");
|
||||
let stage_dir = tmp.path().join(".huskies").join("work").join("2_current");
|
||||
fs::create_dir_all(&stage_dir).unwrap();
|
||||
fs::write(stage_dir.join("42_story_foo.md"), "---\nname: foo\n---").unwrap();
|
||||
fs::write(stage_dir.join("10_story_bar.md"), "---\nname: bar\n---").unwrap();
|
||||
|
||||
@@ -13,7 +13,7 @@ pub(super) fn read_story_front_matter_agent(
|
||||
) -> Option<String> {
|
||||
use crate::io::story_metadata::parse_front_matter;
|
||||
let path = project_root
|
||||
.join(".storkit")
|
||||
.join(".huskies")
|
||||
.join("work")
|
||||
.join(stage_dir)
|
||||
.join(format!("{story_id}.md"));
|
||||
@@ -25,7 +25,7 @@ pub(super) fn read_story_front_matter_agent(
|
||||
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(".storkit")
|
||||
.join(".huskies")
|
||||
.join("work")
|
||||
.join(stage_dir)
|
||||
.join(format!("{story_id}.md"));
|
||||
@@ -43,7 +43,7 @@ pub(super) fn has_review_hold(project_root: &Path, stage_dir: &str, story_id: &s
|
||||
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(".storkit")
|
||||
.join(".huskies")
|
||||
.join("work")
|
||||
.join(stage_dir)
|
||||
.join(format!("{story_id}.md"));
|
||||
@@ -61,7 +61,7 @@ pub(super) fn is_story_blocked(project_root: &Path, stage_dir: &str, story_id: &
|
||||
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(".storkit")
|
||||
.join(".huskies")
|
||||
.join("work")
|
||||
.join(stage_dir)
|
||||
.join(format!("{story_id}.md"));
|
||||
@@ -84,7 +84,7 @@ mod tests {
|
||||
#[test]
|
||||
fn has_review_hold_returns_true_when_set() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let qa_dir = tmp.path().join(".storkit/work/3_qa");
|
||||
let qa_dir = tmp.path().join(".huskies/work/3_qa");
|
||||
std::fs::create_dir_all(&qa_dir).unwrap();
|
||||
let spike_path = qa_dir.join("10_spike_research.md");
|
||||
std::fs::write(
|
||||
@@ -98,7 +98,7 @@ mod tests {
|
||||
#[test]
|
||||
fn has_review_hold_returns_false_when_not_set() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let qa_dir = tmp.path().join(".storkit/work/3_qa");
|
||||
let qa_dir = tmp.path().join(".huskies/work/3_qa");
|
||||
std::fs::create_dir_all(&qa_dir).unwrap();
|
||||
let spike_path = qa_dir.join("10_spike_research.md");
|
||||
std::fs::write(&spike_path, "---\nname: Research spike\n---\n# Spike\n").unwrap();
|
||||
|
||||
@@ -87,7 +87,7 @@ impl AgentPool {
|
||||
let front_matter_agent: Option<String> = if agent_name.is_none() {
|
||||
find_active_story_stage(project_root, story_id).and_then(|stage_dir| {
|
||||
let path = project_root
|
||||
.join(".storkit")
|
||||
.join(".huskies")
|
||||
.join("work")
|
||||
.join(stage_dir)
|
||||
.join(format!("{story_id}.md"));
|
||||
@@ -873,7 +873,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn start_agent_auto_selects_second_coder_when_first_busy() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
std::fs::create_dir_all(&sk).unwrap();
|
||||
std::fs::write(
|
||||
sk.join("project.toml"),
|
||||
@@ -919,7 +919,7 @@ stage = "coder"
|
||||
#[tokio::test]
|
||||
async fn start_agent_returns_busy_when_all_coders_occupied() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
std::fs::create_dir_all(&sk).unwrap();
|
||||
std::fs::write(
|
||||
sk.join("project.toml"),
|
||||
@@ -951,7 +951,7 @@ stage = "coder"
|
||||
#[tokio::test]
|
||||
async fn start_agent_moves_story_to_current_when_coders_busy() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
let backlog = sk.join("work/1_backlog");
|
||||
std::fs::create_dir_all(&backlog).unwrap();
|
||||
std::fs::write(
|
||||
@@ -996,7 +996,7 @@ stage = "coder"
|
||||
#[tokio::test]
|
||||
async fn start_agent_story_already_in_current_is_noop() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
let current = sk.join("work/2_current");
|
||||
std::fs::create_dir_all(¤t).unwrap();
|
||||
std::fs::write(
|
||||
@@ -1023,7 +1023,7 @@ stage = "coder"
|
||||
#[tokio::test]
|
||||
async fn start_agent_explicit_name_unchanged_when_busy() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
std::fs::create_dir_all(&sk).unwrap();
|
||||
std::fs::write(
|
||||
sk.join("project.toml"),
|
||||
@@ -1062,7 +1062,7 @@ stage = "coder"
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
|
||||
let sk_dir = root.join(".storkit");
|
||||
let sk_dir = root.join(".huskies");
|
||||
fs::create_dir_all(&sk_dir).unwrap();
|
||||
fs::write(sk_dir.join("project.toml"), "[[agent]]\nname = \"qa\"\n").unwrap();
|
||||
|
||||
@@ -1089,7 +1089,7 @@ stage = "coder"
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
|
||||
let sk_dir = root.join(".storkit");
|
||||
let sk_dir = root.join(".huskies");
|
||||
fs::create_dir_all(&sk_dir).unwrap();
|
||||
fs::write(sk_dir.join("project.toml"), "[[agent]]\nname = \"qa\"\n").unwrap();
|
||||
|
||||
@@ -1115,7 +1115,7 @@ stage = "coder"
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
|
||||
let sk_dir = root.join(".storkit");
|
||||
let sk_dir = root.join(".huskies");
|
||||
fs::create_dir_all(&sk_dir).unwrap();
|
||||
fs::write(
|
||||
sk_dir.join("project.toml"),
|
||||
@@ -1123,7 +1123,7 @@ stage = "coder"
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let upcoming = root.join(".storkit/work/1_backlog");
|
||||
let upcoming = root.join(".huskies/work/1_backlog");
|
||||
fs::create_dir_all(&upcoming).unwrap();
|
||||
fs::write(upcoming.join("50_story_test.md"), "---\nname: Test\n---\n").unwrap();
|
||||
|
||||
@@ -1181,7 +1181,7 @@ stage = "coder"
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
|
||||
let sk_dir = root.join(".storkit");
|
||||
let sk_dir = root.join(".huskies");
|
||||
fs::create_dir_all(&sk_dir).unwrap();
|
||||
fs::write(sk_dir.join("project.toml"), "[[agent]]\nname = \"qa\"\n").unwrap();
|
||||
|
||||
@@ -1207,7 +1207,7 @@ stage = "coder"
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
|
||||
let sk_dir = root.join(".storkit");
|
||||
let sk_dir = root.join(".huskies");
|
||||
fs::create_dir_all(&sk_dir).unwrap();
|
||||
fs::write(
|
||||
sk_dir.join("project.toml"),
|
||||
@@ -1238,20 +1238,20 @@ stage = "coder"
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path().to_path_buf();
|
||||
|
||||
let sk_dir = root.join(".storkit");
|
||||
let sk_dir = root.join(".huskies");
|
||||
fs::create_dir_all(sk_dir.join("work/1_backlog")).unwrap();
|
||||
fs::write(
|
||||
root.join(".storkit/project.toml"),
|
||||
root.join(".huskies/project.toml"),
|
||||
"[[agent]]\nname = \"coder-1\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
root.join(".storkit/work/1_backlog/86_story_foo.md"),
|
||||
root.join(".huskies/work/1_backlog/86_story_foo.md"),
|
||||
"---\nname: Foo\n---\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
root.join(".storkit/work/1_backlog/130_story_bar.md"),
|
||||
root.join(".huskies/work/1_backlog/130_story_bar.md"),
|
||||
"---\nname: Bar\n---\n",
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1303,7 +1303,7 @@ stage = "coder"
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
|
||||
let sk_dir = root.join(".storkit");
|
||||
let sk_dir = root.join(".huskies");
|
||||
fs::create_dir_all(&sk_dir).unwrap();
|
||||
fs::write(
|
||||
sk_dir.join("project.toml"),
|
||||
@@ -1340,7 +1340,7 @@ stage = "coder"
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
|
||||
let sk_dir = root.join(".storkit");
|
||||
let sk_dir = root.join(".huskies");
|
||||
fs::create_dir_all(&sk_dir).unwrap();
|
||||
fs::write(
|
||||
sk_dir.join("project.toml"),
|
||||
@@ -1372,15 +1372,15 @@ stage = "coder"
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path().to_path_buf();
|
||||
|
||||
let sk_dir = root.join(".storkit");
|
||||
let sk_dir = root.join(".huskies");
|
||||
fs::create_dir_all(sk_dir.join("work/2_current")).unwrap();
|
||||
fs::write(
|
||||
root.join(".storkit/project.toml"),
|
||||
root.join(".huskies/project.toml"),
|
||||
"[[agent]]\nname = \"coder-1\"\n\n[[agent]]\nname = \"coder-2\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
root.join(".storkit/work/2_current/42_story_foo.md"),
|
||||
root.join(".huskies/work/2_current/42_story_foo.md"),
|
||||
"---\nname: Foo\n---\n",
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1426,15 +1426,15 @@ stage = "coder"
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
|
||||
let sk_dir = root.join(".storkit");
|
||||
let sk_dir = root.join(".huskies");
|
||||
fs::create_dir_all(sk_dir.join("work/1_backlog")).unwrap();
|
||||
fs::write(
|
||||
root.join(".storkit/project.toml"),
|
||||
root.join(".huskies/project.toml"),
|
||||
"[[agent]]\nname = \"coder-1\"\n\n[[agent]]\nname = \"coder-2\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
root.join(".storkit/work/1_backlog/99_story_baz.md"),
|
||||
root.join(".huskies/work/1_backlog/99_story_baz.md"),
|
||||
"---\nname: Baz\n---\n",
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1464,7 +1464,7 @@ stage = "coder"
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
|
||||
let sk_dir = root.join(".storkit");
|
||||
let sk_dir = root.join(".huskies");
|
||||
fs::create_dir_all(sk_dir.join("work/2_current")).unwrap();
|
||||
fs::write(
|
||||
sk_dir.join("project.toml"),
|
||||
@@ -1501,7 +1501,7 @@ stage = "coder"
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
|
||||
let sk_dir = root.join(".storkit");
|
||||
let sk_dir = root.join(".huskies");
|
||||
fs::create_dir_all(sk_dir.join("work/3_qa")).unwrap();
|
||||
fs::write(
|
||||
sk_dir.join("project.toml"),
|
||||
@@ -1538,7 +1538,7 @@ stage = "coder"
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
|
||||
let sk_dir = root.join(".storkit");
|
||||
let sk_dir = root.join(".huskies");
|
||||
fs::create_dir_all(sk_dir.join("work/4_merge")).unwrap();
|
||||
fs::write(
|
||||
sk_dir.join("project.toml"),
|
||||
@@ -1575,7 +1575,7 @@ stage = "coder"
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
|
||||
let sk_dir = root.join(".storkit");
|
||||
let sk_dir = root.join(".huskies");
|
||||
fs::create_dir_all(sk_dir.join("work/2_current")).unwrap();
|
||||
fs::write(
|
||||
sk_dir.join("project.toml"),
|
||||
@@ -1611,7 +1611,7 @@ stage = "coder"
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
|
||||
let sk_dir = root.join(".storkit");
|
||||
let sk_dir = root.join(".huskies");
|
||||
fs::create_dir_all(sk_dir.join("work/4_merge")).unwrap();
|
||||
fs::write(
|
||||
sk_dir.join("project.toml"),
|
||||
@@ -1674,7 +1674,7 @@ stage = "coder"
|
||||
#[tokio::test]
|
||||
async fn start_agent_honours_front_matter_agent_when_idle() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
let backlog = sk.join("work/1_backlog");
|
||||
std::fs::create_dir_all(&backlog).unwrap();
|
||||
std::fs::write(
|
||||
@@ -1730,7 +1730,7 @@ stage = "coder"
|
||||
#[tokio::test]
|
||||
async fn start_agent_returns_error_when_front_matter_agent_busy() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
let backlog = sk.join("work/1_backlog");
|
||||
std::fs::create_dir_all(&backlog).unwrap();
|
||||
std::fs::write(
|
||||
@@ -1781,7 +1781,7 @@ stage = "coder"
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
|
||||
let current = root.join(".storkit/work/2_current");
|
||||
let current = root.join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(current.join("60_story_cleanup.md"), "test").unwrap();
|
||||
|
||||
@@ -1804,7 +1804,7 @@ stage = "coder"
|
||||
assert_eq!(remaining[0].story_id, "61_story_other");
|
||||
|
||||
assert!(
|
||||
root.join(".storkit/work/5_done/60_story_cleanup.md")
|
||||
root.join(".huskies/work/5_done/60_story_cleanup.md")
|
||||
.exists()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ impl AgentPool {
|
||||
let default_qa = config.default_qa_mode();
|
||||
// Story is in 2_current/ when a coder completes.
|
||||
let story_path = project_root
|
||||
.join(".storkit/work/2_current")
|
||||
.join(".huskies/work/2_current")
|
||||
.join(format!("{story_id}.md"));
|
||||
crate::io::story_metadata::resolve_qa_mode(&story_path, default_qa)
|
||||
}
|
||||
@@ -104,7 +104,7 @@ impl AgentPool {
|
||||
if let Err(e) = crate::agents::lifecycle::move_story_to_qa(&project_root, story_id) {
|
||||
slog_error!("[pipeline] Failed to move '{story_id}' to 3_qa/: {e}");
|
||||
} else {
|
||||
let qa_dir = project_root.join(".storkit/work/3_qa");
|
||||
let qa_dir = project_root.join(".huskies/work/3_qa");
|
||||
let story_path = qa_dir.join(format!("{story_id}.md"));
|
||||
if let Err(e) =
|
||||
crate::io::story_metadata::write_review_hold(&story_path)
|
||||
@@ -119,7 +119,7 @@ impl AgentPool {
|
||||
} else {
|
||||
// Increment retry count and check if blocked.
|
||||
let story_path = project_root
|
||||
.join(".storkit/work/2_current")
|
||||
.join(".huskies/work/2_current")
|
||||
.join(format!("{story_id}.md"));
|
||||
if let Some(reason) = should_block_story(&story_path, config.max_retries, story_id, "coder") {
|
||||
// Story has exceeded retry limit — do not restart.
|
||||
@@ -174,7 +174,7 @@ impl AgentPool {
|
||||
if item_type == "spike" {
|
||||
true // Spikes always need human review.
|
||||
} else {
|
||||
let qa_dir = project_root.join(".storkit/work/3_qa");
|
||||
let qa_dir = project_root.join(".huskies/work/3_qa");
|
||||
let story_path = qa_dir.join(format!("{story_id}.md"));
|
||||
let default_qa = config.default_qa_mode();
|
||||
matches!(
|
||||
@@ -186,7 +186,7 @@ impl AgentPool {
|
||||
|
||||
if needs_human_review {
|
||||
// Hold in 3_qa/ for human review.
|
||||
let qa_dir = project_root.join(".storkit/work/3_qa");
|
||||
let qa_dir = project_root.join(".huskies/work/3_qa");
|
||||
let story_path = qa_dir.join(format!("{story_id}.md"));
|
||||
if let Err(e) =
|
||||
crate::io::story_metadata::write_review_hold(&story_path)
|
||||
@@ -222,7 +222,7 @@ impl AgentPool {
|
||||
}
|
||||
} else {
|
||||
let story_path = project_root
|
||||
.join(".storkit/work/3_qa")
|
||||
.join(".huskies/work/3_qa")
|
||||
.join(format!("{story_id}.md"));
|
||||
if let Some(reason) = should_block_story(&story_path, config.max_retries, story_id, "qa-coverage") {
|
||||
// Story has exceeded retry limit — do not restart.
|
||||
@@ -250,7 +250,7 @@ impl AgentPool {
|
||||
}
|
||||
} else {
|
||||
let story_path = project_root
|
||||
.join(".storkit/work/3_qa")
|
||||
.join(".huskies/work/3_qa")
|
||||
.join(format!("{story_id}.md"));
|
||||
if let Some(reason) = should_block_story(&story_path, config.max_retries, story_id, "qa") {
|
||||
// Story has exceeded retry limit — do not restart.
|
||||
@@ -330,7 +330,7 @@ impl AgentPool {
|
||||
);
|
||||
} else {
|
||||
let story_path = project_root
|
||||
.join(".storkit/work/4_merge")
|
||||
.join(".huskies/work/4_merge")
|
||||
.join(format!("{story_id}.md"));
|
||||
if let Some(reason) = should_block_story(&story_path, config.max_retries, story_id, "mergemaster") {
|
||||
// Story has exceeded retry limit — do not restart.
|
||||
@@ -469,7 +469,7 @@ mod tests {
|
||||
let root = tmp.path();
|
||||
|
||||
// Set up story in 2_current/ (no qa frontmatter → uses project default "server")
|
||||
let current = root.join(".storkit/work/2_current");
|
||||
let current = root.join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(current.join("50_story_test.md"), "test").unwrap();
|
||||
|
||||
@@ -490,7 +490,7 @@ mod tests {
|
||||
|
||||
// With default qa: server, story skips QA and goes straight to 4_merge/
|
||||
assert!(
|
||||
root.join(".storkit/work/4_merge/50_story_test.md")
|
||||
root.join(".huskies/work/4_merge/50_story_test.md")
|
||||
.exists(),
|
||||
"story should be in 4_merge/"
|
||||
);
|
||||
@@ -507,7 +507,7 @@ mod tests {
|
||||
let root = tmp.path();
|
||||
|
||||
// Set up story in 2_current/ with qa: agent frontmatter
|
||||
let current = root.join(".storkit/work/2_current");
|
||||
let current = root.join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(
|
||||
current.join("50_story_test.md"),
|
||||
@@ -532,7 +532,7 @@ mod tests {
|
||||
|
||||
// With qa: agent, story should move to 3_qa/
|
||||
assert!(
|
||||
root.join(".storkit/work/3_qa/50_story_test.md").exists(),
|
||||
root.join(".huskies/work/3_qa/50_story_test.md").exists(),
|
||||
"story should be in 3_qa/"
|
||||
);
|
||||
assert!(
|
||||
@@ -548,7 +548,7 @@ mod tests {
|
||||
let root = tmp.path();
|
||||
|
||||
// Set up story in 3_qa/
|
||||
let qa_dir = root.join(".storkit/work/3_qa");
|
||||
let qa_dir = root.join(".huskies/work/3_qa");
|
||||
fs::create_dir_all(&qa_dir).unwrap();
|
||||
// qa: server so the story skips human review and goes straight to merge.
|
||||
fs::write(
|
||||
@@ -574,7 +574,7 @@ mod tests {
|
||||
|
||||
// Story should have moved to 4_merge/
|
||||
assert!(
|
||||
root.join(".storkit/work/4_merge/51_story_test.md")
|
||||
root.join(".huskies/work/4_merge/51_story_test.md")
|
||||
.exists(),
|
||||
"story should be in 4_merge/"
|
||||
);
|
||||
@@ -590,7 +590,7 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
|
||||
let current = root.join(".storkit/work/2_current");
|
||||
let current = root.join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(current.join("52_story_test.md"), "test").unwrap();
|
||||
|
||||
@@ -624,18 +624,18 @@ mod tests {
|
||||
let root = tmp.path();
|
||||
|
||||
// Set up story in 2_current/
|
||||
let current = root.join(".storkit/work/2_current");
|
||||
let current = root.join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(current.join("173_story_test.md"), "test").unwrap();
|
||||
// Ensure 3_qa/ exists for the move target
|
||||
fs::create_dir_all(root.join(".storkit/work/3_qa")).unwrap();
|
||||
fs::create_dir_all(root.join(".huskies/work/3_qa")).unwrap();
|
||||
// Ensure 1_backlog/ exists (start_agent calls move_story_to_current)
|
||||
fs::create_dir_all(root.join(".storkit/work/1_backlog")).unwrap();
|
||||
fs::create_dir_all(root.join(".huskies/work/1_backlog")).unwrap();
|
||||
|
||||
// Write a project.toml with a qa agent so start_agent can resolve it.
|
||||
fs::create_dir_all(root.join(".storkit")).unwrap();
|
||||
fs::create_dir_all(root.join(".huskies")).unwrap();
|
||||
fs::write(
|
||||
root.join(".storkit/project.toml"),
|
||||
root.join(".huskies/project.toml"),
|
||||
r#"
|
||||
default_qa = "agent"
|
||||
|
||||
@@ -703,7 +703,7 @@ stage = "qa"
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
|
||||
let sk = root.join(".storkit");
|
||||
let sk = root.join(".huskies");
|
||||
let qa_dir = sk.join("work/3_qa");
|
||||
fs::create_dir_all(&qa_dir).unwrap();
|
||||
|
||||
|
||||
@@ -563,7 +563,7 @@ mod tests {
|
||||
fs::set_permissions(&script_test, perms).unwrap();
|
||||
|
||||
// Story in 4_merge/ — must NOT be moved to 5_done/.
|
||||
let merge_dir = root.join(".storkit/work/4_merge");
|
||||
let merge_dir = root.join(".huskies/work/4_merge");
|
||||
fs::create_dir_all(&merge_dir).unwrap();
|
||||
let story_path = merge_dir.join("99_story_merge445.md");
|
||||
fs::write(&story_path, "---\nname: Merge 445 Test\n---\n").unwrap();
|
||||
@@ -590,7 +590,7 @@ mod tests {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(150)).await;
|
||||
|
||||
// Story must remain in 4_merge/ — not moved to 5_done/.
|
||||
let done_path = root.join(".storkit/work/5_done/99_story_merge445.md");
|
||||
let done_path = root.join(".huskies/work/5_done/99_story_merge445.md");
|
||||
assert!(
|
||||
!done_path.exists(),
|
||||
"Story must NOT be moved to 5_done/ when run_server_owned_completion \
|
||||
|
||||
@@ -289,7 +289,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
// Create the story file in 4_merge/ so we can test archival
|
||||
let merge_dir = repo.join(".storkit/work/4_merge");
|
||||
let merge_dir = repo.join(".huskies/work/4_merge");
|
||||
fs::create_dir_all(&merge_dir).unwrap();
|
||||
let story_file = merge_dir.join("23_test.md");
|
||||
fs::write(&story_file, "---\nname: Test\n---\n").unwrap();
|
||||
@@ -317,7 +317,7 @@ mod tests {
|
||||
"report should be coherent: {report:?}"
|
||||
);
|
||||
if report.story_archived {
|
||||
let done = repo.join(".storkit/work/5_done/23_test.md");
|
||||
let done = repo.join(".huskies/work/5_done/23_test.md");
|
||||
assert!(done.exists(), "done file should exist");
|
||||
}
|
||||
}
|
||||
@@ -502,7 +502,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
// Create story file in 4_merge.
|
||||
let merge_dir = repo.join(".storkit/work/4_merge");
|
||||
let merge_dir = repo.join(".huskies/work/4_merge");
|
||||
fs::create_dir_all(&merge_dir).unwrap();
|
||||
fs::write(merge_dir.join("42_story_foo.md"), "---\nname: Test\n---\n").unwrap();
|
||||
Command::new("git")
|
||||
|
||||
@@ -26,7 +26,7 @@ pub(super) fn find_active_story_stage(project_root: &Path, story_id: &str) -> Op
|
||||
const STAGES: [&str; 3] = ["2_current", "3_qa", "4_merge"];
|
||||
for stage in &STAGES {
|
||||
let path = project_root
|
||||
.join(".storkit")
|
||||
.join(".huskies")
|
||||
.join("work")
|
||||
.join(stage)
|
||||
.join(format!("{story_id}.md"));
|
||||
@@ -46,7 +46,7 @@ mod tests {
|
||||
use std::fs;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let current = root.join(".storkit/work/2_current");
|
||||
let current = root.join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(current.join("10_story_test.md"), "test").unwrap();
|
||||
|
||||
@@ -61,7 +61,7 @@ mod tests {
|
||||
use std::fs;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let qa = root.join(".storkit/work/3_qa");
|
||||
let qa = root.join(".huskies/work/3_qa");
|
||||
fs::create_dir_all(&qa).unwrap();
|
||||
fs::write(qa.join("11_story_test.md"), "test").unwrap();
|
||||
|
||||
@@ -73,7 +73,7 @@ mod tests {
|
||||
use std::fs;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let merge = root.join(".storkit/work/4_merge");
|
||||
let merge = root.join(".huskies/work/4_merge");
|
||||
fs::create_dir_all(&merge).unwrap();
|
||||
fs::write(merge.join("12_story_test.md"), "test").unwrap();
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ use super::{AgentRuntime, RuntimeContext, RuntimeResult, RuntimeStatus};
|
||||
/// `generateContent` REST API.
|
||||
///
|
||||
/// The runtime:
|
||||
/// 1. Fetches MCP tool definitions from storkit's MCP server.
|
||||
/// 1. Fetches MCP tool definitions from huskies' MCP server.
|
||||
/// 2. Converts them to Gemini function-calling format.
|
||||
/// 3. Sends the agent prompt + tools to the Gemini API.
|
||||
/// 4. Executes any requested function calls via MCP `tools/call`.
|
||||
@@ -401,7 +401,7 @@ fn build_generate_content_request(
|
||||
body
|
||||
}
|
||||
|
||||
/// Fetch MCP tool definitions from storkit's MCP server and convert
|
||||
/// Fetch MCP tool definitions from huskies' MCP server and convert
|
||||
/// them to Gemini function declaration format.
|
||||
async fn fetch_and_convert_mcp_tools(
|
||||
client: &Client,
|
||||
@@ -522,7 +522,7 @@ fn clean_schema_properties(properties: &Value) -> Value {
|
||||
Value::Object(cleaned)
|
||||
}
|
||||
|
||||
/// Call an MCP tool via storkit's MCP server.
|
||||
/// Call an MCP tool via huskies' MCP server.
|
||||
async fn call_mcp_tool(
|
||||
client: &Client,
|
||||
mcp_base: &str,
|
||||
|
||||
@@ -22,7 +22,7 @@ pub struct RuntimeContext {
|
||||
pub prompt: String,
|
||||
pub cwd: String,
|
||||
pub inactivity_timeout_secs: u64,
|
||||
/// Port of the storkit MCP server, used by API-based runtimes (Gemini, OpenAI)
|
||||
/// Port of the huskies MCP server, used by API-based runtimes (Gemini, OpenAI)
|
||||
/// to call back for tool execution.
|
||||
pub mcp_port: u16,
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ use super::{AgentRuntime, RuntimeContext, RuntimeResult, RuntimeStatus};
|
||||
/// the OpenAI Chat Completions API.
|
||||
///
|
||||
/// The runtime:
|
||||
/// 1. Fetches MCP tool definitions from storkit's MCP server.
|
||||
/// 1. Fetches MCP tool definitions from huskies' MCP server.
|
||||
/// 2. Converts them to OpenAI function-calling format.
|
||||
/// 3. Sends the agent prompt + tools to the Chat Completions API.
|
||||
/// 4. Executes any requested tool calls via MCP `tools/call`.
|
||||
@@ -311,7 +311,7 @@ fn build_system_text(ctx: &RuntimeContext) -> String {
|
||||
})
|
||||
}
|
||||
|
||||
/// Fetch MCP tool definitions from storkit's MCP server and convert
|
||||
/// Fetch MCP tool definitions from huskies' MCP server and convert
|
||||
/// them to OpenAI function-calling format.
|
||||
async fn fetch_and_convert_mcp_tools(
|
||||
client: &Client,
|
||||
@@ -433,7 +433,7 @@ fn clean_schema_properties(properties: &Value) -> Value {
|
||||
Value::Object(cleaned)
|
||||
}
|
||||
|
||||
/// Call an MCP tool via storkit's MCP server.
|
||||
/// Call an MCP tool via huskies' MCP server.
|
||||
async fn call_mcp_tool(
|
||||
client: &Client,
|
||||
mcp_base: &str,
|
||||
|
||||
@@ -20,7 +20,7 @@ pub struct TokenUsageRecord {
|
||||
/// Append a token usage record to the persistent JSONL file.
|
||||
///
|
||||
/// Each line is a self-contained JSON object, making appends atomic and
|
||||
/// reads simple. The file lives at `.storkit/token_usage.jsonl`.
|
||||
/// reads simple. The file lives at `.huskies/token_usage.jsonl`.
|
||||
pub fn append_record(project_root: &Path, record: &TokenUsageRecord) -> Result<(), String> {
|
||||
let path = token_usage_path(project_root);
|
||||
if let Some(parent) = path.parent() {
|
||||
@@ -87,7 +87,7 @@ pub fn build_record(
|
||||
}
|
||||
|
||||
fn token_usage_path(project_root: &Path) -> std::path::PathBuf {
|
||||
project_root.join(".storkit").join("token_usage.jsonl")
|
||||
project_root.join(".huskies").join("token_usage.jsonl")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -147,7 +147,7 @@ mod tests {
|
||||
fn malformed_lines_are_skipped() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let root = dir.path();
|
||||
let path = root.join(".storkit").join("token_usage.jsonl");
|
||||
let path = root.join(".huskies").join("token_usage.jsonl");
|
||||
fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
fs::write(&path, "not json\n{\"bad\":true}\n").unwrap();
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ const SKIP_DIRS: &[&str] = &[
|
||||
];
|
||||
|
||||
/// Path components that indicate a worktree path that should be skipped.
|
||||
const SKIP_PATH_COMPONENTS: &[&str] = &[".storkit/worktrees"];
|
||||
const SKIP_PATH_COMPONENTS: &[&str] = &[".huskies/worktrees"];
|
||||
|
||||
/// Known-huge or machine-generated files that are excluded from the loc count
|
||||
/// even when they have a recognised source extension (e.g. `.json`, `.yaml`).
|
||||
@@ -88,10 +88,10 @@ fn loc_top_n(project_root: &std::path::Path, top_n: usize) -> String {
|
||||
if SKIP_DIRS.iter().any(|s| *s == name.as_ref()) {
|
||||
return false;
|
||||
}
|
||||
// Skip .storkit/worktrees — use relative path so the check
|
||||
// Skip .huskies/worktrees — use relative path so the check
|
||||
// doesn't exclude the project root itself when running
|
||||
// from inside a worktree (where the absolute path contains
|
||||
// ".storkit/worktrees").
|
||||
// ".huskies/worktrees").
|
||||
let rel = e
|
||||
.path()
|
||||
.strip_prefix(project_root)
|
||||
@@ -332,7 +332,7 @@ mod tests {
|
||||
let ctx = make_ctx(&agents, &ambient_rooms, repo_root, "");
|
||||
let output = handle_loc(&ctx).unwrap();
|
||||
assert!(
|
||||
!output.contains(".storkit/worktrees"),
|
||||
!output.contains(".huskies/worktrees"),
|
||||
"output must not include paths inside worktrees: {output}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ pub(super) fn handle_move(ctx: &CommandContext) -> Option<String> {
|
||||
'outer: for stage_dir in SEARCH_DIRS {
|
||||
let dir = ctx
|
||||
.project_root
|
||||
.join(".storkit")
|
||||
.join(".huskies")
|
||||
.join("work")
|
||||
.join(stage_dir);
|
||||
if !dir.exists() {
|
||||
@@ -242,7 +242,7 @@ mod tests {
|
||||
// Verify the file was actually moved.
|
||||
let new_path = tmp
|
||||
.path()
|
||||
.join(".storkit/work/2_current/42_story_some_feature.md");
|
||||
.join(".huskies/work/2_current/42_story_some_feature.md");
|
||||
assert!(new_path.exists(), "story file should be in 2_current/");
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use super::CommandContext;
|
||||
|
||||
/// Show implementation summary for a story identified by its number.
|
||||
///
|
||||
/// Finds the `storkit: merge {story_id}` commit on master, displays the
|
||||
/// Finds the `huskies: merge {story_id}` commit on master, displays the
|
||||
/// git diff --stat (files changed with line counts), and extracts key
|
||||
/// function/struct/type names added or modified in the implementation.
|
||||
/// Returns a friendly message when no merge commit is found.
|
||||
@@ -82,11 +82,11 @@ pub(super) fn handle_overview(ctx: &CommandContext) -> Option<String> {
|
||||
/// Find the merge commit hash for a story by its numeric ID.
|
||||
///
|
||||
/// Searches git log for a commit whose subject matches
|
||||
/// `storkit: merge {num}_*` or the legacy `story-kit: merge {num}_*`.
|
||||
/// `huskies: merge {num}_*` or the legacy `story-kit: merge {num}_*`.
|
||||
fn find_story_merge_commit(root: &std::path::Path, num_str: &str) -> Option<String> {
|
||||
use std::process::Command;
|
||||
// Match both the current prefix and the legacy one from before the rename.
|
||||
let grep_pattern = format!("(storkit|story-kit): merge {num_str}_");
|
||||
let grep_pattern = format!("(huskies|storkit|story-kit): merge {num_str}_");
|
||||
let output = Command::new("git")
|
||||
.args([
|
||||
"log",
|
||||
@@ -116,7 +116,7 @@ fn find_story_name(root: &std::path::Path, num_str: &str) -> Option<String> {
|
||||
"6_archived",
|
||||
];
|
||||
for stage in &stages {
|
||||
let dir = root.join(".storkit").join("work").join(stage);
|
||||
let dir = root.join(".huskies").join("work").join(stage);
|
||||
if !dir.exists() {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ fn wizard_status_reply(ctx: &CommandContext) -> String {
|
||||
match WizardState::load(ctx.project_root) {
|
||||
Some(state) => format_wizard_state(&state),
|
||||
None => {
|
||||
"No setup wizard active. Run `storkit init` in the project root to begin.".to_string()
|
||||
"No setup wizard active. Run `huskies init` in the project root to begin.".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -215,13 +215,13 @@ mod tests {
|
||||
let rooms = Arc::new(Mutex::new(HashSet::new()));
|
||||
let ctx = make_ctx("", dir.path(), &agents, &rooms);
|
||||
let result = handle_setup(&ctx).unwrap();
|
||||
assert!(result.contains("storkit init"));
|
||||
assert!(result.contains("huskies init"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn setup_with_wizard_shows_status() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
std::fs::create_dir_all(dir.path().join(".storkit")).unwrap();
|
||||
std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
|
||||
WizardState::init_if_missing(dir.path());
|
||||
let agents = Arc::new(crate::agents::AgentPool::new_test(4001));
|
||||
let rooms = Arc::new(Mutex::new(HashSet::new()));
|
||||
@@ -233,7 +233,7 @@ mod tests {
|
||||
#[test]
|
||||
fn setup_skip_advances_wizard() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
std::fs::create_dir_all(dir.path().join(".storkit")).unwrap();
|
||||
std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
|
||||
WizardState::init_if_missing(dir.path());
|
||||
let agents = Arc::new(crate::agents::AgentPool::new_test(4002));
|
||||
let rooms = Arc::new(Mutex::new(HashSet::new()));
|
||||
@@ -247,7 +247,7 @@ mod tests {
|
||||
#[test]
|
||||
fn setup_confirm_advances_wizard() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
std::fs::create_dir_all(dir.path().join(".storkit")).unwrap();
|
||||
std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
|
||||
WizardState::init_if_missing(dir.path());
|
||||
let agents = Arc::new(crate::agents::AgentPool::new_test(4003));
|
||||
let rooms = Arc::new(Mutex::new(HashSet::new()));
|
||||
@@ -261,7 +261,7 @@ mod tests {
|
||||
#[test]
|
||||
fn setup_retry_resets_step() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
std::fs::create_dir_all(dir.path().join(".storkit")).unwrap();
|
||||
std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
|
||||
WizardState::init_if_missing(dir.path());
|
||||
// Stage some content first.
|
||||
{
|
||||
@@ -299,7 +299,7 @@ mod tests {
|
||||
#[test]
|
||||
fn setup_generate_marks_generating_and_returns_hint() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
std::fs::create_dir_all(dir.path().join(".storkit")).unwrap();
|
||||
std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
|
||||
WizardState::init_if_missing(dir.path());
|
||||
let agents = Arc::new(crate::agents::AgentPool::new_test(4006));
|
||||
let rooms = Arc::new(Mutex::new(HashSet::new()));
|
||||
@@ -317,7 +317,7 @@ mod tests {
|
||||
fn setup_generate_bare_project_asks_user() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
// Bare project — only scaffolding files
|
||||
std::fs::create_dir_all(dir.path().join(".storkit")).unwrap();
|
||||
std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
|
||||
WizardState::init_if_missing(dir.path());
|
||||
let agents = Arc::new(crate::agents::AgentPool::new_test(4007));
|
||||
let rooms = Arc::new(Mutex::new(HashSet::new()));
|
||||
|
||||
@@ -34,7 +34,7 @@ pub(super) fn handle_show(ctx: &CommandContext) -> Option<String> {
|
||||
for stage in &stages {
|
||||
let dir = ctx
|
||||
.project_root
|
||||
.join(".storkit")
|
||||
.join(".huskies")
|
||||
.join("work")
|
||||
.join(stage);
|
||||
if !dir.exists() {
|
||||
|
||||
@@ -54,7 +54,7 @@ pub(super) fn story_short_label(stem: &str, name: Option<&str>) -> String {
|
||||
/// Returns `true` when the story has `blocked: true` set (retry limit reached).
|
||||
fn read_story_blocked(project_root: &std::path::Path, stage_dir: &str, stem: &str) -> bool {
|
||||
let path = project_root
|
||||
.join(".storkit")
|
||||
.join(".huskies")
|
||||
.join("work")
|
||||
.join(stage_dir)
|
||||
.join(format!("{stem}.md"));
|
||||
@@ -93,7 +93,7 @@ fn read_stage_items(
|
||||
stage_dir: &str,
|
||||
) -> Vec<(String, Option<String>)> {
|
||||
let dir = project_root
|
||||
.join(".storkit")
|
||||
.join(".huskies")
|
||||
.join("work")
|
||||
.join(stage_dir);
|
||||
if !dir.exists() {
|
||||
@@ -346,7 +346,7 @@ mod tests {
|
||||
use tempfile::TempDir;
|
||||
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let stage_dir = tmp.path().join(".storkit/work/2_current");
|
||||
let stage_dir = tmp.path().join(".huskies/work/2_current");
|
||||
std::fs::create_dir_all(&stage_dir).unwrap();
|
||||
|
||||
// Write a story file with a front-matter name
|
||||
@@ -375,7 +375,7 @@ mod tests {
|
||||
use tempfile::TempDir;
|
||||
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let stage_dir = tmp.path().join(".storkit/work/2_current");
|
||||
let stage_dir = tmp.path().join(".huskies/work/2_current");
|
||||
std::fs::create_dir_all(&stage_dir).unwrap();
|
||||
|
||||
let story_path = stage_dir.join("293_story_register_all_bot_commands.md");
|
||||
@@ -413,7 +413,7 @@ mod tests {
|
||||
use tempfile::TempDir;
|
||||
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let stage_dir = tmp.path().join(".storkit/work/2_current");
|
||||
let stage_dir = tmp.path().join(".huskies/work/2_current");
|
||||
std::fs::create_dir_all(&stage_dir).unwrap();
|
||||
|
||||
let story_path = stage_dir.join("293_story_register_all_bot_commands.md");
|
||||
@@ -436,7 +436,7 @@ mod tests {
|
||||
use tempfile::TempDir;
|
||||
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let stage_dir = tmp.path().join(".storkit/work/2_current");
|
||||
let stage_dir = tmp.path().join(".huskies/work/2_current");
|
||||
std::fs::create_dir_all(&stage_dir).unwrap();
|
||||
|
||||
let story_path = stage_dir.join("293_story_register_all_bot_commands.md");
|
||||
@@ -480,7 +480,7 @@ mod tests {
|
||||
use tempfile::TempDir;
|
||||
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let stage_dir = tmp.path().join(".storkit/work/2_current");
|
||||
let stage_dir = tmp.path().join(".huskies/work/2_current");
|
||||
std::fs::create_dir_all(&stage_dir).unwrap();
|
||||
|
||||
let story_path = stage_dir.join("42_story_idle.md");
|
||||
@@ -502,7 +502,7 @@ mod tests {
|
||||
use tempfile::TempDir;
|
||||
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let stage_dir = tmp.path().join(".storkit/work/2_current");
|
||||
let stage_dir = tmp.path().join(".huskies/work/2_current");
|
||||
std::fs::create_dir_all(&stage_dir).unwrap();
|
||||
|
||||
let story_path = stage_dir.join("42_story_blocked.md");
|
||||
@@ -524,7 +524,7 @@ mod tests {
|
||||
use tempfile::TempDir;
|
||||
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let stage_dir = tmp.path().join(".storkit/work/2_current");
|
||||
let stage_dir = tmp.path().join(".huskies/work/2_current");
|
||||
std::fs::create_dir_all(&stage_dir).unwrap();
|
||||
|
||||
let story_path = stage_dir.join("42_story_idle.md");
|
||||
@@ -578,7 +578,7 @@ mod tests {
|
||||
fn read_story_blocked_returns_true_when_blocked() {
|
||||
use tempfile::TempDir;
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let stage_dir = tmp.path().join(".storkit/work/2_current");
|
||||
let stage_dir = tmp.path().join(".huskies/work/2_current");
|
||||
std::fs::create_dir_all(&stage_dir).unwrap();
|
||||
std::fs::write(
|
||||
stage_dir.join("42_story_foo.md"),
|
||||
@@ -592,7 +592,7 @@ mod tests {
|
||||
fn read_story_blocked_returns_false_when_not_blocked() {
|
||||
use tempfile::TempDir;
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let stage_dir = tmp.path().join(".storkit/work/2_current");
|
||||
let stage_dir = tmp.path().join(".huskies/work/2_current");
|
||||
std::fs::create_dir_all(&stage_dir).unwrap();
|
||||
std::fs::write(
|
||||
stage_dir.join("42_story_foo.md"),
|
||||
@@ -610,7 +610,7 @@ mod tests {
|
||||
use tempfile::TempDir;
|
||||
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let stage_dir = tmp.path().join(".storkit/work/2_current");
|
||||
let stage_dir = tmp.path().join(".huskies/work/2_current");
|
||||
std::fs::create_dir_all(&stage_dir).unwrap();
|
||||
|
||||
let story_path = stage_dir.join("42_story_idle.md");
|
||||
@@ -632,7 +632,7 @@ mod tests {
|
||||
use tempfile::TempDir;
|
||||
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let stage_dir = tmp.path().join(".storkit/work/2_current");
|
||||
let stage_dir = tmp.path().join(".huskies/work/2_current");
|
||||
std::fs::create_dir_all(&stage_dir).unwrap();
|
||||
|
||||
let story_path = stage_dir.join("42_story_blocked.md");
|
||||
|
||||
@@ -28,7 +28,7 @@ pub(super) fn handle_triage(ctx: &CommandContext) -> Option<String> {
|
||||
|
||||
let current_dir = ctx
|
||||
.project_root
|
||||
.join(".storkit")
|
||||
.join(".huskies")
|
||||
.join("work")
|
||||
.join("2_current");
|
||||
|
||||
@@ -179,7 +179,7 @@ fn build_triage_dump(
|
||||
// ---- Agent log tail ----
|
||||
let log_dir = ctx
|
||||
.project_root
|
||||
.join(".storkit")
|
||||
.join(".huskies")
|
||||
.join("logs")
|
||||
.join(story_id);
|
||||
match latest_log_file(&log_dir) {
|
||||
|
||||
@@ -45,7 +45,7 @@ pub(crate) fn unblock_by_number(project_root: &Path, story_number: &str) -> Stri
|
||||
let mut found: Option<(std::path::PathBuf, String)> = None;
|
||||
|
||||
'outer: for stage_dir in SEARCH_DIRS {
|
||||
let dir = project_root.join(".storkit").join("work").join(stage_dir);
|
||||
let dir = project_root.join(".huskies").join("work").join(stage_dir);
|
||||
if !dir.exists() {
|
||||
continue;
|
||||
}
|
||||
@@ -252,7 +252,7 @@ mod tests {
|
||||
);
|
||||
|
||||
let contents = std::fs::read_to_string(
|
||||
tmp.path().join(".storkit/work/2_current/7_story_stuck.md"),
|
||||
tmp.path().join(".huskies/work/2_current/7_story_stuck.md"),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
|
||||
@@ -77,7 +77,7 @@ fn find_last_release_tag(root: &std::path::Path) -> Option<String> {
|
||||
if tag.is_empty() { None } else { Some(tag) }
|
||||
}
|
||||
|
||||
/// Return the subjects of all `storkit: merge …` commits reachable from HEAD
|
||||
/// Return the subjects of all `huskies: merge …` commits reachable from HEAD
|
||||
/// but not from `since_tag` (or all commits when `since_tag` is `None`).
|
||||
fn list_merge_commits_since(
|
||||
root: &std::path::Path,
|
||||
@@ -97,7 +97,7 @@ fn list_merge_commits_since(
|
||||
"--format=%s",
|
||||
"--extended-regexp",
|
||||
"--grep",
|
||||
"(storkit|story-kit): merge [0-9]+_",
|
||||
"(huskies|storkit|story-kit): merge [0-9]+_",
|
||||
])
|
||||
.current_dir(root)
|
||||
.output()
|
||||
@@ -115,14 +115,14 @@ fn list_merge_commits_since(
|
||||
}
|
||||
|
||||
/// Parse a story number and slug from a merge commit subject like
|
||||
/// `storkit: merge 386_story_unreleased_command`.
|
||||
/// `huskies: merge 386_story_unreleased_command`.
|
||||
///
|
||||
/// Returns `(story_number, slug_remainder)` or `None` if the subject doesn't
|
||||
/// match the expected pattern.
|
||||
fn parse_story_from_subject(subject: &str) -> Option<(u64, String)> {
|
||||
// Match "storkit: merge NNN_rest" or "story-kit: merge NNN_rest"
|
||||
// Match "huskies: merge NNN_rest" or "story-kit: merge NNN_rest"
|
||||
let rest = subject
|
||||
.strip_prefix("storkit: merge ")
|
||||
.strip_prefix("huskies: merge ")
|
||||
.or_else(|| subject.strip_prefix("story-kit: merge "))?;
|
||||
|
||||
let (num_str, slug) = rest.split_once('_')?;
|
||||
@@ -159,7 +159,7 @@ fn find_story_name(root: &std::path::Path, num_str: &str) -> Option<String> {
|
||||
"6_archived",
|
||||
];
|
||||
for stage in STAGES {
|
||||
let dir = root.join(".storkit").join("work").join(stage);
|
||||
let dir = root.join(".huskies").join("work").join(stage);
|
||||
if !dir.exists() {
|
||||
continue;
|
||||
}
|
||||
@@ -271,8 +271,8 @@ mod tests {
|
||||
// -- parse_story_from_subject ------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn parse_story_storkit_prefix() {
|
||||
let result = parse_story_from_subject("storkit: merge 386_story_unreleased_command");
|
||||
fn parse_story_huskies_prefix() {
|
||||
let result = parse_story_from_subject("huskies: merge 386_story_unreleased_command");
|
||||
assert_eq!(result, Some((386, "story_unreleased_command".to_string())));
|
||||
}
|
||||
|
||||
@@ -290,7 +290,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parse_story_no_underscore_after_number() {
|
||||
let result = parse_story_from_subject("storkit: merge 123");
|
||||
let result = parse_story_from_subject("huskies: merge 123");
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,10 +6,10 @@ use std::path::Path;
|
||||
|
||||
/// Write a work-item file into the standard pipeline directory structure.
|
||||
///
|
||||
/// Creates `.storkit/work/{stage}/{filename}` under `root`, creating any
|
||||
/// Creates `.huskies/work/{stage}/{filename}` under `root`, creating any
|
||||
/// missing parent directories.
|
||||
pub(crate) fn write_story_file(root: &Path, stage: &str, filename: &str, content: &str) {
|
||||
let dir = root.join(".storkit/work").join(stage);
|
||||
let dir = root.join(".huskies/work").join(stage);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
std::fs::write(dir.join(filename), content).unwrap();
|
||||
}
|
||||
|
||||
+12
-12
@@ -1,6 +1,6 @@
|
||||
//! Deferred agent start via one-shot timers.
|
||||
//!
|
||||
//! Provides [`TimerStore`] for persisting timers to `.storkit/timers.json`,
|
||||
//! Provides [`TimerStore`] for persisting timers to `.huskies/timers.json`,
|
||||
//! a 30-second tick loop ([`spawn_timer_tick_loop`]) that fires due timers,
|
||||
//! and command parsing / handling for the `timer` bot command.
|
||||
|
||||
@@ -398,7 +398,7 @@ pub async fn handle_timer_command(
|
||||
|
||||
// The story must be in backlog or current. When the timer fires,
|
||||
// backlog stories are moved to current automatically.
|
||||
let work_dir = project_root.join(".storkit").join("work");
|
||||
let work_dir = project_root.join(".huskies").join("work");
|
||||
let in_backlog = work_dir.join("1_backlog").join(format!("{story_id}.md")).exists();
|
||||
let in_current = work_dir.join("2_current").join(format!("{story_id}.md")).exists();
|
||||
if !in_backlog && !in_current {
|
||||
@@ -555,7 +555,7 @@ fn resolve_story_id(number_or_id: &str, project_root: &Path) -> Option<String> {
|
||||
}
|
||||
|
||||
for stage in STAGES {
|
||||
let dir = project_root.join(".storkit").join("work").join(stage);
|
||||
let dir = project_root.join(".huskies").join("work").join(stage);
|
||||
if !dir.exists() {
|
||||
continue;
|
||||
}
|
||||
@@ -931,8 +931,8 @@ mod tests {
|
||||
async fn handle_schedule_story_not_in_backlog_or_current() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
// Set up directory structure with no story in backlog or current
|
||||
std::fs::create_dir_all(dir.path().join(".storkit/work/1_backlog")).unwrap();
|
||||
std::fs::create_dir_all(dir.path().join(".storkit/work/2_current")).unwrap();
|
||||
std::fs::create_dir_all(dir.path().join(".huskies/work/1_backlog")).unwrap();
|
||||
std::fs::create_dir_all(dir.path().join(".huskies/work/2_current")).unwrap();
|
||||
let store = TimerStore::load(dir.path().join("timers.json"));
|
||||
let result = handle_timer_command(
|
||||
TimerCommand::Schedule {
|
||||
@@ -952,7 +952,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn handle_schedule_accepts_backlog_story() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let backlog_dir = dir.path().join(".storkit/work/1_backlog");
|
||||
let backlog_dir = dir.path().join(".huskies/work/1_backlog");
|
||||
std::fs::create_dir_all(&backlog_dir).unwrap();
|
||||
std::fs::write(
|
||||
backlog_dir.join("421_story_foo.md"),
|
||||
@@ -978,7 +978,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn handle_schedule_success() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let current_dir = dir.path().join(".storkit/work/2_current");
|
||||
let current_dir = dir.path().join(".huskies/work/2_current");
|
||||
std::fs::create_dir_all(¤t_dir).unwrap();
|
||||
std::fs::write(current_dir.join("421_story_foo.md"), "---\nname: Foo\n---").unwrap();
|
||||
let store = TimerStore::load(dir.path().join("timers.json"));
|
||||
@@ -1001,7 +1001,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn handle_schedule_invalid_time() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let current_dir = dir.path().join(".storkit/work/2_current");
|
||||
let current_dir = dir.path().join(".huskies/work/2_current");
|
||||
std::fs::create_dir_all(¤t_dir).unwrap();
|
||||
std::fs::write(current_dir.join("421_story_foo.md"), "---\nname: Foo\n---").unwrap();
|
||||
let store = TimerStore::load(dir.path().join("timers.json"));
|
||||
@@ -1058,8 +1058,8 @@ mod tests {
|
||||
|
||||
let dir = TempDir::new().unwrap();
|
||||
let root = dir.path();
|
||||
let backlog = root.join(".storkit/work/1_backlog");
|
||||
let current = root.join(".storkit/work/2_current");
|
||||
let backlog = root.join(".huskies/work/1_backlog");
|
||||
let current = root.join(".huskies/work/2_current");
|
||||
fs::create_dir_all(&backlog).unwrap();
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(backlog.join("421_story_foo.md"), "---\nname: Foo\n---\n").unwrap();
|
||||
@@ -1103,8 +1103,8 @@ mod tests {
|
||||
|
||||
let dir = TempDir::new().unwrap();
|
||||
let root = dir.path();
|
||||
let backlog = root.join(".storkit/work/1_backlog");
|
||||
let current = root.join(".storkit/work/2_current");
|
||||
let backlog = root.join(".huskies/work/1_backlog");
|
||||
let current = root.join(".huskies/work/2_current");
|
||||
fs::create_dir_all(&backlog).unwrap();
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(
|
||||
|
||||
@@ -104,7 +104,7 @@ pub async fn handle_assign(
|
||||
// Find the story file across all pipeline stages.
|
||||
let mut found: Option<(std::path::PathBuf, String)> = None;
|
||||
'outer: for stage in STAGES {
|
||||
let dir = project_root.join(".storkit").join("work").join(stage);
|
||||
let dir = project_root.join(".huskies").join("work").join(stage);
|
||||
if !dir.exists() {
|
||||
continue;
|
||||
}
|
||||
@@ -356,7 +356,7 @@ mod tests {
|
||||
async fn handle_assign_returns_not_found_for_unknown_number() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
for stage in STAGES {
|
||||
std::fs::create_dir_all(tmp.path().join(".storkit/work").join(stage)).unwrap();
|
||||
std::fs::create_dir_all(tmp.path().join(".huskies/work").join(stage)).unwrap();
|
||||
}
|
||||
let agents = std::sync::Arc::new(AgentPool::new_test(3000));
|
||||
let response = handle_assign("Timmy", "999", "opus", tmp.path(), &agents).await;
|
||||
@@ -394,7 +394,7 @@ mod tests {
|
||||
);
|
||||
|
||||
let contents = std::fs::read_to_string(
|
||||
tmp.path().join(".storkit/work/1_backlog/42_story_test.md"),
|
||||
tmp.path().join(".huskies/work/1_backlog/42_story_test.md"),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
@@ -426,7 +426,7 @@ mod tests {
|
||||
);
|
||||
|
||||
let contents = std::fs::read_to_string(
|
||||
tmp.path().join(".storkit/work/1_backlog/7_story_small.md"),
|
||||
tmp.path().join(".huskies/work/1_backlog/7_story_small.md"),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
@@ -449,7 +449,7 @@ mod tests {
|
||||
handle_assign("Timmy", "5", "opus", tmp.path(), &agents).await;
|
||||
|
||||
let contents = std::fs::read_to_string(
|
||||
tmp.path().join(".storkit/work/1_backlog/5_story_existing.md"),
|
||||
tmp.path().join(".huskies/work/1_backlog/5_story_existing.md"),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
|
||||
@@ -51,7 +51,7 @@ pub(super) struct PersistedHistory {
|
||||
}
|
||||
|
||||
/// Path to the persisted conversation history file relative to project root.
|
||||
pub(super) const HISTORY_FILE: &str = ".storkit/matrix_history.json";
|
||||
pub(super) const HISTORY_FILE: &str = ".huskies/matrix_history.json";
|
||||
|
||||
/// Load conversation history from disk, returning an empty map on any error.
|
||||
pub fn load_history(project_root: &std::path::Path) -> HashMap<OwnedRoomId, RoomConversation> {
|
||||
@@ -197,7 +197,7 @@ mod tests {
|
||||
#[test]
|
||||
fn save_and_load_history_round_trip() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let story_kit_dir = dir.path().join(".storkit");
|
||||
let story_kit_dir = dir.path().join(".huskies");
|
||||
std::fs::create_dir_all(&story_kit_dir).unwrap();
|
||||
|
||||
let room_id: OwnedRoomId = "!persist:example.com".parse().unwrap();
|
||||
@@ -238,7 +238,7 @@ mod tests {
|
||||
#[test]
|
||||
fn load_history_returns_empty_on_corrupt_file() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let story_kit_dir = dir.path().join(".storkit");
|
||||
let story_kit_dir = dir.path().join(".huskies");
|
||||
std::fs::create_dir_all(&story_kit_dir).unwrap();
|
||||
std::fs::write(dir.path().join(HISTORY_FILE), "not valid json").unwrap();
|
||||
let loaded = load_history(dir.path());
|
||||
|
||||
@@ -27,7 +27,7 @@ pub async fn run_bot(
|
||||
agents: Arc<AgentPool>,
|
||||
shutdown_rx: watch::Receiver<Option<crate::rebuild::ShutdownReason>>,
|
||||
) -> Result<(), String> {
|
||||
let store_path = project_root.join(".storkit").join("matrix_store");
|
||||
let store_path = project_root.join(".huskies").join("matrix_store");
|
||||
let client = Client::builder()
|
||||
.homeserver_url(config.homeserver.as_deref().unwrap_or_default())
|
||||
.sqlite_store(&store_path, None)
|
||||
@@ -36,7 +36,7 @@ pub async fn run_bot(
|
||||
.map_err(|e| format!("Failed to build Matrix client: {e}"))?;
|
||||
|
||||
// Persist device ID so E2EE crypto state survives restarts.
|
||||
let device_id_path = project_root.join(".storkit").join("matrix_device_id");
|
||||
let device_id_path = project_root.join(".huskies").join("matrix_device_id");
|
||||
let saved_device_id: Option<String> = std::fs::read_to_string(&device_id_path)
|
||||
.ok()
|
||||
.map(|s| s.trim().to_string())
|
||||
@@ -48,7 +48,7 @@ pub async fn run_bot(
|
||||
config.username.as_deref().unwrap_or_default(),
|
||||
config.password.as_deref().unwrap_or_default(),
|
||||
)
|
||||
.initial_device_display_name("Storkit Bot");
|
||||
.initial_device_display_name("Huskies Bot");
|
||||
|
||||
if let Some(ref device_id) = saved_device_id {
|
||||
login_builder = login_builder.device_id(device_id);
|
||||
@@ -218,7 +218,7 @@ pub async fn run_bot(
|
||||
let announce_bot_name = bot_name.clone();
|
||||
|
||||
let timer_store = Arc::new(crate::chat::timer::TimerStore::load(
|
||||
project_root.join(".storkit").join("timers.json"),
|
||||
project_root.join(".huskies").join("timers.json"),
|
||||
));
|
||||
crate::chat::timer::spawn_timer_tick_loop(
|
||||
Arc::clone(&timer_store),
|
||||
|
||||
@@ -9,7 +9,7 @@ fn default_permission_timeout_secs() -> u64 {
|
||||
120
|
||||
}
|
||||
|
||||
/// Configuration for the Matrix bot, read from `.storkit/bot.toml`.
|
||||
/// Configuration for the Matrix bot, read from `.huskies/bot.toml`.
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
pub struct BotConfig {
|
||||
/// Matrix homeserver URL, e.g. `https://matrix.example.com`
|
||||
@@ -145,12 +145,12 @@ fn default_whatsapp_provider() -> String {
|
||||
}
|
||||
|
||||
impl BotConfig {
|
||||
/// Load bot configuration from `.storkit/bot.toml`.
|
||||
/// Load bot configuration from `.huskies/bot.toml`.
|
||||
///
|
||||
/// Returns `None` if the file does not exist, fails to parse, has
|
||||
/// `enabled = false`, or specifies no room IDs.
|
||||
pub fn load(project_root: &Path) -> Option<Self> {
|
||||
let path = project_root.join(".storkit").join("bot.toml");
|
||||
let path = project_root.join(".huskies").join("bot.toml");
|
||||
if !path.exists() {
|
||||
return None;
|
||||
}
|
||||
@@ -285,7 +285,7 @@ impl BotConfig {
|
||||
/// array, and writes the result back. Errors are logged but not propagated
|
||||
/// so a persistence failure never interrupts the bot's message handling.
|
||||
pub fn save_ambient_rooms(project_root: &Path, room_ids: &[String]) {
|
||||
let path = project_root.join(".storkit").join("bot.toml");
|
||||
let path = project_root.join(".huskies").join("bot.toml");
|
||||
let content = match std::fs::read_to_string(&path) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
@@ -334,7 +334,7 @@ mod tests {
|
||||
#[test]
|
||||
fn load_returns_none_when_disabled() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
@@ -354,7 +354,7 @@ enabled = false
|
||||
#[test]
|
||||
fn load_returns_config_when_enabled_with_room_ids() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
@@ -382,7 +382,7 @@ enabled = true
|
||||
#[test]
|
||||
fn load_merges_deprecated_room_id_into_room_ids() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
// Old-style single room_id key — should still work.
|
||||
fs::write(
|
||||
@@ -403,7 +403,7 @@ enabled = true
|
||||
#[test]
|
||||
fn load_returns_none_when_no_room_ids() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
@@ -422,7 +422,7 @@ enabled = true
|
||||
#[test]
|
||||
fn load_returns_none_when_toml_invalid() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(sk.join("bot.toml"), "not valid toml {{{").unwrap();
|
||||
let result = BotConfig::load(tmp.path());
|
||||
@@ -432,7 +432,7 @@ enabled = true
|
||||
#[test]
|
||||
fn load_respects_optional_model() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
@@ -453,7 +453,7 @@ model = "claude-sonnet-4-6"
|
||||
#[test]
|
||||
fn load_uses_default_history_size() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
@@ -473,7 +473,7 @@ enabled = true
|
||||
#[test]
|
||||
fn load_respects_custom_history_size() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
@@ -494,7 +494,7 @@ history_size = 50
|
||||
#[test]
|
||||
fn load_reads_display_name() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
@@ -515,7 +515,7 @@ display_name = "Timmy"
|
||||
#[test]
|
||||
fn load_display_name_defaults_to_none_when_absent() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
@@ -535,7 +535,7 @@ enabled = true
|
||||
#[test]
|
||||
fn load_uses_default_permission_timeout() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
@@ -555,7 +555,7 @@ enabled = true
|
||||
#[test]
|
||||
fn load_respects_custom_permission_timeout() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
@@ -579,7 +579,7 @@ permission_timeout_secs = 60
|
||||
// must parse successfully — the field is simply ignored now that
|
||||
// verification is always enforced unconditionally.
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
@@ -604,7 +604,7 @@ require_verified_devices = true
|
||||
#[test]
|
||||
fn load_reads_ambient_rooms() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
@@ -625,7 +625,7 @@ ambient_rooms = ["!abc:example.com"]
|
||||
#[test]
|
||||
fn load_ambient_rooms_defaults_to_empty_when_absent() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
@@ -645,7 +645,7 @@ enabled = true
|
||||
#[test]
|
||||
fn save_ambient_rooms_persists_to_bot_toml() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
@@ -667,7 +667,7 @@ enabled = true
|
||||
#[test]
|
||||
fn save_ambient_rooms_clears_when_empty() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
@@ -690,7 +690,7 @@ ambient_rooms = ["!abc:example.com"]
|
||||
#[test]
|
||||
fn load_transport_defaults_to_matrix() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
@@ -710,7 +710,7 @@ enabled = true
|
||||
#[test]
|
||||
fn load_transport_reads_custom_value() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
@@ -746,7 +746,7 @@ whatsapp_verify_token = "my-verify"
|
||||
#[test]
|
||||
fn load_whatsapp_returns_none_when_missing_phone_number_id() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
@@ -767,7 +767,7 @@ whatsapp_verify_token = "my-verify"
|
||||
#[test]
|
||||
fn load_whatsapp_returns_none_when_missing_access_token() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
@@ -788,7 +788,7 @@ whatsapp_verify_token = "my-verify"
|
||||
#[test]
|
||||
fn load_whatsapp_returns_none_when_missing_verify_token() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
@@ -811,7 +811,7 @@ whatsapp_access_token = "EAAtoken"
|
||||
#[test]
|
||||
fn load_twilio_whatsapp_reads_config() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
@@ -842,7 +842,7 @@ twilio_whatsapp_number = "+14155551234"
|
||||
#[test]
|
||||
fn load_whatsapp_provider_defaults_to_meta() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
@@ -865,7 +865,7 @@ whatsapp_verify_token = "my-verify"
|
||||
#[test]
|
||||
fn load_twilio_returns_none_when_missing_account_sid() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
@@ -887,7 +887,7 @@ twilio_whatsapp_number = "+14155551234"
|
||||
#[test]
|
||||
fn load_twilio_returns_none_when_missing_auth_token() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
@@ -909,7 +909,7 @@ twilio_whatsapp_number = "+14155551234"
|
||||
#[test]
|
||||
fn load_twilio_returns_none_when_missing_whatsapp_number() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
@@ -933,7 +933,7 @@ twilio_auth_token = "authtest"
|
||||
#[test]
|
||||
fn load_slack_transport_reads_config() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
@@ -959,7 +959,7 @@ slack_channel_ids = ["C01ABCDEF"]
|
||||
#[test]
|
||||
fn load_slack_returns_none_when_missing_bot_token() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
@@ -980,7 +980,7 @@ slack_channel_ids = ["C01ABCDEF"]
|
||||
#[test]
|
||||
fn load_slack_returns_none_when_missing_signing_secret() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
@@ -1001,7 +1001,7 @@ slack_channel_ids = ["C01ABCDEF"]
|
||||
#[test]
|
||||
fn load_slack_returns_none_when_missing_channel_ids() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
|
||||
@@ -72,7 +72,7 @@ pub async fn handle_delete(
|
||||
// Find the story file across all pipeline stages.
|
||||
let mut found: Option<(std::path::PathBuf, &str, String)> = None; // (path, stage, story_id)
|
||||
'outer: for stage in STAGES {
|
||||
let dir = project_root.join(".storkit").join("work").join(stage);
|
||||
let dir = project_root.join(".huskies").join("work").join(stage);
|
||||
if !dir.exists() {
|
||||
continue;
|
||||
}
|
||||
@@ -148,8 +148,8 @@ pub async fn handle_delete(
|
||||
}
|
||||
|
||||
// Commit the deletion to git.
|
||||
let commit_msg = format!("storkit: delete {story_id}");
|
||||
let work_rel = std::path::PathBuf::from(".storkit").join("work");
|
||||
let commit_msg = format!("huskies: delete {story_id}");
|
||||
let work_rel = std::path::PathBuf::from(".huskies").join("work");
|
||||
let _ = std::process::Command::new("git")
|
||||
.args(["add", "-A"])
|
||||
.arg(&work_rel)
|
||||
@@ -288,7 +288,7 @@ mod tests {
|
||||
"5_done",
|
||||
"6_archived",
|
||||
] {
|
||||
std::fs::create_dir_all(project_root.join(".storkit").join("work").join(stage))
|
||||
std::fs::create_dir_all(project_root.join(".huskies").join("work").join(stage))
|
||||
.unwrap();
|
||||
}
|
||||
let agents = std::sync::Arc::new(crate::agents::AgentPool::new_test(3000));
|
||||
@@ -321,7 +321,7 @@ mod tests {
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
let backlog_dir = project_root.join(".storkit").join("work").join("1_backlog");
|
||||
let backlog_dir = project_root.join(".huskies").join("work").join("1_backlog");
|
||||
std::fs::create_dir_all(&backlog_dir).unwrap();
|
||||
let story_path = backlog_dir.join("42_story_some_feature.md");
|
||||
std::fs::write(&story_path, "---\nname: Some Feature\n---\n\n# Story 42\n").unwrap();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Matrix bot integration for Story Kit.
|
||||
//!
|
||||
//! When a `.storkit/bot.toml` file is present with `enabled = true`, the
|
||||
//! When a `.huskies/bot.toml` file is present with `enabled = true`, the
|
||||
//! server spawns a Matrix bot that:
|
||||
//!
|
||||
//! 1. Connects to the configured homeserver and joins the configured room.
|
||||
@@ -41,7 +41,7 @@ use tokio::sync::{Mutex as TokioMutex, broadcast, mpsc, watch};
|
||||
|
||||
/// Attempt to start the Matrix bot.
|
||||
///
|
||||
/// Reads the bot configuration from `.storkit/bot.toml`. If the file is
|
||||
/// Reads the bot configuration from `.huskies/bot.toml`. If the file is
|
||||
/// absent or `enabled = false`, this function returns immediately without
|
||||
/// spawning anything — the server continues normally.
|
||||
///
|
||||
|
||||
@@ -55,7 +55,7 @@ pub fn extract_story_number(item_id: &str) -> Option<&str> {
|
||||
/// Returns `None` if the file doesn't exist or has no parseable name.
|
||||
pub fn read_story_name(project_root: &Path, stage: &str, item_id: &str) -> Option<String> {
|
||||
let path = project_root
|
||||
.join(".storkit")
|
||||
.join(".huskies")
|
||||
.join("work")
|
||||
.join(stage)
|
||||
.join(format!("{item_id}.md"));
|
||||
@@ -507,7 +507,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn rate_limit_warning_sends_notification_with_agent_and_story() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let stage_dir = tmp.path().join(".storkit").join("work").join("2_current");
|
||||
let stage_dir = tmp.path().join(".huskies").join("work").join("2_current");
|
||||
std::fs::create_dir_all(&stage_dir).unwrap();
|
||||
std::fs::write(
|
||||
stage_dir.join("365_story_rate_limit.md"),
|
||||
@@ -613,7 +613,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn stage_notification_uses_dynamic_room_ids() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let stage_dir = tmp.path().join(".storkit").join("work").join("3_qa");
|
||||
let stage_dir = tmp.path().join(".huskies").join("work").join("3_qa");
|
||||
std::fs::create_dir_all(&stage_dir).unwrap();
|
||||
std::fs::write(
|
||||
stage_dir.join("10_story_foo.md"),
|
||||
@@ -642,7 +642,7 @@ mod tests {
|
||||
stage: "3_qa".to_string(),
|
||||
item_id: "10_story_foo".to_string(),
|
||||
action: "qa".to_string(),
|
||||
commit_msg: "storkit: qa 10_story_foo".to_string(),
|
||||
commit_msg: "huskies: qa 10_story_foo".to_string(),
|
||||
from_stage: None,
|
||||
}).unwrap();
|
||||
|
||||
@@ -677,7 +677,7 @@ mod tests {
|
||||
stage: "3_qa".to_string(),
|
||||
item_id: "10_story_foo".to_string(),
|
||||
action: "qa".to_string(),
|
||||
commit_msg: "storkit: qa 10_story_foo".to_string(),
|
||||
commit_msg: "huskies: qa 10_story_foo".to_string(),
|
||||
from_stage: None,
|
||||
}).unwrap();
|
||||
|
||||
@@ -746,7 +746,7 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let stage_dir = tmp
|
||||
.path()
|
||||
.join(".storkit")
|
||||
.join(".huskies")
|
||||
.join("work")
|
||||
.join("2_current");
|
||||
std::fs::create_dir_all(&stage_dir).unwrap();
|
||||
@@ -772,7 +772,7 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let stage_dir = tmp
|
||||
.path()
|
||||
.join(".storkit")
|
||||
.join(".huskies")
|
||||
.join("work")
|
||||
.join("2_current");
|
||||
std::fs::create_dir_all(&stage_dir).unwrap();
|
||||
@@ -858,7 +858,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn story_blocked_sends_notification_with_reason() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let stage_dir = tmp.path().join(".storkit").join("work").join("2_current");
|
||||
let stage_dir = tmp.path().join(".huskies").join("work").join("2_current");
|
||||
std::fs::create_dir_all(&stage_dir).unwrap();
|
||||
std::fs::write(
|
||||
stage_dir.join("425_story_blocking_test.md"),
|
||||
@@ -1033,7 +1033,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn rate_limit_warning_suppressed_when_config_false() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk_dir = tmp.path().join(".storkit");
|
||||
let sk_dir = tmp.path().join(".huskies");
|
||||
std::fs::create_dir_all(&sk_dir).unwrap();
|
||||
std::fs::write(
|
||||
sk_dir.join("project.toml"),
|
||||
@@ -1066,7 +1066,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn rate_limit_hard_block_always_sent_when_config_false() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk_dir = tmp.path().join(".storkit");
|
||||
let sk_dir = tmp.path().join(".huskies");
|
||||
std::fs::create_dir_all(&sk_dir).unwrap();
|
||||
std::fs::write(
|
||||
sk_dir.join("project.toml"),
|
||||
@@ -1101,7 +1101,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn story_blocked_always_sent_when_config_false() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk_dir = tmp.path().join(".storkit");
|
||||
let sk_dir = tmp.path().join(".huskies");
|
||||
std::fs::create_dir_all(&sk_dir).unwrap();
|
||||
std::fs::write(
|
||||
sk_dir.join("project.toml"),
|
||||
@@ -1135,7 +1135,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn rate_limit_warning_suppressed_after_hot_reload() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk_dir = tmp.path().join(".storkit");
|
||||
let sk_dir = tmp.path().join(".huskies");
|
||||
std::fs::create_dir_all(&sk_dir).unwrap();
|
||||
// Start with notifications enabled.
|
||||
std::fs::write(
|
||||
|
||||
@@ -51,7 +51,7 @@ pub fn extract_rmtree_command(
|
||||
|
||||
/// Handle an rmtree command asynchronously.
|
||||
///
|
||||
/// Finds the worktree for `story_number` under `.storkit/worktrees/`, stops any
|
||||
/// Finds the worktree for `story_number` under `.huskies/worktrees/`, stops any
|
||||
/// running agent, and removes the worktree directory and its feature branch.
|
||||
/// Returns a markdown-formatted response string.
|
||||
pub async fn handle_rmtree(
|
||||
@@ -201,7 +201,7 @@ mod tests {
|
||||
async fn handle_rmtree_returns_not_found_for_unknown_number() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let project_root = tmp.path();
|
||||
std::fs::create_dir_all(project_root.join(".storkit").join("worktrees")).unwrap();
|
||||
std::fs::create_dir_all(project_root.join(".huskies").join("worktrees")).unwrap();
|
||||
let agents = std::sync::Arc::new(crate::agents::AgentPool::new_test(3000));
|
||||
let response = handle_rmtree("Timmy", "999", project_root, &agents).await;
|
||||
assert!(
|
||||
|
||||
@@ -91,7 +91,7 @@ pub async fn handle_start(
|
||||
// Find the story file across all pipeline stages.
|
||||
let mut found: Option<(std::path::PathBuf, String)> = None; // (path, story_id)
|
||||
'outer: for stage in STAGES {
|
||||
let dir = project_root.join(".storkit").join("work").join(stage);
|
||||
let dir = project_root.join(".huskies").join("work").join(stage);
|
||||
if !dir.exists() {
|
||||
continue;
|
||||
}
|
||||
@@ -274,7 +274,7 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let project_root = tmp.path();
|
||||
for stage in &["1_backlog", "2_current", "3_qa", "4_merge", "5_done", "6_archived"] {
|
||||
std::fs::create_dir_all(project_root.join(".storkit").join("work").join(stage))
|
||||
std::fs::create_dir_all(project_root.join(".huskies").join("work").join(stage))
|
||||
.unwrap();
|
||||
}
|
||||
let agents = std::sync::Arc::new(crate::agents::AgentPool::new_test(3000));
|
||||
@@ -292,7 +292,7 @@ mod tests {
|
||||
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let project_root = tmp.path();
|
||||
let sk = project_root.join(".storkit");
|
||||
let sk = project_root.join(".huskies");
|
||||
let backlog = sk.join("work/1_backlog");
|
||||
std::fs::create_dir_all(&backlog).unwrap();
|
||||
std::fs::write(
|
||||
|
||||
@@ -22,9 +22,9 @@ use super::format::markdown_to_slack;
|
||||
/// Payload sent by Slack for slash commands (application/x-www-form-urlencoded).
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct SlackSlashCommandPayload {
|
||||
/// The slash command that was invoked (e.g. "/storkit-status").
|
||||
/// The slash command that was invoked (e.g. "/huskies-status").
|
||||
pub command: String,
|
||||
/// Any text typed after the command (e.g. "42" for "/storkit-show 42").
|
||||
/// Any text typed after the command (e.g. "42" for "/huskies-show 42").
|
||||
#[serde(default)]
|
||||
pub text: String,
|
||||
/// The user who invoked the command.
|
||||
@@ -44,12 +44,12 @@ pub(super) struct SlashCommandResponse {
|
||||
|
||||
/// Map a Slack slash command name to the corresponding bot command keyword.
|
||||
///
|
||||
/// Supported: `/storkit-status`, `/storkit-cost`, `/storkit-show`,
|
||||
/// `/storkit-git`, `/storkit-htop`.
|
||||
/// Supported: `/huskies-status`, `/huskies-cost`, `/huskies-show`,
|
||||
/// `/huskies-git`, `/huskies-htop`.
|
||||
pub(super) fn slash_command_to_bot_keyword(command: &str) -> Option<&'static str> {
|
||||
// Strip leading "/" and the "storkit-" prefix.
|
||||
// Strip leading "/" and the "huskies-" prefix.
|
||||
let name = command.strip_prefix('/').unwrap_or(command);
|
||||
let keyword = name.strip_prefix("storkit-")?;
|
||||
let keyword = name.strip_prefix("huskies-")?;
|
||||
match keyword {
|
||||
"status" => Some("status"),
|
||||
"cost" => Some("cost"),
|
||||
@@ -539,9 +539,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parse_slash_command_payload() {
|
||||
let body = "command=%2Fstorkit-status&text=&user_id=U123&channel_id=C456";
|
||||
let body = "command=%2Fhuskies-status&text=&user_id=U123&channel_id=C456";
|
||||
let payload: SlackSlashCommandPayload = serde_urlencoded::from_str(body).unwrap();
|
||||
assert_eq!(payload.command, "/storkit-status");
|
||||
assert_eq!(payload.command, "/huskies-status");
|
||||
assert_eq!(payload.text, "");
|
||||
assert_eq!(payload.user_id, "U123");
|
||||
assert_eq!(payload.channel_id, "C456");
|
||||
@@ -549,9 +549,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parse_slash_command_payload_with_text() {
|
||||
let body = "command=%2Fstorkit-show&text=42&user_id=U123&channel_id=C456";
|
||||
let body = "command=%2Fhuskies-show&text=42&user_id=U123&channel_id=C456";
|
||||
let payload: SlackSlashCommandPayload = serde_urlencoded::from_str(body).unwrap();
|
||||
assert_eq!(payload.command, "/storkit-show");
|
||||
assert_eq!(payload.command, "/huskies-show");
|
||||
assert_eq!(payload.text, "42");
|
||||
}
|
||||
|
||||
@@ -559,36 +559,36 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn slash_command_maps_status() {
|
||||
assert_eq!(slash_command_to_bot_keyword("/storkit-status"), Some("status"));
|
||||
assert_eq!(slash_command_to_bot_keyword("/huskies-status"), Some("status"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slash_command_maps_cost() {
|
||||
assert_eq!(slash_command_to_bot_keyword("/storkit-cost"), Some("cost"));
|
||||
assert_eq!(slash_command_to_bot_keyword("/huskies-cost"), Some("cost"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slash_command_maps_show() {
|
||||
assert_eq!(slash_command_to_bot_keyword("/storkit-show"), Some("show"));
|
||||
assert_eq!(slash_command_to_bot_keyword("/huskies-show"), Some("show"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slash_command_maps_git() {
|
||||
assert_eq!(slash_command_to_bot_keyword("/storkit-git"), Some("git"));
|
||||
assert_eq!(slash_command_to_bot_keyword("/huskies-git"), Some("git"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slash_command_maps_htop() {
|
||||
assert_eq!(slash_command_to_bot_keyword("/storkit-htop"), Some("htop"));
|
||||
assert_eq!(slash_command_to_bot_keyword("/huskies-htop"), Some("htop"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slash_command_unknown_returns_none() {
|
||||
assert_eq!(slash_command_to_bot_keyword("/storkit-unknown"), None);
|
||||
assert_eq!(slash_command_to_bot_keyword("/huskies-unknown"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slash_command_non_storkit_returns_none() {
|
||||
fn slash_command_non_huskies_returns_none() {
|
||||
assert_eq!(slash_command_to_bot_keyword("/other-command"), None);
|
||||
}
|
||||
|
||||
@@ -628,8 +628,8 @@ mod tests {
|
||||
let room_id = "C01ABCDEF".to_string();
|
||||
|
||||
// Simulate what slash_command_receive does: build a synthetic message.
|
||||
let bot_name = "Storkit";
|
||||
let keyword = slash_command_to_bot_keyword("/storkit-status").unwrap();
|
||||
let bot_name = "Huskies";
|
||||
let keyword = slash_command_to_bot_keyword("/huskies-status").unwrap();
|
||||
let synthetic = format!("{bot_name} {keyword}");
|
||||
|
||||
let dispatch = CommandDispatch {
|
||||
@@ -654,9 +654,9 @@ mod tests {
|
||||
let ambient_rooms = test_ambient_rooms();
|
||||
let room_id = "C01ABCDEF".to_string();
|
||||
|
||||
let bot_name = "Storkit";
|
||||
let keyword = slash_command_to_bot_keyword("/storkit-show").unwrap();
|
||||
// Simulate /storkit-show with text "999"
|
||||
let bot_name = "Huskies";
|
||||
let keyword = slash_command_to_bot_keyword("/huskies-show").unwrap();
|
||||
// Simulate /huskies-show with text "999"
|
||||
let synthetic = format!("{bot_name} {keyword} 999");
|
||||
|
||||
let dispatch = CommandDispatch {
|
||||
@@ -679,11 +679,11 @@ mod tests {
|
||||
#[test]
|
||||
fn rebuild_command_extracted_from_slack_message() {
|
||||
let result = crate::chat::transport::matrix::rebuild::extract_rebuild_command(
|
||||
"Storkit rebuild",
|
||||
"Storkit",
|
||||
"Huskies rebuild",
|
||||
"Huskies",
|
||||
"slack-bot",
|
||||
);
|
||||
assert!(result.is_some(), "'Storkit rebuild' should be recognised");
|
||||
assert!(result.is_some(), "'Huskies rebuild' should be recognised");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -691,7 +691,7 @@ mod tests {
|
||||
// Slack slash-command synthetic messages may not include a bot mention.
|
||||
let result = crate::chat::transport::matrix::rebuild::extract_rebuild_command(
|
||||
"rebuild",
|
||||
"Storkit",
|
||||
"Huskies",
|
||||
"slack-bot",
|
||||
);
|
||||
assert!(result.is_some(), "plain 'rebuild' should be recognised");
|
||||
@@ -700,8 +700,8 @@ mod tests {
|
||||
#[test]
|
||||
fn non_rebuild_slack_message_not_extracted() {
|
||||
let result = crate::chat::transport::matrix::rebuild::extract_rebuild_command(
|
||||
"Storkit status",
|
||||
"Storkit",
|
||||
"Huskies status",
|
||||
"Huskies",
|
||||
"slack-bot",
|
||||
);
|
||||
assert!(result.is_none(), "'status' should not be recognised as rebuild");
|
||||
@@ -712,18 +712,18 @@ mod tests {
|
||||
#[test]
|
||||
fn reset_command_extracted_from_slack_message() {
|
||||
let result = crate::chat::transport::matrix::reset::extract_reset_command(
|
||||
"Storkit reset",
|
||||
"Storkit",
|
||||
"Huskies reset",
|
||||
"Huskies",
|
||||
"slack-bot",
|
||||
);
|
||||
assert!(result.is_some(), "'Storkit reset' should be recognised");
|
||||
assert!(result.is_some(), "'Huskies reset' should be recognised");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_command_extracted_plain_no_mention() {
|
||||
let result = crate::chat::transport::matrix::reset::extract_reset_command(
|
||||
"reset",
|
||||
"Storkit",
|
||||
"Huskies",
|
||||
"slack-bot",
|
||||
);
|
||||
assert!(result.is_some(), "plain 'reset' should be recognised");
|
||||
@@ -750,7 +750,7 @@ mod tests {
|
||||
}));
|
||||
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
std::fs::create_dir_all(&sk).unwrap();
|
||||
|
||||
{
|
||||
|
||||
@@ -18,7 +18,7 @@ struct PersistedSlackHistory {
|
||||
}
|
||||
|
||||
/// Path to the persisted Slack conversation history file.
|
||||
const SLACK_HISTORY_FILE: &str = ".storkit/slack_history.json";
|
||||
const SLACK_HISTORY_FILE: &str = ".huskies/slack_history.json";
|
||||
|
||||
/// Load Slack conversation history from disk.
|
||||
pub fn load_slack_history(project_root: &std::path::Path) -> HashMap<String, RoomConversation> {
|
||||
@@ -66,7 +66,7 @@ mod tests {
|
||||
#[test]
|
||||
fn save_and_load_slack_history_round_trips() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
std::fs::create_dir_all(&sk).unwrap();
|
||||
|
||||
let mut history = HashMap::new();
|
||||
@@ -110,7 +110,7 @@ mod tests {
|
||||
#[test]
|
||||
fn load_slack_history_returns_empty_on_invalid_json() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
std::fs::create_dir_all(&sk).unwrap();
|
||||
std::fs::write(sk.join("slack_history.json"), "not json {{{").unwrap();
|
||||
let history = load_slack_history(tmp.path());
|
||||
|
||||
@@ -635,7 +635,7 @@ mod tests {
|
||||
}));
|
||||
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
std::fs::create_dir_all(&sk).unwrap();
|
||||
|
||||
{
|
||||
|
||||
@@ -79,7 +79,7 @@ struct PersistedWhatsAppHistory {
|
||||
}
|
||||
|
||||
/// Path to the persisted WhatsApp conversation history file.
|
||||
const WHATSAPP_HISTORY_FILE: &str = ".storkit/whatsapp_history.json";
|
||||
const WHATSAPP_HISTORY_FILE: &str = ".huskies/whatsapp_history.json";
|
||||
|
||||
/// Load WhatsApp conversation history from disk.
|
||||
pub fn load_whatsapp_history(project_root: &std::path::Path) -> HashMap<String, RoomConversation> {
|
||||
@@ -162,7 +162,7 @@ mod tests {
|
||||
#[test]
|
||||
fn save_and_load_whatsapp_history_round_trips() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
std::fs::create_dir_all(&sk).unwrap();
|
||||
|
||||
let mut history = HashMap::new();
|
||||
@@ -206,7 +206,7 @@ mod tests {
|
||||
#[test]
|
||||
fn load_whatsapp_history_returns_empty_on_invalid_json() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
std::fs::create_dir_all(&sk).unwrap();
|
||||
std::fs::write(sk.join("whatsapp_history.json"), "not json {{{").unwrap();
|
||||
let history = load_whatsapp_history(tmp.path());
|
||||
@@ -216,7 +216,7 @@ mod tests {
|
||||
#[test]
|
||||
fn save_whatsapp_history_preserves_multiple_senders() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
std::fs::create_dir_all(&sk).unwrap();
|
||||
|
||||
let mut history = HashMap::new();
|
||||
|
||||
@@ -63,11 +63,11 @@ pub fn strip_bot_mention<'a>(message: &'a str, bot_name: &str, bot_user_id: &str
|
||||
if let Some(close_paren) = url_content.find(')') {
|
||||
let url = &url_content[..close_paren]; // "https://matrix.to/#/@user:server"
|
||||
let matrix_prefix = "https://matrix.to/#/";
|
||||
if let Some(mentioned_id) = url.strip_prefix(matrix_prefix) {
|
||||
if mentioned_id.eq_ignore_ascii_case(bot_user_id) {
|
||||
let rest = &url_content[close_paren + 1..];
|
||||
return strip_mention_separator(rest);
|
||||
}
|
||||
if let Some(mentioned_id) = url.strip_prefix(matrix_prefix)
|
||||
&& mentioned_id.eq_ignore_ascii_case(bot_user_id)
|
||||
{
|
||||
let rest = &url_content[close_paren + 1..];
|
||||
return strip_mention_separator(rest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ fn default_agent_command() -> String {
|
||||
|
||||
fn default_agent_prompt() -> String {
|
||||
"You are working in a git worktree on story {{story_id}}. \
|
||||
Read .storkit/README.md to understand the dev process, then pick up the story. \
|
||||
Read .huskies/README.md to understand the dev process, then pick up the story. \
|
||||
Commit all your work when done — the server will automatically run acceptance \
|
||||
gates (cargo clippy + tests) when your process exits."
|
||||
.to_string()
|
||||
@@ -225,13 +225,13 @@ impl Default for ProjectConfig {
|
||||
}
|
||||
|
||||
impl ProjectConfig {
|
||||
/// Load from `.storkit/project.toml` relative to the given root.
|
||||
/// Load from `.huskies/project.toml` relative to the given root.
|
||||
/// Falls back to sensible defaults if the file doesn't exist.
|
||||
///
|
||||
/// Supports both the new `[[agent]]` array format and the legacy
|
||||
/// `[agent]` single-table format (with a deprecation warning).
|
||||
pub fn load(project_root: &Path) -> Result<Self, String> {
|
||||
let config_path = project_root.join(".storkit/project.toml");
|
||||
let config_path = project_root.join(".huskies/project.toml");
|
||||
if !config_path.exists() {
|
||||
return Ok(Self::default());
|
||||
}
|
||||
@@ -640,7 +640,7 @@ name = "second"
|
||||
#[test]
|
||||
fn parse_project_toml_from_file() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("project.toml"),
|
||||
@@ -721,7 +721,7 @@ name = "coder"
|
||||
#[test]
|
||||
fn watcher_config_from_file() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("project.toml"),
|
||||
|
||||
+16
-16
@@ -158,7 +158,7 @@ struct AllTokenUsageResponse {
|
||||
/// response so the agents panel is not cluttered with old completed items on
|
||||
/// frontend startup.
|
||||
pub fn story_is_archived(project_root: &path::Path, story_id: &str) -> bool {
|
||||
let work = project_root.join(".storkit").join("work");
|
||||
let work = project_root.join(".huskies").join("work");
|
||||
let filename = format!("{story_id}.md");
|
||||
work.join("5_done").join(&filename).exists() || work.join("6_archived").join(&filename).exists()
|
||||
}
|
||||
@@ -307,7 +307,7 @@ impl AgentsApi {
|
||||
))
|
||||
}
|
||||
|
||||
/// Create a git worktree for a story under .storkit/worktrees/{story_id}.
|
||||
/// Create a git worktree for a story under .huskies/worktrees/{story_id}.
|
||||
#[oai(path = "/agents/worktrees", method = "post")]
|
||||
async fn create_worktree(
|
||||
&self,
|
||||
@@ -334,7 +334,7 @@ impl AgentsApi {
|
||||
}))
|
||||
}
|
||||
|
||||
/// List all worktrees under .storkit/worktrees/.
|
||||
/// List all worktrees under .huskies/worktrees/.
|
||||
#[oai(path = "/agents/worktrees", method = "get")]
|
||||
async fn list_worktrees(&self) -> OpenApiResult<Json<Vec<WorktreeListEntry>>> {
|
||||
let project_root = self
|
||||
@@ -380,7 +380,7 @@ impl AgentsApi {
|
||||
("6_archived", "archived"),
|
||||
];
|
||||
|
||||
let work_dir = project_root.join(".storkit").join("work");
|
||||
let work_dir = project_root.join(".huskies").join("work");
|
||||
let filename = format!("{}.md", story_id.0);
|
||||
|
||||
for (stage_dir, stage_name) in &stages {
|
||||
@@ -593,7 +593,7 @@ mod tests {
|
||||
fn make_work_dirs(tmp: &TempDir) -> path::PathBuf {
|
||||
let root = tmp.path().to_path_buf();
|
||||
for stage in &["5_done", "6_archived"] {
|
||||
std::fs::create_dir_all(root.join(".storkit").join("work").join(stage)).unwrap();
|
||||
std::fs::create_dir_all(root.join(".huskies").join("work").join(stage)).unwrap();
|
||||
}
|
||||
root
|
||||
}
|
||||
@@ -610,7 +610,7 @@ mod tests {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let root = make_work_dirs(&tmp);
|
||||
std::fs::write(
|
||||
root.join(".storkit/work/5_done/79_story_foo.md"),
|
||||
root.join(".huskies/work/5_done/79_story_foo.md"),
|
||||
"---\nname: test\n---\n",
|
||||
)
|
||||
.unwrap();
|
||||
@@ -622,7 +622,7 @@ mod tests {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let root = make_work_dirs(&tmp);
|
||||
std::fs::write(
|
||||
root.join(".storkit/work/6_archived/79_story_foo.md"),
|
||||
root.join(".huskies/work/6_archived/79_story_foo.md"),
|
||||
"---\nname: test\n---\n",
|
||||
)
|
||||
.unwrap();
|
||||
@@ -636,7 +636,7 @@ mod tests {
|
||||
|
||||
// Place an archived story file in 6_archived
|
||||
std::fs::write(
|
||||
root.join(".storkit/work/6_archived/79_story_archived.md"),
|
||||
root.join(".huskies/work/6_archived/79_story_archived.md"),
|
||||
"---\nname: archived story\n---\n",
|
||||
)
|
||||
.unwrap();
|
||||
@@ -679,7 +679,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn make_project_toml(root: &path::Path, content: &str) {
|
||||
let sk_dir = root.join(".storkit");
|
||||
let sk_dir = root.join(".huskies");
|
||||
std::fs::create_dir_all(&sk_dir).unwrap();
|
||||
std::fs::write(sk_dir.join("project.toml"), content).unwrap();
|
||||
}
|
||||
@@ -801,7 +801,7 @@ allowed_tools = ["Read", "Bash"]
|
||||
#[tokio::test]
|
||||
async fn list_worktrees_returns_entries_from_dir() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let worktrees_dir = tmp.path().join(".storkit").join("worktrees");
|
||||
let worktrees_dir = tmp.path().join(".huskies").join("worktrees");
|
||||
std::fs::create_dir_all(worktrees_dir.join("42_story_foo")).unwrap();
|
||||
std::fs::create_dir_all(worktrees_dir.join("43_story_bar")).unwrap();
|
||||
|
||||
@@ -894,7 +894,7 @@ allowed_tools = ["Read", "Bash"]
|
||||
// --- get_work_item_content tests ---
|
||||
|
||||
fn make_stage_dir(root: &path::Path, stage: &str) {
|
||||
std::fs::create_dir_all(root.join(".storkit").join("work").join(stage)).unwrap();
|
||||
std::fs::create_dir_all(root.join(".huskies").join("work").join(stage)).unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -903,7 +903,7 @@ allowed_tools = ["Read", "Bash"]
|
||||
let root = tmp.path();
|
||||
make_stage_dir(root, "1_backlog");
|
||||
std::fs::write(
|
||||
root.join(".storkit/work/1_backlog/42_story_foo.md"),
|
||||
root.join(".huskies/work/1_backlog/42_story_foo.md"),
|
||||
"---\nname: \"Foo Story\"\n---\n\n# Story 42: Foo Story\n\nSome content.",
|
||||
)
|
||||
.unwrap();
|
||||
@@ -925,7 +925,7 @@ allowed_tools = ["Read", "Bash"]
|
||||
let root = tmp.path();
|
||||
make_stage_dir(root, "2_current");
|
||||
std::fs::write(
|
||||
root.join(".storkit/work/2_current/43_story_bar.md"),
|
||||
root.join(".huskies/work/2_current/43_story_bar.md"),
|
||||
"---\nname: \"Bar Story\"\n---\n\nBar content.",
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1174,7 +1174,7 @@ allowed_tools = ["Read", "Bash"]
|
||||
let root = tmp.path().to_path_buf();
|
||||
// Create work dirs including 2_current for the story file.
|
||||
for stage in &["1_backlog", "2_current", "5_done", "6_archived"] {
|
||||
std::fs::create_dir_all(root.join(".storkit").join("work").join(stage)).unwrap();
|
||||
std::fs::create_dir_all(root.join(".huskies").join("work").join(stage)).unwrap();
|
||||
}
|
||||
|
||||
// Write a story file with persisted test results.
|
||||
@@ -1185,10 +1185,10 @@ name: "Test story"
|
||||
|
||||
## Test Results
|
||||
|
||||
<!-- storkit-test-results: {"unit":[{"name":"from_file","status":"pass","details":null}],"integration":[]} -->
|
||||
<!-- huskies-test-results: {"unit":[{"name":"from_file","status":"pass","details":null}],"integration":[]} -->
|
||||
"#;
|
||||
std::fs::write(
|
||||
root.join(".storkit/work/2_current/42_story_foo.md"),
|
||||
root.join(".huskies/work/2_current/42_story_foo.md"),
|
||||
story_content,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -74,7 +74,7 @@ impl AppContext {
|
||||
pub fn new_test(project_root: std::path::PathBuf) -> Self {
|
||||
let state = SessionState::default();
|
||||
*state.project_root.lock().unwrap() = Some(project_root.clone());
|
||||
let store_path = project_root.join(".storkit_store.json");
|
||||
let store_path = project_root.join(".huskies_store.json");
|
||||
let (watcher_tx, _) = broadcast::channel(64);
|
||||
let (reconciliation_tx, _) = broadcast::channel(64);
|
||||
let (perm_tx, perm_rx) = mpsc::unbounded_channel();
|
||||
|
||||
@@ -42,11 +42,11 @@ pub(super) async fn tool_start_agent(args: &Value, ctx: &AppContext) -> Result<S
|
||||
|
||||
/// Try to read the overall line coverage percentage from the llvm-cov JSON report.
|
||||
///
|
||||
/// Expects the file at `{project_root}/.storkit/coverage/server.json`.
|
||||
/// Expects the file at `{project_root}/.huskies/coverage/server.json`.
|
||||
/// Returns `None` if the file is absent, unreadable, or cannot be parsed.
|
||||
pub(super) fn read_coverage_percent_from_json(project_root: &std::path::Path) -> Option<f64> {
|
||||
let path = project_root
|
||||
.join(".storkit")
|
||||
.join(".huskies")
|
||||
.join("coverage")
|
||||
.join("server.json");
|
||||
let contents = std::fs::read_to_string(&path).ok()?;
|
||||
@@ -485,7 +485,7 @@ mod tests {
|
||||
// Config has only a supervisor — start_agent without agent_name should
|
||||
// refuse rather than silently assigning supervisor.
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
std::fs::create_dir_all(&sk).unwrap();
|
||||
std::fs::write(
|
||||
sk.join("project.toml"),
|
||||
@@ -513,7 +513,7 @@ stage = "other"
|
||||
// missing git repo / worktree, but the error must NOT be about
|
||||
// "No coder agent configured".
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
std::fs::create_dir_all(&sk).unwrap();
|
||||
std::fs::write(
|
||||
sk.join("project.toml"),
|
||||
@@ -709,7 +709,7 @@ stage = "coder"
|
||||
fn read_coverage_percent_from_json_parses_llvm_cov_format() {
|
||||
use std::fs;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let cov_dir = tmp.path().join(".storkit/coverage");
|
||||
let cov_dir = tmp.path().join(".huskies/coverage");
|
||||
fs::create_dir_all(&cov_dir).unwrap();
|
||||
let json_content = r#"{"data":[{"totals":{"lines":{"count":100,"covered":78,"percent":78.0}}}]}"#;
|
||||
fs::write(cov_dir.join("server.json"), json_content).unwrap();
|
||||
|
||||
@@ -47,7 +47,7 @@ pub(super) async fn tool_rebuild_and_restart(ctx: &AppContext) -> Result<String,
|
||||
///
|
||||
/// - `Edit` / `Write` / `Read` / `Grep` / `Glob` etc. → just the tool name
|
||||
/// - `Bash` → `Bash(first_word *)` derived from the `command` field in `tool_input`
|
||||
/// - `mcp__*` → the full tool name (e.g. `mcp__storkit__create_story`)
|
||||
/// - `mcp__*` → the full tool name (e.g. `mcp__huskies__create_story`)
|
||||
fn generate_permission_rule(tool_name: &str, tool_input: &Value) -> String {
|
||||
if tool_name == "Bash" {
|
||||
// Extract command from tool_input.command and use first word as prefix
|
||||
@@ -109,8 +109,8 @@ pub(super) fn add_permission_rule(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Also check for wildcard coverage: if "mcp__storkit__*" exists, don't add
|
||||
// a more specific "mcp__storkit__create_story".
|
||||
// Also check for wildcard coverage: if "mcp__huskies__*" exists, don't add
|
||||
// a more specific "mcp__huskies__create_story".
|
||||
let dominated = allow.iter().any(|existing| {
|
||||
if let Some(pat) = existing.as_str()
|
||||
&& let Some(prefix) = pat.strip_suffix('*')
|
||||
@@ -470,8 +470,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn generate_rule_for_mcp_tool() {
|
||||
let rule = generate_permission_rule("mcp__storkit__create_story", &json!({"name": "foo"}));
|
||||
assert_eq!(rule, "mcp__storkit__create_story");
|
||||
let rule = generate_permission_rule("mcp__huskies__create_story", &json!({"name": "foo"}));
|
||||
assert_eq!(rule, "mcp__huskies__create_story");
|
||||
}
|
||||
|
||||
// ── Settings.json writing tests ──────────────────────────────
|
||||
@@ -507,17 +507,17 @@ mod tests {
|
||||
fs::create_dir_all(&claude_dir).unwrap();
|
||||
fs::write(
|
||||
claude_dir.join("settings.json"),
|
||||
r#"{"permissions":{"allow":["mcp__storkit__*"]}}"#,
|
||||
r#"{"permissions":{"allow":["mcp__huskies__*"]}}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
add_permission_rule(tmp.path(), "mcp__storkit__create_story").unwrap();
|
||||
add_permission_rule(tmp.path(), "mcp__huskies__create_story").unwrap();
|
||||
|
||||
let content = fs::read_to_string(claude_dir.join("settings.json")).unwrap();
|
||||
let settings: Value = serde_json::from_str(&content).unwrap();
|
||||
let allow = settings["permissions"]["allow"].as_array().unwrap();
|
||||
assert_eq!(allow.len(), 1);
|
||||
assert_eq!(allow[0], "mcp__storkit__*");
|
||||
assert_eq!(allow[0], "mcp__huskies__*");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -548,7 +548,7 @@ mod tests {
|
||||
fs::create_dir_all(&claude_dir).unwrap();
|
||||
fs::write(
|
||||
claude_dir.join("settings.json"),
|
||||
r#"{"permissions":{"allow":["Edit"]},"enabledMcpjsonServers":["storkit"]}"#,
|
||||
r#"{"permissions":{"allow":["Edit"]},"enabledMcpjsonServers":["huskies"]}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -558,7 +558,7 @@ mod tests {
|
||||
let settings: Value = serde_json::from_str(&content).unwrap();
|
||||
let servers = settings["enabledMcpjsonServers"].as_array().unwrap();
|
||||
assert_eq!(servers.len(), 1);
|
||||
assert_eq!(servers[0], "storkit");
|
||||
assert_eq!(servers[0], "huskies");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -598,9 +598,9 @@ mod tests {
|
||||
// binary, otherwise cargo build outputs to a different target dir and
|
||||
// current_exe() still points at the old binary.
|
||||
let build_args: Vec<&str> = if cfg!(debug_assertions) {
|
||||
vec!["build", "-p", "storkit"]
|
||||
vec!["build", "-p", "huskies"]
|
||||
} else {
|
||||
vec!["build", "--release", "-p", "storkit"]
|
||||
vec!["build", "--release", "-p", "huskies"]
|
||||
};
|
||||
|
||||
// Tests always run in debug mode, so --release must NOT be present.
|
||||
@@ -652,7 +652,7 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
// Seed project root in state so get_project_root works
|
||||
let backlog = root.join(".storkit/work/1_backlog");
|
||||
let backlog = root.join(".huskies/work/1_backlog");
|
||||
fs::create_dir_all(&backlog).unwrap();
|
||||
fs::write(backlog.join("1_story_test.md"), "---\nname: Test\n---\n").unwrap();
|
||||
let ctx = test_ctx(root);
|
||||
@@ -668,8 +668,8 @@ mod tests {
|
||||
fn tool_move_story_moves_from_backlog_to_current() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let backlog = root.join(".storkit/work/1_backlog");
|
||||
let current = root.join(".storkit/work/2_current");
|
||||
let backlog = root.join(".huskies/work/1_backlog");
|
||||
let current = root.join(".huskies/work/2_current");
|
||||
fs::create_dir_all(&backlog).unwrap();
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(backlog.join("5_story_test.md"), "---\nname: Test\n---\n").unwrap();
|
||||
@@ -693,8 +693,8 @@ mod tests {
|
||||
fn tool_move_story_moves_from_current_to_backlog() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let current = root.join(".storkit/work/2_current");
|
||||
let backlog = root.join(".storkit/work/1_backlog");
|
||||
let current = root.join(".huskies/work/2_current");
|
||||
let backlog = root.join(".huskies/work/1_backlog");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::create_dir_all(&backlog).unwrap();
|
||||
fs::write(current.join("6_story_back.md"), "---\nname: Back\n---\n").unwrap();
|
||||
@@ -717,7 +717,7 @@ mod tests {
|
||||
fn tool_move_story_idempotent_when_already_in_target() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let current = root.join(".storkit/work/2_current");
|
||||
let current = root.join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(current.join("7_story_idem.md"), "---\nname: Idem\n---\n").unwrap();
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ use serde_json::{json, Value};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Validates that `worktree_path` exists and is inside the project's
|
||||
/// `.storkit/worktrees/` directory. Returns the canonicalized path.
|
||||
/// `.huskies/worktrees/` directory. Returns the canonicalized path.
|
||||
fn validate_worktree_path(worktree_path: &str, ctx: &AppContext) -> Result<PathBuf, String> {
|
||||
let wd = PathBuf::from(worktree_path);
|
||||
|
||||
@@ -17,7 +17,7 @@ fn validate_worktree_path(worktree_path: &str, ctx: &AppContext) -> Result<PathB
|
||||
}
|
||||
|
||||
let project_root = ctx.agents.get_project_root(&ctx.state)?;
|
||||
let worktrees_root = project_root.join(".storkit").join("worktrees");
|
||||
let worktrees_root = project_root.join(".huskies").join("worktrees");
|
||||
|
||||
let canonical_wd = wd
|
||||
.canonicalize()
|
||||
@@ -33,7 +33,7 @@ fn validate_worktree_path(worktree_path: &str, ctx: &AppContext) -> Result<PathB
|
||||
|
||||
if !canonical_wd.starts_with(&canonical_wt) {
|
||||
return Err(format!(
|
||||
"worktree_path must be inside .storkit/worktrees/. Got: {worktree_path}"
|
||||
"worktree_path must be inside .huskies/worktrees/. Got: {worktree_path}"
|
||||
));
|
||||
}
|
||||
|
||||
@@ -312,7 +312,7 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let story_wt = tmp
|
||||
.path()
|
||||
.join(".storkit")
|
||||
.join(".huskies")
|
||||
.join("worktrees")
|
||||
.join("42_test_story");
|
||||
std::fs::create_dir_all(&story_wt).unwrap();
|
||||
@@ -361,12 +361,12 @@ mod tests {
|
||||
#[test]
|
||||
fn validate_rejects_path_outside_worktrees() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let wt_dir = tmp.path().join(".storkit").join("worktrees");
|
||||
let wt_dir = tmp.path().join(".huskies").join("worktrees");
|
||||
std::fs::create_dir_all(&wt_dir).unwrap();
|
||||
let ctx = test_ctx(tmp.path());
|
||||
let result = validate_worktree_path(tmp.path().to_str().unwrap(), &ctx);
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("inside .storkit/worktrees"));
|
||||
assert!(result.unwrap_err().contains("inside .huskies/worktrees"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -374,7 +374,7 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let story_wt = tmp
|
||||
.path()
|
||||
.join(".storkit")
|
||||
.join(".huskies")
|
||||
.join("worktrees")
|
||||
.join("42_test_story");
|
||||
std::fs::create_dir_all(&story_wt).unwrap();
|
||||
|
||||
@@ -138,7 +138,7 @@ pub(super) fn tool_report_merge_failure(args: &Value, ctx: &AppContext) -> Resul
|
||||
// survives server restarts and is visible in the web UI.
|
||||
if let Ok(project_root) = ctx.state.get_project_root() {
|
||||
let story_file = project_root
|
||||
.join(".storkit")
|
||||
.join(".huskies")
|
||||
.join("work")
|
||||
.join("4_merge")
|
||||
.join(format!("{story_id}.md"));
|
||||
@@ -243,7 +243,7 @@ mod tests {
|
||||
async fn tool_move_story_to_merge_moves_file() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
setup_git_repo_in(tmp.path());
|
||||
let current_dir = tmp.path().join(".storkit/work/2_current");
|
||||
let current_dir = tmp.path().join(".huskies/work/2_current");
|
||||
std::fs::create_dir_all(¤t_dir).unwrap();
|
||||
let story_file = current_dir.join("24_story_test.md");
|
||||
std::fs::write(&story_file, "---\nname: Test\n---\n").unwrap();
|
||||
@@ -264,7 +264,7 @@ mod tests {
|
||||
// File should have been moved regardless of agent start outcome
|
||||
assert!(!story_file.exists(), "2_current file should be gone");
|
||||
assert!(
|
||||
tmp.path().join(".storkit/work/4_merge/24_story_test.md").exists(),
|
||||
tmp.path().join(".huskies/work/4_merge/24_story_test.md").exists(),
|
||||
"4_merge file should exist"
|
||||
);
|
||||
// Result is either Ok (agent started) or Err (agent failed - acceptable in tests)
|
||||
|
||||
+19
-19
@@ -308,7 +308,7 @@ fn handle_initialize(id: Option<Value>, params: &Value) -> JsonRpcResponse {
|
||||
"tools": {}
|
||||
},
|
||||
"serverInfo": {
|
||||
"name": "storkit",
|
||||
"name": "huskies",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
}),
|
||||
@@ -537,7 +537,7 @@ fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
|
||||
},
|
||||
{
|
||||
"name": "create_worktree",
|
||||
"description": "Create a git worktree for a story under .storkit/worktrees/{story_id} with deterministic naming. Writes .mcp.json and runs component setup. Returns the worktree path.",
|
||||
"description": "Create a git worktree for a story under .huskies/worktrees/{story_id} with deterministic naming. Writes .mcp.json and runs component setup. Returns the worktree path.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -551,7 +551,7 @@ fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
|
||||
},
|
||||
{
|
||||
"name": "list_worktrees",
|
||||
"description": "List all worktrees under .storkit/worktrees/ for the current project.",
|
||||
"description": "List all worktrees under .huskies/worktrees/ for the current project.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
@@ -670,7 +670,7 @@ fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
|
||||
},
|
||||
{
|
||||
"name": "create_spike",
|
||||
"description": "Create a spike file in .storkit/work/1_backlog/ with a deterministic filename and YAML front matter. Returns the spike_id.",
|
||||
"description": "Create a spike file in .huskies/work/1_backlog/ with a deterministic filename and YAML front matter. Returns the spike_id.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -894,7 +894,7 @@ fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
|
||||
},
|
||||
{
|
||||
"name": "launch_qa_app",
|
||||
"description": "Launch the app from a story's worktree for manual QA testing. Automatically assigns a free port, writes it to .storkit_port, and starts the backend server. Only one QA app instance runs at a time.",
|
||||
"description": "Launch the app from a story's worktree for manual QA testing. Automatically assigns a free port, writes it to .huskies_port, and starts the backend server. Only one QA app instance runs at a time.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1023,7 +1023,7 @@ fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
|
||||
},
|
||||
{
|
||||
"name": "run_command",
|
||||
"description": "Execute a shell command in an agent's worktree directory. The working_dir must be inside .storkit/worktrees/. Returns stdout, stderr, exit_code, and timed_out. Supports SSE streaming (send Accept: text/event-stream) for long-running commands. Dangerous commands (rm -rf /, sudo, etc.) are blocked.",
|
||||
"description": "Execute a shell command in an agent's worktree directory. The working_dir must be inside .huskies/worktrees/. Returns stdout, stderr, exit_code, and timed_out. Supports SSE streaming (send Accept: text/event-stream) for long-running commands. Dangerous commands (rm -rf /, sudo, etc.) are blocked.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1033,7 +1033,7 @@ fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
|
||||
},
|
||||
"working_dir": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to the worktree directory to run the command in. Must be inside .storkit/worktrees/."
|
||||
"description": "Absolute path to the worktree directory to run the command in. Must be inside .huskies/worktrees/."
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
@@ -1045,13 +1045,13 @@ fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
|
||||
},
|
||||
{
|
||||
"name": "git_status",
|
||||
"description": "Return the working tree status of an agent's worktree (staged, unstaged, and untracked files). The worktree_path must be inside .storkit/worktrees/. Push and remote operations are not available.",
|
||||
"description": "Return the working tree status of an agent's worktree (staged, unstaged, and untracked files). The worktree_path must be inside .huskies/worktrees/. Push and remote operations are not available.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"worktree_path": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to the worktree directory. Must be inside .storkit/worktrees/."
|
||||
"description": "Absolute path to the worktree directory. Must be inside .huskies/worktrees/."
|
||||
}
|
||||
},
|
||||
"required": ["worktree_path"]
|
||||
@@ -1059,13 +1059,13 @@ fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
|
||||
},
|
||||
{
|
||||
"name": "git_diff",
|
||||
"description": "Return diff output for an agent's worktree. Supports unstaged (default), staged, or a commit range. The worktree_path must be inside .storkit/worktrees/.",
|
||||
"description": "Return diff output for an agent's worktree. Supports unstaged (default), staged, or a commit range. The worktree_path must be inside .huskies/worktrees/.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"worktree_path": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to the worktree directory. Must be inside .storkit/worktrees/."
|
||||
"description": "Absolute path to the worktree directory. Must be inside .huskies/worktrees/."
|
||||
},
|
||||
"staged": {
|
||||
"type": "boolean",
|
||||
@@ -1081,13 +1081,13 @@ fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
|
||||
},
|
||||
{
|
||||
"name": "git_add",
|
||||
"description": "Stage files by path in an agent's worktree. The worktree_path must be inside .storkit/worktrees/.",
|
||||
"description": "Stage files by path in an agent's worktree. The worktree_path must be inside .huskies/worktrees/.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"worktree_path": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to the worktree directory. Must be inside .storkit/worktrees/."
|
||||
"description": "Absolute path to the worktree directory. Must be inside .huskies/worktrees/."
|
||||
},
|
||||
"paths": {
|
||||
"type": "array",
|
||||
@@ -1100,13 +1100,13 @@ fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
|
||||
},
|
||||
{
|
||||
"name": "git_commit",
|
||||
"description": "Commit staged changes in an agent's worktree with the given message. The worktree_path must be inside .storkit/worktrees/. Push and remote operations are not available.",
|
||||
"description": "Commit staged changes in an agent's worktree with the given message. The worktree_path must be inside .huskies/worktrees/. Push and remote operations are not available.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"worktree_path": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to the worktree directory. Must be inside .storkit/worktrees/."
|
||||
"description": "Absolute path to the worktree directory. Must be inside .huskies/worktrees/."
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
@@ -1118,13 +1118,13 @@ fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
|
||||
},
|
||||
{
|
||||
"name": "git_log",
|
||||
"description": "Return commit history for an agent's worktree with configurable count and format. The worktree_path must be inside .storkit/worktrees/.",
|
||||
"description": "Return commit history for an agent's worktree with configurable count and format. The worktree_path must be inside .huskies/worktrees/.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"worktree_path": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to the worktree directory. Must be inside .storkit/worktrees/."
|
||||
"description": "Absolute path to the worktree directory. Must be inside .huskies/worktrees/."
|
||||
},
|
||||
"count": {
|
||||
"type": "integer",
|
||||
@@ -1363,7 +1363,7 @@ mod tests {
|
||||
let result = resp.result.unwrap();
|
||||
assert_eq!(result["protocolVersion"], "2025-03-26");
|
||||
assert!(result["capabilities"]["tools"].is_object());
|
||||
assert_eq!(result["serverInfo"]["name"], "storkit");
|
||||
assert_eq!(result["serverInfo"]["name"], "huskies");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1638,7 +1638,7 @@ mod tests {
|
||||
)
|
||||
.await;
|
||||
assert_eq!(body["result"]["protocolVersion"], "2025-03-26");
|
||||
assert_eq!(body["result"]["serverInfo"]["name"], "storkit");
|
||||
assert_eq!(body["result"]["serverInfo"]["name"], "huskies");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -48,7 +48,7 @@ pub(super) async fn tool_approve_qa(args: &Value, ctx: &AppContext) -> Result<St
|
||||
|
||||
// Clear review_hold before moving
|
||||
let qa_path = project_root
|
||||
.join(".storkit/work/3_qa")
|
||||
.join(".huskies/work/3_qa")
|
||||
.join(format!("{story_id}.md"));
|
||||
if qa_path.exists() {
|
||||
let _ = crate::io::story_metadata::clear_front_matter_field(&qa_path, "review_hold");
|
||||
@@ -92,7 +92,7 @@ pub(super) async fn tool_reject_qa(args: &Value, ctx: &AppContext) -> Result<Str
|
||||
|
||||
// Restart the coder agent with rejection context
|
||||
let story_path = project_root
|
||||
.join(".storkit/work/2_current")
|
||||
.join(".huskies/work/2_current")
|
||||
.join(format!("{story_id}.md"));
|
||||
let agent_name = if story_path.exists() {
|
||||
let contents = std::fs::read_to_string(&story_path).unwrap_or_default();
|
||||
@@ -152,15 +152,15 @@ pub(super) async fn tool_launch_qa_app(args: &Value, ctx: &AppContext) -> Result
|
||||
// Find a free port starting from 3100
|
||||
let port = find_free_port(3100);
|
||||
|
||||
// Write .storkit_port so the frontend dev server knows where to connect
|
||||
let port_file = wt_path.join(".storkit_port");
|
||||
// Write .huskies_port so the frontend dev server knows where to connect
|
||||
let port_file = wt_path.join(".huskies_port");
|
||||
std::fs::write(&port_file, port.to_string())
|
||||
.map_err(|e| format!("Failed to write .storkit_port: {e}"))?;
|
||||
.map_err(|e| format!("Failed to write .huskies_port: {e}"))?;
|
||||
|
||||
// Launch the server from the worktree
|
||||
let child = std::process::Command::new("cargo")
|
||||
.args(["run"])
|
||||
.env("STORKIT_PORT", port.to_string())
|
||||
.env("HUSKIES_PORT", port.to_string())
|
||||
.current_dir(&wt_path)
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
|
||||
@@ -59,7 +59,7 @@ fn is_dangerous(command: &str) -> Option<String> {
|
||||
}
|
||||
|
||||
/// Validates that `working_dir` exists and is inside the project's
|
||||
/// `.storkit/worktrees/` directory. Returns the canonicalized path.
|
||||
/// `.huskies/worktrees/` directory. Returns the canonicalized path.
|
||||
fn validate_working_dir(working_dir: &str, ctx: &AppContext) -> Result<PathBuf, String> {
|
||||
let wd = PathBuf::from(working_dir);
|
||||
|
||||
@@ -71,7 +71,7 @@ fn validate_working_dir(working_dir: &str, ctx: &AppContext) -> Result<PathBuf,
|
||||
}
|
||||
|
||||
let project_root = ctx.agents.get_project_root(&ctx.state)?;
|
||||
let worktrees_root = project_root.join(".storkit").join("worktrees");
|
||||
let worktrees_root = project_root.join(".huskies").join("worktrees");
|
||||
|
||||
let canonical_wd = wd
|
||||
.canonicalize()
|
||||
@@ -88,7 +88,7 @@ fn validate_working_dir(working_dir: &str, ctx: &AppContext) -> Result<PathBuf,
|
||||
|
||||
if !canonical_wd.starts_with(&canonical_wt) {
|
||||
return Err(format!(
|
||||
"working_dir must be inside .storkit/worktrees/. Got: {working_dir}"
|
||||
"working_dir must be inside .huskies/worktrees/. Got: {working_dir}"
|
||||
));
|
||||
}
|
||||
|
||||
@@ -406,14 +406,14 @@ mod tests {
|
||||
fn validate_working_dir_rejects_path_outside_worktrees() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
// Create the worktrees dir so it exists
|
||||
let wt_dir = tmp.path().join(".storkit").join("worktrees");
|
||||
let wt_dir = tmp.path().join(".huskies").join("worktrees");
|
||||
std::fs::create_dir_all(&wt_dir).unwrap();
|
||||
let ctx = test_ctx(tmp.path());
|
||||
// Try to use /tmp (outside worktrees)
|
||||
let result = validate_working_dir(tmp.path().to_str().unwrap(), &ctx);
|
||||
assert!(result.is_err());
|
||||
assert!(
|
||||
result.unwrap_err().contains("inside .storkit/worktrees"),
|
||||
result.unwrap_err().contains("inside .huskies/worktrees"),
|
||||
"expected sandbox error"
|
||||
);
|
||||
}
|
||||
@@ -423,7 +423,7 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let story_wt = tmp
|
||||
.path()
|
||||
.join(".storkit")
|
||||
.join(".huskies")
|
||||
.join("worktrees")
|
||||
.join("42_test_story");
|
||||
std::fs::create_dir_all(&story_wt).unwrap();
|
||||
@@ -477,7 +477,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn tool_run_command_rejects_path_outside_worktrees() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let wt_dir = tmp.path().join(".storkit").join("worktrees");
|
||||
let wt_dir = tmp.path().join(".huskies").join("worktrees");
|
||||
std::fs::create_dir_all(&wt_dir).unwrap();
|
||||
let ctx = test_ctx(tmp.path());
|
||||
let result = tool_run_command(
|
||||
@@ -500,7 +500,7 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let story_wt = tmp
|
||||
.path()
|
||||
.join(".storkit")
|
||||
.join(".huskies")
|
||||
.join("worktrees")
|
||||
.join("42_test");
|
||||
std::fs::create_dir_all(&story_wt).unwrap();
|
||||
@@ -528,7 +528,7 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let story_wt = tmp
|
||||
.path()
|
||||
.join(".storkit")
|
||||
.join(".huskies")
|
||||
.join("worktrees")
|
||||
.join("43_test");
|
||||
std::fs::create_dir_all(&story_wt).unwrap();
|
||||
@@ -554,7 +554,7 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let story_wt = tmp
|
||||
.path()
|
||||
.join(".storkit")
|
||||
.join(".huskies")
|
||||
.join("worktrees")
|
||||
.join("44_test");
|
||||
std::fs::create_dir_all(&story_wt).unwrap();
|
||||
@@ -580,7 +580,7 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let story_wt = tmp
|
||||
.path()
|
||||
.join(".storkit")
|
||||
.join(".huskies")
|
||||
.join("worktrees")
|
||||
.join("45_test");
|
||||
std::fs::create_dir_all(&story_wt).unwrap();
|
||||
|
||||
@@ -30,10 +30,10 @@ fn parse_ac_items(contents: &str) -> Vec<(String, bool)> {
|
||||
items
|
||||
}
|
||||
|
||||
/// Find the most recent log file for any agent under `.storkit/logs/{story_id}/`.
|
||||
/// Find the most recent log file for any agent under `.huskies/logs/{story_id}/`.
|
||||
fn find_most_recent_log(project_root: &Path, story_id: &str) -> Option<PathBuf> {
|
||||
let dir = project_root
|
||||
.join(".storkit")
|
||||
.join(".huskies")
|
||||
.join("logs")
|
||||
.join(story_id);
|
||||
|
||||
@@ -157,7 +157,7 @@ pub(super) async fn tool_status(args: &Value, ctx: &AppContext) -> Result<String
|
||||
.ok_or("Missing required argument: story_id")?;
|
||||
|
||||
let root = ctx.state.get_project_root()?;
|
||||
let current_dir = root.join(".storkit").join("work").join("2_current");
|
||||
let current_dir = root.join(".huskies").join("work").join("2_current");
|
||||
let filepath = current_dir.join(format!("{story_id}.md"));
|
||||
|
||||
if !filepath.exists() {
|
||||
@@ -206,7 +206,7 @@ pub(super) async fn tool_status(args: &Value, ctx: &AppContext) -> Result<String
|
||||
.collect();
|
||||
|
||||
// --- Worktree ---
|
||||
let worktree_path = root.join(".storkit").join("worktrees").join(story_id);
|
||||
let worktree_path = root.join(".huskies").join("worktrees").join(story_id);
|
||||
let (_, worktree_info) = if worktree_path.is_dir() {
|
||||
let branch = git_branch(&worktree_path).await;
|
||||
(
|
||||
@@ -300,7 +300,7 @@ mod tests {
|
||||
let tmp = tempdir().unwrap();
|
||||
let log_dir = tmp
|
||||
.path()
|
||||
.join(".storkit")
|
||||
.join(".huskies")
|
||||
.join("logs")
|
||||
.join("42_story_foo");
|
||||
fs::create_dir_all(&log_dir).unwrap();
|
||||
@@ -336,7 +336,7 @@ mod tests {
|
||||
let tmp = tempdir().unwrap();
|
||||
let current_dir = tmp
|
||||
.path()
|
||||
.join(".storkit")
|
||||
.join(".huskies")
|
||||
.join("work")
|
||||
.join("2_current");
|
||||
fs::create_dir_all(¤t_dir).unwrap();
|
||||
|
||||
@@ -130,7 +130,7 @@ pub(super) fn tool_get_story_todos(args: &Value, ctx: &AppContext) -> Result<Str
|
||||
.ok_or("Missing required argument: story_id")?;
|
||||
|
||||
let root = ctx.state.get_project_root()?;
|
||||
let current_dir = root.join(".storkit").join("work").join("2_current");
|
||||
let current_dir = root.join(".huskies").join("work").join("2_current");
|
||||
let filepath = current_dir.join(format!("{story_id}.md"));
|
||||
|
||||
if !filepath.exists() {
|
||||
@@ -448,7 +448,7 @@ pub(super) async fn tool_delete_story(args: &Value, ctx: &AppContext) -> Result<
|
||||
}
|
||||
|
||||
// 4. Find and delete the story file from any pipeline stage
|
||||
let sk = project_root.join(".storkit").join("work");
|
||||
let sk = project_root.join(".huskies").join("work");
|
||||
let stage_dirs = [
|
||||
"1_backlog",
|
||||
"2_current",
|
||||
@@ -667,7 +667,7 @@ mod tests {
|
||||
("4_merge", "40_story_merge", "Merge Story"),
|
||||
("5_done", "50_story_done", "Done Story"),
|
||||
] {
|
||||
let dir = root.join(".storkit/work").join(stage);
|
||||
let dir = root.join(".huskies/work").join(stage);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
std::fs::write(
|
||||
dir.join(format!("{id}.md")),
|
||||
@@ -705,7 +705,7 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
|
||||
let current = root.join(".storkit/work/2_current");
|
||||
let current = root.join(".huskies/work/2_current");
|
||||
std::fs::create_dir_all(¤t).unwrap();
|
||||
std::fs::write(
|
||||
current.join("20_story_active.md"),
|
||||
@@ -745,7 +745,7 @@ mod tests {
|
||||
#[test]
|
||||
fn tool_get_story_todos_returns_unchecked() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let current_dir = tmp.path().join(".storkit").join("work").join("2_current");
|
||||
let current_dir = tmp.path().join(".huskies").join("work").join("2_current");
|
||||
fs::create_dir_all(¤t_dir).unwrap();
|
||||
fs::write(
|
||||
current_dir.join("1_test.md"),
|
||||
@@ -839,8 +839,8 @@ mod tests {
|
||||
"create_bug description should reference work/1_backlog/, got: {desc}"
|
||||
);
|
||||
assert!(
|
||||
!desc.contains(".storkit/bugs"),
|
||||
"create_bug description should not reference nonexistent .storkit/bugs/, got: {desc}"
|
||||
!desc.contains(".huskies/bugs"),
|
||||
"create_bug description should not reference nonexistent .huskies/bugs/, got: {desc}"
|
||||
);
|
||||
let required = t["inputSchema"]["required"].as_array().unwrap();
|
||||
let req_names: Vec<&str> = required.iter().map(|v| v.as_str().unwrap()).collect();
|
||||
@@ -865,8 +865,8 @@ mod tests {
|
||||
"list_bugs description should reference work/1_backlog/, got: {desc}"
|
||||
);
|
||||
assert!(
|
||||
!desc.contains(".storkit/bugs"),
|
||||
"list_bugs description should not reference nonexistent .storkit/bugs/, got: {desc}"
|
||||
!desc.contains(".huskies/bugs"),
|
||||
"list_bugs description should not reference nonexistent .huskies/bugs/, got: {desc}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -880,8 +880,8 @@ mod tests {
|
||||
let t = tool.unwrap();
|
||||
let desc = t["description"].as_str().unwrap();
|
||||
assert!(
|
||||
!desc.contains(".storkit/bugs"),
|
||||
"close_bug description should not reference nonexistent .storkit/bugs/, got: {desc}"
|
||||
!desc.contains(".huskies/bugs"),
|
||||
"close_bug description should not reference nonexistent .huskies/bugs/, got: {desc}"
|
||||
);
|
||||
assert!(
|
||||
desc.contains("work/5_done/"),
|
||||
@@ -947,7 +947,7 @@ mod tests {
|
||||
assert!(result.contains("1_bug_login_crash"));
|
||||
let bug_file = tmp
|
||||
.path()
|
||||
.join(".storkit/work/1_backlog/1_bug_login_crash.md");
|
||||
.join(".huskies/work/1_backlog/1_bug_login_crash.md");
|
||||
assert!(bug_file.exists());
|
||||
}
|
||||
|
||||
@@ -963,7 +963,7 @@ mod tests {
|
||||
#[test]
|
||||
fn tool_list_bugs_returns_open_bugs() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let backlog_dir = tmp.path().join(".storkit/work/1_backlog");
|
||||
let backlog_dir = tmp.path().join(".huskies/work/1_backlog");
|
||||
std::fs::create_dir_all(&backlog_dir).unwrap();
|
||||
std::fs::write(backlog_dir.join("1_bug_crash.md"), "# Bug 1: App Crash\n").unwrap();
|
||||
std::fs::write(
|
||||
@@ -995,7 +995,7 @@ mod tests {
|
||||
fn tool_close_bug_moves_to_archive() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
setup_git_repo_in(tmp.path());
|
||||
let backlog_dir = tmp.path().join(".storkit/work/1_backlog");
|
||||
let backlog_dir = tmp.path().join(".huskies/work/1_backlog");
|
||||
std::fs::create_dir_all(&backlog_dir).unwrap();
|
||||
let bug_file = backlog_dir.join("1_bug_crash.md");
|
||||
std::fs::write(&bug_file, "# Bug 1: Crash\n").unwrap();
|
||||
@@ -1017,7 +1017,7 @@ mod tests {
|
||||
assert!(!bug_file.exists());
|
||||
assert!(
|
||||
tmp.path()
|
||||
.join(".storkit/work/5_done/1_bug_crash.md")
|
||||
.join(".huskies/work/5_done/1_bug_crash.md")
|
||||
.exists()
|
||||
);
|
||||
}
|
||||
@@ -1070,7 +1070,7 @@ mod tests {
|
||||
assert!(result.contains("1_spike_compare_encoders"));
|
||||
let spike_file = tmp
|
||||
.path()
|
||||
.join(".storkit/work/1_backlog/1_spike_compare_encoders.md");
|
||||
.join(".huskies/work/1_backlog/1_spike_compare_encoders.md");
|
||||
assert!(spike_file.exists());
|
||||
let contents = std::fs::read_to_string(&spike_file).unwrap();
|
||||
assert!(contents.starts_with("---\nname: \"Compare Encoders\"\n---"));
|
||||
@@ -1087,7 +1087,7 @@ mod tests {
|
||||
|
||||
let spike_file = tmp
|
||||
.path()
|
||||
.join(".storkit/work/1_backlog/1_spike_my_spike.md");
|
||||
.join(".huskies/work/1_backlog/1_spike_my_spike.md");
|
||||
assert!(spike_file.exists());
|
||||
let contents = std::fs::read_to_string(&spike_file).unwrap();
|
||||
assert!(contents.starts_with("---\nname: \"My Spike\"\n---"));
|
||||
@@ -1130,7 +1130,7 @@ mod tests {
|
||||
#[test]
|
||||
fn tool_validate_stories_with_valid_story() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let current_dir = tmp.path().join(".storkit").join("work").join("2_current");
|
||||
let current_dir = tmp.path().join(".huskies").join("work").join("2_current");
|
||||
fs::create_dir_all(¤t_dir).unwrap();
|
||||
fs::write(
|
||||
current_dir.join("1_test.md"),
|
||||
@@ -1147,7 +1147,7 @@ mod tests {
|
||||
#[test]
|
||||
fn tool_validate_stories_with_invalid_front_matter() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let current_dir = tmp.path().join(".storkit").join("work").join("2_current");
|
||||
let current_dir = tmp.path().join(".huskies").join("work").join("2_current");
|
||||
fs::create_dir_all(¤t_dir).unwrap();
|
||||
fs::write(current_dir.join("1_test.md"), "## No front matter at all\n").unwrap();
|
||||
let ctx = test_ctx(tmp.path());
|
||||
@@ -1160,7 +1160,7 @@ mod tests {
|
||||
#[test]
|
||||
fn record_tests_persists_to_story_file() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let current = tmp.path().join(".storkit/work/2_current");
|
||||
let current = tmp.path().join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(
|
||||
current.join("1_story_persist.md"),
|
||||
@@ -1185,7 +1185,7 @@ mod tests {
|
||||
"file should have Test Results section"
|
||||
);
|
||||
assert!(
|
||||
contents.contains("storkit-test-results:"),
|
||||
contents.contains("huskies-test-results:"),
|
||||
"file should have JSON marker"
|
||||
);
|
||||
assert!(contents.contains("u1"), "file should contain test name");
|
||||
@@ -1194,11 +1194,11 @@ mod tests {
|
||||
#[test]
|
||||
fn ensure_acceptance_reads_from_file_when_not_in_memory() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let current = tmp.path().join(".storkit/work/2_current");
|
||||
let current = tmp.path().join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
|
||||
// Write a story file with a pre-populated Test Results section (simulating a restart)
|
||||
let story_content = "---\nname: Persist\n---\n# Story\n\n## Test Results\n\n<!-- storkit-test-results: {\"unit\":[{\"name\":\"u1\",\"status\":\"pass\",\"details\":null}],\"integration\":[{\"name\":\"i1\",\"status\":\"pass\",\"details\":null}]} -->\n";
|
||||
let story_content = "---\nname: Persist\n---\n# Story\n\n## Test Results\n\n<!-- huskies-test-results: {\"unit\":[{\"name\":\"u1\",\"status\":\"pass\",\"details\":null}],\"integration\":[{\"name\":\"i1\",\"status\":\"pass\",\"details\":null}]} -->\n";
|
||||
fs::write(current.join("2_story_file_only.md"), story_content).unwrap();
|
||||
|
||||
// Use a fresh context (empty in-memory state, simulating a restart)
|
||||
@@ -1217,10 +1217,10 @@ mod tests {
|
||||
#[test]
|
||||
fn ensure_acceptance_file_with_failures_still_blocks() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let current = tmp.path().join(".storkit/work/2_current");
|
||||
let current = tmp.path().join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
|
||||
let story_content = "---\nname: Fail\n---\n# Story\n\n## Test Results\n\n<!-- storkit-test-results: {\"unit\":[{\"name\":\"u1\",\"status\":\"fail\",\"details\":\"error\"}],\"integration\":[]} -->\n";
|
||||
let story_content = "---\nname: Fail\n---\n# Story\n\n## Test Results\n\n<!-- huskies-test-results: {\"unit\":[{\"name\":\"u1\",\"status\":\"fail\",\"details\":\"error\"}],\"integration\":[]} -->\n";
|
||||
fs::write(current.join("3_story_fail.md"), story_content).unwrap();
|
||||
|
||||
let ctx = test_ctx(tmp.path());
|
||||
@@ -1254,7 +1254,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn tool_delete_story_deletes_file_from_backlog() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let backlog = tmp.path().join(".storkit/work/1_backlog");
|
||||
let backlog = tmp.path().join(".huskies/work/1_backlog");
|
||||
fs::create_dir_all(&backlog).unwrap();
|
||||
let story_file = backlog.join("10_story_cleanup.md");
|
||||
fs::write(&story_file, "---\nname: Cleanup\n---\n").unwrap();
|
||||
@@ -1268,7 +1268,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn tool_delete_story_deletes_file_from_current() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let current = tmp.path().join(".storkit/work/2_current");
|
||||
let current = tmp.path().join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
let story_file = current.join("11_story_active.md");
|
||||
fs::write(&story_file, "---\nname: Active\n---\n").unwrap();
|
||||
@@ -1328,7 +1328,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
// Create story file in current/ so move_story_to_done would work.
|
||||
let current_dir = tmp.path().join(".storkit/work/2_current");
|
||||
let current_dir = tmp.path().join(".huskies/work/2_current");
|
||||
std::fs::create_dir_all(¤t_dir).unwrap();
|
||||
std::fs::write(
|
||||
current_dir.join("50_story_test.md"),
|
||||
@@ -1356,7 +1356,7 @@ mod tests {
|
||||
setup_git_repo_in(tmp.path());
|
||||
|
||||
// Create story file in current/ (no feature branch).
|
||||
let current_dir = tmp.path().join(".storkit/work/2_current");
|
||||
let current_dir = tmp.path().join(".huskies/work/2_current");
|
||||
std::fs::create_dir_all(¤t_dir).unwrap();
|
||||
std::fs::write(
|
||||
current_dir.join("51_story_no_branch.md"),
|
||||
@@ -1394,7 +1394,7 @@ mod tests {
|
||||
fn tool_check_criterion_marks_unchecked_item() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
setup_git_repo_in(tmp.path());
|
||||
let current_dir = tmp.path().join(".storkit").join("work").join("2_current");
|
||||
let current_dir = tmp.path().join(".huskies").join("work").join("2_current");
|
||||
fs::create_dir_all(¤t_dir).unwrap();
|
||||
fs::write(
|
||||
current_dir.join("1_test.md"),
|
||||
|
||||
@@ -22,19 +22,19 @@ use std::path::Path;
|
||||
/// Return the filesystem path (relative to `project_root`) for a step's output.
|
||||
///
|
||||
/// Returns `None` for `Scaffold` since that step has no single output file — it
|
||||
/// creates the full `.storkit/` directory structure and is handled by
|
||||
/// `storkit init` before the server starts.
|
||||
/// creates the full `.huskies/` directory structure and is handled by
|
||||
/// `huskies init` before the server starts.
|
||||
pub(crate) fn step_output_path(project_root: &Path, step: WizardStep) -> Option<std::path::PathBuf> {
|
||||
match step {
|
||||
WizardStep::Context => Some(
|
||||
project_root
|
||||
.join(".storkit")
|
||||
.join(".huskies")
|
||||
.join("specs")
|
||||
.join("00_CONTEXT.md"),
|
||||
),
|
||||
WizardStep::Stack => Some(
|
||||
project_root
|
||||
.join(".storkit")
|
||||
.join(".huskies")
|
||||
.join("specs")
|
||||
.join("tech")
|
||||
.join("STACK.md"),
|
||||
@@ -99,7 +99,7 @@ fn step_slug(step: WizardStep) -> String {
|
||||
pub(super) fn tool_wizard_status(ctx: &AppContext) -> Result<String, String> {
|
||||
let root = ctx.state.get_project_root()?;
|
||||
let state =
|
||||
WizardState::load(&root).ok_or("No wizard active. Run `storkit init` to begin setup.")?;
|
||||
WizardState::load(&root).ok_or("No wizard active. Run `huskies init` to begin setup.")?;
|
||||
Ok(format_wizard_state(&state))
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ pub(crate) fn is_bare_project(project_root: &Path) -> bool {
|
||||
.filter_map(|e| e.ok())
|
||||
.map(|e| e.file_name().to_string_lossy().to_string())
|
||||
.collect();
|
||||
// A bare project only has storkit scaffolding and no real code
|
||||
// A bare project only has huskies scaffolding and no real code
|
||||
names.iter().all(|n| {
|
||||
n.starts_with('.')
|
||||
|| n == "CLAUDE.md"
|
||||
@@ -182,13 +182,13 @@ pub(crate) fn generation_hint(step: WizardStep, project_root: &Path) -> String {
|
||||
if bare {
|
||||
"This is a bare project with no existing code. Ask the user what they want \
|
||||
to build — the project's purpose, goals, target users, and key features. \
|
||||
Then generate `.storkit/specs/00_CONTEXT.md` from their answers covering:\n\
|
||||
Then generate `.huskies/specs/00_CONTEXT.md` from their answers covering:\n\
|
||||
- High-level goal of the project\n\
|
||||
- Core features\n\
|
||||
- Domain concepts and entities\n\
|
||||
- Glossary of abbreviations and technical terms".to_string()
|
||||
} else {
|
||||
"Read the project source tree and generate a `.storkit/specs/00_CONTEXT.md` describing:\n\
|
||||
"Read the project source tree and generate a `.huskies/specs/00_CONTEXT.md` describing:\n\
|
||||
- High-level goal of the project\n\
|
||||
- Core features\n\
|
||||
- Domain concepts and entities\n\
|
||||
@@ -198,14 +198,14 @@ pub(crate) fn generation_hint(step: WizardStep, project_root: &Path) -> String {
|
||||
WizardStep::Stack => {
|
||||
if bare {
|
||||
"This is a bare project with no existing code. Ask the user what language, \
|
||||
frameworks, and tools they plan to use. Then generate `.storkit/specs/tech/STACK.md` \
|
||||
frameworks, and tools they plan to use. Then generate `.huskies/specs/tech/STACK.md` \
|
||||
from their answers covering:\n\
|
||||
- Language, frameworks, and runtimes\n\
|
||||
- Coding standards and linting rules\n\
|
||||
- Quality gates (commands that must pass before merging)\n\
|
||||
- Approved libraries and their purpose".to_string()
|
||||
} else {
|
||||
"Read the project source tree and generate a `.storkit/specs/tech/STACK.md` describing:\n\
|
||||
"Read the project source tree and generate a `.huskies/specs/tech/STACK.md` describing:\n\
|
||||
- Language, frameworks, and runtimes\n\
|
||||
- Coding standards and linting rules\n\
|
||||
- Quality gates (commands that must pass before merging)\n\
|
||||
@@ -262,7 +262,7 @@ pub(crate) fn generation_hint(step: WizardStep, project_root: &Path) -> String {
|
||||
"Generate a `script/test_coverage` shell script (#!/usr/bin/env bash, set -euo pipefail) that generates a test coverage report (e.g. `cargo llvm-cov nextest` or `npm run coverage`).".to_string()
|
||||
}
|
||||
}
|
||||
WizardStep::Scaffold => "Scaffold step is handled automatically by `storkit init`.".to_string(),
|
||||
WizardStep::Scaffold => "Scaffold step is handled automatically by `huskies init`.".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,7 +391,7 @@ mod tests {
|
||||
|
||||
fn setup(dir: &TempDir) -> AppContext {
|
||||
let root = dir.path().to_path_buf();
|
||||
std::fs::create_dir_all(root.join(".storkit")).unwrap();
|
||||
std::fs::create_dir_all(root.join(".huskies")).unwrap();
|
||||
WizardState::init_if_missing(&root);
|
||||
AppContext::new_test(root)
|
||||
}
|
||||
@@ -408,7 +408,7 @@ mod tests {
|
||||
#[test]
|
||||
fn wizard_status_no_wizard_returns_error() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
std::fs::create_dir_all(dir.path().join(".storkit")).unwrap();
|
||||
std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
|
||||
let ctx = AppContext::new_test(dir.path().to_path_buf());
|
||||
assert!(tool_wizard_status(&ctx).is_err());
|
||||
}
|
||||
@@ -453,7 +453,7 @@ mod tests {
|
||||
// File should now exist.
|
||||
let context_path = dir
|
||||
.path()
|
||||
.join(".storkit")
|
||||
.join(".huskies")
|
||||
.join("specs")
|
||||
.join("00_CONTEXT.md");
|
||||
assert!(context_path.exists());
|
||||
@@ -472,7 +472,7 @@ mod tests {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let ctx = setup(&dir);
|
||||
// Pre-create the specs directory and file.
|
||||
let specs_dir = dir.path().join(".storkit").join("specs");
|
||||
let specs_dir = dir.path().join(".huskies").join("specs");
|
||||
std::fs::create_dir_all(&specs_dir).unwrap();
|
||||
let context_path = specs_dir.join("00_CONTEXT.md");
|
||||
std::fs::write(&context_path, "original content").unwrap();
|
||||
@@ -550,7 +550,7 @@ mod tests {
|
||||
#[test]
|
||||
fn is_bare_project_detects_scaffold_only_dir() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
std::fs::create_dir_all(dir.path().join(".storkit")).unwrap();
|
||||
std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
|
||||
std::fs::write(dir.path().join("CLAUDE.md"), "# Claude").unwrap();
|
||||
std::fs::write(dir.path().join("README.md"), "# Readme").unwrap();
|
||||
std::fs::create_dir_all(dir.path().join("script")).unwrap();
|
||||
@@ -560,7 +560,7 @@ mod tests {
|
||||
#[test]
|
||||
fn is_bare_project_false_when_source_files_exist() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
std::fs::create_dir_all(dir.path().join(".storkit")).unwrap();
|
||||
std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
|
||||
std::fs::write(dir.path().join("Cargo.toml"), "[package]").unwrap();
|
||||
assert!(!is_bare_project(dir.path()));
|
||||
}
|
||||
@@ -576,7 +576,7 @@ mod tests {
|
||||
fn generation_hint_bare_context_asks_user() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
// Bare project — only scaffolding
|
||||
std::fs::create_dir_all(dir.path().join(".storkit")).unwrap();
|
||||
std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
|
||||
let hint = generation_hint(WizardStep::Context, dir.path());
|
||||
assert!(hint.contains("bare project"));
|
||||
assert!(hint.contains("Ask the user"));
|
||||
@@ -585,7 +585,7 @@ mod tests {
|
||||
#[test]
|
||||
fn generation_hint_bare_stack_asks_user() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
std::fs::create_dir_all(dir.path().join(".storkit")).unwrap();
|
||||
std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
|
||||
let hint = generation_hint(WizardStep::Stack, dir.path());
|
||||
assert!(hint.contains("bare project"));
|
||||
assert!(hint.contains("Ask the user"));
|
||||
@@ -594,7 +594,7 @@ mod tests {
|
||||
#[test]
|
||||
fn generation_hint_bare_test_script_references_stack() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
std::fs::create_dir_all(dir.path().join(".storkit")).unwrap();
|
||||
std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
|
||||
let hint = generation_hint(WizardStep::TestScript, dir.path());
|
||||
assert!(hint.contains("bare project"));
|
||||
assert!(hint.contains("STACK.md"));
|
||||
@@ -603,7 +603,7 @@ mod tests {
|
||||
#[test]
|
||||
fn generation_hint_bare_release_script_references_stack() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
std::fs::create_dir_all(dir.path().join(".storkit")).unwrap();
|
||||
std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
|
||||
let hint = generation_hint(WizardStep::ReleaseScript, dir.path());
|
||||
assert!(hint.contains("bare project"));
|
||||
assert!(hint.contains("STACK.md"));
|
||||
@@ -612,7 +612,7 @@ mod tests {
|
||||
#[test]
|
||||
fn generation_hint_bare_test_coverage_references_stack() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
std::fs::create_dir_all(dir.path().join(".storkit")).unwrap();
|
||||
std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
|
||||
let hint = generation_hint(WizardStep::TestCoverage, dir.path());
|
||||
assert!(hint.contains("bare project"));
|
||||
assert!(hint.contains("STACK.md"));
|
||||
|
||||
@@ -47,11 +47,11 @@ pub fn parse_port(value: Option<String>) -> u16 {
|
||||
}
|
||||
|
||||
pub fn resolve_port() -> u16 {
|
||||
parse_port(std::env::var("STORKIT_PORT").ok())
|
||||
parse_port(std::env::var("HUSKIES_PORT").ok())
|
||||
}
|
||||
|
||||
pub fn write_port_file(dir: &Path, port: u16) -> Option<PathBuf> {
|
||||
let path = dir.join(".storkit_port");
|
||||
let path = dir.join(".huskies_port");
|
||||
std::fs::write(&path, port.to_string()).ok()?;
|
||||
Some(path)
|
||||
}
|
||||
@@ -156,7 +156,7 @@ pub fn build_openapi_service(ctx: Arc<AppContext>) -> (ApiService, ApiService) {
|
||||
);
|
||||
|
||||
let api_service =
|
||||
OpenApiService::new(api, "Storkit API", "1.0").server("http://127.0.0.1:3001/api");
|
||||
OpenApiService::new(api, "Huskies API", "1.0").server("http://127.0.0.1:3001/api");
|
||||
|
||||
let docs_api = (
|
||||
ProjectApi { ctx: ctx.clone() },
|
||||
@@ -172,7 +172,7 @@ pub fn build_openapi_service(ctx: Arc<AppContext>) -> (ApiService, ApiService) {
|
||||
);
|
||||
|
||||
let docs_service =
|
||||
OpenApiService::new(docs_api, "Storkit API", "1.0").server("http://127.0.0.1:3001/api");
|
||||
OpenApiService::new(docs_api, "Huskies API", "1.0").server("http://127.0.0.1:3001/api");
|
||||
|
||||
(api_service, docs_service)
|
||||
}
|
||||
@@ -221,7 +221,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn resolve_port_returns_a_valid_port() {
|
||||
// Exercises the resolve_port code path (reads STORKIT_PORT env var or defaults).
|
||||
// Exercises the resolve_port code path (reads HUSKIES_PORT env var or defaults).
|
||||
let port = resolve_port();
|
||||
assert!(port > 0);
|
||||
}
|
||||
|
||||
@@ -289,7 +289,7 @@ pub async fn oauth_callback(
|
||||
html_response(
|
||||
StatusCode::OK,
|
||||
"Authenticated!",
|
||||
"Claude OAuth login successful. You can close this tab and return to Storkit.",
|
||||
"Claude OAuth login successful. You can close this tab and return to Huskies.",
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -237,7 +237,7 @@ mod tests {
|
||||
#[test]
|
||||
fn editor_command_survives_reload() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let store_path = dir.path().join(".storkit_store.json");
|
||||
let store_path = dir.path().join(".huskies_store.json");
|
||||
|
||||
{
|
||||
let ctx = AppContext::new_test(dir.path().to_path_buf());
|
||||
|
||||
@@ -168,7 +168,7 @@ mod tests {
|
||||
fn setup() -> (TempDir, TestClient<impl poem::Endpoint>) {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let root = dir.path().to_path_buf();
|
||||
std::fs::create_dir_all(root.join(".storkit")).unwrap();
|
||||
std::fs::create_dir_all(root.join(".huskies")).unwrap();
|
||||
|
||||
let ctx = Arc::new(AppContext::new_test(root.clone()));
|
||||
let api = WizardApi { ctx };
|
||||
|
||||
@@ -24,7 +24,7 @@ pub fn create_bug_file(
|
||||
}
|
||||
|
||||
let filename = format!("{bug_number}_bug_{slug}.md");
|
||||
let bugs_dir = root.join(".storkit").join("work").join("1_backlog");
|
||||
let bugs_dir = root.join(".huskies").join("work").join("1_backlog");
|
||||
fs::create_dir_all(&bugs_dir)
|
||||
.map_err(|e| format!("Failed to create backlog directory: {e}"))?;
|
||||
|
||||
@@ -88,7 +88,7 @@ pub fn create_spike_file(
|
||||
}
|
||||
|
||||
let filename = format!("{spike_number}_spike_{slug}.md");
|
||||
let backlog_dir = root.join(".storkit").join("work").join("1_backlog");
|
||||
let backlog_dir = root.join(".huskies").join("work").join("1_backlog");
|
||||
fs::create_dir_all(&backlog_dir)
|
||||
.map_err(|e| format!("Failed to create backlog directory: {e}"))?;
|
||||
|
||||
@@ -151,7 +151,7 @@ pub fn create_refactor_file(
|
||||
}
|
||||
|
||||
let filename = format!("{refactor_number}_refactor_{slug}.md");
|
||||
let backlog_dir = root.join(".storkit").join("work").join("1_backlog");
|
||||
let backlog_dir = root.join(".huskies").join("work").join("1_backlog");
|
||||
fs::create_dir_all(&backlog_dir)
|
||||
.map_err(|e| format!("Failed to create backlog directory: {e}"))?;
|
||||
|
||||
@@ -227,7 +227,7 @@ fn extract_bug_name(path: &Path) -> Option<String> {
|
||||
///
|
||||
/// Returns a sorted list of `(bug_id, name)` pairs.
|
||||
pub fn list_bug_files(root: &Path) -> Result<Vec<(String, String)>, String> {
|
||||
let backlog_dir = root.join(".storkit").join("work").join("1_backlog");
|
||||
let backlog_dir = root.join(".huskies").join("work").join("1_backlog");
|
||||
if !backlog_dir.exists() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
@@ -277,7 +277,7 @@ fn is_refactor_item(stem: &str) -> bool {
|
||||
///
|
||||
/// Returns a sorted list of `(refactor_id, name)` pairs.
|
||||
pub fn list_refactor_files(root: &Path) -> Result<Vec<(String, String)>, String> {
|
||||
let backlog_dir = root.join(".storkit").join("work").join("1_backlog");
|
||||
let backlog_dir = root.join(".huskies").join("work").join("1_backlog");
|
||||
if !backlog_dir.exists() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
@@ -357,7 +357,7 @@ mod tests {
|
||||
#[test]
|
||||
fn next_item_number_increments_from_existing_bugs() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let backlog = tmp.path().join(".storkit/work/1_backlog");
|
||||
let backlog = tmp.path().join(".huskies/work/1_backlog");
|
||||
fs::create_dir_all(&backlog).unwrap();
|
||||
fs::write(backlog.join("1_bug_crash.md"), "").unwrap();
|
||||
fs::write(backlog.join("3_bug_another.md"), "").unwrap();
|
||||
@@ -367,8 +367,8 @@ mod tests {
|
||||
#[test]
|
||||
fn next_item_number_scans_archived_too() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let backlog = tmp.path().join(".storkit/work/1_backlog");
|
||||
let archived = tmp.path().join(".storkit/work/5_done");
|
||||
let backlog = tmp.path().join(".huskies/work/1_backlog");
|
||||
let archived = tmp.path().join(".huskies/work/5_done");
|
||||
fs::create_dir_all(&backlog).unwrap();
|
||||
fs::create_dir_all(&archived).unwrap();
|
||||
fs::write(archived.join("5_bug_old.md"), "").unwrap();
|
||||
@@ -385,8 +385,8 @@ mod tests {
|
||||
#[test]
|
||||
fn list_bug_files_excludes_archive_subdir() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let backlog_dir = tmp.path().join(".storkit/work/1_backlog");
|
||||
let archived_dir = tmp.path().join(".storkit/work/5_done");
|
||||
let backlog_dir = tmp.path().join(".huskies/work/1_backlog");
|
||||
let archived_dir = tmp.path().join(".huskies/work/5_done");
|
||||
fs::create_dir_all(&backlog_dir).unwrap();
|
||||
fs::create_dir_all(&archived_dir).unwrap();
|
||||
fs::write(backlog_dir.join("1_bug_open.md"), "# Bug 1: Open Bug\n").unwrap();
|
||||
@@ -401,7 +401,7 @@ mod tests {
|
||||
#[test]
|
||||
fn list_bug_files_sorted_by_id() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let backlog_dir = tmp.path().join(".storkit/work/1_backlog");
|
||||
let backlog_dir = tmp.path().join(".huskies/work/1_backlog");
|
||||
fs::create_dir_all(&backlog_dir).unwrap();
|
||||
fs::write(backlog_dir.join("3_bug_third.md"), "# Bug 3: Third\n").unwrap();
|
||||
fs::write(backlog_dir.join("1_bug_first.md"), "# Bug 1: First\n").unwrap();
|
||||
@@ -443,7 +443,7 @@ mod tests {
|
||||
|
||||
let filepath = tmp
|
||||
.path()
|
||||
.join(".storkit/work/1_backlog/1_bug_login_crash.md");
|
||||
.join(".huskies/work/1_backlog/1_bug_login_crash.md");
|
||||
assert!(filepath.exists());
|
||||
let contents = fs::read_to_string(&filepath).unwrap();
|
||||
assert!(
|
||||
@@ -487,7 +487,7 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let filepath = tmp.path().join(".storkit/work/1_backlog/1_bug_some_bug.md");
|
||||
let filepath = tmp.path().join(".huskies/work/1_backlog/1_bug_some_bug.md");
|
||||
let contents = fs::read_to_string(&filepath).unwrap();
|
||||
assert!(
|
||||
contents.starts_with("---\nname: \"Some Bug\"\n---"),
|
||||
@@ -509,7 +509,7 @@ mod tests {
|
||||
|
||||
let filepath = tmp
|
||||
.path()
|
||||
.join(".storkit/work/1_backlog/1_spike_filesystem_watcher_architecture.md");
|
||||
.join(".huskies/work/1_backlog/1_spike_filesystem_watcher_architecture.md");
|
||||
assert!(filepath.exists());
|
||||
let contents = fs::read_to_string(&filepath).unwrap();
|
||||
assert!(
|
||||
@@ -533,7 +533,7 @@ mod tests {
|
||||
create_spike_file(tmp.path(), "FS Watcher Spike", Some(description)).unwrap();
|
||||
|
||||
let filepath =
|
||||
tmp.path().join(".storkit/work/1_backlog/1_spike_fs_watcher_spike.md");
|
||||
tmp.path().join(".huskies/work/1_backlog/1_spike_fs_watcher_spike.md");
|
||||
let contents = fs::read_to_string(&filepath).unwrap();
|
||||
assert!(contents.contains(description));
|
||||
}
|
||||
@@ -543,7 +543,7 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
create_spike_file(tmp.path(), "My Spike", None).unwrap();
|
||||
|
||||
let filepath = tmp.path().join(".storkit/work/1_backlog/1_spike_my_spike.md");
|
||||
let filepath = tmp.path().join(".huskies/work/1_backlog/1_spike_my_spike.md");
|
||||
let contents = fs::read_to_string(&filepath).unwrap();
|
||||
// Should have placeholder TBD in Question section
|
||||
assert!(contents.contains("## Question\n\n- TBD\n"));
|
||||
@@ -564,7 +564,7 @@ mod tests {
|
||||
let result = create_spike_file(tmp.path(), name, None);
|
||||
assert!(result.is_ok(), "create_spike_file failed: {result:?}");
|
||||
|
||||
let backlog = tmp.path().join(".storkit/work/1_backlog");
|
||||
let backlog = tmp.path().join(".huskies/work/1_backlog");
|
||||
let spike_id = result.unwrap();
|
||||
let filename = format!("{spike_id}.md");
|
||||
let contents = fs::read_to_string(backlog.join(&filename)).unwrap();
|
||||
@@ -576,7 +576,7 @@ mod tests {
|
||||
#[test]
|
||||
fn create_spike_file_increments_from_existing_items() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let backlog = tmp.path().join(".storkit/work/1_backlog");
|
||||
let backlog = tmp.path().join(".huskies/work/1_backlog");
|
||||
fs::create_dir_all(&backlog).unwrap();
|
||||
fs::write(backlog.join("5_story_existing.md"), "").unwrap();
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ fn load_stage_items(
|
||||
agent_map: &HashMap<String, AgentAssignment>,
|
||||
) -> Result<Vec<UpcomingStory>, String> {
|
||||
let root = ctx.state.get_project_root()?;
|
||||
let dir = root.join(".storkit").join("work").join(stage_dir);
|
||||
let dir = root.join(".huskies").join("work").join(stage_dir);
|
||||
|
||||
if !dir.exists() {
|
||||
return Ok(Vec::new());
|
||||
@@ -166,8 +166,8 @@ pub fn validate_story_dirs(
|
||||
|
||||
// Directories to validate: work/2_current/ + work/1_backlog/
|
||||
let dirs_to_validate: Vec<PathBuf> = vec![
|
||||
root.join(".storkit").join("work").join("2_current"),
|
||||
root.join(".storkit").join("work").join("1_backlog"),
|
||||
root.join(".huskies").join("work").join("2_current"),
|
||||
root.join(".huskies").join("work").join("1_backlog"),
|
||||
];
|
||||
|
||||
for dir in &dirs_to_validate {
|
||||
@@ -230,7 +230,7 @@ pub fn validate_story_dirs(
|
||||
/// Searches in priority order: 2_current, 1_backlog, 3_qa, 4_merge, 5_done, 6_archived.
|
||||
pub(super) fn find_story_file(project_root: &Path, story_id: &str) -> Result<PathBuf, String> {
|
||||
let filename = format!("{story_id}.md");
|
||||
let sk = project_root.join(".storkit").join("work");
|
||||
let sk = project_root.join(".huskies").join("work");
|
||||
for stage in &["2_current", "1_backlog", "3_qa", "4_merge", "5_done", "6_archived"] {
|
||||
let path = sk.join(stage).join(&filename);
|
||||
if path.exists() {
|
||||
@@ -370,7 +370,7 @@ pub(super) fn slugify_name(name: &str) -> String {
|
||||
|
||||
/// Scan all `work/` subdirectories for the highest item number across all types (stories, bugs, spikes).
|
||||
pub(super) fn next_item_number(root: &std::path::Path) -> Result<u32, String> {
|
||||
let work_base = root.join(".storkit").join("work");
|
||||
let work_base = root.join(".huskies").join("work");
|
||||
let mut max_num: u32 = 0;
|
||||
|
||||
for subdir in &["1_backlog", "2_current", "3_qa", "4_merge", "5_done", "6_archived"] {
|
||||
@@ -413,7 +413,7 @@ mod tests {
|
||||
("4_merge", "40_story_merge"),
|
||||
("5_done", "50_story_done"),
|
||||
] {
|
||||
let dir = root.join(".storkit").join("work").join(stage);
|
||||
let dir = root.join(".huskies").join("work").join(stage);
|
||||
fs::create_dir_all(&dir).unwrap();
|
||||
fs::write(
|
||||
dir.join(format!("{id}.md")),
|
||||
@@ -445,7 +445,7 @@ mod tests {
|
||||
fn load_upcoming_returns_empty_when_no_dir() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path().to_path_buf();
|
||||
// No .storkit directory at all
|
||||
// No .huskies directory at all
|
||||
let ctx = crate::http::context::AppContext::new_test(root);
|
||||
let result = load_upcoming_stories(&ctx).unwrap();
|
||||
assert!(result.is_empty());
|
||||
@@ -456,7 +456,7 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path().to_path_buf();
|
||||
|
||||
let current = root.join(".storkit/work/2_current");
|
||||
let current = root.join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(
|
||||
current.join("10_story_test.md"),
|
||||
@@ -482,7 +482,7 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path().to_path_buf();
|
||||
|
||||
let current = root.join(".storkit/work/2_current");
|
||||
let current = root.join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(
|
||||
current.join("11_story_done.md"),
|
||||
@@ -507,7 +507,7 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path().to_path_buf();
|
||||
|
||||
let current = root.join(".storkit/work/2_current");
|
||||
let current = root.join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(
|
||||
current.join("12_story_pending.md"),
|
||||
@@ -529,7 +529,7 @@ mod tests {
|
||||
#[test]
|
||||
fn load_upcoming_parses_metadata() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let backlog = tmp.path().join(".storkit/work/1_backlog");
|
||||
let backlog = tmp.path().join(".huskies/work/1_backlog");
|
||||
fs::create_dir_all(&backlog).unwrap();
|
||||
fs::write(
|
||||
backlog.join("31_story_view_upcoming.md"),
|
||||
@@ -554,7 +554,7 @@ mod tests {
|
||||
#[test]
|
||||
fn load_upcoming_skips_non_md_files() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let backlog = tmp.path().join(".storkit/work/1_backlog");
|
||||
let backlog = tmp.path().join(".huskies/work/1_backlog");
|
||||
fs::create_dir_all(&backlog).unwrap();
|
||||
fs::write(backlog.join(".gitkeep"), "").unwrap();
|
||||
fs::write(
|
||||
@@ -572,8 +572,8 @@ mod tests {
|
||||
#[test]
|
||||
fn validate_story_dirs_valid_files() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let current = tmp.path().join(".storkit/work/2_current");
|
||||
let backlog = tmp.path().join(".storkit/work/1_backlog");
|
||||
let current = tmp.path().join(".huskies/work/2_current");
|
||||
let backlog = tmp.path().join(".huskies/work/1_backlog");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::create_dir_all(&backlog).unwrap();
|
||||
fs::write(
|
||||
@@ -596,7 +596,7 @@ mod tests {
|
||||
#[test]
|
||||
fn validate_story_dirs_missing_front_matter() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let current = tmp.path().join(".storkit/work/2_current");
|
||||
let current = tmp.path().join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(current.join("28_story_todos.md"), "# No front matter\n").unwrap();
|
||||
|
||||
@@ -609,7 +609,7 @@ mod tests {
|
||||
#[test]
|
||||
fn validate_story_dirs_missing_required_fields() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let current = tmp.path().join(".storkit/work/2_current");
|
||||
let current = tmp.path().join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(current.join("28_story_todos.md"), "---\n---\n# Story\n").unwrap();
|
||||
|
||||
@@ -667,7 +667,7 @@ mod tests {
|
||||
#[test]
|
||||
fn next_item_number_empty_dirs() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let base = tmp.path().join(".storkit/work/1_backlog");
|
||||
let base = tmp.path().join(".huskies/work/1_backlog");
|
||||
fs::create_dir_all(&base).unwrap();
|
||||
assert_eq!(next_item_number(tmp.path()).unwrap(), 1);
|
||||
}
|
||||
@@ -675,9 +675,9 @@ mod tests {
|
||||
#[test]
|
||||
fn next_item_number_scans_all_dirs() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let backlog = tmp.path().join(".storkit/work/1_backlog");
|
||||
let current = tmp.path().join(".storkit/work/2_current");
|
||||
let archived = tmp.path().join(".storkit/work/5_done");
|
||||
let backlog = tmp.path().join(".huskies/work/1_backlog");
|
||||
let current = tmp.path().join(".huskies/work/2_current");
|
||||
let archived = tmp.path().join(".huskies/work/5_done");
|
||||
fs::create_dir_all(&backlog).unwrap();
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::create_dir_all(&archived).unwrap();
|
||||
@@ -690,7 +690,7 @@ mod tests {
|
||||
#[test]
|
||||
fn next_item_number_no_work_dirs() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
// No .storkit at all
|
||||
// No .huskies at all
|
||||
assert_eq!(next_item_number(tmp.path()).unwrap(), 1);
|
||||
}
|
||||
|
||||
@@ -699,8 +699,8 @@ mod tests {
|
||||
#[test]
|
||||
fn find_story_file_searches_current_then_backlog() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let current = tmp.path().join(".storkit/work/2_current");
|
||||
let backlog = tmp.path().join(".storkit/work/1_backlog");
|
||||
let current = tmp.path().join(".huskies/work/2_current");
|
||||
let backlog = tmp.path().join(".huskies/work/1_backlog");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::create_dir_all(&backlog).unwrap();
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ pub fn create_story_file(
|
||||
}
|
||||
|
||||
let filename = format!("{story_number}_story_{slug}.md");
|
||||
let backlog_dir = root.join(".storkit").join("work").join("1_backlog");
|
||||
let backlog_dir = root.join(".huskies").join("work").join("1_backlog");
|
||||
fs::create_dir_all(&backlog_dir)
|
||||
.map_err(|e| format!("Failed to create backlog directory: {e}"))?;
|
||||
|
||||
@@ -281,7 +281,7 @@ mod tests {
|
||||
#[test]
|
||||
fn create_story_writes_correct_content() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let backlog = tmp.path().join(".storkit/work/1_backlog");
|
||||
let backlog = tmp.path().join(".huskies/work/1_backlog");
|
||||
fs::create_dir_all(&backlog).unwrap();
|
||||
fs::write(backlog.join("36_story_existing.md"), "").unwrap();
|
||||
|
||||
@@ -324,7 +324,7 @@ mod tests {
|
||||
let result = create_story_file(tmp.path(), name, None, None, false);
|
||||
assert!(result.is_ok(), "create_story_file failed: {result:?}");
|
||||
|
||||
let backlog = tmp.path().join(".storkit/work/1_backlog");
|
||||
let backlog = tmp.path().join(".huskies/work/1_backlog");
|
||||
let story_id = result.unwrap();
|
||||
let filename = format!("{story_id}.md");
|
||||
let contents = fs::read_to_string(backlog.join(&filename)).unwrap();
|
||||
@@ -336,7 +336,7 @@ mod tests {
|
||||
#[test]
|
||||
fn create_story_rejects_duplicate() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let backlog = tmp.path().join(".storkit/work/1_backlog");
|
||||
let backlog = tmp.path().join(".huskies/work/1_backlog");
|
||||
fs::create_dir_all(&backlog).unwrap();
|
||||
|
||||
let filepath = backlog.join("1_story_my_feature.md");
|
||||
@@ -352,7 +352,7 @@ mod tests {
|
||||
fn check_criterion_marks_first_unchecked() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
setup_git_repo(tmp.path());
|
||||
let current = tmp.path().join(".storkit/work/2_current");
|
||||
let current = tmp.path().join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
let filepath = current.join("1_test.md");
|
||||
fs::write(&filepath, story_with_criteria(3)).unwrap();
|
||||
@@ -379,7 +379,7 @@ mod tests {
|
||||
fn check_criterion_marks_second_unchecked() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
setup_git_repo(tmp.path());
|
||||
let current = tmp.path().join(".storkit/work/2_current");
|
||||
let current = tmp.path().join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
let filepath = current.join("2_test.md");
|
||||
fs::write(&filepath, story_with_criteria(3)).unwrap();
|
||||
@@ -406,7 +406,7 @@ mod tests {
|
||||
fn check_criterion_out_of_range_returns_error() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
setup_git_repo(tmp.path());
|
||||
let current = tmp.path().join(".storkit/work/2_current");
|
||||
let current = tmp.path().join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
let filepath = current.join("3_test.md");
|
||||
fs::write(&filepath, story_with_criteria(2)).unwrap();
|
||||
@@ -440,7 +440,7 @@ mod tests {
|
||||
#[test]
|
||||
fn add_criterion_appends_after_last_criterion() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let current = tmp.path().join(".storkit/work/2_current");
|
||||
let current = tmp.path().join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
let filepath = current.join("10_test.md");
|
||||
fs::write(&filepath, story_with_ac_section(&["First", "Second"])).unwrap();
|
||||
@@ -460,7 +460,7 @@ mod tests {
|
||||
#[test]
|
||||
fn add_criterion_to_empty_section() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let current = tmp.path().join(".storkit/work/2_current");
|
||||
let current = tmp.path().join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
let filepath = current.join("11_test.md");
|
||||
let content = "---\nname: Test\n---\n\n## Acceptance Criteria\n\n## Out of Scope\n\n- N/A\n";
|
||||
@@ -475,7 +475,7 @@ mod tests {
|
||||
#[test]
|
||||
fn add_criterion_missing_section_returns_error() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let current = tmp.path().join(".storkit/work/2_current");
|
||||
let current = tmp.path().join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
let filepath = current.join("12_test.md");
|
||||
fs::write(&filepath, "---\nname: Test\n---\n\nNo AC section here.\n").unwrap();
|
||||
@@ -490,7 +490,7 @@ mod tests {
|
||||
#[test]
|
||||
fn update_story_replaces_user_story_section() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let current = tmp.path().join(".storkit/work/2_current");
|
||||
let current = tmp.path().join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
let filepath = current.join("20_test.md");
|
||||
let content = "---\nname: T\n---\n\n## User Story\n\nOld text\n\n## Acceptance Criteria\n\n- [ ] AC\n";
|
||||
@@ -507,7 +507,7 @@ mod tests {
|
||||
#[test]
|
||||
fn update_story_replaces_description_section() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let current = tmp.path().join(".storkit/work/2_current");
|
||||
let current = tmp.path().join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
let filepath = current.join("21_test.md");
|
||||
let content = "---\nname: T\n---\n\n## Description\n\nOld description\n\n## Acceptance Criteria\n\n- [ ] AC\n";
|
||||
@@ -523,7 +523,7 @@ mod tests {
|
||||
#[test]
|
||||
fn update_story_no_args_returns_error() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let current = tmp.path().join(".storkit/work/2_current");
|
||||
let current = tmp.path().join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(current.join("22_test.md"), "---\nname: T\n---\n").unwrap();
|
||||
|
||||
@@ -535,7 +535,7 @@ mod tests {
|
||||
#[test]
|
||||
fn update_story_missing_section_returns_error() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let current = tmp.path().join(".storkit/work/2_current");
|
||||
let current = tmp.path().join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(
|
||||
current.join("23_test.md"),
|
||||
@@ -551,7 +551,7 @@ mod tests {
|
||||
#[test]
|
||||
fn update_story_sets_agent_front_matter_field() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let current = tmp.path().join(".storkit/work/2_current");
|
||||
let current = tmp.path().join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
let filepath = current.join("24_test.md");
|
||||
fs::write(&filepath, "---\nname: T\n---\n\n## User Story\n\nSome story\n").unwrap();
|
||||
@@ -568,7 +568,7 @@ mod tests {
|
||||
#[test]
|
||||
fn update_story_sets_arbitrary_front_matter_fields() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let current = tmp.path().join(".storkit/work/2_current");
|
||||
let current = tmp.path().join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
let filepath = current.join("25_test.md");
|
||||
fs::write(&filepath, "---\nname: T\n---\n\n## User Story\n\nSome story\n").unwrap();
|
||||
@@ -587,7 +587,7 @@ mod tests {
|
||||
#[test]
|
||||
fn update_story_front_matter_only_no_section_required() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let current = tmp.path().join(".storkit/work/2_current");
|
||||
let current = tmp.path().join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
// File without a User Story section — front matter update should succeed
|
||||
let filepath = current.join("26_test.md");
|
||||
@@ -605,7 +605,7 @@ mod tests {
|
||||
#[test]
|
||||
fn update_story_bool_front_matter_written_unquoted() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let current = tmp.path().join(".storkit/work/2_current");
|
||||
let current = tmp.path().join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
let filepath = current.join("27_test.md");
|
||||
fs::write(&filepath, "---\nname: T\n---\n\nNo sections.\n").unwrap();
|
||||
@@ -622,7 +622,7 @@ mod tests {
|
||||
#[test]
|
||||
fn update_story_integer_front_matter_written_unquoted() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let current = tmp.path().join(".storkit/work/2_current");
|
||||
let current = tmp.path().join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
let filepath = current.join("28_test.md");
|
||||
fs::write(&filepath, "---\nname: T\n---\n\nNo sections.\n").unwrap();
|
||||
@@ -639,7 +639,7 @@ mod tests {
|
||||
#[test]
|
||||
fn update_story_bool_front_matter_parseable_after_write() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let current = tmp.path().join(".storkit/work/2_current");
|
||||
let current = tmp.path().join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
let filepath = current.join("29_test.md");
|
||||
fs::write(&filepath, "---\nname: My Story\n---\n\nNo sections.\n").unwrap();
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::path::Path;
|
||||
|
||||
use super::{find_story_file, replace_or_append_section};
|
||||
|
||||
const TEST_RESULTS_MARKER: &str = "<!-- storkit-test-results:";
|
||||
const TEST_RESULTS_MARKER: &str = "<!-- huskies-test-results:";
|
||||
|
||||
/// Write (or overwrite) the `## Test Results` section in a story file.
|
||||
///
|
||||
@@ -160,7 +160,7 @@ mod tests {
|
||||
#[test]
|
||||
fn write_and_read_test_results_roundtrip() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let current = tmp.path().join(".storkit/work/2_current");
|
||||
let current = tmp.path().join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(
|
||||
current.join("1_story_test.md"),
|
||||
@@ -186,7 +186,7 @@ mod tests {
|
||||
#[test]
|
||||
fn write_test_results_creates_readable_section() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let current = tmp.path().join(".storkit/work/2_current");
|
||||
let current = tmp.path().join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
let story_path = current.join("2_story_check.md");
|
||||
fs::write(
|
||||
@@ -203,7 +203,7 @@ mod tests {
|
||||
assert!(contents.contains("✅ unit-pass"));
|
||||
assert!(contents.contains("❌ unit-fail"));
|
||||
assert!(contents.contains("assertion failed"));
|
||||
assert!(contents.contains("storkit-test-results:"));
|
||||
assert!(contents.contains("huskies-test-results:"));
|
||||
// Original content still present
|
||||
assert!(contents.contains("## Acceptance Criteria"));
|
||||
}
|
||||
@@ -211,12 +211,12 @@ mod tests {
|
||||
#[test]
|
||||
fn write_test_results_overwrites_existing_section() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let current = tmp.path().join(".storkit/work/2_current");
|
||||
let current = tmp.path().join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
let story_path = current.join("3_story_overwrite.md");
|
||||
fs::write(
|
||||
&story_path,
|
||||
"---\nname: Overwrite\n---\n# Story\n\n## Test Results\n\n<!-- storkit-test-results: {} -->\n\n### Unit Tests (0 passed, 0 failed)\n\n*No unit tests recorded.*\n",
|
||||
"---\nname: Overwrite\n---\n# Story\n\n## Test Results\n\n<!-- huskies-test-results: {} -->\n\n### Unit Tests (0 passed, 0 failed)\n\n*No unit tests recorded.*\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -233,7 +233,7 @@ mod tests {
|
||||
#[test]
|
||||
fn read_test_results_returns_none_when_no_section() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let current = tmp.path().join(".storkit/work/2_current");
|
||||
let current = tmp.path().join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(
|
||||
current.join("4_story_empty.md"),
|
||||
@@ -255,7 +255,7 @@ mod tests {
|
||||
#[test]
|
||||
fn write_test_results_finds_story_in_any_stage() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let qa_dir = tmp.path().join(".storkit/work/3_qa");
|
||||
let qa_dir = tmp.path().join(".huskies/work/3_qa");
|
||||
fs::create_dir_all(&qa_dir).unwrap();
|
||||
fs::write(
|
||||
qa_dir.join("5_story_qa.md"),
|
||||
@@ -280,7 +280,7 @@ mod tests {
|
||||
#[test]
|
||||
fn write_coverage_baseline_to_story_file_updates_front_matter() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let current = tmp.path().join(".storkit/work/2_current");
|
||||
let current = tmp.path().join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(
|
||||
current.join("6_story_cov.md"),
|
||||
|
||||
@@ -64,8 +64,8 @@ pub struct WizardStepInfo {
|
||||
/// - `token` streams partial model output.
|
||||
/// - `update` pushes the updated message history.
|
||||
/// - `error` reports a request or processing failure.
|
||||
/// - `work_item_changed` notifies that a `.storkit/work/` file changed.
|
||||
/// - `agent_config_changed` notifies that `.storkit/project.toml` was modified.
|
||||
/// - `work_item_changed` notifies that a `.huskies/work/` file changed.
|
||||
/// - `agent_config_changed` notifies that `.huskies/project.toml` was modified.
|
||||
enum WsResponse {
|
||||
Token {
|
||||
content: String,
|
||||
@@ -97,7 +97,7 @@ enum WsResponse {
|
||||
merge: Vec<crate::http::workflow::UpcomingStory>,
|
||||
done: Vec<crate::http::workflow::UpcomingStory>,
|
||||
},
|
||||
/// `.storkit/project.toml` was modified; the frontend should re-fetch the
|
||||
/// `.huskies/project.toml` was modified; the frontend should re-fetch the
|
||||
/// agent roster. Does NOT trigger a pipeline state refresh.
|
||||
AgentConfigChanged,
|
||||
/// An agent's state changed (started, stopped, completed, etc.).
|
||||
@@ -773,14 +773,14 @@ mod tests {
|
||||
stage: "2_current".to_string(),
|
||||
item_id: "42_story_foo".to_string(),
|
||||
action: "start".to_string(),
|
||||
commit_msg: "storkit: start 42_story_foo".to_string(),
|
||||
commit_msg: "huskies: start 42_story_foo".to_string(),
|
||||
};
|
||||
let json = serde_json::to_value(&resp).unwrap();
|
||||
assert_eq!(json["type"], "work_item_changed");
|
||||
assert_eq!(json["stage"], "2_current");
|
||||
assert_eq!(json["item_id"], "42_story_foo");
|
||||
assert_eq!(json["action"], "start");
|
||||
assert_eq!(json["commit_msg"], "storkit: start 42_story_foo");
|
||||
assert_eq!(json["commit_msg"], "huskies: start 42_story_foo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -901,7 +901,7 @@ mod tests {
|
||||
stage: "2_current".to_string(),
|
||||
item_id: "42_story_foo".to_string(),
|
||||
action: "start".to_string(),
|
||||
commit_msg: "storkit: start 42_story_foo".to_string(),
|
||||
commit_msg: "huskies: start 42_story_foo".to_string(),
|
||||
from_stage: None,
|
||||
};
|
||||
let ws_msg: Option<WsResponse> = evt.into();
|
||||
@@ -1166,7 +1166,7 @@ mod tests {
|
||||
|
||||
// Create minimal pipeline dirs so load_pipeline_state succeeds.
|
||||
for stage in &["1_backlog", "2_current", "3_qa", "4_merge"] {
|
||||
std::fs::create_dir_all(root.join(".storkit").join("work").join(stage)).unwrap();
|
||||
std::fs::create_dir_all(root.join(".huskies").join("work").join(stage)).unwrap();
|
||||
}
|
||||
|
||||
let ctx = Arc::new(AppContext::new_test(root));
|
||||
@@ -1381,7 +1381,7 @@ mod tests {
|
||||
stage: "2_current".to_string(),
|
||||
item_id: "99_story_test".to_string(),
|
||||
action: "start".to_string(),
|
||||
commit_msg: "storkit: start 99_story_test".to_string(),
|
||||
commit_msg: "huskies: start 99_story_test".to_string(),
|
||||
from_stage: None,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
@@ -15,11 +15,11 @@ pub fn resolve_cli_path(cwd: &Path, path_arg: &str) -> PathBuf {
|
||||
}
|
||||
|
||||
/// Walk from `start` up through parent directories, returning the first
|
||||
/// directory that contains a `.storkit/` subdirectory, or `None`.
|
||||
/// directory that contains a `.huskies/` subdirectory, or `None`.
|
||||
pub fn find_story_kit_root(start: &Path) -> Option<PathBuf> {
|
||||
let mut current = start.to_path_buf();
|
||||
loop {
|
||||
if current.join(".storkit").is_dir() {
|
||||
if current.join(".huskies").is_dir() {
|
||||
return Some(current);
|
||||
}
|
||||
if !current.pop() {
|
||||
@@ -79,7 +79,7 @@ mod tests {
|
||||
#[test]
|
||||
fn find_story_kit_root_returns_cwd_when_story_kit_in_cwd() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
std::fs::create_dir_all(tmp.path().join(".storkit")).unwrap();
|
||||
std::fs::create_dir_all(tmp.path().join(".huskies")).unwrap();
|
||||
|
||||
let result = find_story_kit_root(tmp.path());
|
||||
assert_eq!(result, Some(tmp.path().to_path_buf()));
|
||||
@@ -88,7 +88,7 @@ mod tests {
|
||||
#[test]
|
||||
fn find_story_kit_root_returns_parent_when_story_kit_in_parent() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
std::fs::create_dir_all(tmp.path().join(".storkit")).unwrap();
|
||||
std::fs::create_dir_all(tmp.path().join(".huskies")).unwrap();
|
||||
let child = tmp.path().join("subdir").join("nested");
|
||||
std::fs::create_dir_all(&child).unwrap();
|
||||
|
||||
@@ -107,9 +107,9 @@ mod tests {
|
||||
#[test]
|
||||
fn find_story_kit_root_prefers_nearest_ancestor() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
std::fs::create_dir_all(tmp.path().join(".storkit")).unwrap();
|
||||
std::fs::create_dir_all(tmp.path().join(".huskies")).unwrap();
|
||||
let child = tmp.path().join("inner");
|
||||
std::fs::create_dir_all(child.join(".storkit")).unwrap();
|
||||
std::fs::create_dir_all(child.join(".huskies")).unwrap();
|
||||
|
||||
let result = find_story_kit_root(&child);
|
||||
assert_eq!(result, Some(child));
|
||||
|
||||
+10
-10
@@ -33,7 +33,7 @@ pub(crate) async fn ensure_project_root_with_story_kit(
|
||||
fs::create_dir_all(&path)
|
||||
.map_err(|e| format!("Failed to create project directory: {}", e))?;
|
||||
}
|
||||
if !path.join(".storkit").is_dir() {
|
||||
if !path.join(".huskies").is_dir() {
|
||||
scaffold_story_kit(&path, port)?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -230,9 +230,9 @@ mod tests {
|
||||
assert!(content.contains("localhost"), "mcp.json should reference localhost");
|
||||
}
|
||||
|
||||
/// Regression test for bug 371: no-arg `storkit` in empty directory skips scaffold.
|
||||
/// `open_project` on a directory without `.storkit/` must create all required scaffold
|
||||
/// files — the same files that `storkit .` produces.
|
||||
/// Regression test for bug 371: no-arg `huskies` in empty directory skips scaffold.
|
||||
/// `open_project` on a directory without `.huskies/` must create all required scaffold
|
||||
/// files — the same files that `huskies .` produces.
|
||||
#[tokio::test]
|
||||
async fn open_project_on_empty_dir_creates_full_scaffold() {
|
||||
let dir = tempdir().unwrap();
|
||||
@@ -246,8 +246,8 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
project_dir.join(".storkit/project.toml").exists(),
|
||||
"open_project must create .storkit/project.toml"
|
||||
project_dir.join(".huskies/project.toml").exists(),
|
||||
"open_project must create .huskies/project.toml"
|
||||
);
|
||||
assert!(
|
||||
project_dir.join(".mcp.json").exists(),
|
||||
@@ -386,15 +386,15 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// .storkit/ should have been created automatically
|
||||
assert!(project_dir.join(".storkit").is_dir());
|
||||
// .huskies/ should have been created automatically
|
||||
assert!(project_dir.join(".huskies").is_dir());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn open_project_does_not_overwrite_existing_story_kit() {
|
||||
let dir = tempdir().unwrap();
|
||||
let project_dir = dir.path().join("myproject");
|
||||
let sk_dir = project_dir.join(".storkit");
|
||||
let sk_dir = project_dir.join(".huskies");
|
||||
fs::create_dir_all(&sk_dir).unwrap();
|
||||
let readme = sk_dir.join("README.md");
|
||||
fs::write(&readme, "custom content").unwrap();
|
||||
@@ -405,7 +405,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Existing .storkit/ content should not be overwritten
|
||||
// Existing .huskies/ content should not be overwritten
|
||||
assert_eq!(fs::read_to_string(&readme).unwrap(), "custom content");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
const STORY_KIT_README: &str = include_str!("../../../../.storkit/README.md");
|
||||
const STORY_KIT_README: &str = include_str!("../../../../.huskies/README.md");
|
||||
|
||||
const BOT_TOML_MATRIX_EXAMPLE: &str =
|
||||
include_str!("../../../../.storkit/bot.toml.matrix.example");
|
||||
include_str!("../../../../.huskies/bot.toml.matrix.example");
|
||||
const BOT_TOML_WHATSAPP_META_EXAMPLE: &str =
|
||||
include_str!("../../../../.storkit/bot.toml.whatsapp-meta.example");
|
||||
include_str!("../../../../.huskies/bot.toml.whatsapp-meta.example");
|
||||
const BOT_TOML_WHATSAPP_TWILIO_EXAMPLE: &str =
|
||||
include_str!("../../../../.storkit/bot.toml.whatsapp-twilio.example");
|
||||
const BOT_TOML_SLACK_EXAMPLE: &str = include_str!("../../../../.storkit/bot.toml.slack.example");
|
||||
include_str!("../../../../.huskies/bot.toml.whatsapp-twilio.example");
|
||||
const BOT_TOML_SLACK_EXAMPLE: &str = include_str!("../../../../.huskies/bot.toml.slack.example");
|
||||
|
||||
const STORY_KIT_CONTEXT: &str = "<!-- storkit:scaffold-template -->\n\
|
||||
const STORY_KIT_CONTEXT: &str = "<!-- huskies:scaffold-template -->\n\
|
||||
# Project Context\n\
|
||||
\n\
|
||||
## High-Level Goal\n\
|
||||
@@ -30,7 +30,7 @@ TODO: Define the key domain concepts and entities.\n\
|
||||
\n\
|
||||
TODO: Define abbreviations and technical terms.\n";
|
||||
|
||||
const STORY_KIT_STACK: &str = "<!-- storkit:scaffold-template -->\n\
|
||||
const STORY_KIT_STACK: &str = "<!-- huskies:scaffold-template -->\n\
|
||||
# Tech Stack & Constraints\n\
|
||||
\n\
|
||||
## Core Stack\n\
|
||||
@@ -51,16 +51,16 @@ TODO: List approved libraries and their purpose.\n";
|
||||
|
||||
const STORY_KIT_SCRIPT_TEST: &str = "#!/usr/bin/env bash\nset -euo pipefail\n\n# Add your project's test commands here.\n# Story Kit agents invoke this script as the canonical test runner.\n# Exit 0 on success, non-zero on failure.\necho \"No tests configured\"\n";
|
||||
|
||||
const STORY_KIT_CLAUDE_MD: &str = "<!-- storkit:scaffold-template -->\n\
|
||||
const STORY_KIT_CLAUDE_MD: &str = "<!-- huskies:scaffold-template -->\n\
|
||||
Never chain shell commands with `&&`, `||`, or `;` in a single Bash call. \
|
||||
The permission system validates the entire command string, and chained commands \
|
||||
won't match allow rules like `Bash(git *)`. Use separate Bash calls instead — \
|
||||
parallel calls work fine.\n\
|
||||
\n\
|
||||
Read .storkit/README.md to see our dev process.\n\
|
||||
Read .huskies/README.md to see our dev process.\n\
|
||||
\n\
|
||||
IMPORTANT: On your first conversation, call `wizard_status` to check if \
|
||||
project setup is complete. If not, read .storkit/README.md for the full \
|
||||
project setup is complete. If not, read .huskies/README.md for the full \
|
||||
setup wizard instructions and guide the user through it conversationally.\n";
|
||||
|
||||
const STORY_KIT_CLAUDE_SETTINGS: &str = r#"{
|
||||
@@ -94,11 +94,11 @@ const STORY_KIT_CLAUDE_SETTINGS: &str = r#"{
|
||||
"Bash(./script/test:*)",
|
||||
"Edit",
|
||||
"Write",
|
||||
"mcp__storkit__*"
|
||||
"mcp__huskies__*"
|
||||
]
|
||||
},
|
||||
"enabledMcpjsonServers": [
|
||||
"storkit"
|
||||
"huskies"
|
||||
]
|
||||
}
|
||||
"#;
|
||||
@@ -114,7 +114,7 @@ role = "Full-stack engineer. Implements features across all components."
|
||||
model = "sonnet"
|
||||
max_turns = 50
|
||||
max_budget_usd = 5.00
|
||||
prompt = "You are working in a git worktree on story {{story_id}}. Read CLAUDE.md first, then .storkit/README.md to understand the dev process. Follow the workflow through implementation and verification. The worktree and feature branch already exist - do not create them. Check .mcp.json for MCP tools. Do NOT accept the story or merge - commit your work and stop.\n\nIMPORTANT: Commit all your work before your process exits. The server will automatically run acceptance gates when your process exits.\n\nIf `script/test` still contains the generic 'No tests configured' stub, update it to run the project's actual test suite before starting implementation."
|
||||
prompt = "You are working in a git worktree on story {{story_id}}. Read CLAUDE.md first, then .huskies/README.md to understand the dev process. Follow the workflow through implementation and verification. The worktree and feature branch already exist - do not create them. Check .mcp.json for MCP tools. Do NOT accept the story or merge - commit your work and stop.\n\nIMPORTANT: Commit all your work before your process exits. The server will automatically run acceptance gates when your process exits.\n\nIf `script/test` still contains the generic 'No tests configured' stub, update it to run the project's actual test suite before starting implementation."
|
||||
system_prompt = "You are a full-stack engineer working autonomously in a git worktree. Follow the Story-Driven Test Workflow strictly. Commit all your work before finishing. Do not accept stories, move them to archived, or merge to master."
|
||||
|
||||
[[agent]]
|
||||
@@ -275,12 +275,12 @@ fn write_script_if_missing(path: &Path, content: &str) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write (or idempotently update) `.storkit/.gitignore` with Story Kit–specific
|
||||
/// ignore patterns for files that live inside the `.storkit/` directory.
|
||||
/// Patterns are relative to `.storkit/` as git resolves `.gitignore` files
|
||||
/// Write (or idempotently update) `.huskies/.gitignore` with Story Kit–specific
|
||||
/// ignore patterns for files that live inside the `.huskies/` directory.
|
||||
/// Patterns are relative to `.huskies/` as git resolves `.gitignore` files
|
||||
/// relative to the directory that contains them.
|
||||
fn write_story_kit_gitignore(root: &Path) -> Result<(), String> {
|
||||
// Entries that belong inside .storkit/.gitignore (relative to .storkit/).
|
||||
// Entries that belong inside .huskies/.gitignore (relative to .huskies/).
|
||||
let entries = [
|
||||
"bot.toml",
|
||||
"matrix_store/",
|
||||
@@ -299,10 +299,10 @@ fn write_story_kit_gitignore(root: &Path) -> Result<(), String> {
|
||||
"store.json",
|
||||
];
|
||||
|
||||
let gitignore_path = root.join(".storkit").join(".gitignore");
|
||||
let gitignore_path = root.join(".huskies").join(".gitignore");
|
||||
let existing = if gitignore_path.exists() {
|
||||
fs::read_to_string(&gitignore_path)
|
||||
.map_err(|e| format!("Failed to read .storkit/.gitignore: {}", e))?
|
||||
.map_err(|e| format!("Failed to read .huskies/.gitignore: {}", e))?
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
@@ -327,19 +327,19 @@ fn write_story_kit_gitignore(root: &Path) -> Result<(), String> {
|
||||
}
|
||||
|
||||
fs::write(&gitignore_path, new_content)
|
||||
.map_err(|e| format!("Failed to write .storkit/.gitignore: {}", e))?;
|
||||
.map_err(|e| format!("Failed to write .huskies/.gitignore: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Append root-level Story Kit entries to the project `.gitignore`.
|
||||
/// Only `.storkit_port` and `.mcp.json` remain here because they live at
|
||||
/// Only `.huskies_port` and `.mcp.json` remain here because they live at
|
||||
/// the project root and git does not support `../` patterns in `.gitignore`
|
||||
/// files, so they cannot be expressed in `.storkit/.gitignore`.
|
||||
/// `store.json` is excluded via `.storkit/.gitignore` since it now lives
|
||||
/// inside the `.storkit/` directory.
|
||||
/// files, so they cannot be expressed in `.huskies/.gitignore`.
|
||||
/// `store.json` is excluded via `.huskies/.gitignore` since it now lives
|
||||
/// inside the `.huskies/` directory.
|
||||
fn append_root_gitignore_entries(root: &Path) -> Result<(), String> {
|
||||
let entries = [".storkit_port", ".mcp.json"];
|
||||
let entries = [".huskies_port", ".mcp.json"];
|
||||
|
||||
let gitignore_path = root.join(".gitignore");
|
||||
let existing = if gitignore_path.exists() {
|
||||
@@ -375,7 +375,7 @@ fn append_root_gitignore_entries(root: &Path) -> Result<(), String> {
|
||||
}
|
||||
|
||||
pub(crate) fn scaffold_story_kit(root: &Path, port: u16) -> Result<(), String> {
|
||||
let story_kit_root = root.join(".storkit");
|
||||
let story_kit_root = root.join(".huskies");
|
||||
let specs_root = story_kit_root.join("specs");
|
||||
let tech_root = specs_root.join("tech");
|
||||
let functional_root = specs_root.join("functional");
|
||||
@@ -433,7 +433,7 @@ pub(crate) fn scaffold_story_kit(root: &Path, port: u16) -> Result<(), String> {
|
||||
// Only written when missing — never overwrites an existing file, because
|
||||
// the port is environment-specific and must not clobber a running instance.
|
||||
let mcp_content = format!(
|
||||
"{{\n \"mcpServers\": {{\n \"storkit\": {{\n \"type\": \"http\",\n \"url\": \"http://localhost:{port}/mcp\"\n }}\n }}\n}}\n"
|
||||
"{{\n \"mcpServers\": {{\n \"huskies\": {{\n \"type\": \"http\",\n \"url\": \"http://localhost:{port}/mcp\"\n }}\n }}\n}}\n"
|
||||
);
|
||||
write_file_if_missing(&root.join(".mcp.json"), &mcp_content)?;
|
||||
|
||||
@@ -462,7 +462,7 @@ pub(crate) fn scaffold_story_kit(root: &Path, port: u16) -> Result<(), String> {
|
||||
let add_output = std::process::Command::new("git")
|
||||
.args([
|
||||
"add",
|
||||
".storkit",
|
||||
".huskies",
|
||||
"script",
|
||||
".gitignore",
|
||||
"CLAUDE.md",
|
||||
@@ -481,7 +481,7 @@ pub(crate) fn scaffold_story_kit(root: &Path, port: u16) -> Result<(), String> {
|
||||
let commit_output = std::process::Command::new("git")
|
||||
.args([
|
||||
"-c",
|
||||
"user.email=storkit@localhost",
|
||||
"user.email=huskies@localhost",
|
||||
"-c",
|
||||
"user.name=Story Kit",
|
||||
"commit",
|
||||
@@ -514,12 +514,12 @@ mod tests {
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
assert!(dir.path().join(".storkit/README.md").exists());
|
||||
assert!(dir.path().join(".storkit/project.toml").exists());
|
||||
assert!(dir.path().join(".storkit/specs/00_CONTEXT.md").exists());
|
||||
assert!(dir.path().join(".storkit/specs/tech/STACK.md").exists());
|
||||
assert!(dir.path().join(".huskies/README.md").exists());
|
||||
assert!(dir.path().join(".huskies/project.toml").exists());
|
||||
assert!(dir.path().join(".huskies/specs/00_CONTEXT.md").exists());
|
||||
assert!(dir.path().join(".huskies/specs/tech/STACK.md").exists());
|
||||
// Old stories/ dirs should NOT be created
|
||||
assert!(!dir.path().join(".storkit/stories").exists());
|
||||
assert!(!dir.path().join(".huskies/stories").exists());
|
||||
assert!(dir.path().join("script/test").exists());
|
||||
}
|
||||
|
||||
@@ -537,7 +537,7 @@ mod tests {
|
||||
"6_archived",
|
||||
];
|
||||
for stage in &stages {
|
||||
let path = dir.path().join(".storkit/work").join(stage);
|
||||
let path = dir.path().join(".huskies/work").join(stage);
|
||||
assert!(path.is_dir(), "work/{} should be a directory", stage);
|
||||
assert!(
|
||||
path.join(".gitkeep").exists(),
|
||||
@@ -552,7 +552,7 @@ mod tests {
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
let content = fs::read_to_string(dir.path().join(".storkit/project.toml")).unwrap();
|
||||
let content = fs::read_to_string(dir.path().join(".huskies/project.toml")).unwrap();
|
||||
assert!(content.contains("[[agent]]"));
|
||||
assert!(content.contains("stage = \"coder\""));
|
||||
assert!(content.contains("stage = \"qa\""));
|
||||
@@ -565,8 +565,8 @@ mod tests {
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
let content = fs::read_to_string(dir.path().join(".storkit/specs/00_CONTEXT.md")).unwrap();
|
||||
assert!(content.contains("<!-- storkit:scaffold-template -->"));
|
||||
let content = fs::read_to_string(dir.path().join(".huskies/specs/00_CONTEXT.md")).unwrap();
|
||||
assert!(content.contains("<!-- huskies:scaffold-template -->"));
|
||||
assert!(content.contains("## High-Level Goal"));
|
||||
assert!(content.contains("## Core Features"));
|
||||
assert!(content.contains("## Domain Definition"));
|
||||
@@ -581,8 +581,8 @@ mod tests {
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
let content = fs::read_to_string(dir.path().join(".storkit/specs/tech/STACK.md")).unwrap();
|
||||
assert!(content.contains("<!-- storkit:scaffold-template -->"));
|
||||
let content = fs::read_to_string(dir.path().join(".huskies/specs/tech/STACK.md")).unwrap();
|
||||
assert!(content.contains("<!-- huskies:scaffold-template -->"));
|
||||
assert!(content.contains("## Core Stack"));
|
||||
assert!(content.contains("## Coding Standards"));
|
||||
assert!(content.contains("## Quality Gates"));
|
||||
@@ -612,7 +612,7 @@ mod tests {
|
||||
#[test]
|
||||
fn scaffold_story_kit_does_not_overwrite_existing() {
|
||||
let dir = tempdir().unwrap();
|
||||
let readme = dir.path().join(".storkit/README.md");
|
||||
let readme = dir.path().join(".huskies/README.md");
|
||||
fs::create_dir_all(readme.parent().unwrap()).unwrap();
|
||||
fs::write(&readme, "custom content").unwrap();
|
||||
|
||||
@@ -626,30 +626,30 @@ mod tests {
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
let readme_content = fs::read_to_string(dir.path().join(".storkit/README.md")).unwrap();
|
||||
let toml_content = fs::read_to_string(dir.path().join(".storkit/project.toml")).unwrap();
|
||||
let readme_content = fs::read_to_string(dir.path().join(".huskies/README.md")).unwrap();
|
||||
let toml_content = fs::read_to_string(dir.path().join(".huskies/project.toml")).unwrap();
|
||||
|
||||
// Run again — must not change content or add duplicate .gitignore entries
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
fs::read_to_string(dir.path().join(".storkit/README.md")).unwrap(),
|
||||
fs::read_to_string(dir.path().join(".huskies/README.md")).unwrap(),
|
||||
readme_content
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read_to_string(dir.path().join(".storkit/project.toml")).unwrap(),
|
||||
fs::read_to_string(dir.path().join(".huskies/project.toml")).unwrap(),
|
||||
toml_content
|
||||
);
|
||||
|
||||
let story_kit_gitignore =
|
||||
fs::read_to_string(dir.path().join(".storkit/.gitignore")).unwrap();
|
||||
fs::read_to_string(dir.path().join(".huskies/.gitignore")).unwrap();
|
||||
let count = story_kit_gitignore
|
||||
.lines()
|
||||
.filter(|l| l.trim() == "worktrees/")
|
||||
.count();
|
||||
assert_eq!(
|
||||
count, 1,
|
||||
".storkit/.gitignore should not have duplicate entries"
|
||||
".huskies/.gitignore should not have duplicate entries"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -699,43 +699,43 @@ mod tests {
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
// .storkit/.gitignore must contain relative patterns for files under .storkit/
|
||||
let sk_content = fs::read_to_string(dir.path().join(".storkit/.gitignore")).unwrap();
|
||||
// .huskies/.gitignore must contain relative patterns for files under .huskies/
|
||||
let sk_content = fs::read_to_string(dir.path().join(".huskies/.gitignore")).unwrap();
|
||||
assert!(sk_content.contains("worktrees/"));
|
||||
assert!(sk_content.contains("merge_workspace/"));
|
||||
assert!(sk_content.contains("coverage/"));
|
||||
assert!(sk_content.contains("matrix_history.json"));
|
||||
assert!(sk_content.contains("timers.json"));
|
||||
// Must NOT contain absolute .storkit/ prefixed paths
|
||||
assert!(!sk_content.contains(".storkit/"));
|
||||
// Must NOT contain absolute .huskies/ prefixed paths
|
||||
assert!(!sk_content.contains(".huskies/"));
|
||||
|
||||
// Root .gitignore must contain root-level storkit entries
|
||||
// Root .gitignore must contain root-level huskies entries
|
||||
let root_content = fs::read_to_string(dir.path().join(".gitignore")).unwrap();
|
||||
assert!(root_content.contains(".storkit_port"));
|
||||
// store.json now lives inside .storkit/ and must NOT appear in root .gitignore
|
||||
assert!(root_content.contains(".huskies_port"));
|
||||
// store.json now lives inside .huskies/ and must NOT appear in root .gitignore
|
||||
assert!(!root_content.contains("store.json"));
|
||||
// Root .gitignore must NOT contain .storkit/ sub-directory patterns
|
||||
assert!(!root_content.contains(".storkit/worktrees/"));
|
||||
assert!(!root_content.contains(".storkit/merge_workspace/"));
|
||||
assert!(!root_content.contains(".storkit/coverage/"));
|
||||
// store.json must be in .storkit/.gitignore instead
|
||||
// Root .gitignore must NOT contain .huskies/ sub-directory patterns
|
||||
assert!(!root_content.contains(".huskies/worktrees/"));
|
||||
assert!(!root_content.contains(".huskies/merge_workspace/"));
|
||||
assert!(!root_content.contains(".huskies/coverage/"));
|
||||
// store.json must be in .huskies/.gitignore instead
|
||||
assert!(sk_content.contains("store.json"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_story_kit_gitignore_does_not_duplicate_existing_entries() {
|
||||
let dir = tempdir().unwrap();
|
||||
// Pre-create .storkit dir and .gitignore with some entries already present
|
||||
fs::create_dir_all(dir.path().join(".storkit")).unwrap();
|
||||
// Pre-create .huskies dir and .gitignore with some entries already present
|
||||
fs::create_dir_all(dir.path().join(".huskies")).unwrap();
|
||||
fs::write(
|
||||
dir.path().join(".storkit/.gitignore"),
|
||||
dir.path().join(".huskies/.gitignore"),
|
||||
"worktrees/\ncoverage/\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
let content = fs::read_to_string(dir.path().join(".storkit/.gitignore")).unwrap();
|
||||
let content = fs::read_to_string(dir.path().join(".huskies/.gitignore")).unwrap();
|
||||
let worktrees_count = content.lines().filter(|l| l.trim() == "worktrees/").count();
|
||||
assert_eq!(worktrees_count, 1, "worktrees/ should not be duplicated");
|
||||
let coverage_count = content.lines().filter(|l| l.trim() == "coverage/").count();
|
||||
@@ -759,12 +759,12 @@ mod tests {
|
||||
|
||||
let content = fs::read_to_string(&claude_md).unwrap();
|
||||
assert!(
|
||||
content.contains("<!-- storkit:scaffold-template -->"),
|
||||
content.contains("<!-- huskies:scaffold-template -->"),
|
||||
"CLAUDE.md should contain the scaffold sentinel"
|
||||
);
|
||||
assert!(
|
||||
content.contains("Read .storkit/README.md"),
|
||||
"CLAUDE.md should include directive to read .storkit/README.md"
|
||||
content.contains("Read .huskies/README.md"),
|
||||
"CLAUDE.md should include directive to read .huskies/README.md"
|
||||
);
|
||||
assert!(
|
||||
content.contains("Never chain shell commands"),
|
||||
@@ -801,7 +801,7 @@ mod tests {
|
||||
let content = fs::read_to_string(&mcp_path).unwrap();
|
||||
assert!(content.contains("4242"), ".mcp.json should reference the given port");
|
||||
assert!(content.contains("localhost"), ".mcp.json should reference localhost");
|
||||
assert!(content.contains("storkit"), ".mcp.json should name the storkit server");
|
||||
assert!(content.contains("huskies"), ".mcp.json should name the huskies server");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1149,7 +1149,7 @@ mod tests {
|
||||
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
let content = fs::read_to_string(dir.path().join(".storkit/project.toml")).unwrap();
|
||||
let content = fs::read_to_string(dir.path().join(".huskies/project.toml")).unwrap();
|
||||
assert!(
|
||||
content.contains("[[component]]"),
|
||||
"project.toml should contain a component entry"
|
||||
@@ -1169,7 +1169,7 @@ mod tests {
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
let content = fs::read_to_string(dir.path().join(".storkit/project.toml")).unwrap();
|
||||
let content = fs::read_to_string(dir.path().join(".huskies/project.toml")).unwrap();
|
||||
assert!(
|
||||
content.contains("[[component]]"),
|
||||
"project.toml should always have at least one component"
|
||||
@@ -1188,7 +1188,7 @@ mod tests {
|
||||
#[test]
|
||||
fn scaffold_does_not_overwrite_existing_project_toml_with_components() {
|
||||
let dir = tempdir().unwrap();
|
||||
let sk_dir = dir.path().join(".storkit");
|
||||
let sk_dir = dir.path().join(".huskies");
|
||||
fs::create_dir_all(&sk_dir).unwrap();
|
||||
let existing = "[[component]]\nname = \"custom\"\npath = \".\"\nsetup = [\"make build\"]\n";
|
||||
fs::write(sk_dir.join("project.toml"), existing).unwrap();
|
||||
|
||||
+20
-20
@@ -4,7 +4,7 @@ use std::path::Path;
|
||||
/// Only untouched templates contain this marker — real project content
|
||||
/// will never include it, so it avoids false positives when the project
|
||||
/// itself is an "Agentic AI Code Assistant".
|
||||
const TEMPLATE_SENTINEL: &str = "<!-- storkit:scaffold-template -->";
|
||||
const TEMPLATE_SENTINEL: &str = "<!-- huskies:scaffold-template -->";
|
||||
|
||||
/// Marker found in the default `script/test` scaffold output.
|
||||
const TEMPLATE_MARKER_SCRIPT: &str = "No tests configured";
|
||||
@@ -18,7 +18,7 @@ pub struct OnboardingStatus {
|
||||
pub needs_stack: bool,
|
||||
/// True when `script/test` still contains the scaffold placeholder.
|
||||
pub needs_test_script: bool,
|
||||
/// True when `.storkit/project.toml` is missing or has no
|
||||
/// True when `.huskies/project.toml` is missing or has no
|
||||
/// `[[component]]` entries.
|
||||
pub needs_project_toml: bool,
|
||||
}
|
||||
@@ -33,7 +33,7 @@ impl OnboardingStatus {
|
||||
/// Inspect the project at `project_root` and determine which onboarding
|
||||
/// steps are still required.
|
||||
pub fn check_onboarding_status(project_root: &Path) -> OnboardingStatus {
|
||||
let story_kit = project_root.join(".storkit");
|
||||
let story_kit = project_root.join(".huskies");
|
||||
|
||||
OnboardingStatus {
|
||||
needs_context: is_template_or_missing(
|
||||
@@ -99,13 +99,13 @@ mod tests {
|
||||
|
||||
// Write content that includes the scaffold sentinel
|
||||
fs::write(
|
||||
root.join(".storkit/specs/00_CONTEXT.md"),
|
||||
"<!-- storkit:scaffold-template -->\n# Project Context\nPlaceholder...",
|
||||
root.join(".huskies/specs/00_CONTEXT.md"),
|
||||
"<!-- huskies:scaffold-template -->\n# Project Context\nPlaceholder...",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
root.join(".storkit/specs/tech/STACK.md"),
|
||||
"<!-- storkit:scaffold-template -->\n# Tech Stack\nPlaceholder...",
|
||||
root.join(".huskies/specs/tech/STACK.md"),
|
||||
"<!-- huskies:scaffold-template -->\n# Tech Stack\nPlaceholder...",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -123,12 +123,12 @@ mod tests {
|
||||
// Real project content that happens to mention "Agentic AI Code Assistant"
|
||||
// but does NOT contain the scaffold sentinel — should NOT trigger onboarding.
|
||||
fs::write(
|
||||
root.join(".storkit/specs/00_CONTEXT.md"),
|
||||
root.join(".huskies/specs/00_CONTEXT.md"),
|
||||
"# Project Context\nTo build a standalone Agentic AI Code Assistant application.",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
root.join(".storkit/specs/tech/STACK.md"),
|
||||
root.join(".huskies/specs/tech/STACK.md"),
|
||||
"# Tech Stack\nThis is an Agentic Code Assistant binary.",
|
||||
)
|
||||
.unwrap();
|
||||
@@ -145,12 +145,12 @@ mod tests {
|
||||
let root = setup_project(&dir);
|
||||
|
||||
fs::write(
|
||||
root.join(".storkit/specs/00_CONTEXT.md"),
|
||||
root.join(".huskies/specs/00_CONTEXT.md"),
|
||||
"# My Project\n\nThis is an e-commerce platform for selling widgets.",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
root.join(".storkit/specs/tech/STACK.md"),
|
||||
root.join(".huskies/specs/tech/STACK.md"),
|
||||
"# Tech Stack\n\n## Backend: Python + FastAPI\n## Frontend: React + TypeScript",
|
||||
)
|
||||
.unwrap();
|
||||
@@ -166,8 +166,8 @@ mod tests {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let root = setup_project(&dir);
|
||||
|
||||
fs::write(root.join(".storkit/specs/00_CONTEXT.md"), " \n").unwrap();
|
||||
fs::write(root.join(".storkit/specs/tech/STACK.md"), "").unwrap();
|
||||
fs::write(root.join(".huskies/specs/00_CONTEXT.md"), " \n").unwrap();
|
||||
fs::write(root.join(".huskies/specs/tech/STACK.md"), "").unwrap();
|
||||
|
||||
let status = check_onboarding_status(&root);
|
||||
assert!(status.needs_context);
|
||||
@@ -222,7 +222,7 @@ mod tests {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let root = setup_project(&dir);
|
||||
|
||||
fs::write(root.join(".storkit/project.toml"), "# empty config\n").unwrap();
|
||||
fs::write(root.join(".huskies/project.toml"), "# empty config\n").unwrap();
|
||||
|
||||
let status = check_onboarding_status(&root);
|
||||
assert!(status.needs_project_toml);
|
||||
@@ -234,7 +234,7 @@ mod tests {
|
||||
let root = setup_project(&dir);
|
||||
|
||||
fs::write(
|
||||
root.join(".storkit/project.toml"),
|
||||
root.join(".huskies/project.toml"),
|
||||
"[[component]]\nname = \"app\"\npath = \".\"\nsetup = [\"cargo check\"]\n",
|
||||
)
|
||||
.unwrap();
|
||||
@@ -252,12 +252,12 @@ mod tests {
|
||||
|
||||
// Write real content for the required onboarding files
|
||||
fs::write(
|
||||
root.join(".storkit/specs/00_CONTEXT.md"),
|
||||
root.join(".huskies/specs/00_CONTEXT.md"),
|
||||
"# My Project\n\nReal project context.",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
root.join(".storkit/specs/tech/STACK.md"),
|
||||
root.join(".huskies/specs/tech/STACK.md"),
|
||||
"# My Stack\n\nReal stack content.",
|
||||
)
|
||||
.unwrap();
|
||||
@@ -289,13 +289,13 @@ mod tests {
|
||||
|
||||
// Context still has sentinel
|
||||
fs::write(
|
||||
root.join(".storkit/specs/00_CONTEXT.md"),
|
||||
"<!-- storkit:scaffold-template -->\n# Project Context\nPlaceholder...",
|
||||
root.join(".huskies/specs/00_CONTEXT.md"),
|
||||
"<!-- huskies:scaffold-template -->\n# Project Context\nPlaceholder...",
|
||||
)
|
||||
.unwrap();
|
||||
// Stack is customised (no sentinel)
|
||||
fs::write(
|
||||
root.join(".storkit/specs/tech/STACK.md"),
|
||||
root.join(".huskies/specs/tech/STACK.md"),
|
||||
"# My Stack\nRuby on Rails + PostgreSQL",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -6,13 +6,13 @@ use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// Create a minimal storkit project directory structure under `dir`.
|
||||
/// Create a minimal huskies project directory structure under `dir`.
|
||||
///
|
||||
/// Creates `.storkit/specs/tech/` and `script/`, then returns the root path.
|
||||
/// Creates `.huskies/specs/tech/` and `script/`, then returns the root path.
|
||||
/// Used by onboarding and wizard tests.
|
||||
pub(crate) fn setup_project(dir: &TempDir) -> PathBuf {
|
||||
let root = dir.path().to_path_buf();
|
||||
let sk = root.join(".storkit");
|
||||
let sk = root.join(".huskies");
|
||||
fs::create_dir_all(sk.join("specs").join("tech")).unwrap();
|
||||
fs::create_dir_all(root.join("script")).unwrap();
|
||||
root
|
||||
|
||||
+57
-57
@@ -1,10 +1,10 @@
|
||||
//! Filesystem watcher for `.storkit/work/` and `.storkit/project.toml`.
|
||||
//! Filesystem watcher for `.huskies/work/` and `.huskies/project.toml`.
|
||||
//!
|
||||
//! Watches the work pipeline directories for file changes, infers the lifecycle
|
||||
//! stage from the target directory name, auto-commits with a deterministic message,
|
||||
//! and broadcasts a [`WatcherEvent`] to all connected WebSocket clients.
|
||||
//!
|
||||
//! Also watches `.storkit/project.toml` for modifications and broadcasts
|
||||
//! Also watches `.huskies/project.toml` for modifications and broadcasts
|
||||
//! [`WatcherEvent::ConfigChanged`] so the frontend can reload the agent roster
|
||||
//! without a server restart.
|
||||
//!
|
||||
@@ -48,7 +48,7 @@ pub enum WatcherEvent {
|
||||
/// `None` for creations, deletions, or synthetic events.
|
||||
from_stage: Option<String>,
|
||||
},
|
||||
/// `.storkit/project.toml` was modified at the project root (not inside a worktree).
|
||||
/// `.huskies/project.toml` was modified at the project root (not inside a worktree).
|
||||
ConfigChanged,
|
||||
/// An agent's state changed (started, stopped, completed, etc.).
|
||||
/// Triggers a pipeline state refresh so the frontend can update agent
|
||||
@@ -90,8 +90,8 @@ pub enum WatcherEvent {
|
||||
},
|
||||
}
|
||||
|
||||
/// Return `true` if `path` is the root-level `.storkit/project.toml`, i.e.
|
||||
/// `{git_root}/.storkit/project.toml`.
|
||||
/// Return `true` if `path` is the root-level `.huskies/project.toml`, i.e.
|
||||
/// `{git_root}/.huskies/project.toml`.
|
||||
///
|
||||
/// Returns `false` for paths inside worktree directories (paths containing
|
||||
/// a `worktrees` component).
|
||||
@@ -100,19 +100,19 @@ pub fn is_config_file(path: &Path, git_root: &Path) -> bool {
|
||||
if path.components().any(|c| c.as_os_str() == "worktrees") {
|
||||
return false;
|
||||
}
|
||||
let expected = git_root.join(".storkit").join("project.toml");
|
||||
let expected = git_root.join(".huskies").join("project.toml");
|
||||
path == expected
|
||||
}
|
||||
|
||||
/// Map a pipeline directory name to a (action, commit-message-prefix) pair.
|
||||
fn stage_metadata(stage: &str, item_id: &str) -> Option<(&'static str, String)> {
|
||||
let (action, prefix) = match stage {
|
||||
"1_backlog" => ("create", format!("storkit: create {item_id}")),
|
||||
"2_current" => ("start", format!("storkit: start {item_id}")),
|
||||
"3_qa" => ("qa", format!("storkit: queue {item_id} for QA")),
|
||||
"4_merge" => ("merge", format!("storkit: queue {item_id} for merge")),
|
||||
"5_done" => ("done", format!("storkit: done {item_id}")),
|
||||
"6_archived" => ("accept", format!("storkit: accept {item_id}")),
|
||||
"1_backlog" => ("create", format!("huskies: create {item_id}")),
|
||||
"2_current" => ("start", format!("huskies: start {item_id}")),
|
||||
"3_qa" => ("qa", format!("huskies: queue {item_id} for QA")),
|
||||
"4_merge" => ("merge", format!("huskies: queue {item_id} for merge")),
|
||||
"5_done" => ("done", format!("huskies: done {item_id}")),
|
||||
"6_archived" => ("accept", format!("huskies: accept {item_id}")),
|
||||
_ => return None,
|
||||
};
|
||||
Some((action, prefix))
|
||||
@@ -121,7 +121,7 @@ fn stage_metadata(stage: &str, item_id: &str) -> Option<(&'static str, String)>
|
||||
/// Return the pipeline stage name for a path if it is a `.md` file living
|
||||
/// directly inside one of the known work subdirectories, otherwise `None`.
|
||||
///
|
||||
/// Explicitly returns `None` for any path under `.storkit/worktrees/` so
|
||||
/// Explicitly returns `None` for any path under `.huskies/worktrees/` so
|
||||
/// that code changes made by agents in their isolated worktrees are never
|
||||
/// auto-committed to master by the watcher.
|
||||
fn stage_for_path(path: &Path) -> Option<String> {
|
||||
@@ -146,11 +146,11 @@ fn stage_for_path(path: &Path) -> Option<String> {
|
||||
|
||||
/// Stage all changes in the work directory and commit with the given message.
|
||||
///
|
||||
/// Uses `git add -A .storkit/work/` to catch both additions and deletions in
|
||||
/// Uses `git add -A .huskies/work/` to catch both additions and deletions in
|
||||
/// a single commit. Returns `Ok(true)` if a commit was made, `Ok(false)` if
|
||||
/// there was nothing to commit, and `Err` for unexpected failures.
|
||||
fn git_add_work_and_commit(git_root: &Path, message: &str) -> Result<bool, String> {
|
||||
let work_rel = PathBuf::from(".storkit").join("work");
|
||||
let work_rel = PathBuf::from(".huskies").join("work");
|
||||
|
||||
let add_out = std::process::Command::new("git")
|
||||
.args(["add", "-A"])
|
||||
@@ -199,7 +199,7 @@ fn should_commit_stage(stage: &str) -> bool {
|
||||
///
|
||||
/// Only files that still exist on disk are used to derive the commit message
|
||||
/// (they represent the destination of a move or a new file). Deletions are
|
||||
/// captured by `git add -A .storkit/work/` automatically.
|
||||
/// captured by `git add -A .huskies/work/` automatically.
|
||||
///
|
||||
/// Only terminal stages (`1_backlog` and `6_archived`) trigger git commits.
|
||||
/// All stages broadcast a [`WatcherEvent`] so the frontend stays in sync.
|
||||
@@ -240,7 +240,7 @@ fn flush_pending(
|
||||
(
|
||||
"remove",
|
||||
item.to_string(),
|
||||
format!("storkit: remove {item}"),
|
||||
format!("huskies: remove {item}"),
|
||||
)
|
||||
};
|
||||
|
||||
@@ -392,9 +392,9 @@ fn sweep_done_to_archived(work_dir: &Path, git_root: &Path, done_retention: Dura
|
||||
|
||||
/// Start the filesystem watcher on a dedicated OS thread.
|
||||
///
|
||||
/// `work_dir` — absolute path to `.storkit/work/` (watched recursively).
|
||||
/// `work_dir` — absolute path to `.huskies/work/` (watched recursively).
|
||||
/// `git_root` — project root (passed to `git` commands as cwd, and used to
|
||||
/// derive the config file path `.storkit/project.toml`).
|
||||
/// derive the config file path `.huskies/project.toml`).
|
||||
/// `event_tx` — broadcast sender; each connected WebSocket client holds a receiver.
|
||||
/// `watcher_config` — initial sweep configuration loaded from `project.toml`.
|
||||
pub fn start_watcher(
|
||||
@@ -421,8 +421,8 @@ pub fn start_watcher(
|
||||
return;
|
||||
}
|
||||
|
||||
// Also watch .storkit/project.toml for hot-reload of agent config.
|
||||
let config_file = git_root.join(".storkit").join("project.toml");
|
||||
// Also watch .huskies/project.toml for hot-reload of agent config.
|
||||
let config_file = git_root.join(".huskies").join("project.toml");
|
||||
if config_file.exists()
|
||||
&& let Err(e) = watcher.watch(&config_file, RecursiveMode::NonRecursive)
|
||||
{
|
||||
@@ -575,9 +575,9 @@ mod tests {
|
||||
.expect("git initial commit");
|
||||
}
|
||||
|
||||
/// Create the `.storkit/work/{stage}/` dir tree inside `root`.
|
||||
/// Create the `.huskies/work/{stage}/` dir tree inside `root`.
|
||||
fn make_stage_dir(root: &std::path::Path, stage: &str) -> PathBuf {
|
||||
let dir = root.join(".storkit").join("work").join(stage);
|
||||
let dir = root.join(".huskies").join("work").join(stage);
|
||||
fs::create_dir_all(&dir).expect("create stage dir");
|
||||
dir
|
||||
}
|
||||
@@ -591,7 +591,7 @@ mod tests {
|
||||
let stage_dir = make_stage_dir(tmp.path(), "2_current");
|
||||
fs::write(stage_dir.join("42_story_foo.md"), "---\nname: test\n---\n").unwrap();
|
||||
|
||||
let result = git_add_work_and_commit(tmp.path(), "storkit: start 42_story_foo");
|
||||
let result = git_add_work_and_commit(tmp.path(), "huskies: start 42_story_foo");
|
||||
assert_eq!(
|
||||
result,
|
||||
Ok(true),
|
||||
@@ -607,10 +607,10 @@ mod tests {
|
||||
fs::write(stage_dir.join("42_story_foo.md"), "---\nname: test\n---\n").unwrap();
|
||||
|
||||
// First commit — should succeed.
|
||||
git_add_work_and_commit(tmp.path(), "storkit: start 42_story_foo").unwrap();
|
||||
git_add_work_and_commit(tmp.path(), "huskies: start 42_story_foo").unwrap();
|
||||
|
||||
// Second call with no changes — should return Ok(false).
|
||||
let result = git_add_work_and_commit(tmp.path(), "storkit: start 42_story_foo");
|
||||
let result = git_add_work_and_commit(tmp.path(), "huskies: start 42_story_foo");
|
||||
assert_eq!(
|
||||
result,
|
||||
Ok(false),
|
||||
@@ -646,7 +646,7 @@ mod tests {
|
||||
assert_eq!(stage, "1_backlog");
|
||||
assert_eq!(item_id, "42_story_foo");
|
||||
assert_eq!(action, "create");
|
||||
assert_eq!(commit_msg, "storkit: create 42_story_foo");
|
||||
assert_eq!(commit_msg, "huskies: create 42_story_foo");
|
||||
}
|
||||
other => panic!("unexpected event: {other:?}"),
|
||||
}
|
||||
@@ -659,7 +659,7 @@ mod tests {
|
||||
.expect("git log");
|
||||
let log_msg = String::from_utf8_lossy(&log.stdout);
|
||||
assert!(
|
||||
log_msg.contains("storkit: create 42_story_foo"),
|
||||
log_msg.contains("huskies: create 42_story_foo"),
|
||||
"terminal stage should produce a git commit"
|
||||
);
|
||||
}
|
||||
@@ -691,7 +691,7 @@ mod tests {
|
||||
assert_eq!(stage, "2_current");
|
||||
assert_eq!(item_id, "42_story_foo");
|
||||
assert_eq!(action, "start");
|
||||
assert_eq!(commit_msg, "storkit: start 42_story_foo");
|
||||
assert_eq!(commit_msg, "huskies: start 42_story_foo");
|
||||
}
|
||||
other => panic!("unexpected event: {other:?}"),
|
||||
}
|
||||
@@ -704,7 +704,7 @@ mod tests {
|
||||
.expect("git log");
|
||||
let log_msg = String::from_utf8_lossy(&log.stdout);
|
||||
assert!(
|
||||
!log_msg.contains("storkit:"),
|
||||
!log_msg.contains("huskies:"),
|
||||
"intermediate stage should NOT produce a git commit"
|
||||
);
|
||||
}
|
||||
@@ -712,11 +712,11 @@ mod tests {
|
||||
#[test]
|
||||
fn flush_pending_broadcasts_for_all_pipeline_stages() {
|
||||
let stages = [
|
||||
("1_backlog", "create", "storkit: create 10_story_x"),
|
||||
("3_qa", "qa", "storkit: queue 10_story_x for QA"),
|
||||
("4_merge", "merge", "storkit: queue 10_story_x for merge"),
|
||||
("5_done", "done", "storkit: done 10_story_x"),
|
||||
("6_archived", "accept", "storkit: accept 10_story_x"),
|
||||
("1_backlog", "create", "huskies: create 10_story_x"),
|
||||
("3_qa", "qa", "huskies: queue 10_story_x for QA"),
|
||||
("4_merge", "merge", "huskies: queue 10_story_x for merge"),
|
||||
("5_done", "done", "huskies: done 10_story_x"),
|
||||
("6_archived", "accept", "huskies: accept 10_story_x"),
|
||||
];
|
||||
|
||||
for (stage, expected_action, expected_msg) in stages {
|
||||
@@ -754,7 +754,7 @@ mod tests {
|
||||
make_stage_dir(tmp.path(), "2_current");
|
||||
let deleted_path = tmp
|
||||
.path()
|
||||
.join(".storkit")
|
||||
.join(".huskies")
|
||||
.join("work")
|
||||
.join("2_current")
|
||||
.join("42_story_foo.md");
|
||||
@@ -785,7 +785,7 @@ mod tests {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
init_git_repo(tmp.path());
|
||||
// File sits in an unrecognised directory.
|
||||
let unknown_dir = tmp.path().join(".storkit").join("work").join("9_unknown");
|
||||
let unknown_dir = tmp.path().join(".huskies").join("work").join("9_unknown");
|
||||
fs::create_dir_all(&unknown_dir).unwrap();
|
||||
let path = unknown_dir.join("42_story_foo.md");
|
||||
fs::write(&path, "---\nname: test\n---\n").unwrap();
|
||||
@@ -961,7 +961,7 @@ mod tests {
|
||||
make_stage_dir(tmp.path(), "3_qa");
|
||||
let qa_path = tmp
|
||||
.path()
|
||||
.join(".storkit")
|
||||
.join(".huskies")
|
||||
.join("work")
|
||||
.join("3_qa")
|
||||
.join("42_story_foo.md");
|
||||
@@ -1015,7 +1015,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn stage_for_path_recognises_pipeline_dirs() {
|
||||
let base = PathBuf::from("/proj/.storkit/work");
|
||||
let base = PathBuf::from("/proj/.huskies/work");
|
||||
assert_eq!(
|
||||
stage_for_path(&base.join("2_current/42_story_foo.md")),
|
||||
Some("2_current".to_string())
|
||||
@@ -1037,7 +1037,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn stage_for_path_ignores_worktree_paths() {
|
||||
let worktrees = PathBuf::from("/proj/.storkit/worktrees");
|
||||
let worktrees = PathBuf::from("/proj/.huskies/worktrees");
|
||||
|
||||
// Code changes inside a worktree must be ignored.
|
||||
assert_eq!(
|
||||
@@ -1048,7 +1048,7 @@ mod tests {
|
||||
// Even if a worktree happens to contain a path component that looks
|
||||
// like a pipeline stage, it must still be ignored.
|
||||
assert_eq!(
|
||||
stage_for_path(&worktrees.join("42_story_foo/.storkit/work/2_current/42_story_foo.md")),
|
||||
stage_for_path(&worktrees.join("42_story_foo/.huskies/work/2_current/42_story_foo.md")),
|
||||
None,
|
||||
);
|
||||
|
||||
@@ -1056,7 +1056,7 @@ mod tests {
|
||||
// segment (not an exact component) must NOT be filtered out.
|
||||
assert_eq!(
|
||||
stage_for_path(&PathBuf::from(
|
||||
"/proj/.storkit/work/2_current/not_worktrees_story.md"
|
||||
"/proj/.huskies/work/2_current/not_worktrees_story.md"
|
||||
)),
|
||||
Some("2_current".to_string()),
|
||||
);
|
||||
@@ -1080,15 +1080,15 @@ mod tests {
|
||||
fn stage_metadata_returns_correct_actions() {
|
||||
let (action, msg) = stage_metadata("2_current", "42_story_foo").unwrap();
|
||||
assert_eq!(action, "start");
|
||||
assert_eq!(msg, "storkit: start 42_story_foo");
|
||||
assert_eq!(msg, "huskies: start 42_story_foo");
|
||||
|
||||
let (action, msg) = stage_metadata("5_done", "42_story_foo").unwrap();
|
||||
assert_eq!(action, "done");
|
||||
assert_eq!(msg, "storkit: done 42_story_foo");
|
||||
assert_eq!(msg, "huskies: done 42_story_foo");
|
||||
|
||||
let (action, msg) = stage_metadata("6_archived", "42_story_foo").unwrap();
|
||||
assert_eq!(action, "accept");
|
||||
assert_eq!(msg, "storkit: accept 42_story_foo");
|
||||
assert_eq!(msg, "huskies: accept 42_story_foo");
|
||||
|
||||
assert!(stage_metadata("unknown", "id").is_none());
|
||||
}
|
||||
@@ -1096,7 +1096,7 @@ mod tests {
|
||||
#[test]
|
||||
fn is_config_file_identifies_root_project_toml() {
|
||||
let git_root = PathBuf::from("/proj");
|
||||
let config = git_root.join(".storkit").join("project.toml");
|
||||
let config = git_root.join(".huskies").join("project.toml");
|
||||
assert!(is_config_file(&config, &git_root));
|
||||
}
|
||||
|
||||
@@ -1105,7 +1105,7 @@ mod tests {
|
||||
let git_root = PathBuf::from("/proj");
|
||||
// project.toml inside a worktree must NOT be treated as the root config.
|
||||
let worktree_config =
|
||||
PathBuf::from("/proj/.storkit/worktrees/42_story_foo/.storkit/project.toml");
|
||||
PathBuf::from("/proj/.huskies/worktrees/42_story_foo/.huskies/project.toml");
|
||||
assert!(!is_config_file(&worktree_config, &git_root));
|
||||
}
|
||||
|
||||
@@ -1114,11 +1114,11 @@ mod tests {
|
||||
let git_root = PathBuf::from("/proj");
|
||||
// Random files must not match.
|
||||
assert!(!is_config_file(
|
||||
&PathBuf::from("/proj/.storkit/work/2_current/42_story_foo.md"),
|
||||
&PathBuf::from("/proj/.huskies/work/2_current/42_story_foo.md"),
|
||||
&git_root
|
||||
));
|
||||
assert!(!is_config_file(
|
||||
&PathBuf::from("/proj/.storkit/README.md"),
|
||||
&PathBuf::from("/proj/.huskies/README.md"),
|
||||
&git_root
|
||||
));
|
||||
}
|
||||
@@ -1126,7 +1126,7 @@ mod tests {
|
||||
#[test]
|
||||
fn is_config_file_rejects_wrong_root() {
|
||||
let git_root = PathBuf::from("/proj");
|
||||
let other_root_config = PathBuf::from("/other/.storkit/project.toml");
|
||||
let other_root_config = PathBuf::from("/other/.huskies/project.toml");
|
||||
assert!(!is_config_file(&other_root_config, &git_root));
|
||||
}
|
||||
|
||||
@@ -1135,7 +1135,7 @@ mod tests {
|
||||
#[test]
|
||||
fn sweep_moves_old_items_to_archived() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let work_dir = tmp.path().join(".storkit").join("work");
|
||||
let work_dir = tmp.path().join(".huskies").join("work");
|
||||
let done_dir = work_dir.join("5_done");
|
||||
let archived_dir = work_dir.join("6_archived");
|
||||
fs::create_dir_all(&done_dir).unwrap();
|
||||
@@ -1165,7 +1165,7 @@ mod tests {
|
||||
#[test]
|
||||
fn sweep_keeps_recent_items_in_done() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let work_dir = tmp.path().join(".storkit").join("work");
|
||||
let work_dir = tmp.path().join(".huskies").join("work");
|
||||
let done_dir = work_dir.join("5_done");
|
||||
fs::create_dir_all(&done_dir).unwrap();
|
||||
|
||||
@@ -1182,7 +1182,7 @@ mod tests {
|
||||
#[test]
|
||||
fn sweep_respects_custom_retention() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let work_dir = tmp.path().join(".storkit").join("work");
|
||||
let work_dir = tmp.path().join(".huskies").join("work");
|
||||
let done_dir = work_dir.join("5_done");
|
||||
let archived_dir = work_dir.join("6_archived");
|
||||
fs::create_dir_all(&done_dir).unwrap();
|
||||
@@ -1211,7 +1211,7 @@ mod tests {
|
||||
#[test]
|
||||
fn sweep_custom_retention_keeps_younger_items() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let work_dir = tmp.path().join(".storkit").join("work");
|
||||
let work_dir = tmp.path().join(".huskies").join("work");
|
||||
let done_dir = work_dir.join("5_done");
|
||||
fs::create_dir_all(&done_dir).unwrap();
|
||||
|
||||
@@ -1255,7 +1255,7 @@ mod tests {
|
||||
let git_root = tmp.path().to_path_buf();
|
||||
init_git_repo(&git_root);
|
||||
|
||||
let work_dir = git_root.join(".storkit").join("work");
|
||||
let work_dir = git_root.join(".huskies").join("work");
|
||||
let done_dir = work_dir.join("5_done");
|
||||
fs::create_dir_all(&done_dir).unwrap();
|
||||
|
||||
@@ -1298,7 +1298,7 @@ mod tests {
|
||||
let git_root = tmp.path().to_path_buf();
|
||||
init_git_repo(&git_root);
|
||||
|
||||
let work_dir = git_root.join(".storkit").join("work");
|
||||
let work_dir = git_root.join(".huskies").join("work");
|
||||
let archived_dir = work_dir.join("6_archived");
|
||||
fs::create_dir_all(&archived_dir).unwrap();
|
||||
|
||||
@@ -1339,7 +1339,7 @@ mod tests {
|
||||
let git_root = tmp.path().to_path_buf();
|
||||
init_git_repo(&git_root);
|
||||
|
||||
let work_dir = git_root.join(".storkit").join("work");
|
||||
let work_dir = git_root.join(".huskies").join("work");
|
||||
let done_dir = work_dir.join("5_done");
|
||||
fs::create_dir_all(&done_dir).unwrap();
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::path::Path;
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum WizardStep {
|
||||
/// Step 1: scaffold .storkit/ directory structure and project.toml
|
||||
/// Step 1: scaffold .huskies/ directory structure and project.toml
|
||||
Scaffold,
|
||||
/// Step 2: generate specs/00_CONTEXT.md
|
||||
Context,
|
||||
@@ -76,7 +76,7 @@ pub struct StepState {
|
||||
pub content: Option<String>,
|
||||
}
|
||||
|
||||
/// Persistent wizard state, stored in `.storkit/wizard_state.json`.
|
||||
/// Persistent wizard state, stored in `.huskies/wizard_state.json`.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WizardState {
|
||||
pub steps: Vec<StepState>,
|
||||
@@ -103,7 +103,7 @@ impl Default for WizardState {
|
||||
impl WizardState {
|
||||
/// Path to the wizard state file relative to the project root.
|
||||
fn state_path(project_root: &Path) -> std::path::PathBuf {
|
||||
project_root.join(".storkit").join("wizard_state.json")
|
||||
project_root.join(".huskies").join("wizard_state.json")
|
||||
}
|
||||
|
||||
/// Load wizard state from disk, or return None if it doesn't exist.
|
||||
@@ -122,7 +122,7 @@ impl WizardState {
|
||||
}
|
||||
|
||||
/// Create wizard state file if it doesn't already exist.
|
||||
/// Step 1 (Scaffold) is automatically confirmed since `storkit init`
|
||||
/// Step 1 (Scaffold) is automatically confirmed since `huskies init`
|
||||
/// has already run the scaffold.
|
||||
pub fn init_if_missing(project_root: &Path) {
|
||||
if Self::load(project_root).is_some() {
|
||||
|
||||
@@ -18,7 +18,7 @@ You have the following tools available:
|
||||
|
||||
YOUR WORKFLOW:
|
||||
When the user requests a feature or change:
|
||||
1. **Understand:** Read `.storkit/README.md` if you haven't already to understand the development process
|
||||
1. **Understand:** Read `.huskies/README.md` if you haven't already to understand the development process
|
||||
2. **Explore:** Use `read_file` and `list_directory` to understand the current codebase structure
|
||||
3. **Implement:** Use `write_file` to create or modify files directly
|
||||
4. **Verify:** Use `exec_shell` to run tests, linters, or build commands to verify your changes work
|
||||
@@ -100,7 +100,7 @@ Ask the user:
|
||||
- Who are the target users?
|
||||
- What are the core features or goals?
|
||||
|
||||
Then use `write_file` to write `.storkit/specs/00_CONTEXT.md` with:
|
||||
Then use `write_file` to write `.huskies/specs/00_CONTEXT.md` with:
|
||||
- **High-Level Goal** — a clear, concise summary of what the project does
|
||||
- **Core Features** — 3-5 bullet points
|
||||
- **Domain Definition** — key terms and roles
|
||||
@@ -114,7 +114,7 @@ Ask the user:
|
||||
- What test runner(s)? (e.g. cargo test, pytest, jest, pnpm test)
|
||||
- What linter(s)? (e.g. clippy, eslint, biome, ruff)
|
||||
|
||||
Then use `write_file` to write `.storkit/specs/tech/STACK.md` with:
|
||||
Then use `write_file` to write `.huskies/specs/tech/STACK.md` with:
|
||||
- **Overview** of the architecture
|
||||
- **Core Stack** — languages, frameworks, build tools
|
||||
- **Coding Standards** — formatting, linting, quality gates
|
||||
@@ -131,7 +131,7 @@ Based on the tech stack answers, use `write_file` to write `script/test` — a b
|
||||
The script must start with `#!/usr/bin/env bash` and `set -euo pipefail`.
|
||||
|
||||
## Step 4: Project Configuration
|
||||
The scaffold has written `.storkit/project.toml` with example `[[component]]` sections. You must replace these examples with real definitions that match the project's actual tech stack.
|
||||
The scaffold has written `.huskies/project.toml` with example `[[component]]` sections. You must replace these examples with real definitions that match the project's actual tech stack.
|
||||
|
||||
First, inspect the project structure to identify the tech stack:
|
||||
- Use `list_directory(".")` to see top-level files and directories
|
||||
@@ -139,9 +139,9 @@ First, inspect the project structure to identify the tech stack:
|
||||
- Check subdirectories like `frontend/`, `backend/`, `app/`, `web/` for nested stacks
|
||||
- If you find a `package.json`, check whether `pnpm-lock.yaml`, `yarn.lock`, or `package-lock.json` exists to determine the package manager
|
||||
|
||||
Then use `read_file(".storkit/project.toml")` to see the current content, keeping the `[[agent]]` sections intact.
|
||||
Then use `read_file(".huskies/project.toml")` to see the current content, keeping the `[[agent]]` sections intact.
|
||||
|
||||
Finally, use `write_file` to rewrite `.storkit/project.toml` with real `[[component]]` entries. Each component needs:
|
||||
Finally, use `write_file` to rewrite `.huskies/project.toml` with real `[[component]]` entries. Each component needs:
|
||||
- `name` — component identifier (e.g. "backend", "frontend", "app")
|
||||
- `path` — relative path from project root (use "." for root, "frontend" for a frontend subdirectory)
|
||||
- `setup` — list of setup commands that install dependencies and verify the build (e.g. ["pnpm install"], ["cargo check"])
|
||||
|
||||
@@ -216,10 +216,10 @@ fn run_pty_session(
|
||||
// are emitted and tool-start activity signals never fire.
|
||||
cmd.arg("--include-partial-messages");
|
||||
// Delegate permission decisions to the MCP prompt_permission tool.
|
||||
// Claude Code will call this tool via the storkit MCP server when
|
||||
// Claude Code will call this tool via the huskies MCP server when
|
||||
// a tool requires user approval, instead of using PTY stdin/stdout.
|
||||
cmd.arg("--permission-prompt-tool");
|
||||
cmd.arg("mcp__storkit__prompt_permission");
|
||||
cmd.arg("mcp__huskies__prompt_permission");
|
||||
// Note: --system is not a valid Claude Code CLI flag. System-level
|
||||
// instructions (like bot name) are prepended to the user prompt instead.
|
||||
cmd.cwd(cwd);
|
||||
@@ -229,7 +229,7 @@ fn run_pty_session(
|
||||
cmd.env("CLAUDECODE", "");
|
||||
|
||||
slog!(
|
||||
"[pty-debug] Spawning: claude -p \"{}\" {} --output-format stream-json --verbose --include-partial-messages --permission-prompt-tool mcp__storkit__prompt_permission",
|
||||
"[pty-debug] Spawning: claude -p \"{}\" {} --output-format stream-json --verbose --include-partial-messages --permission-prompt-tool mcp__huskies__prompt_permission",
|
||||
user_message,
|
||||
resume_session_id
|
||||
.map(|s| format!("--resume {s}"))
|
||||
|
||||
+20
-20
@@ -57,7 +57,7 @@ fn parse_cli_args(args: &[String]) -> Result<CliArgs, String> {
|
||||
std::process::exit(0);
|
||||
}
|
||||
"--version" | "-V" => {
|
||||
println!("storkit {}", env!("CARGO_PKG_VERSION"));
|
||||
println!("huskies {}", env!("CARGO_PKG_VERSION"));
|
||||
std::process::exit(0);
|
||||
}
|
||||
"--port" => {
|
||||
@@ -97,18 +97,18 @@ fn parse_cli_args(args: &[String]) -> Result<CliArgs, String> {
|
||||
}
|
||||
|
||||
fn print_help() {
|
||||
println!("storkit [OPTIONS] [PATH]");
|
||||
println!("storkit init [OPTIONS] [PATH]");
|
||||
println!("huskies [OPTIONS] [PATH]");
|
||||
println!("huskies init [OPTIONS] [PATH]");
|
||||
println!();
|
||||
println!("Serve a storkit project.");
|
||||
println!("Serve a huskies project.");
|
||||
println!();
|
||||
println!("COMMANDS:");
|
||||
println!(" init Scaffold a new .storkit/ project and start the interactive setup wizard.");
|
||||
println!(" init Scaffold a new .huskies/ project and start the interactive setup wizard.");
|
||||
println!();
|
||||
println!("ARGS:");
|
||||
println!(
|
||||
" PATH Path to an existing project directory. \
|
||||
If omitted, storkit searches parent directories for a .storkit/ root."
|
||||
If omitted, huskies searches parent directories for a .huskies/ root."
|
||||
);
|
||||
println!();
|
||||
println!("OPTIONS:");
|
||||
@@ -139,10 +139,10 @@ async fn main() -> Result<(), std::io::Error> {
|
||||
|
||||
let app_state = Arc::new(SessionState::default());
|
||||
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
||||
// Migrate legacy root-level store.json into .storkit/ if the new path does
|
||||
// Migrate legacy root-level store.json into .huskies/ if the new path does
|
||||
// not yet exist. This keeps existing deployments working after upgrade.
|
||||
let legacy_store_path = cwd.join("store.json");
|
||||
let store_path = cwd.join(".storkit").join("store.json");
|
||||
let store_path = cwd.join(".huskies").join("store.json");
|
||||
if legacy_store_path.exists() && !store_path.exists() {
|
||||
if let Some(parent) = store_path.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
@@ -160,7 +160,7 @@ async fn main() -> Result<(), std::io::Error> {
|
||||
Ok(args) => args,
|
||||
Err(msg) => {
|
||||
eprintln!("error: {msg}");
|
||||
eprintln!("Run 'storkit --help' for usage.");
|
||||
eprintln!("Run 'huskies --help' for usage.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
@@ -187,7 +187,7 @@ async fn main() -> Result<(), std::io::Error> {
|
||||
}
|
||||
|
||||
if is_init {
|
||||
// `storkit init [PATH]` — always scaffold, never search parents.
|
||||
// `huskies init [PATH]` — always scaffold, never search parents.
|
||||
let init_root = explicit_path.unwrap_or_else(|| cwd.clone());
|
||||
if !init_root.exists() {
|
||||
std::fs::create_dir_all(&init_root).unwrap_or_else(|e| {
|
||||
@@ -218,7 +218,7 @@ async fn main() -> Result<(), std::io::Error> {
|
||||
}
|
||||
} else if let Some(explicit_root) = explicit_path {
|
||||
// An explicit path was given on the command line.
|
||||
// Open it directly — scaffold .storkit/ if it is missing — and
|
||||
// Open it directly — scaffold .huskies/ if it is missing — and
|
||||
// exit with a clear error message if the path is invalid.
|
||||
match io::fs::open_project(
|
||||
explicit_root.to_string_lossy().to_string(),
|
||||
@@ -240,7 +240,7 @@ async fn main() -> Result<(), std::io::Error> {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No path argument — auto-detect a .storkit/ project in cwd or
|
||||
// No path argument — auto-detect a .huskies/ project in cwd or
|
||||
// parent directories (preserves existing behaviour).
|
||||
if let Some(project_root) = find_story_kit_root(&cwd) {
|
||||
io::fs::open_project(
|
||||
@@ -259,8 +259,8 @@ async fn main() -> Result<(), std::io::Error> {
|
||||
config::ProjectConfig::load(&project_root)
|
||||
.unwrap_or_else(|e| panic!("Invalid project.toml: {e}"));
|
||||
} else {
|
||||
// No .storkit/ found in cwd or parents — scaffold cwd as a new
|
||||
// project, exactly like `storkit .` does.
|
||||
// No .huskies/ found in cwd or parents — scaffold cwd as a new
|
||||
// project, exactly like `huskies .` does.
|
||||
io::fs::open_project(
|
||||
cwd.to_string_lossy().to_string(),
|
||||
&app_state,
|
||||
@@ -277,7 +277,7 @@ async fn main() -> Result<(), std::io::Error> {
|
||||
|
||||
// Enable persistent server log file now that the project root is known.
|
||||
if let Some(ref root) = *app_state.project_root.lock().unwrap() {
|
||||
let log_dir = root.join(".storkit").join("logs");
|
||||
let log_dir = root.join(".huskies").join("logs");
|
||||
let _ = std::fs::create_dir_all(&log_dir);
|
||||
log_buffer::global().set_log_file(log_dir.join("server.log"));
|
||||
}
|
||||
@@ -294,7 +294,7 @@ async fn main() -> Result<(), std::io::Error> {
|
||||
let watchdog_root: Option<PathBuf> = app_state.project_root.lock().unwrap().clone();
|
||||
AgentPool::spawn_watchdog(Arc::clone(&agents), watchdog_root);
|
||||
if let Some(ref root) = *app_state.project_root.lock().unwrap() {
|
||||
let work_dir = root.join(".storkit").join("work");
|
||||
let work_dir = root.join(".huskies").join("work");
|
||||
if work_dir.is_dir() {
|
||||
let watcher_config = config::ProjectConfig::load(root)
|
||||
.map(|c| c.watcher)
|
||||
@@ -521,7 +521,7 @@ async fn main() -> Result<(), std::io::Error> {
|
||||
let app = build_routes(ctx, whatsapp_ctx.clone(), slack_ctx.clone(), port);
|
||||
|
||||
// Optional Matrix bot: connect to the homeserver and start listening for
|
||||
// messages if `.storkit/bot.toml` is present and enabled.
|
||||
// messages if `.huskies/bot.toml` is present and enabled.
|
||||
if let Some(ref root) = startup_root {
|
||||
chat::transport::matrix::spawn_bot(
|
||||
root,
|
||||
@@ -574,13 +574,13 @@ async fn main() -> Result<(), std::io::Error> {
|
||||
startup_agents.auto_assign_available_work(&root).await;
|
||||
});
|
||||
}
|
||||
let host = std::env::var("STORKIT_HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
|
||||
let host = std::env::var("HUSKIES_HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
|
||||
let addr = format!("{host}:{port}");
|
||||
|
||||
println!(
|
||||
"\x1b[95;1m ____ _ _ ___ _ \n / ___|| |_ ___ _ __| | _|_ _| |_ \n \\___ \\| __/ _ \\| '__| |/ /| || __|\n ___) | || (_) | | | < | || |_ \n |____/ \\__\\___/|_| |_|\\_\\___|\\__|\n\x1b[0m"
|
||||
);
|
||||
println!("STORKIT_PORT={port}");
|
||||
println!("HUSKIES_PORT={port}");
|
||||
println!("\x1b[96;1mFrontend:\x1b[0m \x1b[94mhttp://{addr}\x1b[0m");
|
||||
println!("\x1b[92;1mOpenAPI Docs:\x1b[0m \x1b[94mhttp://{addr}/docs\x1b[0m");
|
||||
|
||||
@@ -637,7 +637,7 @@ mod tests {
|
||||
#[should_panic(expected = "Invalid project.toml: Duplicate agent name")]
|
||||
fn panics_on_duplicate_agent_names() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
let sk = tmp.path().join(".huskies");
|
||||
std::fs::create_dir_all(&sk).unwrap();
|
||||
std::fs::write(
|
||||
sk.join("project.toml"),
|
||||
|
||||
@@ -85,7 +85,7 @@ impl BotShutdownNotifier {
|
||||
/// Rebuild the server binary and re-exec.
|
||||
///
|
||||
/// 1. Gracefully stops all running agents (kills PTY children).
|
||||
/// 2. Runs `cargo build [-p storkit]` from the workspace root, matching
|
||||
/// 2. Runs `cargo build [-p huskies]` from the workspace root, matching
|
||||
/// the current build profile (debug or release).
|
||||
/// 3. If the build fails, returns the build error (server stays up).
|
||||
/// 4. If the build succeeds, sends a best-effort shutdown notification (if a
|
||||
@@ -151,9 +151,9 @@ pub async fn rebuild_and_restart(
|
||||
// 4. Build the server binary, matching the current build profile so the
|
||||
// re-exec via current_exe() picks up the new binary.
|
||||
let build_args: Vec<&str> = if cfg!(debug_assertions) {
|
||||
vec!["build", "-p", "storkit"]
|
||||
vec!["build", "-p", "huskies"]
|
||||
} else {
|
||||
vec!["build", "--release", "-p", "storkit"]
|
||||
vec!["build", "--release", "-p", "huskies"]
|
||||
};
|
||||
slog!("[rebuild] cargo {}", build_args.join(" "));
|
||||
let output = tokio::task::spawn_blocking({
|
||||
@@ -187,22 +187,22 @@ pub async fn rebuild_and_restart(
|
||||
// 6. Re-exec with the new binary.
|
||||
// Use the cargo output path rather than current_exe() so that rebuilds
|
||||
// inside Docker work correctly — the running binary may be installed at
|
||||
// /usr/local/bin/storkit (read-only) while cargo writes the new binary
|
||||
// to /app/target/release/storkit (a writable volume).
|
||||
// /usr/local/bin/huskies (read-only) while cargo writes the new binary
|
||||
// to /app/target/release/huskies (a writable volume).
|
||||
let new_exe = if cfg!(debug_assertions) {
|
||||
workspace_root.join("target/debug/storkit")
|
||||
workspace_root.join("target/debug/huskies")
|
||||
} else {
|
||||
workspace_root.join("target/release/storkit")
|
||||
workspace_root.join("target/release/huskies")
|
||||
};
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
|
||||
// Remove the port file before re-exec so the new process can write its own.
|
||||
let port_file = project_root.join(".storkit_port");
|
||||
let port_file = project_root.join(".huskies_port");
|
||||
if port_file.exists() {
|
||||
let _ = std::fs::remove_file(&port_file);
|
||||
}
|
||||
// Also check cwd for port file.
|
||||
let cwd_port_file = std::path::Path::new(".storkit_port");
|
||||
let cwd_port_file = std::path::Path::new(".huskies_port");
|
||||
if cwd_port_file.exists() {
|
||||
let _ = std::fs::remove_file(cwd_port_file);
|
||||
}
|
||||
|
||||
+2
-2
@@ -144,13 +144,13 @@ mod tests {
|
||||
|
||||
{
|
||||
let store = JsonFileStore::new(path.clone()).unwrap();
|
||||
store.set("name", json!("storkit"));
|
||||
store.set("name", json!("huskies"));
|
||||
store.set("version", json!(1));
|
||||
store.save().expect("save should succeed");
|
||||
}
|
||||
|
||||
let store = JsonFileStore::new(path).unwrap();
|
||||
assert_eq!(store.get("name"), Some(json!("storkit")));
|
||||
assert_eq!(store.get("name"), Some(json!("huskies")));
|
||||
assert_eq!(store.get("version"), Some(json!(1)));
|
||||
}
|
||||
|
||||
|
||||
+18
-18
@@ -7,7 +7,7 @@ use std::process::Command;
|
||||
/// at the given port.
|
||||
pub fn write_mcp_json(dir: &Path, port: u16) -> Result<(), String> {
|
||||
let content = format!(
|
||||
"{{\n \"mcpServers\": {{\n \"storkit\": {{\n \"type\": \"http\",\n \"url\": \"http://localhost:{port}/mcp\"\n }}\n }}\n}}\n"
|
||||
"{{\n \"mcpServers\": {{\n \"huskies\": {{\n \"type\": \"http\",\n \"url\": \"http://localhost:{port}/mcp\"\n }}\n }}\n}}\n"
|
||||
);
|
||||
std::fs::write(dir.join(".mcp.json"), content).map_err(|e| format!("Write .mcp.json: {e}"))
|
||||
}
|
||||
@@ -25,10 +25,10 @@ pub struct WorktreeListEntry {
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
/// Worktree path inside the project: `{project_root}/.storkit/worktrees/{story_id}`.
|
||||
/// Worktree path inside the project: `{project_root}/.huskies/worktrees/{story_id}`.
|
||||
pub fn worktree_path(project_root: &Path, story_id: &str) -> PathBuf {
|
||||
project_root
|
||||
.join(".storkit")
|
||||
.join(".huskies")
|
||||
.join("worktrees")
|
||||
.join(story_id)
|
||||
}
|
||||
@@ -56,7 +56,7 @@ fn detect_base_branch(project_root: &Path) -> String {
|
||||
|
||||
/// Create a git worktree for the given story.
|
||||
///
|
||||
/// - Creates the worktree at `{project_root}/.storkit/worktrees/{story_id}`
|
||||
/// - Creates the worktree at `{project_root}/.huskies/worktrees/{story_id}`
|
||||
/// on branch `feature/story-{story_id}`.
|
||||
/// - Writes `.mcp.json` in the worktree pointing to the MCP server at `port`.
|
||||
/// - Runs setup commands from the config for each component.
|
||||
@@ -142,14 +142,14 @@ fn create_worktree_sync(project_root: &Path, wt_path: &Path, branch: &str) -> Re
|
||||
}
|
||||
|
||||
// Enable sparse checkout to exclude pipeline files from the worktree.
|
||||
// This prevents .storkit/work/ changes from ending up in feature branches,
|
||||
// This prevents .huskies/work/ changes from ending up in feature branches,
|
||||
// which cause rename/delete merge conflicts when merging back to master.
|
||||
configure_sparse_checkout(wt_path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Placeholder for worktree isolation of `.storkit/work/`.
|
||||
/// Placeholder for worktree isolation of `.huskies/work/`.
|
||||
///
|
||||
/// Previous approaches (sparse checkout, skip-worktree) all leaked state
|
||||
/// from worktrees back to the main checkout's config/index. For now this
|
||||
@@ -214,11 +214,11 @@ pub async fn remove_worktree_by_story_id(
|
||||
remove_worktree(project_root, &info, config).await
|
||||
}
|
||||
|
||||
/// List all worktrees under `{project_root}/.storkit/worktrees/`.
|
||||
/// List all worktrees under `{project_root}/.huskies/worktrees/`.
|
||||
/// Find the worktree path for a given story ID, if it exists.
|
||||
pub fn find_worktree_path(project_root: &Path, story_id: &str) -> Option<PathBuf> {
|
||||
let wt_path = project_root
|
||||
.join(".storkit")
|
||||
.join(".huskies")
|
||||
.join("worktrees")
|
||||
.join(story_id);
|
||||
if wt_path.is_dir() {
|
||||
@@ -229,7 +229,7 @@ pub fn find_worktree_path(project_root: &Path, story_id: &str) -> Option<PathBuf
|
||||
}
|
||||
|
||||
pub fn list_worktrees(project_root: &Path) -> Result<Vec<WorktreeListEntry>, String> {
|
||||
let worktrees_dir = project_root.join(".storkit").join("worktrees");
|
||||
let worktrees_dir = project_root.join(".huskies").join("worktrees");
|
||||
if !worktrees_dir.exists() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
@@ -376,7 +376,7 @@ mod tests {
|
||||
let path = worktree_path(project_root, "42_my_story");
|
||||
assert_eq!(
|
||||
path,
|
||||
Path::new("/home/user/my-project/.storkit/worktrees/42_my_story")
|
||||
Path::new("/home/user/my-project/.huskies/worktrees/42_my_story")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -390,7 +390,7 @@ mod tests {
|
||||
#[test]
|
||||
fn list_worktrees_returns_subdirs() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let worktrees_dir = tmp.path().join(".storkit").join("worktrees");
|
||||
let worktrees_dir = tmp.path().join(".huskies").join("worktrees");
|
||||
fs::create_dir_all(worktrees_dir.join("42_story_a")).unwrap();
|
||||
fs::create_dir_all(worktrees_dir.join("43_story_b")).unwrap();
|
||||
// A file (not dir) — should be ignored
|
||||
@@ -439,8 +439,8 @@ mod tests {
|
||||
fs::create_dir_all(&project_root).unwrap();
|
||||
init_git_repo(&project_root);
|
||||
|
||||
// Create a tracked file under .storkit/work/ on the initial branch
|
||||
let work_dir = project_root.join(".storkit").join("work");
|
||||
// Create a tracked file under .huskies/work/ on the initial branch
|
||||
let work_dir = project_root.join(".huskies").join("work");
|
||||
fs::create_dir_all(&work_dir).unwrap();
|
||||
fs::write(work_dir.join("test_story.md"), "# Test").unwrap();
|
||||
Command::new("git")
|
||||
@@ -458,14 +458,14 @@ mod tests {
|
||||
let branch = "feature/test-sparse";
|
||||
create_worktree_sync(&project_root, &wt_path, branch).unwrap();
|
||||
|
||||
// Worktree should have all files including .storkit/work/
|
||||
assert!(wt_path.join(".storkit").join("work").exists());
|
||||
// Worktree should have all files including .huskies/work/
|
||||
assert!(wt_path.join(".huskies").join("work").exists());
|
||||
assert!(wt_path.join(".git").exists());
|
||||
|
||||
// Main checkout must NOT be affected by worktree creation.
|
||||
assert!(
|
||||
work_dir.exists(),
|
||||
".storkit/work/ must still exist in the main checkout"
|
||||
".huskies/work/ must still exist in the main checkout"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -679,7 +679,7 @@ mod tests {
|
||||
|
||||
// Create a directory that looks like a worktree but isn't registered with git
|
||||
let wt_path = project_root
|
||||
.join(".storkit")
|
||||
.join(".huskies")
|
||||
.join("worktrees")
|
||||
.join("orphan");
|
||||
fs::create_dir_all(&wt_path).unwrap();
|
||||
@@ -700,7 +700,7 @@ mod tests {
|
||||
init_git_repo(&project_root);
|
||||
|
||||
let wt_path = project_root
|
||||
.join(".storkit")
|
||||
.join(".huskies")
|
||||
.join("worktrees")
|
||||
.join("test_rm");
|
||||
create_worktree_sync(&project_root, &wt_path, "feature/test-rm").unwrap();
|
||||
|
||||
Reference in New Issue
Block a user