Compare commits
411 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 358f177584 | |||
| b60bb57aa4 | |||
| 7003fca873 | |||
| b5d825356e | |||
| 896eb4fc52 | |||
| f8d7438eec | |||
| f7f4e8f95b | |||
| af76910f36 | |||
| f06111f045 | |||
| c6020b7f43 | |||
| 488b798275 | |||
| 0df19967ca | |||
| 6e04015676 | |||
| acaf9477a1 | |||
| 46a89d481a | |||
| c51428414e | |||
| 50405800c6 | |||
| 4aca056bc9 | |||
| 5e725340b4 | |||
| 3fa2064e3e | |||
| 16f9722851 | |||
| 5f0680c6c1 | |||
| 57e0197d75 | |||
| dc4bac3a85 | |||
| f16545ec36 | |||
| d132ed8e64 | |||
| 2a633d604a | |||
| 6a44c0b8ee | |||
| 3f97e34f21 | |||
| 49a8a23d75 | |||
| 1358a32476 | |||
| 9b79160c95 | |||
| 0cbe99677f | |||
| 46b1609528 | |||
| 2b0b08ceda | |||
| 19cc684433 | |||
| fecb157291 | |||
| ac84e7240e | |||
| d5d82bdb00 | |||
| f10edd6718 | |||
| 3f6cd55833 | |||
| a9e8bc4d87 | |||
| 063e0fa76e | |||
| 9e7bd33822 | |||
| 7427865e46 | |||
| ff5f9c76fd | |||
| 641bbfbe2e | |||
| 5516ec4595 | |||
| 762467efd4 | |||
| 3f54bda360 | |||
| 4d1e388a48 | |||
| 10be86587a | |||
| 6a10591413 | |||
| 321c88e05e | |||
| 23562dfa61 | |||
| cb6ebf1d69 | |||
| a006985faf | |||
| 3fce9ec082 | |||
| 03026c70cc | |||
| b75679175b | |||
| 440081016d | |||
| e8f3629c76 | |||
| c5cdc0f594 | |||
| fec417cb16 | |||
| a70a06a5fb | |||
| 0a617e1c18 | |||
| 4527f71857 | |||
| 6e0d12d145 | |||
| d471d29c72 | |||
| 0b652eec21 | |||
| b32fdf7d65 | |||
| 2da0e1eb55 | |||
| 269124a1fd | |||
| 5992f9bd19 | |||
| a53967453e | |||
| ab4b218ac7 | |||
| d5b936c88d | |||
| 07cc0e3f29 | |||
| db4a84c70f | |||
| 3048d26e66 | |||
| 8e45b2a08d | |||
| ddc4a57cd2 | |||
| d216f3c267 | |||
| 8cd881c8f1 | |||
| 2867e1d15f | |||
| c2c9d3f9cb | |||
| f734b4a3c6 | |||
| 890693efda | |||
| 5403b29261 | |||
| 8ee59f5dc1 | |||
| 5dcc35a1b3 | |||
| af70b68cd1 | |||
| e356f9b2dd | |||
| 96793de11b | |||
| bfe70f5599 | |||
| 98aedaddf0 | |||
| 496ce864d7 | |||
| 243738551c | |||
| 20f2d97f06 | |||
| b6edc1bff7 | |||
| c45613a3ad | |||
| 7efed33851 | |||
| b00a477070 | |||
| 52f2e89659 | |||
| 08db28d9d6 | |||
| 77ff0ce093 | |||
| 0ab1b1232b | |||
| 209e01bc06 | |||
| 2650b1a42e | |||
| 3595df4d9d | |||
| 5d84100c41 | |||
| dd436ad186 | |||
| b811b9188f | |||
| 9935311c35 | |||
| be0036922a | |||
| 361f9dff0d | |||
| fc160b5c5f | |||
| 9092b8a2c9 | |||
| dfe3d96313 | |||
| bcefa6a25d | |||
| 50bfeddcb5 | |||
| 8e6b8ef338 | |||
| d363eb63e2 | |||
| 422cec370d | |||
| 973b7d6f72 | |||
| 49b78f3642 | |||
| 93576e3f83 | |||
| dd7f71dd87 | |||
| 9a8492c72f | |||
| ac9bdde164 | |||
| 0b2ec64c74 | |||
| fe0a032e8e | |||
| eff8f6a6a6 | |||
| e45eab82f2 | |||
| 310ad365e6 | |||
| 0b50c66caa | |||
| 9feed0f882 | |||
| bb3301c5af | |||
| a2123274a5 | |||
| 3cbbc5387a | |||
| 4e828fbdd1 | |||
| 6d88595e0d | |||
| aa90646edf | |||
| 7235ab7c7c | |||
| a0326dae78 | |||
| 953fce2ca6 | |||
| 5035b84de5 | |||
| c2f477dde6 | |||
| b098c8ff9f | |||
| 7fea543f60 | |||
| f8bb23a6d4 | |||
| 0016841770 | |||
| 3639d64da6 | |||
| ebdcf18134 | |||
| d83f2ae4c1 | |||
| f6c0d35f11 | |||
| facbf51f05 | |||
| 847ebc292f | |||
| 065ca2bd8f | |||
| 34988855bc | |||
| 7fc788baea | |||
| 40575924b5 | |||
| 4f56fa6cbe | |||
| 52513b55ff | |||
| 1ae2fa9b9b | |||
| 6077f74dbd | |||
| 8ab2e19e98 | |||
| b44f3a33e3 | |||
| 57407aed51 | |||
| a29677b3c7 | |||
| 95df450fca | |||
| 6c6bc35785 | |||
| 7652bbba9c | |||
| efd89a26ac | |||
| 71d4746009 | |||
| 98b5475160 | |||
| 740f1b5e6e | |||
| c0bab1e671 | |||
| 306810e4d5 | |||
| 1193b7ac9a | |||
| 05db012aaf | |||
| bc3c852509 | |||
| 04051282da | |||
| 081b33a8a6 | |||
| cf5424f9a6 | |||
| 1ec9aaab8a | |||
| d6f82393f5 | |||
| f4ce0e017b | |||
| c0ea5f0cb8 | |||
| d375c4b1d3 | |||
| 4ea4be1462 | |||
| bc1c1cd2c9 | |||
| c1e4c40f31 | |||
| 203e8f22be | |||
| 665c036a56 | |||
| 73304f08ac | |||
| fe9fc69f96 | |||
| 3b0542cd41 | |||
| 102919e0b3 | |||
| d63aa0a3c2 | |||
| 7f7db57933 | |||
| 043791194f | |||
| 710f839c65 | |||
| b0e21abb6e | |||
| 6b71c07f5b | |||
| 9cff3c753d | |||
| 6acd7f5249 | |||
| 26f5b25f22 | |||
| 8bc0bd592e | |||
| 7c25aca39b | |||
| 5173bf4aef | |||
| 7f7f49d757 | |||
| e88b9bbc63 | |||
| db22ab2229 | |||
| c30ad79398 | |||
| 16853328fa | |||
| 8ac8cdba88 | |||
| c046edebda | |||
| eef9669c95 | |||
| a9cdd3a354 | |||
| b4eeb499e9 | |||
| fca46c3806 | |||
| 2510fe44bc | |||
| e152cf3cb8 | |||
| 7d3b256fff | |||
| f6d632139e | |||
| 204a99c2e7 | |||
| f28a03e42e | |||
| 26f4edadcc | |||
| fd58631e65 | |||
| f70399a28f | |||
| 02d08faaa2 | |||
| 2dc77479ad | |||
| c5761ae968 | |||
| 67754781ca | |||
| 3436507a21 | |||
| 93bc08574b | |||
| 3571511349 | |||
| 04214ca155 | |||
| 4d48df152c | |||
| e0a70a4c1c | |||
| bae50fbc5b | |||
| 8998dac593 | |||
| e6b300e70e | |||
| b22e2b9274 | |||
| 24b1aa6e7f | |||
| 85e37e03a8 | |||
| f22a2666b8 | |||
| 507889627a | |||
| c4cee72938 | |||
| 33cb363651 | |||
| cd3ded278d | |||
| b5bf75aa5a | |||
| f6b5b1b01a | |||
| 26d34245f9 | |||
| de54265c35 | |||
| a52d1e098f | |||
| 015fa48c32 | |||
| abc30c93d1 | |||
| cf2faa9bff | |||
| 92aa1ebccf | |||
| 877f69c897 | |||
| 710b604b7c | |||
| ab4ce2db92 | |||
| 61f6fd60a8 | |||
| e66149e07c | |||
| 108a697483 | |||
| 1a7f419ecf | |||
| 96b1ce373b | |||
| 58e41f7e0b | |||
| c9a2fa58eb | |||
| 64c0f190cf | |||
| fc443ed987 | |||
| 7939a19816 | |||
| 46b5087157 | |||
| a8d6524b56 | |||
| 61d63db84c | |||
| aa4ec8c779 | |||
| 3777042ad3 | |||
| feb340beba | |||
| 23369c514d | |||
| 832da16b6f | |||
| 131964cbc3 | |||
| 81db0504ed | |||
| 584a44a516 | |||
| c7c4a57533 | |||
| 0a67c28f8c | |||
| 6476492caa | |||
| faf8734ea8 | |||
| 862f0704be | |||
| d3df1586c6 | |||
| 8a1996e0e4 | |||
| 61f5a0c3be | |||
| d7bc785de1 | |||
| eaac665a9f | |||
| d702aa59c4 | |||
| 9df9a1454a | |||
| 47163d235c | |||
| a7342fc9d3 | |||
| 5dd8feb75c | |||
| f5024b2648 | |||
| 6521c83eec | |||
| 65e3643655 | |||
| fc95b57a78 | |||
| 7c1a970b13 | |||
| 64e2df20b7 | |||
| 90e3612fd3 | |||
| 962bfe37c6 | |||
| f05c6a42b0 | |||
| 077288e7b7 | |||
| 580ab1ce68 | |||
| 71a6c72614 | |||
| fae7b3be20 | |||
| 775b9ac7e3 | |||
| 5a87d55dd4 | |||
| 0457fbfecc | |||
| 13b16138b5 | |||
| 8249896449 | |||
| dbd932bf46 | |||
| eef49678ce | |||
| 58ee82c988 | |||
| 49ac23044a | |||
| 84a775be77 | |||
| 60c0c95f38 | |||
| a1a30bcc42 | |||
| 96ebd7ecb8 | |||
| 25c8b1ec25 | |||
| bcb7cfabee | |||
| d4dad1d556 | |||
| 195c7c51c4 | |||
| 968d973cff | |||
| 4394ab3fed | |||
| 11bbfca3da | |||
| a9aa88b655 | |||
| b62974dd88 | |||
| ac52a8bb4e | |||
| 18755aac96 | |||
| 5d37421f70 | |||
| 224d269971 | |||
| 6146a173f1 | |||
| 821345d266 | |||
| 0fa63e2de3 | |||
| d8cbec8268 | |||
| 618a2779ff | |||
| 721d12bcfe | |||
| df6d2db327 | |||
| 49285c1865 | |||
| 0c15be43b8 | |||
| 9408bd2cdf | |||
| a24e4c5c85 | |||
| c0133fe733 | |||
| 752c3904bf | |||
| bac53ac09a | |||
| b2ef2eca5f | |||
| fb05f71e76 | |||
| 438be196c9 | |||
| f1b4894d6e | |||
| bd281fd749 | |||
| 79edc28334 | |||
| 92c53704f0 | |||
| 7223fa2f10 | |||
| dedf951b17 | |||
| aad583defd | |||
| 88b02cf746 | |||
| 1a9833d820 | |||
| a904cda629 | |||
| c755c03f0e | |||
| a8630f3e1b | |||
| 9fb1bd5711 | |||
| 0b3ce0f33e | |||
| f4b7573f0a | |||
| bb801ba826 | |||
| 53634d638d | |||
| b50e7cff00 | |||
| 68973b0bb8 | |||
| 34bbf5a122 | |||
| ed3c5f9c95 | |||
| 59d1a2c069 | |||
| 52e73bfbea | |||
| 4e590401a5 | |||
| 6b6815325d | |||
| f874783b09 | |||
| 292f9cdfe2 | |||
| 1cce46d3fa | |||
| e85c06df19 | |||
| 8b85ca743e | |||
| 1a7b6c7342 | |||
| 4a94158ef2 | |||
| f10ea1ecf2 | |||
| 1a3b69301a | |||
| 6d3eab92fd | |||
| f6920a87ad | |||
| 5f9d903987 | |||
| ea916d27f4 | |||
| 970b9bcd9d | |||
| a5ee6890f5 | |||
| 41dc3292bb | |||
| 3766f8b464 | |||
| 0c85ecc85c | |||
| 2c29a4d2b8 | |||
| 454d694d24 | |||
| 96bedd70dc | |||
| fffdd5c5ea | |||
| 4805598932 | |||
| 3d55e2fcc6 | |||
| 96b31d1a48 | |||
| 11168fa426 | |||
| c2c2d65889 | |||
| 5c8c4b7ff3 | |||
| fbab93f493 | |||
| 78ff6d104e |
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"enabledMcpjsonServers": ["storkit"],
|
||||
"enabledMcpjsonServers": [
|
||||
"storkit"
|
||||
],
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(./server/target/debug/storkit:*)",
|
||||
@@ -67,7 +69,8 @@
|
||||
"Bash(tail *)",
|
||||
"Bash(wc *)",
|
||||
"Bash(npx vite:*)",
|
||||
"Bash(npm run dev:*)"
|
||||
"Bash(npm run dev:*)",
|
||||
"Bash(stat *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
# App specific (root-level; storkit subdirectory patterns live in .storkit/.gitignore)
|
||||
store.json
|
||||
.storkit_port
|
||||
.storkit/bot.toml.bak
|
||||
|
||||
# Rust stuff
|
||||
target
|
||||
|
||||
@@ -20,3 +20,6 @@ coverage/
|
||||
|
||||
# Token usage log (generated at runtime, contains cost data)
|
||||
token_usage.jsonl
|
||||
|
||||
# Chat service logs
|
||||
whatsapp_history.json
|
||||
|
||||
+33
-5
@@ -9,16 +9,22 @@
|
||||
|
||||
When you start a new session with this project:
|
||||
|
||||
1. **Check for MCP Tools:** Read `.mcp.json` to discover the MCP server endpoint. Then list available tools by calling:
|
||||
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["storkit"].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.
|
||||
2. **Read Context:** Check `.story_kit/specs/00_CONTEXT.md` for high-level project goals.
|
||||
3. **Read Stack:** Check `.story_kit/specs/tech/STACK.md` for technical constraints and patterns.
|
||||
4. **Check Work Items:** Look at `.story_kit/work/1_backlog/` and `.story_kit/work/2_current/` to see what work is pending.
|
||||
3. **Read Context:** Check `.storkit/specs/00_CONTEXT.md` for high-level project goals.
|
||||
4. **Read Stack:** Check `.storkit/specs/tech/STACK.md` for technical constraints and patterns.
|
||||
5. **Check Work Items:** Look at `.storkit/work/1_backlog/` and `.storkit/work/2_current/` to see what work is pending.
|
||||
|
||||
|
||||
---
|
||||
@@ -228,7 +234,29 @@ If a user hands you this document and says "Apply this process to my project":
|
||||
|
||||
---
|
||||
|
||||
## 6. Code Quality
|
||||
## 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 `.storkit/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 .storkit/bot.toml.matrix.example .storkit/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.
|
||||
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
homeserver = "https://matrix.example.com"
|
||||
username = "@botname:example.com"
|
||||
password = "your-bot-password"
|
||||
|
||||
# List one or more rooms to listen in. Use a single-element list for one room.
|
||||
room_ids = ["!roomid:example.com"]
|
||||
|
||||
# Optional: the deprecated single-room key is still accepted for backwards compat.
|
||||
# room_id = "!roomid:example.com"
|
||||
|
||||
allowed_users = ["@youruser:example.com"]
|
||||
enabled = false
|
||||
|
||||
# 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"]
|
||||
|
||||
# ── WhatsApp Business API ──────────────────────────────────────────────
|
||||
# Set transport = "whatsapp" to use WhatsApp instead of Matrix.
|
||||
# The webhook endpoint will be available at /webhook/whatsapp.
|
||||
# You must configure this URL in the Meta Developer Dashboard.
|
||||
#
|
||||
# transport = "whatsapp"
|
||||
# whatsapp_phone_number_id = "123456789012345"
|
||||
# whatsapp_access_token = "EAAx..."
|
||||
# whatsapp_verify_token = "my-secret-verify-token"
|
||||
#
|
||||
# ── 24-hour messaging window & notification templates ─────────────────
|
||||
# WhatsApp only allows free-form text messages within 24 hours of the last
|
||||
# inbound message from a user. For proactive pipeline notifications sent
|
||||
# after the window expires, an approved Meta message template is used.
|
||||
#
|
||||
# Register the template in the Meta Business Manager:
|
||||
# 1. Go to Business Settings → WhatsApp → Message Templates → Create.
|
||||
# 2. Category: UTILITY
|
||||
# 3. Template name: pipeline_notification (or your chosen name below)
|
||||
# 4. Language: English (en_US)
|
||||
# 5. Body text (example):
|
||||
# Story *{{1}}* has moved to *{{2}}*.
|
||||
# Where {{1}} = story name, {{2}} = pipeline stage.
|
||||
# 6. Submit for review. Meta typically approves utility templates within
|
||||
# minutes; transactional categories may take longer.
|
||||
#
|
||||
# Once approved, set the name below (default: "pipeline_notification"):
|
||||
# whatsapp_notification_template = "pipeline_notification"
|
||||
|
||||
# ── Slack Bot API ─────────────────────────────────────────────────────
|
||||
# Set transport = "slack" to use Slack instead of Matrix.
|
||||
# The webhook endpoint will be available at /webhook/slack.
|
||||
# Configure this URL in the Slack App → Event Subscriptions → Request URL.
|
||||
#
|
||||
# Required Slack App scopes: chat:write, chat:update
|
||||
# Subscribe to bot events: message.channels, message.groups, message.im
|
||||
#
|
||||
# transport = "slack"
|
||||
# slack_bot_token = "xoxb-..."
|
||||
# slack_signing_secret = "your-signing-secret"
|
||||
# slack_channel_ids = ["C01ABCDEF"]
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
@@ -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"]
|
||||
+121
-50
@@ -11,12 +11,17 @@ max_coders = 3
|
||||
|
||||
# Maximum retries per story per pipeline stage before marking as blocked.
|
||||
# Set to 0 to disable retry limits.
|
||||
max_retries = 2
|
||||
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 install", "npm run build"]
|
||||
setup = ["npm ci", "npm run build"]
|
||||
teardown = []
|
||||
|
||||
[[component]]
|
||||
@@ -58,30 +63,52 @@ system_prompt = "You are a full-stack engineer working autonomously in a git wor
|
||||
[[agent]]
|
||||
name = "qa-2"
|
||||
stage = "qa"
|
||||
role = "Reviews coder work in worktrees: runs quality gates, generates testing plans, and reports findings."
|
||||
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 review the coder's work in the worktree and produce a structured QA report.
|
||||
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
|
||||
|
||||
### 1. Code Quality Scan
|
||||
- Run `git diff master...HEAD --stat` to see what files changed
|
||||
- Run `git diff master...HEAD` to review the actual changes for obvious coding mistakes (unused imports, dead code, unhandled errors, hardcoded values)
|
||||
- Run `cargo clippy --all-targets --all-features` and note any warnings
|
||||
### 0. Read the Story
|
||||
- Read the story file at `.storkit/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. Test Verification
|
||||
- Run `cargo test` and verify all tests pass
|
||||
- If `frontend/` exists: run `npm test` and verify all frontend tests pass
|
||||
- Review test quality: look for tests that are trivial or don't assert meaningful behavior
|
||||
### 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. Manual Testing Support
|
||||
### 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:
|
||||
@@ -90,8 +117,8 @@ Read CLAUDE.md first, then .story_kit/README.md to understand the dev process.
|
||||
- curl commands to exercise relevant API endpoints
|
||||
- Kill the test server when done: `pkill -f 'target.*storkit' || true` (NEVER use `pkill -f storkit` — it kills the vite dev server)
|
||||
|
||||
### 4. Produce Structured Report
|
||||
Print your QA report to stdout before your process exits. The server will automatically run acceptance gates. Use this format:
|
||||
### 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}}
|
||||
@@ -100,27 +127,38 @@ Print your QA report to stdout before your process exits. The server will automa
|
||||
- clippy: PASS/FAIL (details)
|
||||
- TypeScript build: PASS/FAIL/SKIP (details)
|
||||
- Biome lint: PASS/FAIL/SKIP (details)
|
||||
- Code review findings: (list any issues found, or "None")
|
||||
|
||||
### Test Verification
|
||||
- cargo test: PASS/FAIL (N tests)
|
||||
- npm test: PASS/FAIL/SKIP (N tests)
|
||||
- Test quality issues: (list any trivial/weak tests, or "None")
|
||||
- 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 "Build failed")
|
||||
- Pages to visit: (list)
|
||||
- Things to check: (list)
|
||||
- curl commands: (list)
|
||||
- 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
|
||||
- If the server fails to start, still provide the testing plan with curl commands
|
||||
- The server automatically runs acceptance gates when your process exits"""
|
||||
system_prompt = "You are a QA agent. Your job is read-only: review code quality, run tests, try to start the server, and produce a structured QA report. Do not modify code. The server automatically runs acceptance gates when your process exits."
|
||||
- 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"
|
||||
@@ -135,30 +173,52 @@ system_prompt = "You are a senior full-stack engineer working autonomously in a
|
||||
[[agent]]
|
||||
name = "qa"
|
||||
stage = "qa"
|
||||
role = "Reviews coder work in worktrees: runs quality gates, generates testing plans, and reports findings."
|
||||
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 review the coder's work in the worktree and produce a structured QA report.
|
||||
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
|
||||
|
||||
### 1. Code Quality Scan
|
||||
- Run `git diff master...HEAD --stat` to see what files changed
|
||||
- Run `git diff master...HEAD` to review the actual changes for obvious coding mistakes (unused imports, dead code, unhandled errors, hardcoded values)
|
||||
- Run `cargo clippy --all-targets --all-features` and note any warnings
|
||||
### 0. Read the Story
|
||||
- Read the story file at `.storkit/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. Test Verification
|
||||
- Run `cargo test` and verify all tests pass
|
||||
- If `frontend/` exists: run `npm test` and verify all frontend tests pass
|
||||
- Review test quality: look for tests that are trivial or don't assert meaningful behavior
|
||||
### 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. Manual Testing Support
|
||||
### 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:
|
||||
@@ -167,8 +227,8 @@ Read CLAUDE.md first, then .story_kit/README.md to understand the dev process.
|
||||
- curl commands to exercise relevant API endpoints
|
||||
- Kill the test server when done: `pkill -f 'target.*storkit' || true` (NEVER use `pkill -f storkit` — it kills the vite dev server)
|
||||
|
||||
### 4. Produce Structured Report
|
||||
Print your QA report to stdout before your process exits. The server will automatically run acceptance gates. Use this format:
|
||||
### 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}}
|
||||
@@ -177,27 +237,38 @@ Print your QA report to stdout before your process exits. The server will automa
|
||||
- clippy: PASS/FAIL (details)
|
||||
- TypeScript build: PASS/FAIL/SKIP (details)
|
||||
- Biome lint: PASS/FAIL/SKIP (details)
|
||||
- Code review findings: (list any issues found, or "None")
|
||||
|
||||
### Test Verification
|
||||
- cargo test: PASS/FAIL (N tests)
|
||||
- npm test: PASS/FAIL/SKIP (N tests)
|
||||
- Test quality issues: (list any trivial/weak tests, or "None")
|
||||
- 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 "Build failed")
|
||||
- Pages to visit: (list)
|
||||
- Things to check: (list)
|
||||
- curl commands: (list)
|
||||
- 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
|
||||
- If the server fails to start, still provide the testing plan with curl commands
|
||||
- The server automatically runs acceptance gates when your process exits"""
|
||||
system_prompt = "You are a QA agent. Your job is read-only: review code quality, run tests, try to start the server, and produce a structured QA report. Do not modify code. The server automatically runs acceptance gates when your process exits."
|
||||
- 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"
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# Example project.toml — copy to .storkit/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 .storkit/README.md to understand the dev process.
|
||||
Run: cd "{{worktree_path}}" && git difftool {{base_branch}}...HEAD
|
||||
Commit all your work before your process exits.
|
||||
"""
|
||||
@@ -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
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: "Fly.io Machines API integration for multi-tenant storkit SaaS"
|
||||
---
|
||||
|
||||
# Spike 408: Fly.io Machines API integration for multi-tenant storkit 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 storkit 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 storkit 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, storkit 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
|
||||
+24
@@ -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
|
||||
+21
@@ -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
|
||||
+26
@@ -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 \"storkit\" to \"huskies\""
|
||||
---
|
||||
|
||||
# Story 455: Rename project from "storkit" to "huskies"
|
||||
|
||||
## User Story
|
||||
|
||||
As a project maintainer, I want to rename the project from "storkit" to "huskies" so that the product has its new identity throughout the codebase, tooling, and documentation.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Rust crate name in server/Cargo.toml changed from 'storkit' 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 'storkit' to 'huskies' in Dockerfile (groupadd, useradd, home dir /home/huskies/.claude)
|
||||
- [ ] MCP server registration renamed from 'storkit' 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__storkit__* to mcp__huskies__* across code and permission configs
|
||||
- [ ] The .storkit/ 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/storkit → dave/huskies, changelog regex updated to match ^(huskies|storkit|story-kit): for backwards-compatible history parsing, binary artifact names updated
|
||||
- [ ] Git commit prefix convention updated from 'storkit:' to 'huskies:' in storkit README and agent prompts
|
||||
- [ ] Website updated: page title, headings, and contact email (hello@storkit.dev) if domain changes
|
||||
- [ ] README.md updated: all CLI examples use 'huskies' binary name, all .storkit/ references become .huskies/
|
||||
- [ ] A migration path exists for existing installs: either storkit auto-detects and migrates .storkit/ → .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/storkit to dave/huskies (external action required, noted in story)
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
---
|
||||
name: "Zombie process accumulation from unrereaped child processes"
|
||||
---
|
||||
|
||||
# Bug 452: Zombie process accumulation from unrereaped child processes
|
||||
|
||||
## Description
|
||||
|
||||
Storkit accumulates zombie processes over time from unrereaped child and grandchild processes. Observed 101 zombies in Docker container, 27 on macOS host. Breakdown: 51 esbuild, 36 echo, 5 claude, 5 sh, 2 bash, 1 cargo.
|
||||
|
||||
Root cause: storkit does not reap orphaned grandchild processes. The zombies are mostly grandchildren (`esbuild`, `echo`, `sh`, `cargo`) spawned by `npm run build`, `cargo test`, etc. during worktree setup and gate checks. This happens both natively (observed 27 zombies on macOS host) and in Docker containers. When the intermediate parent exits, these grandchildren get reparented to storkit (or PID 1 in Docker) and become zombies because nobody calls `waitpid` for them.
|
||||
|
||||
**Already fixed:**
|
||||
- `docker-compose.yml` now has `init: true` which uses tini as PID 1 in Docker — this handles zombie reaping inside containers
|
||||
- `llm/providers/claude_code.rs` now has `child.wait()` after `child.kill()` in all code paths, and the reader thread is joined before returning
|
||||
- `agents/pty.rs` reader thread is now joined before returning
|
||||
|
||||
**Remaining:** Storkit running natively (e.g. on macOS) still accumulates zombie grandchildren because there is no tini. The fix is to add a background reaper thread that periodically calls `waitpid(-1, WNOHANG)` in a loop to clean up any orphaned children. This should be spawned early in `main()` on Unix platforms. Example:
|
||||
|
||||
```rust
|
||||
#[cfg(unix)]
|
||||
std::thread::spawn(|| {
|
||||
loop {
|
||||
unsafe { while libc::waitpid(-1, std::ptr::null_mut(), libc::WNOHANG) > 0 {} }
|
||||
std::thread::sleep(std::time::Duration::from_secs(5));
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## How to Reproduce
|
||||
|
||||
Run several agent sessions. Check with `ps -eo stat,comm | grep Z | awk '{print $2}' | sort | uniq -c | sort -rn`.
|
||||
|
||||
## Actual Result
|
||||
|
||||
Zombie processes accumulate continuously. Never reaped.
|
||||
|
||||
## Expected Result
|
||||
|
||||
No zombie accumulation during normal operation.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] `child.wait()` is called after `child.kill()` in all code paths in `claude_code.rs`
|
||||
- [x] Reader threads are joined in both `pty.rs` and `claude_code.rs`
|
||||
- [x] `init: true` added to docker-compose.yml for Docker deployments
|
||||
- [ ] Background reaper thread added for native (non-Docker) deployments
|
||||
- [ ] Verified with `ps aux | grep '<defunct>'` after running multiple agent sessions natively on macOS
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: "Deduplicate work item display in web UI story panel"
|
||||
---
|
||||
|
||||
# Story 454: Deduplicate work item display in web UI story panel
|
||||
|
||||
## User Story
|
||||
|
||||
As a user, I want the work item detail panel to display cleanly without redundant information, so that I can read story details without noise.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] The story title is not shown twice (remove the duplicate heading)
|
||||
- [ ] The work item type label is not shown twice
|
||||
- [ ] The word 'name' is not shown as a prefix before the story title
|
||||
- [ ] The story ID/title line (e.g. 'Story 3: ...') is left-justified with no extra indentation
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
---
|
||||
name: "Matrix bot ignores in-room verification requests from Element"
|
||||
---
|
||||
|
||||
# Bug 456: Matrix bot ignores in-room verification requests from Element
|
||||
|
||||
## Description
|
||||
|
||||
The Matrix bot (Sally) only registers a handler for to-device verification events (`ToDeviceKeyVerificationRequestEvent`). Modern Element clients use in-room verification (`m.key.verification.request` as a room message event) by default. When a user initiates "Start Verification" from Element, the request is sent as a room event and the bot never sees it — nothing appears in the bot logs and the verification flow hangs indefinitely. As a result, Sally's device remains unverified (Big Red Dot), and if Element has "never send to unverified sessions" enabled, it will not share Megolm room keys with Sally's device, making her deaf to all encrypted room messages.
|
||||
|
||||
## How to Reproduce
|
||||
|
||||
1. Run the storkit Matrix bot (Sally) in a room with E2EE enabled. 2. In Element, open the room member list, click Sally's device, and press "Start Verification". 3. Watch the bot logs: grep for "verif\|Incoming".
|
||||
|
||||
## Actual Result
|
||||
|
||||
Nothing appears in the bot logs. The verification flow hangs in Element and eventually times out. Sally's device remains unverified. If Element is set to encrypt only to verified sessions, Sally cannot decrypt any messages in the room.
|
||||
|
||||
## Expected Result
|
||||
|
||||
The bot receives the in-room verification request, accepts it, drives the SAS emoji flow to completion, and logs "Verification with @user completed successfully!". Sally's device shows as verified in Element.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Bot registers an in-room verification event handler for m.key.verification.request room events (in addition to the existing to-device handler)
|
||||
- [ ] When Element initiates 'Start Verification' from the device list, the bot logs 'Incoming verification request from ...'
|
||||
- [ ] The SAS emoji flow completes: bot logs the emoji string, confirms, and logs 'Verification ... completed successfully!'
|
||||
- [ ] Sally's device shows as verified (no Big Red Dot) in Element after the flow completes
|
||||
- [ ] Existing to-device verification handler is preserved for clients that use the older flow
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
name: "Work item titles render too large in expanded view"
|
||||
merge_failure: "Merge pipeline infrastructure failure: squash merge committed successfully on merge-queue branch, but cherry-pick onto master failed with 'fatal: bad revision merge-queue/237_bug_work_item_titles_render_too_large_in_expanded_view'. The merge worktree setup also failed (ENOENT for .story_kit/merge_workspace — pnpm install, pnpm build, cargo check all skipped). The merge-queue branch appears to have been cleaned up before the cherry-pick step could reference it. Master is untouched."
|
||||
---
|
||||
|
||||
# Bug 237: Work item titles render too large in expanded view
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
name: "Add refactor work item type"
|
||||
merge_failure: "merge_agent_work tool returned empty output on two attempts. The merge-queue branch (merge-queue/254_story_add_refactor_work_item_type) was created with squash merge commit 27d24b2, and the merge workspace worktree exists at .story_kit/merge_workspace, but the pipeline never completed (no success/failure logged after MERGE-DEBUG calls). The stale merge workspace worktree may be blocking completion. Possibly related to bug 250 (merge pipeline cherry-pick fails with bad revision on merge-queue branch). Human intervention needed to: 1) clean up the merge-queue worktree and branch, 2) investigate why the merge pipeline hangs after creating the squash merge commit, 3) retry the merge."
|
||||
---
|
||||
|
||||
# Story 254: Add refactor work item type
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
name: "Show agent logs in expanded story popup"
|
||||
merge_failure: "merge_agent_work tool returned empty output. The merge pipeline created the merge-queue branch (merge-queue/255_story_show_agent_logs_in_expanded_story_popup) and merge workspace worktree at .story_kit/merge_workspace, but hung without completing. This is the same issue that affected story 254 — likely related to bug 250 (merge pipeline cherry-pick fails with bad revision on merge-queue branch). The stale merge workspace worktree on the merge-queue branch may be blocking completion. Human intervention needed to: 1) clean up the merge workspace worktree and merge-queue branch, 2) investigate the root cause in the merge pipeline (possibly the cherry-pick/fast-forward step after squash merge), 3) retry the merge."
|
||||
---
|
||||
|
||||
# Story 255: Show agent logs in expanded story popup
|
||||
|
||||
+1
@@ -1,5 +1,6 @@
|
||||
---
|
||||
name: "Web UI OAuth flow for Claude authentication"
|
||||
agent: "coder-opus"
|
||||
---
|
||||
|
||||
# Story 368: Web UI OAuth flow for Claude authentication
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: "No-arg storkit in empty directory skips scaffold"
|
||||
---
|
||||
|
||||
# Bug 371: No-arg storkit in empty directory skips scaffold
|
||||
|
||||
## Description
|
||||
|
||||
When running `storkit` with no path argument from an empty directory (no `.storkit/`), the server starts but never calls `open_project` or the scaffold. The `find_story_kit_root` check fails to find `.storkit/`, so the fallback at main.rs:179-186 just sets `project_root = cwd` without scaffolding. This means no `.storkit/`, no `project.toml`, no `.mcp.json`, no `CLAUDE.md` — the project is non-functional.
|
||||
|
||||
The explicit path branch (`storkit .`) works correctly because it calls `open_project` → `ensure_project_root_with_story_kit` → `scaffold_story_kit`. The no-arg branch should do the same.
|
||||
|
||||
## How to Reproduce
|
||||
|
||||
1. Create a new empty directory
|
||||
2. cd into it
|
||||
3. Run `storkit` (no path argument)
|
||||
4. Observe that no scaffold is created — `.storkit/`, `CLAUDE.md`, `.mcp.json`, etc. are all missing
|
||||
|
||||
## Actual Result
|
||||
|
||||
Server starts with project_root set to cwd but no scaffold runs. The project is non-functional — no agent config, no MCP endpoint, no work pipeline directories.
|
||||
|
||||
## Expected Result
|
||||
|
||||
Running `storkit` with no arguments from a directory without `.storkit/` should scaffold the project the same as `storkit .` does — calling `open_project` and triggering `ensure_project_root_with_story_kit`.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Running `storkit` with no args from a dir without `.storkit/` calls `open_project` and triggers the full scaffold
|
||||
- [ ] The no-arg fallback path in main.rs calls `open_project(cwd)` instead of just setting project_root directly
|
||||
- [ ] After `storkit` completes startup, `.storkit/project.toml`, `.mcp.json`, `CLAUDE.md`, and `script/test` all exist
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
---
|
||||
name: "Scaffold auto-detects tech stack and configures script/test"
|
||||
---
|
||||
|
||||
# Story 372: Scaffold auto-detects tech stack and configures script/test
|
||||
|
||||
## User Story
|
||||
|
||||
As a user setting up a new project with storkit, I want the scaffold to detect my project's tech stack and generate a working `script/test` automatically, so that agents can run tests immediately without manual configuration.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Scaffold detects Go projects (go.mod) and adds `go test ./...` to script/test
|
||||
- [ ] Scaffold detects Node.js projects (package.json) and adds `npm test` to script/test
|
||||
- [ ] Scaffold detects Rust projects (Cargo.toml) and adds `cargo test` to script/test
|
||||
- [ ] Scaffold detects Python projects (pyproject.toml or requirements.txt) and adds `pytest` to script/test
|
||||
- [ ] Scaffold handles multi-stack projects (e.g. Go + Next.js) by combining the relevant test commands
|
||||
- [ ] project.toml component entries are generated to match detected tech stack
|
||||
- [ ] Falls back to the generic 'No tests configured' stub if no known stack is detected
|
||||
- [ ] Coder agent prompt includes instruction to configure `script/test` for the project's test framework if it still contains the generic stub
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
---
|
||||
name: "Scaffold gitignore missing transient pipeline stage directories"
|
||||
---
|
||||
|
||||
# Bug 373: Scaffold gitignore missing transient pipeline stage directories
|
||||
|
||||
## Description
|
||||
|
||||
The `write_story_kit_gitignore` function in `server/src/io/fs.rs` does not include the transient pipeline stages (`work/2_current/`, `work/3_qa/`, `work/4_merge/`) in the `.storkit/.gitignore` entries list. These stages are not committed to git (only `1_backlog`, `5_done`, and `6_archived` are commit-worthy per spike 92), so they should be ignored for new projects.
|
||||
|
||||
## How to Reproduce
|
||||
|
||||
1. Scaffold a new project with storkit
|
||||
2. Check `.storkit/.gitignore`
|
||||
|
||||
## Actual Result
|
||||
|
||||
`.storkit/.gitignore` only contains `bot.toml`, `matrix_store/`, `matrix_device_id`, `worktrees/`, `merge_workspace/`, `coverage/`. The transient pipeline directories are missing.
|
||||
|
||||
## Expected Result
|
||||
|
||||
`.storkit/.gitignore` also includes `work/2_current/`, `work/3_qa/`, `work/4_merge/`.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Scaffold writes work/2_current/, work/3_qa/, work/4_merge/ to .storkit/.gitignore
|
||||
- [ ] Idempotent — running scaffold again does not duplicate entries
|
||||
- [ ] Existing .storkit/.gitignore files get the new entries appended on next scaffold run
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
---
|
||||
name: "Web UI implements all bot commands as slash commands"
|
||||
---
|
||||
|
||||
# Story 374: Web UI implements all bot commands as slash commands
|
||||
|
||||
## User Story
|
||||
|
||||
As a user working in the storkit web UI, I want to type slash commands (e.g. `/status`, `/start 42`, `/cost`) in the chat input to trigger the same deterministic bot commands available in Matrix, so that I can manage my project entirely from the browser without needing a chat bot.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] /status — shows pipeline status and agent availability; /status <number> shows story triage dump
|
||||
- [ ] /assign <number> <model> — pre-assign a model to a story
|
||||
- [ ] /start <number> — start a coder on a story; /start <number> opus for specific model
|
||||
- [ ] /show <number> — display full text of a work item
|
||||
- [ ] /move <number> <stage> — move a work item to a pipeline stage
|
||||
- [ ] /delete <number> — remove a work item from the pipeline
|
||||
- [ ] /cost — show token spend (24h total, top stories, by agent type, all-time)
|
||||
- [ ] /git — show git status (branch, uncommitted changes, ahead/behind)
|
||||
- [ ] /overview <number> — show implementation summary for a merged story
|
||||
- [ ] /rebuild — rebuild the server binary and restart
|
||||
- [ ] /reset — clear the current Claude Code session
|
||||
- [ ] /help — list all available slash commands
|
||||
- [ ] Slash commands are handled at the frontend/backend level without LLM invocation
|
||||
- [ ] Unrecognised slash commands show a helpful error message
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: "Default project.toml contains Rust-specific setup commands for non-Rust projects"
|
||||
---
|
||||
|
||||
# Bug 375: Default project.toml contains Rust-specific setup commands for non-Rust projects
|
||||
|
||||
## Description
|
||||
|
||||
When scaffolding a new project where no tech stack is detected, the generated `project.toml` contains Rust-specific setup commands (`cargo check`) as example fallback components. This causes coder agents to try to satisfy Rust gates on non-Rust projects.
|
||||
|
||||
## Fix
|
||||
|
||||
1. In `detect_components_toml()` fallback (when no stack markers found): replace the Rust/pnpm example components with a single generic `app` component with empty `setup = []`
|
||||
2. In the onboarding prompt Step 4: simplify to configure `[[component]]` entries based on what the user told the LLM in Step 2 (tech stack), rather than re-scanning the filesystem independently
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Default project.toml does not contain language-specific setup commands when that language is not detected in the project
|
||||
- [ ] If go.mod is present, setup commands use Go tooling
|
||||
- [ ] If package.json is present, setup commands use npm/node tooling
|
||||
- [ ] If no known stack is detected, setup commands are empty or just echo a placeholder
|
||||
|
||||
## How to Reproduce
|
||||
|
||||
1. Create a new Go + Next.js project directory with `go.mod` and `package.json`
|
||||
2. Run `storkit .` to scaffold
|
||||
3. Check `.storkit/project.toml` — the component setup commands reference cargo/Rust
|
||||
4. Start a coder agent — it creates a `Cargo.toml` trying to satisfy the Rust setup commands
|
||||
|
||||
## Actual Result
|
||||
|
||||
The scaffolded `project.toml` has Rust-specific setup commands (`cargo check`) even for non-Rust projects. Agents try to satisfy these and create spurious files.
|
||||
|
||||
## Expected Result
|
||||
|
||||
The scaffolded `project.toml` should have generic or stack-appropriate setup commands. If no known stack is detected, setup commands should be empty or minimal (not Rust-specific).
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Default project.toml does not contain language-specific setup commands when that language is not detected in the project
|
||||
- [ ] If go.mod is present, setup commands use Go tooling
|
||||
- [ ] If package.json is present, setup commands use npm/node tooling
|
||||
- [ ] If no known stack is detected, setup commands are empty or just echo a placeholder
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: "Rename MCP whatsup tool to status for consistency"
|
||||
agent: coder-opus
|
||||
---
|
||||
|
||||
# Story 376: Rename MCP whatsup tool to status for consistency
|
||||
|
||||
## User Story
|
||||
|
||||
As a developer using storkit's MCP tools, I want the MCP tool to be called `status` instead of `whatsup`, so that the naming is consistent between the bot command (`status`), the web UI slash command (`/status`), and the MCP tool.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] MCP tool is renamed from 'whatsup' to 'status'
|
||||
- [ ] MCP tool is discoverable as 'status' via tools/list
|
||||
- [ ] The tool still accepts a story_id parameter and returns the same triage data
|
||||
- [ ] Old 'whatsup' tool name is removed from the MCP registry
|
||||
- [ ] Any internal references to the whatsup tool name are updated
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
---
|
||||
name: "update_story MCP tool writes front matter values as YAML strings instead of native types"
|
||||
---
|
||||
|
||||
# Bug 377: update_story MCP tool writes front matter values as YAML strings instead of native types
|
||||
|
||||
## Description
|
||||
|
||||
The `update_story` MCP tool accepts `front_matter` as a `Map<String, String>`, so all values are written as quoted YAML strings. Fields like `retry_count` (expected `u32`) and `blocked` (expected `bool`) end up as `"0"` and `"false"` in the YAML. This causes `parse_front_matter()` to fail because serde_yaml cannot deserialize a quoted string into `u32` or `bool`. When parsing fails, the story `name` comes back as `None`, so the status command shows no title for the story.
|
||||
|
||||
## How to Reproduce
|
||||
|
||||
1. Call `update_story` with `front_matter: {"blocked": "false", "retry_count": "0"}`
|
||||
2. Read the story file — front matter contains `blocked: "false"` and `retry_count: "0"` (quoted strings)
|
||||
3. Call `get_pipeline_status` or the bot `status` command
|
||||
4. The story shows with no title/name
|
||||
|
||||
## Actual Result
|
||||
|
||||
Front matter values are written as quoted YAML strings. `parse_front_matter()` fails to deserialize `"false"` as `bool` and `"0"` as `u32`, returning an error. The story name is lost and the status command shows no title.
|
||||
|
||||
## Expected Result
|
||||
|
||||
The `update_story` tool should write `blocked` and `retry_count` as native YAML types (unquoted `false` and `0`), or `parse_front_matter()` should accept both string and native representations. The story name should always be displayed correctly in the status command.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] update_story with front_matter {"blocked": "false"} writes `blocked: false` (unquoted) in the YAML
|
||||
- [ ] update_story with front_matter {"retry_count": "0"} writes `retry_count: 0` (unquoted) in the YAML
|
||||
- [ ] Story name is displayed correctly in the status command after update_story modifies front matter fields
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: "Status command shows work item type (story, bug, spike, refactor) next to each item"
|
||||
---
|
||||
|
||||
# Story 378: Status command shows work item type (story, bug, spike, refactor) next to each item
|
||||
|
||||
## User Story
|
||||
|
||||
As a user viewing the pipeline status, I want to see the type of each work item (story, bug, spike, refactor) so that I can quickly understand what kind of work is in progress without having to open individual files.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] The status command displays the work item type (story, bug, spike, refactor) as a label next to each item — e.g. "375 [bug] — Default project.toml contains Rust-specific setup commands"
|
||||
- [ ] The type is extracted from the story_id filename convention ({id}_{type}_{slug})
|
||||
- [ ] All known types are supported: story, bug, spike, refactor
|
||||
- [ ] Unknown or missing types are omitted gracefully (no crash, no placeholder)
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
---
|
||||
name: "start_agent ignores story front matter agent assignment"
|
||||
---
|
||||
|
||||
# Bug 379: start_agent ignores story front matter agent assignment
|
||||
|
||||
## Description
|
||||
|
||||
When a model is pre-assigned to a story via the `assign` command (which writes `agent: coder-opus` to the story's YAML front matter), the MCP `start_agent` tool ignores this field. It only looks at the `agent_name` argument passed directly in the tool call. If none is passed, it auto-selects the first idle coder (usually sonnet), bypassing the user's assignment.
|
||||
|
||||
The auto-assign pipeline (`auto_assign.rs`) correctly reads and respects the front matter `agent` field, but the direct `tool_start_agent` path in `agent_tools.rs` does not.
|
||||
|
||||
Additionally, the `show` (whatsup/triage) command should display the assigned agent from the story's front matter so users can verify their assignment took effect.
|
||||
|
||||
## How to Reproduce
|
||||
|
||||
1. Run `assign 368 opus` — this writes `agent: coder-opus` to story 368's front matter
|
||||
2. Run `start 368` (without specifying a model)
|
||||
3. Observe that a sonnet coder is assigned, not coder-opus
|
||||
4. Run `show 368` — the assigned agent is not displayed
|
||||
|
||||
## Actual Result
|
||||
|
||||
The `start_agent` MCP tool ignores the `agent` field in the story's front matter and picks the first idle coder. The `show` command does not display the pre-assigned agent.
|
||||
|
||||
## Expected Result
|
||||
|
||||
When no explicit `agent_name` is passed to `start_agent`, it should read the story's front matter `agent` field and use that agent if it's available. The `show` command should display the assigned agent from front matter.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] start_agent without an explicit agent_name reads the story's front matter `agent` field and uses it if the agent is idle
|
||||
- [ ] If the preferred agent from front matter is busy, start_agent either waits or falls back to auto-selection (matching auto_assign behavior)
|
||||
- [ ] The show/triage command displays the assigned agent from story front matter when present
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: "Assign command restarts coder when story is already in progress"
|
||||
---
|
||||
|
||||
# Story 380: Assign command restarts coder when story is already in progress
|
||||
|
||||
## User Story
|
||||
|
||||
As a user, I want `assign X opus` on a running story to stop the current coder, update the front matter, and start the newly assigned agent, so that I can switch models mid-flight without manually stopping and restarting.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] When assign is called on a story with a running coder, the current coder agent is stopped
|
||||
- [ ] The story's front matter `agent` field is updated to the new agent name
|
||||
- [ ] The newly assigned agent is started on the story automatically
|
||||
- [ ] When assign is called on a story with no running coder, it behaves as before (just updates front matter)
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: "Bot command to delete a worktree"
|
||||
---
|
||||
|
||||
# Story 381: Bot command to delete a worktree
|
||||
|
||||
## User Story
|
||||
|
||||
As a user, I want a bot command to delete a worktree so that I can clean up orphaned or unwanted worktrees without SSHing into the server.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] A new bot command (e.g. `rmtree <story_number>`) deletes the worktree for the given story
|
||||
- [ ] The command stops any running agent on that story before removing the worktree
|
||||
- [ ] The command returns a confirmation message on success
|
||||
- [ ] The command returns a helpful error if no worktree exists for the given story
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: "WhatsApp transport supports Twilio API as alternative to Meta Cloud API"
|
||||
---
|
||||
|
||||
# Story 382: WhatsApp transport supports Twilio API as alternative to Meta Cloud API
|
||||
|
||||
## User Story
|
||||
|
||||
As a user, I want to use Twilio's WhatsApp API instead of Meta's Cloud API directly, so that I can avoid Meta's painful developer onboarding and use Twilio's simpler signup process.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] bot.toml supports a `whatsapp_provider` field with values `meta` (default, current behavior) or `twilio`
|
||||
- [ ] When provider is `twilio`, messages are sent via Twilio's REST API (`api.twilio.com`) using Account SID + Auth Token
|
||||
- [ ] When provider is `twilio`, inbound webhooks parse Twilio's form-encoded format instead of Meta's JSON
|
||||
- [ ] Twilio config requires `twilio_account_sid`, `twilio_auth_token`, and `twilio_whatsapp_number` in bot.toml
|
||||
- [ ] All existing bot commands and LLM passthrough work identically regardless of provider
|
||||
- [ ] 24-hour messaging window logic still applies (Twilio enforces this server-side too)
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
---
|
||||
name: "Reorganize chat system into chat module with transport submodules"
|
||||
---
|
||||
|
||||
# Refactor 383: Reorganize chat system into chat module with transport submodules
|
||||
|
||||
## Current State
|
||||
|
||||
- TBD
|
||||
|
||||
## Desired State
|
||||
|
||||
Currently chat-related code is scattered at the top level of `src/`: `transport.rs`, `whatsapp.rs`, `slack.rs`, plus `matrix/` as a directory module. This should be reorganized into a clean module hierarchy:
|
||||
|
||||
```
|
||||
src/
|
||||
chat/
|
||||
mod.rs # Generic chat traits, types, ChatTransport etc.
|
||||
transport/
|
||||
mod.rs
|
||||
matrix/ # Existing matrix module moved here
|
||||
whatsapp.rs # Existing whatsapp.rs moved here
|
||||
slack.rs # Existing slack.rs moved here
|
||||
twilio.rs # Future Twilio transport
|
||||
```
|
||||
|
||||
The `ChatTransport` trait and shared chat types should live in `chat/mod.rs`. Each transport implementation becomes a submodule of `chat::transport`.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] ChatTransport trait and shared chat types live in `chat/mod.rs`
|
||||
- [ ] Matrix transport lives in `chat/transport/matrix/`
|
||||
- [ ] WhatsApp transport lives in `chat/transport/whatsapp.rs`
|
||||
- [ ] Slack transport lives in `chat/transport/slack.rs`
|
||||
- [ ] Top-level `transport.rs`, `whatsapp.rs`, `slack.rs`, and `matrix/` are removed
|
||||
- [ ] All existing tests pass without modification (or with only import path changes)
|
||||
- [ ] No functional changes — pure file reorganization and re-exports
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: "WhatsApp markdown-to-WhatsApp formatting conversion"
|
||||
---
|
||||
|
||||
# Story 384: WhatsApp markdown-to-WhatsApp formatting conversion
|
||||
|
||||
## User Story
|
||||
|
||||
As a WhatsApp user, I want bot messages to use WhatsApp-native formatting instead of raw markdown, so that headers, bold text, and links render properly.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Headers (# ## ### etc.) are converted to bold text (*Header*) in WhatsApp messages
|
||||
- [ ] Markdown bold (**text**) is converted to WhatsApp bold (*text*)
|
||||
- [ ] Markdown strikethrough (~~text~~) is converted to WhatsApp strikethrough (~text~)
|
||||
- [ ] Markdown links [text](url) are converted to readable format: text (url)
|
||||
- [ ] Code blocks and inline code are preserved as-is (already compatible)
|
||||
- [ ] Matrix bot formatting is completely unaffected (conversion only applied in WhatsApp send paths)
|
||||
- [ ] Existing WhatsApp chunking (4096 char limit) still works correctly after conversion
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: "Slack markdown-to-mrkdwn formatting conversion"
|
||||
---
|
||||
|
||||
# Story 385: Slack markdown-to-mrkdwn formatting conversion
|
||||
|
||||
## User Story
|
||||
|
||||
As a Slack user, I want bot messages to use Slack-native mrkdwn formatting instead of raw markdown, so that headers, bold text, and links render properly.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Headers (# ## ### etc.) are converted to bold text (*Header*) in Slack messages
|
||||
- [ ] Markdown bold (**text**) is converted to Slack bold (*text*)
|
||||
- [ ] Markdown strikethrough (~~text~~) is converted to Slack strikethrough (~text~)
|
||||
- [ ] Markdown links [text](url) are converted to Slack format: <url|text>
|
||||
- [ ] Code blocks and inline code are preserved as-is (already compatible)
|
||||
- [ ] WhatsApp and Matrix bot formatting are completely unaffected (conversion only applied in Slack send paths)
|
||||
- [ ] Conversion is applied to all Slack send paths: command responses, LLM streaming, htop snapshots, delete responses, and slash command responses
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: "Unreleased command shows list of stories since last release"
|
||||
---
|
||||
|
||||
# Story 386: Unreleased command shows list of stories since last release
|
||||
|
||||
## User Story
|
||||
|
||||
As a user, I want a bot command and web UI slash command called "unreleased" that shows a list of stories completed since the last release, so that I can see what's ready to ship.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Bot command `unreleased` returns a list of stories merged to master since the last release tag
|
||||
- [ ] Web UI slash command /unreleased returns the same list
|
||||
- [ ] Each entry shows story number and name
|
||||
- [ ] If there are no unreleased stories, a clear message is shown
|
||||
- [ ] Command is registered in the help command output
|
||||
- [ ] WhatsApp, Slack, and Matrix transports all support the command via the shared command dispatcher
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: "Configurable base branch name in project.toml"
|
||||
---
|
||||
|
||||
# Story 387: Configurable base branch name in project.toml
|
||||
|
||||
## User Story
|
||||
|
||||
As a project owner, I want to configure the main branch name in project.toml (e.g. "main", "master", "develop"), so that the system doesn't hardcode "master" and works with any branching convention.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] New optional `base_branch` setting in project.toml (e.g. base_branch = "main")
|
||||
- [ ] When set, all worktree creation, merge operations, and agent prompts use the configured branch name
|
||||
- [ ] When not set, falls back to the existing auto-detection logic (detect_base_branch) which reads the current git branch
|
||||
- [ ] The hardcoded "master" fallback in detect_base_branch is replaced by the project.toml setting when available
|
||||
- [ ] Agent prompt template {{base_branch}} resolves to the configured value
|
||||
- [ ] Existing projects without the setting continue to work unchanged (backwards compatible)
|
||||
- [ ] project.toml.example uses base_branch = \"main\" as the example value; the actual project.toml uses base_branch = \"master\"
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
name: "WhatsApp phone number allowlist authorization"
|
||||
---
|
||||
|
||||
# Story 389: WhatsApp phone number allowlist authorization
|
||||
|
||||
## User Story
|
||||
|
||||
As a bot operator, I want to restrict which phone numbers can interact with the bot, so that only authorized users can send commands.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] New optional allowed_phones list in bot.toml for WhatsApp (similar to Matrix allowed_users)
|
||||
- [ ] When configured, only messages from listed phone numbers are processed; all others are silently ignored
|
||||
- [ ] When not configured (empty or absent), all phone numbers are allowed (backwards compatible)
|
||||
- [ ] Unauthorized senders are logged but receive no response
|
||||
- [ ] The allowlist applies to all message types: commands, LLM conversations, and async commands (htop, delete)
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
---
|
||||
name: "WhatsApp missing async command handlers for start, rebuild, reset, rmtree, assign"
|
||||
---
|
||||
|
||||
# Bug 390: WhatsApp missing async command handlers for start, rebuild, reset, rmtree, assign
|
||||
|
||||
## Description
|
||||
|
||||
Five bot commands listed in help don't work in WhatsApp. Matrix's on_room_message pre-dispatches these via extract_*_command() functions before calling try_handle_command(), but WhatsApp's handle_incoming_message only pre-dispatches htop and delete. The missing commands have fallback handlers that return None, so they silently fall through to the LLM instead of executing.
|
||||
|
||||
## How to Reproduce
|
||||
|
||||
1. Send "rebuild" (or "start 386", "reset", "rmtree 386", "assign 386 opus") to the WhatsApp bot\n2. Observe the message is forwarded to the LLM instead of executing the command
|
||||
|
||||
## Actual Result
|
||||
|
||||
The 5 commands (start, rebuild, reset, rmtree, assign) fall through to the LLM and generate a conversational response instead of executing the bot command.
|
||||
|
||||
## Expected Result
|
||||
|
||||
All commands listed in help should work in WhatsApp, matching Matrix behavior. start should spawn an agent, rebuild should rebuild the server, reset should clear the session, rmtree should remove a worktree, assign should pre-assign a model.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] start command works in WhatsApp (extract_start_command dispatch)
|
||||
- [ ] rebuild command works in WhatsApp (extract_rebuild_command dispatch)
|
||||
- [ ] reset command works in WhatsApp (extract_reset_command dispatch)
|
||||
- [ ] rmtree command works in WhatsApp (extract_rmtree_command dispatch)
|
||||
- [ ] assign command works in WhatsApp (extract_assign_command dispatch)
|
||||
- [ ] Same 5 commands also work in Slack transport if similarly missing
|
||||
- [ ] RETRY: Previous attempt was marked done without any code changes — the mergemaster moved the story to done but no async command handlers were actually added to whatsapp.rs. The fix must add extract_start_command, extract_rebuild_command, extract_reset_command, extract_rmtree_command, and extract_assign_command dispatch blocks to handle_incoming_message in whatsapp.rs, following the existing pattern used for htop and delete. Also check and fix Slack if similarly missing.
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: "strip_prefix_ci panics on multi-byte UTF-8 characters"
|
||||
---
|
||||
|
||||
# Bug 391: strip_prefix_ci panics on multi-byte UTF-8 characters
|
||||
|
||||
## Description
|
||||
|
||||
strip_prefix_ci in commands/mod.rs slices text by byte offset using prefix.len(), which panics when the slice boundary falls inside a multi-byte UTF-8 character (e.g. right single quote U+2019, emojis). The function assumes ASCII-safe byte boundaries but real WhatsApp/Matrix messages contain Unicode.
|
||||
|
||||
## How to Reproduce
|
||||
|
||||
1. Send a message to the bot containing a smart quote or emoji within the first N bytes (where N = bot name length)\n2. e.g. "For now let\u2019s just deal with it" where the bot name prefix check slices at byte 12, inside the 3-byte \u2019 character
|
||||
|
||||
## Actual Result
|
||||
|
||||
Thread panics: "byte index 12 is not a char boundary; it is inside \u2018\u2019\u2019 (bytes 11..14)"
|
||||
|
||||
## Expected Result
|
||||
|
||||
The function should safely handle multi-byte UTF-8 without panicking. If the slice boundary isn't a char boundary, the prefix doesn't match — return None.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] strip_prefix_ci does not panic on messages containing multi-byte UTF-8 characters (smart quotes, emojis, CJK, etc.)
|
||||
- [ ] Use text.get(..prefix.len()) or text.is_char_boundary() instead of direct indexing
|
||||
- [ ] Add test cases for messages with emojis and smart quotes
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: "Extract shared transport utilities from matrix module into chat submodule"
|
||||
agent: "coder-opus"
|
||||
---
|
||||
|
||||
# Refactor 392: Extract shared transport utilities from matrix module into chat submodule
|
||||
|
||||
## Current State
|
||||
|
||||
- TBD
|
||||
|
||||
## Desired State
|
||||
|
||||
Several functions currently living in the matrix transport module are used by all transports (WhatsApp, Slack, Matrix). These should be pulled up into a shared location under the chat module. Candidates include: strip_prefix_ci, strip_bot_mention, try_handle_command, drain_complete_paragraphs, markdown_to_whatsapp (pattern could generalize), chunk_for_whatsapp, and the command dispatch infrastructure. A chat::util or chat::text submodule would be a natural home for string utilities like strip_prefix_ci. The command dispatch (try_handle_command, CommandDispatch, BotCommand registry) could live in chat::commands.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Shared string utilities (strip_prefix_ci, strip_bot_mention, drain_complete_paragraphs) moved to a chat::util or chat::text submodule
|
||||
- [ ] Command dispatch infrastructure (try_handle_command, CommandDispatch, BotCommand, command registry) moved to chat::commands
|
||||
- [ ] Per-transport formatting functions (markdown_to_whatsapp, markdown_to_slack) remain in their respective transport modules
|
||||
- [ ] All transports import from the new shared location instead of reaching into matrix::
|
||||
- [ ] No functional changes — purely structural refactor
|
||||
- [ ] All existing tests pass and move with their code
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: "Pipeline stage notifications for WhatsApp and Slack transports"
|
||||
---
|
||||
|
||||
# Story 393: Pipeline stage notifications for WhatsApp and Slack transports
|
||||
|
||||
## User Story
|
||||
|
||||
As a WhatsApp or Slack user, I want to receive pipeline stage transition notifications (e.g. "story moved from Current to QA") just like Matrix users do, so I can track story progress from any transport.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] WhatsApp transport spawns a notification listener at startup using the existing spawn_notification_listener infrastructure
|
||||
- [ ] Slack transport spawns a notification listener at startup using the same infrastructure
|
||||
- [ ] Notifications are sent to all active ambient senders/channels for the respective transport
|
||||
- [ ] Stage transition notifications (story moved between pipeline stages) are delivered
|
||||
- [ ] Error notifications (story failures) are delivered
|
||||
- [ ] Rate limit warnings are delivered with debouncing
|
||||
- [ ] Matrix notification behavior is completely unaffected
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: "WhatsApp and Slack permission prompt forwarding"
|
||||
---
|
||||
|
||||
# Story 394: WhatsApp and Slack permission prompt forwarding
|
||||
|
||||
## User Story
|
||||
|
||||
As a WhatsApp or Slack user, I want permission requests from Claude Code to be forwarded to my chat so I can approve or deny them, rather than having them silently fail.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Permission requests are sent as messages to the WhatsApp sender with tool name and input details
|
||||
- [ ] User can reply yes/y/approve or no/n/deny to approve or deny the permission
|
||||
- [ ] Permission requests time out and auto-deny (fail-closed) if not answered within the configured timeout
|
||||
- [ ] Slack receives the same permission forwarding treatment
|
||||
- [ ] Reuses the existing permission channel infrastructure (perm_rx, PermissionForward, PermissionDecision)
|
||||
- [ ] Matrix permission handling is completely unaffected
|
||||
- [ ] handle_llm_message uses a tokio::select! loop (like Matrix bot.rs) to listen for both LLM output and permission requests concurrently
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
name: "Fix npm deprecated module warnings"
|
||||
---
|
||||
|
||||
# Refactor 395: Fix npm deprecated module warnings
|
||||
|
||||
## Current State
|
||||
|
||||
- TBD
|
||||
|
||||
## Desired State
|
||||
|
||||
Address npm warnings about deprecated modules in the frontend dependencies. Update or replace deprecated packages to eliminate warnings during npm install.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] npm install runs with zero deprecation warnings
|
||||
- [ ] All existing frontend tests (npm test) still pass
|
||||
- [ ] npm run build succeeds without errors
|
||||
- [ ] No functional regressions in the frontend
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
name: "WhatsApp bot startup announcement after restart"
|
||||
---
|
||||
|
||||
# Story 396: WhatsApp bot startup announcement after restart
|
||||
|
||||
## User Story
|
||||
|
||||
As a WhatsApp user, I want the bot to announce its presence when it starts up or restarts, like it does in Matrix, so I know it's back online and ready.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Bot sends a startup message to all known WhatsApp senders (from conversation history or ambient rooms) when the server starts
|
||||
- [ ] Startup message includes the bot name and indicates it is online/ready
|
||||
- [ ] Slack transport gets the same startup announcement treatment
|
||||
- [ ] Matrix startup announcement behavior is unaffected
|
||||
- [ ] After a rebuild command, the new process sends the announcement on startup
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
---
|
||||
name: "Selection screen directory picker unreadable in dark mode"
|
||||
---
|
||||
|
||||
# Bug 397: Selection screen directory picker unreadable in dark mode
|
||||
|
||||
## Description
|
||||
|
||||
The ProjectPathInput component in the selection screen uses hardcoded light-theme inline styles (white backgrounds, dark borders, dark text highlights) that don't adapt to dark mode. When the browser/OS uses dark mode, the global CSS sets text color to #f6f6f6 (white) but the dropdown keeps background: #fff — resulting in white text on a white background, making the directory picker completely unreadable.
|
||||
|
||||
## How to Reproduce
|
||||
|
||||
1. Run storkit under Docker (or locally) with a browser set to dark mode (prefers-color-scheme: dark).
|
||||
2. Open http://localhost:3001 in the browser.
|
||||
3. Click into the project path input and start typing a path to trigger the autocomplete dropdown.
|
||||
|
||||
## Actual Result
|
||||
|
||||
The suggestion dropdown has white background with white/light text inherited from the dark-mode global styles. Match highlights use color: #222 which is barely visible. The close button and header bar also use light-only colors. The entire directory picker is effectively unreadable.
|
||||
|
||||
## Expected Result
|
||||
|
||||
The directory picker dropdown should be readable in both light and dark mode. Colors for background, text, borders, and highlights should adapt to the active color scheme.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] ProjectPathInput dropdown is readable in dark mode (prefers-color-scheme: dark)
|
||||
- [ ] ProjectPathInput dropdown remains readable in light mode
|
||||
- [ ] Suggestion highlight text is visible against the dropdown background in both themes
|
||||
- [ ] No hardcoded light-only colors remain in ProjectPathInput inline styles
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
name: "CLI --port flag with project.toml persistence"
|
||||
---
|
||||
|
||||
# Story 399: CLI --port flag with project.toml persistence
|
||||
|
||||
## User Story
|
||||
|
||||
As a developer, I want to set the server port via a --port CLI flag that persists to project.toml, so that I don't have to remember an environment variable on every run.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `storkit --help` shows a `--port` option
|
||||
- [ ] `storkit --port 4000` starts the server on port 4000
|
||||
- [ ] After first run with `--port`, the port is saved to `project.toml`
|
||||
- [ ] On subsequent runs without `--port`, the port from `project.toml` is used
|
||||
- [ ] CLI `--port` overrides the value in `project.toml`
|
||||
- [ ] Default port is 3001 when neither `--port` nor `project.toml` port is set
|
||||
- [ ] `STORKIT_PORT` env var is removed — no longer read or respected
|
||||
- [ ] `.storkit_port` lock file mechanism is removed (`write_port_file` / `remove_port_file`)
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Docker compose changes (can update `STORKIT_PORT` references separately)
|
||||
- Adding other CLI flags beyond `--port`
|
||||
|
||||
## Technical Notes
|
||||
|
||||
Port resolution priority: `--port` flag > `project.toml` `port` field > default 3001
|
||||
|
||||
The port should be written to `project.toml` on startup so subsequent runs remember it. Use the existing `config.rs` / `ProjectConfig` struct — add a `port` field.
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
name: "WhatsApp and Slack missing reset command handler"
|
||||
---
|
||||
|
||||
# Bug 400: WhatsApp and Slack missing reset command handler
|
||||
|
||||
## Description
|
||||
|
||||
The reset command has a fallback handler in chat/commands/mod.rs that returns None with a comment saying it's handled before try_handle_command. This is only true for Matrix. WhatsApp and Slack don't have pre-dispatch handling, so None causes fallthrough to LLM. This caused a real outage when stale session IDs couldn't be cleared via the bot after switching from Docker to bare-metal.
|
||||
|
||||
## Implementation Note
|
||||
|
||||
Follow the **rebuild pattern** established in story 402, with one complication: `handle_reset` in `server/src/chat/transport/matrix/reset.rs` takes a Matrix-specific `ConversationHistory` (`Arc<TokioMutex<HashMap<OwnedRoomId, RoomConversation>>>`), so it cannot be called directly from WhatsApp or Slack.
|
||||
|
||||
**WhatsApp session storage** (`server/src/chat/transport/whatsapp.rs`):
|
||||
- Type: `WhatsAppConversationHistory = Arc<TokioMutex<HashMap<String, RoomConversation>>>` (key = sender phone number)
|
||||
- Persisted to `.storkit/whatsapp_history.json` via `save_whatsapp_history`
|
||||
|
||||
**Slack session storage** (`server/src/chat/transport/slack.rs`):
|
||||
- Type: `SlackConversationHistory = Arc<TokioMutex<HashMap<String, RoomConversation>>>` (key = channel ID)
|
||||
- Persisted to `.storkit/slack_history.json` via `save_slack_history`
|
||||
|
||||
**Approach:**
|
||||
- Use `extract_reset_command` from `server/src/chat/transport/matrix/reset.rs` to detect the command (it works transport-agnostically)
|
||||
- Implement the reset inline in each transport's async message handler: clear `session_id` and `entries` for the sender/channel key, call the transport's own `save_*_history`, reply with confirmation
|
||||
- Add async intercepts in `whatsapp.rs` (~line 1107, after the rebuild intercept) and `slack.rs` (~line 845, after the rebuild intercept)
|
||||
- The fallback handler in `chat/commands/mod.rs` (`handle_reset_fallback`) stays as-is
|
||||
|
||||
## How to Reproduce
|
||||
|
||||
1. Configure bot with transport = "whatsapp" or "slack"\n2. Send "reset" to the bot\n3. Check server logs
|
||||
|
||||
## Actual Result
|
||||
|
||||
Log shows "No command matched, forwarding to LLM" — reset is sent to the LLM as a conversational message instead of clearing the session.
|
||||
|
||||
## Expected Result
|
||||
|
||||
The bot clears the sender's session_id from conversation history and replies with confirmation like "Session cleared."
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] WhatsApp transport handles reset command: clears sender session_id and replies with confirmation
|
||||
- [ ] Slack transport handles reset command: clears channel session_id and replies with confirmation
|
||||
- [ ] Fallback handler in chat/commands/mod.rs no longer silently swallows the reset command
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: "WhatsApp and Slack missing start command handler"
|
||||
---
|
||||
|
||||
# Bug 401: WhatsApp and Slack missing start command handler
|
||||
|
||||
## Description
|
||||
|
||||
The start command has a fallback handler in chat/commands/mod.rs that returns None. Only Matrix has pre-dispatch handling for this command. On WhatsApp and Slack, the command falls through to the LLM path.
|
||||
|
||||
## Implementation Note
|
||||
|
||||
Follow the **rebuild pattern** established in story 402.
|
||||
|
||||
- `extract_start_command` and `handle_start` already exist in `server/src/chat/transport/matrix/start.rs`
|
||||
- Add an async intercept in `server/src/chat/transport/whatsapp.rs` (see rebuild intercept ~line 1107) and `server/src/chat/transport/slack.rs` (see rebuild intercept ~line 845)
|
||||
- Call `crate::chat::transport::matrix::start::extract_start_command` to detect the command, then `crate::chat::transport::matrix::start::handle_start` to execute it
|
||||
- The fallback handler in `chat/commands/mod.rs` (`handle_start_fallback`) stays as-is — it exists only so `help` lists the command
|
||||
|
||||
## How to Reproduce
|
||||
|
||||
1. Configure bot with transport = "whatsapp" or "slack"\n2. Send "start <story_id>" to the bot\n3. Check server logs
|
||||
|
||||
## Actual Result
|
||||
|
||||
Command falls through to LLM instead of starting an agent.
|
||||
|
||||
## Expected Result
|
||||
|
||||
The bot starts an agent for the specified story and replies with confirmation.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] WhatsApp transport handles start command: starts agent and replies with confirmation
|
||||
- [ ] Slack transport handles start command: starts agent and replies with confirmation
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
---
|
||||
name: "WhatsApp and Slack missing rebuild command handler"
|
||||
---
|
||||
|
||||
# Bug 402: WhatsApp and Slack missing rebuild command handler
|
||||
|
||||
## Description
|
||||
|
||||
The rebuild command has a fallback handler in chat/commands/mod.rs that returns None. Only Matrix has pre-dispatch handling for this command. On WhatsApp and Slack, the command falls through to the LLM path.
|
||||
|
||||
## How to Reproduce
|
||||
|
||||
1. Configure bot with transport = "whatsapp" or "slack"\n2. Send "rebuild" to the bot\n3. Check server logs
|
||||
|
||||
## Actual Result
|
||||
|
||||
Command falls through to LLM instead of triggering a server rebuild.
|
||||
|
||||
## Expected Result
|
||||
|
||||
The bot triggers a server rebuild and replies with confirmation.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] WhatsApp transport handles rebuild command: triggers rebuild and replies with confirmation
|
||||
- [ ] Slack transport handles rebuild command: triggers rebuild and replies with confirmation
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
name: "WhatsApp and Slack missing rmtree command handler"
|
||||
retry_count: 2
|
||||
blocked: true
|
||||
---
|
||||
|
||||
# Bug 403: WhatsApp and Slack missing rmtree command handler
|
||||
|
||||
## Description
|
||||
|
||||
The rmtree command has a fallback handler in chat/commands/mod.rs that returns None. Only Matrix has pre-dispatch handling for this command. On WhatsApp and Slack, the command falls through to the LLM path.
|
||||
|
||||
## Implementation Note
|
||||
|
||||
Follow the **rebuild pattern** established in story 402.
|
||||
|
||||
- `extract_rmtree_command` and `handle_rmtree` already exist in `server/src/chat/transport/matrix/rmtree.rs`
|
||||
- Add an async intercept in `server/src/chat/transport/whatsapp.rs` (see rebuild intercept ~line 1107) and `server/src/chat/transport/slack.rs` (see rebuild intercept ~line 845)
|
||||
- Call `crate::chat::transport::matrix::rmtree::extract_rmtree_command` to detect the command, then `crate::chat::transport::matrix::rmtree::handle_rmtree` to execute it
|
||||
- The fallback handler in `chat/commands/mod.rs` (`handle_rmtree_fallback`) stays as-is — it exists only so `help` lists the command
|
||||
|
||||
## How to Reproduce
|
||||
|
||||
1. Configure bot with transport = "whatsapp" or "slack"\n2. Send "rmtree <story_id>" to the bot\n3. Check server logs
|
||||
|
||||
## Actual Result
|
||||
|
||||
Command falls through to LLM instead of removing the worktree.
|
||||
|
||||
## Expected Result
|
||||
|
||||
The bot removes the worktree for the specified story and replies with confirmation.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] WhatsApp transport handles rmtree command: removes worktree and replies with confirmation
|
||||
- [ ] Slack transport handles rmtree command: removes worktree and replies with confirmation
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
name: "WhatsApp and Slack missing assign command handler"
|
||||
---
|
||||
|
||||
# Bug 404: WhatsApp and Slack missing assign command handler
|
||||
|
||||
## Description
|
||||
|
||||
The assign command has a fallback handler in chat/commands/mod.rs that returns None. Only Matrix has pre-dispatch handling for this command. On WhatsApp and Slack, the command falls through to the LLM path.
|
||||
|
||||
## Implementation Note
|
||||
|
||||
Follow the **rebuild pattern** established in story 402.
|
||||
|
||||
- `extract_assign_command` and `handle_assign` already exist in `server/src/chat/transport/matrix/assign.rs`
|
||||
- Add an async intercept in `server/src/chat/transport/whatsapp.rs` (see rebuild intercept ~line 1107) and `server/src/chat/transport/slack.rs` (see rebuild intercept ~line 845)
|
||||
- Call `crate::chat::transport::matrix::assign::extract_assign_command` to detect the command, then `crate::chat::transport::matrix::assign::handle_assign` to execute it
|
||||
- The fallback handler in `chat/commands/mod.rs` (`handle_assign_fallback` — note: the registry entry for `assign` currently calls `assign::handle_assign` synchronously; verify this doesn't conflict) stays as-is for `help` listing
|
||||
- The fallback in `chat/commands/assign.rs` may need to return `None` instead of a real response once the async path handles it
|
||||
|
||||
## How to Reproduce
|
||||
|
||||
1. Configure bot with transport = "whatsapp" or "slack"\n2. Send "assign <story_id> <agent>" to the bot\n3. Check server logs
|
||||
|
||||
## Actual Result
|
||||
|
||||
Command falls through to LLM instead of assigning the agent.
|
||||
|
||||
## Expected Result
|
||||
|
||||
The bot assigns the specified agent to the story and replies with confirmation.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] WhatsApp transport handles assign command: assigns agent and replies with confirmation
|
||||
- [ ] Slack transport handles assign command: assigns agent and replies with confirmation
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
---
|
||||
name: "Auto-refresh expired OAuth token for Claude Code PTY"
|
||||
---
|
||||
|
||||
# Story 405: Auto-refresh expired OAuth token for Claude Code PTY
|
||||
|
||||
## User Story
|
||||
|
||||
As a storkit user with a Claude Max subscription, I want the server to automatically refresh my expired OAuth token so that chat, Matrix, and WhatsApp integrations don't stop working when the token expires.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
### Detection
|
||||
- [ ] When the Claude Code PTY returns an `authentication_failed` error, storkit detects it instead of passing the raw 401 JSON to the user
|
||||
|
||||
### Auto-refresh (credentials exist, refresh token valid)
|
||||
- [ ] Storkit reads the OAuth refresh token from `~/.claude/.credentials.json`
|
||||
- [ ] Storkit calls the Anthropic OAuth token refresh endpoint (`https://console.anthropic.com/v1/oauth/token` with `grant_type=refresh_token`) to obtain a new access token
|
||||
- [ ] Storkit writes the refreshed access token (and new expiresAt) back to `~/.claude/.credentials.json`
|
||||
- [ ] After a successful refresh, storkit automatically retries the original chat request
|
||||
- [ ] The refresh+retry is transparent to the user — they see no error
|
||||
|
||||
### Full login required (no credentials, or refresh token also expired)
|
||||
- [ ] If `.credentials.json` doesn't exist or the refresh call itself fails, storkit surfaces a clear error: "OAuth session expired. Please run `claude login` to re-authenticate."
|
||||
- [ ] The error message is surfaced through the normal chat stream (not just server logs)
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Implementing the full interactive `claude login` browser OAuth flow inside storkit
|
||||
- Proactive token refresh before expiry (refreshing on demand when the error occurs is sufficient)
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
---
|
||||
name: "Browser-based OAuth login flow from web UI and chat integrations"
|
||||
---
|
||||
|
||||
# Story 406: Browser-based OAuth login flow from web UI and chat integrations
|
||||
|
||||
## User Story
|
||||
|
||||
As a new storkit user (or one whose refresh token has expired), I want to complete the full Claude OAuth login flow from the web UI, Matrix, or WhatsApp so that I don't need terminal access to run `claude login`.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] From the web UI, the user can initiate OAuth login — storkit generates the Anthropic authorize URL and opens it in a new tab
|
||||
- [ ] After the user authenticates in the browser, the OAuth callback writes accessToken, refreshToken, and expiresAt to ~/.claude/.credentials.json
|
||||
- [ ] From Matrix or WhatsApp, storkit sends the user a clickable OAuth authorize link when credentials are missing or fully expired
|
||||
- [ ] After successful login, the user can immediately start chatting without restarting storkit
|
||||
- [ ] If the OAuth callback fails or the user cancels, a clear error is shown
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
@@ -0,0 +1,195 @@
|
||||
---
|
||||
name: "Fly.io Machines for multi-tenant storkit SaaS — docs, security & pricing"
|
||||
retry_count: 2
|
||||
blocked: true
|
||||
---
|
||||
|
||||
# Spike 407: Fly.io Machines for multi-tenant storkit SaaS — docs, security & pricing
|
||||
|
||||
## Question
|
||||
|
||||
What do Fly.io's published docs, security claims, and pricing say about using Machines as the isolation layer for a multi-tenant storkit SaaS? Is there anything that rules it out before we write code?
|
||||
|
||||
## Hypothesis
|
||||
|
||||
Fly.io Machines (Firecracker-based microVMs) are a viable isolation primitive for tenants running arbitrary shell commands, and the pricing model is workable at early SaaS scale.
|
||||
|
||||
## Timebox
|
||||
|
||||
2 hours
|
||||
|
||||
## Investigation Plan
|
||||
|
||||
- [x] Read Fly.io Machines API docs — what are the core primitives (machine lifecycle, networking, volumes, secrets)?
|
||||
- [x] Research Fly.io's published isolation model — what security guarantees do they document for Firecracker microVMs? Summarise claims and explicitly flag what would require independent security review before production use.
|
||||
- [x] Research cold start time — what do Fly.io docs and community benchmarks claim? Note that real numbers require a test account (covered in spike 408).
|
||||
- [x] Research persistent volume support — can a volume be attached per-tenant? What are the size/count limits?
|
||||
- [x] Research secret injection options — env vars, Fly Secrets API, volume mounts. What's the right approach for per-tenant `~/.claude/.credentials.json`?
|
||||
- [x] Research machine count and org limits — any hard caps that would block SaaS growth?
|
||||
- [x] Research pricing — always-on vs stop-on-idle machine costs at 10, 100, 1000 tenants. Include volume and egress costs.
|
||||
- [x] Identify any documented showstoppers.
|
||||
|
||||
## Findings
|
||||
|
||||
### 1. Core API Primitives
|
||||
|
||||
Base URL: `https://api.machines.dev` (or `http://_api.internal:4280` from within 6PN).
|
||||
Auth: `Authorization: Bearer <fly_api_token>`.
|
||||
|
||||
**Machine lifecycle** — full REST API:
|
||||
- `POST /v1/apps/{app}/machines` — create (+ optionally start via `skip_launch: false`)
|
||||
- `POST /v1/apps/{app}/machines/{id}/start` — start stopped machine (~10ms same-region)
|
||||
- `POST /v1/apps/{app}/machines/{id}/stop` — stop (SIGINT/SIGKILL, retains disk)
|
||||
- `POST /v1/apps/{app}/machines/{id}/suspend` — snapshot RAM to disk for fast resume
|
||||
- `DELETE /v1/apps/{app}/machines/{id}` — destroy (irreversible)
|
||||
- `GET /v1/apps/{app}/machines/{id}/wait?state=started` — synchronize on state transitions
|
||||
|
||||
Machine states: `created → started → stopped/suspended → destroyed`.
|
||||
Leases (`POST .../lease`) provide exclusive mutation locks — useful for orchestration.
|
||||
|
||||
**Rate limits**: 1 req/s per action per machine/app ID (burst to 3). Matters for rapid tenant provisioning.
|
||||
|
||||
### 2. Isolation Model
|
||||
|
||||
Each Fly Machine is a **Firecracker microVM** — a separate Linux kernel, not a container. Defense in depth:
|
||||
1. KVM hardware-enforced memory and CPU isolation
|
||||
2. Minimal device model (5 virtual devices vs QEMU's hundreds)
|
||||
3. Rust VMM implementation (no C memory-safety bugs in VMM)
|
||||
4. `seccomp-bpf` limits Firecracker process to ~40 syscalls with argument filters
|
||||
5. Jailer chroots + namespaces + drops privileges around the Firecracker process
|
||||
|
||||
From official docs: *"MicroVMs provide strong hardware-virtualization-based security and workload isolation, which allows us to safely run applications from different customers on shared hardware."* Full VM isolation prevents kernel sharing between apps.
|
||||
|
||||
Tenants have full root inside their VM by design — the kernel boundary contains blast radius.
|
||||
|
||||
**Claims requiring independent verification before production use:**
|
||||
- Whether SMT/hyperthreading is disabled on hosts (directly relevant to Spectre/MDS side-channel attacks — Firecracker's own docs recommend disabling SMT for strict multi-tenancy, but Fly.io does not publicly document this)
|
||||
- CPU dedication is explicitly described as "best-effort", not a hard guarantee
|
||||
- Pentest scope/dates/findings for three named firms (Atredis Partners, Doyensec, Tetrel) are not published
|
||||
- Whether the SOC 2 Type II report scope covers the Firecracker isolation layer specifically
|
||||
|
||||
**Compliance**: SOC 2 Type II certified (report available on request), ISO 27001 datacenters (Equinix), HIPAA BAA available, GDPR DPA available.
|
||||
|
||||
### 3. Network Isolation
|
||||
|
||||
Each machine gets a private IPv6 (6PN) address. Key isolation controls:
|
||||
- Cross-organization: Fly.io platform blocks all cross-org traffic at the platform level — strong boundary
|
||||
- Intra-organization: **open by default** — any machine in the same org can reach any other
|
||||
|
||||
For multi-tenant SaaS, this means tenant machines in the same Fly.io org are NOT network-isolated from each other unless you use **Custom Private Networks (6PNs)**:
|
||||
- `POST /v1/apps` with a `network` field assigns that app to an isolated 6PN
|
||||
- Apps on different 6PNs cannot reach each other via private networking (only via public IPs)
|
||||
- **Assignment is permanent** — cannot be changed after app creation; plan upfront
|
||||
|
||||
Stable machine addressing: `<machine_id>.vm.<appname>.internal` (6PN addresses change on migration).
|
||||
|
||||
### 4. Cold Start Times
|
||||
|
||||
| Scenario | Documented Latency |
|
||||
|---|---|
|
||||
| Cold boot (create + start, same region) | ~300 ms |
|
||||
| Start existing stopped machine (same region) | ~10 ms |
|
||||
| Start stopped machine (cross-region) | ~150 ms |
|
||||
| Resume from suspend (same region) | Sub-100ms (implied) |
|
||||
|
||||
Community-observed: 400–600ms end-to-end (including app init) for stopped machine cold starts.
|
||||
FLAME workloads report 3–8s in some restart-race conditions.
|
||||
|
||||
Real latency numbers with our actual image size require a test account — covered by spike 408.
|
||||
|
||||
### 5. Persistent Volume Support
|
||||
|
||||
- Volumes are created via `POST /v1/apps/{app}/volumes` with `size_gb` (default 3 GB), region, encryption flag
|
||||
- Attached to machine via `config.mounts[].volume` at create/update time
|
||||
- **1:1 constraint**: one volume per machine, one machine per volume, same region required
|
||||
- Volumes persist across machine stop/start/suspend/destroy — they are a separate resource
|
||||
- Can extend volume online (`PUT .../volumes/{id}/extend`)
|
||||
- Volume snapshots available (billed at $0.08/GB/month as of Jan 2026)
|
||||
- No documented per-org volume count cap (separate from machine cap)
|
||||
|
||||
For per-tenant `~/.claude/` home directories, attach one volume per tenant machine — straightforward.
|
||||
|
||||
### 6. Secret Injection
|
||||
|
||||
Four methods, in order of recommendation for sensitive credentials:
|
||||
|
||||
1. **Fly Secrets** (`fly secrets set KEY=value`) — encrypted at rest, injected as env vars at boot to all machines in the app. **Secrets are per-app, not per-machine** — all machines in an app share the same secret set. For per-tenant isolated secrets, each tenant needs their own app (or use method 3).
|
||||
|
||||
2. **`config.files` with `secret_name`** — writes a named secret to a file path inside the machine at start time:
|
||||
```json
|
||||
{"guest_path": "/root/.claude/.credentials.json", "secret_name": "TENANT_CREDENTIALS"}
|
||||
```
|
||||
This is the right approach for per-tenant `~/.claude/.credentials.json` if tenants share an app — pair with `ignore_app_secrets: true` and per-process secret scoping.
|
||||
|
||||
3. **`config.env`** — plain env vars in machine config, not encrypted at rest. Non-sensitive config only.
|
||||
|
||||
4. **`config.processes[].secrets`** — inject named secrets only to specific process groups; `ignore_app_secrets: true` prevents inheritance of app-level secrets.
|
||||
|
||||
**Recommended architecture**: One app per tenant (isolated 6PN + isolated secrets) is the cleanest security model. Secrets stored per app via Fly Secrets, credentials file written via `config.files` at boot.
|
||||
|
||||
### 7. Machine Count and Org Limits
|
||||
|
||||
| Limit | Default | Hard Cap |
|
||||
|---|---|---|
|
||||
| Machines per org (all states) | 50 | None architectural |
|
||||
|
||||
- The 50-machine default is a **fail-safe**, not an architectural limit. Fly.io runs customers with 100,000+ machines.
|
||||
- To raise: email `billing@fly.io` with requirements.
|
||||
- **This limit will be hit immediately in any real multi-tenant deployment** — must budget for an early limit-raise request before launching.
|
||||
- API rate limit of 1 req/s per action also needs consideration for bulk tenant provisioning scripts.
|
||||
|
||||
### 8. Pricing (as of March 2026)
|
||||
|
||||
**Compute (per second, billed only while running):**
|
||||
|
||||
| Preset | Per Month always-on |
|
||||
|---|---|
|
||||
| shared-cpu-1x (256 MB) | $2.05 |
|
||||
| shared-cpu-2x (512 MB) | $4.10 |
|
||||
| performance-1x (2 GB) | $32.64 |
|
||||
|
||||
**Storage**: $0.15/GB/month (provisioned, regardless of machine state)
|
||||
**Egress**: $0.02/GB (North America/Europe), $0.04/GB (APAC/SA), $0.12/GB (Africa/India)
|
||||
**Dedicated IPv4**: $2.00/month per app (shared IPv6 is free)
|
||||
|
||||
**No free tier** for new orgs (eliminated 2024). No minimum spend, no base fee.
|
||||
|
||||
**Monthly cost estimates** (1x shared-cpu-1x, 1 GB volume, 1 GB egress/tenant, US East):
|
||||
|
||||
| Scenario | Per Tenant | 10 Tenants | 100 Tenants | 1,000 Tenants |
|
||||
|---|---|---|---|---|
|
||||
| Always-on (730h/month) | $2.22 | $22 | $222 | $2,220 |
|
||||
| Autostop, 8h/day active | $0.92 | $9 | $92 | $920 |
|
||||
| Autostop, 2h/day active | $0.53 | $5 | $53 | $530 |
|
||||
|
||||
At scale, volume storage becomes the dominant cost when machines are idle. At 1,000 tenants autostopped, storage is ~$150/month vs compute of $170–$370/month.
|
||||
|
||||
### 9. Showstoppers
|
||||
|
||||
**None identified** that rule it out. The following require action before launch:
|
||||
|
||||
| Risk | Severity | Mitigation |
|
||||
|---|---|---|
|
||||
| Default 50-machine org cap | High (blocks launch) | Email billing@fly.io early; no architectural cap |
|
||||
| SMT/hyperthreading not documented | Medium (security) | Request confirmation from Fly.io support before production; mitigated by VM-level isolation |
|
||||
| Intra-org network open by default | Medium (security) | Use one app per tenant with custom 6PNs |
|
||||
| Secrets are per-app not per-machine | Low | Use one app per tenant or `config.files` with `secret_name` |
|
||||
| Volume and machine must be same region | Low (ops) | Enforce region consistency in provisioning code |
|
||||
| API rate limit 1 req/s per machine | Low | Throttle bulk provisioning loops |
|
||||
|
||||
## Recommendation
|
||||
|
||||
**Proceed.** Fly.io Machines are a viable isolation layer for multi-tenant storkit SaaS.
|
||||
|
||||
**Architecture to validate in spike 408:**
|
||||
- One Fly.io app per tenant (provides 6PN network isolation + isolated secrets)
|
||||
- One Firecracker microVM per tenant app (shared-cpu-1x 256 MB baseline; adjust per observed usage)
|
||||
- One persistent volume per tenant (1 GB baseline for `~/.claude/`, repos, storkit state)
|
||||
- Autostop/autoresume enabled — 70–92% compute cost reduction vs always-on for typical dev tool usage
|
||||
- Tenant credentials injected via `config.files` + Fly Secrets at machine start
|
||||
|
||||
**Pricing verdict**: Workable at early SaaS scale. At 100 tenants with autostop (8h/day), costs ~$92/month; at 1,000 tenants ~$920/month. Margins are viable if per-tenant pricing is $5–$20/month.
|
||||
|
||||
**Before production**: Confirm with Fly.io support whether SMT is disabled on worker hosts. Request org machine limit raised to 200–500 during private beta.
|
||||
|
||||
**Spike 408 scope**: Validate cold start latency, autostop resume behavior, and volume persistence with a real test machine running the storkit container image.
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
name: "Split whatsapp.rs into focused modules"
|
||||
retry_count: 2
|
||||
blocked: true
|
||||
---
|
||||
|
||||
# Refactor 409: Split whatsapp.rs into focused modules
|
||||
|
||||
## Current State
|
||||
|
||||
- TBD
|
||||
|
||||
## Desired State
|
||||
|
||||
whatsapp.rs is 2000+ lines making it expensive for agents to navigate and edit. Split into focused modules under chat/transport/whatsapp/.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] mod.rs contains webhook handlers, WebhookContext, and re-exports
|
||||
- [x] meta.rs contains WhatsAppTransport, ChatTransport impl, and Graph API structs/calls
|
||||
- [x] twilio.rs contains TwilioWhatsAppTransport, ChatTransport impl, and Twilio structs/calls
|
||||
- [x] history.rs contains WhatsAppConversationHistory, load/save_whatsapp_history, and MessagingWindowTracker
|
||||
- [x] commands.rs contains handle_incoming_message, handle_llm_message, and all async command dispatch
|
||||
- [x] format.rs contains markdown_to_whatsapp and chunk_for_whatsapp
|
||||
- [x] All existing tests pass
|
||||
- [x] No behaviour changes — pure structural refactor
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
|
||||
## Test Results
|
||||
|
||||
<!-- storkit-test-results: {"unit":[{"name":"whatsapp::format::tests::chunk_short_message_returns_single_chunk","status":"pass","details":null},{"name":"whatsapp::format::tests::chunk_exactly_at_limit_returns_single_chunk","status":"pass","details":null},{"name":"whatsapp::format::tests::chunk_splits_on_paragraph_boundary","status":"pass","details":null},{"name":"whatsapp::format::tests::chunk_splits_on_line_boundary_when_no_paragraph_break","status":"pass","details":null},{"name":"whatsapp::format::tests::chunk_hard_splits_continuous_text","status":"pass","details":null},{"name":"whatsapp::format::tests::chunk_empty_string_returns_single_empty","status":"pass","details":null},{"name":"whatsapp::format::tests::md_to_wa_converts_headers_to_bold","status":"pass","details":null},{"name":"whatsapp::format::tests::md_to_wa_converts_bold","status":"pass","details":null},{"name":"whatsapp::format::tests::md_to_wa_converts_bold_italic","status":"pass","details":null},{"name":"whatsapp::format::tests::md_to_wa_converts_strikethrough","status":"pass","details":null},{"name":"whatsapp::format::tests::md_to_wa_converts_links","status":"pass","details":null},{"name":"whatsapp::format::tests::md_to_wa_removes_horizontal_rules","status":"pass","details":null},{"name":"whatsapp::format::tests::md_to_wa_preserves_inline_code","status":"pass","details":null},{"name":"whatsapp::format::tests::md_to_wa_preserves_code_blocks","status":"pass","details":null},{"name":"whatsapp::format::tests::md_to_wa_mixed_message","status":"pass","details":null},{"name":"whatsapp::format::tests::md_to_wa_passthrough_plain_text","status":"pass","details":null},{"name":"whatsapp::history::tests::messaging_window_tracker_basics","status":"pass","details":null},{"name":"whatsapp::history::tests::messaging_window_tracker_expiry","status":"pass","details":null},{"name":"whatsapp::history::tests::messaging_window_tracker_reset","status":"pass","details":null},{"name":"whatsapp::history::tests::load_empty_history","status":"pass","details":null},{"name":"whatsapp::history::tests::save_and_load_history","status":"pass","details":null},{"name":"whatsapp::twilio::tests::parse_twilio_form_valid","status":"pass","details":null},{"name":"whatsapp::twilio::tests::parse_twilio_form_missing_body","status":"pass","details":null},{"name":"whatsapp::twilio::tests::parse_twilio_form_missing_from","status":"pass","details":null},{"name":"whatsapp::commands::tests::parse_command_help","status":"pass","details":null},{"name":"whatsapp::commands::tests::parse_command_status","status":"pass","details":null},{"name":"whatsapp::commands::tests::parse_command_unknown","status":"pass","details":null},{"name":"whatsapp::mod::tests::webhook_context_basics","status":"pass","details":null}],"integration":[]} -->
|
||||
|
||||
### Unit Tests (28 passed, 0 failed)
|
||||
|
||||
- ✅ whatsapp::format::tests::chunk_short_message_returns_single_chunk
|
||||
- ✅ whatsapp::format::tests::chunk_exactly_at_limit_returns_single_chunk
|
||||
- ✅ whatsapp::format::tests::chunk_splits_on_paragraph_boundary
|
||||
- ✅ whatsapp::format::tests::chunk_splits_on_line_boundary_when_no_paragraph_break
|
||||
- ✅ whatsapp::format::tests::chunk_hard_splits_continuous_text
|
||||
- ✅ whatsapp::format::tests::chunk_empty_string_returns_single_empty
|
||||
- ✅ whatsapp::format::tests::md_to_wa_converts_headers_to_bold
|
||||
- ✅ whatsapp::format::tests::md_to_wa_converts_bold
|
||||
- ✅ whatsapp::format::tests::md_to_wa_converts_bold_italic
|
||||
- ✅ whatsapp::format::tests::md_to_wa_converts_strikethrough
|
||||
- ✅ whatsapp::format::tests::md_to_wa_converts_links
|
||||
- ✅ whatsapp::format::tests::md_to_wa_removes_horizontal_rules
|
||||
- ✅ whatsapp::format::tests::md_to_wa_preserves_inline_code
|
||||
- ✅ whatsapp::format::tests::md_to_wa_preserves_code_blocks
|
||||
- ✅ whatsapp::format::tests::md_to_wa_mixed_message
|
||||
- ✅ whatsapp::format::tests::md_to_wa_passthrough_plain_text
|
||||
- ✅ whatsapp::history::tests::messaging_window_tracker_basics
|
||||
- ✅ whatsapp::history::tests::messaging_window_tracker_expiry
|
||||
- ✅ whatsapp::history::tests::messaging_window_tracker_reset
|
||||
- ✅ whatsapp::history::tests::load_empty_history
|
||||
- ✅ whatsapp::history::tests::save_and_load_history
|
||||
- ✅ whatsapp::twilio::tests::parse_twilio_form_valid
|
||||
- ✅ whatsapp::twilio::tests::parse_twilio_form_missing_body
|
||||
- ✅ whatsapp::twilio::tests::parse_twilio_form_missing_from
|
||||
- ✅ whatsapp::commands::tests::parse_command_help
|
||||
- ✅ whatsapp::commands::tests::parse_command_status
|
||||
- ✅ whatsapp::commands::tests::parse_command_unknown
|
||||
- ✅ whatsapp::mod::tests::webhook_context_basics
|
||||
|
||||
### Integration Tests (0 passed, 0 failed)
|
||||
|
||||
*No integration tests recorded.*
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: "loc bot command — top files by line count"
|
||||
---
|
||||
|
||||
# Story 410: loc bot command — top files by line count
|
||||
|
||||
## User Story
|
||||
|
||||
As a developer, I want to send `loc` to the bot and see the top files by line count, so I can spot files that are getting too large before they become a problem for agents.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] loc command is registered in chat/commands/mod.rs and appears in help output
|
||||
- [ ] `loc` returns the top 10 source files by line count (excluding generated files, node_modules, target/, .storkit/worktrees/)
|
||||
- [ ] `loc 5` returns the top 5 files
|
||||
- [ ] `loc 20` returns the top 20 files
|
||||
- [ ] Output includes file path, line count, and rank
|
||||
- [ ] Command works from all transports (Matrix, WhatsApp, Slack)
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
name: "Split slack.rs into focused modules"
|
||||
---
|
||||
|
||||
# Refactor 413: Split slack.rs into focused modules
|
||||
|
||||
## Current State
|
||||
|
||||
- TBD
|
||||
|
||||
## Desired State
|
||||
|
||||
Refactor the monolithic server/src/chat/transport/slack.rs (1902 lines) into a slack/ directory with focused modules, mirroring the whatsapp/ module structure from story 409.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] slack.rs is replaced by a slack/ directory with mod.rs re-exporting all public types
|
||||
- [ ] meta.rs contains SlackTransport struct, ChatTransport trait impl, and Slack API request/response types
|
||||
- [ ] commands.rs contains incoming message dispatch, permission logic, and slash command handling
|
||||
- [ ] format.rs contains markdown_to_slack() conversion
|
||||
- [ ] history.rs contains load_slack_history(), save_slack_history(), and SlackHistoryDump
|
||||
- [ ] verify.rs contains verify_slack_signature(), sha256(), and constant_time_eq()
|
||||
- [ ] mod.rs contains Slack event types, webhook handlers, and SlackWebhookContext
|
||||
- [ ] All existing tests are preserved and pass in their respective modules
|
||||
- [ ] No public API changes — all existing imports from other crates continue to work
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
name: "loc command filters out known-huge files"
|
||||
---
|
||||
|
||||
# Story 414: loc command filters out known-huge files
|
||||
|
||||
## User Story
|
||||
|
||||
As a ..., I want ..., so that ...
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] loc command excludes lockfiles and generated files (e.g. package-lock.json, Cargo.lock, frontend/package-lock.json) from results
|
||||
- [ ] Exclusion list is defined as a constant, easy to extend
|
||||
- [ ] Excluded files do not count toward line totals
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
name: "Split agents/pool/mod.rs into submodules"
|
||||
---
|
||||
|
||||
# Refactor 415: Split agents/pool/mod.rs into submodules
|
||||
|
||||
## Current State
|
||||
|
||||
- TBD
|
||||
|
||||
## Desired State
|
||||
|
||||
Refactor the monolithic server/src/agents/pool/mod.rs (2407 lines) into focused submodules within the pool/ directory.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] types.rs contains StoryAgent, PendingGuard, AgentInfo, composite_key, and related helper structs
|
||||
- [ ] lifecycle.rs contains start_agent, stop_agent, wait_for_agent and their unit tests
|
||||
- [ ] worktree.rs contains create_worktree, get_project_root, find_active_story_stage and their unit tests
|
||||
- [ ] query.rs contains list_agents, available_agents_for_stage, get_log_info, subscribe, drain_events and their unit tests
|
||||
- [ ] process.rs contains kill_all_children, kill_child_for_key, ChildKiller registry methods and their unit tests
|
||||
- [ ] test_helpers.rs contains inject_test_agent and its variants (4 methods)
|
||||
- [ ] mod.rs contains AgentPool struct, new(), and re-exports all public types
|
||||
- [ ] Unit tests live in their respective module files, not in a separate tests module
|
||||
- [ ] No public API changes — all existing imports continue to work
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
name: "Split io/fs.rs into submodules"
|
||||
---
|
||||
|
||||
# Refactor 416: Split io/fs.rs into submodules
|
||||
|
||||
## Current State
|
||||
|
||||
- TBD
|
||||
|
||||
## Desired State
|
||||
|
||||
Refactor the monolithic server/src/io/fs.rs (2007 lines) into focused submodules within an fs/ directory.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] scaffold.rs contains scaffold_story_kit, write_file_if_missing, write_script_if_missing, write_story_kit_gitignore, append_root_gitignore_entries, detect_components_toml, detect_script_test, generate_project_toml and their unit tests
|
||||
- [ ] project.rs contains open_project, close_project, get_current_project, get_known_projects, forget_known_project, ensure_project_root_with_story_kit, validate_project_path and their unit tests
|
||||
- [ ] files.rs contains read_file, write_file, list_directory, list_project_files, FileEntry, create_directory_absolute and their unit tests
|
||||
- [ ] paths.rs contains resolve_cli_path, resolve_path, resolve_path_impl, find_story_kit_root, get_home_directory and their unit tests
|
||||
- [ ] preferences.rs contains get_model_preference, set_model_preference and their unit tests
|
||||
- [ ] mod.rs re-exports all public types and functions
|
||||
- [ ] Unit tests live in their respective module files
|
||||
- [ ] No public API changes — all existing imports continue to work
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
name: "Split matrix/bot.rs into focused modules"
|
||||
---
|
||||
|
||||
# Refactor 417: Split matrix/bot.rs into focused modules
|
||||
|
||||
## Current State
|
||||
|
||||
- TBD
|
||||
|
||||
## Desired State
|
||||
|
||||
Refactor the monolithic server/src/chat/transport/matrix/bot.rs (1926 lines) into focused submodules.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] history.rs contains ConversationRole, ConversationEntry, RoomConversation, PersistedHistory, load_history, save_history and their unit tests
|
||||
- [ ] context.rs contains BotContext struct
|
||||
- [ ] run.rs contains run_bot main event loop
|
||||
- [ ] messages.rs contains on_room_message, handle_message, format_user_prompt, is_permission_approval and their unit tests
|
||||
- [ ] mentions.rs contains mentions_bot, contains_word, is_reply_to_bot and their unit tests
|
||||
- [ ] verification.rs contains check_sender_verified, on_to_device_verification_request, handle_sas_verification and their unit tests
|
||||
- [ ] format.rs contains markdown_to_html, format_startup_announcement and their unit tests
|
||||
- [ ] mod.rs re-exports all public types
|
||||
- [ ] Unit tests live in their respective module files
|
||||
- [ ] No public API changes — all existing imports continue to work
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
name: "Split pool/auto_assign.rs into submodules"
|
||||
---
|
||||
|
||||
# Refactor 418: Split pool/auto_assign.rs into submodules
|
||||
|
||||
## Current State
|
||||
|
||||
- TBD
|
||||
|
||||
## Desired State
|
||||
|
||||
Refactor the monolithic server/src/agents/pool/auto_assign.rs (1813 lines) into focused submodules.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] auto_assign.rs contains auto_assign_available_work and its unit tests
|
||||
- [ ] reconcile.rs contains reconcile_on_startup and its unit tests
|
||||
- [ ] watchdog.rs contains run_watchdog_once, spawn_watchdog, check_orphaned_agents and their unit tests
|
||||
- [ ] scan.rs contains scan_stage_items, is_story_assigned_for_stage, count_active_agents_for_stage, find_free_agent_for_stage, is_agent_free and their unit tests
|
||||
- [ ] story_checks.rs contains read_story_front_matter_agent, has_review_hold, is_story_blocked, has_merge_failure and their unit tests
|
||||
- [ ] mod.rs wires the submodules and re-exports all public items
|
||||
- [ ] Unit tests live in their respective module files
|
||||
- [ ] No public API changes — all existing imports continue to work
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
---
|
||||
name: "Matrix bot crashes on transient network error instead of retrying"
|
||||
---
|
||||
|
||||
# Bug 419: Matrix bot crashes on transient network error instead of retrying
|
||||
|
||||
## Description
|
||||
|
||||
The Matrix bot treats a transient sync error as fatal and stops entirely. A single failed HTTP request to the homeserver kills the bot, requiring a full server rebuild to recover.
|
||||
|
||||
## How to Reproduce
|
||||
|
||||
1. Run storkit with Matrix bot enabled\n2. Homeserver becomes temporarily unreachable (network blip, DNS hiccup, server restart)\n3. Bot hits sync error and crashes
|
||||
|
||||
## Actual Result
|
||||
|
||||
Bot logs "Fatal error: Matrix sync error: error sending request for url (...)" and stops responding. No retry, no recovery.
|
||||
|
||||
## Expected Result
|
||||
|
||||
Bot logs a warning, backs off with exponential delay, and retries the sync. Only crash on unrecoverable errors (invalid credentials, banned, etc).
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Transient network errors (connection refused, timeout, DNS failure) trigger a retry with exponential backoff
|
||||
- [ ] Bot logs a warning on each failed retry attempt
|
||||
- [ ] Bot resumes normal operation once the homeserver is reachable again
|
||||
- [ ] Unrecoverable errors (401, 403) still cause a clean shutdown with a clear error message
|
||||
- [ ] Bot sends a notification after recovering from a network outage
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: "loc for a specified file — bot command and web UI slash command"
|
||||
---
|
||||
|
||||
# Story 420: loc for a specified file — bot command and web UI slash command
|
||||
|
||||
## User Story
|
||||
|
||||
As a developer, I want to send `loc <filepath>` to the bot or use it as a slash command in the web UI to see the line count for a specific file, so I can quickly check how large a file is without leaving my workflow.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] loc <filepath> returns the line count for the specified file
|
||||
- [ ] Relative paths are resolved against the project root
|
||||
- [ ] If the file does not exist, returns a clear error
|
||||
- [ ] Works from all transports (Matrix, WhatsApp, Slack)
|
||||
- [ ] Works as a slash command in the web UI
|
||||
- [ ] loc with no argument retains existing behavior (top files by line count)
|
||||
- [ ] Exposed as an MCP tool so agents can query file line counts programmatically
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
name: "Timer command for deferred agent start"
|
||||
---
|
||||
|
||||
# Story 421: Timer command for deferred agent start
|
||||
|
||||
## User Story
|
||||
|
||||
As a ..., I want ..., so that ...
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Bot command `timer <story_id> <HH:MM>` schedules a one-shot deferred start for the given story at the next occurrence of that time (server-local timezone)
|
||||
- [ ] Bot command `timer list` shows all pending timers with story ID and scheduled time
|
||||
- [ ] Bot command `timer cancel <story_id>` removes the pending timer for that story
|
||||
- [ ] Timers are persisted to .storkit/timers.json so they survive server restarts
|
||||
- [ ] A 30s tick loop (tokio task, same pattern as watchdog) checks for due timers and calls start_agent when triggered
|
||||
- [ ] When a timer fires, the story must already be in current — timer does not move stories between stages
|
||||
- [ ] Fired timers are removed after execution (one-shot, not recurring)
|
||||
- [ ] Multiple timers for the same time are supported and respect agent slot contention via auto-assign
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: "Unblock command to reset blocked stories"
|
||||
---
|
||||
|
||||
# Story 422: Unblock command to reset blocked stories
|
||||
|
||||
## User Story
|
||||
|
||||
As a ..., I want ..., so that ...
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Bot command `unblock <story_id>` clears blocked flag and resets retry_count to 0 on the story front matter
|
||||
- [ ] Replies with confirmation including story ID and name
|
||||
- [ ] Returns clear error if story is not found or not blocked
|
||||
- [ ] Works from all transports (Matrix, WhatsApp, Slack)
|
||||
- [ ] Exposed as an MCP tool so agents can unblock stories programmatically
|
||||
- [ ] Works as a slash command in the web UI
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: "Auto-schedule timer on rate limit to resume after reset"
|
||||
---
|
||||
|
||||
# Story 423: Auto-schedule timer on rate limit to resume after reset
|
||||
|
||||
## User Story
|
||||
|
||||
As a ..., I want ..., so that ...
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] When a rate_limit_event with a hard block (not just allowed_warning) is received from the PTY stream, parse the reset time from rate_limit_info
|
||||
- [ ] Automatically create a timer (via TimerStore from story 421) for the blocked story at the parsed reset time
|
||||
- [ ] If a timer already exists for that story, update it to the later reset time rather than creating a duplicate
|
||||
- [ ] Log the auto-scheduled timer with story ID, agent name, and scheduled resume time
|
||||
- [ ] Notify chat transports that the story was rate-limited and will auto-resume at the scheduled time
|
||||
- [ ] When the timer fires and restarts the agent, the existing worktree and committed work are preserved
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: "Rate limit traffic light status and hard block alerts"
|
||||
agent: coder-opus
|
||||
---
|
||||
|
||||
# Story 424: Rate limit traffic light status and hard block alerts
|
||||
|
||||
## User Story
|
||||
|
||||
As a ..., I want ..., so that ...
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Remove repetitive per-message throttle warnings (allowed_warning) from chat transports entirely
|
||||
- [ ] Pipeline status messages show a coloured dot next to each work item: green for running normally, yellow for throttled, red for hard blocked, white/grey for idle/no agent
|
||||
- [ ] Hard block events (429 / rate_limit_exceeded) still send an individual chat notification with a red icon, including the reset time
|
||||
- [ ] Throttle and block state tracked per-agent so the status dot updates in real time
|
||||
- [ ] Server-side logging of throttle warnings is preserved for debugging
|
||||
- [ ] Traffic light dots in status report should be small/compact, not large emoji
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: "Chat notification when a story blocks with reason"
|
||||
---
|
||||
|
||||
# Story 425: Chat notification when a story blocks with reason
|
||||
|
||||
## User Story
|
||||
|
||||
As a project owner monitoring agent progress via chat, I want to receive a notification when a story gets blocked, including the reason, so that I can decide whether to unblock it or investigate the failure.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] When a story transitions to blocked state, send a chat notification to all configured transports
|
||||
- [ ] Notification includes the story ID, story name, and the reason for blocking (e.g. gate failure output, max retries exceeded, empty diff)
|
||||
- [ ] Notification uses a red or warning icon to distinguish from normal status messages
|
||||
- [ ] Works across Matrix, WhatsApp, and Slack transports
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
---
|
||||
name: "Mergemaster pipeline marks story done without verifying code landed on master"
|
||||
retry_count: 1
|
||||
---
|
||||
|
||||
# Bug 426: Mergemaster pipeline marks story done without verifying code landed on master
|
||||
|
||||
## Description
|
||||
|
||||
The mergemaster pipeline can mark a story as done even when the feature code never makes it to master. The cherry-pick step in merge.rs may fail or be skipped, but the pipeline still advances the story to done via the filesystem watcher. There is no post-merge verification that the code actually exists on master before marking done.
|
||||
|
||||
## How to Reproduce
|
||||
|
||||
Observed on stories 422 and 403. For 422: mergemaster created merge-queue branch, resolved 2 conflicts in chat/commands/mod.rs and http/mcp/mod.rs, passed quality gates, created merge-queue commit cb2ef6b (4 files, 333 insertions including unblock.rs). But the done commit on master (05db012) only moves the story file — zero code changes. There is no 'storkit: merge 422' commit on master at all. The feature branch (db3157f) still has the code but it was never cherry-picked onto master.
|
||||
|
||||
## Manual Merge Notes
|
||||
|
||||
When manually cherry-picking 422 onto master, two conflicts arose:
|
||||
|
||||
1. `server/src/chat/commands/mod.rs` — both 421 (timer) and 422 (unblock) added entries to the same BotCommand registry. Resolution: keep both.
|
||||
2. `server/src/http/mcp/mod.rs` — 420 (loc_file) and 422 (unblock) both bumped the tool count assertion from 49→50. Resolution: keep loc_file assertion, bump count to 51.
|
||||
|
||||
Additionally, the cherry-pick could not proceed at all because master was on the `merge-queue/424` branch with 3 unresolved files (notifications.rs, ws.rs, watcher.rs). A concurrent in-progress merge left the working tree dirty, which likely caused the original cherry-pick to fail silently. This suggests a race condition: the filesystem watcher commits (story file moves) can leave master in a state where the cherry-pick step in merge.rs fails.
|
||||
|
||||
## Full Audit of Done Stories (2026-03-28)
|
||||
|
||||
Audited all 9 stories in `5_done/` to check whether their code actually landed on master:
|
||||
|
||||
| Story | Merge Commit | Code on Master |
|
||||
|-------|-------------|----------------|
|
||||
| 417 — Split matrix/bot.rs | `665c036` (9 files, +1973/-1926) | YES |
|
||||
| 418 — Split pool/auto_assign.rs | `d375c4b` (7 files, +1901/-1813) | YES |
|
||||
| 419 — Matrix bot network error | `1193b7a` (1 file, +121/-3) | YES |
|
||||
| 420 — loc file command | `d6f8239` (5 files, +112/-32) | YES |
|
||||
| 421 — Timer command | `cf5424f` (7 files, +836) | YES |
|
||||
| 422 — Unblock command | `6c6bc35` (4 files, +336) — manual cherry-pick | YES |
|
||||
| 423 — Auto-schedule timer on rate limit | `b44f3a3` + `8ab2e19` (6 files, +375/-8) — manual cherry-pick | YES |
|
||||
| **424 — Rate limit traffic light** | **None** | **NO — moved back to backlog for redo** |
|
||||
| 425 — Chat notification on story block | `98b5475` (5 files, +184/-15) | YES |
|
||||
| **427 — Text normalization for line breaks** | **None** | **NO — phantom done, code never landed** |
|
||||
|
||||
**4 out of 10 stories (422, 423, 424, 427) had broken merges.** 422 and 423 were fixed via manual cherry-pick. 424 was moved back to backlog for a fresh run. 427 also hit the same bug — marked done without code on master.
|
||||
|
||||
## Actual Result
|
||||
|
||||
Story moved to done with no code on master. The merge-queue commit exists on a detached branch but was never applied to master. No merge commit appears in git log on master.
|
||||
|
||||
## Expected Result
|
||||
|
||||
Pipeline should verify that the cherry-pick produced a merge commit on master before advancing to done. If cherry-pick fails or is missing, the story should remain in merge stage with a merge_failure flag.
|
||||
|
||||
## Suggested Fix
|
||||
|
||||
The code path is: `merge.rs::run_squash_merge` → `pipeline/merge.rs::start_merge_agent_work` → `lifecycle.rs::move_story_to_archived`.
|
||||
|
||||
`run_squash_merge` (merge.rs:354) cherry-picks the merge-queue commit onto `project_root` and checks `cp.status.success()`. If it returns `success: true`, `start_merge_agent_work` (pipeline/merge.rs:106) immediately calls `move_story_to_archived`, which moves the story file to `5_done/`. The watcher then commits "storkit: done".
|
||||
|
||||
The gap: between the cherry-pick returning success and the story moving to done, nobody verifies the cherry-pick actually produced a code commit on master. Possible failure modes:
|
||||
|
||||
1. `project_root` is not on master (e.g. checked out to a merge-queue branch from a concurrent merge)
|
||||
2. Cherry-pick exits 0 but produces an empty commit (no code diff)
|
||||
3. Cherry-pick succeeds on the wrong branch
|
||||
|
||||
**Fix:** After the cherry-pick in `run_squash_merge` succeeds (line 384), before returning `success: true`:
|
||||
|
||||
1. Verify `project_root` is on master: `git rev-parse --abbrev-ref HEAD` must equal the base branch
|
||||
2. Verify the HEAD commit on master contains the expected merge message (e.g. matches `storkit: merge <story_id>`) or has a non-empty diff
|
||||
3. If either check fails, abort the cherry-pick and return `success: false`
|
||||
|
||||
This keeps the fix entirely within `run_squash_merge` — no changes needed to the pipeline advance or lifecycle code.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Pipeline must not move a story to done unless a merge commit containing the feature code exists on master
|
||||
- [ ] If cherry-pick fails or produces no code diff on master, the merge must be reported as failed
|
||||
- [ ] Add a post-merge verification step that checks git log on master for the expected merge commit before advancing to done
|
||||
- [ ] When verification fails, emit a merge_failure and leave the story in the merge stage for retry
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: "Server-side text normalization for chat message line breaks"
|
||||
---
|
||||
|
||||
# Story 427: Server-side text normalization for chat message line breaks
|
||||
|
||||
## User Story
|
||||
|
||||
As a user reading bot messages in Matrix, I want single newlines between sentences to render correctly, so that messages don't show up with words joined together like "sentence one.Sentence two".
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Add a text normalization step before markdown-to-HTML conversion in the Matrix transport that converts single newlines between non-empty prose lines into double newlines
|
||||
- [ ] Preserve intentional single-newline formatting in bullet lists, headings, table rows, and code fences
|
||||
- [ ] Apply the same normalization in WhatsApp and Slack transports
|
||||
- [ ] Unit tests covering prose paragraphs, bullet lists, code blocks, and mixed content
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
name: "Split pool/pipeline.rs into submodules"
|
||||
---
|
||||
|
||||
# Refactor 428: Split pool/pipeline.rs into submodules
|
||||
|
||||
## Current State
|
||||
|
||||
- TBD
|
||||
|
||||
## Desired State
|
||||
|
||||
Refactor the monolithic server/src/agents/pool/pipeline.rs (1789 lines) into focused submodules.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] advance.rs contains run_pipeline_advance, spawn_pipeline_advance, should_block_story and their unit tests
|
||||
- [ ] completion.rs contains run_server_owned_completion, report_completion and their unit tests
|
||||
- [ ] merge.rs contains start_merge_agent_work, run_merge_pipeline, get_merge_status, set_merge_failure_reported and their unit tests
|
||||
- [ ] mod.rs re-exports all public items and wires the submodules
|
||||
- [ ] Unit tests live in their respective module files
|
||||
- [ ] No public API changes — all existing imports continue to work
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: "Interactive project setup wizard for new storkit projects"
|
||||
agent: coder-opus
|
||||
---
|
||||
|
||||
# Story 429: Interactive project setup wizard for new storkit projects
|
||||
|
||||
## User Story
|
||||
|
||||
As a developer adopting storkit on an existing project, I want a guided setup process that scaffolds the .storkit directory and has an agent generate project-specific configuration files, so that I can get up and running without manually writing specs and scripts.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] storkit init scaffolds .storkit/ directory structure, project.toml, and .mcp.json without clobbering any existing files (especially CLAUDE.md)
|
||||
- [ ] Setup wizard tracks progress through ordered steps, resumable if interrupted
|
||||
- [ ] Step 1: scaffold .storkit/ directory structure and project.toml
|
||||
- [ ] Step 2: agent reads codebase and generates specs/00_CONTEXT.md, user confirms or requests revision
|
||||
- [ ] Step 3: agent reads tech stack and generates specs/tech/STACK.md, user confirms or requests revision
|
||||
- [ ] Step 4: agent creates script/test that runs the project's actual test suite, user runs it to verify, then confirms
|
||||
- [ ] Step 5: agent creates script/release tailored to the project's deployment, user confirms
|
||||
- [ ] Step 6: agent creates script/test_coverage if the stack supports it, user confirms
|
||||
- [ ] Each step gates on user confirmation before advancing to the next
|
||||
- [ ] Existing CLAUDE.md is preserved — storkit appends its content or leaves it untouched
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: "Status command traffic light dots not coloured in Matrix"
|
||||
---
|
||||
|
||||
# Bug 430: Status command traffic light dots not coloured in Matrix
|
||||
|
||||
## Description
|
||||
|
||||
The traffic light dots in the status command use plain Unicode characters (○ ● ◑ ✗) which render without colour in Matrix. The HTML formatted_body should use data-mx-color to colour them green/yellow/red.
|
||||
|
||||
## How to Reproduce
|
||||
|
||||
Send the status command to the bot in Matrix. Observe the dots are monochrome.
|
||||
|
||||
## Actual Result
|
||||
|
||||
Dots render as plain monochrome Unicode characters.
|
||||
|
||||
## Expected Result
|
||||
|
||||
Dots render in colour: green (● running), yellow (◑ throttled), red (✗ blocked), grey (○ idle). Use font tag with data-mx-color attribute for Matrix HTML formatted_body.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] HTML formatted_body uses <font data-mx-color="#colour">dot</font> for each traffic light state
|
||||
- [ ] Green (#00cc00) for running, yellow (#ffaa00) for throttled, red (#cc0000) for blocked, grey (#888888) for idle
|
||||
- [ ] Plain text fallback remains unchanged (Unicode dots for non-HTML transports)
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
---
|
||||
name: "QA agent reviews code changes against acceptance criteria"
|
||||
---
|
||||
|
||||
# Story 431: QA agent reviews code changes against acceptance criteria
|
||||
|
||||
## User Story
|
||||
|
||||
As a project owner, I want the QA agent to actually verify that the coder's implementation matches the story's acceptance criteria, so that incomplete or incorrect work is caught before merge.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] QA agent reads the story's acceptance criteria before reviewing code
|
||||
- [ ] QA agent reads the full diff against master to understand what changed
|
||||
- [ ] For each AC, QA agent verifies the code addresses it and explains how
|
||||
- [ ] QA agent flags incomplete implementations: todo!(), unimplemented!(), missing match arms, placeholder values
|
||||
- [ ] QA agent checks that new code has corresponding test coverage
|
||||
- [ ] QA agent produces a structured report: each AC with pass/fail and explanation
|
||||
- [ ] If any AC is not met, QA rejects the story with a clear reason so the coder can fix it
|
||||
- [ ] Deterministic gates (clippy, tests) still run as a prerequisite before the AC review
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: "Complete setup wizard with MCP tools and agent-driven file generation"
|
||||
agent: "coder-opus"
|
||||
---
|
||||
|
||||
# Story 432: Complete setup wizard with MCP tools and agent-driven file generation
|
||||
|
||||
## User Story
|
||||
|
||||
As a developer running storkit init on a new project, I want the setup wizard to walk me through each step interactively — generating files, letting me review them, and confirming before moving on — so that my project is correctly configured without manual file editing.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] MCP tool wizard_status returns the current wizard state: which step is active, which are done/skipped/pending
|
||||
- [ ] MCP tool wizard_generate triggers the agent to read the codebase and generate content for the current step (CONTEXT.md, STACK.md, script/test, script/release, script/test_coverage)
|
||||
- [ ] MCP tool wizard_confirm confirms the current step and advances to the next
|
||||
- [ ] MCP tool wizard_skip skips the current step and advances to the next
|
||||
- [ ] MCP tool wizard_retry re-generates content for the current step if the user isn't happy with it
|
||||
- [ ] Bot command setup shows wizard progress and the current step with instructions
|
||||
- [ ] Bot command setup confirm / setup skip / setup retry drive the wizard from chat
|
||||
- [ ] Generated files are written to disk only after user confirmation, not during generation preview
|
||||
- [ ] The wizard works from Claude Code terminal via MCP tools without requiring the web UI or chat bot
|
||||
- [ ] Existing files (especially CLAUDE.md) are never overwritten — wizard appends or skips
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
---
|
||||
name: "Setup wizard interviews user on bare projects with no existing code"
|
||||
agent: coder-opus
|
||||
---
|
||||
|
||||
# Story 433: Setup wizard interviews user on bare projects with no existing code
|
||||
|
||||
## User Story
|
||||
|
||||
As a developer starting a brand new project from an empty directory, I want the setup wizard to ask me what I'm building and what tech stack I plan to use, so that it can generate meaningful CONTEXT.md and STACK.md without any codebase to analyze.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] wizard_generate detects when the project directory has no source code files
|
||||
- [ ] On bare projects, the wizard asks the user what they want to build instead of trying to analyze code
|
||||
- [ ] Wizard asks about intended tech stack, frameworks, and language choices
|
||||
- [ ] Conversation continues until the user confirms the generated CONTEXT.md captures their intent
|
||||
- [ ] STACK.md is generated from the user's stated tech choices rather than from codebase detection
|
||||
- [ ] script/test and script/release are generated with appropriate stubs for the stated stack
|
||||
- [ ] The interview flow works via both MCP tools (Claude Code terminal) and bot commands (Matrix/WhatsApp/Slack)
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: "Wizard auto-checks completion on first conversation"
|
||||
---
|
||||
|
||||
# Story 434: Wizard auto-checks completion on first conversation
|
||||
|
||||
## User Story
|
||||
|
||||
As a developer opening Claude Code on a storkit project for the first time, I want the wizard to automatically check if setup is complete and prompt me through remaining steps, so I don't have to know to ask for it.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Scaffolded CLAUDE.md includes an IMPORTANT instruction telling Claude to call wizard_status on first conversation
|
||||
- [ ] If wizard is incomplete, Claude guides the user through remaining steps without being asked
|
||||
- [ ] If wizard is already complete, no wizard prompt appears — Claude behaves normally
|
||||
- [ ] Works on both existing projects with code and bare projects with no code
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
name: "strip_prefix_ci panics on multi-byte UTF-8 input"
|
||||
---
|
||||
|
||||
# Bug 437: strip_prefix_ci panics on multi-byte UTF-8 input
|
||||
|
||||
## Description
|
||||
|
||||
The `strip_prefix_ci` function in `server/src/chat/transport/matrix/assign.rs` slices the input string at `prefix.len()` bytes without checking that the offset is a valid UTF-8 char boundary. When the input message starts with multi-byte characters (e.g. `⏺` which is 3 bytes), the slice can land mid-character, causing a panic.
|
||||
|
||||
## How to Reproduce
|
||||
|
||||
Send a Matrix message to the bot that starts with a multi-byte UTF-8 character (e.g. `⏺ storkit - wizard_confirm`) where the bot name byte length falls inside a multi-byte character.
|
||||
|
||||
## Actual Result
|
||||
|
||||
Thread panics: `byte index 6 is not a char boundary; it is inside '⏺' (bytes 4..7)`
|
||||
|
||||
## Expected Result
|
||||
|
||||
The function should return `None` (no match) without panicking, since an ASCII bot name cannot match a slice containing multi-byte characters.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] strip_prefix_ci checks is_char_boundary before slicing
|
||||
- [ ] No panic when input contains multi-byte UTF-8 characters at the prefix boundary
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: "Slash command autocomplete in web UI text input"
|
||||
---
|
||||
|
||||
# Story 438: Slash command autocomplete in web UI text input
|
||||
|
||||
## User Story
|
||||
|
||||
As a user, I want to type `/` at the start of the text box and see a filtered list of available slash commands, so that I can discover and quickly invoke commands without memorizing them.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Typing `/` at position 0 in the ChatInput textarea shows a command picker overlay above the input
|
||||
- [ ] The overlay lists all slash commands with name and description
|
||||
- [ ] Typing further characters after `/` fuzzy-filters the list
|
||||
- [ ] Arrow keys navigate the list, Tab/Enter selects, Escape dismisses
|
||||
- [ ] Selecting a command inserts `/<command> ` into the input (with trailing space)
|
||||
- [ ] Command list is a single shared source of truth used by both the picker and HelpOverlay
|
||||
- [ ] The overlay follows the same visual style as the existing file picker (@-mention overlay)
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
---
|
||||
name: "Deduplicate strip_prefix_ci / strip_bot_mention into chat::util"
|
||||
---
|
||||
|
||||
# Refactor 439: Deduplicate strip_prefix_ci / strip_bot_mention into chat::util
|
||||
|
||||
## Current State
|
||||
|
||||
- TBD
|
||||
|
||||
## Desired State
|
||||
|
||||
Eight Matrix transport files (assign.rs, delete.rs, start.rs, rebuild.rs, reset.rs, rmtree.rs, htop.rs, timer.rs) each contain their own private copies of `strip_prefix_ci` and `strip_bot_mention`. The canonical versions already live in `chat::util` with the correct `is_char_boundary` guard. The duplicates should be removed and all call sites should use `util::strip_bot_mention` instead.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] All 8 private copies of strip_prefix_ci are removed
|
||||
- [ ] All 8 private copies of strip_bot_mention are removed
|
||||
- [ ] All call sites use chat::util::strip_bot_mention instead
|
||||
- [ ] Existing tests in util.rs continue to pass
|
||||
- [ ] No new copies of strip_prefix_ci exist outside util.rs
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
---
|
||||
name: "Consolidate is_permission_approval into chat::util"
|
||||
---
|
||||
|
||||
# Refactor 440: Consolidate is_permission_approval into chat::util
|
||||
|
||||
## Current State
|
||||
|
||||
- TBD
|
||||
|
||||
## Desired State
|
||||
|
||||
Three copies of `is_permission_approval` exist across Slack (`chat/transport/slack/commands.rs`), WhatsApp (`chat/transport/whatsapp/commands.rs`), and Matrix (`chat/transport/matrix/bot/messages.rs`). The Slack and WhatsApp versions are identical; the Matrix version is a superset that also strips @mentions. Consolidate into a single `pub` function in `chat::util` using the Matrix superset behavior, then delete the 3 private copies.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Single pub fn is_permission_approval exists in chat::util
|
||||
- [ ] All 3 private copies are removed
|
||||
- [ ] Matrix @mention-stripping behavior is preserved in the shared version
|
||||
- [ ] All call sites use the shared version
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: "Deduplicate get_project_root wrappers in io modules"
|
||||
---
|
||||
|
||||
# Refactor 441: Deduplicate get_project_root wrappers in io modules
|
||||
|
||||
## Current State
|
||||
|
||||
- TBD
|
||||
|
||||
## Desired State
|
||||
|
||||
Both `io/shell.rs` and `io/search.rs` contain identical private one-liner wrappers around `state.get_project_root()`. Either inline the call at each usage site or create a single shared helper, then delete the duplicate wrappers.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] No duplicate private get_project_root wrappers in io/shell.rs and io/search.rs
|
||||
- [ ] All call sites use the canonical version or inline the call
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: "Deduplicate stage_display_name into shared module"
|
||||
---
|
||||
|
||||
# Refactor 442: Deduplicate stage_display_name into shared module
|
||||
|
||||
## Current State
|
||||
|
||||
- TBD
|
||||
|
||||
## Desired State
|
||||
|
||||
`stage_display_name` has a `pub fn` in `chat/transport/matrix/notifications.rs` and a private copy in `chat/transport/matrix/delete.rs` with slightly different casing ("backlog" vs "Backlog", "in-progress" vs "Current"). The delete.rs copy should use the canonical version from notifications.rs, adjusting the callsite if the casing difference matters.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Private stage_display_name in delete.rs is removed
|
||||
- [ ] delete.rs uses the pub version from notifications.rs
|
||||
- [ ] Display casing is consistent or callsite is adjusted to handle the difference
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: "Extract shared find_story_name from commands"
|
||||
---
|
||||
|
||||
# Refactor 443: Extract shared find_story_name from commands
|
||||
|
||||
## Current State
|
||||
|
||||
- TBD
|
||||
|
||||
## Desired State
|
||||
|
||||
`find_story_name` is nearly identical in `chat/commands/overview.rs` and `chat/commands/unreleased.rs` (minor style diff: `let stages` vs `const STAGES`). Extract to a shared location (e.g. `chat::commands::util` or `io::stories`) and have both callers use it.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Single shared find_story_name function exists
|
||||
- [ ] Both overview.rs and unreleased.rs use the shared version
|
||||
- [ ] Private copies are removed
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
---
|
||||
name: "Extract shared test helpers (test_ctx, write_story_file, make_api)"
|
||||
agent: "coder-opus"
|
||||
---
|
||||
|
||||
# Refactor 444: Extract shared test helpers (test_ctx, write_story_file, make_api)
|
||||
|
||||
## Current State
|
||||
|
||||
- TBD
|
||||
|
||||
## Desired State
|
||||
|
||||
Several test helper functions are copy-pasted across many test modules: `test_ctx` (10 copies across http/ modules), `write_story_file` (5 copies across chat/commands/ and matrix/), `make_api` (5 copies across http/ modules), `setup_project` (3 copies in io/). Extract each into a shared `#[cfg(test)]` utility module so test scaffolding is maintained in one place.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] test_ctx has a single shared definition used by all 10 http test modules
|
||||
- [ ] write_story_file has a single shared definition used by all 5 callers
|
||||
- [ ] make_api has a single shared definition used by all 5 callers
|
||||
- [ ] setup_project has a single shared definition used by all 3 callers
|
||||
- [ ] All private copies in individual test modules are removed
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
---
|
||||
name: "Rate-limited mergemaster exits advance stories to done without merging"
|
||||
---
|
||||
|
||||
# Bug 445: Rate-limited mergemaster exits advance stories to done without merging
|
||||
|
||||
## Description
|
||||
|
||||
When the mergemaster agent is immediately rate-limited (zero turns, zero tool calls), it exits and run_server_owned_completion runs acceptance gates on the existing worktree. Since the coder already committed working code, the gates pass, and the pipeline advances the story to done — even though the mergemaster never executed run_squash_merge and the code was never cherry-picked onto master.
|
||||
|
||||
## How to Reproduce
|
||||
|
||||
Observed on stories 439 and 442. All mergemaster log entries show: init → rate_limit_event → error result. Zero turns, zero MCP tool calls, duration under 350ms. Yet both stories ended up in done with no merge commit on master.
|
||||
|
||||
## Actual Result
|
||||
|
||||
Stories advance to done with no code on master. The mergemaster never ran but the pipeline treated its exit as a successful completion.
|
||||
|
||||
## Expected Result
|
||||
|
||||
If the mergemaster exits without completing its work (no merge commit produced), the story should stay in the merge stage for retry, not advance to done.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] run_server_owned_completion must not run for mergemaster agents — mergemaster has its own completion path via start_merge_agent_work
|
||||
- [ ] If the mergemaster process exits without producing a SquashMergeResult, the story stays in merge stage
|
||||
- [ ] Rate-limited mergemaster exits are treated as transient failures, not gate-passing completions
|
||||
- [ ] Story remains eligible for retry when mergemaster fails due to rate limiting
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: "OAuth login button in web UI"
|
||||
---
|
||||
|
||||
# Story 446: OAuth login button in web UI
|
||||
|
||||
## User Story
|
||||
|
||||
As a user of the storkit web UI, I want a login button that triggers the Anthropic OAuth flow, so that I can authenticate without manually navigating to /oauth/authorize.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Web UI shows a login/authenticate button when no OAuth token is active
|
||||
- [ ] Clicking the button navigates to /oauth/authorize which starts the Anthropic OAuth flow
|
||||
- [ ] After successful OAuth callback, the UI updates to show the authenticated state
|
||||
- [ ] If already authenticated, the button is hidden or shows the current auth status
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: "Element tab-completion display name breaks bot command matching"
|
||||
---
|
||||
|
||||
# Bug 447: Element tab-completion display name breaks bot command matching
|
||||
|
||||
## Description
|
||||
|
||||
When a user tab-completes a bot mention in Element, the Matrix client inserts the display name (e.g. `timmy ⚡️`) rather than the user ID (`@timmy`). If the display name contains emoji or special characters, the `strip_bot_mention` function in chat::util may fail to match it against the bot name, causing commands like `ambient on` to not be recognized.
|
||||
|
||||
## How to Reproduce
|
||||
|
||||
1. Set bot display_name to include emoji (e.g. `timmy ⚡️`) in bot.toml\n2. In Element, tab-complete the bot name to get `timmy ⚡️`\n3. Send `timmy ⚡️ ambient on`\n4. The bot does not respond — command not matched
|
||||
|
||||
## Actual Result
|
||||
|
||||
Bot ignores the command. The display name with emoji doesn't match during strip_bot_mention, so the command text is not correctly extracted.
|
||||
|
||||
## Expected Result
|
||||
|
||||
Bot should recognize commands regardless of whether the mention was tab-completed with the display name (including emoji) or typed manually as @localpart.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] strip_bot_mention handles display names containing emoji and special characters
|
||||
- [ ] strip_bot_mention handles Element's tab-completion format (display name followed by colon or comma)
|
||||
- [ ] Commands work whether the user types @timmy, timmy, or tab-completes timmy ⚡️
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: "Send OAuth login link via chat when credentials are missing"
|
||||
---
|
||||
|
||||
# Story 448: Send OAuth login link via chat when credentials are missing
|
||||
|
||||
## User Story
|
||||
|
||||
As a storkit user on Matrix or WhatsApp, I want the bot to send me a clickable OAuth authorize link when credentials are missing or expired, so that I can authenticate without terminal access or manually constructing the URL.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] When storkit detects missing or expired credentials during a chat interaction, it sends the user a clickable /oauth/authorize link
|
||||
- [ ] Works on Matrix and WhatsApp transports
|
||||
- [ ] After successful OAuth callback, the user can immediately resume chatting without restarting storkit
|
||||
- [ ] If credentials are already valid, no login link is sent
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- TBD
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: "OAuth callback URL ignores --port CLI flag"
|
||||
---
|
||||
|
||||
# Bug 449: OAuth callback URL ignores --port CLI flag
|
||||
|
||||
## Description
|
||||
|
||||
OAuthState is initialized with `resolve_port()` (reads STORKIT_PORT env var, defaults to 3001) instead of the actual port the server is listening on. When the server is started with `--port 4000`, the OAuth callback URL is still generated as `http://localhost:3001/callback`, so the Anthropic redirect lands on the wrong server and the state parameter lookup fails with "Unknown or expired state parameter".
|
||||
|
||||
## How to Reproduce
|
||||
|
||||
Start storkit with `--port 4000` (without setting STORKIT_PORT env var). Click the OAuth login button in the web UI. Authenticate with Anthropic. The callback redirect goes to localhost:3001 instead of localhost:4000.
|
||||
|
||||
## Actual Result
|
||||
|
||||
Callback hits port 3001 (or wrong port). If a different storkit is running there, it returns "Invalid State". If nothing is running there, the page fails to load.
|
||||
|
||||
## Expected Result
|
||||
|
||||
Callback URL should use the actual server port (from --port CLI flag), so the redirect returns to the correct server instance.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] build_routes receives the actual listening port and passes it to OAuthState::new
|
||||
- [ ] OAuth callback URL matches the port the server is actually listening on
|
||||
- [ ] Works with --port flag, STORKIT_PORT env var, and default port
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: "Web UI silently swallows chat errors including OAuth login link"
|
||||
---
|
||||
|
||||
# Bug 450: Web UI silently swallows chat errors including OAuth login link
|
||||
|
||||
## Description
|
||||
|
||||
When the WebSocket chat returns an error (e.g. OAuth authentication failed with a login URL), the `onError` handler in `Chat.tsx` only logs to `console.error` and resets loading state. The error message is never displayed to the user. This means the OAuth login link from story #448 works on Matrix/WhatsApp but is invisible in the web UI.
|
||||
|
||||
## How to Reproduce
|
||||
|
||||
Use the web UI with missing or expired OAuth credentials. Send any chat message. The server detects auth failure, attempts token refresh, fails, and returns an error containing a login URL over WebSocket.
|
||||
|
||||
## Actual Result
|
||||
|
||||
Nothing visible happens. The error is logged to browser console only. The user sees no feedback.
|
||||
|
||||
## Expected Result
|
||||
|
||||
The error message (including the clickable OAuth login link) should be displayed in the chat as an assistant message so the user can act on it.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] WebSocket error messages are displayed in the chat UI as assistant messages
|
||||
- [ ] OAuth login URL in the error is rendered as a clickable link
|
||||
- [ ] Consistent with how Matrix and WhatsApp transports display the same error
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user