423 Commits

Author SHA1 Message Date
Dave
8074e3b420 fix: suppress unused variable TS errors that block release build
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 16:43:42 +00:00
Dave
8a6eeacb5e fix: suppress unused variable TS errors that block release build
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 16:41:04 +00:00
Dave
eb9707d8b6 story-kit: done 311_story_server_enforced_retry_limits_for_failed_merge_and_empty_diff_stories 2026-03-19 16:36:21 +00:00
Dave
3b887e3085 story-kit: merge 311_story_server_enforced_retry_limits_for_failed_merge_and_empty_diff_stories 2026-03-19 16:36:18 +00:00
Dave
662e00f94a fix: biome formatting and lint fixes in App.tsx and TokenUsagePage.tsx
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 16:14:44 +00:00
Dave
932325744c fix: return arrays for list endpoints in test fetch mock
Prevents "agentList is not iterable" warnings in test output.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 16:09:02 +00:00
Dave
3ced187aaa fix: mock fetch in test setup to suppress URL parse errors in jsdom
Also set jsdom base URL to http://localhost:3000 in vitest config.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 16:07:56 +00:00
Dave
64f73e24bf story-kit: done 310_story_bot_delete_command_removes_a_story_from_the_pipeline 2026-03-19 16:05:03 +00:00
Dave
c4282ab2fa story-kit: merge 310_story_bot_delete_command_removes_a_story_from_the_pipeline 2026-03-19 16:05:00 +00:00
Dave
a23fe71232 story-kit: accept 307_story_configurable_coder_pool_size_and_default_model_in_project_toml 2026-03-19 16:01:38 +00:00
Dave
84b1c24073 story-kit: done 307_story_configurable_coder_pool_size_and_default_model_in_project_toml 2026-03-19 16:00:37 +00:00
Dave
429597cbce story-kit: merge 307_story_configurable_coder_pool_size_and_default_model_in_project_toml 2026-03-19 16:00:35 +00:00
Dave
101b365354 story-kit: accept 309_story_show_token_cost_breakdown_in_expanded_work_item_detail_panel 2026-03-19 15:55:39 +00:00
Dave
ca3d5ee7a6 story-kit: create 312_bug_auto_assign_assigns_mergemaster_to_coding_stage_stories 2026-03-19 15:49:38 +00:00
Dave
4af9507764 story-kit: create 312_bug_auto_assign_assigns_mergemaster_to_coding_stage_stories 2026-03-19 15:48:41 +00:00
Dave
b71e8dd2be story-kit: create 310_story_bot_delete_command_removes_a_story_from_the_pipeline 2026-03-19 15:43:11 +00:00
Dave
a6621a7095 story-kit: done 309_story_show_token_cost_breakdown_in_expanded_work_item_detail_panel 2026-03-19 15:42:50 +00:00
Dave
ce380ffb52 story-kit: merge 309_story_show_token_cost_breakdown_in_expanded_work_item_detail_panel 2026-03-19 15:42:47 +00:00
Dave
be9c15efe0 story-kit: create 311_story_server_enforced_retry_limits_for_failed_merge_and_empty_diff_stories 2026-03-19 15:42:23 +00:00
Dave
76e3bf952e story-kit: accept 298_story_bot_htop_command_with_live_updating_process_dashboard 2026-03-19 15:33:32 +00:00
Dave
d6858b690b story-kit: done 306_story_replace_manual_qa_boolean_with_configurable_qa_mode_field 2026-03-19 11:58:50 +00:00
Dave
2067abb2e5 story-kit: merge 306_story_replace_manual_qa_boolean_with_configurable_qa_mode_field 2026-03-19 11:58:47 +00:00
Dave
a058fa5f19 story-kit: create 310_story_bot_delete_command_removes_a_story_from_the_pipeline 2026-03-19 11:57:28 +00:00
Dave
62c2c531e6 story-kit: create 309_story_show_token_cost_breakdown_in_expanded_work_item_detail_panel 2026-03-19 11:55:12 +00:00
Dave
f266bb1d03 story-kit: create 308_bug_token_cost_breakdown_missing_from_expanded_work_item_detail_panel 2026-03-19 11:54:05 +00:00
Dave
7c9261da41 story-kit: create 307_story_configurable_coder_pool_size_and_default_model_in_project_toml 2026-03-19 11:42:35 +00:00
Dave
0eac4ca966 story-kit: done 303_story_bot_cost_command_with_story_filter_for_detailed_breakdown 2026-03-19 11:40:57 +00:00
Dave
a70f6b01e0 story-kit: create 307_story_configurable_coder_pool_size_and_default_model_in_project_toml 2026-03-19 11:39:57 +00:00
Dave
4545b57160 story-kit: done 301_story_dedicated_token_usage_page_in_web_ui 2026-03-19 11:36:16 +00:00
Dave
a6ac6497e9 story-kit: merge 301_story_dedicated_token_usage_page_in_web_ui 2026-03-19 11:36:12 +00:00
Dave
586d06b840 story-kit: create 306_story_replace_manual_qa_boolean_with_configurable_qa_mode_field 2026-03-19 11:30:19 +00:00
Dave
c67f148383 story-kit: done 305_story_bot_show_command_displays_story_text_in_chat 2026-03-19 11:12:37 +00:00
Dave
f88114edbf story-kit: merge 305_story_bot_show_command_displays_story_text_in_chat 2026-03-19 11:12:35 +00:00
Dave
08c7a92d74 story-kit: done 300_story_show_token_cost_badge_on_pipeline_board_work_items 2026-03-19 11:02:14 +00:00
Dave
36535b639f story-kit: merge 300_story_show_token_cost_badge_on_pipeline_board_work_items 2026-03-19 11:02:12 +00:00
Dave
b6f99ce7a2 story-kit: done 304_story_mcp_tool_to_move_stories_between_pipeline_stages 2026-03-19 10:59:50 +00:00
Dave
f4376b01e1 story-kit: merge 304_story_mcp_tool_to_move_stories_between_pipeline_stages 2026-03-19 10:59:47 +00:00
Dave
e7aa4e028e story-kit: create 305_story_bot_show_command_displays_story_text_in_chat 2026-03-19 10:56:25 +00:00
Dave
52c5cc9b72 story-kit: done 302_story_bot_cost_command_shows_total_and_per_story_token_spend 2026-03-19 10:54:09 +00:00
Dave
c327263254 story-kit: merge 302_story_bot_cost_command_shows_total_and_per_story_token_spend 2026-03-19 10:54:04 +00:00
Dave
7c9b86c31b story-kit: create 304_story_mcp_tool_to_move_stories_between_pipeline_stages 2026-03-19 10:44:14 +00:00
Dave
a2c893420b story-kit: create 304_story_mcp_tool_to_move_stories_between_pipeline_stages 2026-03-19 10:43:10 +00:00
Dave
a1fe5356cf story-kit: create 301_story_dedicated_token_usage_page_in_web_ui 2026-03-19 10:41:49 +00:00
Dave
1477fbc02b story-kit: create 303_story_bot_cost_command_with_story_filter_for_detailed_breakdown 2026-03-19 10:40:42 +00:00
Dave
6a74eefd07 chore: gitignore token_usage.jsonl
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 10:35:37 +00:00
Dave
981fd3fd81 story-kit: merge 298_story_bot_htop_command_with_live_updating_process_dashboard
Adds htop bot command with live-updating Matrix message showing system
load and per-agent CPU/memory usage. Supports timeout override and
htop stop. Resolved conflict with git command in commands.rs registry.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 10:33:21 +00:00
Dave
99d301b467 story-kit: done 298_story_bot_htop_command_with_live_updating_process_dashboard 2026-03-19 10:29:35 +00:00
Dave
9ed80384d5 story-kit: create 302_story_bot_cost_command_shows_total_and_per_story_token_spend 2026-03-19 10:14:01 +00:00
Dave
c2cda92337 story-kit: done 299_story_bot_git_status_command_shows_working_tree_and_branch_info 2026-03-19 10:11:30 +00:00
Dave
b25ae42737 story-kit: merge 299_story_bot_git_status_command_shows_working_tree_and_branch_info 2026-03-19 10:11:28 +00:00
Dave
7811130a8b story-kit: create 300_story_show_token_cost_badge_on_pipeline_board_work_items 2026-03-19 10:10:27 +00:00
Dave
ec212cb5a2 story-kit: create 303_story_bot_cost_command_with_story_filter_for_detailed_breakdown 2026-03-19 10:08:57 +00:00
Dave
d174bb41e7 story-kit: create 302_story_bot_cost_command_shows_total_and_per_story_token_spend 2026-03-19 10:08:48 +00:00
Dave
40570888ff story-kit: create 301_story_dedicated_token_usage_page_in_web_ui 2026-03-19 10:08:35 +00:00
Dave
dd75e9e0fa story-kit: create 300_story_show_token_cost_badge_on_pipeline_board_work_items 2026-03-19 10:08:26 +00:00
Dave
c2aa9ef134 story-kit: accept 295_bug_stories_stuck_in_qa_when_qa_agent_is_busy 2026-03-19 09:58:55 +00:00
Dave
501d6d31ff story-kit: create 299_story_bot_git_status_command_shows_working_tree_and_branch_info 2026-03-19 09:58:06 +00:00
Dave
db2f8fcfc5 story-kit: done 295_bug_stories_stuck_in_qa_when_qa_agent_is_busy 2026-03-19 09:57:32 +00:00
Dave
f325ddf9fe story-kit: done 296_story_track_per_agent_token_usage_for_cost_visibility_and_optimisation 2026-03-19 09:55:28 +00:00
Dave
9cdb0d4ea8 story-kit: merge 296_story_track_per_agent_token_usage_for_cost_visibility_and_optimisation 2026-03-19 09:55:25 +00:00
Dave
6c413e1fc7 fix: call auto_assign_available_work after every pipeline advance (bug 295)
Stories got stuck in QA/merge when agents were busy at assignment time.
Consolidates auto_assign into a single unconditional call at the end of
run_pipeline_advance, so whenever any agent completes, the system
immediately scans for pending work and assigns free agents.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 09:53:41 +00:00
Dave
28b29b55a8 story-kit: accept 294_story_rename_app_title_from_story_kit_to_storkit 2026-03-19 09:47:31 +00:00
Dave
376de57252 story-kit: done 294_story_rename_app_title_from_story_kit_to_storkit 2026-03-19 09:46:31 +00:00
Dave
63f46751ac story-kit: merge 294_story_rename_app_title_from_story_kit_to_storkit 2026-03-19 09:46:28 +00:00
Dave
dc9df6d497 story-kit: create 298_story_bot_htop_command_with_live_updating_process_dashboard 2026-03-19 09:41:26 +00:00
Dave
ae7b04fac5 story-kit: create 298_story_bot_htop_command_with_live_updating_process_dashboard 2026-03-19 09:39:38 +00:00
Dave
50959e6b67 story-kit: done 297_story_improve_bot_status_command_formatting 2026-03-19 09:39:08 +00:00
Dave
6353b12c1d story-kit: merge 297_story_improve_bot_status_command_formatting 2026-03-19 09:39:05 +00:00
Dave
170fd53808 story-kit: create 296_story_track_per_agent_token_usage_for_cost_visibility_and_optimisation 2026-03-19 09:37:21 +00:00
Dave
597e6bf1c3 story-kit: create 298_story_bot_htop_command_with_live_updating_process_dashboard 2026-03-19 09:36:14 +00:00
Dave
7a5a56f211 story-kit: create 297_story_improve_bot_status_command_formatting 2026-03-19 09:26:32 +00:00
Dave
73c86b6946 story-kit: merge 293_story_register_all_bot_commands_in_the_command_registry
Moves status, ambient, and help commands into a unified command registry
in commands.rs. Help output now automatically lists all registered
commands. Resolved merge conflict with 1_backlog rename.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 09:14:04 +00:00
Dave
11afd21f17 story-kit: create 296_story_track_per_agent_token_usage_for_cost_visibility_and_optimisation 2026-03-19 09:03:46 +00:00
Dave
1d20cfc679 story-kit: create 296_story_track_per_agent_token_usage_for_cost_visibility_and_optimisation 2026-03-19 08:59:46 +00:00
Dave
959c680e10 story-kit: accept 293_story_register_all_bot_commands_in_the_command_registry 2026-03-19 08:37:52 +00:00
Dave
dd377de7db story-kit: done 293_story_register_all_bot_commands_in_the_command_registry 2026-03-19 08:37:47 +00:00
Dave
9fee4d9478 story-kit: accept 292_story_show_server_logs_in_web_ui 2026-03-19 01:32:28 +00:00
Dave
40c04fcb28 story-kit: done 292_story_show_server_logs_in_web_ui 2026-03-19 01:31:28 +00:00
Dave
2f0d796b38 story-kit: merge 292_story_show_server_logs_in_web_ui 2026-03-19 01:31:25 +00:00
Dave
2346602b30 fix: default manual_qa to false so stories advance automatically
Bug 283 was implemented with manual_qa defaulting to true, causing all
stories to hold in QA for human review. Changed to default false as
originally specified — stories advance automatically unless explicitly
opted in with manual_qa: true.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 00:53:00 +00:00
Dave
13c0ee4c08 story-kit: create 295_bug_stories_stuck_in_qa_when_qa_agent_is_busy 2026-03-18 21:24:11 +00:00
Dave
483dca5b95 story-kit: create 295_bug_stories_stuck_in_qa_when_qa_agent_is_busy 2026-03-18 21:23:04 +00:00
Dave
dc7d070101 story-kit: create 294_story_rename_app_title_from_story_kit_to_storkit 2026-03-18 20:43:09 +00:00
Dave
875d1f88aa story-kit: accept 291_story_show_test_results_in_work_item_detail_panel 2026-03-18 20:39:16 +00:00
Dave
f550018987 Updated toml to 1.0.7 2026-03-18 16:38:16 +00:00
Dave
52ec989c3a Fixed some bot tests. 2026-03-18 16:37:23 +00:00
Dave
d080e8b12d story-kit: accept 273_story_matrix_bot_sends_typing_indicator_while_waiting_for_claude_response 2026-03-18 16:11:51 +00:00
Dave
cfd85d3a0e story-kit: accept 284_story_matrix_bot_status_command_shows_pipeline_and_agent_availability 2026-03-18 16:06:45 +00:00
Dave
070d53068e story-kit: accept 283_bug_pipeline_does_not_check_manual_qa_flag_before_advancing_from_qa_to_merge 2026-03-18 16:00:44 +00:00
Dave
fa8e0f39f6 story-kit: create 293_story_register_all_bot_commands_in_the_command_registry 2026-03-18 15:57:14 +00:00
Dave
503fa6b7bf fix: rename remaining 1_upcoming references to 1_backlog in bot.rs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 15:53:36 +00:00
Dave
51a0fb8297 story-kit: accept 282_story_matrix_bot_ambient_mode_toggle_via_chat_command 2026-03-18 15:52:35 +00:00
Dave
8ac85a0b67 chore: commit pending changes from session
- Add permission rules to .claude/settings.json
- Document empty merge and direct-to-master problems in problems.md
- Fix agent stream URL to use vite proxy instead of hardcoded host
- Add /agents proxy config to vite.config.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 15:50:20 +00:00
Dave
aa4e042e32 story-kit: done 247_story_human_qa_gate_with_rejection_flow 2026-03-18 15:47:51 +00:00
Dave
9352443555 story-kit: merge 247_story_human_qa_gate_with_rejection_flow 2026-03-18 15:47:48 +00:00
Dave
1faacd7812 story-kit: done 291_story_show_test_results_in_work_item_detail_panel 2026-03-18 15:43:41 +00:00
Dave
7451cb7170 story-kit: done 290_story_show_agent_output_stream_in_expanded_work_item_detail_panel 2026-03-18 15:32:01 +00:00
Dave
83ccfece81 story-kit: merge 290_story_show_agent_output_stream_in_expanded_work_item_detail_panel 2026-03-18 15:31:58 +00:00
Dave
68bf179407 story-kit: create 292_story_show_server_logs_in_web_ui 2026-03-18 15:27:56 +00:00
Dave
c35c05d02c story-kit: done 289_bug_rebuild_and_restart_mcp_tool_does_not_rebuild 2026-03-18 15:27:37 +00:00
Dave
3adae6c475 story-kit: merge 289_bug_rebuild_and_restart_mcp_tool_does_not_rebuild 2026-03-18 15:27:29 +00:00
Dave
c4753b51de story-kit: accept 281_story_matrix_bot_announces_itself_when_it_comes_online 2026-03-18 15:26:36 +00:00
Dave
e7a73e7322 story-kit: done 284_story_matrix_bot_status_command_shows_pipeline_and_agent_availability 2026-03-18 15:22:34 +00:00
Dave
e8ec84668f story-kit: merge 284_story_matrix_bot_status_command_shows_pipeline_and_agent_availability 2026-03-18 15:22:19 +00:00
Dave
8d9cf4b283 story-kit: create 291_story_show_test_results_in_work_item_detail_panel 2026-03-18 15:18:47 +00:00
Dave
a8cb38fe27 story-kit: done 273_story_matrix_bot_sends_typing_indicator_while_waiting_for_claude_response 2026-03-18 15:17:50 +00:00
Dave
dd83e0f4ee fix: biome formatting in Chat.test.tsx and ChatInput.tsx
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 15:07:49 +00:00
Dave
3923aafb71 story-kit: done 288_bug_ambient_mode_state_lost_on_server_restart 2026-03-18 15:01:44 +00:00
Dave
8fcfadcb04 story-kit: merge 288_bug_ambient_mode_state_lost_on_server_restart 2026-03-18 15:01:40 +00:00
Dave
7c023c6beb story-kit: done 285_story_matrix_bot_help_command_lists_available_bot_commands 2026-03-18 14:57:30 +00:00
Dave
e7bb8db7c1 story-kit: merge 285_story_matrix_bot_help_command_lists_available_bot_commands 2026-03-18 14:57:27 +00:00
Dave
727da0c6d0 story-kit: create 290_story_show_agent_output_stream_in_expanded_work_item_detail_panel 2026-03-18 14:50:35 +00:00
Dave
257ee05ac6 story-kit: accept 266_story_matrix_bot_structured_conversation_history 2026-03-18 14:49:40 +00:00
Dave
b9f3505738 story-kit: create 289_bug_rebuild_and_restart_mcp_tool_does_not_rebuild 2026-03-18 14:42:24 +00:00
Dave
be56792c6e story-kit: create 289_bug_rebuild_and_restart_mcp_tool_does_not_rebuild 2026-03-18 14:41:46 +00:00
Dave
9daaae2d43 story-kit: create 288_bug_ambient_mode_state_lost_on_server_restart 2026-03-18 14:37:16 +00:00
Dave
c85d02a3ef story-kit: done 287_story_rename_upcoming_pipeline_stage_to_backlog 2026-03-18 14:33:11 +00:00
Dave
df6f792214 story-kit: merge 287_story_rename_upcoming_pipeline_stage_to_backlog 2026-03-18 14:33:08 +00:00
Dave
967ebd7a84 story-kit: accept 271_story_show_assigned_agent_in_expanded_work_item_view 2026-03-18 14:30:43 +00:00
Dave
3bc44289b9 story-kit: accept 279_story_auto_assign_should_respect_agent_stage_when_front_matter_specifies_agent 2026-03-18 14:18:28 +00:00
Dave
17f6bae573 story-kit: done 286_story_server_self_rebuild_and_restart_via_mcp_tool 2026-03-18 14:09:13 +00:00
Dave
baa8bdcfda story-kit: merge 286_story_server_self_rebuild_and_restart_via_mcp_tool 2026-03-18 14:09:09 +00:00
Dave
33492c49fa fix: QA agents must not pkill -f story-kit (kills vite dev server)
Change pkill pattern to 'target.*story-kit' to only match the Rust
binary, not any process with story-kit in its working directory.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 14:08:07 +00:00
Dave
63a90195e7 Update Cargo.lock version to 0.3.1
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 14:06:58 +00:00
Dave
7bd390c762 story-kit: accept 92_spike_stop_auto_committing_intermediate_pipeline_moves 2026-03-18 14:06:26 +00:00
Dave
0d581ab459 story-kit: accept 278_story_auto_assign_agents_to_pipeline_items_on_server_startup 2026-03-18 14:02:46 +00:00
Dave
42f88cc172 story-kit: create 287_story_rename_upcoming_pipeline_stage_to_backlog 2026-03-18 13:52:06 +00:00
Dave
945648bf6e story-kit: create 280_story_long_running_supervisor_agent_with_periodic_pipeline_polling 2026-03-18 13:51:32 +00:00
Dave
bc5a3da2c0 story-kit: create 286_story_server_self_rebuild_and_restart_via_mcp_tool 2026-03-18 13:43:54 +00:00
Dave
04e841643e story-kit: accept 274_story_mcp_pipeline_status_tool_with_agent_assignments 2026-03-18 13:25:50 +00:00
Dave
3d97b0b95a story-kit: done 285_story_matrix_bot_help_command_lists_available_bot_commands 2026-03-18 13:07:48 +00:00
Dave
8f4cb9475c story-kit: done 284_story_matrix_bot_status_command_shows_pipeline_and_agent_availability 2026-03-18 13:01:51 +00:00
Dave
8f63cfda07 story-kit: done 279_story_auto_assign_should_respect_agent_stage_when_front_matter_specifies_agent 2026-03-18 12:58:08 +00:00
Dave
1b3843d913 story-kit: done 278_story_auto_assign_agents_to_pipeline_items_on_server_startup 2026-03-18 12:50:48 +00:00
Dave
4c898996a2 story-kit: done 274_story_mcp_pipeline_status_tool_with_agent_assignments 2026-03-18 12:50:21 +00:00
Dave
281531624d story-kit: done 283_bug_pipeline_does_not_check_manual_qa_flag_before_advancing_from_qa_to_merge 2026-03-18 12:34:46 +00:00
Dave
b09a2cbdf9 story-kit: done 273_story_matrix_bot_sends_typing_indicator_while_waiting_for_claude_response 2026-03-18 12:30:30 +00:00
Dave
a0c1457757 story-kit: done 92_spike_stop_auto_committing_intermediate_pipeline_moves 2026-03-18 12:16:09 +00:00
Dave
e818ac986d story-kit: create 92_spike_stop_auto_committing_intermediate_pipeline_moves 2026-03-18 12:15:09 +00:00
Dave
b29f6628f8 story-kit: create 278_story_auto_assign_agents_to_pipeline_items_on_server_startup 2026-03-18 12:15:02 +00:00
Dave
4dc4fef83b story-kit: create 247_story_human_qa_gate_with_rejection_flow 2026-03-18 12:14:54 +00:00
Dave
7ef85c459c story-kit: create 279_story_auto_assign_should_respect_agent_stage_when_front_matter_specifies_agent 2026-03-18 12:13:36 +00:00
Dave
f6058a50b9 story-kit: create 274_story_mcp_pipeline_status_tool_with_agent_assignments 2026-03-18 12:13:20 +00:00
Dave
d347ba084d story-kit: done 282_story_matrix_bot_ambient_mode_toggle_via_chat_command 2026-03-18 12:12:26 +00:00
Dave
b50d007b40 story-kit: merge 282_story_matrix_bot_ambient_mode_toggle_via_chat_command 2026-03-18 12:12:19 +00:00
Dave
ed3d7311d1 story-kit: create 285_story_matrix_bot_help_command_lists_available_bot_commands 2026-03-18 12:06:34 +00:00
Dave
e7aef3edc7 story-kit: create 284_story_matrix_bot_status_command_shows_pipeline_and_agent_availability 2026-03-18 12:06:02 +00:00
Dave
d5a93fe726 story-kit: create 283_bug_pipeline_does_not_check_manual_qa_flag_before_advancing_from_qa_to_merge 2026-03-18 12:00:08 +00:00
Dave
7e45a1fba0 Bump version to 0.3.1
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 11:49:22 +00:00
Dave
ad348e813f Update Cargo.lock
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 11:49:02 +00:00
Dave
de5dcceeaf story-kit: done 281_story_matrix_bot_announces_itself_when_it_comes_online 2026-03-18 11:48:19 +00:00
Dave
53fdcfec75 story-kit: merge 281_story_matrix_bot_announces_itself_when_it_comes_online 2026-03-18 11:48:15 +00:00
Dave
bad680cf24 story-kit: create 282_story_matrix_bot_ambient_mode_toggle_via_chat_command 2026-03-18 11:47:51 +00:00
Dave
a5e64ded83 fix: unused system_prompt variable warning
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 11:44:16 +00:00
Dave
77e368d354 fix: --system is not a valid Claude Code CLI flag
Removed the --system argument from the PTY runner — Claude Code CLI
doesn't support it. Bot name instruction is now prepended to the user
prompt instead of passed as a system prompt argument.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 11:42:39 +00:00
Dave
db92a78d2b story-kit: accept 276_story_detect_and_log_when_root_mcp_json_port_is_modified 2026-03-18 11:40:54 +00:00
Dave
420deebdb4 story-kit: done 276_story_detect_and_log_when_root_mcp_json_port_is_modified 2026-03-18 11:39:47 +00:00
Dave
0a6de3717a fix: Chat.test.tsx type errors from 271 merge
Cast lastSendChatArgs through unknown to satisfy TypeScript's
strict type narrowing on the nullable union type.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 11:33:54 +00:00
Dave
15645a2a3e Bump version to 0.3.0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 11:31:19 +00:00
Dave
eab65de723 story-kit: create 281_story_matrix_bot_announces_itself_when_it_comes_online 2026-03-18 11:26:29 +00:00
Dave
81a5660f11 story-kit: merge 277 - bot uses configured display_name
Adds system_prompt parameter to chat_stream so the Matrix bot
passes "Your name is {name}" to Claude Code. Reads display_name
from bot.toml config. Resolved conflicts by integrating bot_name
into master's permission-handling code structure.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 11:23:50 +00:00
Dave
4bf01c6cca fix: session_id wiped on every trim, breaking --resume
The history trimming logic cleared session_id = None whenever entries
exceeded history_size. Since the history was always at capacity, every
new message wiped the session_id immediately after storing it. This
meant --resume was never passed to Claude Code, making every turn a
fresh conversation.

Fix: preserve session_id on trim. Claude Code's --resume loads the
full conversation from its own session transcript on disk, so trimming
our local tracking entries doesn't invalidate the session.

Also adds debug logging for session_id capture/storage (temporary).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 11:19:00 +00:00
Dave
a799009720 story-kit: done 279_story_auto_assign_should_respect_agent_stage_when_front_matter_specifies_agent 2026-03-18 11:10:41 +00:00
Dave
549c23bd77 story-kit: done 278_story_auto_assign_agents_to_pipeline_items_on_server_startup 2026-03-18 11:05:14 +00:00
Dave
34be4d1e75 story-kit: done 277_story_matrix_bot_uses_its_configured_name_instead_of_claude 2026-03-18 11:00:39 +00:00
Dave
a390861520 story-kit: done 273_story_matrix_bot_sends_typing_indicator_while_waiting_for_claude_response 2026-03-18 10:55:58 +00:00
Dave
ce9bdbbb9d story-kit: done 266_story_matrix_bot_structured_conversation_history 2026-03-18 10:48:55 +00:00
Dave
5f4591f496 fix: update should_commit_stage test to match 5_done in COMMIT_WORTHY_STAGES
The test was asserting 5_done should NOT trigger commits, but commit 74dc42c
added 5_done to COMMIT_WORTHY_STAGES. Updated test to match.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 10:46:11 +00:00
Dave
dc7968ffbc story-kit: mergemaster conflict resolution, vite proxy fix, bug 279
- Upgrade mergemaster prompt to resolve complex conflicts itself
  instead of just reporting failure. Includes instructions to check
  git history and story files for context before resolving.
- Add proxy error handler to vite config to prevent crashes on
  backend ECONNREFUSED.
- Fix bug 279: auto-assign now checks that preferred agent's stage
  matches the pipeline stage. Coders won't be assigned to QA/merge.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 10:44:10 +00:00
Dave
5fedd9130a story-kit: create 280_story_long_running_supervisor_agent_with_periodic_pipeline_polling 2026-03-18 10:39:51 +00:00
Dave
c7e371c124 story-kit: create 280_story_long_running_supervisor_agent_with_periodic_pipeline_polling 2026-03-18 10:36:20 +00:00
Dave
8748d7d49a story-kit: create 280_story_long_running_supervisor_agent_with_periodic_pipeline_polling 2026-03-18 10:34:18 +00:00
Dave
825d36c204 fix: remove stale 266 from archived (came from 271 branch history)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 10:30:47 +00:00
Dave
65a8feff17 story-kit: done 271_story_show_assigned_agent_in_expanded_work_item_view 2026-03-18 10:29:45 +00:00
Dave
60dabae795 story-kit: merge 271_story_show_assigned_agent_in_expanded_work_item_view
Adds assigned agent display to the expanded work item detail panel.
Resolved conflicts by keeping master versions of bot.rs (permission
handling), ChatInput.tsx, and fs.rs. Removed duplicate list_project_files
endpoint and tests from io.rs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 10:29:37 +00:00
Dave
1bae7bd223 story-kit: create 266_story_matrix_bot_structured_conversation_history 2026-03-18 10:21:55 +00:00
Dave
a0091e81f9 story-kit: create 266_story_matrix_bot_structured_conversation_history 2026-03-18 10:21:33 +00:00
Dave
beb5ea9f53 fix: revert broken auto-merge of 266 in bot.rs
The mergemaster's auto-resolver inserted a duplicate chat_stream call
inside the tokio::select! permission loop, producing mismatched braces.
The 266 merge didn't contribute useful code changes to bot.rs — the
session_id handling was already present.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 10:20:40 +00:00
Dave
89e96dc0a6 story-kit: done 266_story_matrix_bot_structured_conversation_history 2026-03-18 10:18:50 +00:00
Dave
0c686ba170 story-kit: merge 266_story_matrix_bot_structured_conversation_history 2026-03-18 10:18:49 +00:00
Dave
74dc42c1fc story-kit: add 5_done to commit-worthy stages
Keep done stage committed so accepted work is in git history.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 10:13:35 +00:00
Dave
a3d22fd874 story-kit: gitignore intermediate pipeline stages (spike 92 follow-up)
Ignore 2_current/, 3_qa/, and 4_merge/ in git since spike 92 stopped
committing intermediate pipeline moves. Keep 5_done/ tracked so
accepted work is in git history before sweep to archived.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 10:13:10 +00:00
Dave
8561910cd8 story-kit: start 266_story_matrix_bot_structured_conversation_history 2026-03-18 10:09:26 +00:00
Dave
e569c1bcad story-kit: start 266_story_matrix_bot_structured_conversation_history 2026-03-18 10:08:19 +00:00
Dave
4dcb24d5dd story-kit: queue 273_story_matrix_bot_sends_typing_indicator_while_waiting_for_claude_response for merge 2026-03-18 10:07:12 +00:00
Dave
59f37e13b9 story-kit: done 92_spike_stop_auto_committing_intermediate_pipeline_moves 2026-03-18 10:07:05 +00:00
Dave
3a1d7012b4 spike(92): stop auto-committing intermediate pipeline moves
Filter flush_pending() to only git-commit for terminal stages
(1_upcoming and 6_archived) while still broadcasting WatcherEvents
for all stages so the frontend stays in sync.

