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
+25
View File
@@ -0,0 +1,25 @@
# Bot config (contains credentials)
bot.toml
# Matrix SDK state store
matrix_store/
matrix_device_id
matrix_history.json
# Agent worktrees and merge workspace (managed by the server, not tracked in git)
worktrees/
merge_workspace/
# Intermediate pipeline stages (transient, not committed per spike 92)
work/2_current/
work/3_qa/
work/4_merge/
# Coverage reports (generated by cargo-llvm-cov, not tracked in git)
coverage/
# Token usage log (generated at runtime, contains cost data)
token_usage.jsonl
# Chat service logs
whatsapp_history.json
+267
View File
@@ -0,0 +1,267 @@
# Story Kit: The Story-Driven Test Workflow (SDTW)
**Target Audience:** Large Language Models (LLMs) acting as Senior Engineers.
**Goal:** To maintain long-term project coherence, prevent context window exhaustion, and ensure high-quality, testable code generation in large software projects.
---
## 0. First Steps (For New LLM Sessions)
When you start a new session with this project:
1. **Check Setup Wizard:** Call `wizard_status` to check if project setup is complete. If the wizard is not complete, guide the user through the remaining steps. Important rules for the wizard flow:
- **Be conversational.** Don't show tool names, step numbers, or raw wizard output to the user.
- **On projects with existing code:** Read the codebase and generate each file, then show the user what you wrote and ask if it looks right.
- **On bare projects with no code:** Ask the user what they want to build, what language/framework they plan to use, and generate files from their answers.
- **You must actually generate the files.** The workflow for each step is: (1) call `wizard_generate` with no args to get a hint, (2) write the file content yourself based on the conversation, (3) call `wizard_generate` again with the `content` argument containing the full file body, (4) show the user what you wrote, (5) call `wizard_confirm` (they approve), `wizard_retry` (they want changes), or `wizard_skip` (they want to skip). Do not stop after discussing — follow through and write the files.
- **Keep moving.** After each step is confirmed, immediately proceed to the next wizard step without waiting for the user to ask.
2. **Check for MCP Tools:** Read `.mcp.json` to discover the MCP server endpoint. Then list available tools by calling:
```bash
curl -s "$(jq -r '.mcpServers["huskies"].url' .mcp.json)" \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
```
This returns the full tool catalog (create stories, spawn agents, record tests, manage worktrees, etc.). Familiarize yourself with the available tools before proceeding. These tools allow you to directly manipulate the workflow and spawn subsidiary agents without manual file manipulation.
3. **Read Context:** Check `.huskies/specs/00_CONTEXT.md` for high-level project goals.
4. **Read Stack:** Check `.huskies/specs/tech/STACK.md` for technical constraints and patterns.
5. **Check Work Items:** Look at `.huskies/work/1_backlog/` and `.huskies/work/2_current/` to see what work is pending.
---
## 1. The Philosophy
We treat the codebase as the implementation of a **"Living Specification."** driven by **User Stories**
Instead of ephemeral chat prompts ("Fix this", "Add that"), we work through persistent artifacts.
* **Stories** define the *Change*.
* **Tests** define the *Truth*.
* **Code** defines the *Reality*.
**The Golden Rule:** You are not allowed to write code until the Acceptance Criteria are captured in the story.
---
## 1.5 MCP Tools
Agents have programmatic access to the workflow via MCP tools served at `POST /mcp`. The project `.mcp.json` registers this endpoint automatically so Claude Code sessions and spawned agents can call tools like `create_story`, `validate_stories`, `list_upcoming`, `get_story_todos`, `record_tests`, `ensure_acceptance`, `start_agent`, `stop_agent`, `list_agents`, and `get_agent_output` without parsing English instructions.
**To discover what tools are available:** Check `.mcp.json` for the server endpoint, then use the MCP protocol to list available tools.
---
## 2. Directory Structure
```text
project_root/
.mcp.json # MCP server configuration (if MCP tools are available)
.story_kit/
├── README.md # This document
├── project.toml # Agent configuration (roles, models, prompts)
├── work/ # Unified work item pipeline (stories, bugs, spikes)
│ ├── 1_backlog/ # New work items awaiting implementation
│ ├── 2_current/ # Work in progress
│ ├── 3_qa/ # QA review
│ ├── 4_merge/ # Ready to merge to master
│ ├── 5_done/ # Merged and completed (auto-swept to 6_archived after 4 hours)
│ └── 6_archived/ # Long-term archive
├── worktrees/ # Agent worktrees (managed by the server)
├── specs/ # Minimal guardrails (context + stack)
│ ├── 00_CONTEXT.md # High-level goals, domain definition, and glossary
│ ├── tech/ # Implementation details (Stack, Architecture, Constraints)
│ │ └── STACK.md # The "Constitution" (Languages, Libs, Patterns)
│ └── functional/ # Domain logic (Platform-agnostic behavior)
│ └── ...
└── src/ # The Code
```
### Work Items
All work items (stories, bugs, spikes) live in the same `work/` pipeline. Items are named: `{id}_{type}_{slug}.md`
* Stories: `57_story_live_test_gate_updates.md`
* Bugs: `4_bug_run_button_does_not_start_agent.md`
* Spikes: `61_spike_filesystem_watcher_architecture.md`
Items move through stages by moving the file between directories:
`1_backlog` → `2_current` → `3_qa` → `4_merge` → `5_done` → `6_archived`
Items in `5_done` are auto-swept to `6_archived` after 4 hours by the server.
### Filesystem Watcher
The server watches `.story_kit/work/` for changes. When a file is created, moved, or modified, the watcher auto-commits with a deterministic message and broadcasts a WebSocket notification to the frontend. This means:
* MCP tools only need to write/move files — the watcher handles git commits
* IDE drag-and-drop works (drag a story from `1_backlog/` to `2_current/`)
* The frontend updates automatically without manual refresh
---
## 3. The Cycle (The "Loop")
When the user asks for a feature, follow this 4-step loop strictly:
### Step 1: The Story (Ingest)
* **User Input:** "I want the robot to dance."
* **Action:** Create a story via MCP tool `create_story` (guarantees correct front matter and auto-assigns the story number).
* **Front Matter (Required):** Every work item file MUST begin with YAML front matter containing a `name` field:
```yaml
---
name: Short Human-Readable Story Name
---
```
* **Move to Current:** Once the story is validated and ready for coding, move it to `work/2_current/`.
* **Tracking:** Mark Acceptance Criteria as tested directly in the story file as tests are completed.
* **Content:**
* **User Story:** "As a user, I want..."
* **Acceptance Criteria:** Bullet points of observable success.
* **Out of scope:** Things that are out of scope so that the LLM doesn't go crazy
* **Story Quality (INVEST):** Stories should be Independent, Negotiable, Valuable, Estimable, Small, and Testable.
* **Git:** The `start_agent` MCP tool automatically creates a worktree under `.story_kit/worktrees/`, checks out a feature branch, moves the story to `work/2_current/`, and spawns the agent. No manual branch or worktree creation is needed.
### Step 2: The Implementation (Code)
* **Action:** Write the code to satisfy the approved tests and Acceptance Criteria.
* **Constraint:** adhere strictly to `specs/tech/STACK.md` (e.g., if it forbids certain patterns, you must not use them).
* **Full-Stack Completion:** Every story must be completed across all components of the stack. If a feature touches the backend, frontend, and API layer, all three must be fully implemented and working end-to-end before the story can be accepted. Partial implementations (e.g., backend logic with no frontend wiring, or UI scaffolding with no real data) do not satisfy acceptance criteria.
### Step 3: Verification (Close)
* **Action:** For each Acceptance Criterion in the story, write a failing test (red), mark the criterion as tested, make the test pass (green), and refactor if needed. Keep only one failing test at a time.
* **Action:** Run compilation and make sure it succeeds without errors. Consult `specs/tech/STACK.md` and run all required linters listed there (treat warnings as errors). Run tests and make sure they all pass before proceeding. Ask questions here if needed.
* **Action:** Do not accept stories yourself. Ask the user if they accept the story. If they agree, move the story file to `work/5_done/`.
* **Move to Done:** After acceptance, move the story from `work/2_current/` (or `work/4_merge/`) to `work/5_done/`.
* **Action:** When the user accepts:
1. Move the story file to `work/5_done/`
2. Commit both changes to the feature branch
3. Perform the squash merge: `git merge --squash feature/story-name`
4. Commit to master with a comprehensive commit message
5. Delete the feature branch: `git branch -D feature/story-name`
* **Important:** Do NOT mark acceptance criteria as complete before user acceptance. Only mark them complete when the user explicitly accepts the story.
**CRITICAL - NO SUMMARY DOCUMENTS:**
* **NEVER** create a separate summary document (e.g., `STORY_XX_SUMMARY.md`, `IMPLEMENTATION_NOTES.md`, etc.)
* **NEVER** write terminal output to a markdown file for "documentation purposes"
* Tests are the primary source of truth. Keep test coverage and Acceptance Criteria aligned after each story.
* If you find yourself typing `cat << 'EOF' > SUMMARY.md` or similar, **STOP IMMEDIATELY**.
* The only files that should exist after story completion:
* Updated code in `src/`
* Updated guardrails in `specs/` (if needed)
* Archived work item in `work/5_done/` (server auto-sweeps to `work/6_archived/` after 4 hours)
---
## 3.5. Bug Workflow (Simplified Path)
Not everything needs to be a full story. Simple bugs can skip the story process:
### When to Use Bug Workflow
* Defects in existing functionality (not new features)
* State inconsistencies or data corruption
* UI glitches that don't require spec changes
* Performance issues with known fixes
### Bug Process
1. **Document Bug:** Create a bug file in `work/1_backlog/` named `{id}_bug_{slug}.md` with:
* **Symptom:** What the user observes
* **Root Cause:** Technical explanation (if known)
* **Reproduction Steps:** How to trigger the bug
* **Proposed Fix:** Brief technical approach
* **Workaround:** Temporary solution if available
2. **Start an Agent:** Use the `start_agent` MCP tool to create a worktree and spawn an agent for the bug fix.
3. **Write a Failing Test:** Before fixing the bug, write a test that reproduces it (red). This proves the bug exists and prevents regression.
4. **Fix the Bug:** Make minimal code changes to make the test pass (green).
5. **User Testing:** Let the user verify the fix in the worktree before merging. Do not proceed until they confirm.
6. **Archive & Merge:** Move the bug file to `work/5_done/`, squash merge to master, delete the worktree and branch.
7. **No Guardrail Update Needed:** Unless the bug reveals a missing constraint
### Bug vs Story vs Spike
* **Bug:** Existing functionality is broken → Fix it
* **Story:** New functionality is needed → Test it, then build it
* **Spike:** Uncertainty/feasibility discovery → Run spike workflow
---
## 3.6. Spike Workflow (Research Path)
Not everything needs a story or bug fix. Spikes are time-boxed investigations to reduce uncertainty.
### When to Use a Spike
* Unclear root cause or feasibility
* Need to compare libraries/encoders/formats
* Need to validate performance constraints
### Spike Process
1. **Document Spike:** Create a spike file in `work/1_backlog/` named `{id}_spike_{slug}.md` with:
* **Question:** What you need to answer
* **Hypothesis:** What you expect to be true
* **Timebox:** Strict limit for the research
* **Investigation Plan:** Steps/tools to use
* **Findings:** Evidence and observations
* **Recommendation:** Next step (Story, Bug, or No Action)
2. **Execute Research:** Stay within the timebox. No production code changes.
3. **Escalate if Needed:** If implementation is required, open a Story or Bug and follow that workflow.
4. **Archive:** Move the spike file to `work/5_done/`.
### Spike Output
* Decision and evidence, not production code
* Specs updated only if the spike changes system truth
---
## 4. Context Reset Protocol
When the LLM context window fills up (or the chat gets slow/confused):
1. **Stop Coding.**
2. **Instruction:** Tell the user to open a new chat.
3. **Handoff:** The only context the new LLM needs is in the `specs/` folder and `.mcp.json`.
* *Prompt for New Session:* "I am working on Project X. Read `.mcp.json` to discover available tools, then read `specs/00_CONTEXT.md` and `specs/tech/STACK.md`. Then look at `work/1_backlog/` and `work/2_current/` to see what is pending."
---
## 5. Setup Instructions (For the LLM)
If a user hands you this document and says "Apply this process to my project":
1. **Check for MCP Tools:** Look for `.mcp.json` in the project root. If it exists, you have programmatic access to workflow tools and agent spawning capabilities.
2. **Analyze the Request:** Ask for the high-level goal ("What are we building?") and the tech preferences ("Rust or Python?").
3. **Git Check:** Check if the directory is a git repository (`git status`). If not, run `git init`.
4. **Scaffold:** Run commands to create the `work/` and `specs/` folders with the 6-stage pipeline (`work/1_backlog/` through `work/6_archived/`).
5. **Draft Context:** Write `specs/00_CONTEXT.md` based on the user's answer.
6. **Draft Stack:** Write `specs/tech/STACK.md` based on best practices for that language.
7. **Wait:** Ask the user for "Story #1".
---
## 6. Chat Bot Configuration
Story Kit includes a chat bot that can be connected to one messaging platform at a time. The bot handles commands, LLM conversations, and pipeline notifications.
**Only one transport can be active at a time.** To configure the bot, copy the appropriate example file to `.huskies/bot.toml`:
| Transport | Example file | Webhook endpoint |
|-----------|-------------|-----------------|
| Matrix | `bot.toml.matrix.example` | *(uses Matrix sync, no webhook)* |
| WhatsApp (Meta Cloud API) | `bot.toml.whatsapp-meta.example` | `/webhook/whatsapp` |
| WhatsApp (Twilio) | `bot.toml.whatsapp-twilio.example` | `/webhook/whatsapp` |
| Slack | `bot.toml.slack.example` | `/webhook/slack` |
```bash
cp .huskies/bot.toml.matrix.example .huskies/bot.toml
# Edit bot.toml with your credentials
```
The `bot.toml` file is gitignored (it contains secrets). The example files are checked in for reference.
---
## 7. Code Quality
**MANDATORY:** Before completing Step 3 (Verification) of any story, you MUST run all applicable linters, formatters, and test suites and fix ALL errors and warnings. Zero tolerance for warnings or errors.
**AUTO-RUN CHECKS:** Always run the required lint/test/build checks as soon as relevant changes are made. Do not ask for permission to run them—run them automatically and fix any failures.
**ALWAYS FIX DIAGNOSTICS:** At every stage, you must proactively fix all errors and warnings without waiting for user confirmation. Do not pause to ask whether to fix diagnostics—fix them immediately as part of the workflow.
**Consult `specs/tech/STACK.md`** for the specific tools, commands, linter configurations, and quality gates for this project. The STACK file is the single source of truth for what must pass before a story can be accepted.
+26
View File
@@ -0,0 +1,26 @@
# Matrix Transport
# Copy this file to bot.toml and fill in your values.
# Only one transport can be active at a time.
enabled = true
transport = "matrix"
homeserver = "https://matrix.example.com"
username = "@botname:example.com"
password = "your-bot-password"
# List one or more rooms to listen in.
room_ids = ["!roomid:example.com"]
# Users allowed to interact with the bot (fail-closed: empty = nobody).
allowed_users = ["@youruser:example.com"]
# Bot display name in chat.
# display_name = "Assistant"
# Maximum conversation turns to remember per room (default: 20).
# history_size = 20
# Rooms where the bot responds to all messages (not just addressed ones).
# This list is updated automatically when users toggle ambient mode at runtime.
# ambient_rooms = ["!roomid:example.com"]
+23
View File
@@ -0,0 +1,23 @@
# Slack Transport
# Copy this file to bot.toml and fill in your values.
# Only one transport can be active at a time.
#
# Setup:
# 1. Create a Slack App at api.slack.com/apps
# 2. Add OAuth scopes: chat:write, chat:update
# 3. Subscribe to bot events: message.channels, message.groups, message.im
# 4. Install the app to your workspace
# 5. Set your webhook URL in Event Subscriptions: https://your-server/webhook/slack
enabled = true
transport = "slack"
slack_bot_token = "xoxb-..."
slack_signing_secret = "your-signing-secret"
slack_channel_ids = ["C01ABCDEF"]
# Bot display name (used in formatted messages).
# display_name = "Assistant"
# Maximum conversation turns to remember per channel (default: 20).
# history_size = 20
+33
View File
@@ -0,0 +1,33 @@
# WhatsApp Transport (Meta Cloud API)
# Copy this file to bot.toml and fill in your values.
# Only one transport can be active at a time.
#
# Setup:
# 1. Create a Meta Business App at developers.facebook.com
# 2. Add the WhatsApp product
# 3. Copy your Phone Number ID and generate a permanent access token
# 4. Register your webhook URL: https://your-server/webhook/whatsapp
# 5. Set the verify token below to match what you configure in Meta's dashboard
enabled = true
transport = "whatsapp"
whatsapp_provider = "meta"
whatsapp_phone_number_id = "123456789012345"
whatsapp_access_token = "EAAx..."
whatsapp_verify_token = "my-secret-verify-token"
# Optional: name of the approved Meta message template used for notifications
# sent outside the 24-hour messaging window (default: "pipeline_notification").
# whatsapp_notification_template = "pipeline_notification"
# Bot display name (used in formatted messages).
# display_name = "Assistant"
# Maximum conversation turns to remember per user (default: 20).
# history_size = 20
# Optional: restrict which phone numbers can interact with the bot.
# When set, only listed numbers are processed; all others are silently ignored.
# When absent or empty, all numbers are allowed (open by default).
# whatsapp_allowed_phones = ["+15551234567", "+15559876543"]
+29
View File
@@ -0,0 +1,29 @@
# WhatsApp Transport (Twilio)
# Copy this file to bot.toml and fill in your values.
# Only one transport can be active at a time.
#
# Setup:
# 1. Sign up at twilio.com
# 2. Activate the WhatsApp sandbox (Messaging > Try it out > Send a WhatsApp message)
# 3. Send the sandbox join code from your WhatsApp to the sandbox number
# 4. Copy your Account SID, Auth Token, and sandbox number below
# 5. Set your webhook URL in the Twilio console: https://your-server/webhook/whatsapp
enabled = true
transport = "whatsapp"
whatsapp_provider = "twilio"
twilio_account_sid = "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
twilio_auth_token = "your_auth_token"
twilio_whatsapp_number = "+14155238886"
# Bot display name (used in formatted messages).
# display_name = "Assistant"
# Maximum conversation turns to remember per user (default: 20).
# history_size = 20
# Optional: restrict which phone numbers can interact with the bot.
# When set, only listed numbers are processed; all others are silently ignored.
# When absent or empty, all numbers are allowed (open by default).
# whatsapp_allowed_phones = ["+15551234567", "+15559876543"]
+28
View File
@@ -0,0 +1,28 @@
# Problems
Recurring issues observed during pipeline operation. Review periodically and create stories for systemic problems.
## 2026-03-18: Stories graduating to "done" with empty merges (7 of 10)
Pipeline allows stories to move through coding → QA → merge → done without any actual code changes landing on master. The squash-merge produces an empty diff but the pipeline still marks the story as done. Affected stories: 247, 273, 274, 278, 279, 280, 92. Only 266, 271, 277, and 281 actually shipped code. Root cause: no check that the merge commit contains a non-empty diff. Filed bug 283 for the manual_qa gate issue specifically, but the empty-merge-to-done problem is broader and needs its own fix.
## 2026-03-18: Agent committed directly to master instead of worktree
Multiple agents have committed directly to master instead of their worktree/feature branch:
- Commit `5f4591f` ("fix: update should_commit_stage test to match 5_done") — likely mergemaster
- Commit `a32cfbd` ("Add bot-level command registry with help command") — story 285 coder committed code + Cargo.lock directly to master
Agents should only commit to their feature branch or merge-queue branch, never to master directly. Suspect agents are running `git commit` in the project root instead of the worktree directory. This can also revert uncommitted fixes on master (e.g. project.toml pkill fix was overwritten). Frequency: at least 2 confirmed cases. This is a recurring and serious problem — needs a guard in the server or agent prompts.
## 2026-03-19: Auto-assign re-assigns mergemaster to failed merge stories in a loop
After bug 295 fix (`auto_assign_available_work` after every pipeline advance), mergemaster gets re-assigned to stories that already have a merge failure flag. Story 310 had an empty diff merge failure — mergemaster correctly reported the failure, but auto-assign immediately re-assigned mergemaster to the same story, creating an infinite retry loop. The auto-assign logic needs to check for the `merge_failure` front matter flag before re-assigning agents to stories in `4_merge/`.
## 2026-03-19: Coder produces no code (complete ghost — story 310)
Story 310 (Bot delete command) went through the full pipeline — coder session ran, passed QA/gates, moved to merge — but the coder produced zero code. No commits on the feature branch, no commits on master. The entire agent session was a no-op. This is different from the "committed to master instead of worktree" problem — in this case, the coder simply did nothing. Need to investigate the coder logs to understand what happened. The empty-diff merge check would catch this at merge time, but ideally the server should detect "coder finished with no commits on feature branch" at the gate-check stage and fail early.
## 2026-03-19: Auto-assign assigns mergemaster to coding-stage stories
Auto-assign picked mergemaster for story 310 which was in `2_current/`. Mergemaster should only work on stories in `4_merge/`. The `auto_assign_available_work` function doesn't enforce that the agent's configured stage matches the pipeline stage of the story it's being assigned to. Story 279 (auto-assign respects agent stage from front matter) was supposed to fix this, but the check may only apply to front-matter preferences, not the fallback assignment path.
+343
View File
@@ -0,0 +1,343 @@
# Project-wide default QA mode: "server", "agent", or "human".
# Per-story `qa` front matter overrides this setting.
default_qa = "server"
# Default model for coder agents. Only agents with this model are auto-assigned.
# Opus coders are reserved for explicit per-story `agent:` front matter requests.
default_coder_model = "sonnet"
# Maximum concurrent coder agents. Stories wait in 2_current/ when all slots are full.
max_coders = 3
# Maximum retries per story per pipeline stage before marking as blocked.
# Set to 0 to disable retry limits.
max_retries = 3
# Base branch name for this project. Worktree creation, merges, and agent prompts
# use this value for {{base_branch}}. When not set, falls back to auto-detection
# (reads current HEAD branch).
base_branch = "master"
[[component]]
name = "frontend"
path = "frontend"
setup = ["npm ci", "npm run build"]
teardown = []
[[component]]
name = "server"
path = "."
setup = ["mkdir -p frontend/dist", "cargo check"]
teardown = []
[[agent]]
name = "coder-1"
stage = "coder"
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 .story_kit/README.md to understand the dev process. The story details are in your prompt above. Follow the SDTW process through implementation and verification (Steps 1-3). 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. If the user asks to review your changes, tell them to run: cd \"{{worktree_path}}\" && git difftool {{base_branch}}...HEAD\n\nIMPORTANT: Commit all your work before your process exits. The server will automatically run acceptance gates (cargo clippy + tests) when your process exits and advance the pipeline based on the results.\n\n## Bug Workflow: Root Cause First\nWhen working on bugs:\n1. Investigate the root cause before writing any fix. Use `git bisect` to find the breaking commit or `git log` to trace history. Read the relevant code before touching anything.\n2. Fix the root cause with a surgical, minimal change. Do NOT add new abstractions, wrappers, or workarounds when a targeted fix to the original code is possible.\n3. Write commit messages that explain what broke and why, not just what was changed.\n4. If you cannot determine the root cause after thorough investigation, document what you tried and why it was inconclusive — do not guess and ship a speculative fix."
system_prompt = "You are a full-stack engineer working autonomously in a git worktree. Follow the Story-Driven Test Workflow strictly. Run cargo clippy --all-targets --all-features and biome checks before considering work complete. Commit all your work before finishing - use a descriptive commit message. Do not accept stories, move them to archived, or merge to master - a human will do that. Do not coordinate with other agents - focus on your assigned story. The server automatically runs acceptance gates when your process exits. For bugs, always find and fix the root cause. Use git bisect to find breaking commits. Do not layer new code on top of existing code when a surgical fix is possible. If root cause is unclear after investigation, document what you tried rather than guessing."
[[agent]]
name = "coder-2"
stage = "coder"
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 .story_kit/README.md to understand the dev process. The story details are in your prompt above. Follow the SDTW process through implementation and verification (Steps 1-3). 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. If the user asks to review your changes, tell them to run: cd \"{{worktree_path}}\" && git difftool {{base_branch}}...HEAD\n\nIMPORTANT: Commit all your work before your process exits. The server will automatically run acceptance gates (cargo clippy + tests) when your process exits and advance the pipeline based on the results.\n\n## Bug Workflow: Root Cause First\nWhen working on bugs:\n1. Investigate the root cause before writing any fix. Use `git bisect` to find the breaking commit or `git log` to trace history. Read the relevant code before touching anything.\n2. Fix the root cause with a surgical, minimal change. Do NOT add new abstractions, wrappers, or workarounds when a targeted fix to the original code is possible.\n3. Write commit messages that explain what broke and why, not just what was changed.\n4. If you cannot determine the root cause after thorough investigation, document what you tried and why it was inconclusive — do not guess and ship a speculative fix."
system_prompt = "You are a full-stack engineer working autonomously in a git worktree. Follow the Story-Driven Test Workflow strictly. Run cargo clippy --all-targets --all-features and biome checks before considering work complete. Commit all your work before finishing - use a descriptive commit message. Do not accept stories, move them to archived, or merge to master - a human will do that. Do not coordinate with other agents - focus on your assigned story. The server automatically runs acceptance gates when your process exits. For bugs, always find and fix the root cause. Use git bisect to find breaking commits. Do not layer new code on top of existing code when a surgical fix is possible. If root cause is unclear after investigation, document what you tried rather than guessing."
[[agent]]
name = "coder-3"
stage = "coder"
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 .story_kit/README.md to understand the dev process. The story details are in your prompt above. Follow the SDTW process through implementation and verification (Steps 1-3). 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. If the user asks to review your changes, tell them to run: cd \"{{worktree_path}}\" && git difftool {{base_branch}}...HEAD\n\nIMPORTANT: Commit all your work before your process exits. The server will automatically run acceptance gates (cargo clippy + tests) when your process exits and advance the pipeline based on the results.\n\n## Bug Workflow: Root Cause First\nWhen working on bugs:\n1. Investigate the root cause before writing any fix. Use `git bisect` to find the breaking commit or `git log` to trace history. Read the relevant code before touching anything.\n2. Fix the root cause with a surgical, minimal change. Do NOT add new abstractions, wrappers, or workarounds when a targeted fix to the original code is possible.\n3. Write commit messages that explain what broke and why, not just what was changed.\n4. If you cannot determine the root cause after thorough investigation, document what you tried and why it was inconclusive — do not guess and ship a speculative fix."
system_prompt = "You are a full-stack engineer working autonomously in a git worktree. Follow the Story-Driven Test Workflow strictly. Run cargo clippy --all-targets --all-features and biome checks before considering work complete. Commit all your work before finishing - use a descriptive commit message. Do not accept stories, move them to archived, or merge to master - a human will do that. Do not coordinate with other agents - focus on your assigned story. The server automatically runs acceptance gates when your process exits. For bugs, always find and fix the root cause. Use git bisect to find breaking commits. Do not layer new code on top of existing code when a surgical fix is possible. If root cause is unclear after investigation, document what you tried rather than guessing."
[[agent]]
name = "qa-2"
stage = "qa"
role = "Reviews coder work in worktrees: runs quality gates, verifies acceptance criteria, and reports findings."
model = "sonnet"
max_turns = 40
max_budget_usd = 4.00
prompt = """You are the QA agent for story {{story_id}}. Your job is to verify the coder's work satisfies the story's acceptance criteria and produce a structured QA report.
Read CLAUDE.md first, then .story_kit/README.md to understand the dev process.
## Your Workflow
### 0. Read the Story
- Read the story file at `.huskies/work/3_qa/{{story_id}}.md`
- Extract every acceptance criterion (the `- [ ]` checkbox lines)
- Keep this list in mind for Step 3
### 1. Deterministic Gates (Prerequisites)
Run these first — if any fail, reject immediately without proceeding to AC review:
- Run `cargo clippy --all-targets --all-features` — must show 0 errors, 0 warnings
- Run `cargo test` and verify all tests pass
- If a `frontend/` directory exists:
- Run `npm run build` and note any TypeScript errors
- Run `npx @biomejs/biome check src/` and note any linting issues
- Run `npm test` and verify all frontend tests pass
### 2. Code Change Review
- Run `git diff master...HEAD --stat` to see what files changed
- Run `git diff master...HEAD` to review the actual changes
- Flag any incomplete implementations:
- `todo!()`, `unimplemented!()`, `panic!()` used as stubs
- Placeholder strings like "TODO", "FIXME", "not implemented"
- Empty match arms or arms that just return `Default::default()`
- Hardcoded values where real logic is expected
- Note any obvious coding mistakes (unused imports, dead code, unhandled errors)
### 3. Acceptance Criteria Review
For each AC extracted in Step 0:
- Review the diff and test files to determine if the code addresses this AC
- PASS: describe specifically how the code addresses it (which file/function/test)
- FAIL: explain exactly what is missing or incorrect
An AC fails if:
- No code change or test relates to it
- The implementation is stubbed out (todo!/unimplemented!)
- A test exists but doesn't actually assert the behaviour described
### 4. Manual Testing Support (only if all gates PASS and all ACs PASS)
- Build the server: run `cargo build` and note success/failure
- If build succeeds: find a free port (try 3010-3020) and attempt to start the server
- Generate a testing plan including:
- URL to visit in the browser
- Things to check in the UI
- curl commands to exercise relevant API endpoints
- Kill the test server when done: `pkill -f 'target.*huskies' || true` (NEVER use `pkill -f huskies` — it kills the vite dev server)
### 5. Produce Structured Report and Verdict
Print your QA report to stdout. Then call `approve_qa` or `reject_qa` via the MCP tool based on the overall result. Use this format:
```
## QA Report for {{story_id}}
### Code Quality
- clippy: PASS/FAIL (details)
- TypeScript build: PASS/FAIL/SKIP (details)
- Biome lint: PASS/FAIL/SKIP (details)
- cargo test: PASS/FAIL (N tests)
- npm test: PASS/FAIL/SKIP (N tests)
- Incomplete implementations: (list any todo!/unimplemented!/stubs found, or "None")
- Other code review findings: (list any issues found, or "None")
### Acceptance Criteria Review
- AC: <criterion text>
Result: PASS/FAIL
Evidence: <how the code addresses it, or what is missing>
(repeat for each AC)
### Manual Testing Plan
- Server URL: http://localhost:PORT (or "Skipped gate/AC failure" or "Build failed")
- Pages to visit: (list, or "N/A")
- Things to check: (list, or "N/A")
- curl commands: (list, or "N/A")
### Overall: PASS/FAIL
Reason: (summary of why it passed or the primary reason it failed)
```
After printing the report:
- If Overall is PASS: call `approve_qa(story_id='{{story_id}}')` via MCP
- If Overall is FAIL: call `reject_qa(story_id='{{story_id}}', notes='<concise reason>')` via MCP so the coder knows exactly what to fix
## Rules
- Do NOT modify any code — read-only review only
- Gates must pass before AC review — a gate failure is an automatic reject
- If any AC is not met, the overall result is FAIL
- Always call approve_qa or reject_qa — never leave the story without a verdict"""
system_prompt = "You are a QA agent. Your job is read-only: run quality gates, verify each acceptance criterion against the diff, and produce a structured QA report. Always call approve_qa or reject_qa via MCP to record your verdict. Do not modify code."
[[agent]]
name = "coder-opus"
stage = "coder"
role = "Senior full-stack engineer for complex tasks. Implements features across all components."
model = "opus"
max_turns = 80
max_budget_usd = 20.00
prompt = "You are working in a git worktree on story {{story_id}}. Read CLAUDE.md first, then .story_kit/README.md to understand the dev process. The story details are in your prompt above. Follow the SDTW process through implementation and verification (Steps 1-3). 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. If the user asks to review your changes, tell them to run: cd \"{{worktree_path}}\" && git difftool {{base_branch}}...HEAD\n\nIMPORTANT: Commit all your work before your process exits. The server will automatically run acceptance gates (cargo clippy + tests) when your process exits and advance the pipeline based on the results.\n\n## Bug Workflow: Root Cause First\nWhen working on bugs:\n1. Investigate the root cause before writing any fix. Use `git bisect` to find the breaking commit or `git log` to trace history. Read the relevant code before touching anything.\n2. Fix the root cause with a surgical, minimal change. Do NOT add new abstractions, wrappers, or workarounds when a targeted fix to the original code is possible.\n3. Write commit messages that explain what broke and why, not just what was changed.\n4. If you cannot determine the root cause after thorough investigation, document what you tried and why it was inconclusive — do not guess and ship a speculative fix."
system_prompt = "You are a senior full-stack engineer working autonomously in a git worktree. You handle complex tasks requiring deep architectural understanding. Follow the Story-Driven Test Workflow strictly. Run cargo clippy --all-targets --all-features and biome checks before considering work complete. Commit all your work before finishing - use a descriptive commit message. Do not accept stories, move them to archived, or merge to master - a human will do that. Do not coordinate with other agents - focus on your assigned story. The server automatically runs acceptance gates when your process exits. For bugs, always find and fix the root cause. Use git bisect to find breaking commits. Do not layer new code on top of existing code when a surgical fix is possible. If root cause is unclear after investigation, document what you tried rather than guessing."
[[agent]]
name = "qa"
stage = "qa"
role = "Reviews coder work in worktrees: runs quality gates, verifies acceptance criteria, and reports findings."
model = "sonnet"
max_turns = 40
max_budget_usd = 4.00
prompt = """You are the QA agent for story {{story_id}}. Your job is to verify the coder's work satisfies the story's acceptance criteria and produce a structured QA report.
Read CLAUDE.md first, then .story_kit/README.md to understand the dev process.
## Your Workflow
### 0. Read the Story
- Read the story file at `.huskies/work/3_qa/{{story_id}}.md`
- Extract every acceptance criterion (the `- [ ]` checkbox lines)
- Keep this list in mind for Step 3
### 1. Deterministic Gates (Prerequisites)
Run these first — if any fail, reject immediately without proceeding to AC review:
- Run `cargo clippy --all-targets --all-features` — must show 0 errors, 0 warnings
- Run `cargo test` and verify all tests pass
- If a `frontend/` directory exists:
- Run `npm run build` and note any TypeScript errors
- Run `npx @biomejs/biome check src/` and note any linting issues
- Run `npm test` and verify all frontend tests pass
### 2. Code Change Review
- Run `git diff master...HEAD --stat` to see what files changed
- Run `git diff master...HEAD` to review the actual changes
- Flag any incomplete implementations:
- `todo!()`, `unimplemented!()`, `panic!()` used as stubs
- Placeholder strings like "TODO", "FIXME", "not implemented"
- Empty match arms or arms that just return `Default::default()`
- Hardcoded values where real logic is expected
- Note any obvious coding mistakes (unused imports, dead code, unhandled errors)
### 3. Acceptance Criteria Review
For each AC extracted in Step 0:
- Review the diff and test files to determine if the code addresses this AC
- PASS: describe specifically how the code addresses it (which file/function/test)
- FAIL: explain exactly what is missing or incorrect
An AC fails if:
- No code change or test relates to it
- The implementation is stubbed out (todo!/unimplemented!)
- A test exists but doesn't actually assert the behaviour described
### 4. Manual Testing Support (only if all gates PASS and all ACs PASS)
- Build the server: run `cargo build` and note success/failure
- If build succeeds: find a free port (try 3010-3020) and attempt to start the server
- Generate a testing plan including:
- URL to visit in the browser
- Things to check in the UI
- curl commands to exercise relevant API endpoints
- Kill the test server when done: `pkill -f 'target.*huskies' || true` (NEVER use `pkill -f huskies` — it kills the vite dev server)
### 5. Produce Structured Report and Verdict
Print your QA report to stdout. Then call `approve_qa` or `reject_qa` via the MCP tool based on the overall result. Use this format:
```
## QA Report for {{story_id}}
### Code Quality
- clippy: PASS/FAIL (details)
- TypeScript build: PASS/FAIL/SKIP (details)
- Biome lint: PASS/FAIL/SKIP (details)
- cargo test: PASS/FAIL (N tests)
- npm test: PASS/FAIL/SKIP (N tests)
- Incomplete implementations: (list any todo!/unimplemented!/stubs found, or "None")
- Other code review findings: (list any issues found, or "None")
### Acceptance Criteria Review
- AC: <criterion text>
Result: PASS/FAIL
Evidence: <how the code addresses it, or what is missing>
(repeat for each AC)
### Manual Testing Plan
- Server URL: http://localhost:PORT (or "Skipped gate/AC failure" or "Build failed")
- Pages to visit: (list, or "N/A")
- Things to check: (list, or "N/A")
- curl commands: (list, or "N/A")
### Overall: PASS/FAIL
Reason: (summary of why it passed or the primary reason it failed)
```
After printing the report:
- If Overall is PASS: call `approve_qa(story_id='{{story_id}}')` via MCP
- If Overall is FAIL: call `reject_qa(story_id='{{story_id}}', notes='<concise reason>')` via MCP so the coder knows exactly what to fix
## Rules
- Do NOT modify any code — read-only review only
- Gates must pass before AC review — a gate failure is an automatic reject
- If any AC is not met, the overall result is FAIL
- Always call approve_qa or reject_qa — never leave the story without a verdict"""
system_prompt = "You are a QA agent. Your job is read-only: run quality gates, verify each acceptance criterion against the diff, and produce a structured QA report. Always call approve_qa or reject_qa via MCP to record your verdict. Do not modify code."
[[agent]]
name = "mergemaster"
stage = "mergemaster"
role = "Merges completed coder work into master, runs quality gates, archives stories, and cleans up worktrees."
model = "opus"
max_turns = 30
max_budget_usd = 5.00
prompt = """You are the mergemaster agent for story {{story_id}}. Your job is to merge the completed coder work into master.
Read CLAUDE.md first, then .story_kit/README.md to understand the dev process.
## Your Workflow
1. Call merge_agent_work(story_id='{{story_id}}') via the MCP tool to trigger the full merge pipeline
2. Review the result: check success, had_conflicts, conflicts_resolved, gates_passed, and gate_output
3. If merge succeeded and gates passed: report success to the human
4. If conflicts were auto-resolved (conflicts_resolved=true) and gates passed: report success, noting which conflicts were resolved
5. If conflicts could not be auto-resolved: **resolve them yourself** in the merge worktree (see below)
6. If merge failed for any other reason: call report_merge_failure(story_id='{{story_id}}', reason='<details>') and report to the human
7. If gates failed after merge: attempt to fix the issues yourself in the merge worktree, then re-trigger merge_agent_work. After 3 fix attempts, call report_merge_failure and stop.
## Resolving Complex Conflicts Yourself
When the auto-resolver fails, you have access to the merge worktree at `.story_kit/merge_workspace/`. Go in there and resolve the conflicts manually:
1. Run `git diff --name-only --diff-filter=U` in the merge worktree to list conflicted files
2. **Build context before touching code.** Run `git log --oneline master...HEAD` on the feature branch to see its commits. Then run `git log --oneline --since="$(git log -1 --format=%ci <feature-branch-base-commit>)" master` to see what landed on master since the branch was created. Read the story files in `.story_kit/work/` for any recently merged stories that touch the same files — this tells you WHY master changed and what must be preserved.
3. Read each conflicted file and understand both sides of the conflict
4. **Understand intent, not just syntax.** The feature branch may be behind master — master's version of shared infrastructure is almost always correct. The feature branch's contribution is the NEW functionality it adds. Your job is to integrate the new into master's structure, not pick one side.
5. Resolve by integrating the feature's new functionality into master's code structure
5. Stage resolved files with `git add`
6. Run `cargo check` (and `npm run build` if frontend changed) to verify compilation
7. If it compiles, commit and re-trigger merge_agent_work
### Common conflict patterns in this project:
**Story file rename/rename conflicts:** Both branches moved the story .md file to different pipeline directories. Resolution: `git rm` both sides — story files in `work/2_current/`, `work/3_qa/`, `work/4_merge/` are gitignored and don't need to be committed.
**bot.rs tokio::select! conflicts:** Master has a `tokio::select!` loop in `handle_message()` that handles permission forwarding (story 275). Feature branches created before story 275 have a simpler direct `provider.chat_stream().await` call. Resolution: KEEP master's tokio::select! loop. Integrate only the feature's new logic (e.g. typing indicators, new callbacks) into the existing loop structure. Do NOT replace the loop with the old direct call.
**Duplicate functions/imports:** The auto-resolver keeps both sides, producing duplicates. Resolution: keep one copy (prefer master's version), delete the duplicate.
**Formatting-only conflicts:** Both sides reformatted the same code differently. Resolution: pick either side (prefer master).
## Fixing Gate Failures
If quality gates fail (cargo clippy, cargo test, npm run build, npm test), attempt to fix issues yourself in the merge worktree.
**Fix yourself (up to 3 attempts total):**
- Syntax errors (missing semicolons, brackets, commas)
- Duplicate definitions from merge artifacts
- Simple type annotation errors
- Unused import warnings flagged by clippy
- Mismatched braces from bad conflict resolution
- Trivial formatting issues that block compilation or linting
**Report to human without attempting a fix:**
- Logic errors or incorrect business logic
- Missing function implementations
- Architectural changes required
- Non-trivial refactoring needed
**Max retry limit:** If gates still fail after 3 fix attempts, call report_merge_failure to record the failure, then stop immediately and report the full gate output to the human.
## CRITICAL Rules
- NEVER manually move story files between pipeline stages (e.g. from 4_merge/ to 5_done/)
- NEVER call accept_story — only merge_agent_work can move stories to done after a successful merge
- When merge fails after exhausting your fix attempts, ALWAYS call report_merge_failure
- Report conflict resolution outcomes clearly
- Report gate failures with full output so the human can act if needed
- The server automatically runs acceptance gates when your process exits"""
system_prompt = "You are the mergemaster agent. Your primary job is to merge feature branches to master. First try the merge_agent_work MCP tool. If the auto-resolver fails on complex conflicts, resolve them yourself in the merge worktree — you are an opus-class agent capable of understanding both sides of a conflict and producing correct merged code. Common patterns: keep master's tokio::select! permission loop in bot.rs, discard story file rename conflicts (gitignored), remove duplicate definitions. After resolving, verify compilation before re-triggering merge. CRITICAL: Never manually move story files or call accept_story. After 3 failed fix attempts, call report_merge_failure and stop."
+43
View File
@@ -0,0 +1,43 @@
# Example project.toml — copy to .huskies/project.toml and customise.
# This file is checked in; project.toml itself is gitignored (it may contain
# instance-specific settings).
# Project-wide default QA mode: "server", "agent", or "human".
# Per-story `qa` front matter overrides this setting.
default_qa = "server"
# Default model for coder agents. Only agents with this model are auto-assigned.
# Opus coders are reserved for explicit per-story `agent:` front matter requests.
default_coder_model = "sonnet"
# Maximum concurrent coder agents. Stories wait in 2_current/ when all slots are full.
max_coders = 3
# Maximum retries per story per pipeline stage before marking as blocked.
# Set to 0 to disable retry limits.
max_retries = 2
# Base branch name for this project. Worktree creation, merges, and agent prompts
# use this value for {{base_branch}}. When not set, falls back to auto-detection
# (reads current HEAD branch).
base_branch = "main"
[[component]]
name = "server"
path = "."
setup = ["cargo build"]
teardown = []
[[agent]]
name = "coder-1"
role = "Full-stack engineer"
stage = "coder"
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 .huskies/README.md to understand the dev process.
Run: cd "{{worktree_path}}" && git difftool {{base_branch}}...HEAD
Commit all your work before your process exits.
"""
+33
View File
@@ -0,0 +1,33 @@
# Project Context
## High-Level Goal
To build a standalone **Agentic AI Code Assistant** application as a single Rust binary that serves a Vite/React web UI and exposes a WebSocket API. The assistant will facilitate a test-driven development (TDD) workflow first, with both unit and integration tests providing the primary guardrails for code changes. Once the single-threaded TDD workflow is stable and usable (including compatibility with lower-cost agents), the project will evolve to a multi-agent orchestration model using Git worktrees and supervisory roles to maximize throughput. Unlike a passive chat interface, this assistant acts as an **Agent**, capable of using tools to read the filesystem, execute shell commands, manage git repositories, and modify code directly to implement features.
## Core Features
1. **Chat Interface:** A conversational UI for the user to interact with the AI assistant.
2. **Agentic Tool Bridge:** A robust system mapping LLM "Tool Calls" to native Rust functions.
* **Filesystem:** Read/Write access (scoped to the target project).
* **Search:** High-performance file searching (ripgrep-style) and content retrieval.
* **Shell Integration:** Ability to execute approved commands (e.g., `cargo`, `npm`, `git`) to run tests, linters, and version control.
3. **Workflow Management:** Specialized tools to manage a TDD-first lifecycle:
* Defining test requirements (unit + integration) before code changes.
* Implementing code via red-green-refactor.
* Enforcing test and quality gates before acceptance.
* Scaling later to multi-agent orchestration with Git worktrees and supervisory checks, after the single-threaded process is stable.
4. **LLM Integration:** Connection to an LLM backend to drive the intelligence and tool selection.
* **Remote:** Support for major APIs (Anthropic Claude, Google Gemini, OpenAI, etc).
* **Local:** Support for local inference via Ollama.
## Domain Definition
* **User:** A software engineer using the assistant to build a project.
* **Target Project:** The local software project the user is working on.
* **Agent:** The AI entity that receives prompts and decides which **Tools** to invoke to solve the problem.
* **Tool:** A discrete function exposed to the Agent (e.g., `run_shell_command`, `write_file`, `search_project`).
* **Story:** A unit of work defining a change (Feature Request).
* **Spec:** A persistent documentation artifact defining the current truth of the system.
## Glossary
* **SDSW:** Story-Driven Spec Workflow.
* **Web Server Binary:** The Rust binary that serves the Vite/React frontend and exposes the WebSocket API.
* **Living Spec:** The collection of Markdown files in `.story_kit/` that define the project.
* **Tool Call:** A structured request from the LLM to execute a specific native function.
+44
View File
@@ -0,0 +1,44 @@
# Slack Integration Setup
## Bot Configuration
Slack integration is configured via `bot.toml` in the project's `.story_kit/` directory:
```toml
transport = "slack"
display_name = "Huskies"
slack_bot_token = "xoxb-..."
slack_signing_secret = "..."
slack_channel_ids = ["C01ABCDEF"]
```
## Slack App Configuration
### Event Subscriptions
1. In your Slack app settings, enable **Event Subscriptions**.
2. Set the **Request URL** to: `https://<your-host>/webhook/slack`
3. Subscribe to the `message.channels` and `message.im` bot events.
### Slash Commands
Slash commands provide quick access to pipeline commands without mentioning the bot.
1. In your Slack app settings, go to **Slash Commands**.
2. Create the following commands, all pointing to the same **Request URL**: `https://<your-host>/webhook/slack/command`
| Command | Description |
|---------|-------------|
| `/huskies-status` | Show pipeline status and agent availability |
| `/huskies-cost` | Show token spend: 24h total, top stories, and breakdown |
| `/huskies-show` | Display the full text of a work item (e.g. `/huskies-show 42`) |
| `/huskies-git` | Show git status: branch, changes, ahead/behind |
| `/huskies-htop` | Show system and agent process dashboard |
All slash command responses are **ephemeral** — only the user who invoked the command sees the response.
### OAuth & Permissions
Required bot token scopes:
- `chat:write` — send messages
- `commands` — handle slash commands
+33
View File
@@ -0,0 +1,33 @@
# Functional Spec: UI Layout
## 1. Global Structure
The application uses a **fixed-layout** strategy to maximize chat visibility.
```text
+-------------------------------------------------------+
| HEADER (Fixed Height, e.g., 50px) |
| [Project: ~/foo/bar] [Model: llama3] [x] Tools |
+-------------------------------------------------------+
| |
| CHAT AREA (Flex Grow, Scrollable) |
| |
| (User Message) |
| (Agent Message) |
| |
+-------------------------------------------------------+
| INPUT AREA (Fixed Height, Bottom) |
| [ Input Field ........................... ] [Send] |
+-------------------------------------------------------+
```
## 2. Components
* **Header:** Contains global context (Project) and session config (Model/Tools).
* *Constraint:* Must not scroll away.
* **ChatList:** The scrollable container for messages.
* **InputBar:** Pinned to the bottom.
## 3. Styling
* Use Flexbox (`flex-direction: column`) on the main container.
* Header: `flex-shrink: 0`.
* ChatList: `flex-grow: 1`, `overflow-y: auto`.
* InputBar: `flex-shrink: 0`.
+474
View File
@@ -0,0 +1,474 @@
# Functional Spec: UI/UX Responsiveness
## Problem
Currently, the `chat` command in Rust is an async function that performs a long-running, blocking loop (waiting for LLM, executing tools). While Tauri executes this on a separate thread from the UI, the frontend awaits the *entire* result before re-rendering. This makes the app feel "frozen" because there is no feedback during the 10-60 seconds of generation.
## Solution: Event-Driven Feedback
Instead of waiting for the final array of messages, the Backend should emit **Events** to the Frontend in real-time.
### 1. Events
* `chat:token`: Emitted when a text token is generated (Streaming text).
* `chat:tool-start`: Emitted when a tool call begins (e.g., `{ tool: "git status" }`).
* `chat:tool-end`: Emitted when a tool call finishes (e.g., `{ output: "..." }`).
### 2. Implementation Strategy
#### Token-by-Token Streaming (Story 18)
The system now implements full token streaming for real-time response display:
* **Backend (Rust):**
* Set `stream: true` in Ollama API requests
* Parse newline-delimited JSON from Ollama's streaming response
* Emit `chat:token` events for each token received
* Use `reqwest` streaming body with async iteration
* After streaming completes, emit `chat:update` with the full message
* **Frontend (TypeScript):**
* Listen for `chat:token` events
* Append tokens to the current assistant message in real-time
* Maintain smooth auto-scroll as tokens arrive
* After streaming completes, process `chat:update` for final state
* **Event-Driven Updates:**
* `chat:token`: Emitted for each token during streaming (payload: `{ content: string }`)
* `chat:update`: Emitted after LLM response complete or after Tool Execution (payload: `Message[]`)
* Frontend maintains streaming state separate from message history
### 3. Visuals
* **Loading State:** The "Send" button should show a spinner or "Stop" button.
* **Auto-Scroll:** The chat view uses smart auto-scroll that respects user scrolling (see Smart Auto-Scroll section below).
## Smart Auto-Scroll (Story 22)
### Problem
Users need to review previous messages while the AI is streaming new content, but aggressive auto-scrolling constantly drags them back to the bottom, making it impossible to read older content.
### Solution: Scroll-Position-Aware Auto-Scroll
The chat implements intelligent auto-scroll that:
* Automatically scrolls to show new content when the user is at/near the bottom
* Pauses auto-scroll when the user scrolls up to review older messages
* Resumes auto-scroll when the user scrolls back to the bottom
### Requirements
1. **Scroll Detection:** Track whether the user is at the bottom of the chat
2. **Threshold:** Define "near bottom" as within 25px of the bottom
3. **Auto-Scroll Logic:** Only trigger auto-scroll if user is at/near bottom
4. **Smooth Operation:** No flickering or jarring behavior during scrolling
5. **Universal:** Works during both streaming responses and tool execution
### Implementation Notes
**Core Components:**
* `scrollContainerRef`: Reference to the scrollable messages container
* `shouldAutoScrollRef`: Tracks whether auto-scroll should be active (uses ref to avoid re-renders)
* `messagesEndRef`: Target element for scroll-to-bottom behavior
**Detection Function:**
```typescript
const isScrolledToBottom = () => {
const element = scrollContainerRef.current;
if (!element) return true;
const threshold = 25; // pixels from bottom
return (
element.scrollHeight - element.scrollTop - element.clientHeight < threshold
);
};
```
**Scroll Handler:**
```typescript
const handleScroll = () => {
// Update auto-scroll state based on scroll position
shouldAutoScrollRef.current = isScrolledToBottom();
};
```
**Conditional Auto-Scroll:**
```typescript
useEffect(() => {
if (shouldAutoScrollRef.current) {
scrollToBottom();
}
}, [messages, streamingContent]);
```
**DOM Setup:**
* Attach `ref={scrollContainerRef}` to the messages container
* Attach `onScroll={handleScroll}` to detect user scrolling
* Initialize `shouldAutoScrollRef` to `true` (enable auto-scroll by default)
### Edge Cases
1. **Initial Load:** Auto-scroll is enabled by default
2. **Rapid Scrolling:** Uses refs to avoid race conditions and excessive re-renders
3. **Manual Scroll to Bottom:** Auto-scroll re-enables when user scrolls near bottom
4. **No Container:** Falls back to always allowing auto-scroll if container ref is null
## Tool Output Display
### Problem
Tool outputs (like file contents, search results, or command output) can be very long, making the chat history difficult to read. Users need to see the Agent's reasoning and responses without being overwhelmed by verbose tool output.
### Solution: Collapsible Tool Outputs
Tool outputs should be rendered in a collapsible component that is **closed by default**.
### Requirements
1. **Default State:** Tool outputs are collapsed/closed when first rendered
2. **Summary Line:** Shows essential information without expanding:
- Tool name (e.g., `read_file`, `exec_shell`)
- Key arguments (e.g., file path, command name)
- Format: "▶ tool_name(key_arg)"
- Example: "▶ read_file(src/main.rs)"
- Example: "▶ exec_shell(cargo check)"
3. **Expandable:** User can click the summary to toggle expansion
4. **Output Display:** When expanded, shows the complete tool output in a readable format:
- Use `<pre>` or monospace font for code/terminal output
- Preserve whitespace and line breaks
- Limit height with scrolling for very long outputs (e.g., max-height: 300px)
5. **Visual Indicator:** Clear arrow or icon showing collapsed/expanded state
6. **Styling:** Consistent with the dark theme, distinguishable from assistant messages
### Implementation Notes
* Use native `<details>` and `<summary>` HTML elements for accessibility
* Or implement custom collapsible component with proper ARIA attributes
* Tool outputs should be visually distinct (border, background color, or badge)
* Multiple tool calls in sequence should each be independently collapsible
## Scroll Bar Styling
### Problem
Visible scroll bars create visual clutter and make the interface feel less polished. Standard browser scroll bars can be distracting and break the clean aesthetic of the dark theme.
### Solution: Hidden Scroll Bars with Maintained Functionality
Scroll bars should be hidden while maintaining full scroll functionality.
### Requirements
1. **Visual:** Scroll bars should not be visible to the user
2. **Functionality:** Scrolling must still work perfectly:
- Mouse wheel scrolling
- Trackpad scrolling
- Keyboard navigation (arrow keys, page up/down)
- Auto-scroll to bottom for new messages
3. **Cross-browser:** Solution must work on Chrome, Firefox, and Safari
4. **Areas affected:**
- Main chat message area (vertical scroll)
- Tool output content (both vertical and horizontal)
- Any other scrollable containers
### Implementation Notes
* Use CSS `scrollbar-width: none` for Firefox
* Use `::-webkit-scrollbar { display: none; }` for Chrome/Safari/Edge
* Maintain `overflow: auto` or `overflow-y: scroll` to preserve scroll functionality
* Ensure `overflow-x: hidden` where horizontal scroll is not needed
* Test with very long messages and large tool outputs to ensure no layout breaking
## Text Alignment and Readability
### Problem
Center-aligned text in a chat interface is unconventional and reduces readability, especially for code blocks and long-form content. Standard chat UIs align messages differently based on the sender.
### Solution: Context-Appropriate Text Alignment
Messages should follow standard chat UI conventions with proper alignment based on message type.
### Requirements
1. **User Messages:** Right-aligned (standard pattern showing messages sent by the user)
2. **Assistant Messages:** Left-aligned (standard pattern showing messages received)
3. **Tool Outputs:** Left-aligned (part of the system/assistant response flow)
4. **Code Blocks:** Always left-aligned regardless of message type (for readability)
5. **Container:** Remove any center-alignment from the chat container
6. **Max-Width:** Maintain current max-width constraint (e.g., 768px) for optimal readability
7. **Spacing:** Maintain proper padding and visual hierarchy between messages
### Implementation Notes
* Check for `textAlign: "center"` in inline styles and remove
* Check for `text-align: center` in CSS and remove from chat-related classes
* Ensure flexbox alignment is set appropriately:
* User messages: `alignItems: "flex-end"`
* Assistant/Tool messages: `alignItems: "flex-start"`
* Code blocks should have `text-align: left` explicitly set
## Syntax Highlighting
### Problem
Code blocks in assistant responses currently lack syntax highlighting, making them harder to read and understand. Developers expect colored syntax highlighting similar to their code editors.
### Solution: Syntax Highlighting for Code Blocks
Integrate syntax highlighting into markdown code blocks rendered by the assistant.
### Requirements
1. **Languages Supported:** At minimum:
- JavaScript/TypeScript
- Rust
- Python
- JSON
- Markdown
- Shell/Bash
- HTML/CSS
- SQL
2. **Theme:** Use a dark theme that complements the existing dark UI (e.g., `oneDark`, `vsDark`, `dracula`)
3. **Integration:** Work seamlessly with `react-markdown` component
4. **Performance:** Should not significantly impact rendering performance
5. **Fallback:** Plain monospace text for unrecognized languages
6. **Inline Code:** Inline code (single backticks) should maintain simple styling without full syntax highlighting
### Implementation Notes
* Use `react-syntax-highlighter` library with `react-markdown`
* Or use `rehype-highlight` plugin for `react-markdown`
* Configure with a dark theme preset (e.g., `oneDark` from `react-syntax-highlighter/dist/esm/styles/prism`)
* Apply to code blocks via `react-markdown` components prop:
```tsx
<Markdown
components={{
code: ({node, inline, className, children, ...props}) => {
const match = /language-(\w+)/.exec(className || '');
return !inline && match ? (
<SyntaxHighlighter style={oneDark} language={match[1]} {...props}>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
) : (
<code className={className} {...props}>{children}</code>
);
}
}}
/>
```
* Ensure syntax highlighted code blocks are left-aligned
* Test with various code samples to ensure proper rendering
## Token Streaming
### Problem
Without streaming, users see no feedback during model generation. The response appears all at once after waiting, which feels unresponsive and provides no indication that the system is working.
### Solution: Token-by-Token Streaming
Stream tokens from Ollama in real-time and display them as they arrive, providing immediate feedback and a responsive chat experience similar to ChatGPT.
### Requirements
1. **Real-time Display:** Tokens appear immediately as Ollama generates them
2. **Smooth Performance:** No lag or stuttering during high token throughput
3. **Tool Compatibility:** Streaming works correctly with tool calls and multi-turn conversations
4. **Auto-scroll:** Chat view follows streaming content automatically
5. **Error Handling:** Gracefully handle stream interruptions or errors
6. **State Management:** Maintain clean separation between streaming state and final message history
### Implementation Notes
#### Backend (Rust)
* Enable streaming in Ollama requests: `stream: true`
* Parse newline-delimited JSON from response body
* Each line is a separate JSON object: `{"message":{"content":"token"},"done":false}`
* Use `futures::StreamExt` or similar for async stream processing
* Emit `chat:token` event for each token
* Emit `chat:update` when streaming completes
* Handle both streaming text and tool call interruptions
#### Frontend (TypeScript)
* Create streaming state separate from message history
* Listen for `chat:token` events and append to streaming buffer
* Render streaming content in real-time
* On `chat:update`, replace streaming content with final message
* Maintain scroll position during streaming
#### Ollama Streaming Format
```json
{"message":{"role":"assistant","content":"Hello"},"done":false}
{"message":{"role":"assistant","content":" world"},"done":false}
{"message":{"role":"assistant","content":"!"},"done":true}
{"message":{"role":"assistant","tool_calls":[...]},"done":true}
```
### Edge Cases
* Tool calls during streaming: Switch from text streaming to tool execution
* Cancellation during streaming: Clean up streaming state properly
* Network interruptions: Show error and preserve partial content
* Very fast streaming: Throttle UI updates if needed for performance
## Input Focus Management
### Problem
When the app loads with a project selected, users need to click into the chat input box before they can start typing. This adds unnecessary friction to the user experience.
### Solution: Auto-focus on Component Mount
The chat input field should automatically receive focus when the chat component mounts, allowing users to immediately start typing.
### Requirements
1. **Auto-focus:** Input field receives focus automatically when chat component loads
2. **Visible Cursor:** Cursor should be visible and blinking in the input field
3. **Immediate Typing:** User can start typing without clicking into the field
4. **Non-intrusive:** Should not interfere with other UI interactions or accessibility
5. **Timing:** Focus should be set after the component fully mounts
### Implementation Notes
* Use React `useRef` to create a reference to the input element
* Use `useEffect` with empty dependency array to run once on mount
* Call `inputRef.current?.focus()` in the effect
* Ensure the ref is properly attached to the input element
* Example implementation:
```tsx
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} ... />
```
## Response Interruption
### Problem
Users may want to interrupt a long-running model response to ask a different question or change direction. Having to wait for the full response to complete creates friction and wastes time.
### Solution: Interrupt on Typing
When the user starts typing in the input field while the model is generating a response, the generation should be cancelled immediately, allowing the user to send a new message.
### Requirements
1. **Input Always Enabled:** The input field should remain enabled and usable even while the model is generating
2. **Interrupt Detection:** Detect when user types in the input field while `loading` state is true
3. **Immediate Cancellation:** Cancel the ongoing generation as soon as typing is detected
4. **Preserve Partial Response:** Any partial response generated before interruption should remain visible in the chat
5. **State Reset:** UI should return to normal state (ready to send) after interruption
6. **Preserve User Input:** The user's new input should be preserved in the input field
7. **Visual Feedback:** "Thinking..." indicator should disappear when generation is interrupted
### Implementation Notes
* Do NOT disable the input field during loading
* Listen for input changes while `loading` is true
* When user types during loading, call backend to cancel generation (if possible) or just stop waiting
* Set `loading` state to false immediately when typing detected
* Backend may need a `cancel_chat` command or similar
* Consider if Ollama requests can be cancelled mid-generation or if we just stop processing the response
* Example implementation:
```tsx
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
setInput(newValue);
// If user starts typing while model is generating, interrupt
if (loading && newValue.length > input.length) {
setLoading(false);
// Optionally call backend to cancel: invoke("cancel_chat")
}
};
```
## Session Management
### Problem
Users may want to start a fresh conversation without restarting the application. Long conversations can become unwieldy, and users need a way to clear context for new tasks while keeping the same project open.
### Solution: New Session Button
Provide a clear, accessible way for users to start a new session by clearing the chat history.
### Requirements
1. **Button Placement:** Located in the header area, near model controls
2. **Visual Design:** Secondary/subtle styling to prevent accidental clicks
3. **Confirmation Dialog:** Ask "Are you sure? This will clear all messages." before clearing
4. **State Management:**
- Clear `messages` state array
- Clear `streamingContent` if any streaming is in progress
- Preserve project path, model selection, and tool settings
- Cancel any in-flight backend operations before clearing
5. **User Feedback:** Immediate visual response (messages disappear)
6. **Empty State:** Show a welcome message or empty state after clearing
### Implementation Notes
**Frontend:**
- Add "New Session" button to header
- Implement confirmation modal/dialog
- Call `setMessages([])` after confirmation
- Cancel any ongoing streaming/tool execution
- Consider keyboard shortcut (e.g., Cmd/Ctrl+K)
**Backend:**
- May need to cancel ongoing chat operations
- Clear any server-side state if applicable
- No persistent session history (sessions are ephemeral)
**Edge Cases:**
- Don't clear while actively streaming (cancel first, then clear)
- Handle confirmation dismissal (do nothing)
- Ensure button is always accessible (not disabled)
### Button Label Options
- "New Session" (clear and descriptive)
- "Clear Chat" (direct but less friendly)
- "Start Over" (conversational)
- Icon: 🔄 or ⊕ (plus in circle)
## Context Window Usage Display
### Problem
Users have no visibility into how much of the model's context window they're using. This leads to:
- Unexpected quality degradation when context limit is reached
- Uncertainty about when to start a new session
- Inability to gauge conversation length
### Solution: Real-time Context Usage Indicator
Display a persistent indicator showing current token usage vs. model's context window limit.
### Requirements
1. **Visual Indicator:** Always visible in header area
2. **Real-time Updates:** Updates as messages are added
3. **Model-Aware:** Shows correct limit based on selected model
4. **Color Coding:** Visual warning as limit approaches
- Green/default: 0-74% usage
- Yellow/warning: 75-89% usage
- Red/danger: 90-100% usage
5. **Clear Format:** "2.5K / 8K tokens (31%)" or similar
6. **Token Estimation:** Approximate token count for all messages
### Implementation Notes
**Token Estimation:**
- Use simple approximation: 1 token ≈ 4 characters
- Or integrate `gpt-tokenizer` for more accuracy
- Count: system prompts + user messages + assistant responses + tool outputs + tool calls
**Model Context Windows:**
- llama3.1, llama3.2: 8K tokens
- qwen2.5-coder: 32K tokens
- deepseek-coder: 16K tokens
- Default/unknown: 8K tokens
**Calculation:**
```tsx
const estimateTokens = (text: string): number => {
return Math.ceil(text.length / 4);
};
const calculateContextUsage = (messages: Message[], systemPrompt: string) => {
let total = estimateTokens(systemPrompt);
messages.forEach(msg => {
total += estimateTokens(msg.content);
if (msg.tool_calls) {
total += estimateTokens(JSON.stringify(msg.tool_calls));
}
});
return total;
};
```
**UI Placement:**
- Header area, near model selector
- Non-intrusive but always visible
- Optional tooltip with breakdown on hover
### Edge Cases
- Empty conversation: Show "0 / 8K"
- During streaming: Include partial content
- After clearing: Reset to 0
- Model change: Update context window limit
+130
View File
@@ -0,0 +1,130 @@
# Tech Stack & Constraints
## Overview
This project is a standalone Rust **web server binary** that serves a Vite/React frontend and exposes a **WebSocket API**. The built frontend assets are packaged with the binary (in a `frontend` directory) and served as static files. It functions as an **Agentic Code Assistant** capable of safely executing tools on the host system.
## Core Stack
* **Backend:** Rust (Web Server)
* **MSRV:** Stable (latest)
* **Framework:** Poem HTTP server with WebSocket support for streaming; HTTP APIs should use Poem OpenAPI (Swagger) for non-streaming endpoints.
* **Frontend:** TypeScript + React
* **Build Tool:** Vite
* **Package Manager:** npm
* **Styling:** CSS Modules or Tailwind (TBD - Defaulting to CSS Modules)
* **State Management:** React Context / Hooks
* **Chat UI:** Rendered Markdown with syntax highlighting.
## Agent Architecture
The application follows a **Tool-Use (Function Calling)** architecture:
1. **Frontend:** Collects user input and sends it to the LLM.
2. **LLM:** Decides to generate text OR request a **Tool Call** (e.g., `execute_shell`, `read_file`).
3. **Web Server Backend (The "Hand"):**
* Intercepts Tool Calls.
* Validates the request against the **Safety Policy**.
* Executes the native code (File I/O, Shell Process, Search).
* Returns the output (stdout/stderr/file content) to the LLM.
* **Streaming:** The backend sends real-time updates over WebSocket to keep the UI responsive during long-running Agent tasks.
## LLM Provider Abstraction
To support both Remote and Local models, the system implements a `ModelProvider` abstraction layer.
* **Strategy:**
* Abstract the differences between API formats (OpenAI-compatible vs Anthropic vs Gemini).
* Normalize "Tool Use" definitions, as each provider handles function calling schemas differently.
* **Supported Providers:**
* **Ollama:** Local inference (e.g., Llama 3, DeepSeek Coder) for privacy and offline usage.
* **Anthropic:** Claude 3.5 models (Sonnet, Haiku) via API for coding tasks (Story 12).
* **Provider Selection:**
* Automatic detection based on model name prefix:
* `claude-` → Anthropic API
* Otherwise → Ollama
* Single unified model dropdown with section headers ("Anthropic", "Ollama")
* **API Key Management:**
* Anthropic API key stored server-side and persisted securely
* On first use of Claude model, user prompted to enter API key
* Key persists across sessions (no re-entry needed)
## Tooling Capabilities
### 1. Filesystem (Native)
* **Scope:** Strictly limited to the user-selected `project_root`.
* **Operations:** Read, Write, List, Delete.
* **Constraint:** Modifications to `.git/` are strictly forbidden via file APIs (use Git tools instead).
### 2. Shell Execution
* **Library:** `tokio::process` for async execution.
* **Constraint:** We do **not** run an interactive shell (repl). We run discrete, stateless commands.
* **Allowlist:** The agent may only execute specific binaries:
* `git`
* `cargo`, `rustc`, `rustfmt`, `clippy`
* `npm`, `node`, `yarn`, `pnpm`, `bun`
* `ls`, `find`, `grep` (if not using internal search)
* `mkdir`, `rm`, `touch`, `mv`, `cp`
### 3. Search & Navigation
* **Library:** `ignore` (by BurntSushi) + `grep` logic.
* **Behavior:**
* Must respect `.gitignore` files automatically.
* Must be performant (parallel traversal).
## Coding Standards
### Rust
* **Style:** `rustfmt` standard.
* **Linter:** `clippy` - Must pass with 0 warnings before merging.
* **Error Handling:** Custom `AppError` type deriving `thiserror`. All Commands return `Result<T, AppError>`.
* **Concurrency:** Heavy tools (Search, Shell) must run on `tokio` threads to avoid blocking the UI.
* **Quality Gates:**
* `cargo clippy --all-targets --all-features` must show 0 errors, 0 warnings
* `cargo check` must succeed
* `cargo nextest run` must pass all tests
* **Test Coverage:**
* Generate JSON report: `cargo llvm-cov nextest --no-clean --json --output-path .story_kit/coverage/server.json`
* Generate lcov report: `cargo llvm-cov report --lcov --output-path .story_kit/coverage/server.lcov`
* Reports are written to `.story_kit/coverage/` (excluded from git)
### TypeScript / React
* **Style:** Biome formatter (replaces Prettier/ESLint).
* **Linter:** Biome - Must pass with 0 errors, 0 warnings before merging.
* **Types:** Shared types with Rust (via `tauri-specta` or manual interface matching) are preferred to ensure type safety across the bridge.
* **Testing:** Vitest for unit/component tests; Playwright for end-to-end tests.
* **Quality Gates:**
* `npx @biomejs/biome check src/` must show 0 errors, 0 warnings
* `npm run build` must succeed
* `npm test` must pass
* `npm run test:e2e` must pass
* No `any` types allowed (use proper types or `unknown`)
* React keys must use stable IDs, not array indices
* All buttons must have explicit `type` attribute
## Libraries (Approved)
* **Rust:**
* `serde`, `serde_json`: Serialization.
* `ignore`: Fast recursive directory iteration respecting gitignore.
* `walkdir`: Simple directory traversal.
* `tokio`: Async runtime.
* `reqwest`: For LLM API calls (Anthropic, Ollama).
* `eventsource-stream`: For Server-Sent Events (Anthropic streaming).
* `uuid`: For unique message IDs.
* `chrono`: For timestamps.
* `poem`: HTTP server framework.
* `poem-openapi`: OpenAPI (Swagger) for non-streaming HTTP APIs.
* **JavaScript:**
* `react-markdown`: For rendering chat responses.
* `vitest`: Unit/component testing.
* `playwright`: End-to-end testing.
## Running the App (Worktrees & Ports)
Multiple instances can run simultaneously in different worktrees. To avoid port conflicts:
- **Backend:** Set `HUSKIES_PORT` to a unique port (default is 3001). Example: `HUSKIES_PORT=3002 cargo run`
- **Frontend:** Run `npm run dev` from `frontend/`. It auto-selects the next unused port. It reads `HUSKIES_PORT` to know which backend to talk to, so export it before running: `export HUSKIES_PORT=3002 && cd frontend && npm run dev`
When running in a worktree, use a port that won't conflict with the main instance (3001). Ports 3002+ are good choices.
## Safety & Sandbox
1. **Project Scope:** The application must strictly enforce that it does not read/write outside the `project_root` selected by the user.
2. **Human in the Loop:**
* Shell commands that modify state (non-readonly) should ideally require a UI confirmation (configurable).
* File writes must be confirmed or revertible.
View File
@@ -0,0 +1,24 @@
---
name: "Upgrade libsqlite3-sys"
---
# Refactor 260: Upgrade libsqlite3-sys
## Description
Upgrade the `libsqlite3-sys` dependency from `0.35.0` to `0.37.0`. The crate is used with `features = ["bundled"]` for static builds.
## Version Notes
- Current: `libsqlite3-sys 0.35.0` (pinned transitively by `matrix-sdk 0.16.0``matrix-sdk-sqlite``rusqlite 0.37.x`)
- Target: `libsqlite3-sys 0.37.0`
- Latest upstream rusqlite: `0.39.0`
- **Blocker**: `matrix-sdk 0.16.0` pins `rusqlite 0.37.x` which pins `libsqlite3-sys 0.35.0`. A clean upgrade requires either waiting for matrix-sdk to bump their rusqlite dep, or upgrading matrix-sdk itself.
- **Reverted 2026-03-17**: A previous coder vendored the entire rusqlite crate with a fake `0.37.99` version and patched its libsqlite3-sys dep. This was too hacky — reverted to clean `0.35.0`.
## Acceptance Criteria
- [ ] `libsqlite3-sys` is upgraded to `0.37.0` via a clean dependency path (no vendored forks)
- [ ] `cargo build` succeeds
- [ ] All tests pass
- [ ] No `[patch.crates-io]` hacks or vendored crates
@@ -0,0 +1,24 @@
---
name: "WhatsApp webhook HMAC signature verification"
retry_count: 3
blocked: true
---
# Story 388: WhatsApp webhook HMAC signature verification
## User Story
As a bot operator, I want incoming WhatsApp webhook requests to be cryptographically verified, so that forged requests from unauthorized sources are rejected.
## Acceptance Criteria
- [ ] Meta webhooks: validate X-Hub-Signature-256 HMAC-SHA256 header using the app secret before processing
- [ ] Twilio webhooks: validate request signature using the auth token before processing
- [ ] Requests with missing or invalid signatures are rejected with 403 Forbidden
- [ ] Verification is fail-closed: if signature checking is configured, unsigned requests are rejected
- [ ] Existing bot.toml config is extended with any needed secrets (e.g. Meta app_secret for HMAC verification)
- [ ] MUST use audited crypto crates (hmac, sha2, sha1, base64) — no hand-rolled cryptographic primitives
## Out of Scope
- TBD
@@ -0,0 +1,40 @@
---
name: "Fly.io Machines API integration for multi-tenant huskies SaaS"
---
# Spike 408: Fly.io Machines API integration for multi-tenant huskies SaaS
## Question
Can we build a working Rust integration that creates and manages per-tenant Fly.io Machines, attaches volumes, injects Claude credentials, and proxies JWT-authenticated HTTP/WebSocket traffic to the right machine?
## Hypothesis
A thin Rust service using `reqwest` for the Machines API and `axum` for the reverse proxy is sufficient. No heavyweight orchestration framework needed.
## Prerequisites
- Fly.io account with API token (set `FLY_API_TOKEN` env var)
- Spike 407 findings reviewed
## Timebox
4 hours
## Investigation Plan
- [ ] Create a minimal Rust crate in `spikes/fly_machines/` — do not touch production code
- [ ] Implement machine lifecycle: create, start, stop, destroy via Fly Machines REST API using `reqwest`
- [ ] Test attaching a persistent volume to a machine and verify it persists across stop/start
- [ ] Test secret injection — pass a dummy `credentials.json` as a Fly secret and verify it's readable inside the machine
- [ ] Sketch the auth proxy: JWT validation → machine lookup → reverse proxy to machine's private IP; verify WebSocket proxying works
- [ ] Measure actual cold start time for a minimal huskies container image
- [ ] Document any API quirks, rate limits, or sharp edges discovered during testing
## Findings
- TBD
## Recommendation
- TBD
@@ -0,0 +1,22 @@
---
name: "Multi-account OAuth token rotation on rate limit"
---
# Story 411: Multi-account OAuth token rotation on rate limit
## User Story
As a huskies user with multiple Claude Max subscriptions, I want the system to automatically rotate to a different account when one gets rate limited, so that agents and chat don't stall out waiting for limits to reset.
## Acceptance Criteria
- [ ] OAuth login flow stores credentials per-account (keyed by email), not overwriting previous accounts
- [ ] GET /oauth/status returns all stored accounts and their status (active, rate-limited, expired)
- [ ] When the active account hits a rate limit, huskies automatically swaps to the next available account's refresh token, refreshes, and retries
- [ ] The bot sends a notification in Matrix/WhatsApp when it swaps accounts
- [ ] If all accounts are rate limited, the bot surfaces a clear message with the time until the earliest reset
- [ ] A new /oauth/authorize login adds to the account pool rather than replacing the current credentials
## Out of Scope
- TBD
@@ -0,0 +1,24 @@
---
name: "Recheck bot command to re-run gates without restarting agent"
---
# Story 412: Recheck bot command to re-run gates without restarting agent
## User Story
As a user, I want to send `recheck <number>` to the bot so that it re-runs acceptance gates on an existing worktree without spawning a new agent, so I can unblock stories that failed due to environment issues without wasting agent turns.
## Acceptance Criteria
- [ ] recheck command is registered in chat/commands/mod.rs and appears in help output
- [ ] `recheck <number>` runs run_acceptance_gates on the story's existing worktree
- [ ] If gates pass, the story advances through the pipeline (same as if a coder completed successfully)
- [ ] If gates fail, the error output is returned to the user (not silently retried)
- [ ] If no worktree exists for the story, returns a clear error
- [ ] Does not spawn a new agent or increment retry_count
- [ ] Works from all transports (Matrix, WhatsApp, Slack)
- [ ] Works from web UI slash commands
## Out of Scope
- TBD
@@ -0,0 +1,21 @@
---
name: "Unblock command handles all stuck states not just blocked flag"
---
# Story 435: Unblock command handles all stuck states not just blocked flag
## User Story
As a project owner, I want the unblock command to clear any stuck state on a story — not just the blocked flag — so that I have a single command to unstick stories regardless of why they're stuck.
## Acceptance Criteria
- [ ] Unblock clears merge_failure field in addition to blocked flag
- [ ] Unblock clears review_hold field
- [ ] Unblock reports which fields were cleared in the confirmation message
- [ ] Unblock works on stories in any pipeline stage (backlog, current, qa, merge, done)
- [ ] If no stuck state is found (no blocked, merge_failure, or review_hold), returns a clear message saying so
## Out of Scope
- TBD
@@ -0,0 +1,26 @@
---
name: "Unify story stuck states into a single status field"
---
# Refactor 436: Unify story stuck states into a single status field
## Current State
- TBD
## Desired State
Replace the separate blocked, merge_failure, and review_hold front matter fields with a single status field (e.g. status: blocked, status: merge_failure, status: review_hold). Simplifies the unblock command, auto-assign checks, and pipeline advance logic.
## Acceptance Criteria
- [ ] Replace blocked: true, merge_failure: string, and review_hold: true with a single status: field in story front matter
- [ ] Auto-assign checks a single field instead of three separate ones
- [ ] Pipeline advance and lifecycle code reads/writes the unified status field
- [ ] Unblock command clears the status field regardless of which stuck state it was
- [ ] retry_count remains a separate field (it's a counter, not a state)
- [ ] Migration: existing stories with old fields are handled gracefully on read
## Out of Scope
- TBD
@@ -0,0 +1,31 @@
---
name: "Rename project from \"huskies\" to \"huskies\""
---
# Story 455: Rename project from "huskies" to "huskies"
## User Story
As a project maintainer, I want to rename the project from \"huskies\" to \"huskies\" so that the product has its new identity throughout the codebase, tooling, and documentation. The new domain is huskies.dev — update all references to huskies.dev accordingly (website, contact email hello@huskies.dev, etc).
## Acceptance Criteria
- [ ] Rust crate name in server/Cargo.toml changed from 'huskies' to 'huskies'
- [ ] Binary name changed to 'huskies' (Dockerfile CMD, release script binary names)
- [ ] Environment variables renamed: STORKIT_PORT → HUSKIES_PORT, STORKIT_HOST → HUSKIES_HOST
- [ ] Docker service name, container_name, image name, and volume names updated in docker-compose.yml
- [ ] Docker user/group renamed from 'huskies' to 'huskies' in Dockerfile (groupadd, useradd, home dir /home/huskies/.claude)
- [ ] MCP server registration renamed from 'huskies' to 'huskies' in scaffold-generated .mcp.json and in server/src/http/mcp/mod.rs serverInfo name
- [ ] All 35+ MCP tool permission patterns updated from mcp__huskies__* to mcp__huskies__* across code and permission configs
- [ ] The .huskies/ project directory marker renamed to .huskies/ throughout all Rust source (paths.rs, config.rs, scaffold.rs, watcher.rs, prompts.rs, and all agent/pipeline code)
- [ ] Release script updated: Gitea repo path dave/huskies → dave/huskies, changelog regex updated to match ^(huskies|huskies|story-kit): for backwards-compatible history parsing, binary artifact names updated
- [ ] Git commit prefix convention updated from 'huskies:' to 'huskies:' in huskies README and agent prompts
- [ ] Website updated: page title, headings, and contact email (hello@huskies.dev) if domain changes
- [ ] README.md updated: all CLI examples use 'huskies' binary name, all .huskies/ references become .huskies/
- [ ] A migration path exists for existing installs: either huskies auto-detects and migrates .huskies/ → .huskies/, or a migration script (script/migrate) is provided
- [ ] All Claude Code .mcp.json files in existing worktrees are regenerated via scaffold or migration
- [ ] Gitea repository renamed from dave/huskies to dave/huskies (external action required, noted in story)
## Out of Scope
- TBD
@@ -0,0 +1,20 @@
---
name: "MCP tool to return current time in project timezone"
---
# Story 467: MCP tool to return current time in project timezone
## User Story
As an LLM agent (coder, QA, or top-level bot), I want an MCP tool that returns the current time in the project's configured timezone so I can reason about time without having to read project.toml and convert from container UTC manually.
## Acceptance Criteria
- [ ] New MCP tool `get_current_time` returns current date/time in the project's configured timezone
- [ ] Output includes both local time and UTC for clarity
- [ ] Uses the `timezone` field from project.toml, falls back to UTC if not set
- [ ] Includes day of week and timezone abbreviation (e.g. 'Fri 2026-04-03 14:30 BST (13:30 UTC)')
## Out of Scope
- TBD
View File
@@ -0,0 +1,41 @@
---
name: "Stage transition notifications can arrive out of order and show wrong story name"
agent: coder-opus
---
# Bug 462: Stage transition notifications can arrive out of order and show wrong story name
## Description
When a story moves through stages quickly (e.g. QA → Merge → Done), the stage transition notifications can arrive out of order in Matrix chat. The Done notification appears before the Merge notification.
Two issues:
1. **Out-of-order delivery**: When two notifications are sent close together, the Matrix homeserver can deliver them in the wrong order. The notification handler processes events sequentially and awaits each send, but the homeserver does not guarantee ordering for near-simultaneous messages.
2. **Missing story name on stale notifications**: The second notification shows the raw item_id instead of the story name because `read_story_name` looks in the stage directory from the event (e.g. `4_merge/`), but the file has already moved to the next stage (e.g. `5_done/`).
3. **`inferred_from_stage` guesses the source stage** instead of tracking the actual from-stage. This means skipped stages would show incorrect transitions.
## How to Reproduce
1. Have a story in QA that passes quickly
2. Story moves QA → Merge → Done in rapid succession
3. Observe notifications in Matrix
## Actual Result
Notifications arrive in wrong order (Done before Merge). The later notification shows the raw item_id instead of the story name:
🎉 #32 upgrade TypeScript... — Merge → Done
#32 32_story_upgrade_typescript_and_tsconfig... — QA → Merge
## Expected Result
Notifications arrive in chronological order. All notifications show the story name. Ideally, rapid transitions are coalesced into a single notification for the final stage.
## Acceptance Criteria
- [ ] read_story_name falls back to searching all stages when the expected stage directory has no match
- [ ] Consider deduplicating rapid transitions within a short window (e.g. only notify for the final stage)
- [ ] Track actual from-stage in WatcherEvent instead of guessing via inferred_from_stage
@@ -0,0 +1,21 @@
---
name: "Configurable rate limit notification suppression"
---
# Story 463: Configurable rate limit notification suppression
## User Story
As a ..., I want ..., so that ...
## Acceptance Criteria
- [ ] New boolean config field in project.toml (e.g. rate_limit_notifications) defaults to true
- [ ] When false, RateLimitWarning chat notifications are suppressed
- [ ] RateLimitHardBlock and StoryBlocked notifications are always sent regardless of the setting
- [ ] Stage transition notifications are unaffected
- [ ] Config is hot-reloaded when project.toml changes
## Out of Scope
- TBD
@@ -0,0 +1,33 @@
---
name: "Timer rejects backlog stories — should move to current on fire"
---
# Bug 464: Timer rejects backlog stories — should move to current on fire
## Description
The `timer` bot command requires stories to be in `work/2_current/` before scheduling. When a user tries to schedule a backlog story (e.g. `timer 463 12:45`), it returns:
"Story **463_story_...** is not in `work/2_current/`. Move it to current before scheduling a timer."
The timer should accept backlog stories. When the timer fires, it should move the story from backlog to current and let auto-assign start an agent.
## How to Reproduce
1. Have a story in backlog (e.g. 463)
2. Run `timer 463 12:45`
3. Observe rejection message
## Actual Result
Timer command rejects stories not in `work/2_current/`.
## Expected Result
Timer command accepts backlog stories. When the timer fires, it moves the story to current and auto-assign picks it up.
## Acceptance Criteria
- [ ] Timer bot command accepts stories in backlog or current
- [ ] Timer tick loop calls move_story_to_current before start_agent for backlog stories
- [ ] Unit tests cover scheduling and firing for backlog stories
@@ -0,0 +1,36 @@
---
name: "Timer tick loop never fires due entries"
agent: coder-opus
---
# Bug 465: Timer tick loop never fires due entries
## Description
The timer tick loop (`spawn_timer_tick_loop`) is spawned by the Matrix bot runner, the Matrix bot is confirmed running (processing messages), but timers never fire. Past-due entries remain in `.huskies/timers.json` indefinitely — `take_due` never consumes them.
The tick loop uses `tokio::spawn` which swallows panics silently. If `move_story_to_current` or `start_agent` panics on the first tick (when all past-due entries fire at once), the entire task dies with no log output. The PTY debug spam may also push any `[timer]` log entries out of the ring buffer.
The bot command successfully adds entries to the in-memory store and persists them to disk, but the tick loop never processes them.
## How to Reproduce
1. Set a timer via bot command: `timer 463 HH:MM` (a time in the near future)
2. Wait past the scheduled time
3. Check `.huskies/timers.json` — entries are still present
4. Check server logs for `[timer]` — no entries found
## Actual Result
Timer entries remain in timers.json indefinitely. No `[timer] Timer fired` log entries appear. The story is never moved to current and no agent is started.
## Expected Result
Within 30 seconds of the scheduled time, the tick loop should call `take_due`, remove the entry from disk, move the story to current (if in backlog), and start an agent.
## Acceptance Criteria
- [ ] Add panic-catching (catch_unwind or tokio CancellationToken) to the tick loop so failures are logged
- [ ] Add a startup log line confirming the tick loop is running and how many pending timers were loaded
- [ ] Verify take_due runs on each 30-second tick by adding periodic debug logging
- [ ] Integration test: create a past-due timer entry, run the tick loop, assert the entry is consumed
@@ -0,0 +1,21 @@
---
name: "Configurable timezone in project.toml for timer scheduling"
---
# Story 466: Configurable timezone in project.toml for timer scheduling
## User Story
As a user running huskies in a container where TZ defaults to UTC, I want to configure my project's timezone in project.toml so that timer HH:MM inputs are interpreted in my actual timezone.
## Acceptance Criteria
- [ ] Add a `timezone` field to project.toml (e.g. `timezone = "Europe/London"`)
- [ ] next_occurrence_of_hhmm uses the configured timezone instead of chrono::Local
- [ ] Falls back to chrono::Local if no timezone is configured
- [ ] Timer confirmation message displays the time in the configured timezone
- [ ] timer list command shows times in the configured timezone
## Out of Scope
- TBD
@@ -0,0 +1,22 @@
---
name: Project Selection & Read Verification
---
# Story: Project Selection & Read Verification
## User Story
**As a** User
**I want to** select a local folder on my computer as the "Target Project"
**So that** the assistant knows which codebase to analyze and work on.
## Acceptance Criteria
* [ ] UI has an "Open Project" button.
* [ ] Clicking the button opens the native OS folder picker.
* [ ] Upon selection, the UI displays the selected path.
* [ ] The system verifies the folder exists and is readable.
* [ ] The application state persists the "Current Project" (in memory is fine for now).
## Out of Scope
* Persisting the selection across app restarts (save that for later).
* Scanning the file tree (just verify the root exists).
* Git validation (we'll assume any folder is valid for now).
@@ -0,0 +1,24 @@
---
name: Core Agent Tools (The Hands)
---
# Story: Core Agent Tools (The Hands)
## User Story
**As an** Agent
**I want to** be able to read files, list directories, search content, and execute shell commands
**So that** I can autonomously explore and modify the target project.
## Acceptance Criteria
* [ ] Rust Backend: Implement `read_file(path)` command (scoped to project).
* [ ] Rust Backend: Implement `write_file(path, content)` command (scoped to project).
* [ ] Rust Backend: Implement `list_directory(path)` command.
* [ ] Rust Backend: Implement `exec_shell(command, args)` command.
* [ ] Must enforce allowlist (git, cargo, npm, etc).
* [ ] Must run in project root.
* [ ] Rust Backend: Implement `search_files(query, globs)` using `ignore` crate.
* [ ] Frontend: Expose these as tools to the (future) LLM interface.
## Out of Scope
* The LLM Chat UI itself (connecting these to a visual chat window comes later).
* Complex git merges (simple commands only).
@@ -0,0 +1,26 @@
---
name: The Agent Brain (Ollama Integration)
---
# Story: The Agent Brain (Ollama Integration)
## User Story
**As a** User
**I want to** connect the Assistant to a local Ollama instance
**So that** I can chat with the Agent and have it execute tools without sending data to the cloud.
## Acceptance Criteria
* [ ] Backend: Implement `ModelProvider` trait/interface.
* [ ] Backend: Implement `OllamaProvider` (POST /api/chat).
* [ ] Backend: Implement `chat(message, history, provider_config)` command.
* [ ] Must support passing Tool Definitions to Ollama (if model supports it) or System Prompt instructions.
* [ ] Must parse Tool Calls from the response.
* [ ] Frontend: Settings Screen to toggle "Ollama" and set Model Name (default: `llama3`).
* [ ] Frontend: Chat Interface.
* [ ] Message History (User/Assistant).
* [ ] Tool Call visualization (e.g., "Running git status...").
## Out of Scope
* Remote Providers (Anthropic/OpenAI) - Future Story.
* Streaming responses (wait for full completion for MVP).
* Complex context window management (just send full history for now).
@@ -0,0 +1,21 @@
---
name: Ollama Model Detection
---
# Story: Ollama Model Detection
## User Story
**As a** User
**I want to** select my Ollama model from a dropdown list of installed models
**So that** I don't have to manually type (and potentially mistype) the model names.
## Acceptance Criteria
* [ ] Backend: Implement `get_ollama_models()` command.
* [ ] Call `GET /api/tags` on the Ollama instance.
* [ ] Parse the JSON response to extracting model names.
* [ ] Frontend: Replace the "Ollama Model" text input with a `<select>` dropdown.
* [ ] Frontend: Populate the dropdown on load.
* [ ] Frontend: Handle connection errors gracefully (if Ollama isn't running, show empty or error).
## Out of Scope
* Downloading new models via the UI (pulling).
@@ -0,0 +1,20 @@
---
name: Persist Project Selection
---
# Story: Persist Project Selection
## User Story
**As a** User
**I want** the application to remember the last project I opened
**So that** I don't have to re-select the directory every time I restart the app.
## Acceptance Criteria
* [ ] Backend: Use `tauri-plugin-store` (or simple JSON file) to persist `last_project_path`.
* [ ] Backend: On app startup, check if a saved path exists.
* [ ] Backend: If saved path exists and is valid, automatically load it into `SessionState`.
* [ ] Frontend: On load, check if backend has a project ready. If so, skip selection screen.
* [ ] Frontend: Add a "Close Project" button to clear the state and return to selection screen.
## Out of Scope
* Managing a list of "Recent Projects" (just the last one is fine for now).
@@ -0,0 +1,23 @@
---
name: Fix UI Responsiveness (Tech Debt)
---
# Story: Fix UI Responsiveness (Tech Debt)
## User Story
**As a** User
**I want** the UI to remain interactive and responsive while the Agent is thinking or executing tools
**So that** I don't feel like the application has crashed.
## Context
Currently, the UI locks up or becomes unresponsive during long LLM generations or tool executions. Even though the backend commands are async, the frontend experience degrades.
## Acceptance Criteria
* [ ] Investigate the root cause of the freezing (JS Main Thread blocking vs. Tauri IPC blocking).
* [ ] Implement a "Streaming" architecture for Chat if necessary (getting partial tokens instead of waiting for full response).
* *Note: This might overlap with future streaming stories, but basic responsiveness is the priority here.*
* [ ] Add visual indicators (Spinner/Progress Bar) that animate smoothly during the wait.
* [ ] Ensure the "Stop Generation" button (if added) can actually interrupt the backend task.
## Out of Scope
* Full streaming text (unless that is the only way to fix the freezing).
@@ -0,0 +1,21 @@
---
name: UI Polish - Sticky Header & Compact Layout
---
# Story: UI Polish - Sticky Header & Compact Layout
## User Story
**As a** User
**I want** key controls (Model Selection, Tool Toggle, Project Path) to be visible at all times
**So that** I don't have to scroll up to check my configuration or change settings.
## Acceptance Criteria
* [ ] Frontend: Create a fixed `<Header />` component at the top of the viewport.
* [ ] Frontend: Move "Active Project" display into this header (make it compact/truncated if long).
* [ ] Frontend: Move "Ollama Model" and "Enable Tools" controls into this header.
* [ ] Frontend: Ensure the Chat message list scrolls *under* the header (taking up remaining height).
* [ ] Frontend: Remove the redundant "Active Project" bar from the main workspace area.
## Out of Scope
* Full visual redesign (just layout fixing).
* Settings modal (keep controls inline for now).
@@ -0,0 +1,29 @@
---
name: Collapsible Tool Outputs
---
# Story: Collapsible Tool Outputs
## User Story
**As a** User
**I want** tool outputs (like long file contents or search results) to be collapsed by default
**So that** the chat history remains readable and I can focus on the Agent's reasoning.
## Acceptance Criteria
* [x] Frontend: Render tool outputs inside a `<details>` / `<summary>` component (or custom equivalent).
* [x] Frontend: Default state should be **Closed/Collapsed**.
* [x] Frontend: The summary line should show the Tool Name + minimal args (e.g., "▶ read_file(src/main.rs)").
* [x] Frontend: Clicking the arrow/summary expands to show the full output.
## Out of Scope
* Complex syntax highlighting for tool outputs (plain text/pre is fine).
## Implementation Plan
1. Create a reusable component for displaying tool outputs with collapsible functionality
2. Update the chat message rendering logic to use this component for tool outputs
3. Ensure the summary line displays tool name and minimal arguments
4. Verify that the component maintains proper styling and readability
5. Test expand/collapse functionality across different tool output types
## Related Functional Specs
* Functional Spec: Tool Outputs
@@ -0,0 +1,31 @@
---
name: Remove Unnecessary Scroll Bars
---
# Story: Remove Unnecessary Scroll Bars
## User Story
**As a** User
**I want** the UI to have clean, minimal scrolling without visible scroll bars
**So that** the interface looks polished and doesn't have distracting visual clutter.
## Acceptance Criteria
* [x] Remove or hide the vertical scroll bar on the right side of the chat area
* [x] Remove or hide any horizontal scroll bars that appear
* [x] Maintain scrolling functionality (content should still be scrollable, just without visible bars)
* [x] Consider using overlay scroll bars or auto-hiding scroll bars for better aesthetics
* [x] Ensure the solution works across different browsers (Chrome, Firefox, Safari)
* [x] Verify that long messages and tool outputs still scroll properly
## Out of Scope
* Custom scroll bar designs with fancy styling
* Touch/gesture scrolling improvements for mobile (desktop focus for now)
## Implementation Notes
* Use CSS `scrollbar-width: none` for Firefox
* Use `::-webkit-scrollbar { display: none; }` for Chrome/Safari
* Ensure `overflow: auto` or `overflow-y: scroll` is still applied to maintain scroll functionality
* Test with long tool outputs and chat histories to ensure no layout breaking
## Related Functional Specs
* Functional Spec: UI/UX
@@ -0,0 +1,22 @@
---
name: System Prompt & Persona
---
# Story: System Prompt & Persona
## User Story
**As a** User
**I want** the Agent to behave like a Senior Engineer and know exactly how to use its tools
**So that** it writes high-quality code and doesn't hallucinate capabilities or refuse to edit files.
## Acceptance Criteria
* [ ] Backend: Define a robust System Prompt constant (likely in `src-tauri/src/llm/prompts.rs`).
* [ ] Content: The prompt should define:
* Role: "Senior Software Engineer / Agent".
* Tone: Professional, direct, no fluff.
* Tool usage instructions: "You have access to the local filesystem. Use `read_file` to inspect context before editing."
* Workflow: "When asked to implement a feature, read relevant files first, then write."
* [ ] Backend: Inject this system message at the *start* of every `chat` session sent to the Provider.
## Out of Scope
* User-editable system prompts (future story).
@@ -0,0 +1,20 @@
---
name: "Test Coverage: http/context.rs to 100%"
---
# Story 100: Test Coverage: http/context.rs to 100%
## User Story
As a developer, I want http/context.rs to have 100% test coverage, so that regressions in AppContext helper methods are caught early.
## Acceptance Criteria
- [ ] server/src/http/context.rs reaches 100% line coverage (3 missing lines covered)
- [ ] cargo clippy passes with no warnings
- [ ] cargo test passes with all tests green
- [ ] No changes to production code, only test code added
## Out of Scope
- TBD
@@ -0,0 +1,20 @@
---
name: "Test Coverage: http/chat.rs to 80%"
---
# Story 101: Test Coverage: http/chat.rs to 80%
## User Story
As a developer, I want http/chat.rs to have at least 80% test coverage, so that regressions in the chat HTTP handler are caught early.
## Acceptance Criteria
- [ ] server/src/http/chat.rs reaches at least 80% line coverage (currently 0%, 5 lines total)
- [ ] cargo clippy passes with no warnings
- [ ] cargo test passes with all tests green
- [ ] No changes to production code, only test code added
## Out of Scope
- TBD
@@ -0,0 +1,20 @@
---
name: "Test Coverage: http/model.rs to 80%"
---
# Story 102: Test Coverage: http/model.rs to 80%
## User Story
As a developer, I want http/model.rs to have at least 80% test coverage, so that regressions in model preference get/set handlers are caught early.
## Acceptance Criteria
- [ ] server/src/http/model.rs reaches at least 80% line coverage (currently 0%, 22 lines missed)
- [ ] cargo clippy passes with no warnings
- [ ] cargo test passes with all tests green
- [ ] No changes to production code, only test code added
## Out of Scope
- TBD
@@ -0,0 +1,20 @@
---
name: "Test Coverage: http/project.rs to 80%"
---
# Story 103: Test Coverage: http/project.rs to 80%
## User Story
As a developer, I want http/project.rs to have at least 80% test coverage, so that regressions in project list/open handlers are caught early.
## Acceptance Criteria
- [ ] server/src/http/project.rs reaches at least 80% line coverage (currently 0%, 30 lines missed)
- [ ] cargo clippy passes with no warnings
- [ ] cargo test passes with all tests green
- [ ] No changes to production code, only test code added
## Out of Scope
- TBD
@@ -0,0 +1,20 @@
---
name: "Test Coverage: io/search.rs to 95%"
---
# Story 104: Test Coverage: io/search.rs to 95%
## User Story
As a developer, I want io/search.rs to have at least 95% test coverage, so that regressions in search edge cases are caught early.
## Acceptance Criteria
- [ ] server/src/io/search.rs reaches at least 95% line coverage (currently 89%, 14 lines missed)
- [ ] cargo clippy passes with no warnings
- [ ] cargo test passes with all tests green
- [ ] No changes to production code, only test code added
## Out of Scope
- TBD
@@ -0,0 +1,20 @@
---
name: "Test Coverage: io/shell.rs to 95%"
---
# Story 105: Test Coverage: io/shell.rs to 95%
## User Story
As a developer, I want io/shell.rs to have at least 95% test coverage, so that regressions in shell execution edge cases are caught early.
## Acceptance Criteria
- [ ] server/src/io/shell.rs reaches at least 95% line coverage (currently 84%, 15 lines missed)
- [ ] cargo clippy passes with no warnings
- [ ] cargo test passes with all tests green
- [ ] No changes to production code, only test code added
## Out of Scope
- TBD
@@ -0,0 +1,20 @@
---
name: "Test Coverage: http/settings.rs to 80%"
---
# Story 106: Test Coverage: http/settings.rs to 80%
## User Story
As a developer, I want http/settings.rs to have at least 80% test coverage, so that regressions in settings get/set handlers are caught early.
## Acceptance Criteria
- [ ] server/src/http/settings.rs reaches at least 80% line coverage (currently 59%, 35 lines missed)
- [ ] cargo clippy passes with no warnings
- [ ] cargo test passes with all tests green
- [ ] No changes to production code, only test code added
## Out of Scope
- TBD
@@ -0,0 +1,20 @@
---
name: "Test Coverage: http/assets.rs to 85%"
---
# Story 107: Test Coverage: http/assets.rs to 85%
## User Story
As a developer, I want http/assets.rs to have at least 85% test coverage, so that regressions in static asset serving are caught early.
## Acceptance Criteria
- [ ] server/src/http/assets.rs reaches at least 85% line coverage (currently 70%, 18 lines missed)
- [ ] cargo clippy passes with no warnings
- [ ] cargo test passes with all tests green
- [ ] No changes to production code, only test code added
## Out of Scope
- TBD
@@ -0,0 +1,20 @@
---
name: "Test Coverage: http/agents.rs to 70%"
---
# Story 108: Test Coverage: http/agents.rs to 70%
## User Story
As a developer, I want http/agents.rs to have at least 70% test coverage, so that regressions in REST agent status/control endpoints are caught early.
## Acceptance Criteria
- [ ] server/src/http/agents.rs reaches at least 70% line coverage (currently 38%, 155 lines missed)
- [ ] cargo clippy passes with no warnings
- [ ] cargo test passes with all tests green
- [ ] No changes to production code, only test code added
## Out of Scope
- TBD
@@ -0,0 +1,21 @@
---
name: "Add test coverage for LozengeFlyContext, SelectionScreen, and ChatHeader components"
---
# Story 109: Add test coverage for LozengeFlyContext, SelectionScreen, and ChatHeader components
## User Story
As a developer, I want better test coverage for LozengeFlyContext.tsx, SelectionScreen.tsx, and ChatHeader.tsx, so that regressions are caught early.
## Acceptance Criteria
- [ ] LozengeFlyContext.tsx reaches 100% coverage (currently 98.1%, 5 lines missing)
- [ ] SelectionScreen.tsx reaches 100% coverage (currently 93.5%, 5 lines missing)
- [ ] ChatHeader.tsx reaches 95% coverage (currently 87.7%, 25 lines missing)
- [ ] All vitest tests pass
- [ ] No production code changes are made
## Out of Scope
- TBD
@@ -0,0 +1,19 @@
---
name: Persist Model Selection
---
# Story: Persist Model Selection
## User Story
**As a** User
**I want** the application to remember which LLM model I selected
**So that** I don't have to switch from "llama3" to "deepseek" every time I launch the app.
## Acceptance Criteria
* [ ] Backend/Frontend: Use `tauri-plugin-store` to save the `selected_model` string.
* [ ] Frontend: On mount (after fetching available models), check the store.
* [ ] Frontend: If the stored model exists in the available list, select it.
* [ ] Frontend: When the user changes the dropdown, update the store.
## Out of Scope
* Persisting per-project model settings (global setting is fine for now).
@@ -0,0 +1,20 @@
---
name: "Add test coverage for api/settings.ts"
---
# Story 110: Add test coverage for api/settings.ts
## User Story
As a developer, I want better test coverage for api/settings.ts, so that regressions in the settings API wrapper are caught early.
## Acceptance Criteria
- [ ] api/settings.ts reaches 90% coverage (currently 55%, 18 lines missing)
- [ ] Tests use fetch mocks to exercise all API wrapper functions
- [ ] All vitest tests pass
- [ ] No production code changes are made
## Out of Scope
- TBD
@@ -0,0 +1,20 @@
---
name: "Add test coverage for api/agents.ts"
---
# Story 111: Add test coverage for api/agents.ts
## User Story
As a developer, I want better test coverage for api/agents.ts, so that regressions in the agent API wrapper are caught early.
## Acceptance Criteria
- [ ] api/agents.ts reaches 80% coverage (currently 29.5%, 67 lines missing)
- [ ] Tests use fetch mocks to exercise all agent API wrapper functions
- [ ] All vitest tests pass
- [ ] No production code changes are made
## Out of Scope
- TBD
@@ -0,0 +1,20 @@
---
name: "Add test coverage for App.tsx"
---
# Story 112: Add test coverage for App.tsx
## User Story
As a developer, I want better test coverage for App.tsx, so that regressions in the main application component are caught early.
## Acceptance Criteria
- [ ] App.tsx reaches 85% coverage (currently 73.1%, 43 lines missing)
- [ ] Tests cover additional integration-style scenarios for the main app component
- [ ] All vitest tests pass
- [ ] No production code changes are made
## Out of Scope
- TBD
@@ -0,0 +1,20 @@
---
name: "Add test coverage for usePathCompletion hook"
---
# Story 113: Add test coverage for usePathCompletion hook
## User Story
As a developer, I want better test coverage for the usePathCompletion hook, so that regressions in path completion behavior are caught early.
## Acceptance Criteria
- [ ] usePathCompletion.ts reaches 95% coverage (currently 81.7%, 26 lines missing)
- [ ] Tests use renderHook to exercise all hook code paths
- [ ] All vitest tests pass
- [ ] No production code changes are made
## Out of Scope
- TBD
@@ -0,0 +1,40 @@
---
name: "Web UI SSE socket stops updating after a while"
---
# Bug 114: Web UI SSE socket stops updating after a while
## Description
After the first several pipeline updates, the UI stops reflecting changes. Lozenges stop flying, stories stop moving between stages, but the server is still advancing the pipeline fine. A page refresh fixes it.
The root cause is likely not the SSE transport itself but rather the large combined pipeline state push that the frontend subscribes to. Investigate the SSE event handler in the frontend client — it receives a single big `pipeline_state` event that everything listens to. Something may be going wrong in the processing/diffing of that state after several rapid updates.
## Investigation hints
- Start with the SSE client in `frontend/src/api/client.ts` — look at `onPipelineState` handling
- Check if the SSE connection is actually dropping (add a log on close/error) or if events arrive but stop being processed
- The `LozengeFlyContext` diffing logic in `useLayoutEffect` compares prev vs current pipeline — could a stale ref or missed update break the chain?
- Server-side: the `broadcast::channel` has a 1024 buffer — if a slow consumer lags, tokio drops it silently
## How to Reproduce
1. Open the web UI
2. Start several agents working on stories
3. Wait a few minutes while agents complete and pipeline advances
4. Observe that the UI stops reflecting pipeline changes
5. Refresh the page — state is correct again
## Actual Result
UI freezes showing stale pipeline state after several updates.
## Expected Result
UI should always reflect current pipeline state in real time without needing a manual refresh.
## Acceptance Criteria
- [ ] Root cause identified (SSE transport vs frontend state processing)
- [ ] Fix implemented with auto-recovery if connection drops
- [ ] UI stays live through sustained agent activity (10+ minutes)
@@ -0,0 +1,23 @@
---
name: "Hot-reload project.toml agent config without server restart"
---
# Story 115: Hot-reload project.toml agent config without server restart
## User Story
As a developer, I want changes to `.story_kit/project.toml` to be picked up automatically by the running server, so that I can update the agent roster without restarting the server.
## Acceptance Criteria
- [ ] When `.story_kit/project.toml` is saved on disk, the server detects the change within the debounce window (300 ms) and broadcasts an `agent_config_changed` WebSocket event to all connected clients
- [ ] The frontend `AgentPanel` automatically re-fetches and displays the updated agent roster upon receiving `agent_config_changed`, without any manual action
- [ ] `project.toml` changes inside worktree directories (paths containing `worktrees/`) are NOT broadcast
- [ ] Config file changes do NOT trigger a pipeline state refresh (only work-item events do)
- [ ] A helper `is_config_file(path, git_root)` correctly identifies the root-level `.story_kit/project.toml` (returns false for worktree copies)
## Out of Scope
- Watching for newly created `project.toml` (only file modification events)
- Validating the new config before broadcasting (parse errors are surfaced on next `get_agent_config` call)
- Reloading config into in-memory agent state (agents already read config from disk on each start)
@@ -0,0 +1,43 @@
---
name: "Init command scaffolds deterministic project structure"
---
# Story 116: Init command scaffolds deterministic project structure
## User Story
As a new Story Kit user, I want to point at a directory and have the `.story_kit/` workflow structure scaffolded automatically, so that I have a working pipeline without manual configuration.
## Context
Currently `scaffold_story_kit()` in `server/src/io/fs.rs`:
- Creates the old `stories/archive/` structure instead of the `work/` pipeline dirs
- Writes `00_CONTEXT.md` and `STACK.md` with content that describes Story Kit itself, not a blank template for the user's project
- Does not create `project.toml` (agent config)
- Does not create `.mcp.json` (MCP endpoint registration)
- Does not run `git init`
- The embedded `STORY_KIT_README` constant is a stale copy that diverges from the actual `.story_kit/README.md` checked into this repo
## Acceptance Criteria
- [ ] Creates the `work/` pipeline: `work/1_upcoming/`, `work/2_current/`, `work/3_qa/`, `work/4_merge/`, `work/5_archived/` — each with a `.gitkeep` file so empty dirs survive git clone
- [ ] Removes creation of the old `stories/` and `stories/archive/` directories
- [ ] Creates `specs/`, `specs/tech/`, `specs/functional/` (unchanged)
- [ ] Creates `script/test` with the existing stub (unchanged)
- [ ] Writes `.story_kit/README.md` using `include_str!` to embed the canonical README.md at build time (replacing the stale `STORY_KIT_README` constant)
- [ ] Writes `.story_kit/project.toml` with a sensible default agent config (one coder agent, one qa agent, one mergemaster — using `sonnet` model aliases)
- [ ] Writes `.mcp.json` in the project root with the default port (reuse `write_mcp_json` from `worktree.rs`)
- [ ] Writes `specs/00_CONTEXT.md` as a blank template with section headings (High-Level Goal, Core Features, Domain Definition, Glossary) and placeholder instructions — NOT content about Story Kit itself
- [ ] Writes `specs/tech/STACK.md` as a blank template with section headings (Core Stack, Coding Standards, Quality Gates, Libraries) and placeholder instructions — NOT content about Story Kit itself
- [ ] Runs `git init` if the directory is not already a git repo
- [ ] Makes an initial commit with the scaffolded files (only on fresh `git init`, not into an existing repo)
- [ ] Unit tests for `scaffold_story_kit()` that run against a temp directory and verify: all expected directories exist, all expected files exist with correct content, `.gitkeep` files are present in work dirs, template specs contain placeholder headings (not Story Kit content), `project.toml` has valid default agent config, `.mcp.json` is valid JSON with correct endpoint
- [ ] Test that scaffold is idempotent — running it twice on the same directory doesn't overwrite or duplicate files
- [ ] Test that scaffold into an existing git repo does not run `git init` or create an initial commit
## Out of Scope
- Interactive onboarding (guided conversation to populate specs) — see Story 139
- Generating actual application code or project boilerplate (e.g. `cargo init`, `create-react-app`) — Story Kit is stack-agnostic, it only scaffolds the `.story_kit/` workflow layer
- Template galleries or presets for common stacks (future enhancement)
- Migrating existing projects that already have a `.story_kit/` directory
@@ -0,0 +1,22 @@
---
name: "Show startup reconciliation progress in UI"
---
# Story 117: Show startup reconciliation progress in UI
## User Story
As a developer using Story Kit, I want to see what's happening during server startup reconciliation in the UI, so that I can understand why stories are moving between pipeline stages automatically.
## Acceptance Criteria
- [ ] The server emits `reconciliation_progress` WebSocket events during `reconcile_on_startup` with a `story_id`, `status`, and `message` for each story being processed
- [ ] The server emits a final `reconciliation_progress` event with `status: "done"` when reconciliation completes
- [ ] The frontend displays an in-progress indicator (e.g. a banner) while reconciliation is active, showing recent events
- [ ] The reconciliation banner dismisses itself when the `done` event is received
- [ ] Existing tests continue to pass
## Out of Scope
- Persisting reconciliation history across sessions
- Showing reconciliation progress for `auto_assign_available_work`
@@ -0,0 +1,90 @@
---
name: "Agent pool retains stale running state after completion, blocking auto-assign"
---
# Bug 118: Agent pool retains stale running state after completion, blocking auto-assign
## Description
When an agent (QA, mergemaster) completes its work and the story advances in the pipeline, the agent pool still reports the agent as running on the old story. This blocks auto-assign from picking up new work in the queue.
This is different from bug 94 (stale state after restart). This happens during normal operation within a single server session.
## How to Reproduce
1. Have mergemaster complete a merge (e.g. story 106)
2. Story moves to archived
3. New items arrive in 4_merge/ (e.g. 107, 108, 109)
4. Try to start mergemaster on a new story
5. Server responds: Agent mergemaster is already running on story 106
## Actual Result
Agent pool reports mergemaster as running on the completed/archived story. Auto-assign skips the merge queue. Manual stop of the stale entry is required before the agent can be reassigned.
## Expected Result
When an agent process exits and the story advances, the agent pool should clear the running state so auto-assign can immediately dispatch the agent to the next queued item.
## Root Cause Analysis
The bug is in `server/src/agents.rs`, in the `start_agent` method.
### The Leak
In `start_agent` (line ~177), a `Pending` entry is inserted into the in-memory `HashMap<String, StoryAgent>` at line ~263:
```rust
{
let mut agents = self.agents.lock().map_err(|e| e.to_string())?;
agents.insert(
key.clone(),
StoryAgent {
agent_name: resolved_name.clone(),
status: AgentStatus::Pending,
// ...
},
);
}
```
Then at line ~290, `create_worktree` is called:
```rust
let wt_info = worktree::create_worktree(project_root, story_id, &config, self.port).await?;
```
**If `create_worktree` fails** (e.g. `pnpm run build` error during worktree setup), the function returns `Err` but **never removes the Pending entry** from the HashMap.
### The Blocking Effect
`find_free_agent_for_stage` (line ~1418) considers an agent "busy" if any HashMap entry has `Running | Pending` status:
```rust
let is_busy = agents.values().any(|a| {
a.agent_name == agent_config.name
&& matches!(a.status, AgentStatus::Running | AgentStatus::Pending)
});
```
The leaked Pending entry permanently blocks this agent from being auto-assigned until someone manually stops the stale entry via the API.
### Scope
This affects **all agent types** (coders, QA, mergemaster) equally — anywhere `start_agent` is called and the subsequent worktree creation or process spawn can fail. Anywhere there's a gate that can fail after the Pending entry is inserted, the leak can happen.
The code currently enforces gates but doesn't clean up if a gate fails — the Pending entry just stays in the HashMap forever.
### Fix Strategy
Add cleanup logic: if any step after the Pending insertion fails, remove the entry from the HashMap before returning the error. A guard/RAII pattern or explicit cleanup in the error path would both work. The key is that `start_agent` must be atomic — either the agent is fully started, or no trace of it remains in the pool.
Also audit other code paths that insert entries into the agents HashMap to ensure they all have proper cleanup on failure.
## Acceptance Criteria
- [ ] `start_agent` cleans up the Pending entry from the HashMap if `create_worktree` or any subsequent step fails
- [ ] No leaked Pending/Running entries remain after a failed agent start
- [ ] Automated test covers the failure case: simulate `create_worktree` failure and verify the agent pool is clean afterward
- [ ] All agent types (coder, QA, mergemaster) benefit from the fix
- [ ] Bug is fixed and verified with `cargo test` and `cargo clippy`
@@ -0,0 +1,56 @@
---
name: "Mergemaster should resolve merge conflicts instead of leaving conflict markers on master"
---
# Story 119: Mergemaster should resolve merge conflicts instead of leaving conflict markers on master
## Problem
When mergemaster squash-merges a feature branch that conflicts with current master, conflict markers end up committed to master. This breaks the frontend build and requires manual intervention.
## Root Cause
There is a race condition between `run_squash_merge` and the file watcher:
1. `git merge --squash` runs on the main working tree
2. The squash brings `.story_kit/work/` files from the feature branch (e.g. story moved to `2_current`)
3. The watcher detects these file changes and auto-commits — including any conflict markers in frontend/server files
4. `run_squash_merge` checks the exit status and aborts, but the watcher already committed the broken state
The merge tool itself does the right thing (aborts on conflicts at `agents.rs:2157-2171`), but the watcher races it.
## Proposed Solution: Merge-Queue Branch
1. Create a `merge-queue` branch that always tracks master
2. Mergemaster performs squash-merges on `merge-queue` instead of master
3. If the merge is clean and gates pass, fast-forward master to merge-queue
4. If conflicts occur, the watcher does not care (it only watches the main worktree)
5. Mergemaster can resolve conflicts on the merge-queue branch without affecting master
6. If resolution fails, reset merge-queue to master and report the conflict
## Also Required: Pause Watcher During Merges
Add a lock/pause mechanism to the watcher that `merge_agent_work` acquires before running `git merge --squash`. The watcher skips auto-commits while the lock is held. This is a belt-and-suspenders defense — even with the merge-queue branch, we want the watcher to not interfere with merge operations.
**Implement both approaches** — the merge-queue branch for isolation, and the watcher pause as a safety net.
## Also Update Mergemaster Prompt
- Remove the instruction to NOT resolve conflicts
- Instead instruct mergemaster to resolve simple conflicts (e.g. both branches adding code at same location)
- For complex conflicts (semantic changes to the same logic), still report to human
## Key Files
- `server/src/agents.rs``run_squash_merge` (lines 2136-2199), `merge_agent_work` (lines 992-1066)
- `server/src/http/mcp.rs``tool_merge_agent_work` (lines 1392-1425)
- `server/src/io/watcher.rs` — file watcher that races with the merge
- `.story_kit/project.toml` — mergemaster prompt (lines 210-232)
## Acceptance Criteria
- [ ] Merge conflicts never leave conflict markers on master
- [ ] Mergemaster resolves simple additive conflicts automatically
- [ ] Complex conflicts are reported clearly without breaking master
- [ ] Frontend build stays clean throughout the merge process
- [ ] Existing tests pass
@@ -0,0 +1,44 @@
---
name: Left-Align Chat Text and Add Syntax Highlighting
---
# Story: Left-Align Chat Text and Add Syntax Highlighting
## User Story
**As a** User
**I want** chat messages and code to be left-aligned instead of centered, with proper syntax highlighting for code blocks
**So that** the text is more readable, follows standard chat UI conventions, and code is easier to understand.
## Acceptance Criteria
* [x] User messages should be right-aligned (standard chat pattern)
* [x] Assistant messages should be left-aligned
* [x] Tool outputs should be left-aligned
* [x] Code blocks and monospace text should be left-aligned
* [x] Remove any center-alignment styling from the chat container
* [x] Maintain the current max-width constraint for readability
* [x] Ensure proper spacing and padding for visual hierarchy
* [x] Add syntax highlighting for code blocks in assistant messages
* [x] Support common languages: JavaScript, TypeScript, Rust, Python, JSON, Markdown, Shell, etc.
* [x] Syntax highlighting should work with the dark theme
## Out of Scope
* Redesigning the entire chat layout
* Adding avatars or profile pictures
* Changing the overall color scheme or theme (syntax highlighting colors should complement existing dark theme)
* Custom themes for syntax highlighting
## Implementation Notes
* Check `Chat.tsx` for any `textAlign: "center"` styles
* Check `App.css` for any center-alignment rules affecting the chat
* User messages should align to the right with appropriate styling
* Assistant and tool messages should align to the left
* Code blocks should always be left-aligned for readability
* For syntax highlighting, consider using:
* `react-syntax-highlighter` (works with react-markdown)
* Or `prism-react-renderer` for lighter bundle size
* Or integrate with `rehype-highlight` plugin for react-markdown
* Use a dark theme preset like `oneDark`, `vsDark`, or `dracula`
* Syntax highlighting should be applied to markdown code blocks automatically
## Related Functional Specs
* Functional Spec: UI/UX
@@ -0,0 +1,27 @@
---
name: "Add test coverage for llm/chat.rs (2.6% -> 60%+)"
---
# Story 120: Add test coverage for llm/chat.rs
Currently at 2.6% line coverage (343 lines, 334 missed). This is the chat completion orchestration layer — the biggest uncovered module by missed line count.
## What to test
- Message construction and formatting
- Token counting/estimation logic
- Chat session management
- Error handling paths (provider errors, timeout, malformed responses)
- Any pure functions that don't require a live LLM connection
## Notes
- Mock the LLM provider trait/interface rather than making real API calls
- Focus on the logic layer, not the provider integration
- Target 60%+ line coverage
## Acceptance Criteria
- [ ] Line coverage for `llm/chat.rs` reaches 60%+
- [ ] Tests pass with `cargo test`
- [ ] `cargo clippy` clean
@@ -0,0 +1,27 @@
---
name: "Add test coverage for io/watcher.rs (40% -> 70%+)"
---
# Story 121: Add test coverage for io/watcher.rs
Currently at 40% line coverage (238 lines, 142 missed). The file watcher is critical infrastructure — it drives pipeline advancement and auto-commits.
## What to test
- Story file detection and classification (which directory, what kind of move)
- Debounce/flush logic
- Git add/commit message generation
- Watcher pause/resume mechanism (added in story 119 for merge safety)
- Edge cases: rapid file changes, missing directories, git failures
## Notes
- Use temp directories for filesystem tests
- Mock git commands where needed
- The watcher pause lock is especially important to test given its role in merge safety
## Acceptance Criteria
- [ ] Line coverage for `io/watcher.rs` reaches 70%+
- [ ] Tests pass with `cargo test`
- [ ] `cargo clippy` clean
@@ -0,0 +1,27 @@
---
name: "Add test coverage for http/ws.rs (0% -> 50%+)"
---
# Story 122: Add test coverage for http/ws.rs
Currently at 0% line coverage (160 lines). This is the WebSocket handler that powers the real-time UI — pipeline state pushes, chat streaming, permission requests, and reconciliation progress.
## What to test
- WebSocket message parsing (incoming WsRequest variants)
- Pipeline state serialization to WsResponse
- Message routing (chat, cancel, permission_response)
- Connection lifecycle (open, close, reconnect handling server-side)
- Broadcast channel subscription and message delivery
## Notes
- May need to set up a test server context or mock the broadcast channel
- Focus on the message handling logic rather than actual WebSocket transport
- Test the serialization/deserialization of all WsResponse variants
## Acceptance Criteria
- [ ] Line coverage for `http/ws.rs` reaches 50%+
- [ ] Tests pass with `cargo test`
- [ ] `cargo clippy` clean
@@ -0,0 +1,27 @@
---
name: "Add test coverage for llm/providers/anthropic.rs (0% -> 50%+)"
---
# Story 123: Add test coverage for llm/providers/anthropic.rs
Currently at 0% line coverage (204 lines). The Anthropic provider handles API communication for Claude models.
## What to test
- Request construction (headers, body format, model selection)
- Response parsing (streaming chunks, tool use responses, error responses)
- API key validation
- Rate limit / error handling
- Message format conversion (internal Message -> Anthropic API format)
## Notes
- Mock HTTP responses rather than calling the real Anthropic API
- Use `mockito` or similar for HTTP mocking, or test the pure functions directly
- Focus on serialization/deserialization and error paths
## Acceptance Criteria
- [ ] Line coverage for `llm/providers/anthropic.rs` reaches 50%+
- [ ] Tests pass with `cargo test`
- [ ] `cargo clippy` clean
@@ -0,0 +1,28 @@
---
name: "Add test coverage for llm/providers/claude_code.rs (54% -> 75%+)"
---
# Story 124: Add test coverage for llm/providers/claude_code.rs
Currently at 54% line coverage (496 lines, 259 missed). The Claude Code provider spawns `claude` CLI processes and manages their I/O.
## What to test
- Command argument construction (model, max-turns, budget, system prompt, append flags)
- Output parsing (streaming JSON events from claude CLI)
- Session ID extraction
- Process lifecycle management
- Error handling (process crash, invalid output, timeout)
- Permission request/response flow
## Notes
- Mock the process spawning rather than running real `claude` commands
- Test the output parsing logic with sample JSON event streams
- The argument construction logic is especially testable as pure functions
## Acceptance Criteria
- [ ] Line coverage for `llm/providers/claude_code.rs` reaches 75%+
- [ ] Tests pass with `cargo test`
- [ ] `cargo clippy` clean
@@ -0,0 +1,26 @@
---
name: "Add test coverage for http/io.rs (0% -> 60%+)"
---
# Story 125: Add test coverage for http/io.rs
Currently at 0% line coverage (76 lines). These are the IO-related HTTP endpoints (absolute path listing, directory creation, home directory).
## What to test
- `list_directory_absolute` endpoint — valid path, invalid path, permission errors
- `create_directory_absolute` endpoint — new dir, existing dir, nested creation
- `get_home_directory` endpoint — returns correct home path
- Error responses for invalid inputs
## Notes
- Use temp directories for filesystem tests
- These are straightforward CRUD-style endpoints, should be quick to cover
- Follow the test patterns used in `http/project.rs` and `http/settings.rs`
## Acceptance Criteria
- [ ] Line coverage for `http/io.rs` reaches 60%+
- [ ] Tests pass with `cargo test`
- [ ] `cargo clippy` clean
@@ -0,0 +1,26 @@
---
name: "Add test coverage for http/anthropic.rs (0% -> 60%+)"
---
# Story 126: Add test coverage for http/anthropic.rs
Currently at 0% line coverage (66 lines). These are the Anthropic-related HTTP endpoints (key exists check, models list, set API key).
## What to test
- `get_anthropic_api_key_exists` — returns true/false based on stored key
- `get_anthropic_models` — returns model list
- `set_anthropic_api_key` — stores key, validates format
- Error handling for missing/invalid keys
## Notes
- Follow the test patterns in `http/settings.rs` and `http/model.rs`
- Small file, should be quick to get good coverage
- Mock any external API calls
## Acceptance Criteria
- [ ] Line coverage for `http/anthropic.rs` reaches 60%+
- [ ] Tests pass with `cargo test`
- [ ] `cargo clippy` clean
@@ -0,0 +1,27 @@
---
name: "Add test coverage for http/mod.rs (39% -> 70%+)"
---
# Story 127: Add test coverage for http/mod.rs
Currently at 39% line coverage (77 lines, 47 missed). This is the HTTP route setup and server initialization module.
## What to test
- Route registration (all expected paths are mounted)
- CORS configuration
- Static asset serving setup
- Server builder configuration
- Any middleware setup
## Notes
- May need integration-style tests that start a test server and verify routes exist
- Or test the route builder functions in isolation
- Follow patterns from existing HTTP module tests
## Acceptance Criteria
- [ ] Line coverage for `http/mod.rs` reaches 70%+
- [ ] Tests pass with `cargo test`
- [ ] `cargo clippy` clean
@@ -0,0 +1,28 @@
---
name: "Add test coverage for worktree.rs (65% -> 80%+)"
---
# Story 128: Add test coverage for worktree.rs
Currently at 65% line coverage (330 lines, 124 missed). Worktree management is core infrastructure — creating, removing, and managing git worktrees for agent isolation.
## What to test
- `worktree_path` construction
- `create_worktree` — branch naming, git worktree add, setup command execution
- `remove_worktree_by_story_id` — cleanup, branch deletion
- Setup command runner (pnpm install, pnpm build, cargo check)
- Error paths: git failures, setup failures, missing directories
- Edge cases: worktree already exists, branch already exists
## Notes
- Use temp git repos for integration tests
- Mock expensive operations (pnpm install, cargo check) where possible
- The setup command failure path is especially important (this was the root cause of bug 118)
## Acceptance Criteria
- [ ] Line coverage for `worktree.rs` reaches 80%+
- [ ] Tests pass with `cargo test`
- [ ] `cargo clippy` clean
@@ -0,0 +1,29 @@
---
name: "Add test coverage for http/mcp.rs (72% -> 85%+)"
---
# Story 129: Add test coverage for http/mcp.rs
Currently at 72% line coverage (1826 lines, 475 missed). This is the MCP tool server — the largest module and the interface agents use to interact with the system.
## What to test
- Uncovered MCP tool handlers (check which tools lack test coverage)
- Tool argument validation and error messages
- Edge cases in existing tool handlers
- The merge-queue and watcher-pause logic (added in story 119)
- `resolve_simple_conflicts` edge cases
- Tool dispatch routing
## Notes
- This is a large file — focus on the uncovered handlers rather than trying to test everything
- Run `cargo llvm-cov --html` to identify specific uncovered lines/functions
- The merge-related tools are the most critical gaps given recent changes
- 475 missed lines is a lot — even covering half would be a big win
## Acceptance Criteria
- [ ] Line coverage for `http/mcp.rs` reaches 85%+
- [ ] Tests pass with `cargo test`
- [ ] `cargo clippy` clean
@@ -0,0 +1,121 @@
---
name: Be Able to Use Claude
---
# Story 12: Be Able to Use Claude
## User Story
As a user, I want to be able to select Claude (via Anthropic API) as my LLM provider so I can use Claude models instead of only local Ollama models.
## Acceptance Criteria
- [x] Claude models appear in the unified model dropdown (same dropdown as Ollama models)
- [x] Dropdown is organized with section headers: "Anthropic" and "Ollama" with models listed under each
- [x] When user first selects a Claude model, a dialog prompts for Anthropic API key
- [x] API key is stored securely (using Tauri store plugin for reliable cross-platform storage)
- [x] Provider is auto-detected from model name (starts with `claude-` = Anthropic, otherwise = Ollama)
- [x] Chat requests route to Anthropic API when Claude model is selected
- [x] Streaming responses work with Claude (token-by-token display)
- [x] Tool calling works with Claude (using Anthropic's tool format)
- [x] Context window calculation accounts for Claude models (200k tokens)
- [x] User's model selection persists between sessions
- [x] Clear error messages if API key is missing or invalid
## Out of Scope
- Support for other providers (OpenAI, Google, etc.) - can be added later
- API key management UI (rotation, multiple keys, view/edit key after initial entry)
- Cost tracking or usage monitoring
- Model fine-tuning or custom models
- Switching models mid-conversation (user can start new session)
- Fetching available Claude models from API (hardcoded list is fine)
## Technical Notes
- Anthropic API endpoint: `https://api.anthropic.com/v1/messages`
- API key should be stored securely (environment variable or secure storage)
- Claude models support tool use (function calling)
- Context windows: claude-3-5-sonnet (200k), claude-3-5-haiku (200k)
- Streaming uses Server-Sent Events (SSE)
- Tool format differs from OpenAI/Ollama - needs conversion
## Design Considerations
- Single unified model dropdown with section headers ("Anthropic", "Ollama")
- Use `<optgroup>` in HTML select for visual grouping
- API key dialog appears on-demand (first use of Claude model)
- Store API key in OS keychain using `keyring` crate (cross-platform)
- Backend auto-detects provider from model name pattern
- Handle API key in backend only (don't expose to frontend logs)
- Alphabetical sorting within each provider section
## Implementation Approach
### Backend (Rust)
1. Add `anthropic` feature/module for Claude API client
2. Create `AnthropicClient` with streaming support
3. Convert tool definitions to Anthropic format
4. Handle Anthropic streaming response format
5. Add API key storage (encrypted or environment variable)
### Frontend (TypeScript)
1. Add hardcoded list of Claude models (claude-3-5-sonnet-20241022, claude-3-5-haiku-20241022)
2. Merge Ollama and Claude models into single dropdown with `<optgroup>` sections
3. Create API key input dialog/modal component
4. Trigger API key dialog when Claude model selected and no key stored
5. Add Tauri command to check if API key exists in keychain
6. Add Tauri command to set API key in keychain
7. Update context window calculations for Claude models (200k tokens)
### API Differences
- Anthropic uses `messages` array format (similar to OpenAI)
- Tools are called `tools` with different schema
- Streaming events have different structure
- Need to map our tool format to Anthropic's format
## Security Considerations
- API key stored in OS keychain (not in files or environment variables)
- Use `keyring` crate for cross-platform secure storage
- Never log API key in console or files
- Backend validates API key format before making requests
- Handle API errors gracefully (rate limits, invalid key, network errors)
- API key only accessible to the app process
## UI Flow
1. User opens model dropdown → sees "Anthropic" section with Claude models, "Ollama" section with local models
2. User selects `claude-3-5-sonnet-20241022`
3. Backend checks Tauri store for saved API key
4. If not found → Frontend shows dialog: "Enter your Anthropic API key"
5. User enters key → Backend stores in Tauri store (persistent JSON file)
6. Chat proceeds with Anthropic API
7. Future sessions: API key auto-loaded from store (no prompt)
## Implementation Notes (Completed)
### Storage Solution
Initially attempted to use the `keyring` crate for OS keychain integration, but encountered issues in macOS development mode:
- Unsigned Tauri apps in dev mode cannot reliably access the system keychain
- The `keyring` crate reported successful saves but keys were not persisting
- No macOS keychain permission dialogs appeared
**Solution:** Switched to Tauri's `store` plugin (`tauri-plugin-store`)
- Provides reliable cross-platform persistent storage
- Stores data in a JSON file managed by Tauri
- Works consistently in both development and production builds
- Simpler implementation without platform-specific entitlements
### Key Files Modified
- `src-tauri/src/commands/chat.rs`: API key storage/retrieval using Tauri store
- `src/components/Chat.tsx`: API key dialog and flow with pending message preservation
- `src-tauri/Cargo.toml`: Removed `keyring` dependency, kept `tauri-plugin-store`
- `src-tauri/src/llm/anthropic.rs`: Anthropic API client with streaming support
### Frontend Implementation
- Added `pendingMessageRef` to preserve user's message when API key dialog is shown
- Modified `sendMessage()` to accept optional message parameter for retry scenarios
- API key dialog appears on first Claude model usage
- After saving key, automatically retries sending the pending message
### Backend Implementation
- `get_anthropic_api_key_exists()`: Checks if API key exists in store
- `set_anthropic_api_key()`: Saves API key to store with verification
- `get_anthropic_api_key()`: Retrieves API key for Anthropic API calls
- Provider auto-detection based on `claude-` model name prefix
- Tool format conversion from internal format to Anthropic's schema
- SSE streaming implementation for real-time token display
@@ -0,0 +1,32 @@
---
name: "Permission approval returns wrong format — tools fail after user approves"
---
# Bug 130: Permission approval returns wrong format — tools fail after user approves
## Description
The `prompt_permission` MCP tool returns plain text ("Permission granted for '...'") but Claude Code's `--permission-prompt-tool` expects a JSON object with a `behavior` field. After the user approves a permission request in the web UI dialog, every tool call fails with a Zod validation error: `"expected object, received null"`.
## How to Reproduce
1. Start the huskies server and open the web UI
2. Chat with the claude-code-pty model
3. Ask it to do something that requires a tool NOT in `.claude/settings.json` allow list (e.g. `wc -l /etc/hosts`, or WebFetch to a non-allowed domain)
4. The permission dialog appears — click Approve
5. Observe the tool call fails with: `[{"code":"invalid_union","errors":[[{"expected":"object","code":"invalid_type","path":[],"message":"Invalid input: expected object, received null"}]],"path":[],"message":"Invalid input"}]`
## Actual Result
After approval, the tool fails with a Zod validation error. Claude Code cannot parse the plain-text response as a permission decision.
## Expected Result
After approval, the tool executes successfully. The MCP tool should return JSON that Claude Code understands: `{"behavior": "allow"}` for approval or `{"behavior": "deny", "message": "..."}` for denial.
## Acceptance Criteria
- [ ] prompt_permission returns `{"behavior": "allow"}` JSON when user approves
- [ ] prompt_permission returns `{"behavior": "deny"}` JSON when user denies
- [ ] After approving a permission request, the tool executes successfully and returns its result
- [ ] After denying a permission request, the tool is skipped gracefully
@@ -0,0 +1,47 @@
---
name: "get_agent_output stream always times out for running agents"
---
# Bug 131: get_agent_output stream always times out for running agents
## Description
The `get_agent_output` MCP tool consistently returns "Stream timed out; call again to continue" even when the agent process is actively running, making API calls, and committing work. The `list_agents` call shows the agent as `running` with `session_id: null` throughout its entire execution, only populating the session_id after the process exits. This makes it impossible to observe agent progress in real time via MCP.
## How to Reproduce
1. Start an agent on a story (e.g. `start_agent` with `coder-1`)
2. Confirm the claude process is running (`ps aux | grep claude`)
3. Call `get_agent_output` with the story_id and agent_name
4. Observe it returns "Stream timed out" every time, regardless of timeout_ms value (tested up to 10000ms)
5. `list_agents` shows `session_id: null` throughout
6. Agent completes its work and commits without ever producing observable output
## Actual Result
`get_agent_output` never returns any events. `session_id` stays null while the agent is running. The only way to observe progress is to poll the worktree's git log directly.
## Expected Result
`get_agent_output` streams back text tokens and status events from the running agent in real time. `session_id` is populated once the agent's first streaming event arrives.
## Reopened — Previous Fix Did Not Work
This was archived after a coder pass but the bug is still present. With 3 agents actively running:
- `get_agent_output` returned 141 events on one call, then 0 events on the next call with a 5s timeout
- None of the events contained text output — only metadata/status events
- The server logs (`get_server_logs`) DO show agent activity (spawn commands, MCP calls), so the agents are working — the output just isn't being captured/forwarded
### Investigation needed
The coder needs to trace the full data path:
1. How does `run_agent_pty_streaming` (server/src/agents.rs) capture PTY output from the claude process?
2. How are those events published to the broadcast channel that `get_agent_output` subscribes to?
3. Is the broadcast channel being created before the agent starts producing output, or is there a race where early events are lost?
4. Are text tokens from the PTY being sent as `AgentEvent` variants that `get_agent_output` actually serializes, or are they filtered out?
## Acceptance Criteria
- [ ] get_agent_output returns streaming text events while an agent is actively running
- [ ] session_id is populated in list_agents shortly after agent spawn
- [ ] Calling get_agent_output multiple times yields incremental output from the agent
@@ -0,0 +1,48 @@
---
name: "Fix TOCTOU race in agent check-and-insert"
---
# Story 132: Fix TOCTOU race in agent check-and-insert
## User Story
As a user running multiple agents, I want the agent pool to correctly enforce single-instance-per-agent so that two agents never end up running on the same story or the same agent name running on two stories concurrently.
## Acceptance Criteria
- [ ] The lock in start_agent (server/src/agents.rs ~lines 262-324) is held continuously from the availability check through the HashMap insert — no lock release between check and insert
- [ ] The lock in auto_assign_available_work (server/src/agents.rs ~lines 1196-1228) is held from find_free_agent_for_stage through the start_agent call, preventing a concurrent auto_assign from selecting the same agent
- [ ] A test demonstrates that concurrent start_agent calls for the same agent name on different stories result in exactly one running agent and one rejection
- [ ] A test demonstrates that concurrent auto_assign_available_work calls do not produce duplicate assignments
## Analysis
### Race 1: start_agent check-then-insert (server/src/agents.rs)
The single-instance check at ~lines 262-296 acquires the mutex, checks for duplicate agents, then **releases the lock**. The HashMap insert happens later at ~line 324 after **re-acquiring the lock**. Between release and reacquire, a concurrent call can pass the same check:
```
Thread A: lock → check coder-1 available? YES → unlock
Thread B: lock → check coder-1 available? YES → unlock → lock → insert "86:coder-1"
Thread A: lock → insert "130:coder-1"
Result: both coder-1 entries exist, two processes spawned
```
The composite key at ~line 27 is `format!("{story_id}:{agent_name}")`, so `86:coder-1` and `130:coder-1` are different keys. The name-only check at ~lines 277-295 iterates the HashMap looking for a Running/Pending agent with the same name — but both threads read the HashMap before either has inserted, so both pass.
**Fix**: Hold the lock from the check (~line 264) through the insert (~line 324). This means the worktree setup and process spawn (~lines 297-322) must either happen inside the lock (blocking other callers) or the entry must be inserted as `Pending` before releasing the lock, with the process spawn happening after.
### Race 2: auto_assign_available_work (server/src/agents.rs)
At ~lines 1196-1215, the function locks the mutex, calls `find_free_agent_for_stage` to pick an available agent name, then **releases the lock**. It then calls `start_agent` at ~line 1228, which re-acquires the lock. Two concurrent `auto_assign` calls can both select the same free agent for different stories (or the same story) in this window.
**Fix**: Either hold the lock across the full loop iteration, or restructure so that `start_agent` receives a reservation/guard rather than just an agent name string.
### Observed symptoms
- Both `coder-1` and `coder-2` showing as "running" on the same story
- `coder-1` appearing on story 86 immediately after completing on bug 130, due to pipeline advancement calling `auto_assign_available_work` concurrently with other state transitions
## Out of Scope
- TBD
@@ -0,0 +1,21 @@
---
name: "Clean up agent state on story archive and add TTL for completed entries"
---
# Story 133: Clean up agent state on story archive and add TTL for completed entries
## User Story
As a user, I want completed and archived agent entries to be cleaned up automatically so that the agent pool reflects reality and stale entries do not accumulate or confuse the UI.
## Acceptance Criteria
- [ ] When a story is archived (move_story_to_archived), all agent entries for that story_id are removed from the HashMap
- [ ] Completed and Failed agent entries are automatically removed after a configurable TTL (default 1 hour)
- [ ] list_agents never returns agents for archived stories, even without the filesystem filter fallback
- [ ] A test demonstrates that archiving a story removes its agent entries from the pool
- [ ] A test demonstrates that completed entries are reaped after TTL expiry
## Out of Scope
- TBD
@@ -0,0 +1,21 @@
---
name: "Add process health monitoring and timeout to agent PTY sessions"
---
# Story 134: Add process health monitoring and timeout to agent PTY sessions
## User Story
As a user, I want hung or unresponsive agent processes to be detected and cleaned up automatically so that the system recovers without manual intervention.
## Acceptance Criteria
- [ ] The PTY read loop has a configurable inactivity timeout (default 5 minutes) — if no output is received within the timeout, the process is killed and the agent status set to Failed
- [ ] A background watchdog task periodically checks that Running agents still have a live process, and marks orphaned entries as Failed
- [ ] When an agent process is killed externally (e.g. SIGKILL), the agent status transitions to Failed within the timeout period rather than hanging indefinitely
- [ ] A test demonstrates that a hung agent (no PTY output) is killed and marked Failed after the timeout
- [ ] A test demonstrates that an externally killed agent is detected and cleaned up by the watchdog
## Out of Scope
- TBD
@@ -0,0 +1,22 @@
---
name: "Update mergemaster prompt to allow conflict resolution and code fixes"
---
# Story 135: Update mergemaster prompt to allow conflict resolution and code fixes
## User Story
As a user, I want the mergemaster agent to be able to resolve simple conflicts and fix minor gate failures itself, instead of being told to never write code and looping infinitely on failures.
## Acceptance Criteria
- [ ] The mergemaster prompt in project.toml no longer says "Do NOT implement code yourself" or "Do not write code"
- [ ] The mergemaster prompt instructs the agent to resolve simple additive conflicts (both branches adding code at the same location) automatically
- [ ] The mergemaster prompt instructs the agent to attempt minor fixes when quality gates fail (e.g. syntax errors, missing semicolons) rather than just reporting and looping
- [ ] For complex conflicts or non-trivial gate failures, the mergemaster prompt instructs the agent to report clearly to the human rather than attempting a fix
- [ ] The system_prompt field is updated to match the new prompt behaviour
- [ ] The mergemaster prompt includes a max retry limit instruction — if gates fail after 2 fix attempts, stop and report to the human instead of retrying
## Out of Scope
- TBD
@@ -0,0 +1,29 @@
---
name: "Broadcast channel silently drops events on subscriber lag"
---
# Bug 136: Broadcast channel silently drops events on subscriber lag
## Description
The watcher broadcast channel (capacity 1024) silently drops events when a subscriber lags behind. In the WebSocket handler, the `Lagged` error is caught and handled with a bare `continue`, meaning the frontend never receives those state updates and falls out of sync.
## How to Reproduce
1. Open the web UI
2. Start agents that generate pipeline state changes
3. If the WebSocket consumer is momentarily slow (e.g., blocked on send), the broadcast subscriber falls behind
4. Lagged events are silently skipped
## Actual Result
Events are silently dropped with `continue` on `RecvError::Lagged`. The frontend misses state transitions and shows stale data.
## Expected Result
When a lag occurs, the system should recover by re-sending the full current pipeline state so the frontend catches up, rather than silently dropping events.
## Acceptance Criteria
- [ ] Lagged broadcast events trigger a full state resync to the affected subscriber
- [ ] No silent event drops — lag events are logged as warnings
@@ -0,0 +1,29 @@
---
name: "LozengeFlyContext animation queue race condition on rapid updates"
---
# Bug 137: LozengeFlyContext animation queue race condition on rapid updates
## Description
In LozengeFlyContext.tsx, the useEffect that executes animations clears pending action refs at the start of each run. When rapid pipeline updates arrive, useLayoutEffect queues actions into refs, but the useEffect can clear them before they're processed. This breaks the diffing chain and causes the UI to stop reflecting state changes.
## How to Reproduce
1. Open the web UI
2. Trigger several pipeline state changes in quick succession (e.g., start multiple agents)
3. Observe that lozenge animations stop firing after a few updates
4. The pipeline state in the server is correct but the UI is stale
## Actual Result
The useEffect clears pendingFlyInActionsRef before processing, racing with useLayoutEffect that queues new actions. After a few rapid updates the animation queue gets into an inconsistent state and stops processing.
## Expected Result
Animation queue should handle rapid pipeline updates without losing actions or breaking the diffing chain.
## Acceptance Criteria
- [ ] No animation actions are lost during rapid pipeline updates
- [ ] Lozenge fly animations remain functional through sustained agent activity
@@ -0,0 +1,30 @@
---
name: "No heartbeat to detect stale WebSocket connections"
---
# Bug 138: No heartbeat to detect stale WebSocket connections
## Description
The WebSocket client in frontend/src/api/client.ts only reconnects when the onclose event fires. If the connection half-closes (appears open but stops receiving data), onclose never fires and reconnection never happens. There is no ping/pong heartbeat mechanism to detect this state.
## How to Reproduce
1. Open the web UI and establish a WebSocket connection
2. Wait for a network disruption or half-close scenario
3. The connection appears open but stops delivering messages
4. No reconnection is attempted
## Actual Result
The frontend keeps a dead WebSocket open indefinitely with no way to detect it has stopped receiving data. UI becomes permanently stale until manual refresh.
## Expected Result
A heartbeat mechanism should detect stale connections and trigger automatic reconnection.
## Acceptance Criteria
- [ ] WebSocket client implements a periodic heartbeat/ping to detect stale connections
- [ ] Stale connections are automatically closed and reconnected
- [ ] Server responds to ping frames or implements server-side keepalive
@@ -0,0 +1,21 @@
---
name: "Retry limit for mergemaster and pipeline restarts"
---
# Story 139: Retry limit for mergemaster and pipeline restarts
## User Story
As a developer using huskies, I want pipeline auto-restarts to have a configurable retry limit so that failing agents don't loop infinitely consuming CPU and API credits.
## Acceptance Criteria
- [ ] Pipeline auto-restart has a configurable max_retries per agent in project.toml (default 3)
- [ ] After max retries exhausted, agent status is set to Failed and no further restarts occur
- [ ] Server logs clearly indicate attempt number and when max retries are exhausted
- [ ] Retry count resets when a human manually restarts the agent (resume_context is None)
- [ ] Retry limit applies to all pipeline stages: Coder, QA, and Mergemaster restarts
## Out of Scope
- TBD
@@ -0,0 +1,86 @@
---
name: Stop Button
---
# Story 13: Stop Button
## User Story
**As a** User
**I want** a Stop button to cancel the model's response while it's generating
**So that** I can immediately stop long-running or unwanted responses without waiting for completion
## The Problem
**Current Behavior:**
- User sends message → Model starts generating
- User realizes they don't want the response (wrong question, too long, etc.)
- **No way to stop it** - must wait for completion
- Tool calls will execute even if user wants to cancel
**Why This Matters:**
- Long responses waste time
- Tool calls have side effects (file writes, searches, shell commands)
- User has no control once generation starts
- Standard UX pattern in ChatGPT, Claude, etc.
## Acceptance Criteria
- [ ] Stop button (⬛) appears in place of Send button (↑) while model is generating
- [ ] Clicking Stop immediately cancels the backend request
- [ ] Tool calls that haven't started yet are NOT executed after cancellation
- [ ] Streaming stops immediately
- [ ] Partial response generated before stopping remains visible in chat
- [ ] Stop button becomes Send button again after cancellation
- [ ] User can immediately send a new message after stopping
- [ ] Input field remains enabled during generation
## Out of Scope
- Escape key shortcut (can add later)
- Confirmation dialog (immediate action is better UX)
- Undo/redo functionality
- New Session flow (that's Story 14)
## Implementation Approach
### Backend
- Add `cancel_chat` command callable from frontend
- Use `tokio::select!` to race chat execution vs cancellation signal
- Check cancellation before executing each tool
- Return early when cancelled (not an error - expected behavior)
### Frontend
- Replace Send button with Stop button when `loading` is true
- On Stop click: call `invoke("cancel_chat")` and set `loading = false`
- Keep input enabled during generation
- Visual: Make Stop button clearly distinct (⬛ or "Stop" text)
## Testing Strategy
1. **Test Stop During Streaming:**
- Send message requesting long response
- Click Stop while streaming
- Verify streaming stops immediately
- Verify partial response remains visible
- Verify can send new message
2. **Test Stop Before Tool Execution:**
- Send message that will use tools
- Click Stop while "thinking" (before tool executes)
- Verify tool does NOT execute (check logs/filesystem)
3. **Test Stop During Tool Execution:**
- Send message with multiple tool calls
- Click Stop after first tool executes
- Verify remaining tools do NOT execute
## Success Criteria
**Before:**
- User sends message → No way to stop → Must wait for completion → Frustrating UX
**After:**
- User sends message → Stop button appears → User clicks Stop → Generation cancels immediately → Partial response stays → Can send new message
## Related Stories
- Story 14: New Session Cancellation (same backend mechanism, different trigger)
- Story 18: Streaming Responses (Stop must work with streaming)
@@ -0,0 +1,37 @@
---
name: "Activity status indicator never visible due to display condition"
---
# Bug 140: Activity status indicator never visible due to display condition
## Description
Story 86 wired up live activity status end-to-end (server emits tool_activity events over WebSocket, frontend receives them and calls setActivityStatus), but the UI condition `loading && !streamingContent` on line 686 of Chat.tsx guarantees the activity labels are never visible.
The timeline within a Claude Code turn:
1. Model starts generating text → onToken fires → streamingContent accumulates → streaming bubble shown, activity indicator hidden
2. Model decides to call a tool → content_block_start with tool_use arrives → setActivityStatus("Reading file...") fires
3. But streamingContent is still full of text from step 1 → condition !streamingContent is false → activity never renders
4. onUpdate arrives with the complete assistant message → setStreamingContent("") → now !streamingContent is true, but the next turn starts immediately or loading ends
The "Thinking..." fallback only shows in the brief window before the very first token of a request arrives — and at that point no tool has been called yet, so activityStatus is still null.
## How to Reproduce
1. Open the Story Kit web UI chat
2. Send any message that causes the agent to use tools (e.g. ask it to read a file)
3. Watch the thinking indicator
## Actual Result
The indicator always shows "Thinking..." and never changes to activity labels like "Reading file...", "Writing file...", etc.
## Expected Result
The indicator should cycle through tool activity labels (e.g. "Reading file...", "Executing command...") as the agent works, as specified in Story 86's acceptance criteria.
## Acceptance Criteria
- [ ] Activity status labels (e.g. 'Reading file...', 'Executing command...') are visible in the UI when the agent calls tools
- [ ] Activity is shown even when streamingContent is non-empty (e.g. between assistant turns or alongside the streaming bubble)
- [ ] The indicator still falls back to 'Thinking...' when no tool activity is in progress
@@ -0,0 +1,22 @@
---
name: "Improve server logging with timestamps and error visibility"
---
# Story 141: Improve server logging with timestamps and error visibility
## User Story
As a developer operating the system, I want server logs to include timestamps and surface errors and warnings prominently, so that I can diagnose problems instead of guessing why things silently failed.
## Acceptance Criteria
- [ ] All log lines emitted by slog!() include an ISO 8601 timestamp prefix
- [ ] Errors and warnings are logged at distinct severity levels (e.g. ERROR, WARN, INFO) so they can be filtered and stand out visually
- [ ] Agent lifecycle failures (process crashes, gate failures, worktree setup failures, pipeline advancement errors) are logged at ERROR or WARN level rather than silently swallowed
- [ ] MCP tool call failures are logged at WARN level with the tool name and error details
- [ ] Permission request timeouts and denials are logged at WARN level
- [ ] The get_server_logs MCP tool supports filtering by severity level (e.g. filter by ERROR to see only errors)
## Out of Scope
- TBD
@@ -0,0 +1,57 @@
---
name: "Quality gates run after fast-forward to master instead of before"
---
# Bug 142: Quality gates run after fast-forward to master instead of before
## Description
## Bug
The `merge_agent_work` function in `server/src/agents.rs` runs quality gates AFTER the squash merge has already been fast-forwarded to master. This means broken code lands on master before gates catch it.
### Current Flow (broken)
1. `run_squash_merge()` creates merge-queue branch + temp worktree
2. Squash merge + conflict resolution in temp worktree
3. **Fast-forward master to merge-queue commit** (line 2522)
4. Clean up temp worktree + branch
5. `run_merge_quality_gates()` runs on master (line 1047)
6. If gates fail, broken code is already on master
### Expected Flow
1. `run_squash_merge()` creates merge-queue branch + temp worktree
2. Squash merge + conflict resolution in temp worktree
3. **Run quality gates in the merge-queue worktree BEFORE fast-forward**
4. If gates fail: report failure back to mergemaster with the temp worktree still intact, so mergemaster can attempt fixes there (up to 2 attempts per story 135's prompt)
5. If gates still fail after mergemaster's retry attempts: tear down temp worktree + branch, leave master untouched, report to human
6. If gates pass: fast-forward master, clean up
### Key Files
- `server/src/agents.rs` line 1013: `merge_agent_work()` — orchestrator
- `server/src/agents.rs` line 2367: `run_squash_merge()` — does merge + fast-forward
- `server/src/agents.rs` line 2522: fast-forward step that should happen AFTER gates
- `server/src/agents.rs` line 1047: `run_merge_quality_gates()` — runs too late
### Impact
Broken merges (conflict markers, missing braces) land on master and break all worktrees that pull from it. Mergemaster then has to fix master directly, adding noise commits.
## How to Reproduce
1. Have a feature branch with code that conflicts with master
2. Call merge_agent_work for that story
3. run_squash_merge resolves conflicts (possibly incorrectly)
4. Fast-forwards master to the merge-queue commit BEFORE gates run
5. run_merge_quality_gates runs on master and finds broken code
6. Master is already broken
## Actual Result
Broken code (conflict markers, missing braces) lands on master. Mergemaster then fixes master directly, adding noise commits. All active worktrees pulling from master also break.
## Expected Result
Quality gates should run in the merge-queue worktree BEFORE fast-forwarding master. If gates fail, master should remain untouched.
## Acceptance Criteria
- [ ] Bug is fixed and verified
@@ -0,0 +1,18 @@
---
name: "Remove 0 running count from Agents panel header"
---
# Story 143: Remove 0 running count from Agents panel header
## User Story
As a user, I want the Agents panel header to hide the running count when no agents are running, so that the UI is less cluttered when idle.
## Acceptance Criteria
- [ ] When no agents are running, "0 running" is NOT visible in the Agents panel header
- [ ] When one or more agents are running, "N running" IS visible in the Agents panel header
## Out of Scope
- Changing the running count display format when agents are running
@@ -0,0 +1,19 @@
---
name: "Add build timestamp to frontend UI"
---
# Story 144: Add build timestamp to frontend UI
## User Story
As a developer, I want to see when the frontend was last built so I can tell whether it includes recent changes.
## Acceptance Criteria
- [ ] Inject a `__BUILD_TIME__` compile-time constant via `define` in `frontend/vite.config.ts`
- [ ] Display the build timestamp somewhere subtle in the UI (e.g. bottom corner, header tooltip, or footer)
- [ ] Timestamp should be human-readable (e.g. "Built: 2026-02-24 14:30")
## Out of Scope
- TBD
@@ -0,0 +1,24 @@
---
name: "Persist chat history to localStorage across rebuilds"
---
# Story 145: Persist chat history to localStorage across rebuilds
## User Story
As a developer using the Story Kit web UI, I want my chat history to persist across page reloads and Vite HMR rebuilds, so that I don't lose my conversation context during development.
## Acceptance Criteria
- [ ] AC1: Chat messages are restored from localStorage on component mount (surviving page reload / HMR rebuild)
- [ ] AC2: Chat messages are saved to localStorage whenever the message history updates (via WebSocket `onUpdate` or user sending a message)
- [ ] AC3: Clearing the session via "New Session" button also removes persisted messages from localStorage
- [ ] AC4: localStorage quota errors are handled gracefully (fail silently with console.warn, never crash the app)
- [ ] AC5: Messages are stored under a key scoped to the project path so different projects have separate histories
## Out of Scope
- Server-side persistence of chat history
- Multi-tab synchronization of chat state
- Compression or size management of stored messages
- Persisting streaming content or loading state
@@ -0,0 +1,29 @@
---
name: "Permission approval still returns wrong format - needs updatedInput not behavior allow"
---
# Bug 146: Permission approval still returns wrong format - needs updatedInput not behavior allow
## Description
Bug 130 changed prompt_permission from plain text to JSON, but used the wrong format. Claude Code permission-prompt-tool expects a union type: Approve = {updatedInput: {original tool input}}, Deny = {behavior: deny, message: string}. Current code at server/src/http/mcp.rs line 1642 returns {behavior: allow} which matches neither variant. Fix: change json!({behavior: allow}) to json!({updatedInput: tool_input}) where tool_input is already captured at line 1613. Also update the test at line 3076.
## How to Reproduce
1. Start server and open web UI
2. Chat with claude-code-pty agent
3. Ask it to do something requiring permission
4. Approve the permission dialog
5. Tool fails with Zod validation error about invalid_union
## Actual Result
prompt_permission returns behavior:allow on approval. Claude Code expects updatedInput with original input to approve.
## Expected Result
On approval, prompt_permission should return updatedInput containing the original tool input object.
## Acceptance Criteria
- [ ] Bug is fixed and verified
@@ -0,0 +1,80 @@
---
name: "Activity indicator still only shows Thinking despite bug 140 fix"
---
# Bug 147: Activity indicator still only shows Thinking despite bug 140 fix
## Description
Bug 140 fixed the frontend display condition but activity labels still never appear. The full data path has been traced and the suspected failure point identified.
## End-to-End Data Path
### 1. Frontend display (FIXED by bug 140)
- `frontend/src/components/Chat.tsx` line 686: `{loading && (activityStatus != null || !streamingContent) && (`
- `frontend/src/components/Chat.tsx` line 697: `{activityStatus ?? "Thinking..."}`
- `frontend/src/components/Chat.tsx` line 204: `setActivityStatus(formatToolActivity(toolName))` — called by `onActivity` callback
### 2. WebSocket client receives event
- `frontend/src/api/client.ts` line 350: `if (data.type === "tool_activity") this.onActivity?.(data.tool_name)`
### 3. Server sends ToolActivity over WebSocket (WIRED CORRECTLY)
- `server/src/http/ws.rs` line 251-254: activity callback sends `WsResponse::ToolActivity { tool_name }`
- This callback is passed to `chat::chat()` as the `on_activity` closure
### 4. chat::chat passes callback to Claude Code provider
- `server/src/llm/chat.rs`: passes `on_activity` through to `claude_code::chat_stream`
- `server/src/llm/providers/claude_code.rs` line 47: `mut on_activity: A` parameter
- `server/src/llm/providers/claude_code.rs` line 70: creates internal `activity_tx` channel
- `server/src/llm/providers/claude_code.rs` line 94: drains channel and calls `on_activity(&name)`
### 5. PTY event processing (SUSPECTED FAILURE POINT)
- `server/src/llm/providers/claude_code.rs` line 327: `process_json_event()` dispatches parsed JSON
- Line 348-353: matches `"stream_event"` type → extracts inner `event` → calls `handle_stream_event()`
- `server/src/llm/providers/claude_code.rs` line 486: `handle_stream_event()` matches on event type
- Line 494-500: matches `"content_block_start"` with `content_block.type == "tool_use"` → sends to `activity_tx`
### 6. The problem
`handle_stream_event` only matches `content_block_start` — this is the **raw Anthropic streaming API format**. But Claude Code's `--output-format stream-json` may NOT emit raw Anthropic events wrapped in `stream_event`. It likely uses its own event types for tool calls (e.g. `tool_use_begin`, `tool_use`, or similar).
The existing `process_json_event` also matches `"assistant"` (line 355) and `"user"` (line 363) event types from stream-json, but these are complete messages — they arrive after the tool call is done, not when it starts. So there's no event being caught at tool-call-start time.
## Investigation Steps
1. Add logging in `process_json_event` (line 334) to print every `event_type` received from the PTY during a chat session with tool use
2. Identify which event type Claude Code emits when it starts a tool call
3. Add matching for that event type to fire `activity_tx.send(tool_name)`
## Key Files
- `server/src/llm/providers/claude_code.rs` line 327: `process_json_event` — event dispatcher
- `server/src/llm/providers/claude_code.rs` line 486: `handle_stream_event` — only handles Anthropic API format
- `server/src/http/ws.rs` line 251: activity callback wiring to WebSocket
- `frontend/src/components/Chat.tsx` line 203: `onActivity` handler that sets display state
## How to Reproduce
1. Rebuild both frontend and backend from master (which includes story 86 and bug 140)
2. Open web UI chat
3. Send a message that causes tool use (e.g. ask agent to read a file)
4. Watch the activity indicator
## Actual Result
Indicator always shows "Thinking..." and never changes to tool activity labels like "Reading file..." or "Executing command..."
## Expected Result
Indicator should cycle through tool activity labels as the agent calls tools
## Hints for the Coder
- **Check external docs**: The Claude Code CLI `--output-format stream-json` format may be documented at https://docs.anthropic.com or in the Claude Code repo. Search for the actual event schema before guessing.
- **Add logging as an intermediate step**: If unsure about the event format, add a `slog!` or `eprintln!` in `process_json_event` (line 334) to log every `event_type` received. Rebuild, run a web UI chat with tool use, and inspect the output to see exactly what events arrive.
- **Run the CLI directly**: You can run `claude -p "read /etc/hosts" --output-format stream-json` in a terminal to see the raw stream-json output and identify the event types for tool calls.
- **Don't assume the Anthropic API format**: The existing `content_block_start` matching was likely copied from the Anthropic provider. Claude Code's stream-json is a different format.
## Acceptance Criteria
- [ ] Activity indicator shows tool names (e.g. "Reading file...", "Executing command...") when the web UI agent calls tools
- [ ] Indicator still falls back to "Thinking..." when no tool activity is in progress
- [ ] Works for all tool types (Read, Write, Bash, Glob, Grep, etc.)
@@ -0,0 +1,22 @@
---
name: "Interactive onboarding guides user through project setup after init"
---
# Story 148: Interactive onboarding guides user through project setup after init
## User Story
As a new Story Kit user, after the project structure has been scaffolded, I want a guided conversation that asks me about my project goals and tech stack, so that the specs are populated and I'm ready to write Story #1.
## Acceptance Criteria
- [ ] After scaffold completes and the user opens the chat UI, the agent detects empty/template specs and enters onboarding mode
- [ ] The agent asks the user what the project is about (goal, domain) and writes a populated specs/00_CONTEXT.md based on their answers
- [ ] The agent asks the user what tech stack they want (language, framework, build tools, test runner, linter) and writes a populated specs/tech/STACK.md based on their answers
- [ ] The agent updates script/test to invoke the project's actual test runner (e.g. cargo test, pytest, pnpm test)
- [ ] The agent updates project.toml component setup commands to match the chosen stack (e.g. pnpm install for a JS project, cargo check for Rust)
- [ ] After onboarding completes, the agent commits the populated specs and tells the user they're ready for Story #1
## Out of Scope
- TBD
@@ -0,0 +1,43 @@
---
name: "Web UI does not update when agents are started or stopped"
---
# Bug 149: Web UI does not update when agents are started or stopped
## Description
Agent start/stop changes are in-memory HashMap mutations in the agent pool. No WatcherEvent is emitted for these changes, so the WebSocket never pushes an update to the frontend. The agent panel only refreshes on its polling interval, meaning agent swaps and new agent starts are invisible until the next poll.
Additionally, when an agent is assigned to a work item (e.g. a coder starts on a story), the pipeline board should reflect the change immediately — the work item should go from amber (unassigned) to green (agent working). Currently this requires a full page refresh.
The key insight is that agent assignment is an in-memory event, not a filesystem event, so the watcher won't catch it. The server needs to push agent state changes over WebSocket explicitly.
Fix options:
1. Emit a WatcherEvent (e.g. AgentStateChanged) when start_agent/stop_agent modifies the pool, and have the WebSocket handler forward it to the frontend
2. Or have the frontend subscribe to a dedicated agent-state WebSocket message type
Key files:
- server/src/agents.rs: start_agent / stop_agent — where the state change happens
- server/src/http/ws.rs: WebSocket handler that could forward agent state events
- frontend/src/components/AgentPanel.tsx: polling-based agent list refresh
- frontend pipeline board: wherever work item color (amber/green) is derived from agent assignment
## How to Reproduce
1. Open the web UI and look at the pipeline board
2. Start an agent on a story via MCP or the API
3. Observe the pipeline board — the work item stays amber until a full page refresh
## Actual Result
Agent panel and pipeline board do not update until the next polling interval or a full page refresh. Starting/stopping agents and agent assignment to work items are invisible in real-time.
## Expected Result
Agent panel and pipeline board should update immediately when agents are started, stopped, or assigned to work items.
## Acceptance Criteria
- [ ] Agent start/stop events are pushed over WebSocket to the frontend
- [ ] Pipeline board work items update color (amber → green) immediately when an agent is assigned
- [ ] No full page refresh required to see agent state changes
@@ -0,0 +1,31 @@
---
name: Auto-focus Chat Input on Startup
---
# Story: Auto-focus Chat Input on Startup
## User Story
**As a** User
**I want** the cursor to automatically appear in the chat input box when the app starts
**So that** I can immediately start typing without having to click into the input field first.
## Acceptance Criteria
* [x] When the app loads and a project is selected, the chat input box should automatically receive focus
* [x] The cursor should be visible and blinking in the input field
* [x] User can immediately start typing without any additional clicks
* [x] Focus should be set after the component mounts
* [x] Should not interfere with other UI interactions
## Out of Scope
* Auto-focus when switching between projects (only on initial load)
* Remembering cursor position across sessions
* Focus management for other input fields
## Implementation Notes
* Use React `useEffect` hook to set focus on component mount
* Use a ref to reference the input element
* Call `inputRef.current?.focus()` after component renders
* Ensure it works consistently across different browsers
## Related Functional Specs
* Functional Spec: UI/UX
@@ -0,0 +1,63 @@
---
name: "qa-2 agent never auto-assigned because pipeline_stage only matches exact qa"
---
# Bug 150: qa-2 agent never auto-assigned because pipeline_stage only matches exact qa
## Description
The `pipeline_stage()` function in `server/src/agents.rs` (line 154) determines an agent's pipeline role by parsing its **name** — there's no structured `stage` field in the agent config. This means `qa-2` falls through to `PipelineStage::Other` because it doesn't exactly match `"qa"`.
### Root Cause
`project.toml` agent config has `name` and `role` (freetext description), but no `stage` or `pipeline_role` field. The code guesses the pipeline stage from the name:
```rust
match agent_name {
"qa" => PipelineStage::Qa,
"mergemaster" => PipelineStage::Mergemaster,
name if name.starts_with("coder") => PipelineStage::Coder,
_ => PipelineStage::Other,
}
```
### The Fix
1. Add a `stage` field to `[[agent]]` in `project.toml` config schema. Valid values: `"coder"`, `"qa"`, `"mergemaster"`, `"other"`.
2. Update `ProjectConfig` / agent config deserialization in the server to parse the new field.
3. Replace `pipeline_stage(agent_name)` with a lookup from the agent's config `stage` field.
4. Update `project.toml` to add `stage` to all agents:
- supervisor: `stage = "other"`
- coder-1, coder-2, coder-opus: `stage = "coder"`
- qa, qa-2: `stage = "qa"`
- mergemaster: `stage = "mergemaster"`
5. Remove the name-based `pipeline_stage()` function entirely. The `stage` field is required.
### Key Files
- `server/src/agents.rs` line 154: `pipeline_stage()` — name-based matching
- `server/src/agents.rs` line 1728: `find_free_agent_for_stage()` — uses `pipeline_stage()`
- `server/src/config.rs` (or wherever `ProjectConfig` is defined): agent config deserialization
- `.story_kit/project.toml`: agent definitions
## How to Reproduce
1. Have multiple items in `3_qa/`
2. `qa` agent gets assigned to one
3. `qa-2` never gets assigned to the others
## Actual Result
`qa-2` is never auto-assigned. `pipeline_stage("qa-2")` returns `PipelineStage::Other`.
## Expected Result
`qa-2` should be recognized as a QA agent and auto-assigned to items in `3_qa/`.
## Acceptance Criteria
- [ ] Agent config in `project.toml` supports a `stage` field (`coder`, `qa`, `mergemaster`, `other`)
- [ ] `find_free_agent_for_stage` uses the config `stage` field instead of name parsing
- [ ] `qa-2` is correctly auto-assigned to QA work
- [ ] `stage` is a required field — server refuses to start if any agent is missing it
- [ ] The old name-based `pipeline_stage()` function is removed
- [ ] All existing agents in `project.toml` have `stage` set
@@ -0,0 +1,42 @@
---
name: "Split archived into done and archived with time-based promotion"
---
# Story 151: Split archived into done and archived with time-based promotion
## User Story
As a developer watching work flow through the pipeline in my IDE, I want recently completed items separated from old ones, so that the folder view stays useful without being cluttered by dozens of ancient stories.
## Description
The `5_archived` folder has grown huge and clutters the IDE sidebar. Split it into two stages:
- `5_done`: recently completed work — visible in IDE, useful for watching flow
- `6_archived`: old work moved here automatically after 4 hours
The watcher should periodically check `5_done/` and move items older than 4 hours (based on file mtime) to `6_archived/`.
### Key Files
- `server/src/io/watcher.rs`: filesystem watcher — add periodic sweep of `5_done/`
- `server/src/agents.rs`: `move_story_to_archived` → rename to `move_story_to_done`, target `5_done/`
- All MCP tools and pipeline logic that reference `5_archived` need updating to use `5_done`
- Frontend pipeline display if it shows archived/done items
- `.story_kit/README.md`: update pipeline stage documentation
- Story 116's init scaffolding: `huskies init` must create `5_done/` and `6_archived/` directories
- Any templates or scaffold code that creates the `.story_kit/work/` directory structure
## Acceptance Criteria
- [ ] `5_archived/` is renamed to `6_archived/`
- [ ] New `5_done/` directory is created and used as the immediate completion target
- [ ] Mergemaster, accept_story, and all pipeline functions move completed work to `5_done/` (not directly to archived)
- [ ] Watcher periodically sweeps `5_done/` and moves items older than 4 hours to `6_archived/`
- [ ] Existing items in old `5_archived/` are migrated to `6_archived/`
- [ ] Frontend pipeline display updated if applicable
- [ ] `.story_kit/README.md` updated to reflect the new pipeline stages
- [ ] `huskies init` scaffolding creates `5_done/` and `6_archived/` (coordinate with story 116)
## Out of Scope
- TBD
@@ -0,0 +1,29 @@
---
name: "Ollama not running kills the entire web UI"
---
# Bug 152: Ollama not running kills the entire web UI
## Description
The UI fetches Ollama models on load via /api/ollama/models (server/src/http/model.rs line 40). When Ollama is not running, the request fails and the error propagates in a way that kills the whole UI.
The server endpoint at server/src/http/model.rs should return an empty list instead of an error when Ollama is unreachable. Or the frontend should catch the error gracefully and just show no Ollama models in the dropdown.
## How to Reproduce
1. Stop Ollama (or never start it)
2. Open the web UI
3. Observe error: Request failed: error sending request for url (http://localhost:11434/api/tags)
## Actual Result
The entire web UI is broken. Nothing works.
## Expected Result
Ollama model fetch should fail silently or show an empty model list. The rest of the UI should work normally with Claude Code or Anthropic providers.
## Acceptance Criteria
- [ ] Bug is fixed and verified
@@ -0,0 +1,38 @@
---
name: "Auto-assign broken after stage field was added to agent config"
---
# Bug 153: Auto-assign broken after stage field was added to agent config
## Description
Bug 150 changed agent pipeline role detection from name-based matching (pipeline_stage function) to a stage field in project.toml. The auto_assign_available_work function in server/src/agents.rs (line 1212) uses find_free_agent_for_stage (line 1728) to match agents to pipeline stages. After the stage field change, auto-assign stopped working — free coders are not picked up for items in 2_current/.
Likely causes:
- find_free_agent_for_stage still calls the old pipeline_stage() by name instead of reading the config stage field
- Or the PipelineStage enum comparison is failing due to a mismatch between config values and enum variants
- Or auto_assign_available_work is not being triggered after agent completion
Key files:
- server/src/agents.rs line 1212: auto_assign_available_work
- server/src/agents.rs line 1728: find_free_agent_for_stage
- server/src/config.rs: agent config with new stage field
- .story_kit/project.toml: stage values on each agent
## How to Reproduce
1. Have items in 2_current/ with free coders available
2. Wait for auto_assign_available_work to trigger
3. Free coders are not assigned to waiting items
## Actual Result
Free coders sit idle while items wait in current. Manual start_agent works fine.
## Expected Result
auto_assign_available_work should detect free coders and assign them to waiting items, using the new stage field from project.toml.
## Acceptance Criteria
- [ ] Bug is fixed and verified
@@ -0,0 +1,48 @@
---
name: "Mergemaster quality gates fail because merge worktree has no frontend dependencies"
---
# Bug 154: Mergemaster quality gates always fail — merge worktree missing frontend deps
## Description
`run_squash_merge()` in `server/src/agents.rs` creates an ephemeral git worktree at `.story_kit/merge_workspace`, does the squash merge + commit, then runs quality gates. But it **never installs frontend dependencies**, so every gate fails and master never moves forward.
## Root Cause
The merge worktree is created via `git worktree add` (line 2383-2396) which is just a git checkout — no `node_modules/`, no `frontend/dist/`. The quality gates (`run_merge_quality_gates` at line 2773) then run:
1. **`cargo clippy`** (line 2781) — FAILS because RustEmbed requires `frontend/dist/` to exist at compile time
2. **`pnpm build`** (line 2818) — FAILS because no `node_modules/` (never ran `pnpm install`)
3. **`pnpm test`** (line 2841) — FAILS for the same reason
Result: `gates_passed: false`, worktree cleaned up, master unchanged. Every single merge attempt fails.
## The Fix
Add frontend dependency setup between worktree creation (line 2396) and quality gates (line 2513). After the squash merge commit succeeds, but before gates run:
1. `mkdir -p frontend/dist` — minimum for cargo clippy to not fail on RustEmbed
2. Run `pnpm install` in the worktree's `frontend/` directory
3. The existing `pnpm build` gate (line 2818) will then populate `frontend/dist/` properly
The dependency install step should happen right after the commit (line 2511) and before the quality gates comment at line 2513. Add it as a clearly labeled section:
```
// ── Install frontend dependencies for quality gates ──────────
```
If `pnpm install` fails, treat it the same as a gate failure: log output, cleanup, return `success: false`.
## Key File
- `server/src/agents.rs` line 2383-2396: worktree creation (no deps installed)
- `server/src/agents.rs` line 2513-2549: quality gates (need deps to pass)
- `server/src/agents.rs` line 2773: `run_merge_quality_gates()` — runs cargo clippy, pnpm build, pnpm test
## Acceptance Criteria
- [ ] After merge worktree is created and commit is made, `pnpm install` runs in the worktree's `frontend/` directory
- [ ] `mkdir -p frontend/dist` is created before cargo clippy runs (as a fallback in case pnpm install succeeds but build hasn't run yet)
- [ ] If `pnpm install` fails, merge is aborted cleanly with diagnostic output
- [ ] Quality gates (cargo clippy, pnpm build, pnpm test) pass in the merge worktree for a normal merge with no conflicts

Some files were not shown because too many files have changed in this diff Show More