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:
Timmy
2026-04-03 16:12:52 +01:00
parent a7035b6ba7
commit 2d8ccb3eb6
572 changed files with 1340 additions and 1220 deletions
+70 -70
View File
@@ -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 Kitspecific
/// 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 Kitspecific
/// 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();