Reduces pipeline commits from 5+ to 2 per story run. No system
dependencies on intermediate commits were found.

Preserves merge_failure front matter cleanup from master.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 10:06:58 +00:00
Dave
41b24e4b7a story-kit: queue 266_story_matrix_bot_structured_conversation_history for merge 2026-03-18 10:05:09 +00:00
Dave
06948dae74 story-kit: queue 273_story_matrix_bot_sends_typing_indicator_while_waiting_for_claude_response for QA 2026-03-18 10:03:09 +00:00
Dave
bbd4aee828 story-kit: start 271_story_show_assigned_agent_in_expanded_work_item_view 2026-03-18 10:03:06 +00:00
Dave
d40f007818 story-kit: start 278_story_auto_assign_agents_to_pipeline_items_on_server_startup 2026-03-18 10:01:54 +00:00
Dave
3819a02159 story-kit: start 278_story_auto_assign_agents_to_pipeline_items_on_server_startup 2026-03-18 10:01:54 +00:00
Dave
9b65845c90 story-kit: start 277_story_matrix_bot_uses_its_configured_name_instead_of_claude 2026-03-18 10:01:52 +00:00
Dave
28176727d7 story-kit: queue 271_story_show_assigned_agent_in_expanded_work_item_view for merge 2026-03-18 10:00:38 +00:00
Dave
1d59cdcc25 story-kit: start 273_story_matrix_bot_sends_typing_indicator_while_waiting_for_claude_response 2026-03-18 10:00:28 +00:00
Dave
edc6b9ea05 story-kit: queue 271_story_show_assigned_agent_in_expanded_work_item_view for merge 2026-03-18 09:57:30 +00:00
Dave
8e4a8ce57a story-kit: queue 279_story_auto_assign_should_respect_agent_stage_when_front_matter_specifies_agent for merge 2026-03-18 09:54:32 +00:00
Dave
c863ee4135 story-kit: queue 279_story_auto_assign_should_respect_agent_stage_when_front_matter_specifies_agent for merge 2026-03-18 09:53:48 +00:00
Dave
dd4a1140fe story-kit: queue 278_story_auto_assign_agents_to_pipeline_items_on_server_startup for merge 2026-03-18 09:49:53 +00:00
Dave
895317330b story-kit: queue 278_story_auto_assign_agents_to_pipeline_items_on_server_startup for merge 2026-03-18 09:48:57 +00:00
Dave
11e32f9802 story-kit: queue 277_story_matrix_bot_uses_its_configured_name_instead_of_claude for merge 2026-03-18 09:44:22 +00:00
Dave
8b7ff6383f story-kit: queue 277_story_matrix_bot_uses_its_configured_name_instead_of_claude for merge 2026-03-18 09:43:30 +00:00
Dave
964a8bfcff story-kit: queue 273_story_matrix_bot_sends_typing_indicator_while_waiting_for_claude_response for merge 2026-03-18 09:41:34 +00:00
Dave
978b84893c story-kit: queue 266_story_matrix_bot_structured_conversation_history for merge 2026-03-18 09:39:58 +00:00
Dave
7dd6821dc5 story-kit: queue 273_story_matrix_bot_sends_typing_indicator_while_waiting_for_claude_response for merge 2026-03-18 09:38:05 +00:00
Dave
6abf5c87b2 story-kit: done 247_story_human_qa_gate_with_rejection_flow 2026-03-18 09:37:38 +00:00
Dave
b682c67f97 story-kit: queue 278_story_auto_assign_agents_to_pipeline_items_on_server_startup for QA 2026-03-18 09:35:50 +00:00
Dave
81309a5559 story-kit: queue 277_story_matrix_bot_uses_its_configured_name_instead_of_claude for QA 2026-03-18 09:35:49 +00:00
Dave
2006ad6d8c story-kit: queue 279_story_auto_assign_should_respect_agent_stage_when_front_matter_specifies_agent for QA 2026-03-18 09:35:45 +00:00
Dave
41bafb80e4 story-kit: accept 275_story_matrix_bot_surfaces_claude_code_permission_prompts_to_chat 2026-03-18 09:33:21 +00:00
Dave
569380e133 story-kit: done 275_story_matrix_bot_surfaces_claude_code_permission_prompts_to_chat 2026-03-18 09:32:20 +00:00
Dave
10a5bea2b1 story-kit: merge 275_story_matrix_bot_surfaces_claude_code_permission_prompts_to_chat 2026-03-18 09:32:17 +00:00
Dave
110815c1c5 story-kit: queue 247_story_human_qa_gate_with_rejection_flow for merge 2026-03-18 09:31:02 +00:00
Dave
29fc761980 fix: skip frontend tests when frontend dir absent in sparse worktrees
script/test unconditionally cd'd into frontend/ and ran npm test, which
failed in sparse-checkout worktrees that only contain the server/ subtree.
Guard the frontend step with a directory existence check so acceptance
gates pass for worktrees that have no frontend checkout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 09:30:51 +00:00
Dave
d537aceb63 story-kit: start 278_story_auto_assign_agents_to_pipeline_items_on_server_startup 2026-03-18 09:29:43 +00:00
Dave
72b89c8ccc story-kit: start 277_story_matrix_bot_uses_its_configured_name_instead_of_claude 2026-03-18 09:29:27 +00:00
Dave
e19de02967 story-kit: start 279_story_auto_assign_should_respect_agent_stage_when_front_matter_specifies_agent 2026-03-18 09:29:00 +00:00
Dave
1c5f13e7eb story-kit: create 279_story_auto_assign_should_respect_agent_stage_when_front_matter_specifies_agent 2026-03-18 09:28:33 +00:00
Dave
816c771a2a story-kit: queue 275_story_matrix_bot_surfaces_claude_code_permission_prompts_to_chat for merge 2026-03-18 09:28:30 +00:00
Dave
642a8486cd story-kit: queue 266_story_matrix_bot_structured_conversation_history for QA 2026-03-18 09:25:33 +00:00
Dave
605bcadea7 story-kit: done 274_story_mcp_pipeline_status_tool_with_agent_assignments 2026-03-18 09:25:29 +00:00
Dave
ccc1ead8c9 story-kit: remove 274_story_mcp_pipeline_status_tool_with_agent_assignments 2026-03-18 09:25:07 +00:00
Dave
8bbbe8fbdd story-kit: start 274_story_mcp_pipeline_status_tool_with_agent_assignments 2026-03-18 09:25:01 +00:00
Dave
d9775834ed story-kit: queue 275_story_matrix_bot_surfaces_claude_code_permission_prompts_to_chat for QA 2026-03-18 09:22:43 +00:00
Dave
c32f0dce45 story-kit: queue 274_story_mcp_pipeline_status_tool_with_agent_assignments for merge 2026-03-18 09:22:39 +00:00
Dave
d864941665 story-kit: start 266_story_matrix_bot_structured_conversation_history 2026-03-18 09:21:40 +00:00
Dave
9c2d831c65 story-kit: create 266_story_matrix_bot_structured_conversation_history 2026-03-18 09:21:26 +00:00
Dave
2ab91f933f story-kit: create 266_story_matrix_bot_structured_conversation_history 2026-03-18 09:21:00 +00:00
Dave
1fcb8cb332 story-kit: create 266_story_matrix_bot_structured_conversation_history 2026-03-18 09:20:36 +00:00
Dave
3439c16e66 story-kit: create 278_story_auto_assign_agents_to_pipeline_items_on_server_startup 2026-03-18 09:18:23 +00:00
Dave
ce93987da8 story-kit: queue 274_story_mcp_pipeline_status_tool_with_agent_assignments for merge 2026-03-18 09:15:01 +00:00
Dave
bd7b7cc34a story-kit: create 277_story_matrix_bot_uses_its_configured_name_instead_of_claude 2026-03-18 09:09:49 +00:00
Dave
855452b4a2 story-kit: queue 274_story_mcp_pipeline_status_tool_with_agent_assignments for merge 2026-03-18 09:09:06 +00:00
Dave
1fcfa9123f story-kit: queue 274_story_mcp_pipeline_status_tool_with_agent_assignments for merge 2026-03-18 09:07:42 +00:00
Dave
e66b811436 story-kit: queue 273_story_matrix_bot_sends_typing_indicator_while_waiting_for_claude_response for merge 2026-03-18 09:06:00 +00:00
Dave
8d5fa85a3a story-kit: accept 264_bug_claude_code_session_id_not_persisted_across_browser_refresh 2026-03-18 09:05:30 +00:00
Dave
a4e7a23ca6 story-kit: queue 274_story_mcp_pipeline_status_tool_with_agent_assignments for QA 2026-03-17 18:43:05 +00:00
Dave
b67eea7b9a story-kit: queue 273_story_matrix_bot_sends_typing_indicator_while_waiting_for_claude_response for QA 2026-03-17 18:42:03 +00:00
Dave
4a89b46857 story-kit: start 275_story_matrix_bot_surfaces_claude_code_permission_prompts_to_chat 2026-03-17 18:33:23 +00:00
Dave
047bf83b76 story-kit: start 274_story_mcp_pipeline_status_tool_with_agent_assignments 2026-03-17 18:32:43 +00:00
Dave
62aa142409 story-kit: start 273_story_matrix_bot_sends_typing_indicator_while_waiting_for_claude_response 2026-03-17 18:32:37 +00:00
Dave
c93a2e80f9 story-kit: queue 92_spike_stop_auto_committing_intermediate_pipeline_moves for QA 2026-03-17 18:32:30 +00:00
Dave
9176fe3303 story-kit: create 276_story_detect_and_log_when_root_mcp_json_port_is_modified 2026-03-17 18:26:07 +00:00
Dave
296a59def3 story-kit: create 275_story_matrix_bot_surfaces_claude_code_permission_prompts_to_chat 2026-03-17 18:23:52 +00:00
Dave
90bb2fb137 story-kit: done 272_story_clear_merge_error_front_matter_when_story_leaves_merge_stage 2026-03-17 18:21:59 +00:00
Dave
bc0bb91a83 story-kit: merge 272_story_clear_merge_error_front_matter_when_story_leaves_merge_stage 2026-03-17 18:21:56 +00:00
Dave
0b39b2acfc story-kit: create 275_story_matrix_bot_surfaces_claude_code_permission_prompts_to_chat 2026-03-17 18:21:40 +00:00
Dave
75c27f5853 story-kit: create 273_story_matrix_bot_sends_typing_indicator_while_waiting_for_claude_response 2026-03-17 18:18:29 +00:00
Dave
349866606c story-kit: done 270_bug_qa_test_server_overwrites_root_mcp_json_with_wrong_port 2026-03-17 18:16:30 +00:00
Dave
901f7a65d3 story-kit: merge 270_bug_qa_test_server_overwrites_root_mcp_json_with_wrong_port 2026-03-17 18:16:13 +00:00
Dave
c52b41b99c story-kit: queue 272_story_clear_merge_error_front_matter_when_story_leaves_merge_stage for merge 2026-03-17 18:15:18 +00:00
Dave
ec76005c63 story-kit: create 274_story_mcp_pipeline_status_tool_with_agent_assignments 2026-03-17 18:10:26 +00:00
Dave
1736f8d924 story-kit: queue 92_spike_stop_auto_committing_intermediate_pipeline_moves for QA 2026-03-17 18:08:42 +00:00
Dave
f8b5e11c27 story-kit: done 269_story_file_references_in_web_ui_chat_input 2026-03-17 18:04:48 +00:00
Dave
12c500ee90 story-kit: remove 269_story_file_references_in_web_ui_chat_input 2026-03-17 18:03:21 +00:00
Dave
81c9cf797f story-kit: done 269_story_file_references_in_web_ui_chat_input 2026-03-17 18:03:10 +00:00
Dave
d18c1105c7 story-kit: start 269_story_file_references_in_web_ui_chat_input 2026-03-17 18:01:09 +00:00
Dave
ca8e6dc51c story-kit: accept 245_bug_chat_history_persistence_lost_on_page_refresh_story_145_regression 2026-03-17 17:58:30 +00:00
Dave
30ad59c6eb story-kit: queue 269_story_file_references_in_web_ui_chat_input for merge 2026-03-17 17:57:29 +00:00
Dave
123f140244 story-kit: start 269_story_file_references_in_web_ui_chat_input 2026-03-17 17:56:24 +00:00
Dave
8db23f77cd story-kit: start 92_spike_stop_auto_committing_intermediate_pipeline_moves 2026-03-17 17:55:34 +00:00
Dave
6bfa10b0e5 story-kit: queue 269_story_file_references_in_web_ui_chat_input for merge 2026-03-17 17:55:15 +00:00
Dave
65036b2ce7 story-kit: start 92_spike_stop_auto_committing_intermediate_pipeline_moves 2026-03-17 17:54:03 +00:00
Dave
76d73b2d0b story-kit: gitignore matrix_history.json
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 17:53:33 +00:00
Dave
78618a1b76 story-kit: queue 269_story_file_references_in_web_ui_chat_input for merge 2026-03-17 17:51:06 +00:00
Dave
47e07b23d1 story-kit: queue 272_story_clear_merge_error_front_matter_when_story_leaves_merge_stage for QA 2026-03-17 17:50:17 +00:00
Dave
45ae7b8f01 story-kit: queue 269_story_file_references_in_web_ui_chat_input for merge 2026-03-17 17:49:46 +00:00
Dave
e1c30b5953 story-kit: accept 262_story_bot_error_notifications_for_story_failures 2026-03-17 17:49:07 +00:00
Dave
b0d9fb4f39 story-kit: queue 269_story_file_references_in_web_ui_chat_input for merge 2026-03-17 17:47:07 +00:00
Dave
dcc11c2b0f story-kit: queue 271_story_show_assigned_agent_in_expanded_work_item_view for QA 2026-03-17 17:46:43 +00:00
Dave
7f21454880 story-kit: queue 269_story_file_references_in_web_ui_chat_input for merge 2026-03-17 17:45:44 +00:00
Dave
a893a1cef7 story-kit: merge 266_story_matrix_bot_structured_conversation_history 2026-03-17 17:42:08 +00:00
Dave
3fb48cdf51 story-kit: queue 269_story_file_references_in_web_ui_chat_input for QA 2026-03-17 17:41:55 +00:00
Dave
f1bb1216bf story-kit: done 266_story_matrix_bot_structured_conversation_history 2026-03-17 17:41:29 +00:00
Dave
b3faf7b810 story-kit: queue 247_story_human_qa_gate_with_rejection_flow for merge 2026-03-17 17:40:54 +00:00
Dave
89e4ee1c9c story-kit: queue 270_bug_qa_test_server_overwrites_root_mcp_json_with_wrong_port for merge 2026-03-17 17:37:50 +00:00
Dave
4df39eb1f2 story-kit: create 273_story_matrix_bot_sends_typing_indicator_while_waiting_for_claude_response 2026-03-17 17:37:41 +00:00
Dave
a7d23143ef story-kit: done 267_story_mcp_update_story_tool_should_support_front_matter_fields 2026-03-17 17:36:00 +00:00
Dave
f72666b39e story-kit: merge 267_story_mcp_update_story_tool_should_support_front_matter_fields 2026-03-17 17:35:57 +00:00
Dave
1f8ffee38e story-kit: start 272_story_clear_merge_error_front_matter_when_story_leaves_merge_stage 2026-03-17 17:35:23 +00:00
Dave
798f841b9a story-kit: queue 267_story_mcp_update_story_tool_should_support_front_matter_fields for merge 2026-03-17 17:34:26 +00:00
Dave
25c3dbb3d1 story-kit: start 271_story_show_assigned_agent_in_expanded_work_item_view 2026-03-17 17:33:34 +00:00
Dave
71cbc21b01 story-kit: queue 266_story_matrix_bot_structured_conversation_history for merge 2026-03-17 17:31:57 +00:00
Dave
6deeba81a8 story-kit: queue 266_story_matrix_bot_structured_conversation_history for merge 2026-03-17 17:30:53 +00:00
Dave
b862a7a6d0 story-kit: queue 267_story_mcp_update_story_tool_should_support_front_matter_fields for merge 2026-03-17 17:29:36 +00:00
Dave
fe1f76957d story-kit: queue 267_story_mcp_update_story_tool_should_support_front_matter_fields for merge 2026-03-17 17:28:38 +00:00
Dave
266e676dd4 story-kit: queue 247_story_human_qa_gate_with_rejection_flow for QA 2026-03-17 17:27:04 +00:00
Dave
402159c19a story-kit: queue 270_bug_qa_test_server_overwrites_root_mcp_json_with_wrong_port for QA 2026-03-17 17:26:23 +00:00
Dave
6d1b36e515 story-kit: start 269_story_file_references_in_web_ui_chat_input 2026-03-17 17:25:42 +00:00
Dave
81d4889cee Expand vite watch ignore list to prevent silent crashes on merge
Vite only needs to watch frontend/ sources. Ignore git objects,
Rust source, Cargo files, node_modules, and vendor directories
that change in bulk during squash merges.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 17:17:47 +00:00
Dave
0eb2cd8ec3 Stop tracking .mcp.json — it is generated at runtime
The server writes .mcp.json with its current port on startup and
during worktree creation. Tracking it in git causes QA gate failures
when the worktree copy diverges from master (e.g. different port).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 17:15:35 +00:00
Dave
b251ed7421 story-kit: create 272_story_clear_merge_error_front_matter_when_story_leaves_merge_stage 2026-03-17 17:14:19 +00:00
Dave
4a600e9954 story-kit: create 271_story_show_assigned_agent_in_expanded_work_item_view 2026-03-17 17:13:08 +00:00
Dave
cfb810b061 story-kit: start 270_bug_qa_test_server_overwrites_root_mcp_json_with_wrong_port 2026-03-17 17:10:45 +00:00
Dave
71bd999586 story-kit: create 270_bug_qa_test_server_overwrites_root_mcp_json_with_wrong_port 2026-03-17 17:10:30 +00:00
Dave
10d0cdeeae story-kit: accept 256_story_bot_must_verify_other_users_cross_signing_identity_before_checking_device_verification 2026-03-17 17:09:27 +00:00
Dave
6e375aaab5 story-kit: create 270_bug_qa_test_server_overwrites_root_mcp_json_with_wrong_port 2026-03-17 17:08:26 +00:00
Dave
e7edf9a8d5 story-kit: done 268_refactor_upgrade_tokio_tungstenite_to_0_29_0 2026-03-17 17:02:34 +00:00
Dave
20431f625b story-kit: merge 268_refactor_upgrade_tokio_tungstenite_to_0_29_0 2026-03-17 17:02:31 +00:00
Dave
d35f0f19fb story-kit: queue 268_refactor_upgrade_tokio_tungstenite_to_0_29_0 for merge 2026-03-17 16:59:54 +00:00
Dave
4303b33b90 story-kit: queue 266_story_matrix_bot_structured_conversation_history for QA 2026-03-17 16:58:19 +00:00
Dave
f9c0d24d7a story-kit: accept 259_story_move_story_kit_ignores_into_story_kit_gitignore 2026-03-17 16:57:12 +00:00
Dave
ec3277234c story-kit: queue 267_story_mcp_update_story_tool_should_support_front_matter_fields for QA 2026-03-17 16:53:12 +00:00
Dave
0a28aae041 story-kit: queue 268_refactor_upgrade_tokio_tungstenite_to_0_29_0 for QA 2026-03-17 16:47:46 +00:00
Dave
a7a8358cbb story-kit: create 269_story_file_references_in_web_ui_chat_input 2026-03-17 16:46:55 +00:00
Dave
6b6cb525a7 story-kit: start 268_refactor_upgrade_tokio_tungstenite_to_0_29_0 2026-03-17 16:43:45 +00:00
Dave
27465b1130 story-kit: create 268_refactor_upgrade_tokio_tungstenite_to_0_29_0 2026-03-17 16:43:34 +00:00
Dave
e74c370c7e Improve release changelog and fix MCP port
Generate structured changelogs from completed stories instead of raw
commit messages. Group by features, bug fixes, and refactors. Filter
out story-kit automation commits.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 16:42:48 +00:00
Dave
8defd5c671 story-kit: start 267_story_mcp_update_story_tool_should_support_front_matter_fields 2026-03-17 16:42:08 +00:00
Dave
a5c4fb553a story-kit: create 267_story_mcp_update_story_tool_should_support_front_matter_fields 2026-03-17 16:41:47 +00:00
Dave
a7772d1421 story-kit: start 247_story_human_qa_gate_with_rejection_flow 2026-03-17 16:41:27 +00:00
Dave
ed967403fb story-kit: start 247_story_human_qa_gate_with_rejection_flow 2026-03-17 16:40:09 +00:00
Dave
998b188ac7 story-kit: start 266_story_matrix_bot_structured_conversation_history 2026-03-17 16:39:42 +00:00
Dave
115c9fd6df story-kit: done 265_story_spikes_skip_merge_and_stop_for_human_review 2026-03-17 16:36:03 +00:00
Dave
86694a4383 story-kit: merge 265_story_spikes_skip_merge_and_stop_for_human_review 2026-03-17 16:36:00 +00:00
Dave
7b324ea96e story-kit: accept 257_story_rename_storkit_to_story_kit_in_header 2026-03-17 16:35:37 +00:00
Dave
744a12eeea story-kit: queue 265_story_spikes_skip_merge_and_stop_for_human_review for merge 2026-03-17 16:33:37 +00:00
Dave
cffe63680d Fix MCP server URL to match actual running port (3010)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 16:17:54 +00:00
Dave
f5fffd64b8 story-kit: create 266_story_matrix_bot_structured_conversation_history 2026-03-17 16:17:34 +00:00
Dave
ad68bc912f story-kit: remove 266_story_matrix_bot_structured_conversation_history 2026-03-17 16:17:20 +00:00
Dave
d02d53d112 story-kit: create 266_story_matrix_bot_structured_conversation_history 2026-03-17 16:14:07 +00:00
Dave
3ce7276e89 Fix TS narrowing errors in Chat.test.tsx
TypeScript control flow analysis can't track reassignment inside vi.mock
callbacks, causing lastSendChatArgs to narrow to never. Use non-null
assertions after the explicit toBeNull() guard.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 16:13:29 +00:00
Dave
6d87e64859 story-kit: queue 265_story_spikes_skip_merge_and_stop_for_human_review for QA 2026-03-17 16:11:52 +00:00
Dave
83db282892 Bump version to 0.2.0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 16:08:59 +00:00
Dave
f5d5196bf5 Fix cross-signing bootstrap by passing UIA password auth
The homeserver requires User-Interactive Authentication before accepting
cross-signing keys. Pass the bot's password so bootstrap succeeds.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 16:08:39 +00:00
Dave
7ec869baa8 Fixing mcp location 2026-03-17 15:58:40 +00:00
Dave
1a257b3057 story-kit: start 265_story_spikes_skip_merge_and_stop_for_human_review 2026-03-17 15:58:28 +00:00
Dave
b9fd87ed7c story-kit: create 265_story_spikes_skip_merge_and_stop_for_human_review 2026-03-17 15:58:24 +00:00
Dave
fda763d3f0 story-kit: create 260_refactor_upgrade_libsqlite3_sys 2026-03-17 15:47:07 +00:00
Dave
77d89b17e8 story-kit: done 260_refactor_upgrade_libsqlite3_sys 2026-03-17 15:47:01 +00:00
Dave
df0fa46591 Revert "story-kit: merge 260_refactor_upgrade_libsqlite3_sys"
This reverts commit ea062400e5.
2026-03-17 15:45:52 +00:00
Dave
1f5d70ce0d story-kit: done 264_bug_claude_code_session_id_not_persisted_across_browser_refresh 2026-03-17 15:41:43 +00:00
Dave
0d46c86469 story-kit: merge 264_bug_claude_code_session_id_not_persisted_across_browser_refresh 2026-03-17 15:41:41 +00:00
Dave
a439f8fdcb story-kit: queue 264_bug_claude_code_session_id_not_persisted_across_browser_refresh for merge 2026-03-17 15:39:34 +00:00
Dave
1adddf4e4c story-kit: create 247_story_human_qa_gate_with_rejection_flow 2026-03-17 15:37:25 +00:00
Dave
23484716e2 story-kit: create 265_story_spikes_skip_merge_and_stop_for_human_review 2026-03-17 15:36:52 +00:00
Dave
92085f9071 story-kit: done 263_story_matrix_bot_self_signs_device_keys_at_startup_for_verified_encryption 2026-03-17 15:35:01 +00:00
Dave
ce899b569e story-kit: merge 263_story_matrix_bot_self_signs_device_keys_at_startup_for_verified_encryption 2026-03-17 15:34:58 +00:00
Dave
da7216630b story-kit: queue 263_story_matrix_bot_self_signs_device_keys_at_startup_for_verified_encryption for merge 2026-03-17 15:33:11 +00:00
Dave
b57c270144 story-kit: queue 263_story_matrix_bot_self_signs_device_keys_at_startup_for_verified_encryption for merge 2026-03-17 15:31:25 +00:00
Dave
230b8fdc35 story-kit: queue 263_story_matrix_bot_self_signs_device_keys_at_startup_for_verified_encryption for merge 2026-03-17 15:28:49 +00:00
Dave
75b2446801 story-kit: done 262_story_bot_error_notifications_for_story_failures 2026-03-17 15:27:51 +00:00
Dave
96779c9caf story-kit: merge 262_story_bot_error_notifications_for_story_failures 2026-03-17 15:27:48 +00:00
Dave
bf5d9ff6b1 story-kit: queue 264_bug_claude_code_session_id_not_persisted_across_browser_refresh for QA 2026-03-17 15:25:47 +00:00
Dave
c551faeea3 story-kit: queue 263_story_matrix_bot_self_signs_device_keys_at_startup_for_verified_encryption for merge 2026-03-17 15:19:41 +00:00
Dave
3f38f90a50 story-kit: queue 263_story_matrix_bot_self_signs_device_keys_at_startup_for_verified_encryption for merge 2026-03-17 15:19:33 +00:00
Dave
26a1328c89 story-kit: queue 262_story_bot_error_notifications_for_story_failures for merge 2026-03-17 15:18:57 +00:00
Dave
21b45b8dd7 story-kit: queue 263_story_matrix_bot_self_signs_device_keys_at_startup_for_verified_encryption for merge 2026-03-17 15:18:32 +00:00
Dave
3a860bd2d5 story-kit: start 264_bug_claude_code_session_id_not_persisted_across_browser_refresh 2026-03-17 15:16:14 +00:00
Dave
c2c95c18b4 story-kit: create 264_bug_claude_code_session_id_not_persisted_across_browser_refresh 2026-03-17 15:15:51 +00:00
Dave
e3a301009b story-kit: queue 263_story_matrix_bot_self_signs_device_keys_at_startup_for_verified_encryption for QA 2026-03-17 15:08:23 +00:00
Dave
c90bdc8907 story-kit: queue 262_story_bot_error_notifications_for_story_failures for QA 2026-03-17 15:07:17 +00:00
Dave
dba12a38c2 story-kit: start 263_story_matrix_bot_self_signs_device_keys_at_startup_for_verified_encryption 2026-03-17 15:00:34 +00:00
Dave
4b60452b27 story-kit: start 262_story_bot_error_notifications_for_story_failures 2026-03-17 14:57:27 +00:00
Dave
d2f677ae0c story-kit: create 263_story_matrix_bot_self_signs_device_keys_at_startup_for_verified_encryption 2026-03-17 14:56:59 +00:00
Dave
427bb6929a Ignoring some folders to keep vite running across merges 2026-03-17 14:48:37 +00:00
Dave
78c04ee576 story-kit: create 262_story_bot_error_notifications_for_story_failures 2026-03-17 14:44:04 +00:00
Dave
3309d26142 story-kit: done 245_bug_chat_history_persistence_lost_on_page_refresh_story_145_regression 2026-03-17 14:38:08 +00:00
Dave
5a4a2aaa17 story-kit: merge 245_bug_chat_history_persistence_lost_on_page_refresh_story_145_regression 2026-03-17 14:38:05 +00:00
Dave
d3786253ef story-kit: queue 245_bug_chat_history_persistence_lost_on_page_refresh_story_145_regression for merge 2026-03-17 14:35:34 +00:00
Dave
76db12a53e story-kit: queue 262_story_bot_error_notifications_for_story_failures for QA 2026-03-17 14:25:37 +00:00
Dave
4eb5a01774 story-kit: queue 245_bug_chat_history_persistence_lost_on_page_refresh_story_145_regression for QA 2026-03-17 14:20:40 +00:00
Dave
198f9ff5bf story-kit: start 262_story_bot_error_notifications_for_story_failures 2026-03-17 14:20:39 +00:00
Dave
e30773d088 story-kit: done 261_story_bot_notifications_when_stories_move_between_stages 2026-03-17 14:05:04 +00:00
Dave
a4affca9be story-kit: merge 261_story_bot_notifications_when_stories_move_between_stages 2026-03-17 14:05:02 +00:00
Dave
a067091354 story-kit: queue 261_story_bot_notifications_when_stories_move_between_stages for merge 2026-03-17 14:02:47 +00:00
Dave
da423d9c97 story-kit: queue 261_story_bot_notifications_when_stories_move_between_stages for QA 2026-03-17 13:58:47 +00:00
Dave
d6d080e30a story-kit: start 245_bug_chat_history_persistence_lost_on_page_refresh_story_145_regression 2026-03-17 13:57:34 +00:00
Dave
9098c1ba9d story-kit: done 245_bug_chat_history_persistence_lost_on_page_refresh_story_145_regression 2026-03-17 13:57:28 +00:00
Dave
511c5809f2 story-kit: remove 263_refactor_extract_common_matrix_messaging_for_story_notifications 2026-03-17 13:48:54 +00:00
Dave
ace8e59536 story-kit: create 262_story_bot_error_notifications_for_story_failures 2026-03-17 13:48:49 +00:00
Dave
fa128c52d9 story-kit: queue 262_story_bot_error_notifications_for_story_failures for merge 2026-03-17 13:48:41 +00:00
Dave
621cdea6df story-kit: start 261_story_bot_notifications_when_stories_move_between_stages 2026-03-17 13:48:31 +00:00
Dave
68233e3355 story-kit: queue 261_story_bot_notifications_when_stories_move_between_stages for merge 2026-03-17 13:48:17 +00:00
Dave
99d298035b story-kit: queue 262_story_bot_error_notifications_for_story_failures for merge 2026-03-17 13:45:56 +00:00
Dave
73b41d1c6c story-kit: queue 262_story_bot_error_notifications_for_story_failures for merge 2026-03-17 13:44:49 +00:00
Dave
1a56844661 story-kit: queue 261_story_bot_notifications_when_stories_move_between_stages for merge 2026-03-17 13:42:50 +00:00
Dave
48ff0ba205 story-kit: queue 261_story_bot_notifications_when_stories_move_between_stages for merge 2026-03-17 13:41:49 +00:00
Dave
50b29e0bed story-kit: done 260_refactor_upgrade_libsqlite3_sys 2026-03-17 13:41:35 +00:00
Dave
ea062400e5 story-kit: merge 260_refactor_upgrade_libsqlite3_sys 2026-03-17 13:41:33 +00:00
Dave
b0e4e04c9d story-kit: done 258_bug_auto_assign_not_called_after_merge_failure 2026-03-17 13:38:45 +00:00
Dave
02fe364349 story-kit: create 263_refactor_extract_common_matrix_messaging_for_story_notifications 2026-03-17 13:38:43 +00:00
Dave
3602f882d2 story-kit: merge 258_bug_auto_assign_not_called_after_merge_failure 2026-03-17 13:38:42 +00:00
Dave
730e7324ea story-kit: queue 260_refactor_upgrade_libsqlite3_sys for merge 2026-03-17 13:38:24 +00:00
Dave
ae73d95d50 story-kit: queue 262_story_bot_error_notifications_for_story_failures for QA 2026-03-17 13:36:34 +00:00
Dave
ae6dd3217b story-kit: queue 258_bug_auto_assign_not_called_after_merge_failure for merge 2026-03-17 13:35:27 +00:00
Dave
9a6f63b591 story-kit: queue 258_bug_auto_assign_not_called_after_merge_failure for QA 2026-03-17 13:32:08 +00:00
Dave
421eaec7ba story-kit: start 262_story_bot_error_notifications_for_story_failures 2026-03-17 13:27:28 +00:00
Dave
2c4e376054 story-kit: start 261_story_bot_notifications_when_stories_move_between_stages 2026-03-17 13:27:24 +00:00
Dave
1896a0ac49 story-kit: start 258_bug_auto_assign_not_called_after_merge_failure 2026-03-17 13:23:57 +00:00
Dave
b8d3978a54 story-kit: queue 260_refactor_upgrade_libsqlite3_sys for QA 2026-03-17 13:22:31 +00:00
Dave
72c50b6ffc story-kit: create 262_story_bot_error_notifications_for_story_failures 2026-03-17 13:20:14 +00:00
Dave
bab77fe105 story-kit: create 261_story_bot_notifications_when_stories_move_between_stages 2026-03-17 13:20:12 +00:00
Dave
1d935192e1 story-kit: done 259_story_move_story_kit_ignores_into_story_kit_gitignore 2026-03-17 13:15:04 +00:00
Dave
f89f78d77d story-kit: merge 259_story_move_story_kit_ignores_into_story_kit_gitignore 2026-03-17 13:15:02 +00:00
Dave
09a71b4515 story-kit: queue 259_story_move_story_kit_ignores_into_story_kit_gitignore for merge 2026-03-17 13:12:12 +00:00
Dave
988562fc82 story-kit: done 256_story_bot_must_verify_other_users_cross_signing_identity_before_checking_device_verification 2026-03-17 13:10:55 +00:00
Dave
ed0d5d9253 story-kit: merge 256_story_bot_must_verify_other_users_cross_signing_identity_before_checking_device_verification 2026-03-17 13:10:51 +00:00
Dave
bb265d7bd5 story-kit: queue 256_story_bot_must_verify_other_users_cross_signing_identity_before_checking_device_verification for merge 2026-03-17 13:08:13 +00:00
Dave
126a6f8dc3 story-kit: queue 256_story_bot_must_verify_other_users_cross_signing_identity_before_checking_device_verification for merge 2026-03-17 13:07:33 +00:00
Dave
3b66b89c90 story-kit: remove 256_story_bot_must_verify_other_users_cross_signing_identity_before_checking_device_verification 2026-03-17 13:07:03 +00:00
Dave
e9879ce1c7 story-kit: create 92_spike_stop_auto_committing_intermediate_pipeline_moves 2026-03-17 13:06:37 +00:00
Dave
d30192b6a3 story-kit: create 92_spike_stop_auto_committing_intermediate_pipeline_moves 2026-03-17 13:06:26 +00:00
Dave
93c4f06818 story-kit: queue 259_story_move_story_kit_ignores_into_story_kit_gitignore for QA 2026-03-17 13:06:24 +00:00
Dave
7dab810572 story-kit: start 260_refactor_upgrade_libsqlite3_sys 2026-03-17 13:01:20 +00:00
Dave
cb7dde9fc1 story-kit: queue 256_story_bot_must_verify_other_users_cross_signing_identity_before_checking_device_verification for merge 2026-03-17 13:01:15 +00:00
Dave
7f70d1118f story-kit: create 260_refactor_upgrade_libsqlite3_sys 2026-03-17 13:01:10 +00:00
Dave
5638402745 story-kit: create 260_refactor_upgrade_libsqlite3_sys 2026-03-17 13:01:09 +00:00
Dave
e90bf38fa2 story-kit: create 260_refactor_upgrade_libsqlite3_sys 2026-03-17 13:00:48 +00:00
Dave
46ab4cdd8a story-kit: queue 256_story_bot_must_verify_other_users_cross_signing_identity_before_checking_device_verification for merge 2026-03-17 13:00:18 +00:00
Dave
7341fca72e story-kit: start 259_story_move_story_kit_ignores_into_story_kit_gitignore 2026-03-17 12:57:20 +00:00
Dave
fdb4a4fb62 story-kit: create 259_story_move_story_kit_ignores_into_story_kit_gitignore 2026-03-17 12:56:39 +00:00
Dave
87791c755e story-kit: create 259_story_move_story_kit_ignores_into_story_kit_gitignore 2026-03-17 12:53:52 +00:00
Dave
a4ce5f8f7c story-kit: queue 256_story_bot_must_verify_other_users_cross_signing_identity_before_checking_device_verification for QA 2026-03-17 12:52:04 +00:00
Dave
a9a84bee6d story-kit: start 256_story_bot_must_verify_other_users_cross_signing_identity_before_checking_device_verification 2026-03-17 12:52:01 +00:00
Dave
34755d3f63 story-kit: start 256_story_bot_must_verify_other_users_cross_signing_identity_before_checking_device_verification 2026-03-17 12:50:00 +00:00
Dave
ec553a5b8a story-kit: start 256_story_bot_must_verify_other_users_cross_signing_identity_before_checking_device_verification 2026-03-17 12:49:40 +00:00
Dave
076324c470 Fix pipe buffer deadlock in quality gate test runner
run_command_with_timeout piped stdout/stderr but only read them after
the child exited. When test output exceeded the 64KB OS pipe buffer,
the child blocked on write() while the parent blocked on waitpid() —
a permanent deadlock that caused every merge pipeline to hang.

Drain both pipes in background threads so the buffers never fill.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 12:49:12 +00:00
Dave
5ed2737edc story-kit: accept 250_bug_merge_pipeline_cherry_pick_fails_with_bad_revision_on_merge_queue_branch 2026-03-17 12:48:11 +00:00
Dave
0eafddd186 story-kit: done 257_story_rename_storkit_to_story_kit_in_header 2026-03-17 12:46:33 +00:00
Dave
7d4f722942 story-kit: merge 257_story_rename_storkit_to_story_kit_in_header 2026-03-17 12:46:30 +00:00
Dave
5d80d289c4 story-kit: create 258_bug_auto_assign_not_called_after_merge_failure 2026-03-17 12:35:08 +00:00
Dave
7c6e1b445d Make merge_agent_work async to avoid MCP 60-second tool timeout
The merge pipeline (squash merge + quality gates) takes well over 60
seconds. Claude Code's MCP HTTP transport times out at 60s, causing
"completed with no output" — the mergemaster retries fruitlessly.

merge_agent_work now starts the pipeline as a background task and
returns immediately. A new get_merge_status tool lets the mergemaster
poll until the job reaches a terminal state. Also adds a double-start
guard so concurrent calls for the same story are rejected.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 12:15:42 +00:00
136 changed files with 12607 additions and 1326 deletions

View File

@@ -60,7 +60,16 @@
"Edit",
"Write",
"Bash(find *)",
"Bash(sqlite3 *)"
"Bash(sqlite3 *)",
"Bash(cat <<:*)",
"Bash(cat <<'ENDJSON:*)",
"Bash(make release:*)",
"Bash(npm test:*)",
"Bash(head *)",
"Bash(tail *)",
"Bash(wc *)",
"Bash(npx vite:*)",
"Bash(npm run dev:*)"
]
}
}

18
.gitignore vendored
View File

@@ -1,27 +1,14 @@
# Claude Code
.claude/settings.local.json
.mcp.json
# Local environment (secrets)
.env
# App specific
# App specific (root-level; story-kit subdirectory patterns live in .story_kit/.gitignore)
store.json
.story_kit_port
# Bot config (contains credentials)
.story_kit/bot.toml
# Matrix SDK state store
.story_kit/matrix_store/
.story_kit/matrix_device_id
# Agent worktrees and merge workspace (managed by the server, not tracked in git)
.story_kit/worktrees/
.story_kit/merge_workspace/
# Coverage reports (generated by cargo-llvm-cov, not tracked in git)
.story_kit/coverage/
# Rust stuff
target
@@ -39,6 +26,7 @@ frontend/node_modules
frontend/dist
frontend/dist-ssr
frontend/test-results
frontend/serve
frontend/*.local
server/target

View File

@@ -1,8 +0,0 @@
{
"mcpServers": {
"story-kit": {
"type": "http",
"url": "http://localhost:3001/mcp"
}
}
}

22
.story_kit/.gitignore vendored Normal file
View File

@@ -0,0 +1,22 @@
# Bot config (contains credentials)
bot.toml
# Matrix SDK state store
matrix_store/
matrix_device_id
matrix_history.json
# Agent worktrees and merge workspace (managed by the server, not tracked in git)
worktrees/
merge_workspace/
# Intermediate pipeline stages (transient, not committed per spike 92)
work/2_current/
work/3_qa/
work/4_merge/
# Coverage reports (generated by cargo-llvm-cov, not tracked in git)
coverage/
# Token usage log (generated at runtime, contains cost data)
token_usage.jsonl

View File

@@ -18,7 +18,7 @@ When you start a new session with this project:
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_upcoming/` and `.story_kit/work/2_current/` to see what work is pending.
4. **Check Work Items:** Look at `.story_kit/work/1_backlog/` and `.story_kit/work/2_current/` to see what work is pending.
---
@@ -52,7 +52,7 @@ project_root/
├── README.md # This document
├── project.toml # Agent configuration (roles, models, prompts)
├── work/ # Unified work item pipeline (stories, bugs, spikes)
│ ├── 1_upcoming/ # New work items awaiting implementation
│ ├── 1_backlog/ # New work items awaiting implementation
│ ├── 2_current/ # Work in progress
│ ├── 3_qa/ # QA review
│ ├── 4_merge/ # Ready to merge to master
@@ -78,7 +78,7 @@ All work items (stories, bugs, spikes) live in the same `work/` pipeline. Items
Items move through stages by moving the file between directories:
`1_upcoming` → `2_current` → `3_qa` → `4_merge` → `5_done` → `6_archived`
`1_backlog` → `2_current` → `3_qa` → `4_merge` → `5_done` → `6_archived`
Items in `5_done` are auto-swept to `6_archived` after 4 hours by the server.
@@ -87,7 +87,7 @@ Items in `5_done` are auto-swept to `6_archived` after 4 hours by the server.
The server watches `.story_kit/work/` for changes. When a file is created, moved, or modified, the watcher auto-commits with a deterministic message and broadcasts a WebSocket notification to the frontend. This means:
* MCP tools only need to write/move files — the watcher handles git commits
* IDE drag-and-drop works (drag a story from `1_upcoming/` to `2_current/`)
* IDE drag-and-drop works (drag a story from `1_backlog/` to `2_current/`)
* The frontend updates automatically without manual refresh
---
@@ -156,7 +156,7 @@ Not everything needs to be a full story. Simple bugs can skip the story process:
* Performance issues with known fixes
### Bug Process
1. **Document Bug:** Create a bug file in `work/1_upcoming/` named `{id}_bug_{slug}.md` with:
1. **Document Bug:** Create a bug file in `work/1_backlog/` named `{id}_bug_{slug}.md` with:
* **Symptom:** What the user observes
* **Root Cause:** Technical explanation (if known)
* **Reproduction Steps:** How to trigger the bug
@@ -186,7 +186,7 @@ Not everything needs a story or bug fix. Spikes are time-boxed investigations to
* Need to validate performance constraints
### Spike Process
1. **Document Spike:** Create a spike file in `work/1_upcoming/` named `{id}_spike_{slug}.md` with:
1. **Document Spike:** Create a spike file in `work/1_backlog/` named `{id}_spike_{slug}.md` with:
* **Question:** What you need to answer
* **Hypothesis:** What you expect to be true
* **Timebox:** Strict limit for the research
@@ -209,7 +209,7 @@ When the LLM context window fills up (or the chat gets slow/confused):
1. **Stop Coding.**
2. **Instruction:** Tell the user to open a new chat.
3. **Handoff:** The only context the new LLM needs is in the `specs/` folder and `.mcp.json`.
* *Prompt for New Session:* "I am working on Project X. Read `.mcp.json` to discover available tools, then read `specs/00_CONTEXT.md` and `specs/tech/STACK.md`. Then look at `work/1_upcoming/` and `work/2_current/` to see what is pending."
* *Prompt for New Session:* "I am working on Project X. Read `.mcp.json` to discover available tools, then read `specs/00_CONTEXT.md` and `specs/tech/STACK.md`. Then look at `work/1_backlog/` and `work/2_current/` to see what is pending."
---
@@ -221,7 +221,7 @@ If a user hands you this document and says "Apply this process to my project":
1. **Check for MCP Tools:** Look for `.mcp.json` in the project root. If it exists, you have programmatic access to workflow tools and agent spawning capabilities.
2. **Analyze the Request:** Ask for the high-level goal ("What are we building?") and the tech preferences ("Rust or Python?").
3. **Git Check:** Check if the directory is a git repository (`git status`). If not, run `git init`.
4. **Scaffold:** Run commands to create the `work/` and `specs/` folders with the 6-stage pipeline (`work/1_upcoming/` through `work/6_archived/`).
4. **Scaffold:** Run commands to create the `work/` and `specs/` folders with the 6-stage pipeline (`work/1_backlog/` through `work/6_archived/`).
5. **Draft Context:** Write `specs/00_CONTEXT.md` based on the user's answer.
6. **Draft Stack:** Write `specs/tech/STACK.md` based on best practices for that language.
7. **Wait:** Ask the user for "Story #1".

View File

@@ -13,3 +13,7 @@ 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"]

16
.story_kit/problems.md Normal file
View File

@@ -0,0 +1,16 @@
# Problems
Recurring issues observed during pipeline operation. Review periodically and create stories for systemic problems.
## 2026-03-18: Stories graduating to "done" with empty merges (7 of 10)
Pipeline allows stories to move through coding → QA → merge → done without any actual code changes landing on master. The squash-merge produces an empty diff but the pipeline still marks the story as done. Affected stories: 247, 273, 274, 278, 279, 280, 92. Only 266, 271, 277, and 281 actually shipped code. Root cause: no check that the merge commit contains a non-empty diff. Filed bug 283 for the manual_qa gate issue specifically, but the empty-merge-to-done problem is broader and needs its own fix.
## 2026-03-18: Agent committed directly to master instead of worktree
Multiple agents have committed directly to master instead of their worktree/feature branch:
- Commit `5f4591f` ("fix: update should_commit_stage test to match 5_done") — likely mergemaster
- Commit `a32cfbd` ("Add bot-level command registry with help command") — story 285 coder committed code + Cargo.lock directly to master
Agents should only commit to their feature branch or merge-queue branch, never to master directly. Suspect agents are running `git commit` in the project root instead of the worktree directory. This can also revert uncommitted fixes on master (e.g. project.toml pkill fix was overwritten). Frequency: at least 2 confirmed cases. This is a recurring and serious problem — needs a guard in the server or agent prompts.

View File

@@ -1,3 +1,18 @@
# 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
[[component]]
name = "frontend"
path = "frontend"
@@ -34,7 +49,7 @@ You have these tools via the story-kit MCP server:
## Your Workflow
1. Read CLAUDE.md and .story_kit/README.md to understand the project and dev process
2. Read the story file from .story_kit/work/ to understand requirements
3. Move it to work/2_current/ if it is in work/1_upcoming/
3. Move it to work/2_current/ if it is in work/1_backlog/
4. Start coder-1 on the story: call start_agent with story_id="{{story_id}}" and agent_name="coder-1"
5. Wait for completion: call wait_for_agent with story_id="{{story_id}}" and agent_name="coder-1". The server automatically runs acceptance gates (cargo clippy + tests) when the coder process exits. wait_for_agent returns when the coder reaches a terminal state.
6. Check the result: inspect the "completion" field in the wait_for_agent response — if gates_passed is true, the work is done; if false, review the gate_output and decide whether to start a fresh coder.
@@ -69,6 +84,16 @@ max_budget_usd = 5.00
prompt = "You are working in a git worktree on story {{story_id}}. Read CLAUDE.md first, then .story_kit/README.md to understand the dev process. The story details are in your prompt above. Follow the SDTW process through implementation and verification (Steps 1-3). The worktree and feature branch already exist - do not create them. Check .mcp.json for MCP tools. Do NOT accept the story or merge - commit your work and stop. If the user asks to review your changes, tell them to run: cd \"{{worktree_path}}\" && git difftool {{base_branch}}...HEAD\n\nIMPORTANT: Commit all your work before your process exits. The server will automatically run acceptance gates (cargo clippy + tests) when your process exits and advance the pipeline based on the results.\n\n## Bug Workflow: Root Cause First\nWhen working on bugs:\n1. Investigate the root cause before writing any fix. Use `git bisect` to find the breaking commit or `git log` to trace history. Read the relevant code before touching anything.\n2. Fix the root cause with a surgical, minimal change. Do NOT add new abstractions, wrappers, or workarounds when a targeted fix to the original code is possible.\n3. Write commit messages that explain what broke and why, not just what was changed.\n4. If you cannot determine the root cause after thorough investigation, document what you tried and why it was inconclusive — do not guess and ship a speculative fix."
system_prompt = "You are a full-stack engineer working autonomously in a git worktree. Follow the Story-Driven Test Workflow strictly. Run cargo clippy and biome checks before considering work complete. Commit all your work before finishing - use a descriptive commit message. Do not accept stories, move them to archived, or merge to master - a human will do that. Do not coordinate with other agents - focus on your assigned story. The server automatically runs acceptance gates when your process exits. For bugs, always find and fix the root cause. Use git bisect to find breaking commits. Do not layer new code on top of existing code when a surgical fix is possible. If root cause is unclear after investigation, document what you tried rather than guessing."
[[agent]]
name = "coder-3"
stage = "coder"
role = "Full-stack engineer. Implements features across all components."
model = "sonnet"
max_turns = 50
max_budget_usd = 5.00
prompt = "You are working in a git worktree on story {{story_id}}. Read CLAUDE.md first, then .story_kit/README.md to understand the dev process. The story details are in your prompt above. Follow the SDTW process through implementation and verification (Steps 1-3). The worktree and feature branch already exist - do not create them. Check .mcp.json for MCP tools. Do NOT accept the story or merge - commit your work and stop. If the user asks to review your changes, tell them to run: cd \"{{worktree_path}}\" && git difftool {{base_branch}}...HEAD\n\nIMPORTANT: Commit all your work before your process exits. The server will automatically run acceptance gates (cargo clippy + tests) when your process exits and advance the pipeline based on the results.\n\n## Bug Workflow: Root Cause First\nWhen working on bugs:\n1. Investigate the root cause before writing any fix. Use `git bisect` to find the breaking commit or `git log` to trace history. Read the relevant code before touching anything.\n2. Fix the root cause with a surgical, minimal change. Do NOT add new abstractions, wrappers, or workarounds when a targeted fix to the original code is possible.\n3. Write commit messages that explain what broke and why, not just what was changed.\n4. If you cannot determine the root cause after thorough investigation, document what you tried and why it was inconclusive — do not guess and ship a speculative fix."
system_prompt = "You are a full-stack engineer working autonomously in a git worktree. Follow the Story-Driven Test Workflow strictly. Run cargo clippy and biome checks before considering work complete. Commit all your work before finishing - use a descriptive commit message. Do not accept stories, move them to archived, or merge to master - a human will do that. Do not coordinate with other agents - focus on your assigned story. The server automatically runs acceptance gates when your process exits. For bugs, always find and fix the root cause. Use git bisect to find breaking commits. Do not layer new code on top of existing code when a surgical fix is possible. If root cause is unclear after investigation, document what you tried rather than guessing."
[[agent]]
name = "qa-2"
stage = "qa"
@@ -102,7 +127,7 @@ Read CLAUDE.md first, then .story_kit/README.md to understand the dev process.
- URL to visit in the browser
- Things to check in the UI
- curl commands to exercise relevant API endpoints
- Kill the test server when done: `pkill -f story-kit || true`
- Kill the test server when done: `pkill -f 'target.*story-kit' || true` (NEVER use `pkill -f story-kit` — 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:
@@ -179,7 +204,7 @@ Read CLAUDE.md first, then .story_kit/README.md to understand the dev process.
- URL to visit in the browser
- Things to check in the UI
- curl commands to exercise relevant API endpoints
- Kill the test server when done: `pkill -f story-kit || true`
- Kill the test server when done: `pkill -f 'target.*story-kit' || true` (NEVER use `pkill -f story-kit` — 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:
@@ -220,7 +245,7 @@ role = "Merges completed coder work into master, runs quality gates, archives st
model = "opus"
max_turns = 30
max_budget_usd = 5.00
prompt = """You are the mergemaster agent for story {{story_id}}. Your job is to merge the completed coder work into master using the merge_agent_work MCP tool.
prompt = """You are the mergemaster agent for story {{story_id}}. Your job is to merge the completed coder work into master.
Read CLAUDE.md first, then .story_kit/README.md to understand the dev process.
@@ -229,20 +254,43 @@ Read CLAUDE.md first, then .story_kit/README.md to understand the dev process.
2. Review the result: check success, had_conflicts, conflicts_resolved, gates_passed, and gate_output
3. If merge succeeded and gates passed: report success to the human
4. If conflicts were auto-resolved (conflicts_resolved=true) and gates passed: report success, noting which conflicts were resolved
5. If conflicts could not be auto-resolved: call report_merge_failure(story_id='{{story_id}}', reason='<conflict details>') and report to the human. Master is untouched.
6. If merge failed for any other reason: call report_merge_failure(story_id='{{story_id}}', reason='<details>') and report to the human.
7. If gates failed after merge: attempt to fix minor issues (see below), then re-trigger merge_agent_work. After 2 fix attempts, call report_merge_failure and stop.
5. If conflicts could not be auto-resolved: **resolve them yourself** in the merge worktree (see below)
6. If merge failed for any other reason: call report_merge_failure(story_id='{{story_id}}', reason='<details>') and report to the human
7. If gates failed after merge: attempt to fix the issues yourself in the merge worktree, then re-trigger merge_agent_work. After 3 fix attempts, call report_merge_failure and stop.
## How Conflict Resolution Works
The merge pipeline uses a temporary merge-queue branch and worktree to isolate merges from master. Simple additive conflicts (both branches adding code at the same location) are resolved automatically by keeping both additions. Complex conflicts (modifying the same lines differently) are reported without touching master.
## Resolving Complex Conflicts Yourself
## Fixing Minor Gate Failures
If quality gates fail (cargo clippy, cargo test, npm run build, npm test), attempt to fix minor issues yourself before reporting to the human.
When the auto-resolver fails, you have access to the merge worktree at `.story_kit/merge_workspace/`. Go in there and resolve the conflicts manually:
**Fix yourself (up to 2 attempts total):**
1. Run `git diff --name-only --diff-filter=U` in the merge worktree to list conflicted files
2. **Build context before touching code.** Run `git log --oneline master...HEAD` on the feature branch to see its commits. Then run `git log --oneline --since="$(git log -1 --format=%ci <feature-branch-base-commit>)" master` to see what landed on master since the branch was created. Read the story files in `.story_kit/work/` for any recently merged stories that touch the same files — this tells you WHY master changed and what must be preserved.
3. Read each conflicted file and understand both sides of the conflict
4. **Understand intent, not just syntax.** The feature branch may be behind master — master's version of shared infrastructure is almost always correct. The feature branch's contribution is the NEW functionality it adds. Your job is to integrate the new into master's structure, not pick one side.
5. Resolve by integrating the feature's new functionality into master's code structure
5. Stage resolved files with `git add`
6. Run `cargo check` (and `npm run build` if frontend changed) to verify compilation
7. If it compiles, commit and re-trigger merge_agent_work
### Common conflict patterns in this project:
**Story file rename/rename conflicts:** Both branches moved the story .md file to different pipeline directories. Resolution: `git rm` both sides — story files in `work/2_current/`, `work/3_qa/`, `work/4_merge/` are gitignored and don't need to be committed.
**bot.rs tokio::select! conflicts:** Master has a `tokio::select!` loop in `handle_message()` that handles permission forwarding (story 275). Feature branches created before story 275 have a simpler direct `provider.chat_stream().await` call. Resolution: KEEP master's tokio::select! loop. Integrate only the feature's new logic (e.g. typing indicators, new callbacks) into the existing loop structure. Do NOT replace the loop with the old direct call.
**Duplicate functions/imports:** The auto-resolver keeps both sides, producing duplicates. Resolution: keep one copy (prefer master's version), delete the duplicate.
**Formatting-only conflicts:** Both sides reformatted the same code differently. Resolution: pick either side (prefer master).
## Fixing Gate Failures
If quality gates fail (cargo clippy, cargo test, npm run build, npm test), attempt to fix issues yourself in the merge worktree.
**Fix yourself (up to 3 attempts total):**
- Syntax errors (missing semicolons, brackets, commas)
- Duplicate definitions from merge artifacts
- Simple type annotation errors
- Unused import warnings flagged by clippy
- Mismatched braces from bad conflict resolution
- Trivial formatting issues that block compilation or linting
**Report to human without attempting a fix:**
@@ -250,17 +298,14 @@ If quality gates fail (cargo clippy, cargo test, npm run build, npm test), attem
- Missing function implementations
- Architectural changes required
- Non-trivial refactoring needed
- Anything requiring understanding of broader system context
**Max retry limit:** If gates still fail after 2 fix attempts, call report_merge_failure to record the failure, then stop immediately and report the full gate output to the human. Do not retry further.
**Max retry limit:** If gates still fail after 3 fix attempts, call report_merge_failure to record the failure, then stop immediately and report the full gate output to the human.
## CRITICAL Rules
- NEVER manually move story files between pipeline stages (e.g. from 4_merge/ to 5_done/)
- NEVER call accept_story — only merge_agent_work can move stories to done after a successful merge
- When merge fails, ALWAYS call report_merge_failure to record the failure — do NOT improvise with file moves
- Only use MCP tools (merge_agent_work, report_merge_failure) to drive the merge process
- Only attempt fixes that are clearly minor and low-risk
- When merge fails after exhausting your fix attempts, ALWAYS call report_merge_failure
- Report conflict resolution outcomes clearly
- Report gate failures with full output so the human can act if needed
- The server automatically runs acceptance gates when your process exits"""
system_prompt = "You are the mergemaster agent. Your primary responsibility is to trigger the merge_agent_work MCP tool and report the results. CRITICAL: Never manually move story files or call accept_story. When merge fails, call report_merge_failure to record the failure. For minor gate failures (syntax errors, unused imports, missing semicolons), attempt to fix them yourself — but stop after 2 attempts, call report_merge_failure, and report to the human. For complex failures or unresolvable conflicts, call report_merge_failure and report clearly so the human can act. The merge pipeline automatically resolves simple additive conflicts."
system_prompt = "You are the mergemaster agent. Your primary job is to merge feature branches to master. First try the merge_agent_work MCP tool. If the auto-resolver fails on complex conflicts, resolve them yourself in the merge worktree — you are an opus-class agent capable of understanding both sides of a conflict and producing correct merged code. Common patterns: keep master's tokio::select! permission loop in bot.rs, discard story file rename conflicts (gitignored), remove duplicate definitions. After resolving, verify compilation before re-triggering merge. CRITICAL: Never manually move story files or call accept_story. After 3 failed fix attempts, call report_merge_failure and stop."

View File

@@ -0,0 +1,24 @@
---
name: "Upgrade libsqlite3-sys"
---
# Refactor 260: Upgrade libsqlite3-sys
## Description
Upgrade the `libsqlite3-sys` dependency from `0.35.0` to `0.37.0`. The crate is used with `features = ["bundled"]` for static builds.
## Version Notes
- Current: `libsqlite3-sys 0.35.0` (pinned transitively by `matrix-sdk 0.16.0``matrix-sdk-sqlite``rusqlite 0.37.x`)
- Target: `libsqlite3-sys 0.37.0`
- Latest upstream rusqlite: `0.39.0`
- **Blocker**: `matrix-sdk 0.16.0` pins `rusqlite 0.37.x` which pins `libsqlite3-sys 0.35.0`. A clean upgrade requires either waiting for matrix-sdk to bump their rusqlite dep, or upgrading matrix-sdk itself.
- **Reverted 2026-03-17**: A previous coder vendored the entire rusqlite crate with a fake `0.37.99` version and patched its libsqlite3-sys dep. This was too hacky — reverted to clean `0.35.0`.
## Acceptance Criteria
- [ ] `libsqlite3-sys` is upgraded to `0.37.0` via a clean dependency path (no vendored forks)
- [ ] `cargo build` succeeds
- [ ] All tests pass
- [ ] No `[patch.crates-io]` hacks or vendored crates

View File

@@ -0,0 +1,32 @@
---
name: "Long-running supervisor agent with periodic pipeline polling"
agent: coder-opus
---
# Story 280: Long-running supervisor agent with periodic pipeline polling
## User Story
As a project owner, I want a long-running supervisor agent (opus) that automatically monitors the pipeline, assigns agents, resolves stuck items, and handles routine operational tasks, so that I don't have to manually check status, kick agents, or babysit the pipeline in every conversation.
## Acceptance Criteria
- [ ] Server can start a persistent supervisor agent that stays alive across the session (not per-story)
- [ ] Server prods the supervisor periodically (default 30s, configurable in project.toml) with a pipeline status update
- [ ] Supervisor auto-assigns agents to unassigned items in current/qa/merge stages
- [ ] Supervisor detects stuck agents (no progress for configurable timeout) and restarts them
- [ ] Supervisor detects merge failures and sends stories back to current for rebase when appropriate
- [ ] Supervisor can be chatted with via Matrix (timmy relays to supervisor) or via the web UI
- [ ] Supervisor logs its decisions so the human can review what it did and why
- [ ] Polling interval is configurable in project.toml (e.g. supervisor_poll_interval_secs = 30)
- [ ] Supervisor logs persistent/recurring problems to `.story_kit/problems.md` with timestamp, description, and frequency — humans review this file periodically to create stories for systemic issues
## Notes
- **2026-03-18**: Moved back to current from merge. Previous attempt went through the full pipeline but the squash-merge produced an empty diff — no code was actually implemented. Needs a real implementation.
## Out of Scope
- Supervisor accepting or merging stories to master (human job)
- Supervisor making architectural decisions
- Replacing the existing per-story agent spawning — supervisor coordinates on top of it

View File

@@ -0,0 +1,37 @@
---
name: "Auto-assign assigns mergemaster to coding-stage stories"
---
# Bug 312: Auto-assign assigns mergemaster to coding-stage stories
## Description
Auto-assign picks agents whose configured stage doesn't match the pipeline stage of the story. Observed multiple mismatch types:
- **Mergemaster assigned to coding-stage stories**: Story 310 was in `2_current/` but got mergemaster instead of a coder (2026-03-19)
- **Coders assigned to QA-stage stories**: Coders (stage=coder) have been observed running on stories in `3_qa/`
- **QA agents assigned to merge-stage stories**: QA agents (stage=qa) have been observed running on stories in `4_merge/`
The `auto_assign_available_work` function doesn't enforce that the agent's configured stage matches the pipeline stage of the story it's being assigned to. Story 279 (auto-assign respects agent stage from front matter) was supposed to fix stage matching, but the check may only apply to front-matter preferences, not the fallback assignment path.
## How to Reproduce
1. Move a story to any pipeline stage with no agent front matter preference
2. Wait for auto_assign_available_work to run
3. Observe that agents from the wrong stage get assigned (e.g. mergemaster to coding, coder to QA)
## Actual Result
Agents are assigned to stories in pipeline stages that don't match their configured stage. Mergemaster codes, coders do QA, QA agents attempt merges.
## Expected Result
Only coder-stage agents should be assigned to stories in 2_current/. Mergemaster should only be assigned to stories in 4_merge/.
## Acceptance Criteria
- [ ] auto_assign_available_work checks that the agent's configured stage matches the pipeline stage of the story before assigning
- [ ] Coder agents only assigned to stories in 2_current/
- [ ] QA agents only assigned to stories in 3_qa/
- [ ] Mergemaster only assigned to stories in 4_merge/
- [ ] Fallback assignment path respects stage matching (not just front-matter preference path)

View File

@@ -1,55 +0,0 @@
---
name: "Stop auto-committing intermediate pipeline moves"
---
# Spike 92: Stop auto-committing intermediate pipeline moves
## Goal
Determine how to stop the filesystem watcher from auto-committing every pipeline stage move (upcoming -> current -> qa -> merge) while still committing at terminal states (creation in upcoming, acceptance in archived). This keeps git history clean while preserving cross-machine portability for completed work.
## Context
The watcher in `server/src/io/watcher.rs` currently auto-commits every file change in `.story_kit/work/`. A single story run generates 5+ commits just from pipeline moves:
- `story-kit: create 42_story_foo`
- `story-kit: start 42_story_foo`
- `story-kit: queue 42_story_foo for QA`
- `story-kit: queue 42_story_foo for merge`
- `story-kit: accept 42_story_foo`
Since story runs complete relatively quickly, the intermediate state (current/qa/merge) is transient and doesn't need to be committed. Only creation and archival are meaningful checkpoints.
## Questions to Answer
1. Can we filter `stage_metadata()` to only commit for `1_upcoming` and `5_archived` stages while still broadcasting `WatcherEvent`s for all stages (so the frontend stays in sync)?
2. Should we keep `git add -A .story_kit/work/` for the committed stages, or narrow it to only the specific file?
3. What happens if the server crashes mid-pipeline? Uncommitted moves are lost — is this acceptable given the story can just be re-run?
4. Should intermediate moves be `.gitignore`d at the directory level, or is filtering in the watcher sufficient?
5. Do any other parts of the system (agent worktree setup, merge_agent_work, sparse checkout) depend on intermediate pipeline files being committed to master?
## Approach to Investigate
### Option A: Filter in `flush_pending()`
- In `flush_pending()`, still broadcast the `WatcherEvent` for all stages
- Only call `git_add_work_and_commit()` for stages `1_upcoming` and `5_archived`
- Simplest change — ~5 lines modified in `watcher.rs`
### Option B: Two-tier watcher
- Split into "commit-worthy" events (create, archive) and "notify-only" events (start, qa, merge)
- Commit-worthy events go through git
- Notify-only events just broadcast to WebSocket clients
- More explicit but same end result as Option A
### Option C: .gitignore intermediate directories
- Add `2_current/`, `3_qa/`, `4_merge/` to `.gitignore`
- Watcher still sees events (gitignore doesn't affect filesystem watching)
- Git naturally ignores them
- Risk: harder to debug, `git status` won't show pipeline state
## Acceptance Criteria
- [ ] Spike document updated with findings and recommendation
- [ ] If Option A is viable: prototype the change and verify git log is clean during a full story run
- [ ] Confirm frontend still receives real-time pipeline updates for all stages
- [ ] Confirm no other system depends on intermediate pipeline commits being on master
- [ ] Identify any edge cases (server crash, manual git operations, multi-machine sync)

View File

@@ -1,36 +0,0 @@
---
name: "Chat history persistence lost on page refresh (story 145 regression)"
---
## Rejection Notes
**2026-03-16:** Previous coder produced zero code changes — feature branch had no diff against master. The coder must actually use `git bisect` to find the breaking commit and produce a surgical fix. Do not submit with no code changes.
# Bug 245: Chat history persistence lost on page refresh (story 145 regression)
## Description
Story 145 implemented localStorage persistence for chat history across page reloads. This is no longer working — refreshing the page loses all conversation context. This is a regression of the feature delivered in story 145.
## How to Reproduce
1. Open the web UI and have a conversation with the agent
2. Refresh the page (F5 or Cmd+R)
## Actual Result
Chat history is gone after refresh — the UI shows a blank conversation.
## Expected Result
Chat history is restored from localStorage on page load, as implemented in story 145.
## Acceptance Criteria
- [ ] Chat messages survive a full page refresh
- [ ] Chat messages are restored from localStorage on component mount
- [ ] Behaviour matches the original acceptance criteria from story 145
## Investigation Notes
**Use `git bisect` to find the commit that broke this.** Story 145 delivered working localStorage persistence — something after that regressed it. Find the breaking commit, understand the root cause, and fix it there. Do NOT layer on a new implementation. Revert or surgically fix the regression.

View File

@@ -0,0 +1,24 @@
---
name: "Bot delete command removes a story from the pipeline"
---
# Story 310: Bot delete command removes a story from the pipeline
## User Story
As a project owner in a Matrix room, I want to type "{bot_name} delete {story_number}" to remove a story/bug/spike from the pipeline, so that I can clean up obsolete or duplicate work items from chat.
## Acceptance Criteria
- [ ] '{bot_name} delete {number}' finds the story/bug/spike by number across all pipeline stages and deletes the file
- [ ] Confirms deletion with the story name and stage it was in
- [ ] Returns a friendly message if no story with that number exists
- [ ] Stops any running agent on the story before deleting
- [ ] Removes the worktree if one exists for the story
- [ ] Registered in the command registry so it appears in help output
- [ ] Handled at bot level without LLM invocation
- [ ] Commits the deletion to git
## Out of Scope
- TBD

View File

@@ -0,0 +1,22 @@
---
name: "Server-enforced retry limits for failed merge and empty-diff stories"
---
# Story 311: Server-enforced retry limits for failed merge and empty-diff stories
## User Story
As a project owner, I want the server to enforce retry limits on stories that fail merge or produce empty diffs, so that agents don't loop infinitely on broken stories and waste tokens.
## Acceptance Criteria
- [ ] auto_assign_available_work checks the merge_failure front matter flag and skips stories in 4_merge that already have a reported failure
- [ ] Server tracks retry count per story per stage in front matter (e.g. retry_count: 2)
- [ ] After N retries (configurable in project.toml, default 2), story is flagged as blocked and auto-assign stops trying
- [ ] Blocked stories show a clear indicator in pipeline status (MCP and bot status command)
- [ ] Server detects 'coder finished with no commits on feature branch' at gate-check stage and fails the gates early instead of advancing to QA
- [ ] Empty-diff merge failures are detected and reported without needing the mergemaster agent to discover them
## Out of Scope
- TBD

View File

@@ -0,0 +1,68 @@
---
name: "Chat history persistence lost on page refresh (story 145 regression)"
agent: coder-opus
---
## Rejection Notes
**2026-03-16:** Previous coder produced zero code changes — feature branch had no diff against master. The coder must actually use `git bisect` to find the breaking commit and produce a surgical fix. Do not submit with no code changes.
**2026-03-17:** Re-opened. Multiple fix attempts have failed. See investigation notes below for the actual root cause.
# Bug 245: Chat history persistence lost on page refresh (story 145 regression)
## Description
Story 145 implemented localStorage persistence for chat history across page reloads. This is no longer working — refreshing the page loses all conversation context. This is a regression of the feature delivered in story 145.
## How to Reproduce
1. Open the web UI and have a conversation with the agent
2. Refresh the page (F5 or Cmd+R)
3. Send a new message
4. The LLM has no knowledge of the prior conversation
## Actual Result
Chat history is gone after refresh — the UI shows a blank conversation. Even if messages appear in the UI (loaded from localStorage), the LLM does not receive them as context on the next exchange.
## Expected Result
Chat history is restored from localStorage on page load, as implemented in story 145. The LLM should receive the full conversation history when the user sends a new message after refresh.
## Acceptance Criteria
- [ ] Chat messages survive a full page refresh (visible in UI)
- [ ] Chat messages are restored from localStorage on component mount
- [ ] After refresh, the LLM receives full prior conversation history as context when the user sends the next message
- [ ] Behaviour matches the original acceptance criteria from story 145
## Investigation Notes (2026-03-17)
### Root cause analysis
The frontend correctly:
1. Persists messages to localStorage in `useChatHistory.ts` (key: `storykit-chat-history:{projectPath}`)
2. Loads them on mount
3. Sends the FULL history array to the backend via `wsRef.current?.sendChat(newHistory, config)` in `Chat.tsx` line ~558
The backend bug is in `server/src/llm/chat.rs`:
- The `chat()` function receives the full `messages: Vec<Message>` from the client
- Line ~283: `let mut current_history = messages.clone()` — correctly clones full history
- Lines ~299-318: Adds 2 system prompts at position 0 and 1
- Lines ~323-404: Main LLM loop generates new assistant/tool messages
- **Line ~407: `ChatResult { messages: new_messages }` — BUG: returns ONLY the newly generated turn, not the full `current_history`**
During streaming, the `on_update()` callbacks DO send `current_history[2..]` (full history minus system prompts), which is correct. But there may be a reconciliation issue on the frontend where the final state doesn't include the full history.
### Key files
- `frontend/src/hooks/useChatHistory.ts` — localStorage persistence
- `frontend/src/components/Chat.tsx` — sends full history, handles `onUpdate` callbacks
- `frontend/src/api/client.ts` — WebSocket client
- `server/src/http/ws.rs` — WebSocket handler, passes messages to chat()
- `server/src/llm/chat.rs`**THE BUG** at line ~407, ChatResult returns only new_messages
### What NOT to do
- Do NOT layer on a new localStorage implementation. The localStorage code works fine.
- Do NOT add server-side persistence. The "dumb pipe" architecture is correct.
- The fix should be surgical — ensure the full conversation history round-trips correctly through the backend.

View File

@@ -1,5 +1,6 @@
---
name: "Bot must verify other users' cross-signing identity before checking device verification"
agent: mergemaster
---
# Story 256: Bot must verify other users' cross-signing identity before checking device verification
@@ -18,3 +19,16 @@ As a Matrix user messaging the bot, I want the bot to correctly recognize my cro
## Out of Scope
- TBD
## Test Results
<!-- story-kit-test-results: {"unit":[{"name":"sender_with_cross_signing_identity_is_accepted","status":"pass","details":"Verifies get_user_identity Some(_) → accepted"},{"name":"sender_without_cross_signing_identity_is_rejected","status":"pass","details":"Verifies get_user_identity None → rejected"}],"integration":[]} -->
### Unit Tests (2 passed, 0 failed)
- ✅ sender_with_cross_signing_identity_is_accepted — Verifies get_user_identity Some(_) → accepted
- ✅ sender_without_cross_signing_identity_is_rejected — Verifies get_user_identity None → rejected
### Integration Tests (0 passed, 0 failed)
*No integration tests recorded.*

View File

@@ -0,0 +1,26 @@
---
name: "Auto-assign not called after merge failure"
---
# Bug 258: Auto-assign not called after merge failure
## Description
When the background merge pipeline fails (e.g. quality gate timeout), `auto_assign_available_work` is never called. The story stays in `4_merge/` with no agent assigned, requiring manual intervention.
### Root cause
In `pool.rs`, `start_merge_agent_work` spawns a tokio task that calls `run_merge_pipeline`. On failure, the task updates the job status to `Failed` but does NOT call `auto_assign_available_work`. The only call to `auto_assign` in the merge pipeline is inside `run_merge_pipeline` on the success path (line ~1251).
The `spawn_pipeline_advance` completion handler does call `auto_assign` after the mergemaster agent exits, but only on the success path (post-merge tests pass → move to done → auto_assign). On failure, it returns early without triggering auto-assign.
There is no periodic sweep — auto-assign is purely reactive (watcher events, agent completions, startup).
### Impact
After a merge failure, the story is permanently stuck in `4_merge/` with no agent. The only way to unstick it is to restart the server or manually trigger a watcher event.
## Acceptance Criteria
- [ ] After a merge pipeline failure, `auto_assign_available_work` is called so the mergemaster can retry
- [ ] Stories in `4_merge/` do not get permanently stuck after transient merge failures

View File

@@ -0,0 +1,20 @@
---
name: "Move story-kit ignores into .story_kit/.gitignore"
---
# Story 259: Move story-kit ignores into .story_kit/.gitignore
## User Story
As a developer using story-kit, I want story-kit-specific gitignore patterns to live inside .story_kit/.gitignore, so that the host project's root .gitignore stays clean and story-kit concerns are self-contained.
## Acceptance Criteria
- [ ] A .gitignore file exists at .story_kit/.gitignore containing all story-kit-specific ignore patterns
- [ ] The root .gitignore no longer contains story-kit-specific ignore patterns
- [ ] The deterministic project scaffold process creates .story_kit/.gitignore when initialising a new project
- [ ] Existing repos continue to work correctly after the change (no previously-ignored files become tracked)
## Out of Scope
- TBD

View File

@@ -0,0 +1,19 @@
---
name: "Bot notifications when stories move between stages"
agent: coder-opus
---
# Story 261: Bot notifications when stories move between stages
## User Story
As a user, I want to receive bot notifications in the channel whenever a story moves between pipeline stages, so that I can track progress without manually checking status.
## Acceptance Criteria
- [ ] Bot sends a notification to the channel each time a story transitions between stages (e.g. upcoming → current, current → QA, QA → merge, merge → done)
- [ ] Notification includes the story number, name, and the stage transition (from → to)
## Out of Scope
- TBD

View File

@@ -0,0 +1,24 @@
---
name: "Bot error notifications for story failures (with shared messaging)"
---
# Story 262: Bot error notifications for story failures
## User Story
As a user, I want to receive bot notifications with an error icon in the channel whenever a story errors out (e.g. merge failure), so that I'm immediately aware of problems.
## Design Constraint
Story 261 adds stage-transition notifications using the same Matrix messaging path. Extract a shared utility/module for sending Matrix messages so that both error notifications (this story) and stage-transition notifications (261) use the same code path. Do not duplicate Matrix message-sending logic.
## Acceptance Criteria
- [ ] Bot sends an error notification to the channel when a story encounters a failure (e.g. merge failure)
- [ ] Notification includes an error icon to distinguish it from normal stage-transition notifications
- [ ] Notification includes the story number, name, and a description of the error
- [ ] Matrix message-sending logic is in a shared module usable by both error and stage-transition notifications
## Out of Scope
- Stage-transition notifications (covered by story 261)

View File

@@ -0,0 +1,21 @@
---
name: "Matrix bot self-signs device keys at startup for verified encryption"
agent: mergemaster
---
# Story 263: Matrix bot self-signs device keys at startup for verified encryption
## User Story
As a Matrix room participant, I want the bot's messages to not show "encrypted by a device not verified by its owner" warnings, so that I have confidence the bot's encryption is fully verified.
## Acceptance Criteria
- [ ] At startup the bot checks whether its own device keys have been self-signed (cross-signed by its own user identity)
- [ ] If the device keys are not self-signed, the bot signs them automatically
- [ ] After signing, the bot uploads the new signatures to the homeserver
- [ ] After a clean start (fresh matrix_store / device_id) the bot's messages no longer show the 'encrypted by a device not verified by its owner' warning
## Out of Scope
- TBD

View File

@@ -0,0 +1,43 @@
---
name: "Claude Code session ID not persisted across browser refresh"
---
# Bug 264: Claude Code session ID not persisted across browser refresh
## Description
The Claude Code provider uses a session_id to resume conversations via `--resume <id>`. This session_id is stored in React state (`claudeSessionId`) but is NOT persisted to localStorage. After a browser refresh, the session_id is lost (`null`), so Claude Code cannot resume the prior session.
A fallback exists (`build_claude_code_context_prompt` in `server/src/llm/chat.rs:188`) that injects prior messages as flattened text inside a `<conversation_history>` block, but this loses structure (tool calls, tool results, reasoning) and Claude Code treats it as informational text rather than actual conversation turns. In practice, the LLM does not retain meaningful context after refresh.
This is the root cause behind bug 245 (chat history persistence regression). The localStorage message persistence from story 145 works correctly for the UI, but the LLM context is not properly restored because the session cannot be resumed.
Key files:
- `frontend/src/components/Chat.tsx:174``claudeSessionId` is ephemeral React state
- `frontend/src/components/Chat.tsx:553` — session_id only sent when non-null
- `server/src/llm/chat.rs:278` — backend branches on session_id presence
- `server/src/llm/providers/claude_code.rs:44``--resume` flag passed to Claude CLI
## How to Reproduce
1. Open the Story Kit web UI and select claude-code-pty as the model
2. Have a multi-turn conversation with the agent
3. Refresh the browser (F5 or Cmd+R)
4. Send a new message referencing the prior conversation
5. The LLM has no knowledge of the prior conversation
## Actual Result
After refresh, claudeSessionId is null. Claude Code spawns a fresh session without --resume. The fallback text injection is too lossy to provide meaningful context. The LLM behaves as if the conversation never happened.
## Expected Result
After refresh, the Claude Code session is resumed via --resume, giving the LLM full context of the prior conversation including tool calls, reasoning, and all turns.
## Acceptance Criteria
- [ ] claudeSessionId is persisted to localStorage (scoped by project path) and restored on component mount
- [ ] After browser refresh, the next chat message includes session_id in the ProviderConfig
- [ ] Claude Code receives --resume with the persisted session_id after refresh
- [ ] Clearing the session (clear button) also clears the persisted session_id
- [ ] After server restart with session files intact on disk, conversation resumes correctly

View File

@@ -0,0 +1,33 @@
---
name: "Spikes skip merge and stop for human review"
agent: coder-opus
---
# Story 265: Spikes skip merge and stop for human review
## User Story
As a user, I want spike work items to stop after QA instead of auto-advancing to the merge stage, so that I can review the spike's findings and prototype code in the worktree before deciding what to do with them.
## Context
Spikes are investigative — their value is the findings and any prototype code, not a merge to master. The user needs to:
- Read the spike document with findings
- Review prototype code in the worktree
- Optionally build and run the prototype to validate the approach
- Then manually decide: archive the spike and create follow-up stories, or reject and re-investigate
Currently all work items follow the same pipeline: coder → QA → merge → done. Spikes should diverge after QA and wait for human review instead of auto-advancing to merge.
## Acceptance Criteria
- [ ] Items with `_spike_` in the filename skip the merge stage after QA passes
- [ ] After QA, spike items remain accessible for human review (worktree preserved, not cleaned up)
- [ ] Spikes do not auto-advance to `4_merge/` — they stay in `3_qa/` or move to a review-hold state
- [ ] The human can manually archive the spike when done reviewing
- [ ] Non-spike items (stories, bugs, refactors) continue through the full pipeline as before
## Out of Scope
- New UI for spike review (manual file inspection is fine)
- Changes to the spike creation flow

View File

@@ -0,0 +1,60 @@
---
name: "Matrix bot structured conversation history"
agent: coder-opus
---
# Story 266: Matrix bot structured conversation history
## User Story
As a user chatting with the Matrix bot, I want it to remember and own its prior responses naturally, so that conversations feel like talking to one continuous entity rather than a new instance each message.
## Acceptance Criteria
- [ ] Conversation history is passed as structured API messages (user/assistant turns) rather than a flattened text prefix
- [ ] Claude recognises its prior responses as its own, maintaining consistent personality across a conversation
- [ ] Per-room history survives server restarts (persisted to disk or database)
- [ ] Rolling window trimming still applies to keep context bounded
- [ ] Multi-user rooms still attribute messages to the correct sender
## Investigation Notes (2026-03-18)
The current implementation attempts session resumption via `--resume <session_id>` but it's not working:
### Code path: how session resumption is supposed to work
1. `server/src/matrix/bot.rs:671-676``handle_message()` reads `conv.session_id` from the per-room `RoomConversation` to get the resume ID.
2. `server/src/matrix/bot.rs:717` — passes `resume_session_id` to `provider.chat_stream()`.
3. `server/src/llm/providers/claude_code.rs:57``chat_stream()` stores it as `resume_id`.
4. `server/src/llm/providers/claude_code.rs:170-173` — if `resume_session_id` is `Some`, appends `--resume <id>` to the `claude -p` command.
5. `server/src/llm/providers/claude_code.rs:348``process_json_event()` looks for `json["session_id"]` in each streamed NDJSON event and sends it via a oneshot channel (`sid_tx`).
6. `server/src/llm/providers/claude_code.rs:122` — after the PTY exits, `sid_rx.await.ok()` captures the session ID (or `None` if never sent).
7. `server/src/matrix/bot.rs:785-787` — stores `new_session_id` back into `conv.session_id` and persists via `save_history()`.
### What's broken
- **No session_id captured:** `.story_kit/matrix_history.json` contains conversation entries but no `session_id`. `RoomConversation.session_id` is always `None`.
- **Root cause:** `claude -p --output-format stream-json` may not emit a `session_id` in its NDJSON events, or the parser at step 5 isn't matching the actual event shape. The oneshot channel never fires.
- **Effect:** Every message spawns a fresh Claude Code process with no `--resume` flag. Each turn is a blank slate.
- **History persistence works fine** — serialization round-trips correctly (test at `bot.rs:1335-1339`). The problem is purely that `--resume` is never invoked.
### Debugging steps
1. Run `claude -p "hello" --output-format stream-json --verbose 2>/dev/null` manually and inspect the NDJSON for a `session_id` field. Check what event type carries it and whether the key name matches what `process_json_event()` expects.
2. If `session_id` is present but nested differently (e.g. inside an `event` wrapper), fix the JSON path at `claude_code.rs:348`.
3. If `-p` mode doesn't emit `session_id` at all, consider an alternative: pass conversation history as a structured prompt prefix, or switch to the Claude API directly.
### Previous attempt failed (2026-03-18)
A sonnet coder attempted this story but did NOT fix the root cause. It rewrote the `chat_stream()` call in `bot.rs` to look identical to what was already there — it never investigated why `session_id` isn't being captured. The merge auto-resolver then jammed the duplicate call inside the `tokio::select!` permission loop, producing mismatched braces. The broken merge was reverted.
**What the coder must actually do:**
1. **Do NOT rewrite the `chat_stream()` call or the `tokio::select!` loop in `bot.rs`.** That code is correct and handles permission forwarding (story 275). Do not touch it.
2. **The bug is in `claude_code.rs`, not `bot.rs`.** The `process_json_event()` function at line ~348 looks for `json["session_id"]` but it's likely never finding it. Start by running step 1 above to see what the actual NDJSON output looks like.
3. **If `claude -p` doesn't emit `session_id` at all**, the `--resume` approach won't work. In that case, the fix is to pass conversation history as a prompt prefix (prepend prior turns to the user message) or use `--continue` instead of `--resume`, or call the Claude API directly instead of shelling out to the CLI.
4. **Rebase onto current master before starting.** Master has changed significantly (spike 92, story 275 permission handling, gitignore changes).
## Out of Scope
- TBD

View File

@@ -0,0 +1,19 @@
---
name: "MCP update_story tool should support front matter fields"
---
# Story 267: MCP update_story tool should support front matter fields
## User Story
As an operator using the MCP tools, I want update_story to accept optional front matter fields (like agent, manual_qa, etc.) so that I can update story metadata without editing files by hand.
## Acceptance Criteria
- [ ] update_story MCP tool accepts optional agent parameter to set/change the agent front matter field
- [ ] update_story MCP tool accepts optional arbitrary front matter key-value pairs
- [ ] Front matter updates are auto-committed via the filesystem watcher like other story mutations
## Out of Scope
- TBD

View File

@@ -0,0 +1,23 @@
---
name: "Upgrade tokio-tungstenite to 0.29.0"
---
# Refactor 268: Upgrade tokio-tungstenite to 0.29.0
## Current State
- TBD
## Desired State
Upgrade tokio-tungstenite from 0.28.0 to 0.29.0 in workspace Cargo.toml and fix any breaking API changes.
## Acceptance Criteria
- [ ] tokio-tungstenite = "0.29.0" in workspace Cargo.toml
- [ ] All code compiles without errors
- [ ] All tests pass
## Out of Scope
- TBD

View File

@@ -0,0 +1,22 @@
---
name: "@ file references in web UI chat input"
---
# Story 269: @ file references in web UI chat input
## User Story
As a user chatting in the web UI, I want to type @ to get an autocomplete overlay listing project files, so that I can reference specific files in my messages the same way Zed and Claude Code do.
## Acceptance Criteria
- [ ] Typing @ in the chat input triggers a file picker overlay
- [ ] Overlay searches project files with fuzzy matching as the user types after @
- [ ] Selecting a file inserts a reference into the message (e.g. @path/to/file.rs)
- [ ] The referenced file contents are included as context when the message is sent to the LLM
- [ ] Overlay is dismissable with Escape
- [ ] Multiple @ references can be used in a single message
## Out of Scope
- TBD

View File

@@ -0,0 +1,31 @@
---
name: "QA test server overwrites root .mcp.json with wrong port"
---
# Bug 270: QA test server overwrites root .mcp.json with wrong port
## Description
When the QA agent starts a test server in a worktree (e.g. on port 3012), that server auto-detects the shared project root and calls open_project, which writes .mcp.json with the test server's port. This clobbers the root .mcp.json that should always point to the main server (port 3001).
Root cause: open_project in server/src/io/fs.rs:527 unconditionally calls write_mcp_json(&p, port) with its own port. Because worktrees share .story_kit/ with the real project, the test server resolves to the real project root and overwrites the root .mcp.json instead of writing to its own worktree directory.
Fix: Remove the write_mcp_json call from open_project entirely. Worktree .mcp.json files are already written correctly during worktree creation (worktree.rs:81,97), and the root .mcp.json is committed in git. open_project should not touch it.
## How to Reproduce
1. QA agent starts on a story\n2. QA agent starts a test server in the worktree on a non-default port (e.g. 3012)\n3. Test server auto-opens the project root\n4. Root .mcp.json is overwritten with test port
## Actual Result
Root .mcp.json contains the QA test server's port (e.g. 3012) instead of the main server's port (3001). Interactive Claude sessions lose MCP connectivity.
## Expected Result
Root .mcp.json always points to the primary server's port. Test servers started by QA agents should not overwrite it.
## Acceptance Criteria
- [ ] QA test servers do not overwrite root .mcp.json
- [ ] Root .mcp.json always reflects the primary server's port
- [ ] Worktree .mcp.json files are only written during worktree creation

View File

@@ -0,0 +1,19 @@
---
name: "Show assigned agent in expanded work item view"
---
# Story 271: Show assigned agent in expanded work item view
## User Story
As a project owner viewing an expanded work item in the web UI, I want to see which agent (e.g. coder-opus) has been assigned via front matter, so that I know which coder is working on or will pick up the story.
## Acceptance Criteria
- [ ] Expanded work item view displays the agent front matter field if set
- [ ] Shows the specific agent name (e.g. 'coder-opus') not just 'assigned'
- [ ] If no agent is set in front matter, the field is omitted or shows unassigned
## Out of Scope
- TBD

View File

@@ -0,0 +1,19 @@
---
name: "Clear merge error front matter when story leaves merge stage"
---
# Story 272: Clear merge error front matter when story leaves merge stage
## User Story
As an operator, I want merge error front matter to be automatically removed when a story is moved out of the merge stage via MCP, so that stale error metadata doesn't persist when the story is retried.
## Acceptance Criteria
- [ ] When a story with merge_error front matter is moved out of 4_merge via MCP, the merge_error field is automatically stripped
- [ ] Works for all destinations: back to 2_current, back to 1_upcoming, or forward to 5_done
- [ ] Stories without merge_error front matter are unaffected
## Out of Scope
- TBD

View File

@@ -0,0 +1,24 @@
---
name: "Matrix bot sends typing indicator while waiting for Claude response"
---
# Story 273: Matrix bot sends typing indicator while waiting for Claude response
## User Story
As a user chatting with the Matrix bot, I want to see a typing indicator in Element while the bot is processing my message, so that I know it received my request and is working on a response.
## Acceptance Criteria
- [ ] Bot sets m.typing on the room as soon as it starts the Claude API call
- [ ] Typing indicator is cleared when the first response chunk is sent to the room
- [ ] Typing indicator is cleared on error so it doesn't get stuck
- [ ] No visible delay between sending a message and seeing the typing indicator
## Notes
- **2026-03-18**: Moved back to current from done. Previous attempt went through the full pipeline but merged with an empty diff — no typing indicator code was actually implemented. Needs a real implementation this time.
## Out of Scope
- TBD

View File

@@ -0,0 +1,20 @@
---
name: "MCP pipeline status tool with agent assignments"
---
# Story 274: MCP pipeline status tool with agent assignments
## User Story
As a user checking pipeline status, I want an MCP tool that returns a structured status report including which agent is assigned to each work item, so that I can quickly see what's active and spot stuck items.
## Acceptance Criteria
- [ ] New MCP tool (e.g. `get_pipeline_status`) returns all work items across all active pipeline stages (current, qa, merge, done) with their stage, name, and assigned agent
- [ ] Upcoming backlog items are included with count or listing
- [ ] Agent assignment info comes from story front matter (`agent` field) and/or the running agent list
- [ ] Response is structured/deterministic (not free-form prose)
## Out of Scope
- TBD

View File

@@ -0,0 +1,22 @@
---
name: "Matrix bot surfaces Claude Code permission prompts to chat"
agent: coder-opus
---
# Story 275: Matrix bot surfaces Claude Code permission prompts to chat
## User Story
As a user chatting with the Matrix bot, I want to see permission prompts from Claude Code in the chat and be able to approve or deny them, so that headless Claude Code sessions don't silently hang when they need authorization to proceed.
## Acceptance Criteria
- [ ] When Claude Code hits a permission prompt during a bot-initiated session, the bot sends the prompt text to the Matrix room as a message
- [ ] The user can approve or deny the permission by replying in chat (e.g. yes/no or a reaction)
- [ ] The bot relays the user decision back to the Claude Code subprocess so execution continues
- [ ] If the user does not respond within a configurable timeout, the permission is denied (fail-closed)
- [ ] The bot does not hang or timeout silently when a permission prompt is pending - the user always sees what is happening
## Out of Scope
- TBD

View File

@@ -0,0 +1,17 @@
---
name: "Detect and log when root .mcp.json port is modified"
---
# Story 276: Detect and log when root .mcp.json port is modified
## User Story
As a ..., I want ..., so that ...
## Acceptance Criteria
- [ ] TODO
## Out of Scope
- TBD

View File

@@ -0,0 +1,19 @@
---
name: "Matrix bot uses its configured name instead of \"Claude\""
---
# Story 277: Matrix bot uses its configured name instead of "Claude"
## User Story
As a Matrix user, I want the bot to identify itself by its configured name (e.g., "Timmy") rather than "Claude", so that the bot feels like a distinct personality in the chat.
## Acceptance Criteria
- [ ] The Matrix bot refers to itself by its configured display name (e.g., 'Timmy') in conversations, not 'Claude'
- [ ] The bot's self-referencing name is derived from configuration, not hardcoded
- [ ] If no custom name is configured, the bot falls back to a sensible default
## Out of Scope
- TBD

View File

@@ -0,0 +1,17 @@
---
name: "Auto-assign agents to pipeline items on server startup"
---
# Story 278: Auto-assign agents to pipeline items on server startup
## User Story
As a ..., I want ..., so that ...
## Acceptance Criteria
- [ ] TODO
## Out of Scope
- TBD

View File

@@ -0,0 +1,20 @@
---
name: "Auto-assign should respect agent stage when front matter specifies agent"
---
# Story 279: Auto-assign should respect agent stage when front matter specifies agent
## User Story
As a project operator, I want auto-assign to respect the pipeline stage when a story's front matter specifies a preferred agent, so that a coder agent isn't assigned to do QA work just because the story originally requested that coder.
## Acceptance Criteria
- [ ] When a story in `3_qa/` has `agent: coder-opus` in front matter, auto-assign skips the preferred agent (stage mismatch) and assigns a free QA-stage agent instead
- [ ] When a story in `2_current/` has `agent: coder-opus` in front matter, auto-assign still respects the preference (stage matches)
- [ ] When the preferred agent's stage mismatches, auto-assign logs a message indicating the stage mismatch and fallback
## Out of Scope
- Changing the front matter `agent` field automatically when a story advances stages
- Adding per-stage agent preferences to front matter

View File

@@ -0,0 +1,20 @@
---
name: "Matrix bot announces itself when it comes online"
---
# Story 281: Matrix bot announces itself when it comes online
## User Story
As a user in the Matrix room, I want Timmy to post a message when he starts up, so that I know the bot is online and ready to accept commands.
## Acceptance Criteria
- [ ] Bot sends a brief greeting message to each configured room on startup (e.g. 'Timmy is online.')
- [ ] Message uses the configured display_name, not a hardcoded name
- [ ] Message is only sent once per startup, not on reconnects or sync resumptions
- [ ] Bot does not announce if it was already running (e.g. after a brief network blip)
## Out of Scope
- TBD

View File

@@ -0,0 +1,24 @@
---
name: "Matrix bot ambient mode toggle via chat command"
---
# Story 282: Matrix bot ambient mode toggle via chat command
## User Story
As a user chatting with Timmy in a Matrix room, I want to toggle between "addressed mode" (bot only responds when mentioned by name) and "ambient mode" (bot responds to all messages) via a chat command, so that I don't have to @-mention the bot on every message when I'm the only one around.
## Acceptance Criteria
- [ ] Matrix bot defaults to addressed mode — only forwards messages containing the bot's name to Claude
- [ ] Chat command "{bot_name} ambient on" switches to ambient mode — bot forwards all room messages to Claude (bot name comes from display_name in bot.toml)
- [ ] Chat command "{bot_name} ambient off" switches back to addressed mode
- [ ] Mode is persisted per-room in bot.toml so it survives bot restarts
- [ ] bot.toml.example includes the ambient_mode setting with a comment explaining it
- [ ] Bot confirms the mode switch with a short response in chat
- [ ] When other users join or are active, user can flip back to addressed mode to avoid noise
- [ ] Ambient mode applies per-room (not globally across all rooms)
## Out of Scope
- TBD

View File

@@ -0,0 +1,30 @@
---
name: "Pipeline does not check manual_qa flag before advancing from QA to merge"
---
# Bug 283: Pipeline does not check manual_qa flag before advancing from QA to merge
## Description
Story 247 added the manual_qa front matter field and the MCP tooling to set it, but the pipeline in pool.rs never actually checks the flag. After QA passes gates and coverage, stories move straight to merge regardless of manual_qa setting.
## How to Reproduce
1. Create a story with manual_qa: true in front matter
2. Let it go through the coder and QA stages
3. Observe that it moves directly to merge without waiting for human approval
## Actual Result
Stories always advance from QA to merge automatically, ignoring the manual_qa flag.
## Expected Result
Stories with manual_qa: true should pause after QA passes and wait for human approval before moving to merge. Stories with manual_qa: false (the default) should advance automatically as they do now.
## Acceptance Criteria
- [ ] Pipeline checks manual_qa front matter field after QA gates pass
- [ ] manual_qa defaults to false — stories advance automatically unless explicitly opted in
- [ ] Stories with manual_qa: true wait in 3_qa for human approval via accept_story or the UI
- [ ] Stories with manual_qa: false proceed directly from QA to merge as before

View File

@@ -0,0 +1,22 @@
---
name: "Matrix bot status command shows pipeline and agent availability"
---
# Story 284: Matrix bot status command shows pipeline and agent availability
## User Story
As a user in a Matrix room, I want to type "{bot_name} status" and get a formatted summary of the full pipeline (upcoming through done) with agent assignments, plus which agents are currently free, so that I can check project status without leaving chat.
## Acceptance Criteria
- [ ] Chat command "{bot_name} status" triggers a pipeline status display (bot name comes from display_name in bot.toml)
- [ ] Output shows all stages: upcoming, current, qa, merge, done — with story names and IDs
- [ ] Each active story shows its assigned agent name and model
- [ ] Output includes a section showing which agents are free (not currently assigned to any story)
- [ ] Response is formatted for readability in Matrix (monospace or markdown as appropriate)
- [ ] Command is handled at the bot level — does not require a full Claude invocation
## Out of Scope
- TBD

View File

@@ -0,0 +1,25 @@
---
name: "Matrix bot help command lists available bot commands"
---
# Story 285: Matrix bot help command lists available bot commands
## User Story
As a user in a Matrix room, I want to type "{bot_name} help" and get a list of all available bot commands with brief descriptions, so that I can discover what the bot can do without having to ask or remember.
## Acceptance Criteria
- [ ] Chat command "{bot_name} help" displays a list of all available bot-level commands (bot name comes from display_name in bot.toml)
- [ ] Each command is shown with a short description of what it does
- [ ] Help output is formatted for readability in Matrix
- [ ] Help command is handled at the bot level — does not require a full Claude invocation
- [ ] Help list automatically includes new commands as they are added (driven by a registry or similar, not a hardcoded string)
## Notes
- **2026-03-18**: Moved back to current from done. Previous attempt committed code directly to master (commit a32cfbd) instead of the worktree, and the help command is not functional in the running server. Needs a clean implementation this time.
## Out of Scope
- TBD

View File

@@ -0,0 +1,22 @@
---
name: "Server self-rebuild and restart via MCP tool"
---
# Story 286: Server self-rebuild and restart via MCP tool
## User Story
As a project owner away from my terminal, I want to tell the bot to restart the server so that it picks up new code changes, without needing physical access to the machine.
## Acceptance Criteria
- [ ] MCP tool `rebuild_and_restart` triggers a cargo build of the server
- [ ] If the build fails, server stays up and returns the build error
- [ ] If the build succeeds, server re-execs itself with the new binary using std::os::unix::process::CommandExt::exec()
- [ ] Server logs the restart so it's traceable
- [ ] Matrix bot reconnects automatically after the server comes back up
- [ ] Running agents are gracefully stopped before re-exec
## Out of Scope
- TBD

View File

@@ -0,0 +1,22 @@
---
name: "Rename upcoming pipeline stage to backlog"
---
# Story 287: Rename upcoming pipeline stage to backlog
## User Story
As a project owner, I want the "upcoming" pipeline stage renamed to "backlog" throughout the codebase, UI, and directory structure, so that the terminology better reflects that these items are not necessarily coming up next.
## Acceptance Criteria
- [ ] Directory renamed from 1_upcoming to 1_backlog
- [ ] All server code references updated (watcher, lifecycle, MCP tools, workflow, etc.)
- [ ] Frontend UI labels updated
- [ ] MCP tool descriptions and outputs use "backlog" instead of "upcoming"
- [ ] Existing story/bug files moved to the new directory
- [ ] Git commit messages use "backlog" for new items going forward
## Out of Scope
- TBD

View File

@@ -0,0 +1,29 @@
---
name: "Ambient mode state lost on server restart"
---
# Bug 288: Ambient mode state lost on server restart
## Description
Story 282 implemented ambient mode toggle but only in-memory. The acceptance criterion requiring persistence in bot.toml was not implemented. Every server restart (including rebuild_and_restart) clears ambient mode for all rooms.
## How to Reproduce
1. Type "timmy ambient on" — get confirmation
2. Restart server (or rebuild_and_restart)
3. Send unaddressed message — bot ignores it, ambient mode is gone
## Actual Result
Ambient mode state is lost on server restart.
## Expected Result
Ambient mode per-room state is persisted in bot.toml and restored on startup.
## Acceptance Criteria
- [ ] Ambient mode per-room state is saved to bot.toml when toggled
- [ ] Ambient mode state is restored from bot.toml on server startup
- [ ] bot.toml.example includes the ambient_rooms setting with a comment

View File

@@ -0,0 +1,28 @@
---
name: "rebuild_and_restart MCP tool does not rebuild"
agent: coder-opus
---
# Bug 289: rebuild_and_restart MCP tool does not rebuild
## Description
The rebuild_and_restart MCP tool re-execs the server binary but does not run cargo build first. It restarts with the old binary, so code changes are not picked up.
## How to Reproduce
1. Make a code change to the server
2. Call rebuild_and_restart via MCP
3. Observe the server restarts but the code change is not reflected
## Actual Result
Server re-execs with the old binary. Code changes are not compiled.
## Expected Result
Server runs cargo build --release (or cargo build) before re-execing, so the new binary includes the latest code changes.
## Acceptance Criteria
- [ ] Bug is fixed and verified

View File

@@ -0,0 +1,22 @@
---
name: "Remove agent thinking traces from agents sidebar"
---
# Story 290: Remove agent thinking traces from agents sidebar
## User Story
As a user viewing an expanded work item in the web UI, I want to see the live agent output stream (thinking traces, tool calls, progress) for the agent working on that story, so that I can monitor progress in context rather than in the agents sidebar.
## Acceptance Criteria
- [ ] Agent thinking traces are removed from the agents sidebar panel — they should only appear in the work item detail panel (which already has SSE streaming wired up in `WorkItemDetailPanel.tsx`)
## Notes
The detail panel (`WorkItemDetailPanel.tsx`) already has agent log streaming implemented — SSE subscription, real-time output, status badges, etc. The only remaining work is removing the thinking traces from the agents sidebar (`AgentPanel.tsx` or similar) so they don't appear in both places.
## Out of Scope
- Replacing the agents sidebar entirely — it still shows agent names, status, and assignments
- Historical agent output (only live stream while agent is running)

View File

@@ -0,0 +1,21 @@
---
name: "Show test results in work item detail panel"
---
# Story 291: Show test results in work item detail panel
## User Story
As a project owner viewing a work item in the web UI, I want to see the most recent test run results in the expanded detail panel, so that I can quickly see pass/fail status without digging through agent logs.
## Acceptance Criteria
- [ ] Expanded work item detail panel shows the most recent test results for that story
- [ ] Test results display pass/fail counts for unit and integration tests
- [ ] Failed tests are listed by name so you can see what broke
- [ ] Test results are read from the story file's ## Test Results section (already written by record_tests MCP tool)
- [ ] Panel shows a clear empty state when no test results exist yet
## Out of Scope
- TBD

View File

@@ -0,0 +1,22 @@
---
name: "Show server logs in web UI"
review_hold: true
---
# Story 292: Show server logs in web UI
## User Story
As a project owner using the web UI, I want to see live server logs in the interface, so that I can debug agent behavior and pipeline issues without needing terminal access.
## Acceptance Criteria
- [ ] Web UI has a server logs panel accessible from the main interface
- [ ] Logs stream in real-time via WebSocket or SSE
- [ ] Logs can be filtered by keyword (same as get_server_logs MCP tool's filter param)
- [ ] Log entries show timestamp and severity level
- [ ] Panel doesn't interfere with the existing pipeline board and work item views
## Out of Scope
- TBD

View File

@@ -0,0 +1,23 @@
---
name: "Register all bot commands in the command registry"
review_hold: true
---
# Story 293: Register all bot commands in the command registry
## User Story
As a user, I want all bot commands (help, status, ambient on/off) to be registered in the command registry in commands.rs, so that the help command lists everything the bot can do and there's one consistent mechanism for handling bot-level commands.
## Acceptance Criteria
- [ ] Status command is moved from bot.rs into the command registry in commands.rs
- [ ] Ambient on/off command is moved from bot.rs into the command registry in commands.rs
- [ ] Help command output lists all registered commands including status and ambient
- [ ] No bot-level commands are handled outside of the registry (single mechanism)
- [ ] Existing behavior of all commands is preserved
- [ ] Registry handler functions receive enough context to perform their work (e.g. project_root for status, ambient_rooms for ambient)
## Out of Scope
- TBD

View File

@@ -0,0 +1,19 @@
---
name: "Rename app title from Story Kit to Storkit"
review_hold: true
---
# Story 294: Rename app title from Story Kit to Storkit
## User Story
As a user, I want the application title to say "Storkit" instead of "Story Kit", so that the branding reflects the new name.
## Acceptance Criteria
- [ ] The top title in the web UI header displays "Storkit" instead of "Story Kit"
- [ ] Any other visible references to "Story Kit" in the UI are updated to "Storkit"
## Out of Scope
- TBD

View File

@@ -0,0 +1,64 @@
---
name: "Stories stuck in QA when QA agent is busy"
review_hold: true
---
# Bug 295: Stories stuck in QA when QA agent is busy
## Description
When multiple stories pass coding gates simultaneously and move to QA, only the first one gets a QA agent assigned. The others fail with "Agent 'qa' is already running" and are never retried when the QA agent becomes free. Stories get stuck in QA with no agent indefinitely.
The root cause is in the server-owned agent completion handler in `server/src/agents/pool.rs`. When a coder finishes and gates pass, the server calls the pipeline advance logic which tries to start a QA agent. If the QA agent is already busy on another story, the start fails with an error and the story is left in `3_qa/` with no agent. There is no retry mechanism — the `auto_assign_available_work` function is only called on startup (via `reconcile_on_startup`) and when agents are manually started, not when agents complete.
## How to Reproduce
1. Have 3 stories in current with coders running (e.g. coder-1, coder-2, coder-opus)
2. All 3 coders finish within seconds of each other and pass gates
3. Server tries to start QA agent on all 3:
- Story 292: `qa` agent starts successfully
- Story 293: fails — `"Agent 'qa' is already running on story '292'"`
- Story 294: fails — same error
4. QA on 292 completes (gates pass after retry)
5. Stories 293 and 294 remain stuck in QA with no agent — nobody retries them
## Server Log Evidence (2026-03-18)
```
21:00:35 [agent:292:coder-1] Done.
21:00:42 [agents] Server-owned completion for '292:coder-1': gates_passed=true
21:00:47 [agent:292:qa] Spawning claude...
21:01:32 [agent:293:coder-2] Done.
21:01:34 [agent:294:coder-opus] Done.
21:01:41 [agents] Server-owned completion for '293:coder-2': gates_passed=true
21:01:41 [ERROR] Failed to start qa agent for '293': Agent 'qa' is already running on story '292'
21:01:48 [agents] Server-owned completion for '294:coder-opus': gates_passed=true
21:01:48 [ERROR] Failed to start qa agent for '294': Agent 'qa' is already running on story '292'
21:08:18 [agents] Server-owned completion for '292:qa': gates_passed=true
(293 and 294 are never picked up)
```
## Actual Result
Stories 293 and 294 stuck in QA with no agent after 292's QA agent was busy. The pipeline status shows them in `3_qa` with `agent: null` indefinitely.
## Expected Result
When a QA agent finishes a story, `auto_assign_available_work` should be called to scan for unassigned stories in all active stages and assign free agents. Stories 293 and 294 should get QA agents as soon as the QA agent finishes with 292.
## Suggested Fix
In the server-owned completion handler (the code path that runs after an agent's process exits), call `auto_assign_available_work()` after processing the completed story. This ensures that when any agent becomes free, the server immediately looks for pending work to assign it to.
The relevant code is in `server/src/agents/pool.rs` — the `handle_agent_completion` path (around line 804-950) and `auto_assign_available_work` (around line 1437-1559).
## Acceptance Criteria
- [ ] When an agent completes (any stage), `auto_assign_available_work` is called to pick up pending stories
- [ ] Stories that failed agent assignment due to busy agents are picked up when agents become available
- [ ] Server logs when a story is queued for retry vs permanently failed
- [ ] Multiple stories completing QA sequentially works correctly (story A finishes QA → story B gets QA agent)

View File

@@ -0,0 +1,54 @@
---
name: "Track per-agent token usage for cost visibility and optimisation"
---
# Story 296: Track per-agent token usage for cost visibility and optimisation
## User Story
As a project owner, I want to see how many tokens each agent consumes per story, so that I can identify expensive operations and optimise token usage across the pipeline.
## Acceptance Criteria
- [ ] Implement per-agent token tracking that captures input tokens, output tokens, and cache tokens for each agent run
- [ ] Token usage is recorded per story and per agent (e.g. coder-1 on story 293 used X tokens)
- [ ] Running totals are visible — either via MCP tool, web UI, or both
- [ ] Historical token usage is persisted so it survives server restarts (e.g. in story files or a separate log)
- [ ] Data is structured to support later analysis (e.g. which agent types are most expensive, which stories cost the most)
## Research Notes
Claude Code's JSON stream already emits all the data we need. No external library required.
**Data available in the `result` event at end of each agent session:**
```json
{
"type": "result",
"total_cost_usd": 1.57,
"usage": {
"input_tokens": 7,
"output_tokens": 475,
"cache_creation_input_tokens": 185020,
"cache_read_input_tokens": 810585
},
"modelUsage": {
"claude-opus-4-6[1m]": {
"inputTokens": 7,
"outputTokens": 475,
"cacheReadInputTokens": 810585,
"cacheCreationInputTokens": 185020,
"costUSD": 1.57
}
}
}
```
**Where to hook in:**
- `server/src/llm/providers/claude_code.rs``process_json_event()` already parses the JSON stream but currently ignores usage data from the `result` event
- Parse `usage` + `total_cost_usd` from the `result` event and pipe it to the agent completion handler in `server/src/agents/pool.rs`
**No external libraries needed** — Anthropic SDK, LiteLLM, Helicone, Langfuse etc. are all overkill since we have direct access to Claude Code's output stream.
## Out of Scope
- TBD

View File

@@ -0,0 +1,20 @@
---
name: "Improve bot status command formatting"
---
# Story 297: Improve bot status command formatting
## User Story
As a user reading the bot's status output in Matrix, I want to see clean story numbers and titles (not filenames), with agent assignments shown inline, so that the output is easy to scan at a glance.
## Acceptance Criteria
- [ ] Status output shows story number and title (e.g. '293 — Register all bot commands') not the full filename stem
- [ ] Each story shows which agent is working on it if one is assigned (e.g. 'coder-1 (sonnet)')
- [ ] Stories with no agent assigned show no agent info rather than cluttering the output
- [ ] Output is compact and scannable in a Matrix chat window
## Out of Scope
- TBD

View File

@@ -0,0 +1,25 @@
---
name: "Bot htop command with live-updating process dashboard"
---
# Story 298: Bot htop command with live-updating process dashboard
## User Story
As a project owner in a Matrix room, I want to type "{bot_name} htop" and see a live-updating dashboard of system load and agent processes, so that I can monitor resource usage without needing terminal access.
## Acceptance Criteria
- [ ] '{bot_name} htop' sends an initial status message showing load average and per-agent process info (CPU, memory, story assignment)
- [ ] Message is edited every 5 seconds with updated stats
- [ ] Only shows processes related to the project (agent PIDs and their child process trees)
- [ ] '{bot_name} htop stop' stops the live updating and sends a final 'monitoring stopped' edit
- [ ] Works regardless of what language/toolchain the agents are using (monitors by PID tree, not by process name)
- [ ] Uses Matrix message editing (replacement events) to update in place
- [ ] Only one htop session per room at a time — a second '{bot_name} htop' stops the existing session and starts a fresh one
- [ ] Auto-stops after 5 minutes by default to prevent runaway editing
- [ ] Optional timeout override: '{bot_name} htop 10m' to set a custom duration
## Out of Scope
- TBD

View File

@@ -0,0 +1,22 @@
---
name: "Bot git status command shows working tree and branch info"
---
# Story 299: Bot git status command shows working tree and branch info
## User Story
As a project owner in a Matrix room, I want to type "{bot_name} git" and see the current git status (branch, uncommitted changes, how far ahead/behind remote), so that I can check the repo state without terminal access.
## Acceptance Criteria
- [ ] '{bot_name} git' displays current branch name
- [ ] Shows count of uncommitted changes (staged and unstaged) with filenames
- [ ] Shows how many commits ahead/behind the remote branch
- [ ] Output is formatted compactly for Matrix chat
- [ ] Registered in the command registry in commands.rs so it appears in help output
- [ ] Handled at bot level without LLM invocation
## Out of Scope
- TBD

View File

@@ -0,0 +1,21 @@
---
name: "Show token cost badge on pipeline board work items"
---
# Story 300: Show token cost badge on pipeline board work items
## User Story
As a project owner viewing the pipeline board, I want to see the total token cost for each work item displayed as a badge, so that I can quickly spot expensive stories at a glance.
## Acceptance Criteria
- [ ] Each work item on the pipeline board shows its total cost in USD as a small badge
- [ ] Cost is fetched from the token_usage.jsonl data via a new API endpoint
- [ ] Items with no recorded usage show no badge (not $0.00)
- [ ] Cost updates when the pipeline refreshes (e.g. after an agent completes)
- [ ] Expanded work item detail panel shows per-agent cost breakdown (coder, QA, mergemaster) with token counts
## Out of Scope
- TBD

View File

@@ -0,0 +1,21 @@
---
name: "Dedicated token usage page in web UI"
---
# Story 301: Dedicated token usage page in web UI
## User Story
As a project owner, I want a dedicated token usage page in the web UI that shows per-story and per-agent cost breakdowns with totals, so that I can analyse where tokens are being spent and identify optimisation opportunities.
## Acceptance Criteria
- [ ] New page/panel accessible from the main navigation
- [ ] Shows a table of all recorded agent sessions with story, agent name, model, token counts, and cost
- [ ] Sortable by cost, story, agent, or date
- [ ] Shows summary totals: total cost, cost by agent type (coder vs QA vs mergemaster), cost by model (opus vs sonnet)
- [ ] Data loads from the token_usage.jsonl log via API endpoint
## Out of Scope
- TBD

View File

@@ -0,0 +1,22 @@
---
name: "Bot cost command shows total and per-story token spend"
---
# Story 302: Bot cost command shows total and per-story token spend
## User Story
As a project owner in a Matrix room, I want to type "{bot_name} cost" to see total token spend and the top most expensive stories, so that I can check burn rate from my phone.
## Acceptance Criteria
- [ ] '{bot_name} cost' shows total spend for the last 24 hours
- [ ] Shows top 5 most expensive stories from the last 24 hours with their costs
- [ ] Shows cost breakdown by agent type (coder, QA, mergemaster) for the last 24 hours
- [ ] Also shows an all-time total for context
- [ ] Registered in the command registry so it appears in help output
- [ ] Handled at bot level without LLM invocation
## Out of Scope
- TBD

View File

@@ -0,0 +1,21 @@
---
name: "Bot cost command with story filter for detailed breakdown"
---
# Story 303: Bot cost command with story filter for detailed breakdown
## User Story
As a project owner in a Matrix room, I want to type "{bot_name} cost 293" to see a detailed token breakdown for a specific story, so that I can understand where the tokens went on an expensive item.
## Acceptance Criteria
- [ ] '{bot_name} cost {story_number}' shows all agent sessions for that story
- [ ] Each session shows agent name, model, input/output/cache tokens, and cost in USD
- [ ] Shows total cost for the story at the bottom
- [ ] Registered in the command registry (can share the 'cost' command with args parsing)
- [ ] Returns a friendly message if no usage data exists for the story
## Out of Scope
- TBD

View File

@@ -0,0 +1,23 @@
---
name: "MCP tool to move stories between pipeline stages"
---
# Story 304: MCP tool to move stories between pipeline stages
## User Story
As a bot operator (Timmy), I want an MCP tool that moves stories between pipeline stages, so that I don't need shell mv permissions to manage the pipeline.
## Acceptance Criteria
- [ ] New MCP tool 'move_story' accepts story_id and target_stage (e.g. 'backlog', 'current', 'qa', 'merge', 'done')
- [ ] Validates the story exists before moving
- [ ] Handles the file move between stage directories
- [ ] Returns a confirmation message with the old and new stage
- [ ] Works for stories, bugs, spikes, and refactors
- [ ] Replaces the need for shell mv commands to move story files
- [ ] Tool description tells bots to prefer specific tools (accept_story, move_story_to_merge, request_qa) when available, and use move_story only for arbitrary moves that lack a dedicated tool (e.g. moving to backlog, moving ghost stories back to current)
## Out of Scope
- TBD

View File

@@ -0,0 +1,21 @@
---
name: "Bot show command displays story text in chat"
---
# Story 305: Bot show command displays story text in chat
## User Story
As a project owner in a Matrix room, I want to type "{bot_name} show {story_number}" and see the full story text displayed in chat, so that I can review story details without accessing the file system.
## Acceptance Criteria
- [ ] '{bot_name} show {number}' finds the story/bug/spike by number across all pipeline stages and displays its full markdown content
- [ ] Output is formatted for readability in Matrix
- [ ] Returns a friendly message if no story with that number exists
- [ ] Registered in the command registry so it appears in help output
- [ ] Handled at bot level without LLM invocation
## Out of Scope
- TBD

View File

@@ -0,0 +1,26 @@
---
name: "Replace manual_qa boolean with configurable qa mode field"
---
# Story 306: Replace manual_qa boolean with configurable qa mode field
## User Story
As a project owner, I want to configure QA mode per-story and set a project-wide default, so that I can choose between human review, server-only gate checks, or full agent QA on a per-story basis.
## Acceptance Criteria
- [ ] Replace manual_qa: true/false front matter field with qa: human|server|agent
- [ ] qa: server — skip the QA agent entirely, rely on server's automated gate checks (clippy + tests + coverage). If gates pass, advance straight to merge
- [ ] qa: agent — current behavior, spin up a QA agent (Claude session) to review code and run gates
- [ ] qa: human — hold in QA for human approval after server gates pass (current manual_qa: true behavior)
- [ ] Default qa mode is configurable in project.toml (e.g. default_qa = "server")
- [ ] Set the initial default in project.toml to "server"
- [ ] Per-story front matter qa field overrides the project default
- [ ] Backwards compatible: existing stories without a qa field use the project default
- [ ] Remove the old manual_qa field handling and replace with the new qa field throughout pool.rs, story_metadata.rs, and any other references
- [ ] Update bot.toml.example and project.toml documentation to reflect the new field
## Out of Scope
- TBD

View File

@@ -0,0 +1,25 @@
---
name: "Configurable coder pool size and default model in project.toml"
agent: coder-opus
---
# Story 307: Configurable coder pool size and default model in project.toml
## User Story
As a project owner, I want to configure the number of concurrent coder agents and their default model in project.toml, so that I can control resource usage and cost while still being able to override per-story when needed.
## Acceptance Criteria
- [ ] New project.toml setting: default_coder_model (e.g. 'sonnet') determines which model is used for coder agents by default
- [ ] New project.toml setting: max_coders (e.g. 3) limits concurrent coder agent slots
- [ ] Add one more sonnet coder to the agent config (coder-3) for a total of 3 sonnet coders
- [ ] When all coder slots are full, new stories wait in current until a slot frees up
- [ ] Per-story front matter agent field still overrides the default (e.g. agent: coder-opus assigns opus)
- [ ] Opus coders are only used when explicitly requested via front matter
- [ ] QA and mergemaster limits are unchanged (not configurable via this story)
- [ ] auto_assign_available_work respects the max_coders limit
## Out of Scope
- TBD

View File

@@ -0,0 +1,20 @@
---
name: "Show token cost breakdown in expanded work item detail panel"
---
# Story 309: Show token cost breakdown in expanded work item detail panel
## User Story
As a project owner viewing a work item in the web UI, I want to see a per-agent token cost breakdown in the expanded detail panel, so that I can understand where tokens were spent on that story.
## Acceptance Criteria
- [ ] WorkItemDetailPanel fetches token cost data using the existing /work-items/:story_id/token-cost endpoint
- [ ] Shows per-agent session breakdown: agent name, model, token counts (input/output/cache), cost in USD
- [ ] Shows total cost for the story
- [ ] Shows empty state when no token data exists for the story
## Out of Scope
- TBD

View File

@@ -0,0 +1,119 @@
---
name: "Stop auto-committing intermediate pipeline moves"
agent: "coder-opus"
review_hold: true
---
# Spike 92: Stop auto-committing intermediate pipeline moves
## Goal
Determine how to stop the filesystem watcher from auto-committing every pipeline stage move (upcoming -> current -> qa -> merge -> done -> archive) while still committing at terminal states (creation in upcoming, acceptance in done and archived). This keeps git history clean while preserving cross-machine portability for completed work.
## Context
The watcher in `server/src/io/watcher.rs` currently auto-commits every file change in `.story_kit/work/`. A single story run generates 5+ commits just from pipeline moves:
- `story-kit: create 42_story_foo`
- `story-kit: start 42_story_foo`
- `story-kit: queue 42_story_foo for QA`
- `story-kit: queue 42_story_foo for merge`
- `story-kit: accept 42_story_foo`
Since story runs complete relatively quickly, the intermediate state (current/qa/merge) is transient and doesn't need to be committed. Only creation and archival are meaningful checkpoints.
## Questions to Answer
1. Can we filter `stage_metadata()` to only commit for `1_upcoming` and `6_archived` stages while still broadcasting `WatcherEvent`s for all stages (so the frontend stays in sync)?
2. Should we keep `git add -A .story_kit/work/` for the committed stages, or narrow it to only the specific file?
3. What happens if the server crashes mid-pipeline? Uncommitted moves are lost — is this acceptable given the story can just be re-run?
4. Should intermediate moves be `.gitignore`d at the directory level, or is filtering in the watcher sufficient?
5. Do any other parts of the system (agent worktree setup, merge_agent_work, sparse checkout) depend on intermediate pipeline files being committed to master?
## Findings
### Q1: Can we filter to only commit terminal stages?
**Yes.** The fix is in `flush_pending()`, not `stage_metadata()`. We add a `should_commit_stage()` predicate that returns `true` only for `1_upcoming` and `6_archived`. The event broadcast path is decoupled from the commit path — `flush_pending()` always broadcasts a `WatcherEvent` regardless of whether it commits.
Prototype implemented: added `COMMIT_WORTHY_STAGES` constant and `should_commit_stage()` function. The change is ~15 lines including the constant, predicate, and conditional in `flush_pending()`.
### Q2: Keep `git add -A .story_kit/work/` or narrow to specific file?
**Keep `git add -A .story_kit/work/`.** When committing a terminal stage (e.g. `6_archived`), the file has been moved from a previous stage (e.g. `5_done`). Using `-A` on the whole work directory captures both the addition in the new stage and the deletion from the old stage in a single commit. Narrowing to the specific file would miss the deletion side of the move.
### Q3: Server crash mid-pipeline — acceptable?
**Yes.** If the server crashes while a story is in `2_current`, `3_qa`, or `4_merge`, the file is lost from git but:
- The story file still exists on the filesystem (it's just not committed)
- The agent's work is in its own feature branch/worktree (independent of pipeline file state)
- The story can be re-queued from `1_upcoming` which IS committed
- Pipeline state is transient by nature — it reflects "what's happening right now", not permanent record
### Q4: `.gitignore` vs watcher filtering?
**Watcher filtering is sufficient.** `.gitignore` approach (Option C) has downsides:
- `git status` won't show pipeline state, making debugging harder
- If you ever need to commit an intermediate state (e.g. for a new feature), you'd have to fight `.gitignore`
- Watcher filtering is explicit and easy to understand — a constant lists the commit-worthy stages
- No risk of accidentally ignoring files that should be tracked
### Q5: Dependencies on intermediate pipeline commits?
**None found.** Thorough investigation confirmed:
1. **`merge_agent_work`** (`agents/merge.rs`): Creates a temporary `merge-queue/` branch and worktree. Reads the feature branch, not pipeline files. After merge, calls `move_story_to_archived()` which is a filesystem operation.
2. **Agent worktree setup** (`worktree.rs`): Creates worktrees from feature branches. Sparse checkout is a no-op (disabled). Does not read pipeline file state from git.
3. **MCP tool handlers** (`agents/lifecycle.rs`): `move_story_to_current()`, `move_story_to_merge()`, `move_story_to_qa()`, `move_story_to_archived()` — all pure filesystem `fs::rename()` operations. None perform git commits.
4. **Frontend** (`http/workflow.rs`): `load_pipeline_state()` reads directories from the filesystem directly via `fs::read_dir()`. Never calls git. WebSocket events keep the frontend in sync.
5. **No git inspection commands** reference pipeline stage directories anywhere in the codebase.
### Edge Cases
- **Multi-machine sync:** Only `1_upcoming` and `6_archived` are committed. If you push/pull, you'll see story creation and archival but not intermediate pipeline state. This is correct — intermediate state is machine-local runtime state.
- **Manual git operations:** `git status` will show uncommitted files in intermediate stages. This is actually helpful for debugging — you can see what's in the pipeline without grepping git log.
- **Sweep (5_done → 6_archived):** The sweep moves files to `6_archived`, which triggers a watcher event that WILL commit (since `6_archived` is a terminal stage). This naturally captures the final state.
## Approach to Investigate
### Option A: Filter in `flush_pending()` ← **RECOMMENDED**
- In `flush_pending()`, still broadcast the `WatcherEvent` for all stages
- Only call `git_add_work_and_commit()` for stages `1_upcoming` and `6_archived`
- Simplest change — ~15 lines modified in `watcher.rs`
### Option B: Two-tier watcher
- Split into "commit-worthy" events (create, archive) and "notify-only" events (start, qa, merge)
- Commit-worthy events go through git
- Notify-only events just broadcast to WebSocket clients
- More explicit but same end result as Option A
### Option C: .gitignore intermediate directories
- Add `2_current/`, `3_qa/`, `4_merge/` to `.gitignore`
- Watcher still sees events (gitignore doesn't affect filesystem watching)
- Git naturally ignores them
- Risk: harder to debug, `git status` won't show pipeline state
## Recommendation
**Option A is viable and implemented.** The prototype is in `server/src/io/watcher.rs`:
- Added `COMMIT_WORTHY_STAGES` constant: `["1_upcoming", "6_archived"]`
- Added `should_commit_stage()` predicate
- Modified `flush_pending()` to conditionally commit based on stage, while always broadcasting events
- All 872 tests pass, clippy clean
A full story run will now produce only 2 pipeline commits instead of 5+:
- `story-kit: create 42_story_foo` (creation in `1_upcoming`)
- `story-kit: accept 42_story_foo` (archival in `6_archived`)
The intermediate moves (`start`, `queue for QA`, `queue for merge`, `done`) are still broadcast to WebSocket clients for real-time frontend updates, but no longer clutter git history.
## Acceptance Criteria
- [x] Spike document updated with findings and recommendation
- [x] If Option A is viable: prototype the change and verify git log is clean during a full story run
- [x] Confirm frontend still receives real-time pipeline updates for all stages
- [x] Confirm no other system depends on intermediate pipeline commits being on master
- [x] Identify any edge cases (server crash, manual git operations, multi-machine sync)

49
Cargo.lock generated
View File

@@ -3997,7 +3997,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "story-kit"
version = "0.1.0"
version = "0.3.1"
dependencies = [
"async-stream",
"async-trait",
@@ -4025,8 +4025,8 @@ dependencies = [
"strip-ansi-escapes",
"tempfile",
"tokio",
"tokio-tungstenite 0.28.0",
"toml 1.0.6+spec-1.1.0",
"tokio-tungstenite 0.29.0",
"toml 1.0.7+spec-1.1.0",
"uuid",
"wait-timeout",
"walkdir",
@@ -4333,14 +4333,14 @@ dependencies = [
[[package]]
name = "tokio-tungstenite"
version = "0.28.0"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857"
checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c"
dependencies = [
"futures-util",
"log",
"tokio",
"tungstenite 0.28.0",
"tungstenite 0.29.0",
]
[[package]]
@@ -4367,22 +4367,22 @@ dependencies = [
"serde_spanned",
"toml_datetime 0.7.5+spec-1.1.0",
"toml_parser",
"winnow",
"winnow 0.7.14",
]
[[package]]
name = "toml"
version = "1.0.6+spec-1.1.0"
version = "1.0.7+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "399b1124a3c9e16766831c6bba21e50192572cdd98706ea114f9502509686ffc"
checksum = "dd28d57d8a6f6e458bc0b8784f8fdcc4b99a437936056fa122cb234f18656a96"
dependencies = [
"indexmap",
"serde_core",
"serde_spanned",
"toml_datetime 1.0.0+spec-1.1.0",
"toml_datetime 1.0.1+spec-1.1.0",
"toml_parser",
"toml_writer",
"winnow",
"winnow 1.0.0",
]
[[package]]
@@ -4396,9 +4396,9 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "1.0.0+spec-1.1.0"
version = "1.0.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e"
checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9"
dependencies = [
"serde_core",
]
@@ -4412,23 +4412,23 @@ dependencies = [
"indexmap",
"toml_datetime 0.7.5+spec-1.1.0",
"toml_parser",
"winnow",
"winnow 0.7.14",
]
[[package]]
name = "toml_parser"
version = "1.0.9+spec-1.1.0"
version = "1.0.10+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4"
checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420"
dependencies = [
"winnow",
"winnow 1.0.0",
]
[[package]]
name = "toml_writer"
version = "1.0.6+spec-1.1.0"
version = "1.0.7+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
checksum = "f17aaa1c6e3dc22b1da4b6bba97d066e354c7945cac2f7852d4e4e7ca7a6b56d"
[[package]]
name = "tower"
@@ -4562,9 +4562,9 @@ dependencies = [
[[package]]
name = "tungstenite"
version = "0.28.0"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442"
checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8"
dependencies = [
"bytes 1.11.1",
"data-encoding",
@@ -4574,7 +4574,6 @@ dependencies = [
"rand 0.9.2",
"sha1",
"thiserror 2.0.18",
"utf-8",
]
[[package]]
@@ -5445,6 +5444,12 @@ dependencies = [
"memchr",
]
[[package]]
name = "winnow"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8"
[[package]]
name = "winreg"
version = "0.10.1"

View File

@@ -24,9 +24,9 @@ serde_yaml = "0.9"
strip-ansi-escapes = "0.2"
tempfile = "3"
tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync"] }
toml = "1.0.6"
toml = "1.0.7"
uuid = { version = "1.22.0", features = ["v4", "serde"] }
tokio-tungstenite = "0.28.0"
tokio-tungstenite = "0.29.0"
walkdir = "2.5.0"
filetime = "0.2"
matrix-sdk = { version = "0.16.0", default-features = false, features = [

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Story Kit</title>
<title>Storkit</title>
</head>
<body>

View File

@@ -7,6 +7,7 @@ import "./App.css";
function App() {
const [projectPath, setProjectPath] = React.useState<string | null>(null);
const [_view, setView] = React.useState<"chat" | "token-usage">("chat");
const [isCheckingProject, setIsCheckingProject] = React.useState(true);
const [errorMsg, setErrorMsg] = React.useState<string | null>(null);
const [pathInput, setPathInput] = React.useState("");
@@ -120,6 +121,7 @@ function App() {
try {
await api.closeProject();
setProjectPath(null);
setView("chat");
} catch (e) {
console.error(e);
}

View File

@@ -128,8 +128,7 @@ export function subscribeAgentStream(
onEvent: (event: AgentEvent) => void,
onError?: (error: Event) => void,
): () => void {
const host = import.meta.env.DEV ? "http://127.0.0.1:3001" : "";
const url = `${host}/agents/${encodeURIComponent(storyId)}/${encodeURIComponent(agentName)}/stream`;
const url = `/agents/${encodeURIComponent(storyId)}/${encodeURIComponent(agentName)}/stream`;
const eventSource = new EventSource(url);

View File

@@ -262,7 +262,7 @@ describe("ChatWebSocket", () => {
// Server pushes pipeline_state on fresh connection
const freshState = {
upcoming: [{ story_id: "1_story_test", name: "Test", error: null }],
backlog: [{ story_id: "1_story_test", name: "Test", error: null }],
current: [],
qa: [],
merge: [],

View File

@@ -33,10 +33,12 @@ export interface PipelineStageItem {
error: string | null;
merge_failure: string | null;
agent: AgentAssignment | null;
review_hold: boolean | null;
qa: string | null;
}
export interface PipelineState {
upcoming: PipelineStageItem[];
backlog: PipelineStageItem[];
current: PipelineStageItem[];
qa: PipelineStageItem[];
merge: PipelineStageItem[];
@@ -50,7 +52,7 @@ export type WsResponse =
| { type: "error"; message: string }
| {
type: "pipeline_state";
upcoming: PipelineStageItem[];
backlog: PipelineStageItem[];
current: PipelineStageItem[];
qa: PipelineStageItem[];
merge: PipelineStageItem[];
@@ -83,7 +85,9 @@ export type WsResponse =
/** Streaming token from a /btw side question response. */
| { type: "side_question_token"; content: string }
/** Final signal that the /btw side question has been fully answered. */
| { type: "side_question_done"; response: string };
| { type: "side_question_done"; response: string }
/** A single server log entry (bulk on connect, then live). */
| { type: "log_entry"; timestamp: string; level: string; message: string };
export interface ProviderConfig {
provider: string;
@@ -115,6 +119,7 @@ export interface WorkItemContent {
content: string;
stage: string;
name: string | null;
agent: string | null;
}
export interface TestCaseResult {
@@ -138,6 +143,37 @@ export interface SearchResult {
matches: number;
}
export interface AgentCostEntry {
agent_name: string;
model: string | null;
input_tokens: number;
output_tokens: number;
cache_creation_input_tokens: number;
cache_read_input_tokens: number;
total_cost_usd: number;
}
export interface TokenCostResponse {
total_cost_usd: number;
agents: AgentCostEntry[];
}
export interface TokenUsageRecord {
story_id: string;
agent_name: string;
model: string | null;
timestamp: string;
input_tokens: number;
output_tokens: number;
cache_creation_input_tokens: number;
cache_read_input_tokens: number;
total_cost_usd: number;
}
export interface AllTokenUsageResponse {
records: TokenUsageRecord[];
}
export interface CommandOutput {
stdout: string;
stderr: string;
@@ -277,6 +313,9 @@ export const api = {
getHomeDirectory(baseUrl?: string) {
return requestJson<string>("/io/fs/home", {}, baseUrl);
},
listProjectFiles(baseUrl?: string) {
return requestJson<string[]>("/io/fs/files", {}, baseUrl);
},
searchFiles(query: string, baseUrl?: string) {
return requestJson<SearchResult[]>(
"/fs/search",
@@ -308,8 +347,52 @@ export const api = {
baseUrl,
);
},
getTokenCost(storyId: string, baseUrl?: string) {
return requestJson<TokenCostResponse>(
`/work-items/${encodeURIComponent(storyId)}/token-cost`,
{},
baseUrl,
);
},
getAllTokenUsage(baseUrl?: string) {
return requestJson<AllTokenUsageResponse>("/token-usage", {}, baseUrl);
},
/** Approve a story in QA, moving it to merge. */
approveQa(storyId: string) {
return callMcpTool("approve_qa", { story_id: storyId });
},
/** Reject a story in QA, moving it back to current with notes. */
rejectQa(storyId: string, notes: string) {
return callMcpTool("reject_qa", { story_id: storyId, notes });
},
/** Launch the QA app for a story's worktree. */
launchQaApp(storyId: string) {
return callMcpTool("launch_qa_app", { story_id: storyId });
},
};
async function callMcpTool(
toolName: string,
args: Record<string, unknown>,
): Promise<string> {
const res = await fetch("/mcp", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "tools/call",
params: { name: toolName, arguments: args },
}),
});
const json = await res.json();
if (json.error) {
throw new Error(json.error.message);
}
const text = json.result?.content?.[0]?.text ?? "";
return text;
}
export class ChatWebSocket {
private static sharedSocket: WebSocket | null = null;
private static refCount = 0;
@@ -336,6 +419,11 @@ export class ChatWebSocket {
private onOnboardingStatus?: (needsOnboarding: boolean) => void;
private onSideQuestionToken?: (content: string) => void;
private onSideQuestionDone?: (response: string) => void;
private onLogEntry?: (
timestamp: string,
level: string,
message: string,
) => void;
private connected = false;
private closeTimer?: number;
private wsPath = DEFAULT_WS_PATH;
@@ -394,7 +482,7 @@ export class ChatWebSocket {
if (data.type === "error") this.onError?.(data.message);
if (data.type === "pipeline_state")
this.onPipelineState?.({
upcoming: data.upcoming,
backlog: data.backlog,
current: data.current,
qa: data.qa,
merge: data.merge,
@@ -421,6 +509,8 @@ export class ChatWebSocket {
this.onSideQuestionToken?.(data.content);
if (data.type === "side_question_done")
this.onSideQuestionDone?.(data.response);
if (data.type === "log_entry")
this.onLogEntry?.(data.timestamp, data.level, data.message);
if (data.type === "pong") {
window.clearTimeout(this.heartbeatTimeout);
this.heartbeatTimeout = undefined;
@@ -476,6 +566,7 @@ export class ChatWebSocket {
onOnboardingStatus?: (needsOnboarding: boolean) => void;
onSideQuestionToken?: (content: string) => void;
onSideQuestionDone?: (response: string) => void;
onLogEntry?: (timestamp: string, level: string, message: string) => void;
},
wsPath = DEFAULT_WS_PATH,
) {
@@ -493,6 +584,7 @@ export class ChatWebSocket {
this.onOnboardingStatus = handlers.onOnboardingStatus;
this.onSideQuestionToken = handlers.onSideQuestionToken;
this.onSideQuestionDone = handlers.onSideQuestionDone;
this.onLogEntry = handlers.onLogEntry;
this.wsPath = wsPath;
this.shouldReconnect = true;

View File

@@ -213,7 +213,7 @@ describe("RosterBadge availability state", () => {
});
});
describe("Thinking traces hidden from agent stream UI", () => {
describe("Agent output not shown in sidebar (story 290)", () => {
beforeAll(() => {
Element.prototype.scrollIntoView = vi.fn();
});
@@ -224,7 +224,51 @@ describe("Thinking traces hidden from agent stream UI", () => {
mockedSubscribeAgentStream.mockReturnValue(() => {});
});
// AC1: thinking block is never rendered even when thinking events arrive
// AC1: output events do not appear in the agents sidebar
it("does not render agent output when output event arrives", async () => {
let emitEvent: ((e: AgentEvent) => void) | null = null;
mockedSubscribeAgentStream.mockImplementation(
(_storyId, _agentName, onEvent) => {
emitEvent = onEvent;
return () => {};
},
);
const agentList: AgentInfo[] = [
{
story_id: "290_output",
agent_name: "coder-1",
status: "running",
session_id: null,
worktree_path: "/tmp/wt",
base_branch: "master",
log_session_id: null,
},
];
mockedAgents.listAgents.mockResolvedValue(agentList);
const { container } = render(<AgentPanel />);
await screen.findByTestId("roster-badge-coder-1");
await act(async () => {
emitEvent?.({
type: "output",
story_id: "290_output",
agent_name: "coder-1",
text: "doing some work...",
});
});
// No output elements in the sidebar
expect(
container.querySelector('[data-testid^="agent-output-"]'),
).not.toBeInTheDocument();
expect(
container.querySelector('[data-testid^="agent-stream-"]'),
).not.toBeInTheDocument();
});
// AC1: thinking events do not appear in the agents sidebar
it("does not render thinking block when thinking event arrives", async () => {
let emitEvent: ((e: AgentEvent) => void) | null = null;
mockedSubscribeAgentStream.mockImplementation(
@@ -236,7 +280,7 @@ describe("Thinking traces hidden from agent stream UI", () => {
const agentList: AgentInfo[] = [
{
story_id: "218_thinking",
story_id: "290_thinking",
agent_name: "coder-1",
status: "running",
session_id: null,
@@ -253,109 +297,16 @@ describe("Thinking traces hidden from agent stream UI", () => {
await act(async () => {
emitEvent?.({
type: "thinking",
story_id: "218_thinking",
story_id: "290_thinking",
agent_name: "coder-1",
text: "Let me consider the problem carefully...",
});
});
// AC1: thinking block must not be present
// No thinking block or output in sidebar
expect(screen.queryByTestId("thinking-block")).not.toBeInTheDocument();
});
// AC2: after thinking events, only regular output is rendered
it("renders regular output but not thinking block when both arrive", async () => {
let emitEvent: ((e: AgentEvent) => void) | null = null;
mockedSubscribeAgentStream.mockImplementation(
(_storyId, _agentName, onEvent) => {
emitEvent = onEvent;
return () => {};
},
);
const agentList: AgentInfo[] = [
{
story_id: "218_output",
agent_name: "coder-1",
status: "running",
session_id: null,
worktree_path: "/tmp/wt",
base_branch: "master",
log_session_id: null,
},
];
mockedAgents.listAgents.mockResolvedValue(agentList);
render(<AgentPanel />);
await screen.findByTestId("roster-badge-coder-1");
// Thinking event — must be ignored visually
await act(async () => {
emitEvent?.({
type: "thinking",
story_id: "218_output",
agent_name: "coder-1",
text: "thinking deeply",
});
});
// AC3: output event still renders correctly (no regression)
await act(async () => {
emitEvent?.({
type: "output",
story_id: "218_output",
agent_name: "coder-1",
text: "Here is the result.",
});
});
// AC1: no thinking block
expect(screen.queryByTestId("thinking-block")).not.toBeInTheDocument();
// AC2+AC3: output area renders the text but NOT thinking text
const outputArea = screen.getByTestId("agent-output-coder-1");
expect(outputArea).toBeInTheDocument();
expect(outputArea.textContent).toContain("Here is the result.");
expect(outputArea.textContent).not.toContain("thinking deeply");
});
// AC3: output-only event stream (no thinking) still works
it("renders output event text without a thinking block", async () => {
let emitEvent: ((e: AgentEvent) => void) | null = null;
mockedSubscribeAgentStream.mockImplementation(
(_storyId, _agentName, onEvent) => {
emitEvent = onEvent;
return () => {};
},
);
const agentList: AgentInfo[] = [
{
story_id: "218_noThink",
agent_name: "coder-1",
status: "running",
session_id: null,
worktree_path: "/tmp/wt",
base_branch: "master",
log_session_id: null,
},
];
mockedAgents.listAgents.mockResolvedValue(agentList);
render(<AgentPanel />);
await screen.findByTestId("roster-badge-coder-1");
await act(async () => {
emitEvent?.({
type: "output",
story_id: "218_noThink",
agent_name: "coder-1",
text: "plain output line",
});
});
expect(screen.queryByTestId("thinking-block")).not.toBeInTheDocument();
const outputArea = screen.getByTestId("agent-output-coder-1");
expect(outputArea.textContent).toContain("plain output line");
expect(
screen.queryByText("Let me consider the problem carefully..."),
).not.toBeInTheDocument();
});
});

View File

@@ -13,7 +13,6 @@ const { useCallback, useEffect, useRef, useState } = React;
interface AgentState {
agentName: string;
status: AgentStatusValue;
log: string[];
sessionId: string | null;
worktreePath: string | null;
baseBranch: string | null;
@@ -120,7 +119,6 @@ export function AgentPanel({
const current = prev[key] ?? {
agentName,
status: "pending" as AgentStatusValue,
log: [],
sessionId: null,
worktreePath: null,
baseBranch: null,
@@ -144,14 +142,6 @@ export function AgentPanel({
},
};
}
case "output":
return {
...prev,
[key]: {
...current,
log: [...current.log, event.text ?? ""],
},
};
case "done":
return {
...prev,
@@ -168,17 +158,12 @@ export function AgentPanel({
[key]: {
...current,
status: "failed",
log: [
...current.log,
`[ERROR] ${event.message ?? "Unknown error"}`,
],
terminalAt: current.terminalAt ?? Date.now(),
},
};
case "thinking":
// Thinking traces are internal model state — never display them.
return prev;
default:
// output, thinking, and other events are not displayed in the sidebar.
// Agent output streams appear in the work item detail panel instead.
return prev;
}
});
@@ -204,7 +189,6 @@ export function AgentPanel({
agentMap[key] = {
agentName: a.agent_name,
status: a.status,
log: [],
sessionId: a.session_id,
worktreePath: a.worktree_path,
baseBranch: a.base_branch,
@@ -261,9 +245,6 @@ export function AgentPanel({
}
};
// Agents that have streaming content to show
const activeAgents = Object.values(agents).filter((a) => a.log.length > 0);
return (
<div
style={{
@@ -420,35 +401,6 @@ export function AgentPanel({
</div>
)}
{/* Per-agent streaming output */}
{activeAgents.map((agent) => (
<div
key={`stream-${agent.agentName}`}
data-testid={`agent-stream-${agent.agentName}`}
style={{
display: "flex",
flexDirection: "column",
gap: "4px",
}}
>
{agent.log.length > 0 && (
<div
data-testid={`agent-output-${agent.agentName}`}
style={{
fontSize: "0.8em",
fontFamily: "monospace",
color: "#ccc",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
lineHeight: "1.5",
}}
>
{agent.log.join("")}
</div>
)}
</div>
))}
{actionError && (
<div
style={{

View File

@@ -26,6 +26,8 @@ type WsHandlers = {
) => void;
};
let capturedWsHandlers: WsHandlers | null = null;
// Captures the last sendChat call's arguments for assertion.
let lastSendChatArgs: { messages: Message[]; config: unknown } | null = null;
vi.mock("../api/client", () => {
const api = {
@@ -36,13 +38,17 @@ vi.mock("../api/client", () => {
setModelPreference: vi.fn(),
cancelChat: vi.fn(),
setAnthropicApiKey: vi.fn(),
readFile: vi.fn(),
listProjectFiles: vi.fn(),
};
class ChatWebSocket {
connect(handlers: WsHandlers) {
capturedWsHandlers = handlers;
}
close() {}
sendChat() {}
sendChat(messages: Message[], config: unknown) {
lastSendChatArgs = { messages, config };
}
cancel() {}
}
return { api, ChatWebSocket };
@@ -56,6 +62,8 @@ const mockedApi = {
setModelPreference: vi.mocked(api.setModelPreference),
cancelChat: vi.mocked(api.cancelChat),
setAnthropicApiKey: vi.mocked(api.setAnthropicApiKey),
readFile: vi.mocked(api.readFile),
listProjectFiles: vi.mocked(api.listProjectFiles),
};
function setupMocks() {
@@ -64,6 +72,8 @@ function setupMocks() {
mockedApi.getAnthropicModels.mockResolvedValue([]);
mockedApi.getModelPreference.mockResolvedValue(null);
mockedApi.setModelPreference.mockResolvedValue(true);
mockedApi.readFile.mockResolvedValue("");
mockedApi.listProjectFiles.mockResolvedValue([]);
mockedApi.cancelChat.mockResolvedValue(true);
mockedApi.setAnthropicApiKey.mockResolvedValue(true);
}
@@ -580,6 +590,66 @@ describe("Chat localStorage persistence (Story 145)", () => {
expect(storedAfterRemount).toEqual(history);
});
it("Bug 245: after refresh, sendChat includes full prior history", async () => {
// Step 1: Render, populate messages via onUpdate, then unmount (simulate refresh)
const { unmount } = render(
<Chat projectPath={PROJECT_PATH} onCloseProject={vi.fn()} />,
);
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
const priorHistory: Message[] = [
{ role: "user", content: "What is Rust?" },
{ role: "assistant", content: "Rust is a systems programming language." },
];
act(() => {
capturedWsHandlers?.onUpdate(priorHistory);
});
// Verify localStorage has the prior history
const stored = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "[]");
expect(stored).toEqual(priorHistory);
unmount();
// Step 2: Remount (simulates page reload) — messages load from localStorage
capturedWsHandlers = null;
lastSendChatArgs = null;
render(<Chat projectPath={PROJECT_PATH} onCloseProject={vi.fn()} />);
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
// Verify prior messages are displayed
expect(await screen.findByText("What is Rust?")).toBeInTheDocument();
// Step 3: Send a new message — sendChat should include the full prior history
const input = screen.getByPlaceholderText("Send a message...");
await act(async () => {
fireEvent.change(input, { target: { value: "Tell me more" } });
});
await act(async () => {
fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
});
// Verify sendChat was called with ALL prior messages + the new one
expect(lastSendChatArgs).not.toBeNull();
const args = lastSendChatArgs as unknown as {
messages: Message[];
config: unknown;
};
expect(args.messages).toHaveLength(3);
expect(args.messages[0]).toEqual({
role: "user",
content: "What is Rust?",
});
expect(args.messages[1]).toEqual({
role: "assistant",
content: "Rust is a systems programming language.",
});
expect(args.messages[2]).toEqual({
role: "user",
content: "Tell me more",
});
});
it("AC5: uses project-scoped storage key", async () => {
const otherKey = "storykit-chat-history:/other/project";
localStorage.setItem(
@@ -1215,3 +1285,175 @@ describe("Remove bubble styling from streaming messages (Story 163)", () => {
expect(styleAttr).not.toContain("background: transparent");
});
});
describe("Bug 264: Claude Code session ID persisted across browser refresh", () => {
const PROJECT_PATH = "/tmp/project";
const SESSION_KEY = `storykit-claude-session-id:${PROJECT_PATH}`;
const STORAGE_KEY = `storykit-chat-history:${PROJECT_PATH}`;
beforeEach(() => {
capturedWsHandlers = null;
lastSendChatArgs = null;
localStorage.clear();
setupMocks();
});
afterEach(() => {
localStorage.clear();
});
it("AC1: session_id is persisted to localStorage when onSessionId fires", async () => {
render(<Chat projectPath={PROJECT_PATH} onCloseProject={vi.fn()} />);
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
act(() => {
capturedWsHandlers?.onSessionId("test-session-abc");
});
await waitFor(() => {
expect(localStorage.getItem(SESSION_KEY)).toBe("test-session-abc");
});
});
it("AC2: after remount, next sendChat includes session_id from localStorage", async () => {
// Step 1: Render, receive a session ID, then unmount (simulate refresh)
localStorage.setItem(SESSION_KEY, "persisted-session-xyz");
localStorage.setItem(
STORAGE_KEY,
JSON.stringify([
{ role: "user", content: "Prior message" },
{ role: "assistant", content: "Prior reply" },
]),
);
const { unmount } = render(
<Chat projectPath={PROJECT_PATH} onCloseProject={vi.fn()} />,
);
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
unmount();
// Step 2: Remount (simulates page reload)
capturedWsHandlers = null;
lastSendChatArgs = null;
render(<Chat projectPath={PROJECT_PATH} onCloseProject={vi.fn()} />);
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
// Prior messages should be visible
expect(await screen.findByText("Prior message")).toBeInTheDocument();
// Step 3: Send a new message — config should include session_id
const input = screen.getByPlaceholderText("Send a message...");
await act(async () => {
fireEvent.change(input, { target: { value: "Continue" } });
});
await act(async () => {
fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
});
expect(lastSendChatArgs).not.toBeNull();
expect(
(
(
lastSendChatArgs as unknown as {
messages: Message[];
config: unknown;
}
)?.config as Record<string, unknown>
).session_id,
).toBe("persisted-session-xyz");
});
it("AC3: clearing the session also clears the persisted session_id", async () => {
localStorage.setItem(SESSION_KEY, "session-to-clear");
const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(true);
render(<Chat projectPath={PROJECT_PATH} onCloseProject={vi.fn()} />);
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
const newSessionBtn = screen.getByText(/New Session/);
await act(async () => {
fireEvent.click(newSessionBtn);
});
expect(localStorage.getItem(SESSION_KEY)).toBeNull();
confirmSpy.mockRestore();
});
it("AC1: storage key is scoped to project path", async () => {
const otherPath = "/other/project";
const otherKey = `storykit-claude-session-id:${otherPath}`;
localStorage.setItem(otherKey, "other-session");
render(<Chat projectPath={PROJECT_PATH} onCloseProject={vi.fn()} />);
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
act(() => {
capturedWsHandlers?.onSessionId("my-session");
});
await waitFor(() => {
expect(localStorage.getItem(SESSION_KEY)).toBe("my-session");
});
// Other project's session should be untouched
expect(localStorage.getItem(otherKey)).toBe("other-session");
});
});
describe("File reference expansion (Story 269 AC4)", () => {
beforeEach(() => {
vi.clearAllMocks();
capturedWsHandlers = null;
lastSendChatArgs = null;
setupMocks();
});
it("includes file contents as context when message contains @file reference", async () => {
mockedApi.readFile.mockResolvedValue('fn main() { println!("hello"); }');
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
const input = screen.getByPlaceholderText("Send a message...");
await act(async () => {
fireEvent.change(input, { target: { value: "explain @src/main.rs" } });
});
await act(async () => {
fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
});
await waitFor(() => expect(lastSendChatArgs).not.toBeNull());
const sentMessages = (
lastSendChatArgs as NonNullable<typeof lastSendChatArgs>
).messages;
const userMsg = sentMessages[sentMessages.length - 1];
expect(userMsg.content).toContain("explain @src/main.rs");
expect(userMsg.content).toContain("[File: src/main.rs]");
expect(userMsg.content).toContain("fn main()");
});
it("sends message without modification when no @file references are present", async () => {
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
const input = screen.getByPlaceholderText("Send a message...");
await act(async () => {
fireEvent.change(input, { target: { value: "hello world" } });
});
await act(async () => {
fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
});
await waitFor(() => expect(lastSendChatArgs).not.toBeNull());
const sentMessages = (
lastSendChatArgs as NonNullable<typeof lastSendChatArgs>
).messages;
const userMsg = sentMessages[sentMessages.length - 1];
expect(userMsg.content).toBe("hello world");
expect(mockedApi.readFile).not.toHaveBeenCalled();
});
});

View File

@@ -13,6 +13,8 @@ import { ChatInput } from "./ChatInput";
import { HelpOverlay } from "./HelpOverlay";
import { LozengeFlyProvider } from "./LozengeFlyContext";
import { MessageItem } from "./MessageItem";
import type { LogEntry } from "./ServerLogsPanel";
import { ServerLogsPanel } from "./ServerLogsPanel";
import { SideQuestionOverlay } from "./SideQuestionOverlay";
import { StagePanel } from "./StagePanel";
import { WorkItemDetailPanel } from "./WorkItemDetailPanel";
@@ -165,13 +167,22 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
const [apiKeyInput, setApiKeyInput] = useState("");
const [hasAnthropicKey, setHasAnthropicKey] = useState(false);
const [pipeline, setPipeline] = useState<PipelineState>({
upcoming: [],
backlog: [],
current: [],
qa: [],
merge: [],
done: [],
});
const [claudeSessionId, setClaudeSessionId] = useState<string | null>(null);
const [claudeSessionId, setClaudeSessionId] = useState<string | null>(() => {
try {
return (
localStorage.getItem(`storykit-claude-session-id:${projectPath}`) ??
null
);
} catch {
return null;
}
});
const [activityStatus, setActivityStatus] = useState<string | null>(null);
const [permissionQueue, setPermissionQueue] = useState<
{
@@ -191,6 +202,9 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
const [agentConfigVersion, setAgentConfigVersion] = useState(0);
const [agentStateVersion, setAgentStateVersion] = useState(0);
const [pipelineVersion, setPipelineVersion] = useState(0);
const [storyTokenCosts, setStoryTokenCosts] = useState<Map<string, number>>(
new Map(),
);
const [needsOnboarding, setNeedsOnboarding] = useState(false);
const onboardingTriggeredRef = useRef(false);
const [selectedWorkItemId, setSelectedWorkItemId] = useState<string | null>(
@@ -205,6 +219,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
loading: boolean;
} | null>(null);
const [showHelp, setShowHelp] = useState(false);
const [serverLogs, setServerLogs] = useState<LogEntry[]>([]);
// Ref so stale WebSocket callbacks can read the current queued messages
const queuedMessagesRef = useRef<{ id: string; text: string }[]>([]);
const queueIdCounterRef = useRef(0);
@@ -247,6 +262,21 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
};
}, [messages, streamingContent, model]);
useEffect(() => {
try {
if (claudeSessionId !== null) {
localStorage.setItem(
`storykit-claude-session-id:${projectPath}`,
claudeSessionId,
);
} else {
localStorage.removeItem(`storykit-claude-session-id:${projectPath}`);
}
} catch {
// Ignore — quota or security errors.
}
}, [claudeSessionId, projectPath]);
useEffect(() => {
api
.getOllamaModels()
@@ -336,6 +366,29 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
onPipelineState: (state) => {
setPipeline(state);
setPipelineVersion((v) => v + 1);
const allItems = [
...state.backlog,
...state.current,
...state.qa,
...state.merge,
...state.done,
];
for (const item of allItems) {
api
.getTokenCost(item.story_id)
.then((cost) => {
if (cost.total_cost_usd > 0) {
setStoryTokenCosts((prev) => {
const next = new Map(prev);
next.set(item.story_id, cost.total_cost_usd);
return next;
});
}
})
.catch(() => {
// Silently ignore — cost data may not exist yet.
});
}
},
onPermissionRequest: (requestId, toolName, toolInput) => {
setPermissionQueue((prev) => [
@@ -378,6 +431,9 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
prev ? { ...prev, response, loading: false } : prev,
);
},
onLogEntry: (timestamp, level, message) => {
setServerLogs((prev) => [...prev, { timestamp, level, message }]);
},
});
return () => {
@@ -530,7 +586,26 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
}
}
const userMsg: Message = { role: "user", content: messageText };
// Expand @file references: append file contents as context
const fileRefs = [...messageText.matchAll(/(^|[\s\n])@([^\s@]+)/g)].map(
(m) => m[2],
);
let expandedText = messageText;
if (fileRefs.length > 0) {
const expansions = await Promise.allSettled(
fileRefs.map(async (ref) => {
const contents = await api.readFile(ref);
return { ref, contents };
}),
);
for (const result of expansions) {
if (result.status === "fulfilled") {
expandedText += `\n\n[File: ${result.value.ref}]\n\`\`\`\n${result.value.contents}\n\`\`\``;
}
}
}
const userMsg: Message = { role: "user", content: expandedText };
const newHistory = [...messages, userMsg];
setMessages(newHistory);
@@ -664,6 +739,11 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
setLoading(false);
setActivityStatus(null);
setClaudeSessionId(null);
try {
localStorage.removeItem(`storykit-claude-session-id:${projectPath}`);
} catch {
// Ignore — quota or security errors.
}
}
};
@@ -765,7 +845,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
fontSize: "1.1rem",
}}
>
Welcome to Story Kit
Welcome to Storkit
</h3>
<p
style={{
@@ -951,28 +1031,34 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
<StagePanel
title="Done"
items={pipeline.done ?? []}
costs={storyTokenCosts}
onItemClick={(item) => setSelectedWorkItemId(item.story_id)}
/>
<StagePanel
title="To Merge"
items={pipeline.merge}
costs={storyTokenCosts}
onItemClick={(item) => setSelectedWorkItemId(item.story_id)}
/>
<StagePanel
title="QA"
items={pipeline.qa}
costs={storyTokenCosts}
onItemClick={(item) => setSelectedWorkItemId(item.story_id)}
/>
<StagePanel
title="Current"
items={pipeline.current}
costs={storyTokenCosts}
onItemClick={(item) => setSelectedWorkItemId(item.story_id)}
/>
<StagePanel
title="Upcoming"
items={pipeline.upcoming}
title="Backlog"
items={pipeline.backlog}
costs={storyTokenCosts}
onItemClick={(item) => setSelectedWorkItemId(item.story_id)}
/>
<ServerLogsPanel logs={serverLogs} />
</>
)}
</LozengeFlyProvider>

View File

@@ -136,9 +136,9 @@ describe("ChatHeader", () => {
expect(screen.getByText("Built: 2026-01-01 00:00")).toBeInTheDocument();
});
it("displays StorkIt branding in the header", () => {
it("displays Storkit branding in the header", () => {
render(<ChatHeader {...makeProps()} />);
expect(screen.getByText("StorkIt")).toBeInTheDocument();
expect(screen.getByText("Storkit")).toBeInTheDocument();
});
it("labels the claude-pty optgroup as 'Claude Code'", () => {

View File

@@ -82,7 +82,7 @@ export function ChatHeader({
letterSpacing: "0.02em",
}}
>
StorkIt
Storkit
</span>
<div
title={projectPath}

View File

@@ -1,6 +1,14 @@
import * as React from "react";
import { api } from "../api/client";
const { forwardRef, useEffect, useImperativeHandle, useRef, useState } = React;
const {
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useRef,
useState,
} = React;
export interface ChatInputHandle {
appendToInput(text: string): void;
@@ -14,6 +22,97 @@ interface ChatInputProps {
onRemoveQueuedMessage: (id: string) => void;
}
/** Fuzzy-match: returns true if all chars of `query` appear in order in `str`. */
function fuzzyMatch(str: string, query: string): boolean {
if (!query) return true;
const lower = str.toLowerCase();
const q = query.toLowerCase();
let qi = 0;
for (let i = 0; i < lower.length && qi < q.length; i++) {
if (lower[i] === q[qi]) qi++;
}
return qi === q.length;
}
/** Score a fuzzy match: lower is better. Exact prefix match wins, then shorter paths. */
function fuzzyScore(str: string, query: string): number {
const lower = str.toLowerCase();
const q = query.toLowerCase();
// Prefer matches where query appears as a contiguous substring
if (lower.includes(q)) return lower.indexOf(q);
return str.length;
}
interface FilePickerOverlayProps {
query: string;
files: string[];
selectedIndex: number;
onSelect: (file: string) => void;
onDismiss: () => void;
anchorRef: React.RefObject<HTMLTextAreaElement | null>;
}
function FilePickerOverlay({
query,
files,
selectedIndex,
onSelect,
}: FilePickerOverlayProps) {
const filtered = files
.filter((f) => fuzzyMatch(f, query))
.sort((a, b) => fuzzyScore(a, query) - fuzzyScore(b, query))
.slice(0, 10);
if (filtered.length === 0) return null;
return (
<div
data-testid="file-picker-overlay"
style={{
position: "absolute",
bottom: "100%",
left: 0,
right: 0,
background: "#1e1e1e",
border: "1px solid #444",
borderRadius: "8px",
marginBottom: "6px",
overflow: "hidden",
zIndex: 100,
boxShadow: "0 4px 16px rgba(0,0,0,0.4)",
maxHeight: "240px",
overflowY: "auto",
}}
>
{filtered.map((file, idx) => (
<button
key={file}
type="button"
data-testid={`file-picker-item-${idx}`}
onClick={() => onSelect(file)}
style={{
display: "block",
width: "100%",
textAlign: "left",
padding: "8px 14px",
background: idx === selectedIndex ? "#2d4a6e" : "transparent",
border: "none",
color: idx === selectedIndex ? "#ececec" : "#aaa",
cursor: "pointer",
fontFamily: "monospace",
fontSize: "0.85rem",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{file}
</button>
))}
</div>
);
}
export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
function ChatInput(
{ loading, queuedMessages, onSubmit, onCancel, onRemoveQueuedMessage },
@@ -22,6 +121,12 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
const [input, setInput] = useState("");
const inputRef = useRef<HTMLTextAreaElement>(null);
// File picker state
const [projectFiles, setProjectFiles] = useState<string[]>([]);
const [pickerQuery, setPickerQuery] = useState<string | null>(null);
const [pickerSelectedIndex, setPickerSelectedIndex] = useState(0);
const [pickerAtStart, setPickerAtStart] = useState(0);
useImperativeHandle(ref, () => ({
appendToInput(text: string) {
setInput((prev) => (prev ? `${prev}\n${text}` : text));
@@ -32,10 +137,118 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
inputRef.current?.focus();
}, []);
// Compute filtered files for current picker query
const filteredFiles =
pickerQuery !== null
? projectFiles
.filter((f) => fuzzyMatch(f, pickerQuery))
.sort(
(a, b) => fuzzyScore(a, pickerQuery) - fuzzyScore(b, pickerQuery),
)
.slice(0, 10)
: [];
const dismissPicker = useCallback(() => {
setPickerQuery(null);
setPickerSelectedIndex(0);
}, []);
const selectFile = useCallback(
(file: string) => {
// Replace the @query portion with @file
const before = input.slice(0, pickerAtStart);
const cursorPos = inputRef.current?.selectionStart ?? input.length;
const after = input.slice(cursorPos);
setInput(`${before}@${file}${after}`);
dismissPicker();
// Restore focus after state update
setTimeout(() => inputRef.current?.focus(), 0);
},
[input, pickerAtStart, dismissPicker],
);
const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
const val = e.target.value;
setInput(val);
const cursor = e.target.selectionStart ?? val.length;
// Find the last @ before the cursor that starts a reference token
const textUpToCursor = val.slice(0, cursor);
// Match @ not preceded by non-whitespace (i.e. @ at start or after space/newline)
const atMatch = textUpToCursor.match(/(^|[\s\n])@([^\s@]*)$/);
if (atMatch) {
const query = atMatch[2];
const atPos = textUpToCursor.lastIndexOf("@");
setPickerAtStart(atPos);
setPickerQuery(query);
setPickerSelectedIndex(0);
// Lazily load files on first trigger
if (projectFiles.length === 0) {
api
.listProjectFiles()
.then(setProjectFiles)
.catch(() => {});
}
} else {
if (pickerQuery !== null) dismissPicker();
}
},
[projectFiles.length, pickerQuery, dismissPicker],
);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (pickerQuery !== null && filteredFiles.length > 0) {
if (e.key === "ArrowDown") {
e.preventDefault();
setPickerSelectedIndex((i) =>
Math.min(i + 1, filteredFiles.length - 1),
);
return;
}
if (e.key === "ArrowUp") {
e.preventDefault();
setPickerSelectedIndex((i) => Math.max(i - 1, 0));
return;
}
if (e.key === "Enter" || e.key === "Tab") {
e.preventDefault();
selectFile(filteredFiles[pickerSelectedIndex] ?? filteredFiles[0]);
return;
}
if (e.key === "Escape") {
e.preventDefault();
dismissPicker();
return;
}
} else if (e.key === "Escape" && pickerQuery !== null) {
e.preventDefault();
dismissPicker();
return;
}
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSubmit();
}
},
[
pickerQuery,
filteredFiles,
pickerSelectedIndex,
selectFile,
dismissPicker,
],
);
const handleSubmit = () => {
if (!input.trim()) return;
onSubmit(input);
setInput("");
dismissPicker();
};
return (
@@ -135,24 +348,30 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
</button>
</div>
))}
{/* Input row */}
{/* Input row with file picker overlay */}
<div
style={{
display: "flex",
gap: "8px",
alignItems: "center",
position: "relative",
}}
>
{pickerQuery !== null && (
<FilePickerOverlay
query={pickerQuery}
files={projectFiles}
selectedIndex={pickerSelectedIndex}
onSelect={selectFile}
onDismiss={dismissPicker}
anchorRef={inputRef}
/>
)}
<textarea
ref={inputRef}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSubmit();
}
}}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
placeholder="Send a message..."
rows={1}
style={{

View File

@@ -0,0 +1,194 @@
import {
act,
fireEvent,
render,
screen,
waitFor,
} from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { api } from "../api/client";
import { ChatInput } from "./ChatInput";
vi.mock("../api/client", () => ({
api: {
listProjectFiles: vi.fn(),
},
}));
const mockedListProjectFiles = vi.mocked(api.listProjectFiles);
const defaultProps = {
loading: false,
queuedMessages: [],
onSubmit: vi.fn(),
onCancel: vi.fn(),
onRemoveQueuedMessage: vi.fn(),
};
beforeEach(() => {
vi.clearAllMocks();
mockedListProjectFiles.mockResolvedValue([
"src/main.rs",
"src/lib.rs",
"frontend/index.html",
"README.md",
]);
});
describe("File picker overlay (Story 269 AC1)", () => {
it("shows file picker overlay when @ is typed", async () => {
render(<ChatInput {...defaultProps} />);
const textarea = screen.getByPlaceholderText("Send a message...");
await act(async () => {
fireEvent.change(textarea, { target: { value: "@" } });
});
await waitFor(() => {
expect(screen.getByTestId("file-picker-overlay")).toBeInTheDocument();
});
});
it("does not show file picker overlay for text without @", async () => {
render(<ChatInput {...defaultProps} />);
const textarea = screen.getByPlaceholderText("Send a message...");
await act(async () => {
fireEvent.change(textarea, { target: { value: "hello world" } });
});
expect(screen.queryByTestId("file-picker-overlay")).not.toBeInTheDocument();
});
});
describe("File picker fuzzy matching (Story 269 AC2)", () => {
it("filters files by query typed after @", async () => {
render(<ChatInput {...defaultProps} />);
const textarea = screen.getByPlaceholderText("Send a message...");
await act(async () => {
fireEvent.change(textarea, { target: { value: "@main" } });
});
await waitFor(() => {
expect(screen.getByTestId("file-picker-overlay")).toBeInTheDocument();
});
// main.rs should be visible, README.md should not
expect(screen.getByText("src/main.rs")).toBeInTheDocument();
expect(screen.queryByText("README.md")).not.toBeInTheDocument();
});
it("shows all files when @ is typed with no query", async () => {
render(<ChatInput {...defaultProps} />);
const textarea = screen.getByPlaceholderText("Send a message...");
await act(async () => {
fireEvent.change(textarea, { target: { value: "@" } });
});
await waitFor(() => {
expect(screen.getByTestId("file-picker-overlay")).toBeInTheDocument();
});
// All 4 files should be visible
expect(screen.getByText("src/main.rs")).toBeInTheDocument();
expect(screen.getByText("src/lib.rs")).toBeInTheDocument();
expect(screen.getByText("README.md")).toBeInTheDocument();
});
});
describe("File picker selection (Story 269 AC3)", () => {
it("clicking a file inserts @path into the message", async () => {
render(<ChatInput {...defaultProps} />);
const textarea = screen.getByPlaceholderText("Send a message...");
await act(async () => {
fireEvent.change(textarea, { target: { value: "@" } });
});
await waitFor(() => {
expect(screen.getByTestId("file-picker-item-0")).toBeInTheDocument();
});
await act(async () => {
fireEvent.click(screen.getByTestId("file-picker-item-0"));
});
// Picker should be dismissed and the file reference inserted
expect(screen.queryByTestId("file-picker-overlay")).not.toBeInTheDocument();
expect((textarea as HTMLTextAreaElement).value).toMatch(/^@\S+/);
});
it("Enter key selects highlighted file and inserts it into message", async () => {
render(<ChatInput {...defaultProps} />);
const textarea = screen.getByPlaceholderText("Send a message...");
await act(async () => {
fireEvent.change(textarea, { target: { value: "@main" } });
});
await waitFor(() => {
expect(screen.getByTestId("file-picker-overlay")).toBeInTheDocument();
});
await act(async () => {
fireEvent.keyDown(textarea, { key: "Enter" });
});
expect(screen.queryByTestId("file-picker-overlay")).not.toBeInTheDocument();
expect((textarea as HTMLTextAreaElement).value).toContain("@src/main.rs");
});
});
describe("File picker dismiss (Story 269 AC5)", () => {
it("Escape key dismisses the file picker", async () => {
render(<ChatInput {...defaultProps} />);
const textarea = screen.getByPlaceholderText("Send a message...");
await act(async () => {
fireEvent.change(textarea, { target: { value: "@" } });
});
await waitFor(() => {
expect(screen.getByTestId("file-picker-overlay")).toBeInTheDocument();
});
await act(async () => {
fireEvent.keyDown(textarea, { key: "Escape" });
});
expect(screen.queryByTestId("file-picker-overlay")).not.toBeInTheDocument();
});
});
describe("Multiple @ references (Story 269 AC6)", () => {
it("typing @ after a completed reference triggers picker again", async () => {
render(<ChatInput {...defaultProps} />);
const textarea = screen.getByPlaceholderText("Send a message...");
// First reference
await act(async () => {
fireEvent.change(textarea, { target: { value: "@main" } });
});
await waitFor(() => {
expect(screen.getByTestId("file-picker-overlay")).toBeInTheDocument();
});
// Select file
await act(async () => {
fireEvent.keyDown(textarea, { key: "Enter" });
});
// Type a second @
await act(async () => {
const current = (textarea as HTMLTextAreaElement).value;
fireEvent.change(textarea, { target: { value: `${current} @` } });
});
await waitFor(() => {
expect(screen.getByTestId("file-picker-overlay")).toBeInTheDocument();
});
});
});

View File

@@ -9,7 +9,7 @@ import { StagePanel } from "./StagePanel";
function makePipeline(overrides: Partial<PipelineState> = {}): PipelineState {
return {
upcoming: [],
backlog: [],
current: [],
qa: [],
merge: [],
@@ -59,6 +59,8 @@ describe("AgentLozenge fixed intrinsic width", () => {
error: null,
merge_failure: null,
agent: { agent_name: "coder-1", model: "sonnet", status: "running" },
review_hold: null,
qa: null,
},
];
const pipeline = makePipeline({ current: items });
@@ -111,6 +113,8 @@ describe("LozengeFlyProvider fly-in visibility", () => {
error: null,
merge_failure: null,
agent: { agent_name: "coder-1", model: null, status: "running" },
review_hold: null,
qa: null,
},
],
});
@@ -151,6 +155,8 @@ describe("LozengeFlyProvider fly-in visibility", () => {
model: null,
status: "running",
},
review_hold: null,
qa: null,
},
],
});
@@ -213,6 +219,8 @@ describe("LozengeFlyProvider fly-in clone", () => {
error: null,
merge_failure: null,
agent: { agent_name: "coder-1", model: "sonnet", status: "running" },
review_hold: null,
qa: null,
},
],
});
@@ -254,6 +262,8 @@ describe("LozengeFlyProvider fly-in clone", () => {
error: null,
merge_failure: null,
agent: { agent_name: "coder-1", model: null, status: "running" },
review_hold: null,
qa: null,
},
],
});
@@ -301,6 +311,8 @@ describe("LozengeFlyProvider fly-in clone", () => {
error: null,
merge_failure: null,
agent: { agent_name: "coder-1", model: null, status: "running" },
review_hold: null,
qa: null,
},
],
});
@@ -370,6 +382,8 @@ describe("LozengeFlyProvider fly-out", () => {
error: null,
merge_failure: null,
agent: { agent_name: "coder-1", model: "haiku", status: "completed" },
review_hold: null,
qa: null,
},
],
});
@@ -395,6 +409,8 @@ describe("LozengeFlyProvider fly-out", () => {
error: null,
merge_failure: null,
agent: null,
review_hold: null,
qa: null,
},
],
});
@@ -427,6 +443,8 @@ describe("AgentLozenge idle vs active appearance", () => {
error: null,
merge_failure: null,
agent: { agent_name: "coder-1", model: null, status: "running" },
review_hold: null,
qa: null,
},
];
const { container } = render(
@@ -451,6 +469,8 @@ describe("AgentLozenge idle vs active appearance", () => {
error: null,
merge_failure: null,
agent: { agent_name: "coder-1", model: null, status: "pending" },
review_hold: null,
qa: null,
},
];
const { container } = render(
@@ -475,6 +495,8 @@ describe("AgentLozenge idle vs active appearance", () => {
error: null,
merge_failure: null,
agent: { agent_name: "coder-1", model: null, status: "running" },
review_hold: null,
qa: null,
},
];
const { container } = render(
@@ -526,6 +548,8 @@ describe("hiddenRosterAgents: assigned agents are absent from roster", () => {
error: null,
merge_failure: null,
agent: { agent_name: "coder-1", model: null, status: "running" },
review_hold: null,
qa: null,
},
],
});
@@ -547,6 +571,8 @@ describe("hiddenRosterAgents: assigned agents are absent from roster", () => {
error: null,
merge_failure: null,
agent: null,
review_hold: null,
qa: null,
},
],
});
@@ -569,6 +595,8 @@ describe("hiddenRosterAgents: assigned agents are absent from roster", () => {
error: null,
merge_failure: null,
agent: { agent_name: "coder-1", model: null, status: "running" },
review_hold: null,
qa: null,
},
],
});
@@ -629,6 +657,8 @@ describe("hiddenRosterAgents: fly-out keeps agent hidden until clone lands", ()
error: null,
merge_failure: null,
agent: { agent_name: "coder-1", model: null, status: "completed" },
review_hold: null,
qa: null,
},
],
});
@@ -640,6 +670,8 @@ describe("hiddenRosterAgents: fly-out keeps agent hidden until clone lands", ()
error: null,
merge_failure: null,
agent: null,
review_hold: null,
qa: null,
},
],
});
@@ -682,6 +714,8 @@ describe("hiddenRosterAgents: fly-out keeps agent hidden until clone lands", ()
error: null,
merge_failure: null,
agent: { agent_name: "coder-1", model: null, status: "completed" },
review_hold: null,
qa: null,
},
],
});
@@ -693,6 +727,8 @@ describe("hiddenRosterAgents: fly-out keeps agent hidden until clone lands", ()
error: null,
merge_failure: null,
agent: null,
review_hold: null,
qa: null,
},
],
});
@@ -766,6 +802,8 @@ describe("LozengeFlyProvider agent swap (name change)", () => {
error: null,
merge_failure: null,
agent: { agent_name: "coder-1", model: "sonnet", status: "running" },
review_hold: null,
qa: null,
},
],
});
@@ -777,6 +815,8 @@ describe("LozengeFlyProvider agent swap (name change)", () => {
error: null,
merge_failure: null,
agent: { agent_name: "coder-2", model: "haiku", status: "running" },
review_hold: null,
qa: null,
},
],
});
@@ -861,6 +901,8 @@ describe("LozengeFlyProvider fly-out without roster element", () => {
model: null,
status: "completed",
},
review_hold: null,
qa: null,
},
],
});
@@ -872,6 +914,8 @@ describe("LozengeFlyProvider fly-out without roster element", () => {
error: null,
merge_failure: null,
agent: null,
review_hold: null,
qa: null,
},
],
});
@@ -943,6 +987,8 @@ describe("FlyingLozengeClone initial non-flying render", () => {
error: null,
merge_failure: null,
agent: { agent_name: "coder-1", model: null, status: "running" },
review_hold: null,
qa: null,
},
],
});
@@ -1018,6 +1064,8 @@ describe("Bug 137: no animation actions lost during rapid pipeline updates", ()
error: null,
merge_failure: null,
agent: { agent_name: "coder-1", model: "sonnet", status: "running" },
review_hold: null,
qa: null,
},
],
});
@@ -1029,6 +1077,8 @@ describe("Bug 137: no animation actions lost during rapid pipeline updates", ()
error: null,
merge_failure: null,
agent: { agent_name: "coder-2", model: "haiku", status: "running" },
review_hold: null,
qa: null,
},
],
});
@@ -1095,6 +1145,8 @@ describe("Bug 137: no animation actions lost during rapid pipeline updates", ()
error: null,
merge_failure: null,
agent: { agent_name: "coder-1", model: null, status: "running" },
review_hold: null,
qa: null,
},
],
});
@@ -1106,6 +1158,8 @@ describe("Bug 137: no animation actions lost during rapid pipeline updates", ()
error: null,
merge_failure: null,
agent: { agent_name: "coder-2", model: null, status: "running" },
review_hold: null,
qa: null,
},
],
});
@@ -1191,6 +1245,8 @@ describe("Bug 137: animations remain functional through sustained agent activity
error: null,
merge_failure: null,
agent: { agent_name: agentName, model: null, status: "running" },
review_hold: null,
qa: null,
},
],
});

View File

@@ -115,7 +115,7 @@ export function LozengeFlyProvider({
const assignedAgentNames = useMemo(() => {
const names = new Set<string>();
for (const item of [
...pipeline.upcoming,
...pipeline.backlog,
...pipeline.current,
...pipeline.qa,
...pipeline.merge,
@@ -165,13 +165,13 @@ export function LozengeFlyProvider({
const prev = prevPipelineRef.current;
const allPrev = [
...prev.upcoming,
...prev.backlog,
...prev.current,
...prev.qa,
...prev.merge,
];
const allCurr = [
...pipeline.upcoming,
...pipeline.backlog,
...pipeline.current,
...pipeline.qa,
...pipeline.merge,

View File

@@ -0,0 +1,246 @@
import * as React from "react";
const { useCallback, useEffect, useRef, useState } = React;
export interface LogEntry {
timestamp: string;
level: string;
message: string;
}
interface ServerLogsPanelProps {
logs: LogEntry[];
}
function levelColor(level: string): string {
switch (level.toUpperCase()) {
case "ERROR":
return "#e06c75";
case "WARN":
return "#e5c07b";
default:
return "#98c379";
}
}
export function ServerLogsPanel({ logs }: ServerLogsPanelProps) {
const [isOpen, setIsOpen] = useState(false);
const [filter, setFilter] = useState("");
const [severityFilter, setSeverityFilter] = useState<string>("ALL");
const scrollRef = useRef<HTMLDivElement>(null);
const userScrolledUpRef = useRef(false);
const lastScrollTopRef = useRef(0);
const filteredLogs = logs.filter((entry) => {
const matchesSeverity =
severityFilter === "ALL" || entry.level.toUpperCase() === severityFilter;
const matchesFilter =
filter === "" ||
entry.message.toLowerCase().includes(filter.toLowerCase()) ||
entry.timestamp.includes(filter);
return matchesSeverity && matchesFilter;
});
const scrollToBottom = useCallback(() => {
const el = scrollRef.current;
if (el) {
el.scrollTop = el.scrollHeight;
lastScrollTopRef.current = el.scrollTop;
}
}, []);
// Auto-scroll when new entries arrive (unless user scrolled up).
useEffect(() => {
if (!isOpen) return;
if (!userScrolledUpRef.current) {
scrollToBottom();
}
}, [filteredLogs.length, isOpen, scrollToBottom]);
const handleScroll = () => {
const el = scrollRef.current;
if (!el) return;
const isAtBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 5;
if (el.scrollTop < lastScrollTopRef.current) {
userScrolledUpRef.current = true;
}
if (isAtBottom) {
userScrolledUpRef.current = false;
}
lastScrollTopRef.current = el.scrollTop;
};
const severityButtons = ["ALL", "INFO", "WARN", "ERROR"] as const;
return (
<div
data-testid="server-logs-panel"
style={{
borderRadius: "8px",
border: "1px solid #333",
overflow: "hidden",
}}
>
{/* Header / toggle */}
<button
type="button"
data-testid="server-logs-panel-toggle"
onClick={() => setIsOpen((v) => !v)}
style={{
width: "100%",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "8px 12px",
background: "#1e1e1e",
border: "none",
cursor: "pointer",
color: "#ccc",
fontSize: "0.85em",
fontWeight: 600,
textAlign: "left",
}}
>
<span>Server Logs</span>
<span style={{ color: "#666", fontSize: "0.85em" }}>
{logs.length > 0 && (
<span style={{ marginRight: "8px", color: "#555" }}>
{logs.length}
</span>
)}
{isOpen ? "▲" : "▼"}
</span>
</button>
{isOpen && (
<div style={{ background: "#0d1117" }}>
{/* Filter controls */}
<div
style={{
display: "flex",
gap: "6px",
padding: "8px",
borderBottom: "1px solid #1e1e1e",
flexWrap: "wrap",
alignItems: "center",
}}
>
<input
type="text"
data-testid="server-logs-filter-input"
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Filter logs..."
style={{
flex: 1,
minWidth: "80px",
padding: "4px 8px",
borderRadius: "4px",
border: "1px solid #333",
background: "#161b22",
color: "#ccc",
fontSize: "0.8em",
outline: "none",
}}
/>
{severityButtons.map((sev) => (
<button
key={sev}
type="button"
data-testid={`server-logs-severity-${sev.toLowerCase()}`}
onClick={() => setSeverityFilter(sev)}
style={{
padding: "3px 8px",
borderRadius: "4px",
border: "1px solid",
borderColor:
severityFilter === sev ? levelColor(sev) : "#333",
background:
severityFilter === sev
? "rgba(255,255,255,0.06)"
: "transparent",
color:
sev === "ALL"
? severityFilter === "ALL"
? "#ccc"
: "#555"
: levelColor(sev),
fontSize: "0.75em",
cursor: "pointer",
fontWeight: severityFilter === sev ? 700 : 400,
}}
>
{sev}
</button>
))}
</div>
{/* Log entries */}
<div
ref={scrollRef}
onScroll={handleScroll}
data-testid="server-logs-entries"
style={{
maxHeight: "240px",
overflowY: "auto",
padding: "4px 0",
fontFamily: "monospace",
fontSize: "0.75em",
}}
>
{filteredLogs.length === 0 ? (
<div
style={{
padding: "16px",
color: "#444",
textAlign: "center",
fontSize: "0.9em",
}}
>
No log entries
</div>
) : (
filteredLogs.map((entry, idx) => (
<div
key={`${entry.timestamp}-${idx}`}
style={{
display: "flex",
gap: "6px",
padding: "1px 8px",
lineHeight: "1.5",
borderBottom: "1px solid #111",
}}
>
<span
style={{ color: "#444", flexShrink: 0, minWidth: "70px" }}
>
{entry.timestamp.replace("T", " ").replace("Z", "")}
</span>
<span
style={{
color: levelColor(entry.level),
flexShrink: 0,
minWidth: "38px",
fontWeight: 700,
}}
>
{entry.level}
</span>
<span
style={{
color: "#c9d1d9",
wordBreak: "break-word",
whiteSpace: "pre-wrap",
}}
>
{entry.message}
</span>
</div>
))
)}
</div>
</div>
)}
</div>
);
}

View File

@@ -17,6 +17,8 @@ describe("StagePanel", () => {
error: null,
merge_failure: null,
agent: null,
review_hold: null,
qa: null,
},
];
render(<StagePanel title="Current" items={items} />);
@@ -37,6 +39,8 @@ describe("StagePanel", () => {
model: "sonnet",
status: "running",
},
review_hold: null,
qa: null,
},
];
render(<StagePanel title="Current" items={items} />);
@@ -56,6 +60,8 @@ describe("StagePanel", () => {
model: null,
status: "running",
},
review_hold: null,
qa: null,
},
];
render(<StagePanel title="Current" items={items} />);
@@ -74,6 +80,8 @@ describe("StagePanel", () => {
model: "haiku",
status: "pending",
},
review_hold: null,
qa: null,
},
];
render(<StagePanel title="QA" items={items} />);
@@ -88,6 +96,8 @@ describe("StagePanel", () => {
error: null,
merge_failure: null,
agent: null,
review_hold: null,
qa: null,
},
];
render(<StagePanel title="Current" items={items} />);
@@ -102,6 +112,8 @@ describe("StagePanel", () => {
error: "Missing front matter",
merge_failure: null,
agent: null,
review_hold: null,
qa: null,
},
];
render(<StagePanel title="Upcoming" items={items} />);
@@ -116,6 +128,8 @@ describe("StagePanel", () => {
error: null,
merge_failure: null,
agent: null,
review_hold: null,
qa: null,
},
];
render(<StagePanel title="Upcoming" items={items} />);
@@ -132,6 +146,8 @@ describe("StagePanel", () => {
error: null,
merge_failure: null,
agent: null,
review_hold: null,
qa: null,
},
];
render(<StagePanel title="Current" items={items} />);
@@ -148,6 +164,8 @@ describe("StagePanel", () => {
error: null,
merge_failure: null,
agent: null,
review_hold: null,
qa: null,
},
];
render(<StagePanel title="QA" items={items} />);
@@ -164,6 +182,8 @@ describe("StagePanel", () => {
error: null,
merge_failure: null,
agent: null,
review_hold: null,
qa: null,
},
];
render(<StagePanel title="Done" items={items} />);
@@ -180,6 +200,8 @@ describe("StagePanel", () => {
error: null,
merge_failure: null,
agent: null,
review_hold: null,
qa: null,
},
];
render(<StagePanel title="Upcoming" items={items} />);
@@ -199,6 +221,8 @@ describe("StagePanel", () => {
error: null,
merge_failure: null,
agent: null,
review_hold: null,
qa: null,
},
];
render(<StagePanel title="Current" items={items} />);
@@ -215,6 +239,8 @@ describe("StagePanel", () => {
error: null,
merge_failure: null,
agent: null,
review_hold: null,
qa: null,
},
];
render(<StagePanel title="QA" items={items} />);
@@ -231,6 +257,8 @@ describe("StagePanel", () => {
error: null,
merge_failure: null,
agent: null,
review_hold: null,
qa: null,
},
];
render(<StagePanel title="Done" items={items} />);
@@ -247,6 +275,8 @@ describe("StagePanel", () => {
error: null,
merge_failure: "Squash merge failed: conflicts in Cargo.lock",
agent: null,
review_hold: null,
qa: null,
},
];
render(<StagePanel title="Merge" items={items} />);
@@ -266,6 +296,8 @@ describe("StagePanel", () => {
error: null,
merge_failure: null,
agent: null,
review_hold: null,
qa: null,
},
];
render(<StagePanel title="Merge" items={items} />);

View File

@@ -42,6 +42,8 @@ interface StagePanelProps {
items: PipelineStageItem[];
emptyMessage?: string;
onItemClick?: (item: PipelineStageItem) => void;
/** Map of story_id → total_cost_usd for displaying cost badges. */
costs?: Map<string, number>;
}
function AgentLozenge({
@@ -128,6 +130,7 @@ export function StagePanel({
items,
emptyMessage = "Empty.",
onItemClick,
costs,
}: StagePanelProps) {
return (
<div
@@ -240,6 +243,19 @@ export function StagePanel({
{typeLabel}
</span>
)}
{costs?.has(item.story_id) && (
<span
data-testid={`cost-badge-${item.story_id}`}
style={{
fontSize: "0.65em",
fontWeight: 600,
color: "#e3b341",
marginRight: "8px",
}}
>
${costs.get(item.story_id)?.toFixed(2)}
</span>
)}
{item.name ?? item.story_id}
</div>
{item.error && (

View File

@@ -0,0 +1,440 @@
import * as React from "react";
import type { TokenUsageRecord } from "../api/client";
import { api } from "../api/client";
type SortKey =
| "timestamp"
| "story_id"
| "agent_name"
| "model"
| "total_cost_usd";
type SortDir = "asc" | "desc";
function formatCost(usd: number): string {
if (usd === 0) return "$0.00";
if (usd < 0.001) return `$${usd.toFixed(6)}`;
if (usd < 0.01) return `$${usd.toFixed(4)}`;
return `$${usd.toFixed(3)}`;
}
function formatTokens(n: number): string {
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
return String(n);
}
function formatTimestamp(iso: string): string {
const d = new Date(iso);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
const h = String(d.getHours()).padStart(2, "0");
const m = String(d.getMinutes()).padStart(2, "0");
return `${year}-${month}-${day} ${h}:${m}`;
}
/** Infer an agent type from the agent name. */
function agentType(agentName: string): string {
const lower = agentName.toLowerCase();
if (lower.startsWith("coder")) return "coder";
if (lower.startsWith("qa")) return "qa";
if (lower.startsWith("mergemaster") || lower.startsWith("merge"))
return "mergemaster";
return "other";
}
interface SortHeaderProps {
label: string;
sortKey: SortKey;
current: SortKey;
dir: SortDir;
onSort: (key: SortKey) => void;
align?: "left" | "right";
}
function SortHeader({
label,
sortKey,
current,
dir,
onSort,
align = "left",
}: SortHeaderProps) {
const active = current === sortKey;
return (
<th
style={{
padding: "8px 12px",
textAlign: align,
cursor: "pointer",
userSelect: "none",
borderBottom: "1px solid #333",
color: active ? "#ececec" : "#aaa",
fontWeight: active ? "700" : "500",
whiteSpace: "nowrap",
fontSize: "0.8em",
letterSpacing: "0.05em",
textTransform: "uppercase",
}}
onClick={() => onSort(sortKey)}
>
{label}
{active ? (dir === "asc" ? " ↑" : " ↓") : ""}
</th>
);
}
interface TokenUsagePageProps {
projectPath: string;
}
export function TokenUsagePage({
projectPath: _projectPath,
}: TokenUsagePageProps) {
const [records, setRecords] = React.useState<TokenUsageRecord[]>([]);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState<string | null>(null);
const [sortKey, setSortKey] = React.useState<SortKey>("timestamp");
const [sortDir, setSortDir] = React.useState<SortDir>("desc");
React.useEffect(() => {
setLoading(true);
setError(null);
api
.getAllTokenUsage()
.then((resp) => setRecords(resp.records))
.catch((e) =>
setError(e instanceof Error ? e.message : "Failed to load token usage"),
)
.finally(() => setLoading(false));
}, []);
function handleSort(key: SortKey) {
if (key === sortKey) {
setSortDir((d) => (d === "asc" ? "desc" : "asc"));
} else {
setSortKey(key);
setSortDir(key === "timestamp" ? "desc" : "asc");
}
}
const sorted = React.useMemo(() => {
return [...records].sort((a, b) => {
let cmp = 0;
switch (sortKey) {
case "timestamp":
cmp = a.timestamp.localeCompare(b.timestamp);
break;
case "story_id":
cmp = a.story_id.localeCompare(b.story_id);
break;
case "agent_name":
cmp = a.agent_name.localeCompare(b.agent_name);
break;
case "model":
cmp = (a.model ?? "").localeCompare(b.model ?? "");
break;
case "total_cost_usd":
cmp = a.total_cost_usd - b.total_cost_usd;
break;
}
return sortDir === "asc" ? cmp : -cmp;
});
}, [records, sortKey, sortDir]);
// Compute summary totals
const totalCost = records.reduce((s, r) => s + r.total_cost_usd, 0);
const byAgentType = React.useMemo(() => {
const map: Record<string, number> = {};
for (const r of records) {
const t = agentType(r.agent_name);
map[t] = (map[t] ?? 0) + r.total_cost_usd;
}
return map;
}, [records]);
const byModel = React.useMemo(() => {
const map: Record<string, number> = {};
for (const r of records) {
const m = r.model ?? "unknown";
map[m] = (map[m] ?? 0) + r.total_cost_usd;
}
return map;
}, [records]);
const cellStyle: React.CSSProperties = {
padding: "7px 12px",
borderBottom: "1px solid #222",
fontSize: "0.85em",
color: "#ccc",
whiteSpace: "nowrap",
};
return (
<div
style={{
height: "100%",
overflowY: "auto",
background: "#111",
padding: "24px",
fontFamily: "monospace",
}}
>
<h2
style={{
color: "#ececec",
margin: "0 0 20px",
fontSize: "1.1em",
fontWeight: "700",
letterSpacing: "0.04em",
}}
>
Token Usage
</h2>
{/* Summary totals */}
<div
style={{
display: "flex",
gap: "16px",
flexWrap: "wrap",
marginBottom: "24px",
}}
>
<SummaryCard
label="Total Cost"
value={formatCost(totalCost)}
highlight
/>
{Object.entries(byAgentType)
.sort(([a], [b]) => a.localeCompare(b))
.map(([type, cost]) => (
<SummaryCard
key={type}
label={`${type.charAt(0).toUpperCase()}${type.slice(1)}`}
value={formatCost(cost)}
/>
))}
{Object.entries(byModel)
.sort(([, a], [, b]) => b - a)
.map(([model, cost]) => (
<SummaryCard key={model} label={model} value={formatCost(cost)} />
))}
</div>
{loading && (
<p style={{ color: "#555", fontSize: "0.9em" }}>Loading...</p>
)}
{error && <p style={{ color: "#e05c5c", fontSize: "0.9em" }}>{error}</p>}
{!loading && !error && records.length === 0 && (
<p style={{ color: "#555", fontSize: "0.9em" }}>
No token usage records found.
</p>
)}
{!loading && !error && records.length > 0 && (
<div style={{ overflowX: "auto" }}>
<table
style={{
width: "100%",
borderCollapse: "collapse",
fontSize: "0.9em",
}}
>
<thead>
<tr style={{ background: "#1a1a1a" }}>
<SortHeader
label="Date"
sortKey="timestamp"
current={sortKey}
dir={sortDir}
onSort={handleSort}
/>
<SortHeader
label="Story"
sortKey="story_id"
current={sortKey}
dir={sortDir}
onSort={handleSort}
/>
<SortHeader
label="Agent"
sortKey="agent_name"
current={sortKey}
dir={sortDir}
onSort={handleSort}
/>
<SortHeader
label="Model"
sortKey="model"
current={sortKey}
dir={sortDir}
onSort={handleSort}
/>
<th
style={{
...cellStyle,
borderBottom: "1px solid #333",
textAlign: "right",
color: "#aaa",
fontSize: "0.8em",
letterSpacing: "0.05em",
textTransform: "uppercase",
fontWeight: "500",
}}
>
Input
</th>
<th
style={{
...cellStyle,
borderBottom: "1px solid #333",
textAlign: "right",
color: "#aaa",
fontSize: "0.8em",
letterSpacing: "0.05em",
textTransform: "uppercase",
fontWeight: "500",
}}
>
Cache+
</th>
<th
style={{
...cellStyle,
borderBottom: "1px solid #333",
textAlign: "right",
color: "#aaa",
fontSize: "0.8em",
letterSpacing: "0.05em",
textTransform: "uppercase",
fontWeight: "500",
}}
>
Cache
</th>
<th
style={{
...cellStyle,
borderBottom: "1px solid #333",
textAlign: "right",
color: "#aaa",
fontSize: "0.8em",
letterSpacing: "0.05em",
textTransform: "uppercase",
fontWeight: "500",
}}
>
Output
</th>
<SortHeader
label="Cost"
sortKey="total_cost_usd"
current={sortKey}
dir={sortDir}
onSort={handleSort}
align="right"
/>
</tr>
</thead>
<tbody>
{sorted.map((r, i) => (
<tr
key={`${r.story_id}-${r.agent_name}-${r.timestamp}`}
style={{ background: i % 2 === 0 ? "#111" : "#161616" }}
>
<td style={cellStyle}>{formatTimestamp(r.timestamp)}</td>
<td
style={{
...cellStyle,
color: "#8b9cf7",
maxWidth: "220px",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{r.story_id}
</td>
<td style={{ ...cellStyle, color: "#7ec8a4" }}>
{r.agent_name}
</td>
<td style={{ ...cellStyle, color: "#c9a96e" }}>
{r.model ?? "—"}
</td>
<td style={{ ...cellStyle, textAlign: "right" }}>
{formatTokens(r.input_tokens)}
</td>
<td style={{ ...cellStyle, textAlign: "right" }}>
{formatTokens(r.cache_creation_input_tokens)}
</td>
<td style={{ ...cellStyle, textAlign: "right" }}>
{formatTokens(r.cache_read_input_tokens)}
</td>
<td style={{ ...cellStyle, textAlign: "right" }}>
{formatTokens(r.output_tokens)}
</td>
<td
style={{
...cellStyle,
textAlign: "right",
color: "#e08c5c",
fontWeight: "600",
}}
>
{formatCost(r.total_cost_usd)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
);
}
function SummaryCard({
label,
value,
highlight = false,
}: {
label: string;
value: string;
highlight?: boolean;
}) {
return (
<div
style={{
background: highlight ? "#1e1e2e" : "#1a1a1a",
border: `1px solid ${highlight ? "#3a3a5a" : "#2a2a2a"}`,
borderRadius: "8px",
padding: "12px 16px",
minWidth: "120px",
}}
>
<div
style={{
fontSize: "0.7em",
color: "#666",
textTransform: "uppercase",
letterSpacing: "0.07em",
marginBottom: "4px",
}}
>
{label}
</div>
<div
style={{
fontSize: "1.1em",
fontWeight: "700",
color: highlight ? "#c9a96e" : "#ececec",
fontFamily: "monospace",
}}
>
{value}
</div>
</div>
);
}

View File

@@ -1,7 +1,7 @@
import { act, render, screen, waitFor } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { AgentEvent, AgentInfo } from "../api/agents";
import type { TestResultsResponse } from "../api/client";
import type { TestResultsResponse, TokenCostResponse } from "../api/client";
vi.mock("../api/client", async () => {
const actual =
@@ -12,6 +12,7 @@ vi.mock("../api/client", async () => {
...actual.api,
getWorkItemContent: vi.fn(),
getTestResults: vi.fn(),
getTokenCost: vi.fn(),
},
};
});
@@ -30,6 +31,7 @@ const { WorkItemDetailPanel } = await import("./WorkItemDetailPanel");
const mockedGetWorkItemContent = vi.mocked(api.getWorkItemContent);
const mockedGetTestResults = vi.mocked(api.getTestResults);
const mockedGetTokenCost = vi.mocked(api.getTokenCost);
const mockedListAgents = vi.mocked(agentsApi.listAgents);
const mockedSubscribeAgentStream = vi.mocked(subscribeAgentStream);
@@ -37,6 +39,7 @@ const DEFAULT_CONTENT = {
content: "# Big Title\n\nSome content here.",
stage: "current",
name: "Big Title Story",
agent: null,
};
const sampleTestResults: TestResultsResponse = {
@@ -51,6 +54,7 @@ beforeEach(() => {
vi.clearAllMocks();
mockedGetWorkItemContent.mockResolvedValue(DEFAULT_CONTENT);
mockedGetTestResults.mockResolvedValue(null);
mockedGetTokenCost.mockResolvedValue({ total_cost_usd: 0, agents: [] });
mockedListAgents.mockResolvedValue([]);
mockedSubscribeAgentStream.mockReturnValue(() => {});
});
@@ -436,6 +440,60 @@ describe("WorkItemDetailPanel - Agent Logs", () => {
});
});
describe("WorkItemDetailPanel - Assigned Agent", () => {
it("shows assigned agent name when agent front matter field is set", async () => {
mockedGetWorkItemContent.mockResolvedValue({
...DEFAULT_CONTENT,
agent: "coder-opus",
});
render(
<WorkItemDetailPanel
storyId="271_story_test"
pipelineVersion={0}
onClose={() => {}}
/>,
);
const agentEl = await screen.findByTestId("detail-panel-assigned-agent");
expect(agentEl).toHaveTextContent("coder-opus");
});
it("omits assigned agent field when no agent is set in front matter", async () => {
render(
<WorkItemDetailPanel
storyId="271_story_test"
pipelineVersion={0}
onClose={() => {}}
/>,
);
await screen.findByTestId("detail-panel-content");
expect(
screen.queryByTestId("detail-panel-assigned-agent"),
).not.toBeInTheDocument();
});
it("shows the specific agent name not just 'assigned'", async () => {
mockedGetWorkItemContent.mockResolvedValue({
...DEFAULT_CONTENT,
agent: "coder-haiku",
});
render(
<WorkItemDetailPanel
storyId="271_story_test"
pipelineVersion={0}
onClose={() => {}}
/>,
);
const agentEl = await screen.findByTestId("detail-panel-assigned-agent");
expect(agentEl).toHaveTextContent("coder-haiku");
expect(agentEl).not.toHaveTextContent("assigned");
});
});
describe("WorkItemDetailPanel - Test Results", () => {
it("shows empty test results message when no results exist", async () => {
mockedGetTestResults.mockResolvedValue(null);
@@ -553,3 +611,146 @@ describe("WorkItemDetailPanel - Test Results", () => {
});
});
});
describe("WorkItemDetailPanel - Token Cost", () => {
const sampleTokenCost: TokenCostResponse = {
total_cost_usd: 0.012345,
agents: [
{
agent_name: "coder-1",
model: "claude-sonnet-4-6",
input_tokens: 1000,
output_tokens: 500,
cache_creation_input_tokens: 200,
cache_read_input_tokens: 100,
total_cost_usd: 0.009,
},
{
agent_name: "coder-2",
model: null,
input_tokens: 800,
output_tokens: 300,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0,
total_cost_usd: 0.003345,
},
],
};
it("shows empty state when no token data exists", async () => {
mockedGetTokenCost.mockResolvedValue({ total_cost_usd: 0, agents: [] });
render(
<WorkItemDetailPanel
storyId="42_story_foo"
pipelineVersion={0}
onClose={() => {}}
/>,
);
await waitFor(() => {
expect(screen.getByTestId("token-cost-empty")).toBeInTheDocument();
});
expect(screen.getByText("No token data recorded")).toBeInTheDocument();
});
it("shows per-agent breakdown and total cost when data exists", async () => {
mockedGetTokenCost.mockResolvedValue(sampleTokenCost);
render(
<WorkItemDetailPanel
storyId="42_story_foo"
pipelineVersion={0}
onClose={() => {}}
/>,
);
await waitFor(() => {
expect(screen.getByTestId("token-cost-content")).toBeInTheDocument();
});
expect(screen.getByTestId("token-cost-total")).toHaveTextContent(
"$0.012345",
);
expect(screen.getByTestId("token-cost-agent-coder-1")).toBeInTheDocument();
expect(screen.getByTestId("token-cost-agent-coder-2")).toBeInTheDocument();
});
it("shows agent name and model when model is present", async () => {
mockedGetTokenCost.mockResolvedValue(sampleTokenCost);
render(
<WorkItemDetailPanel
storyId="42_story_foo"
pipelineVersion={0}
onClose={() => {}}
/>,
);
await waitFor(() => {
expect(
screen.getByTestId("token-cost-agent-coder-1"),
).toBeInTheDocument();
});
const agentRow = screen.getByTestId("token-cost-agent-coder-1");
expect(agentRow).toHaveTextContent("coder-1");
expect(agentRow).toHaveTextContent("claude-sonnet-4-6");
});
it("shows agent name without model when model is null", async () => {
mockedGetTokenCost.mockResolvedValue(sampleTokenCost);
render(
<WorkItemDetailPanel
storyId="42_story_foo"
pipelineVersion={0}
onClose={() => {}}
/>,
);
await waitFor(() => {
expect(
screen.getByTestId("token-cost-agent-coder-2"),
).toBeInTheDocument();
});
const agentRow = screen.getByTestId("token-cost-agent-coder-2");
expect(agentRow).toHaveTextContent("coder-2");
expect(agentRow).not.toHaveTextContent("null");
});
it("re-fetches token cost when pipelineVersion changes", async () => {
mockedGetTokenCost.mockResolvedValue({ total_cost_usd: 0, agents: [] });
const { rerender } = render(
<WorkItemDetailPanel
storyId="42_story_foo"
pipelineVersion={0}
onClose={() => {}}
/>,
);
await waitFor(() => {
expect(mockedGetTokenCost).toHaveBeenCalledTimes(1);
});
mockedGetTokenCost.mockResolvedValue(sampleTokenCost);
rerender(
<WorkItemDetailPanel
storyId="42_story_foo"
pipelineVersion={1}
onClose={() => {}}
/>,
);
await waitFor(() => {
expect(mockedGetTokenCost).toHaveBeenCalledTimes(2);
});
await waitFor(() => {
expect(screen.getByTestId("token-cost-content")).toBeInTheDocument();
});
});
});

View File

@@ -2,13 +2,18 @@ import * as React from "react";
import Markdown from "react-markdown";
import type { AgentEvent, AgentInfo, AgentStatusValue } from "../api/agents";
import { agentsApi, subscribeAgentStream } from "../api/agents";
import type { TestCaseResult, TestResultsResponse } from "../api/client";
import type {
AgentCostEntry,
TestCaseResult,
TestResultsResponse,
TokenCostResponse,
} from "../api/client";
import { api } from "../api/client";
const { useEffect, useRef, useState } = React;
const STAGE_LABELS: Record<string, string> = {
upcoming: "Upcoming",
backlog: "Backlog",
current: "Current",
qa: "QA",
merge: "To Merge",
@@ -27,6 +32,8 @@ interface WorkItemDetailPanelProps {
storyId: string;
pipelineVersion: number;
onClose: () => void;
/** True when the item is in QA and awaiting human review. */
reviewHold?: boolean;
}
function TestCaseRow({ tc }: { tc: TestCaseResult }) {
@@ -109,10 +116,12 @@ export function WorkItemDetailPanel({
storyId,
pipelineVersion,
onClose,
reviewHold: _reviewHold,
}: WorkItemDetailPanelProps) {
const [content, setContent] = useState<string | null>(null);
const [stage, setStage] = useState<string>("");
const [name, setName] = useState<string | null>(null);
const [assignedAgent, setAssignedAgent] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [agentInfo, setAgentInfo] = useState<AgentInfo | null>(null);
@@ -121,6 +130,7 @@ export function WorkItemDetailPanel({
const [testResults, setTestResults] = useState<TestResultsResponse | null>(
null,
);
const [tokenCost, setTokenCost] = useState<TokenCostResponse | null>(null);
const panelRef = useRef<HTMLDivElement>(null);
const cleanupRef = useRef<(() => void) | null>(null);
@@ -133,6 +143,7 @@ export function WorkItemDetailPanel({
setContent(data.content);
setStage(data.stage);
setName(data.name);
setAssignedAgent(data.agent);
})
.catch((err: unknown) => {
setError(err instanceof Error ? err.message : "Failed to load content");
@@ -154,6 +165,18 @@ export function WorkItemDetailPanel({
});
}, [storyId, pipelineVersion]);
// Fetch token cost on mount and when pipeline updates arrive.
useEffect(() => {
api
.getTokenCost(storyId)
.then((data) => {
setTokenCost(data);
})
.catch(() => {
// Silently ignore — token cost may not exist yet.
});
}, [storyId, pipelineVersion]);
useEffect(() => {
cleanupRef.current?.();
cleanupRef.current = null;
@@ -278,6 +301,14 @@ export function WorkItemDetailPanel({
{stageLabel}
</div>
)}
{assignedAgent ? (
<div
data-testid="detail-panel-assigned-agent"
style={{ fontSize: "0.75em", color: "#888" }}
>
Agent: {assignedAgent}
</div>
) : null}
</div>
<button
type="button"
@@ -352,6 +383,96 @@ export function WorkItemDetailPanel({
</div>
)}
{/* Token Cost section */}
<div
data-testid="token-cost-section"
style={{
border: "1px solid #2a2a2a",
borderRadius: "8px",
padding: "10px 12px",
background: "#161616",
}}
>
<div
style={{
fontWeight: 600,
fontSize: "0.8em",
color: "#555",
marginBottom: "8px",
}}
>
Token Cost
</div>
{tokenCost && tokenCost.agents.length > 0 ? (
<div data-testid="token-cost-content">
<div
style={{
fontSize: "0.75em",
color: "#888",
marginBottom: "8px",
}}
>
Total:{" "}
<span data-testid="token-cost-total" style={{ color: "#ccc" }}>
${tokenCost.total_cost_usd.toFixed(6)}
</span>
</div>
{tokenCost.agents.map((agent: AgentCostEntry) => (
<div
key={agent.agent_name}
data-testid={`token-cost-agent-${agent.agent_name}`}
style={{
fontSize: "0.75em",
color: "#888",
padding: "4px 0",
borderTop: "1px solid #222",
}}
>
<div
style={{
display: "flex",
justifyContent: "space-between",
marginBottom: "2px",
}}
>
<span style={{ color: "#ccc", fontWeight: 600 }}>
{agent.agent_name}
{agent.model ? (
<span
style={{ color: "#666", fontWeight: 400 }}
>{` (${agent.model})`}</span>
) : null}
</span>
<span style={{ color: "#aaa" }}>
${agent.total_cost_usd.toFixed(6)}
</span>
</div>
<div style={{ color: "#555" }}>
in {agent.input_tokens.toLocaleString()} / out{" "}
{agent.output_tokens.toLocaleString()}
{(agent.cache_creation_input_tokens > 0 ||
agent.cache_read_input_tokens > 0) && (
<>
{" "}
/ cache +
{agent.cache_creation_input_tokens.toLocaleString()}{" "}
read {agent.cache_read_input_tokens.toLocaleString()}
</>
)}
</div>
</div>
))}
</div>
) : (
<div
data-testid="token-cost-empty"
style={{ fontSize: "0.75em", color: "#444" }}
>
No token data recorded
</div>
)}
</div>
{/* Test Results section */}
<div
data-testid="test-results-section"
@@ -410,10 +531,34 @@ export function WorkItemDetailPanel({
}}
>
{/* Agent Logs section */}
{!agentInfo && (
<div
data-testid={
agentInfo ? "agent-logs-section" : "placeholder-agent-logs"
}
data-testid="placeholder-agent-logs"
style={{
border: "1px solid #2a2a2a",
borderRadius: "8px",
padding: "10px 12px",
background: "#161616",
}}
>
<div
style={{
fontWeight: 600,
fontSize: "0.8em",
color: "#555",
marginBottom: "4px",
}}
>
Agent Logs
</div>
<div style={{ fontSize: "0.75em", color: "#444" }}>
Coming soon
</div>
</div>
)}
{agentInfo && (
<div
data-testid="agent-logs-section"
style={{
border: "1px solid #2a2a2a",
borderRadius: "8px",
@@ -426,19 +571,19 @@ export function WorkItemDetailPanel({
display: "flex",
alignItems: "center",
justifyContent: "space-between",
marginBottom: agentInfo ? "6px" : "4px",
marginBottom: "6px",
}}
>
<div
style={{
fontWeight: 600,
fontSize: "0.8em",
color: agentInfo ? "#888" : "#555",
color: "#888",
}}
>
Agent Logs
</div>
{agentInfo && agentStatus && (
{agentStatus && (
<div
data-testid="agent-status-badge"
style={{
@@ -451,7 +596,7 @@ export function WorkItemDetailPanel({
</div>
)}
</div>
{agentInfo && agentLog.length > 0 ? (
{agentLog.length > 0 ? (
<div
data-testid="agent-log-output"
style={{
@@ -467,18 +612,15 @@ export function WorkItemDetailPanel({
>
{agentLog.join("")}
</div>
) : agentInfo ? (
) : (
<div style={{ fontSize: "0.75em", color: "#444" }}>
{agentStatus === "running" || agentStatus === "pending"
? "Waiting for output..."
: "No output."}
</div>
) : (
<div style={{ fontSize: "0.75em", color: "#444" }}>
Coming soon
</div>
)}
</div>
)}
{/* Placeholder sections for future content */}
{(

View File

@@ -33,7 +33,7 @@ function makeProps(
describe("SelectionScreen", () => {
it("renders the title and description", () => {
render(<SelectionScreen {...makeProps()} />);
expect(screen.getByText("Story Kit")).toBeInTheDocument();
expect(screen.getByText("Storkit")).toBeInTheDocument();
expect(
screen.getByText("Paste or complete a project path to start."),
).toBeInTheDocument();

View File

@@ -54,7 +54,7 @@ export function SelectionScreen({
className="selection-screen"
style={{ padding: "2rem", maxWidth: "800px", margin: "0 auto" }}
>
<h1>Story Kit</h1>
<h1>Storkit</h1>
<p>Paste or complete a project path to start.</p>
{knownProjects.length > 0 && (

View File

@@ -1 +1,20 @@
import "@testing-library/jest-dom";
import { beforeEach, vi } from "vitest";
// Provide a default fetch mock so components that call API endpoints on mount
// don't throw URL-parse errors in the jsdom test environment. Tests that need
// specific responses should mock the relevant `api.*` method as usual.
beforeEach(() => {
vi.stubGlobal(
"fetch",
vi.fn((input: string | URL | Request) => {
const url = typeof input === "string" ? input : input.toString();
// Endpoints that return arrays need [] not {} to avoid "not iterable" errors.
const arrayEndpoints = ["/agents", "/agents/config"];
const body = arrayEndpoints.some((ep) => url.endsWith(ep))
? JSON.stringify([])
: JSON.stringify({});
return Promise.resolve(new Response(body, { status: 200 }));
}),
);
});

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