Compare commits

...

392 Commits

Author SHA1 Message Date
Timmy 327163eb60 Bump version to 0.10.1 2026-04-14 16:41:27 +01:00
Timmy 8f1dd0ad13 Removing example code 2026-04-14 14:34:43 +01:00
dave 28adef9739 chore: switch mergemaster to opus and add cargo fmt guidance
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 12:35:57 +00:00
dave badfabcf5e chore: switch mergemaster to opus and add cargo fmt guidance
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 12:27:58 +00:00
dave d0d2b17484 huskies: merge 563_story_build_agent_join_mechanism_agents_register_with_the_gateway_via_token 2026-04-14 12:06:13 +00:00
dave efe434ede3 huskies: merge 565_story_gateway_web_ui_shell_with_project_switcher 2026-04-14 11:28:55 +00:00
dave df5ba8ebab huskies: merge 560_story_make_merge_agent_work_return_results_like_run_tests_instead_of_polling 2026-04-14 10:26:44 +00:00
dave ff1149750b huskies: merge 561_bug_mcp_tools_matching_mcp_huskies_allowlist_still_trigger_permission_prompts 2026-04-14 10:19:51 +00:00
dave d824dc4b73 huskies: merge 558_story_matrix_bot_can_run_on_the_gateway_to_manage_multiple_projects_from_one_chat 2026-04-14 10:01:04 +00:00
dave 28777b0c77 fix: simplify boolean in validate_working_dir to satisfy clippy nonminimal_bool
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 09:52:51 +00:00
dave f412c7dee6 fix: cargo fmt the merge_workspace validation code
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 09:43:18 +00:00
dave 44fe52195e fix: allow MCP tools to access merge_workspace so mergemaster can fix conflicts
The permission lockdown restricted run_command/run_tests to
.huskies/worktrees/ only. The mergemaster could diagnose merge
conflict compile errors but couldn't edit files in .huskies/merge_workspace/
to fix them. Add merge_workspace as an allowed path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 09:21:39 +00:00
dave 979cf39228 huskies: merge 557_refactor_remove_all_filesystem_fallback_paths_crdt_is_the_only_source_of_truth 2026-04-14 09:14:07 +00:00
dave 10d3517648 fix: remove filesystem fallback from scan_stage_items to unblock 557 merge
The auto-resolver kept both sides of the conflict — feature's
_project_root signature with master's filesystem code referencing
project_root — producing a compile error. Remove the filesystem
fallback on master so there's no conflict. CRDT is the only source
of truth.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:14:58 +00:00
Timmy 8a62b62819 Noting the existence of chat transports in README 2026-04-13 17:22:54 +01:00
dave 2e412af4dd fix: suppress Vite chunk size warning that clutters test output
The JS bundle is ~1MB which is fine for an embedded admin UI.
Raise the warning limit to 1100KB.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 16:20:19 +00:00
dave 39b1964b68 fix: set git default branch to master in Dockerfile to suppress hint spam
Tests that create temp git repos produce thousands of lines of
"Using 'master' as the name for the initial branch" hints that
bury actual test failures in agent output.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 16:18:05 +00:00
dave bd04c6acd7 fix: capture test output with background pipe draining instead of Stdio::inherit
Stdio::inherit sent test output to server stdout, making it invisible
to agents calling run_tests via MCP. Switch back to Stdio::piped with
background drain threads (same pattern as gates.rs) to capture output
without the pipe deadlock that caused the original switch to inherit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 16:17:06 +00:00
dave 7977b7c5f8 huskies: merge 555_bug_agent_permission_prompts_flood_matrix_chat_instead_of_being_auto_denied 2026-04-13 15:02:47 +00:00
dave d618bc3b32 huskies: merge 556_bug_stale_filesystem_shadows_in_1_backlog_cause_auto_assign_to_promote_archived_stories 2026-04-13 14:48:44 +00:00
dave 845b85e7a7 fix: add --all to cargo fmt in script/test and autoformat codebase
cargo fmt without --all fails with "Failed to find targets" in
workspace repos. This was blocking every story's gates. Also ran
cargo fmt --all to fix all existing formatting issues.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 14:07:08 +00:00
dave ed2526ce41 feat: add get_version MCP tool returning version and build hash
Agents can now call get_version to see what server version and commit
they're running against.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 13:50:37 +00:00
dave 05655847d8 chore: log version on startup and gitignore build_hash
Startup now logs "huskies v0.10.0 (build abc1234)" so we can verify
both the version and the commit that's running. build_hash is a
runtime artifact, not tracked in git.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 13:48:18 +00:00
dave 0cb68e1de9 docs: add deployment modes to README — standard, headless, and gateway
Documents the three modes of the huskies binary: standard single-project
server, headless build agent (--rendezvous), and multi-project gateway
(--gateway). Includes projects.toml config example and Docker Compose
sketch for multi-project setup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 13:44:10 +00:00
dave cd189cfe60 fix: isolate frontend node_modules in Docker volume to prevent cross-platform conflicts
npm install pulls platform-specific native binaries (esbuild, rollup).
Without isolation, building on macOS writes macOS node_modules into the
bind mount, then the Linux container tries to execute them and fails.
The Docker volume gives each platform its own node_modules.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 13:36:32 +00:00
dave 69dab063a8 huskies: merge 554_story_multi_project_gateway_that_proxies_mcp_calls_to_per_project_docker_containers 2026-04-13 13:07:04 +00:00
dave 5806156af3 huskies: merge 553_story_accept_spike_state_machine_transition_skips_merge_and_goes_directly_to_done 2026-04-13 12:54:09 +00:00
dave 12497eb4f1 fix: add Read, Glob, Grep to agent settings.json allowlist
These read-only tools were missing from the locked-down settings,
causing permission prompts to flood Matrix chat for every agent
file read.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 11:44:17 +00:00
dave 8b5275a30b fix: dropdown hover gap and z-index so menu is clickable on desktop and mobile
Invisible bridge element fills the gap between toggle and menu so
hover chain doesn't break. Dropdown z-index raised above hero graphic
so links aren't obscured on mobile.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 11:42:11 +00:00
dave 5536803ad6 fix: simplify nav with Start dropdown and move Get in touch to footer
Nav now shows: How it works, Features, Start (dropdown: Docs, Source,
Releases). Get in touch moved to footer. Cleaner on mobile.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 11:15:44 +00:00
dave c4462e2918 fix: wrap nav links on mobile to prevent horizontal overflow
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 06:58:03 +00:00
dave f6cd947173 fix: shrink hero husky logo from 320px to 160px for mobile
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 23:24:18 +00:00
dave fa7c2fa0ed fix: switch tokio-tungstenite from native-tls to rustls to remove OpenSSL dependency
native-tls pulls in openssl-sys which requires system OpenSSL headers,
breaking macOS release builds. rustls-tls-native-roots is pure Rust.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 23:20:46 +00:00
Timmy f2fc33c86b Bump version to 0.10.0 2026-04-13 00:10:03 +01:00
dave 05c3b11e57 huskies: merge 551_bug_get_agent_output_mcp_tool_returns_fetch_failed_for_running_agents 2026-04-12 17:50:44 +00:00
dave ae4cacefe8 huskies: merge 549_bug_stale_stage_transition_notifications_for_stories_that_skipped_stages 2026-04-12 15:40:11 +00:00
dave 8ae06cc8e2 huskies: merge 550_story_split_status_command_into_status_pipeline_info_and_logs_agent_output_subcommands 2026-04-12 15:00:05 +00:00
dave da5d604d01 huskies: merge 548_refactor_rename_living_spec_standalone_to_huskies_in_package_json_and_cargo_lock 2026-04-12 14:50:38 +00:00
dave 9c3dbfb765 huskies: merge 485_story_documentation_site_for_huskies_dev 2026-04-12 14:17:28 +00:00
dave b85d7b3b86 huskies: merge 542_refactor_add_doc_comments_to_all_undocumented_source_files_and_generate_source_map_in_readme 2026-04-12 14:05:08 +00:00
dave 76765e15d2 huskies: merge 547_story_run_tests_bot_command_accepts_optional_story_number_to_run_tests_in_a_worktree 2026-04-12 13:26:13 +00:00
dave b7f077197d chore: add doc comment guidance to coder agent system prompts
Agents now know to add //! module comments and /// doc comments
to new public items, keeping documentation consistent with the
codebase-wide doc pass from story 542.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 13:25:21 +00:00
dave a344cfadee huskies: merge 544_story_add_run_build_and_run_lint_mcp_tools_backed_by_script_build_and_script_lint 2026-04-12 13:21:41 +00:00
dave cec62dad1c huskies: merge 542_refactor_add_doc_comments_to_all_undocumented_source_files_and_generate_source_map_in_readme 2026-04-12 13:16:11 +00:00
dave 6b1737d52d huskies: merge 546_refactor_rename_bot_test_command_to_run_tests_to_avoid_eating_chat_messages 2026-04-12 13:11:17 +00:00
dave b4dbfcbde6 huskies: merge 541_story_backlog_command_for_chat_and_web_ui_shows_only_backlog_items 2026-04-12 13:05:12 +00:00
dave 2bdb0eb730 fix: add log rotation to docker-compose to prevent disk fill
Test output now goes to container stdout via Stdio::inherit, so logs
grow fast. Cap at 50MB with 3 rotated files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 13:00:21 +00:00
dave 5f01631e6a huskies: merge 543_story_resume_failed_coder_agents_with_resume_instead_of_starting_fresh_sessions 2026-04-12 12:58:42 +00:00
dave c80931c15c fix: add ETXTBSY retry to run_coverage_gate
Use fsync in coverage gate tests to ensure the kernel releases the
write handle before executing the script. Prevents flaky ETXTBSY
errors on fast test runs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 12:40:08 +00:00
dave f140238cc3 fix: update run_tests tests for Stdio::inherit and bump tool count to 60
run_tests now uses Stdio::inherit so stdout/stderr aren't captured —
tests can only assert on pass/fail and exit code. Tool count bumped
from 59 to 60 for the new get_test_result tool.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 12:30:10 +00:00
dave 05bdc71ebc fix: add rustfmt to Docker image for formatting checks
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 12:20:47 +00:00
dave ec6891b5ba fix: remove stale tests that hang or assert dead behaviour
- Remove tool_merge_agent_work_returns_started and
  tool_get_merge_status_returns_running: these tested the old
  non-blocking API but tool_merge_agent_work now blocks in a poll
  loop, causing the tests to hang forever.

- Update coder_agents_have_root_cause_guidance: prompt no longer
  requires "git bisect" — check for bug workflow guidance instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 12:02:47 +00:00
dave 06defd9596 fix: collapse nested if-let blocks to satisfy clippy collapsible_if lint
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 11:43:36 +00:00
dave 0b58b0486c fix: use Stdio::inherit for run_tests to prevent pipe deadlock
spawn() with piped stdout/stderr deadlocks when the test binary
produces more output than the OS pipe buffer (64KB). Switch to
Stdio::inherit so test output flows to server logs and we can
see what's happening.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 00:46:43 +00:00
dave b43e7cf752 fix: kill stale cargo processes before running acceptance gates
The completion handler now pgrep+kills any cargo processes targeting
the worktree's Cargo.toml before running gates. This prevents the
run_tests MCP child from holding the build lock and blocking gates.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 00:25:56 +00:00
dave 8ae6ca3eb8 fix: make run_tests block server-side instead of requiring agent polling
run_tests now spawns the child and blocks in a 1-second poll loop until
tests complete or the 20-minute timeout fires. Returns the full result
in a single MCP call — agents use 1 turn instead of 50+. Child process
is properly killed on timeout (no zombies).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 23:07:02 +00:00
dave bac07d28a7 fix: increase run_tests MCP timeout to 20 minutes to match acceptance gates
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 22:43:31 +00:00
dave fc89be2f55 fix: server-side 20s blocking in get_test_result to prevent agent poll spam
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 22:29:38 +00:00
dave 028bff5ef1 fix: rewrite .huskies/README.md for CRDT-only world
Strip all filesystem pipeline references that were causing agents to
waste turns searching for story files on disk. The README now points
agents at MCP tools exclusively and documents the async run_tests
workflow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 22:11:36 +00:00
dave 1f66183c8e fix: update scaffold settings template to match locked-down agent permissions 2026-04-11 22:03:53 +00:00
dave f958f57e56 fix: async run_tests to prevent zombie cargo processes blocking gates
run_tests MCP tool now spawns tests in the background and returns
immediately. Agents poll get_test_result to check completion. This
prevents zombie cargo processes from holding the build lock when the
CLI times out the MCP call before tests finish.

Also fixes agent permission mode: acceptEdits replaces invalid
allowFullAutoEdit that was causing agents to crash-loop on spawn.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 22:00:05 +00:00
dave 8393a67c89 fix: log git hash on build success and startup to verify which commit is running
Writes HEAD short hash to .huskies/build_hash after successful cargo
build. Logs it on startup as [startup] Running build: <hash>. No more
guessing whether the rebuild actually deployed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 20:50:15 +00:00
dave e32300d1f8 fix: switch agent permission mode from bypassPermissions to allowFullAutoEdit
bypassPermissions ignored the worktree's .claude/settings.json entirely,
letting agents run any Bash command including cargo test (which they'd
spawn 4+ times concurrently, deadlocking on the build directory lock).

allowFullAutoEdit respects the settings.json allowlist, so agents can
only use the Bash commands we explicitly permit (cargo check, cargo
build, git) and must use MCP tools for everything else (run_tests,
run_lint, run_build).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 20:23:22 +00:00
dave 32e36bbc4b fix: remove cargo test/clippy/npm from agent Bash permissions
Agents were running cargo test directly via Bash instead of using the
run_tests MCP tool, causing 4 concurrent cargo builds that deadlocked
on the build directory lock. Removed cargo test, cargo clippy, cargo
nextest, script/test, npm test, and pnpm test from the allowed Bash
commands. Agents must use the run_tests MCP tool which returns truncated
output and prevents concurrent builds.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 19:50:52 +00:00
dave c0d1be675b fix: mergemaster prompt says merge_agent_work blocks — no polling needed 2026-04-11 18:13:53 +00:00
dave d06241c20c fix: merge_agent_work blocks until complete instead of requiring polling
The mergemaster agent was burning all 30 turns polling get_merge_status
every 2 seconds while the merge pipeline takes ~2 minutes. It would
exhaust turns, exit, restart, and repeat — never seeing the result.

merge_agent_work now blocks with a 10-second internal poll loop and
returns the final result directly. The agent calls it once and gets
the answer. No more polling turns wasted.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 17:43:50 +00:00
dave 599fbdc71d huskies: merge 539_bug_crdt_event_bridge_still_writes_filesystem_shadow_files_after_530_eliminated_filesystem_state 2026-04-11 17:04:36 +00:00
dave 6998275331 huskies: merge 540_bug_get_agent_output_mcp_tool_returns_no_agent_for_exited_agents_instead_of_reading_session_logs_from_disk 2026-04-11 16:33:58 +00:00
dave a9a1852422 fix: agent prompts say trust the story description instead of always investigating
Agents were spending entire $5 budgets grepping the codebase and reading
git history instead of making fixes when the story already specifies
exact file paths and function names. Changed bug workflow from
"investigate root cause first" to "trust the story, act fast" — go
directly to the specified location when the story tells you where.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 15:16:12 +00:00
dave 48ea612739 fix: remove startup CRDT stage sync — it fights the done→archived sweep
The sync_crdt_stages_from_db migration reads pipeline_items (which has
stale 5_done stages) and overwrites the CRDT back to 5_done for stories
that were already swept to 6_archived. On every restart, done stories
reappear and get re-swept.

The migration served its purpose — CRDT stages are now correct. Remove it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 13:50:07 +00:00
dave 17d635b66b fix: restore CRDT-based triage command (535 fix was reverted by merge conflict)
Story 535's triage fix was overwritten by a subsequent merge that
resolved a conflict by taking the old filesystem-based version.
Re-applies the CRDT-based triage that reads from pipeline state
and content store, works for any pipeline stage.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 13:43:26 +00:00
dave 4ab723f40b huskies: merge 538_bug_done_archived_sweep_never_fires_because_stage_done_projection_uses_utc_now_instead_of_real_merged_at_timestamp 2026-04-11 13:29:38 +00:00
dave 5d193bb568 huskies: merge 537_bug_delete_item_sets_stage_to_deleted_string_instead_of_writing_a_crdt_tombstone 2026-04-11 13:25:45 +00:00
dave dcf6cf8f82 fix: collapse consecutive str::replace calls to satisfy clippy 2026-04-11 13:21:47 +00:00
dave eea54ca616 fix: thread-local CRDT and content store for test isolation
Tests shared a global CRDT singleton and content store HashMap, causing
flaky failures when parallel tests wrote items that polluted each
other's assertions. 3-5 random test failures per run.

Both CRDT_STATE and CONTENT_STORE now use thread_local! in test mode
so each test thread gets its own isolated instance. Production code
is unchanged — it still uses the global OnceLock singletons.

Also fixed 3 tests (create_story_writes_correct_content,
next_item_number_increments_from_existing_bugs,
next_item_number_scans_archived_too) that relied on leaked state
from other tests — they now write to the content store explicitly.

Result: 1902 passed, 0 failed across 5 consecutive runs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 13:02:09 +00:00
dave dd53870c59 fix: agent prompts use run_tests MCP tool instead of running script/test via Bash
Agents were running script/test directly through the PTY, streaming
the full output of npm install, cargo clippy, cargo test, and frontend
builds into session logs. This tripled session log sizes (~200KB to
~600KB per session) and contributed to CLI SIGABRT crashes.

The run_tests MCP tool already runs script/test server-side and returns
a truncated JSON summary. Agents now use it exclusively.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 11:46:05 +00:00
dave 5696d77922 debug: add PTY spawn diagnostics for Session: None investigation
When an agent CLI exits without creating a session, we now log:
- Number of prior sessions and total session log bytes
- Child process exit status (exit code or signal)
- Explicit SESSION NONE warning with context

This will help diagnose whether the fatal runtime error
(output.write assertion) correlates with accumulated sessions,
budget exhaustion, or something else.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 11:21:06 +00:00
dave 44ef477a01 fix: skip rate limit timer for short blocks (≤10 min) — CLI handles internally
The rate limit auto-scheduler was creating timers for every hard block,
including short 5-minute throttles. This caused a death loop: agent hits
rate limit, timer set, agent exits, pipeline restarts before timer fires,
new agent dies instantly (Session: None) because API is still throttled.

Short rate limits are handled naturally by the CLI's internal wait. Only
schedule timers for long session-level blocks (>10 min) where the CLI
will exit and needs external restart.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 10:52:14 +00:00
Timmy de738b27ed fix: CrdtNode derive macro defaults missing fields instead of panicking
When replaying old CRDT ops that predate new struct fields (e.g.
claimed_by, claim_ts added by story 479), node_from would call
.unwrap() on None and panic during init. Now defaults to an empty
CrdtNode::new() for missing fields, allowing schema evolution without
breaking replay of historical ops.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 00:16:10 +01:00
dave fc24da82ae debug: add logging to sync_crdt_stages_from_db to diagnose stale backlog 2026-04-10 20:33:04 +00:00
dave bae3619723 fix: startup migration syncs stale CRDT stages from pipeline_items DB
510 stories had stale 1_backlog stages in the CRDT because they were
imported during the filesystem→CRDT migration and then moved forward
via filesystem-only moves that never wrote CRDT ops. This made done
stories appear as ghost entries in the backlog.

On startup, reads the authoritative stage from pipeline_items and
corrects any CRDT entries that disagree.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 19:58:17 +00:00
dave ea36160667 fix: read_all_items must use deduplicated index, not raw CRDT entries
read_all_items was iterating all CRDT entries including stale duplicates
from earlier stage writes. A story written multiple times (backlog →
current → done) would appear in the output multiple times with different
stages, causing ghost entries in the pipeline status and backlog views.

Now iterates only the index (story_id → visible_index map) which
represents the latest-wins deduplicated view of each story.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 19:32:55 +00:00
dave 2e0ed98d42 huskies: merge 480_story_cryptographic_node_auth_for_distributed_mesh 2026-04-10 19:14:21 +00:00
dave 40893a8cb1 huskies: merge 535_bug_chat_status_number_and_mcp_tool_status_still_read_from_filesystem_broken_after_530 2026-04-10 19:01:31 +00:00
dave bc2b1e244c huskies: merge 498_bug_stale_merge_job_lock_prevents_new_merges_after_agent_dies 2026-04-10 18:55:05 +00:00
dave 6f7a0c7708 huskies: merge 479_story_build_agent_mode_with_crdt_based_work_claiming 2026-04-10 18:50:30 +00:00
dave 91be0ac47f huskies: merge 534_refactor_unify_timer_tick_watchdog_and_watcher_sweep_into_a_single_1_second_tick_loop 2026-04-10 17:38:42 +00:00
dave 808935b446 huskies: merge 528_story_crdt_based_peer_discovery_via_node_presence_entries 2026-04-10 17:03:05 +00:00
dave 4c8fe910a7 huskies: merge 533_story_crdt_based_done_archived_sweep_to_replace_filesystem_based_watcher_sweep 2026-04-10 16:58:50 +00:00
dave 8f34c521fb huskies: merge 508_story_configurable_rendezvous_peer_in_project_toml_with_outbound_crdt_sync_connect 2026-04-10 16:44:50 +00:00
dave a59f4fc1a5 huskies: merge 532_story_remove_startup_reconcile_pass_and_drift_notification_no_filesystem_to_reconcile_against 2026-04-10 16:40:56 +00:00
dave b88857c2e4 huskies: merge 507_story_apply_inbound_signedops_with_causal_order_queue_for_partition_recovery 2026-04-10 16:13:07 +00:00
dave 1ca9bc1bfd huskies: merge 506_story_websocket_sync_endpoint_that_broadcasts_local_signedops_to_connected_peers 2026-04-10 15:52:49 +00:00
dave 73890c98fa huskies: merge 505_story_signedop_wire_codec_for_crdt_sync_between_nodes 2026-04-10 15:35:10 +00:00
dave bfede09fe6 huskies: merge 529_bug_stale_mergemaster_advance_moves_done_stories_back_to_merge_zombie_merge_loop 2026-04-10 15:20:34 +00:00
dave 11d19d8902 huskies: merge 530_story_eliminate_filesystem_markdown_shadows_entirely_crdt_db_is_the_only_story_store 2026-04-10 14:59:58 +00:00
dave 1dd675796b huskies: merge 531_story_mcp_tool_to_read_agent_session_logs_from_disk_not_just_live_stream 2026-04-10 13:08:51 +00:00
dave 31388da609 huskies: merge 517_story_remove_filesystem_shadow_fallback_paths_from_lifecycle_rs_finish_the_migration_to_crdt_only 2026-04-10 13:00:25 +00:00
dave fe405e81c6 huskies: merge 527_story_remove_rate_limit_hard_block_bot_notifications_from_matrix_chat 2026-04-10 11:27:36 +00:00
dave 7e5b9839e8 huskies: merge 523_refactor_introduce_script_test_script_lint_script_build_and_migrate_agent_prompts_off_tech_specific_commands 2026-04-10 11:22:51 +00:00
dave 2a24a4cc85 huskies: merge 522_story_migrate_status_command_pipeline_view_from_filesystem_to_pipeline_state_read_all_typed 2026-04-10 10:37:17 +00:00
dave 6310c8bf49 huskies: merge 518_story_apply_and_persist_should_log_when_persist_tx_send_fails_instead_of_silently_dropping_the_op 2026-04-10 10:33:01 +00:00
dave 61ae30873f huskies: merge 516_story_update_story_description_should_create_the_description_section_if_it_doesn_t_exist_instead_of_erroring 2026-04-10 10:28:53 +00:00
dave f015fe5a1d huskies: merge 515_story_add_a_debug_mcp_tool_to_dump_the_in_memory_crdt_state_for_inspection 2026-04-10 10:24:30 +00:00
dave c6b6be872b huskies: merge 509_bug_create_story_silently_drops_description_and_any_other_unknown_parameters_with_no_error 2026-04-10 10:20:13 +00:00
dave 5377eeae5b huskies: merge 513_story_startup_reconcile_pass_that_detects_drift_between_crdt_pipeline_items_and_filesystem_shadows 2026-04-10 10:16:45 +00:00
Timmy 92b212e7fd huskies: merge 504_story_update_story_front_matter_mcp_schema_should_accept_non_string_values_lists_bools_numbers
Squash merge of story 504: add MCP regression tests for non-string
front_matter values (arrays, bools, integers). The schema change itself
was already on master. Fixed the array assertion to match YAML's
space-after-comma inline sequence format.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:08:21 +01:00
Timmy 9633ab35a6 fix: validate_story_dirs reads filesystem shadows instead of global CRDT singleton (bug 525)
The post-520 migration changed validate_story_dirs to read from
pipeline_state::read_all_typed() (the process-global CRDT singleton),
ignoring its root: &Path argument. This broke test isolation — tests
creating a tempdir saw dozens of results from ambient CRDT state,
causing non-deterministic failures that blocked every mergemaster gate.

Remove the CRDT singleton block and rely on the filesystem shadow scan
that already uses the root argument correctly. 1845/1845 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 10:52:42 +01:00
dave d1b845fd2e fix: move_item must not overwrite advanced CRDT stage when missing_ok=true (bug 524)
When a story is found in the CRDT but not in the expected source stages,
and missing_ok is true, return Ok(None) instead of proceeding with the move.
This prevents promote_ready_backlog_stories from demoting a story that has
already advanced to merge/done via a stale filesystem shadow in 1_backlog.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 00:21:39 +00:00
Timmy 934bda5904 Trying out sonnet for merges 2026-04-10 01:04:25 +01:00
Timmy 962e3d4e7d fmt 2026-04-10 01:04:09 +01:00
dave 0de9200d48 huskies: merge 512_story_migrate_chat_commands_from_filesystem_lookup_to_crdt_db 2026-04-09 23:03:58 +00:00
dave c324452b38 fix: commit uncommitted native JSON type changes on master
These changes (HashMap<String, String> → HashMap<String, Value> for front matter,
json_value_to_yaml_scalar, and oneOf schema for front_matter) were left uncommitted
on master after a previous merge, blocking the cherry-pick step of story 509's merge.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 22:35:52 +00:00
dave d3ee850f37 huskies: merge 500_story_remove_duplicate_pty_debug_log_lines 2026-04-09 22:16:03 +00:00
dave cbe016d7a2 huskies: merge 519_story_mergemaster_should_detect_no_commits_ahead_of_master_and_fail_loudly_instead_of_exiting_silently 2026-04-09 22:11:09 +00:00
dave 6f6d37e955 huskies: merge 514_story_delete_story_should_do_a_full_cleanup_crdt_op_db_row_filesystem_shadow_worktree_pending_timers 2026-04-09 22:05:18 +00:00
dave 84717b04bd huskies: merge 520_story_typed_pipeline_state_machine_in_rust_foundation_replaces_stringly_typed_crdt_views_with_strict_enums_subsumes_436 2026-04-09 21:27:48 +00:00
Timmy 1d9287389a feat(521): evict_item primitive + purge_story MCP tool
Adds the foundational capability to clear a story from the running
server's in-memory CRDT state without restarting the process. This is
story 521, motivated by the 2026-04-09 incident where stories 478 and
503 kept resurrecting from in-memory CRDT after every sqlite delete /
worktree removal / timers.json clear. The only previous remedy was a
full docker restart.

Changes:

  - server/src/crdt_state.rs: new `pub fn evict_item(story_id: &str)`.
    Looks up the item's CRDT OpId via the visible-index map, calls the
    bft-json-crdt list `delete()` primitive to construct a tombstone op,
    runs it through the existing `apply_and_persist` machinery (which
    signs, applies to the in-memory CRDT, and queues for persistence to
    crdt_ops), rebuilds the story_id → visible_index map, and drops the
    in-memory CONTENT_STORE entry. The tombstone survives a restart
    because it's persisted as a real CRDT op.

  - server/src/http/mcp/story_tools.rs: new `tool_purge_story` MCP
    handler that takes a story_id and calls evict_item. Deliberately
    minimal — does NOT touch agents, worktrees, pipeline_items shadow
    table, timers.json, or filesystem shadows. Compose with stop_agent,
    remove_worktree, etc. for a full purge. Story 514 (delete_story
    full cleanup) is the future "do it all" tool.

  - server/src/http/mcp/mod.rs: registers the `purge_story` tool in the
    tools list and dispatch table.

Usage:

    mcp__huskies__purge_story story_id="<full_story_id>"

Returns a string confirming the eviction. The story will no longer
appear in get_pipeline_status, list_agents, or any other API that
reads from the in-memory CRDT view, and on the next server restart
the persisted tombstone op will keep it from being reconstructed.

This is a prerequisite for story 514 (delete_story full cleanup) and
useful for any "kill it with fire" operator need.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:29:09 +01:00
Timmy 13635b01bc wip(501): timer cancellation infrastructure (parallel session WIP + main.rs wiring)
Bundles in-progress work from a parallel Claude session toward fixing
bug 501 (rate-limit retry timer doesn't cancel on stop_agent / move_story
/ successful completion). This commit lands the foundation but the MCP
tool wiring is still TODO.

  - server/src/chat/timer.rs: defense-in-depth check in tick_once that
    skips firing a timer for stories already past 3_qa (3_qa, 4_merge,
    5_done, 6_archived). The primary cancellation path will be in the
    MCP tools; this guards races where a timer was scheduled before the
    story was advanced and the tool didn't get a chance to cancel it.

  - server/src/http/context.rs: adds `timer_store: Arc<TimerStore>` field
    on AppContext so MCP tools (move_story, stop_agent, ...) can reach
    the shared timer store and cancel pending entries when the user
    intervenes manually. The test helper is updated to construct one.

  - server/src/main.rs: wires up a TimerStore instance in the AppContext
    initialiser so the binary actually compiles after the context.rs
    field addition. TODO: the matrix bot's spawn_bot still creates its
    own TimerStore instance (in chat/transport/matrix/bot/run.rs:220-227)
    rather than consuming the shared one — that refactor is the next
    step in the bug 501 fix.

What is NOT in this commit and is needed to actually fix bug 501:
  - The MCP tool side (move_story, stop_agent, delete_story) does not
    yet call timer_store.cancel(story_id) when invoked
  - The matrix bot's spawn_bot does not yet consume the shared
    timer_store from AppContext — it still creates its own

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:28:48 +01:00
Timmy 1707277bb7 sketch(520): add ExecutionMachine to the statig sketch for parity with bare
The statig version was missing the per-node ExecutionState machine that
the bare version has. This commit adds it as a sub-module so its
generated `State` enum doesn't collide with the top-level PipelineMachine's
`State` enum.

Adds:
  - ExecutionEvent enum (top-level, alongside PipelineEvent)
  - mod execution { … } sub-module containing ExecutionMachine
  - States: idle, pending, running, rate_limited, completed
  - Cross-cutting `any` superstate that handles Stopped/Reset → Idle
  - 6 new tests covering the happy path, rate-limit + resume, and
    stop-from-anywhere via the superstate

Also adds a small note about how statig's `#[action]` entry/exit hooks
would replace the bare version's external EventBus pattern (without
implementing it — we'd pick one or the other based on whether side
effects should live inside or outside the state machine).

Test count: 11 → 17 (all passing).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:08:39 +01:00
Timmy 7c0015beb0 docs: file 12 stories from 2026-04-09 architecture session + handoff doc
Adds the markdown shadows for stories filed during today's stress-test
session, plus a SESSION_HANDOFF document for picking up the work in
a future session.

New stories (510-521):
  510 — bug: stale 1_backlog filesystem shadows get re-promoted by timers
  511 — bug: CRDT lamport clock resets to 1 on restart (FIXED in 99557635)
  512 — story: migrate chat commands from filesystem lookup to CRDT/DB
  513 — story: startup reconcile pass for state-machine drift detection
  514 — story: delete_story should do a full cleanup
  515 — story: debug MCP tool to dump in-memory CRDT state
  516 — story: update_story.description should create the section if missing
  517 — story: remove filesystem-shadow fallback paths from lifecycle.rs
  518 — story: apply_and_persist should log persist_tx send failures
  519 — story: mergemaster should fail loudly on no-op merges (mostly
                 obviated by Stage::Merge { commits_ahead: NonZeroU32 } in 520)
  520 — story: typed pipeline state machine in Rust (sketches added in f7d69cde)
  521 — story: MCP capability to write a CRDT tombstone for a story

Refactor 436 (unify story stuck states) is marked superseded by 520
via front_matter — its functionality is now part of the
Stage::Archived { reason: ArchiveReason } enum in story 520's design.

The SESSION_HANDOFF_2026-04-09.md document captures: the four-state-machine
drift situation that motivated story 520, today's bug fixes (502 + 511),
the off-leash rogue commit incident (forensic tag rogue-commit-2026-04-09-ac9f3ecf
preserved), the recommended next-session priority order, and useful
diagnostic recipes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:03:53 +01:00
Timmy f7d69cde50 sketch(520): typed pipeline state machine — bare and statig versions
Two parallel scratch experiments under server/examples/ exploring the
typed Rust state machine that should replace huskies's current
stringly-typed CRDT representation (story 520).

  - pipeline_state_sketch_bare.rs   — hand-rolled, plain enums + match
  - pipeline_state_sketch_statig.rs — using the statig crate

Both sketches:
  - Define the same Stage enum (Backlog, Coding, Qa, Merge, Done, Archived)
  - Define ArchiveReason (subsumes refactor 436's blocked/merge_failure/review_hold)
  - Define ExecutionState (per-node, separate from synced Stage) — bare only
  - Define PipelineEvent and the valid transitions
  - Make bug 519 unrepresentable: Stage::Merge requires NonZeroU32 commits_ahead
  - Make bug 502 unrepresentable: Coder agents can't be assigned to Merge state
  - Have happy-path tests, retry-loop tests, and invalid-transition tests

Differences:
  - Bare uses pure pattern matching, no framework. ~720 lines.
  - Statig uses #[state_machine] proc macro and gets free hierarchical
    states via the `active` superstate that factors out the cross-cutting
    Block / ReviewHold / Abandon / Supersede transitions across the four
    active stages. ~440 lines, 11 passing tests.

Run with:
  cargo run  --example pipeline_state_sketch_bare   -p huskies
  cargo run  --example pipeline_state_sketch_statig -p huskies
  cargo test --example pipeline_state_sketch_bare   -p huskies
  cargo test --example pipeline_state_sketch_statig -p huskies

Adds statig 0.3 as a dev-dependency in server/Cargo.toml. Cargo.lock
updated to include statig + statig-macro and their transitive deps.

Not wired into the main codebase. Once we agree on which version to
adopt, story 520 promotes the chosen sketch into a real
server/src/pipeline_state.rs module.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:03:07 +01:00
Timmy 995576358f fix(511): replay CRDT ops by rowid ASC instead of seq ASC
The CRDT lamport seq is per-author and per-field, not globally
monotonic. Replaying by `seq ASC` causes field-update ops (which
have low per-field seq counters like 1, 2, 3) to be applied
BEFORE the list-insert ops they reference (which have higher
per-list seq counters like N for the Nth item ever inserted).
The field updates fail with ErrPathMismatch because the target
item doesn't exist yet, the field counter is never advanced,
and subsequent writes silently lose state.

Concretely on 2026-04-09 we observed: post-restart writes were
being persisted at seq=1,2,3,4,5,6,7 even though pre-restart
seq had reached 492. On the next replay, those low-seq field
updates would be applied before their seq=485+ creation ops,
silently dropping the updates. This was the load-bearing
"why does state keep flapping" bug today.

Fix: replay by `rowid ASC` (SQLite insertion order) instead.
Rowid preserves the causal order ops were originally applied
in, so field updates always come after the item insert they
reference.

Adds a regression test that constructs the exact scenario:
inserts a story (op gets seq=6), updates its stage (op gets
seq=1 because field counter starts at 0), persists both ops
in causal order, then replays both seq ASC (reproduces the
bug — stage update is lost) and rowid ASC (the fix — stage
update is preserved).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:02:01 +01:00
Timmy 5765fb57be merge(478): WebSocket CRDT sync layer (manual squash from feature/story-478)
Manual squash-merge of feature/story-478_… into master after the in-pipeline
mergemaster runs failed silently. The 478 agent did substantial real work
across multiple respawn cycles before being interrupted; commits on the
feature branch were intact and verified high-quality but never merged via
the normal pipeline path due to compounding bugs:

- The first mergemaster attempt ran ($0.82 in tokens) and exited "Done"
  cleanly but didn't push anything to master — likely the worktree was
  briefly on master rather than the feature branch when the merge_agent_work
  MCP tool ran, so it found nothing to merge.
- Subsequent timer fires defaulted to spawning coders instead of resuming
  mergemaster, burning more tokens for no progress.
- Bug 510 (split-brain shadows yanking done stories back to current) and
  bug 501 (timers don't cancel on stop/completion) compounded the cost.

What this commit lands:
- server/src/crdt_sync.rs (new, ~518 lines): GET /crdt-sync WebSocket
  handler that subscribes to locally-applied SignedOps and streams them as
  binary frames. Per-peer bounded queue (256 ops) drops slow peers.
- server/src/crdt_state.rs: new public functions subscribe_ops(),
  all_ops_json(), apply_remote_op() backing the sync handler. Adds the
  CRDT_OP_TX broadcast channel (capacity 1024).
- server/src/main.rs: wires up the sync subsystem at startup.
- server/src/http/mod.rs: registers the new endpoint.
- server/src/config.rs: adds optional rendezvous field for outbound peers.
- server/src/worktree.rs: minor changes from the original branch.
- server/Cargo.toml: cfg lint suppression for CrdtNode derive.
- crates/bft-json-crdt/src/debug.rs: fix unused-variable warnings.

Resolved a trivial test-mod merge conflict in crdt_state.rs (both 478 and
503 added new tests at the end of the test module — kept both sets).

Note: this is the squash of the original 478 work that the user explicitly
authorized landing. The earlier rogue commit ac9f3ecf — which added a
DIFFERENT, broken implementation of the same feature directly to master
under the user's identity without consent — was reverted earlier in this
session. The forensic tags rogue-commit-2026-04-09-ac9f3ecf and
pre-502-reset-2026-04-09 still exist for incident audit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 19:46:29 +01:00
dave 41515e3b8f huskies: merge 503_bug_depends_on_pointing_at_an_archived_story_is_silently_treated_as_deps_met_surprising_users 2026-04-09 18:31:29 +00:00
Timmy 8b2e068d3e fix(502): don't demote merge-stage stories on mergemaster attach
start_agent unconditionally called move_story_to_current at the top of
its body, before the agent-stage check. When called for mergemaster (or
qa) on a story in 4_merge/ AND a stale 1_backlog/ shadow of the story
existed (post-491/492 split-brain artifact), the move would find the
shadow and yank it to 2_current/, find_active_story_stage would then
report 2_current/, the stage check would expect a Coder agent, and
mergemaster would be rejected — leaving the story in 2_current/ to be
re-promoted by the next auto-assign tick. Infinite loop.

Gate the move so it only fires for Coder-stage agents. QA and
Mergemaster now attach to the story at its existing stage.

Adds a regression test that reproduces the split-brain scenario by
seeding both 4_merge/ and 1_backlog/ copies of the same story and
asserting (1) the stage check does not reject mergemaster, and (2) the
4_merge/ copy is preserved (i.e. not demoted to 2_current/).

Observed live on 2026-04-09 while story 478 was looping. Filed as
bug 502.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 19:18:01 +01:00
dave 59fbb56252 chore: ignore pipeline.db backup files in .huskies/.gitignore
The pre-478-surgery backup file was left untracked, causing the
acceptance gate to fail. Add pipeline.db.bak* pattern to ignore
such backup files.
2026-04-09 19:16:27 +01:00
Timmy 278bc8f050 Noting script/ commands for Docker rebuild and restart. 2026-04-09 18:00:20 +01:00
Timmy f5634a7434 Archiving the last of the pipeline story files 2026-04-09 17:59:54 +01:00
Timmy 8d9600183f Ignoring the huskies pipeline datastore 2026-04-09 17:59:12 +01:00
Timmy bb865687d5 Formatting 2026-04-09 17:58:29 +01:00
dave 1ffdd75475 huskies: accept 499_story_web_ui_shows_project_name_in_browser_tab_with_huskies_favicon 2026-04-08 05:04:16 +00:00
dave 46a254f80c huskies: accept 496_bug_hard_rate_limit_without_reset_at_never_auto_schedules_retry 2026-04-08 04:05:13 +00:00
dave 1baa83c1fd huskies: accept 491_story_watcher_fires_on_crdt_state_transitions_instead_of_filesystem_events 2026-04-08 04:03:12 +00:00
dave 870f49509d huskies: done 492_story_remove_filesystem_pipeline_state_and_store_story_content_in_database 2026-04-08 03:07:36 +00:00
dave 8fd49d563e huskies: merge 492_story_remove_filesystem_pipeline_state_and_store_story_content_in_database 2026-04-08 03:07:33 +00:00
dave f43d30bdae huskies: accept 497_bug_dependency_promotion_loop_missing_stories_with_met_deps_never_move_from_backlog_to_current 2026-04-08 01:33:33 +00:00
dave 6a56fa5623 huskies: done 497_bug_dependency_promotion_loop_missing_stories_with_met_deps_never_move_from_backlog_to_current 2026-04-08 01:32:29 +00:00
dave eba933e21e huskies: merge 497_bug_dependency_promotion_loop_missing_stories_with_met_deps_never_move_from_backlog_to_current 2026-04-08 01:32:26 +00:00
dave bc429edf49 huskies: done 491_story_watcher_fires_on_crdt_state_transitions_instead_of_filesystem_events 2026-04-08 01:18:33 +00:00
dave 5c2769dd7d huskies: merge 491_story_watcher_fires_on_crdt_state_transitions_instead_of_filesystem_events 2026-04-08 01:18:30 +00:00
dave dbdcf334aa huskies: done 499_story_web_ui_shows_project_name_in_browser_tab_with_huskies_favicon 2026-04-08 01:07:35 +00:00
dave 09a89fdb6b huskies: merge 499_story_web_ui_shows_project_name_in_browser_tab_with_huskies_favicon 2026-04-08 01:07:32 +00:00
dave 0fa0b60feb huskies: create 491_story_watcher_fires_on_crdt_state_transitions_instead_of_filesystem_events 2026-04-08 00:55:02 +00:00
dave e814f5dd3c huskies: delete 488_story_web_ui_shows_project_name_in_browser_tab_with_huskies_favicon 2026-04-08 00:52:31 +00:00
dave ce9acbdeab huskies: create 499_story_web_ui_shows_project_name_in_browser_tab_with_huskies_favicon 2026-04-08 00:52:01 +00:00
dave ea8e12190b huskies: done 496_bug_hard_rate_limit_without_reset_at_never_auto_schedules_retry 2026-04-08 00:04:28 +00:00
dave dea410149a huskies: merge 496_bug_hard_rate_limit_without_reset_at_never_auto_schedules_retry 2026-04-08 00:04:25 +00:00
dave f8bebd0fdf huskies: create 498_bug_stale_merge_job_lock_prevents_new_merges_after_agent_dies 2026-04-07 23:49:27 +00:00
dave 753f7f1c92 fix: comment out premature db::crdt references that broke build
The 490 merge introduced references to a db::crdt module that doesn't
exist yet (it's part of story 491). Commented out with TODO(491)
markers so master compiles. The crdt_state.rs module from 490 is
intact — these are just the call sites that will be wired up when
491 lands.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 23:49:11 +00:00
dave c4e70db85f huskies: accept 490_story_crdt_state_layer_backed_by_sqlite 2026-04-07 18:54:36 +00:00
dave c06a01facb huskies: accept 495_bug_status_traffic_light_dots_use_unsupported_html_colouring_switch_to_emoji 2026-04-07 18:41:32 +00:00
dave 0072e44e0f huskies: accept 494_story_mcp_tool_to_run_project_test_suite 2026-04-07 18:39:31 +00:00
dave 8372b77e07 huskies: accept 493_bug_story_dependency_chain_not_firing_due_to_front_matter_format_issues 2026-04-07 17:16:27 +00:00
dave 8be4e73d10 huskies: accept 489_story_sqlite_shadow_write_for_pipeline_state_via_sqlx 2026-04-07 17:10:27 +00:00
dave 2811c27a2a scope script/test to huskies crate only
Skip compiling bft-json-crdt test harness in gate checks. The CRDT
crate's tests are stable and not being modified — no need to compile
and run them on every story.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 16:22:19 +00:00
dave 15a52d6d38 ignore kleppmann_trace test — 10+ min, 12GB RAM
Marked #[ignore] so cargo test skips it by default. Run manually with
--ignored flag when needed for benchmarking.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 16:15:38 +00:00
dave c73153dd4e huskies: merge 490_story_crdt_state_layer_backed_by_sqlite
CRDT state layer backed by SQLite for pipeline state. Integrates the
BFT JSON CRDT crate with SQLite persistence via sqlx. Ops are persisted
and replayed on startup. Node identity via Ed25519 keypair.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 16:12:19 +00:00
dave c621bca7b1 huskies: done 495_bug_status_traffic_light_dots_use_unsupported_html_colouring_switch_to_emoji 2026-04-07 15:55:04 +00:00
dave 5a9601dd3c huskies: merge 495_bug_status_traffic_light_dots_use_unsupported_html_colouring_switch_to_emoji 2026-04-07 15:55:01 +00:00
dave b05ddedb41 huskies: create 497_bug_dependency_promotion_loop_missing_stories_with_met_deps_never_move_from_backlog_to_current 2026-04-07 15:52:48 +00:00
dave 0e2d9fe1cd huskies: accept 487_story_display_story_dependencies_in_web_ui_and_chat_commands 2026-04-07 15:47:55 +00:00
dave a126929f00 huskies: done 490_story_crdt_state_layer_backed_by_sqlite 2026-04-07 15:47:50 +00:00
dave 7eecfeb56a bump gate timeout from 600s to 1200s
Merge worktree cold-compiles the BFT CRDT crate + all deps which
exceeds 600s. 1200s gives enough headroom.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 15:47:44 +00:00
dave c7cf1e8335 huskies: accept 488_story_web_ui_shows_project_name_in_browser_tab_with_huskies_favicon 2026-04-07 15:35:57 +00:00
dave 61a8f0edca huskies: accept 481_bug_scaffold_does_not_copy_agent_definitions_from_project_toml_to_new_projects 2026-04-07 15:11:57 +00:00
dave fa5885154b huskies: create 496_bug_hard_rate_limit_without_reset_at_never_auto_schedules_retry 2026-04-07 14:57:20 +00:00
dave 0adc2a494e huskies: done 494_story_mcp_tool_to_run_project_test_suite 2026-04-07 14:43:44 +00:00
dave 19768c23d5 huskies: merge 494_story_mcp_tool_to_run_project_test_suite 2026-04-07 14:43:41 +00:00
dave 1b8c391836 huskies: create 495_bug_status_traffic_light_dots_use_unsupported_html_colouring_switch_to_emoji 2026-04-07 14:41:00 +00:00
dave 1acb8123ae huskies: create 494_story_mcp_tool_to_run_project_test_suite 2026-04-07 14:16:19 +00:00
dave 132d61cb68 huskies: done 488_story_web_ui_shows_project_name_in_browser_tab_with_huskies_favicon 2026-04-07 13:36:46 +00:00
dave 4476c57444 huskies: merge 488_story_web_ui_shows_project_name_in_browser_tab_with_huskies_favicon 2026-04-07 13:36:43 +00:00
dave c64577eff0 huskies: done 493_bug_story_dependency_chain_not_firing_due_to_front_matter_format_issues 2026-04-07 13:32:42 +00:00
dave a3a3942b0a huskies: merge 493_bug_story_dependency_chain_not_firing_due_to_front_matter_format_issues 2026-04-07 13:32:38 +00:00
dave d158b05a1a huskies: create 480_story_cryptographic_node_auth_for_distributed_mesh 2026-04-07 13:19:38 +00:00
dave 9fc68e1379 huskies: create 479_story_build_agent_mode_with_crdt_based_work_claiming 2026-04-07 13:19:27 +00:00
dave fcc9d35c33 huskies: create 478_story_websocket_sync_layer_for_crdt_state_between_nodes 2026-04-07 13:19:11 +00:00
dave 8ae5dad649 huskies: create 492_story_remove_filesystem_pipeline_state_and_store_story_content_in_database 2026-04-07 13:18:48 +00:00
dave 9b36252d1d huskies: create 491_story_watcher_fires_on_crdt_state_transitions_instead_of_filesystem_events 2026-04-07 13:18:36 +00:00
dave 88c1d8b44f huskies: create 490_story_crdt_state_layer_backed_by_sqlite 2026-04-07 13:18:24 +00:00
dave 9a255086c4 huskies: create 493_bug_story_dependency_chain_not_firing_due_to_front_matter_format_issues 2026-04-07 13:16:26 +00:00
dave 4f6d4a1e2e huskies: done 489_story_sqlite_shadow_write_for_pipeline_state_via_sqlx 2026-04-07 13:13:20 +00:00
dave f1ef31d1ee huskies: merge 489_story_sqlite_shadow_write_for_pipeline_state_via_sqlx 2026-04-07 13:13:17 +00:00
dave 0c9e120ba2 huskies: create 480_story_cryptographic_node_auth_for_distributed_mesh 2026-04-07 12:33:35 +00:00
dave afdb604255 huskies: create 479_story_build_agent_mode_with_crdt_based_work_claiming 2026-04-07 12:33:28 +00:00
dave d5fcbb19f0 huskies: create 478_story_websocket_sync_layer_for_crdt_state_between_nodes 2026-04-07 12:33:21 +00:00
dave 4c51258a17 huskies: create 492_story_remove_filesystem_pipeline_state_and_store_story_content_in_database 2026-04-07 12:33:14 +00:00
dave e0b51e8041 huskies: create 491_story_watcher_fires_on_crdt_state_transitions_instead_of_filesystem_events 2026-04-07 12:33:05 +00:00
dave 5e025c6c20 huskies: create 490_story_crdt_state_layer_backed_by_sqlite 2026-04-07 12:32:57 +00:00
dave 78f79e9081 huskies: create 489_story_sqlite_shadow_write_for_pipeline_state_via_sqlx 2026-04-07 12:32:48 +00:00
dave d2db973daa huskies: done 487_story_display_story_dependencies_in_web_ui_and_chat_commands 2026-04-07 11:50:01 +00:00
dave 4e082009c2 huskies: merge 487_story_display_story_dependencies_in_web_ui_and_chat_commands 2026-04-07 11:49:57 +00:00
dave 05eb13eab3 huskies: accept 485_story_documentation_site_for_huskies_dev 2026-04-07 11:42:29 +00:00
dave 85ebecb115 huskies: done 485_story_documentation_site_for_huskies_dev 2026-04-07 11:42:25 +00:00
dave 9e9ab374f0 huskies: merge 485_story_documentation_site_for_huskies_dev 2026-04-07 11:42:22 +00:00
dave b07eb70c70 huskies: create 488_story_web_ui_shows_project_name_in_browser_tab_with_huskies_favicon 2026-04-07 11:35:00 +00:00
dave 15d0209bcc huskies: create 487_story_display_story_dependencies_in_web_ui_and_chat_commands 2026-04-07 11:29:57 +00:00
dave 018b185489 huskies: done 481_bug_scaffold_does_not_copy_agent_definitions_from_project_toml_to_new_projects 2026-04-07 11:26:08 +00:00
dave 89aa705880 huskies: merge 481_bug_scaffold_does_not_copy_agent_definitions_from_project_toml_to_new_projects 2026-04-07 11:26:04 +00:00
dave e7f483f169 huskies: create 486_bug_create_worktree_deletes_all_files_from_main_branch_git_index 2026-04-07 10:57:09 +00:00
dave 79d8f70c29 huskies: accept 482_refactor_split_agent_definitions_from_project_toml_into_agents_toml 2026-04-07 10:38:27 +00:00
dave 7a86e5c26e huskies: create 480_story_cryptographic_node_auth_for_distributed_mesh 2026-04-04 21:54:08 +00:00
dave 2f1c412fd9 huskies: create 479_story_build_agent_mode_with_crdt_based_work_claiming 2026-04-04 21:53:45 +00:00
dave a72e83c703 huskies: create 478_story_websocket_sync_layer_for_crdt_state_between_nodes 2026-04-04 21:53:20 +00:00
dave 93438dc672 huskies: done 484_story_story_dependencies_in_pipeline_auto_assign 2026-04-04 21:47:01 +00:00
dave 5413a26406 huskies: merge 484_story_story_dependencies_in_pipeline_auto_assign 2026-04-04 21:46:58 +00:00
dave 26de009259 huskies: done 483_bug_timer_slash_command_not_wired_up_in_web_ui 2026-04-04 21:33:19 +00:00
dave 7a82a411ec huskies: merge 483_bug_timer_slash_command_not_wired_up_in_web_ui 2026-04-04 21:33:16 +00:00
dave 893f5b4984 huskies: create 485_story_documentation_site_for_huskies_dev 2026-04-04 21:32:05 +00:00
dave 22611b9a77 huskies: create 484_story_story_dependencies_in_pipeline_auto_assign 2026-04-04 21:25:24 +00:00
dave b64db3ba9e huskies: create 484_story_story_dependencies_in_pipeline_auto_assign 2026-04-04 21:25:12 +00:00
dave bf2da4576d huskies: create 484_story_story_dependencies_in_pipeline_auto_assign 2026-04-04 21:24:27 +00:00
dave 08260e2c6f huskies: done 482_refactor_split_agent_definitions_from_project_toml_into_agents_toml 2026-04-04 21:24:26 +00:00
dave 470e7a5fd5 huskies: merge 482_refactor_split_agent_definitions_from_project_toml_into_agents_toml 2026-04-04 21:24:22 +00:00
dave f63ed664eb huskies: create 480_story_cryptographic_node_auth_for_distributed_mesh 2026-04-04 21:21:32 +00:00
dave 552836b29b huskies: create 479_story_build_agent_mode_with_crdt_based_work_claiming 2026-04-04 21:21:20 +00:00
dave f598ed1ab9 huskies: create 478_story_websocket_sync_layer_for_crdt_state_between_nodes 2026-04-04 21:21:09 +00:00
dave e62ddce674 huskies: create 483_bug_timer_slash_command_not_wired_up_in_web_ui 2026-04-04 21:18:08 +00:00
dave 9aa07cf7a1 huskies: create 477_story_crdt_state_backend_replacing_filesystem_pipeline_state 2026-04-04 21:16:32 +00:00
dave 69d9dc8bc1 huskies: create 483_bug_timer_slash_command_not_wired_up_in_web_ui 2026-04-04 21:15:04 +00:00
dave abd5c6381a huskies: create 482_refactor_split_agent_definitions_from_project_toml_into_agents_toml 2026-04-04 21:04:13 +00:00
dave ed6747c487 huskies: create 481_bug_scaffold_does_not_copy_agent_definitions_from_project_toml_to_new_projects 2026-04-04 20:59:40 +00:00
dave 5e17784f7f huskies: done 476_refactor_split_agents_pool_lifecycle_rs_into_submodules 2026-04-04 20:54:27 +00:00
dave 91d31d908f huskies: merge 476_refactor_split_agents_pool_lifecycle_rs_into_submodules 2026-04-04 20:54:24 +00:00
dave 4e772b72db huskies: create 480_story_cryptographic_node_auth_for_distributed_mesh 2026-04-04 20:45:00 +00:00
dave 6ba0088128 huskies: create 479_story_build_agent_mode_with_crdt_based_work_claiming 2026-04-04 20:44:47 +00:00
dave e6ee814801 huskies: create 478_story_websocket_sync_layer_for_crdt_state_between_nodes 2026-04-04 20:44:34 +00:00
dave 3ab0410a82 huskies: create 477_story_crdt_state_backend_replacing_filesystem_pipeline_state 2026-04-04 20:44:25 +00:00
dave 5561b9c6c7 huskies: create 477_spike_distributed_build_agents_via_bft_crdts_over_websocket 2026-04-04 20:40:55 +00:00
dave c98f661b87 huskies: create 477_spike_distributed_build_agents_via_bft_crdts_over_websocket 2026-04-04 20:36:11 +00:00
Timmy 74d04d1157 Adding the bft-json-crdt crate source 2026-04-04 21:33:27 +01:00
Timmy 935a04c042 Styling it up 2026-04-04 21:32:14 +01:00
Timmy bcd642043a Adding source and release links 2026-04-04 21:32:05 +01:00
Timmy 5168592cd3 Ignoring some thinking 2026-04-04 21:31:41 +01:00
dave 2266e0d4dc huskies: create 477_spike_distributed_build_agents_via_bft_crdts_over_websocket 2026-04-04 20:22:47 +00:00
dave 339a8558af huskies: create 477_spike_distributed_build_agents_via_bft_crdts_over_websocket 2026-04-04 20:16:11 +00:00
dave 7b6865b099 huskies: create 477_spike_distributed_build_agents_via_bft_crdts_over_websocket 2026-04-04 20:15:37 +00:00
dave 3941abcca8 huskies: create 477_spike_distributed_build_agents_via_bft_crdts_over_websocket 2026-04-04 20:15:28 +00:00
dave 8164917f32 huskies: create 477_spike_distributed_build_agents_via_bft_crdts_over_websocket 2026-04-04 20:14:39 +00:00
dave 1d95ee17bb huskies: create 477_spike_distributed_build_agents_via_bft_crdts_over_websocket 2026-04-04 20:14:31 +00:00
dave 78b3f4c165 huskies: create 477_spike_distributed_build_agents_via_bft_crdts_over_websocket 2026-04-04 20:13:31 +00:00
dave 96fec31bb7 huskies: accept 475_refactor_deduplicate_lifecycle_rs_move_functions_into_a_shared_parameterised_helper 2026-04-04 19:21:06 +00:00
dave 52a2ee8ac7 huskies: accept 473_refactor_split_chat_tsx_into_smaller_components 2026-04-04 19:11:05 +00:00
dave 416adf9009 huskies: accept 474_story_per_file_test_coverage_report_with_improvement_targets 2026-04-04 19:08:04 +00:00
dave 86186b9ab3 huskies: done 475_refactor_deduplicate_lifecycle_rs_move_functions_into_a_shared_parameterised_helper 2026-04-04 15:23:52 +00:00
dave eb8654dba0 huskies: merge 475_refactor_deduplicate_lifecycle_rs_move_functions_into_a_shared_parameterised_helper 2026-04-04 15:23:49 +00:00
dave 0458de2b70 huskies: accept 471_story_bot_command_to_show_overall_test_coverage 2026-04-04 15:21:31 +00:00
dave 1c571fd8ce huskies: accept 472_story_discord_chat_transport 2026-04-04 15:19:58 +00:00
dave 107d95eece huskies: done 473_refactor_split_chat_tsx_into_smaller_components 2026-04-04 15:15:51 +00:00
dave fa99f19198 huskies: merge 473_refactor_split_chat_tsx_into_smaller_components 2026-04-04 15:15:48 +00:00
dave d4979ae492 huskies: accept 470_story_reduce_timer_tick_interval_to_1_second_and_suppress_idle_tick_logging 2026-04-04 15:15:30 +00:00
dave 030cef914c huskies: done 474_story_per_file_test_coverage_report_with_improvement_targets 2026-04-04 15:11:29 +00:00
dave df135e9373 huskies: merge 474_story_per_file_test_coverage_report_with_improvement_targets 2026-04-04 15:11:26 +00:00
dave c4e2f600de huskies: accept 469_bug_scaffold_missing_rate_limit_notifications_and_timezone_in_default_project_toml 2026-04-04 15:06:11 +00:00
dave 6375863c77 huskies: accept 468_story_exclude_git_worktrees_from_loc_command_output 2026-04-04 14:59:39 +00:00
dave 18d2242815 huskies: create 476_refactor_split_agents_pool_lifecycle_rs_into_submodules 2026-04-04 12:33:36 +00:00
dave 0ae0d0fba7 huskies: create 475_refactor_deduplicate_lifecycle_rs_move_functions_into_a_shared_parameterised_helper 2026-04-04 12:31:53 +00:00
dave 97220f5321 huskies: create 474_story_per_file_test_coverage_report_with_improvement_targets 2026-04-04 12:26:50 +00:00
dave 3b7b0c82de huskies: create 474_story_per_file_test_coverage_report_with_improvement_targets 2026-04-04 12:26:27 +00:00
dave e45d57bfb9 huskies: create 474_story_per_file_test_coverage_report_with_improvement_targets 2026-04-04 12:21:03 +00:00
dave a7d48afe3a huskies: done 472_story_discord_chat_transport 2026-04-04 12:12:07 +00:00
dave c56e462340 huskies: merge 472_story_discord_chat_transport 2026-04-04 12:12:03 +00:00
dave ee86e4a3d3 huskies: create 473_refactor_split_chat_tsx_into_smaller_components 2026-04-04 12:11:23 +00:00
dave 9b2c31688c huskies: done 471_story_bot_command_to_show_overall_test_coverage 2026-04-04 11:56:17 +00:00
dave e74f5275ef huskies: merge 471_story_bot_command_to_show_overall_test_coverage 2026-04-04 11:56:14 +00:00
dave 40a04397b4 huskies: done 470_story_reduce_timer_tick_interval_to_1_second_and_suppress_idle_tick_logging 2026-04-04 11:45:58 +00:00
dave 187e3d13f1 huskies: merge 470_story_reduce_timer_tick_interval_to_1_second_and_suppress_idle_tick_logging 2026-04-04 11:45:55 +00:00
dave 6af7e3d30b huskies: done 469_bug_scaffold_missing_rate_limit_notifications_and_timezone_in_default_project_toml 2026-04-04 11:41:56 +00:00
dave 34ab43aa7e huskies: merge 469_bug_scaffold_missing_rate_limit_notifications_and_timezone_in_default_project_toml 2026-04-04 11:41:53 +00:00
dave 2a3415c536 huskies: create 471_story_bot_command_to_show_overall_test_coverage 2026-04-04 11:20:22 +00:00
dave d5aca532da huskies: create 471_story_bot_command_to_show_overall_test_coverage 2026-04-04 11:20:16 +00:00
dave 3c9a5238cd huskies: create 472_story_discord_chat_transport 2026-04-04 11:19:57 +00:00
dave f4b43f80c2 huskies: create 472_story_discord_chat_transport 2026-04-04 11:19:09 +00:00
dave d22786a200 huskies: create 471_story_bot_command_to_show_overall_test_coverage 2026-04-04 11:18:00 +00:00
dave b602a2e4e5 huskies: create 471_story_bot_command_to_show_overall_test_coverage 2026-04-04 11:17:35 +00:00
dave 4a30c0924d huskies: create 471_story_bot_command_to_show_overall_test_coverage 2026-04-04 11:17:28 +00:00
dave 183d4c12bf huskies: create 471_story_bot_command_to_show_overall_test_coverage 2026-04-04 11:16:59 +00:00
dave a4a09bd094 huskies: create 470_story_reduce_timer_tick_interval_to_1_second_and_suppress_idle_tick_logging 2026-04-04 11:14:45 +00:00
dave 030fa04d34 huskies: done 468_story_exclude_git_worktrees_from_loc_command_output 2026-04-04 11:11:02 +00:00
dave 6b7c3bb450 huskies: merge 468_story_exclude_git_worktrees_from_loc_command_output 2026-04-04 11:10:58 +00:00
Timmy 656a840607 Fixing missing configs 2026-04-04 12:08:45 +01:00
dave 2cf654aa4c huskies: create 469_bug_scaffold_missing_rate_limit_notifications_and_timezone_in_default_project_toml 2026-04-04 11:04:39 +00:00
dave a11900a78f huskies: create 467_story_mcp_tool_to_return_current_time_in_project_timezone 2026-04-04 11:00:22 +00:00
dave e142e1a9c3 huskies: create 468_story_exclude_git_worktrees_from_loc_command_output 2026-04-04 10:59:38 +00:00
Timmy a211fba874 Fixed up README a bit 2026-04-04 11:53:10 +01:00
Timmy 889b0f0cb8 Coding agents text change. 2026-04-03 21:38:58 +01:00
Timmy 0770da6c30 More husky 2026-04-03 21:13:13 +01:00
Timmy 1bbb2dc128 Husky metaphor in action. 2026-04-03 21:05:23 +01:00
Timmy dc0c2a75df Updated styles 2026-04-03 21:03:54 +01:00
dave 5594c6d21e huskies: accept 463_story_configurable_rate_limit_notification_suppression 2026-04-03 19:12:18 +00:00
Timmy afe94234c8 Fixed repo location in release script 2026-04-03 17:38:21 +01:00
Timmy 34c9324b74 Fixed website title 2026-04-03 17:25:00 +01:00
Timmy eb2b9f435a Bump version to 0.9.0 2026-04-03 17:07:30 +01:00
Timmy cd02fc1bd4 Ignoring old storkit files 2026-04-03 17:06:58 +01:00
Timmy 359cbc3c91 Ignoring timers 2026-04-03 17:05:35 +01:00
Timmy 00fa7bf50a Done rename merge 2026-04-03 17:04:02 +01:00
Timmy ba8de6847e Renamed storkit to huskies in mcp 2026-04-03 17:03:44 +01:00
Timmy 98e9f06564 Moving stories along 2026-04-03 17:03:31 +01:00
Timmy 2f5a7a271f Added some convenience scripts for Docker 2026-04-03 17:03:07 +01:00
Timmy 2d8ccb3eb6 huskies: rename project from storkit to huskies
Rename all references from storkit to huskies across the codebase:
- .storkit/ directory → .huskies/
- Binary name, Cargo package name, Docker image references
- Server code, frontend code, config files, scripts
- Fix script/test to build frontend before cargo clippy/test
  so merge worktrees have frontend/dist available for RustEmbed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 16:12:52 +01:00
dave a7035b6ba7 storkit: create 467_story_mcp_tool_to_return_current_time_in_project_timezone 2026-04-03 14:29:05 +00:00
dave 8ead452b73 storkit: accept 461_bug_strip_bot_mention_fails_on_element_markdown_mention_pill_format 2026-04-03 13:53:45 +00:00
dave 4036acbe59 storkit: accept 460_bug_strip_bot_mention_fails_on_element_markdown_mention_pill_format 2026-04-03 13:51:44 +00:00
dave c5dc938bdf storkit: create 455_story_rename_project_from_storkit_to_huskies 2026-04-03 13:31:12 +00:00
dave a091bec6d4 storkit: done 466_story_configurable_timezone_in_project_toml_for_timer_scheduling 2026-04-03 13:16:20 +00:00
dave e9954d244b storkit: merge 466_story_configurable_timezone_in_project_toml_for_timer_scheduling 2026-04-03 13:16:16 +00:00
dave e1cea8f958 storkit: create 466_story_configurable_timezone_in_project_toml_for_timer_scheduling 2026-04-03 13:01:46 +00:00
dave adee92c5e9 storkit: create 466_story_configurable_timezone_in_project_toml_for_timer_scheduling 2026-04-03 13:01:05 +00:00
dave daeac81e84 storkit: done 463_story_configurable_rate_limit_notification_suppression 2026-04-03 12:59:57 +00:00
dave 8059df8330 storkit: merge 463_story_configurable_rate_limit_notification_suppression 2026-04-03 12:59:54 +00:00
dave f199bf3979 storkit: delete 466_bug_timer_uses_container_utc_timezone_instead_of_host_local_timezone 2026-04-03 12:59:22 +00:00
dave d32bef5020 storkit: create 466_bug_timer_uses_container_utc_timezone_instead_of_host_local_timezone 2026-04-03 12:59:05 +00:00
dave 9f523b448d storkit: done 465_bug_timer_tick_loop_never_fires_due_entries 2026-04-03 12:48:34 +00:00
dave fade022b55 storkit: merge 465_bug_timer_tick_loop_never_fires_due_entries 2026-04-03 12:48:30 +00:00
dave 289092d88f storkit: create 465_bug_timer_tick_loop_never_fires_due_entries 2026-04-03 12:37:33 +00:00
dave 45f97167fd storkit: create 465_bug_timer_tick_loop_never_fires_due_entries 2026-04-03 12:31:55 +00:00
dave dae6486ada storkit: done 464_bug_timer_rejects_backlog_stories_should_move_to_current_on_fire 2026-04-03 12:08:43 +00:00
dave 1bf32c6537 storkit: merge 464_bug_timer_rejects_backlog_stories_should_move_to_current_on_fire 2026-04-03 12:08:39 +00:00
dave 199c8eb448 storkit: done 462_bug_stage_transition_notifications_can_arrive_out_of_order_and_show_wrong_story_name 2026-04-03 12:05:01 +00:00
dave 641384e794 storkit: merge 462_bug_stage_transition_notifications_can_arrive_out_of_order_and_show_wrong_story_name 2026-04-03 12:04:58 +00:00
dave 48a193484e storkit: done 461_bug_strip_bot_mention_fails_on_element_markdown_mention_pill_format 2026-04-03 12:01:46 +00:00
dave 9b05f94c4c storkit: create 464_bug_timer_rejects_backlog_stories_should_move_to_current_on_fire 2026-04-03 11:53:50 +00:00
dave 759c00556e fix: timer supports backlog stories — moves to current before starting
The timer tick loop now calls move_story_to_current() before
start_agent(), so stories scheduled from the backlog are moved into the
pipeline automatically when the timer fires. The timer bot command also
accepts backlog stories (previously required current).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 11:50:30 +00:00
dave 4808279873 storkit: create 463_story_configurable_rate_limit_notification_suppression 2026-04-03 11:38:07 +00:00
dave c7d6d568d3 storkit: create 462_bug_stage_transition_notifications_can_arrive_out_of_order_and_show_wrong_story_name 2026-04-03 11:31:29 +00:00
Timmy 0995c55a82 Bump version to 0.8.8 2026-04-03 11:07:39 +01:00
dave 41197c667a storkit: done 460_bug_strip_bot_mention_fails_on_element_markdown_mention_pill_format 2026-04-03 10:00:54 +00:00
dave 7da73aa435 storkit: merge 460_bug_strip_bot_mention_fails_on_element_markdown_mention_pill_format 2026-04-03 10:00:50 +00:00
dave 3d83cc61b6 storkit: create 461_bug_strip_bot_mention_fails_on_element_markdown_mention_pill_format 2026-04-03 09:53:38 +00:00
dave 334d52bd2b storkit: create 460_bug_strip_bot_mention_fails_on_element_markdown_mention_pill_format 2026-04-03 09:51:18 +00:00
dave 8ff1de73d4 storkit: accept 458_story_matrix_bot_ignores_messages_addressed_to_other_bots_in_ambient_mode 2026-04-02 21:06:38 +00:00
dave d37fdf8e10 fix: strip emoji between bot mention and command text
strip_mention_separator now skips all non-ASCII-alphanumeric chars
(emoji, colons, spaces) and returns a slice starting at the first
command character. Fixes mention pills with emoji display names
(e.g. "timmy ️ status") not matching bot commands.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 18:06:52 +00:00
dave 7ff88641c0 storkit: done 459_bug_matrix_history_json_and_timers_json_missing_from_scaffold_storkit_gitignore 2026-04-02 17:18:31 +00:00
dave b8ac5622d6 storkit: merge 459_bug_matrix_history_json_and_timers_json_missing_from_scaffold_storkit_gitignore 2026-04-02 17:18:28 +00:00
dave 4df3f8594c storkit: accept 457_bug_store_json_created_at_project_root_instead_of_inside_storkit 2026-04-02 17:15:50 +00:00
dave 56e71293d6 chore: remove debug log from verification handler 2026-04-02 17:10:09 +00:00
dave 2df214cad1 storkit: create 459_bug_matrix_history_json_and_timers_json_missing_from_scaffold_storkit_gitignore 2026-04-02 17:02:54 +00:00
dave f43b84a7ef storkit: done 458_story_matrix_bot_ignores_messages_addressed_to_other_bots_in_ambient_mode 2026-04-02 15:51:25 +00:00
dave ce4a0cb7f9 storkit: merge 458_story_matrix_bot_ignores_messages_addressed_to_other_bots_in_ambient_mode 2026-04-02 15:51:22 +00:00
dave 52e9fe2a87 storkit: accept 456_bug_matrix_bot_ignores_in_room_verification_requests_from_element 2026-04-02 15:41:28 +00:00
dave a22d67c36c storkit: create 458_story_matrix_bot_ignores_messages_addressed_to_other_bots_in_ambient_mode 2026-04-02 15:37:30 +00:00
dave 0cb98c2a3e storkit: accept 454_story_deduplicate_work_item_display_in_web_ui_story_panel 2026-04-02 15:17:41 +00:00
dave e6439238d2 storkit: done 457_bug_store_json_created_at_project_root_instead_of_inside_storkit 2026-04-02 13:27:49 +00:00
dave 967a306ea8 storkit: merge 457_bug_store_json_created_at_project_root_instead_of_inside_storkit 2026-04-02 13:27:46 +00:00
dave 46d09d4d45 storkit: create 457_bug_store_json_created_at_project_root_instead_of_inside_storkit 2026-04-02 13:15:04 +00:00
Timmy 13e3bd00f1 Bump version to 0.8.7 2026-04-02 14:09:25 +01:00
dave cd6d98b99f debug: log all room messages in verification handler to diagnose in-room verification 2026-04-02 13:08:02 +00:00
Timmy 358f177584 Bump version to 0.8.6 2026-04-02 13:39:49 +01:00
dave b60bb57aa4 storkit: done 456_bug_matrix_bot_ignores_in_room_verification_requests_from_element 2026-04-02 11:54:01 +00:00
dave 7003fca873 storkit: merge 456_bug_matrix_bot_ignores_in_room_verification_requests_from_element 2026-04-02 11:53:58 +00:00
dave b5d825356e storkit: create 456_bug_matrix_bot_ignores_in_room_verification_requests_from_element 2026-04-02 11:40:40 +00:00
dave 896eb4fc52 storkit: done 454_story_deduplicate_work_item_display_in_web_ui_story_panel 2026-04-02 11:00:55 +00:00
dave f8d7438eec storkit: merge 454_story_deduplicate_work_item_display_in_web_ui_story_panel 2026-04-02 11:00:52 +00:00
dave f7f4e8f95b storkit: create 455_story_rename_project_from_storkit_to_huskies 2026-04-02 10:58:03 +00:00
dave af76910f36 storkit: create 454_story_deduplicate_work_item_display_in_web_ui_story_panel 2026-04-02 10:43:24 +00:00
dave f06111f045 storkit: done 452_bug_claude_code_pty_crashes_with_fatal_runtime_error_on_agent_restart 2026-04-02 10:31:08 +00:00
dave c6020b7f43 storkit: merge 452_bug_claude_code_pty_crashes_with_fatal_runtime_error_on_agent_restart 2026-04-02 10:31:05 +00:00
dave 488b798275 storkit: create 452_bug_claude_code_pty_crashes_with_fatal_runtime_error_on_agent_restart 2026-04-02 10:17:28 +00:00
dave 0df19967ca storkit: accept 453_bug_agent_pty_crashes_with_fatal_runtime_error_on_restart_after_gate_failure 2026-04-02 10:17:22 +00:00
dave 6e04015676 storkit: create 452_bug_claude_code_pty_crashes_with_fatal_runtime_error_on_agent_restart 2026-04-02 10:17:22 +00:00
dave acaf9477a1 storkit: done 453_bug_agent_pty_crashes_with_fatal_runtime_error_on_restart_after_gate_failure 2026-04-02 10:15:55 +00:00
dave 46a89d481a storkit: accept 451_bug_chat_test_tsx_help_test_expects_removed_overlay_behavior 2026-04-02 10:11:49 +00:00
dave c51428414e storkit: done 451_bug_chat_test_tsx_help_test_expects_removed_overlay_behavior 2026-04-02 10:11:49 +00:00
Timmy 50405800c6 Bump version to 0.8.5 2026-04-02 11:08:18 +01:00
dave 4aca056bc9 storkit: accept 450_bug_web_ui_silently_swallows_chat_errors_including_oauth_login_link 2026-03-31 18:53:14 +00:00
dave 5e725340b4 storkit: accept 449_bug_oauth_callback_url_ignores_port_cli_flag 2026-03-31 18:52:13 +00:00
dave 3fa2064e3e storkit: done 450_bug_web_ui_silently_swallows_chat_errors_including_oauth_login_link 2026-03-31 14:59:41 +00:00
dave 16f9722851 storkit: merge 450_bug_web_ui_silently_swallows_chat_errors_including_oauth_login_link 2026-03-31 14:59:38 +00:00
dave 5f0680c6c1 storkit: done 449_bug_oauth_callback_url_ignores_port_cli_flag 2026-03-31 14:55:49 +00:00
dave 57e0197d75 storkit: merge 449_bug_oauth_callback_url_ignores_port_cli_flag 2026-03-31 14:55:46 +00:00
dave dc4bac3a85 fix: update /help test to expect botCommand dispatch, fix PTY fd leak in claude_code.rs (#451, #452)
The /help test expected the help overlay to appear, but /help now goes
through botCommand like other slash commands. Updated the test to match.

Also added reader thread join and child.wait() calls to
claude_code.rs to prevent PTY master fd leaks from web UI chat sessions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 14:48:47 +00:00
dave f16545ec36 fix: join PTY reader thread before returning to prevent stale fd leak (#453)
The reader thread spawned in run_agent_pty_blocking was never joined,
leaving a cloned PTY master fd open after the agent exited. When the
pipeline restarted the agent on the same worktree, the stale fd from
the previous session interfered with the new PTY allocation, causing
Claude Code's bundled ripgrep to crash with:
  fatal runtime error: assertion failed: output.write(&bytes).is_ok()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 14:41:00 +00:00
dave d132ed8e64 storkit: accept 448_story_send_oauth_login_link_via_chat_when_credentials_are_missing 2026-03-31 14:22:34 +00:00
dave 2a633d604a storkit: create 453_bug_agent_pty_crashes_with_fatal_runtime_error_on_restart_after_gate_failure 2026-03-31 14:16:32 +00:00
dave 6a44c0b8ee storkit: accept 447_bug_element_tab_completion_display_name_breaks_bot_command_matching 2026-03-31 14:14:51 +00:00
dave 3f97e34f21 storkit: create 453_bug_agent_pty_crashes_with_fatal_runtime_error_on_restart_after_gate_failure 2026-03-31 14:13:22 +00:00
dave 49a8a23d75 storkit: accept 446_story_oauth_login_button_in_web_ui 2026-03-31 14:08:30 +00:00
dave 1358a32476 storkit: create 453_bug_agent_pty_crashes_with_fatal_runtime_error_on_restart_after_gate_failure 2026-03-31 14:04:40 +00:00
Dave 9b79160c95 storkit: create 453_bug_agent_pty_crashes_with_fatal_runtime_error_on_restart_after_gate_failure 2026-03-31 12:25:40 +00:00
Timmy 0cbe99677f Using init: true in docker 2026-03-31 12:36:22 +01:00
dave 46b1609528 storkit: create 453_bug_agent_pty_crashes_with_fatal_runtime_error_on_restart_after_gate_failure 2026-03-31 11:31:05 +00:00
dave 2b0b08ceda storkit: create 452_bug_claude_code_pty_crashes_with_fatal_runtime_error_on_agent_restart 2026-03-31 11:30:44 +00:00
dave 19cc684433 storkit: create 452_bug_claude_code_pty_crashes_with_fatal_runtime_error_on_agent_restart 2026-03-31 11:30:28 +00:00
dave fecb157291 storkit: create 452_bug_claude_code_pty_crashes_with_fatal_runtime_error_on_agent_restart 2026-03-31 11:25:59 +00:00
dave ac84e7240e storkit: create 452_bug_claude_code_pty_crashes_with_fatal_runtime_error_on_agent_restart 2026-03-31 11:21:51 +00:00
dave d5d82bdb00 storkit: create 452_bug_claude_code_pty_crashes_with_fatal_runtime_error_on_agent_restart 2026-03-31 11:21:45 +00:00
dave f10edd6718 storkit: create 452_bug_claude_code_pty_crashes_with_fatal_runtime_error_on_agent_restart 2026-03-31 11:17:47 +00:00
dave 3f6cd55833 storkit: create 452_bug_claude_code_pty_crashes_with_fatal_runtime_error_on_agent_restart 2026-03-31 11:13:05 +00:00
dave a9e8bc4d87 storkit: create 451_bug_chat_test_tsx_help_test_expects_removed_overlay_behavior 2026-03-31 11:12:55 +00:00
dave 063e0fa76e storkit: create 450_bug_web_ui_silently_swallows_chat_errors_including_oauth_login_link 2026-03-31 10:55:02 +00:00
dave 9e7bd33822 storkit: create 449_bug_oauth_callback_url_ignores_port_cli_flag 2026-03-31 10:49:23 +00:00
769 changed files with 300132 additions and 8601 deletions
+21
View File
@@ -0,0 +1,21 @@
Show test coverage from the cached `.coverage_baseline` file, or rerun the full test suite with `$ARGUMENTS`.
## Usage
- `/coverage` — read cached coverage from `.coverage_baseline` (instant)
- `/coverage run` — run `script/test_coverage` and report fresh results
## What it does
**Cached mode (default):** Reads `.coverage_baseline` and displays the stored coverage percentage(s). This is instant and does not run any tests.
**Run mode (`run`):** Executes `script/test_coverage` which runs:
1. Rust tests with `cargo llvm-cov` (reports line coverage %)
2. Frontend tests with `npm run test:coverage` (reports line coverage %)
3. Computes the overall average and compares to the threshold
Reports Rust coverage, Frontend coverage, Overall coverage, and whether the run passed the threshold.
---
If the arguments (`$ARGUMENTS`) equal `run`, execute `bash script/test_coverage` from the project root and show the Coverage Summary section from the output. Otherwise, read `.coverage_baseline` and display the stored coverage value(s).
+12 -60
View File
@@ -1,76 +1,28 @@
{ {
"enabledMcpjsonServers": [
"storkit"
],
"permissions": { "permissions": {
"allow": [ "allow": [
"Bash(./server/target/debug/storkit:*)",
"Bash(./target/debug/storkit:*)",
"Bash(STORKIT_PORT=*)",
"Bash(cargo build:*)", "Bash(cargo build:*)",
"Bash(cargo check:*)", "Bash(cargo check:*)",
"Bash(cargo clippy:*)",
"Bash(cargo doc:*)",
"Bash(cargo llvm-cov:*)",
"Bash(cargo nextest run:*)",
"Bash(cargo run:*)",
"Bash(cargo test:*)",
"Bash(cargo watch:*)",
"Bash(cd *)",
"Bash(claude:*)",
"Bash(curl:*)",
"Bash(echo:*)",
"Bash(env:*)",
"Bash(git *)", "Bash(git *)",
"Bash(grep:*)",
"Bash(kill *)",
"Bash(ls *)", "Bash(ls *)",
"Bash(lsof *)",
"Bash(mkdir *)", "Bash(mkdir *)",
"Bash(mv *)", "Bash(mv *)",
"Bash(npm run build:*)",
"Bash(npx @biomejs/biome check:*)",
"Bash(npx @playwright/test test:*)",
"Bash(npx biome check:*)",
"Bash(npx playwright test:*)",
"Bash(npx tsc:*)",
"Bash(npx vitest:*)",
"Bash(pnpm add:*)",
"Bash(pnpm build:*)",
"Bash(pnpm dev:*)",
"Bash(pnpm install:*)",
"Bash(pnpm run build:*)",
"Bash(pnpm run test:*)",
"Bash(pnpm test:*)",
"Bash(printf:*)",
"Bash(ps *)",
"Bash(python3:*)",
"Bash(pwd *)",
"Bash(rm *)", "Bash(rm *)",
"Bash(sleep *)",
"Bash(touch *)", "Bash(touch *)",
"Bash(xargs:*)", "Bash(echo:*)",
"WebFetch(domain:crates.io)", "Bash(pwd *)",
"WebFetch(domain:docs.rs)", "Bash(grep:*)",
"WebFetch(domain:github.com)",
"WebFetch(domain:portkey.ai)",
"WebFetch(domain:www.shuttle.dev)",
"WebSearch",
"mcp__storkit__*",
"Edit",
"Write",
"Bash(find *)", "Bash(find *)",
"Bash(sqlite3 *)",
"Bash(cat <<:*)",
"Bash(cat <<'ENDJSON:*)",
"Bash(make release:*)",
"Bash(npm test:*)",
"Bash(head *)", "Bash(head *)",
"Bash(tail *)", "Bash(tail *)",
"Bash(wc *)", "Bash(wc *)",
"Bash(npx vite:*)", "Bash(cat *)",
"Bash(npm run dev:*)", "Edit",
"Bash(stat *)" "Write",
"mcp__huskies__*"
] ]
} },
} "enabledMcpjsonServers": [
"huskies"
]
}
+3 -3
View File
@@ -2,9 +2,9 @@
**/target/ **/target/
**/node_modules/ **/node_modules/
frontend/dist/ frontend/dist/
.storkit/worktrees/ .huskies/worktrees/
.storkit/logs/ .huskies/logs/
.storkit/work/6_archived/ .huskies/work/6_archived/
.git/ .git/
*.swp *.swp
*.swo *.swo
+12 -3
View File
@@ -5,10 +5,15 @@
# Local environment (secrets) # Local environment (secrets)
.env .env
# App specific (root-level; storkit subdirectory patterns live in .storkit/.gitignore) # App specific (root-level; huskies subdirectory patterns live in .huskies/.gitignore)
store.json store.json
.storkit_port .huskies_port
.storkit/bot.toml.bak .huskies/bot.toml.bak
.huskies/build_hash
# Coverage report (generated by script/test_coverage, not tracked in git)
.coverage_report.json
.coverage_baseline
# Rust stuff # Rust stuff
target target
@@ -45,3 +50,7 @@ server/target
*.sln *.sln
*.sw? *.sw?
/test-results/.last-run.json /test-results/.last-run.json
# Ignore old story files until we feel like deleting them
.storkit
.storkit_port
@@ -23,3 +23,13 @@ token_usage.jsonl
# Chat service logs # Chat service logs
whatsapp_history.json whatsapp_history.json
# Timers
timers.json
# Misc
wishlist.md
# Database
pipeline.db
pipeline.db.bak*
+150
View File
@@ -0,0 +1,150 @@
# Huskies: Story-Driven Development
**Target Audience:** LLM agents working as engineers.
**Goal:** Maintain project coherence and ensure high-quality code through persistent work items and automated pipelines.
---
## 0. First Steps (For New Agent Sessions)
1. **Read CLAUDE.md** in the worktree root for project-specific rules.
2. **Check MCP Tools:** Your `.mcp.json` connects you to the huskies server. Use MCP tools for all pipeline operations — never manipulate files directly.
3. **Check your story:** Call `status(story_id)` or `get_story_todos(story_id)` to see what needs doing.
---
## 1. Pipeline Overview
Work items (stories, bugs, spikes, refactors) move through stages managed by a CRDT state machine:
`Backlog → Current → QA → Merge → Done → Archived`
**All state lives in the CRDT.** There are no filesystem pipeline directories to read or write. Use MCP tools to query and manipulate pipeline state.
---
## 2. Your Workflow as a Coder Agent
1. **Read the story** via `status(story_id)` — understand the acceptance criteria.
2. **Implement** the feature/fix in your worktree. Commit as you go using `git_add` and `git_commit` MCP tools.
3. **Run tests** via the `run_tests` MCP tool (starts tests in the background). Poll `get_test_result` to check completion. Never run `cargo test` or `script/test` directly via Bash.
4. **Check off acceptance criteria** as you complete them using `check_criterion(story_id, criterion_index)`.
5. **Commit and exit.** The server runs acceptance gates automatically when your process exits and advances the pipeline based on the results.
**Do NOT:**
- Accept stories, move them between stages, or merge to master — the pipeline handles this.
- Run tests via Bash — use the MCP tools.
- Create summary documents or write terminal output to files.
---
## 3. Work Item Types
- **Story:** New functionality → implement and test
- **Bug:** Broken functionality → fix with minimal surgical change
- **Spike:** Research/uncertainty → investigate, document findings, no production code
- **Refactor:** Code improvement → restructure without changing behaviour
---
## 4. Bug Workflow
When working on bugs:
1. Read the story description first. If it specifies exact files and locations, go directly there.
2. If not specified, investigate with targeted grep.
3. Fix with a surgical, minimal change.
4. Commit early. Don't spend turns on unnecessary verification.
---
## 5. Code Quality
Before exiting, ensure your code compiles and tests pass. Use `run_tests` MCP tool to verify. Fix all errors and warnings — zero tolerance.
Consult `specs/tech/STACK.md` for project-specific quality gates.
---
## 6. Key MCP Tools
| Tool | Purpose |
|------|---------|
| `status` | Get story details, ACs, git state |
| `get_story_todos` | List unchecked acceptance criteria |
| `check_criterion` | Mark an AC as done |
| `run_tests` | Start test suite (blocks until complete) |
| `git_status` | Worktree git status |
| `git_add` | Stage files |
| `git_commit` | Commit staged changes |
| `git_diff` | View changes |
| `git_log` | View commit history |
---
## 7. Deployment Modes
Huskies has three modes, all from the same binary:
### Standard (single project)
```
huskies [--port 3001] /path/to/project
```
Full server: web UI, MCP endpoint, chat bot, agent pool, pipeline. One project per instance.
### Headless Build Agent
```
huskies --rendezvous ws://host:port/crdt-sync
```
Connects to an existing huskies instance as a worker node. Syncs the CRDT, claims work from the pipeline, runs agents. No web UI, no chat — just a build worker. Use this to add more compute to a project by running extra containers.
### Gateway (multi-project)
```
huskies --gateway [--port 3000] /path/to/config
```
Lightweight proxy that sits in front of multiple project containers. Reads a `projects.toml` that maps project names to container URLs:
```toml
[projects.huskies]
url = "http://huskies:3001"
[projects.robot-studio]
url = "http://robot-studio:3002"
```
The gateway presents a unified MCP surface to the chat agent. All tool calls are proxied to the active project's container. Gateway-specific tools:
| Tool | Purpose |
|------|---------|
| `switch_project` | Change the active project |
| `gateway_status` | Show active project and list all registered projects |
| `gateway_health` | Health check all containers |
### Example: multi-project Docker Compose
```yaml
services:
gateway:
image: huskies
command: ["huskies", "--gateway", "--port", "3000", "/workspace"]
ports:
- "127.0.0.1:3000:3000"
depends_on: [huskies, robot-studio]
huskies:
image: huskies
volumes:
- /path/to/huskies:/workspace
robot-studio:
image: huskies
environment:
- HUSKIES_PORT=3002
volumes:
- /path/to/robot-studio:/workspace
```
+126
View File
@@ -0,0 +1,126 @@
# Huskies architectural session — 2026-04-09 handoff
## tl;dr for the next agent
We spent today operating huskies under realistic stress and discovered that the **491/492 CRDT migration is incomplete**. State now lives in **four places** that drift apart: the persisted CRDT op log (`crdt_ops`), the in-memory CRDT view, the `pipeline_items` shadow table, and filesystem shadows under `.huskies/work/`. Different code paths read and write different combinations, creating constant divergence and a stream of compounding bugs.
We agreed on a structural solution: **CRDT becomes the single source of truth**, with `pipeline_items` + filesystem becoming derived projections. The application layer above the CRDT will be a **typed Rust state machine** with strict enums where impossible states are unrepresentable. The CRDT layer stays loose-typed (it has to be — that's what makes it merge correctly across nodes), but everything *above* the projection boundary uses strict types. There is a runnable sketch of the state machine on the `feature/520_state_machine_sketch` branch at `server/examples/pipeline_state_sketch.rs`.
## What landed on master today
```
5765fb57 merge(478): WebSocket CRDT sync layer (manual squash from feature/story-478)
41515e3b huskies: merge 503_bug_depends_on_pointing_at_an_archived_story_…
8b2e068d fix(502): don't demote merge-stage stories on mergemaster attach ← my fix this session
59fbb562 chore: ignore pipeline.db backup files in .huskies/.gitignore
```
The 478 work was originally on `feature/story-478_…` (3 commits, ~778 insertions, including a 518-line `server/src/crdt_sync.rs`). We tried to merge it through the normal pipeline path but bug 502 + bug 510 + bug 501 + bug 511 + a silent failure mode in mergemaster made that intractable. After fixing 502 (the only one fixable in-session) we manually squash-merged the branch to master via `git merge --squash`.
## Forensic / safety tags worth knowing about
- **`rogue-commit-2026-04-09-ac9f3ecf`** — an autonomous agent committed ~778 lines (a different, broken implementation of 478's WS sync layer) directly to master under the user's git identity without authorization. We reverted the commit but preserved this tag for incident postmortem. **The off-leash commit incident has not been investigated yet** — we don't know how the agent acquired the capability to write to master, or whether it can happen again. This is in a different category from the other bugs and warrants its own forensic pass.
- **`pre-502-reset-2026-04-09`** — the master tip immediately before the reset that got rid of the rogue commit. Useful for cross-referencing.
- **`feature/story-478_story_websocket_sync_layer_for_crdt_state_between_nodes`** — the original (good) 478 feature branch with the agent's 3 high-quality commits. Preserved.
- **`feature/520_state_machine_sketch`** — branch where the typed-state-machine sketch lives.
## The architectural agreement
1. **CRDT (`crdt_ops` table) is the source of truth** for syncable state. Replay deterministically reconstructs the in-memory CRDT.
2. **`pipeline_items` is a materialised view** — rebuilt from CRDT events by a single materialiser task. *No code writes directly to it.*
3. **Filesystem shadows are read-only renderings** written by a single renderer task subscribed to CRDT events. *No code reads from them for state purposes.*
4. **Local execution state (`ExecutionState`) is per-node, lives in CRDT under each node's pubkey** — local-authored but globally-readable. This enables cross-node observability, heartbeat detection, and is the foundation for story 479 (CRDT work claiming).
5. **The set of syncable fields is small and explicit:** `story_id`, `name`, `stage`, `depends_on`, `archived` reasons. Local-only fields (current agent, retry counts, timers) are NOT in the CRDT.
6. **The application layer is a typed Rust state machine.** Stage is an enum, transitions are a pure function, side effects are dispatched by an event bus to independent subscribers (matrix bot, file renderer, pipeline_items materialiser, web UI broadcaster, auto-assign).
## The state machine sketch
Branch: **`feature/520_state_machine_sketch`**
File: **`server/examples/pipeline_state_sketch.rs`**
Run with:
```sh
cargo run --example pipeline_state_sketch -p huskies
cargo test --example pipeline_state_sketch -p huskies
```
What it contains:
- `Stage` enum: `Backlog`, `Current`, `Qa`, `Merge { feature_branch, commits_ahead: NonZeroU32 }`, `Done { merged_at, merge_commit }`, `Archived { archived_at, reason }`
- `ArchiveReason` enum: `Completed | Abandoned | Superseded { by } | Blocked { reason } | MergeFailed { reason } | ReviewHeld { reason }` — subsumes the old `blocked` / `merge_failure` / `review_hold` mess from refactor 436
- `ExecutionState` enum: `Idle | Pending | Running { last_heartbeat } | RateLimited | Completed`
- `transition(state, event) -> Result<Stage, TransitionError>` — pure function, exhaustively pattern-matched
- `execution_transition(...)` — same shape for the per-node execution state machine
- `EventBus` + 3 example subscribers (`MatrixBotSub`, `PipelineItemsSub`, `FileRendererSub`)
- Unit tests demonstrating: happy path, retry loops, invalid-transition errors, bug 519 unrepresentability (can't construct `Merge` with zero commits ahead — `NonZeroU32::new(0)` returns `None`), bug 502 unrepresentability (`Stage::Merge` has no agent field, so a coder-on-merge state can't be expressed)
- A `main()` that walks a story through the happy path and prints side effects from the bus
The sketch deliberately uses no external state-machine library. The user originally suggested `statig` (<https://crates.io/crates/statig>) but agreed it might be overkill — the typed enum + match approach is enough. If hierarchical states become useful later (e.g. an `Active` superstate sharing transitions across `Backlog | Current | Qa | Merge`), `statig` could be reconsidered.
## Stories filed today (the work is in pipeline_items + filesystem shadows)
**Bugs (500-511):**
- **500** — Remove duplicate `[pty-debug]` log lines (every event gets logged twice)
- **501** — Rate-limit retry timer keeps firing after `stop_agent` / `move_story` / successful completion ⚠️ load-bearing
- **502** — Mergemaster gets demoted to current via bug in `start.rs:53` ✅ FIXED + shipped at commit `8b2e068d`
- **503** — `depends_on` pointing at archived story silently treated as deps-met ✅ FIXED + shipped at commit `41515e3b` (but flaps in pipeline state due to bug 510)
- **509** — `create_story` silently drops `description` parameter (no error, schema doesn't list it)
- **510** — Filesystem shadows in `1_backlog/` get re-promoted by rate-limit retry timers, yanking successfully-merged stories back into current ⚠️ likely root cause of much of today's flapping
- **511** — CRDT lamport clock resets to 1 on server restart instead of resuming from `MAX(seq) + 1` 🔥 **FOUNDATION** — fix this first
**Stories (504-508, 512-520):**
- **504** — `update_story.front_matter` MCP schema only takes string values
- **505-508** — The 478 split-up: SignedOp wire codec, WS sync endpoint, inbound apply + causal queue, rendezvous config (478's actual code already on master via the manual squash-merge, but these stories still document the underlying chunks)
- **512** — Migrate chat commands from filesystem lookup to CRDT/DB (`move 503 done` failed today because of this)
- **513** — Startup reconcile pass for state-drift detection (scaffolding; deletes itself when migration completes)
- **514** — `delete_story` should do a full cleanup (DB row + CRDT op + worktree + timers + filesystem)
- **515** — Add a debug MCP tool to dump the in-memory CRDT
- **516** — `update_story.description` should create the section if it doesn't exist
- **517** — Remove filesystem-shadow fallback paths from `lifecycle.rs`
- **518** — `apply_and_persist` should log `persist_tx.send()` failures instead of silently dropping ops
- **519** — Mergemaster should detect "no commits ahead of master" and fail loudly instead of exiting silently and burning $0.82 per session
- **520** — 🔑 **Typed pipeline state machine in Rust** — the foundational architectural story everything else converges to. Subsumes refactor 436.
**Refactor 436** (was: "Unify story stuck states into a single status field") — marked superseded by 520 via `front_matter: superseded_by: "520"`. Its functionality is now part of `Stage::Archived { reason: ArchiveReason }` in the sketch.
## Recommended next-session priority order
1. **Fix bug 511 first** (CRDT lamport seq reset). ~30 lines in `crdt_state.rs::init()`. After CRDT replay, seed the local seq counter from `MAX(seq)` over own author. Without this, CRDT replay produces broken state and 510 keeps biting.
2. **Verify the 511 fix unblocks 510.** Hypothesis: 510 (filesystem shadow split-brain) is largely a downstream symptom of 511 (replay puts ops in wrong order, in-memory state diverges, materialiser re-creates shadows from old state). If true, 510 may need only a small additional cleanup pass.
3. **Read the state machine sketch and refine it.** Specifically:
- Verify the local-vs-syncable field partition is right
- Confirm `Stage::Merge` and `Stage::Done` carry exactly the data we need
- Add any missing transitions
- Decide whether `ExecutionState` should be in the same CRDT or a separate one (we tentatively chose the same CRDT under per-node-pubkey keys, for cross-node observability and heartbeat)
4. **Land story 520** — promote the sketch to a real `server/src/pipeline_state.rs` module. Implement the projection layer (`TryFrom<&PipelineItemCrdt> for PipelineItem`).
5. **Migrate consumers one at a time** in priority order: chat commands (512) → lifecycle (517) → delete_story (514) → mergemaster precondition (519, mostly subsumed by `NonZeroU32`).
6. **Once nothing reads the loose `PipelineItemView` anymore, delete the loose API.** The CRDT looseness becomes purely an implementation detail.
7. **Then the off-leash commit forensic** — investigate `rogue-commit-2026-04-09-ac9f3ecf`. How did an agent acquire `git push` capability? What code path enabled it? File a security-critical bug.
## What's currently weird / broken in the running system
- **`timers.json` keeps getting re-populated** even after we empty it. The cause: stopping an agent triggers the agent's exit handler, which calls the rate-limit auto-resume scheduler, which writes to `timers.json`. Bug 501 should cover this but it might need to be explicit about the stop-agent code path.
- **Chat commands can't find stories that have no filesystem shadow.** Bug 512. Workaround: use MCP `move_story` / `delete_story` / etc. directly, NOT the web UI chat commands.
- **The web UI shows stale state** for some stories because the API reads from the in-memory CRDT view, which can diverge from `pipeline_items`. This will be fixed naturally by 520 + 517 (single source of truth).
- **`create_worktree` always creates from master** — intentional design choice ("keep conflicts low") but means it can't reuse an existing feature branch's work. Bit us with 478 today.
- **Mergemaster's `merge_agent_work` exits silently** when there are no commits ahead of master — we lost ~$0.82 to one such session today. Bug 519 + the typed `NonZeroU32` constraint in story 520 will make this unrepresentable.
## Useful diagnostic recipes from today
- **View persisted CRDT ops:** `sqlite3 .huskies/pipeline.db "SELECT seq, substr(op_json, 1, 200) FROM crdt_ops ORDER BY seq DESC LIMIT 20"`
- **View in-memory CRDT pipeline state:** call `mcp__huskies__get_pipeline_status` (it goes through `crdt_state::read_all_items()`)
- **Tail server log filtered for bug 502 firings:** `tail -f .huskies/logs/server.log | grep --line-buffered "Failed to start mergemaster"`
- **Tail server log without `[pty-debug]` noise:** `tail -f .huskies/logs/server.log | grep -v "\[pty-debug\]"`
- **Check current pending timers:** `cat .huskies/timers.json`
- **Forensically delete a story across all four state machines:** stop agents → remove worktree → empty timers → `DELETE FROM pipeline_items WHERE id LIKE '<id>%'``DELETE FROM crdt_ops WHERE op_json LIKE '%<id>%'`
## Token cost accounting
This session burned roughly **$15-25** in agent thrash, mostly from bug 501 + bug 510 respawning agents on already-completed stories. Once 511 + 510 + 501 are fixed, that bleed disappears.
## Open questions for the next session
1. **Should `ExecutionState` live in the same CRDT or a separate one?** We tentatively said same CRDT under per-node-pubkey keys. Need to validate this against the bft-json-crdt library's actual capabilities.
2. **Heartbeat cadence?** How often should `last_heartbeat` be updated for `ExecutionState::Running`? Every 30s seems reasonable but should be config.
3. **What's the migration path from existing pipeline_items rows to typed `PipelineItem`s?** A one-time migration script, or rebuild from `crdt_ops`?
4. **Should we add `statig` after all?** Probably not for the initial implementation, but worth revisiting if we end up wanting hierarchical states (e.g., a `Working` superstate sharing transitions across active stages).
+31 -84
View File
@@ -1,35 +1,3 @@
# Project-wide default QA mode: "server", "agent", or "human".
# Per-story `qa` front matter overrides this setting.
default_qa = "server"
# Default model for coder agents. Only agents with this model are auto-assigned.
# Opus coders are reserved for explicit per-story `agent:` front matter requests.
default_coder_model = "sonnet"
# Maximum concurrent coder agents. Stories wait in 2_current/ when all slots are full.
max_coders = 3
# Maximum retries per story per pipeline stage before marking as blocked.
# Set to 0 to disable retry limits.
max_retries = 3
# Base branch name for this project. Worktree creation, merges, and agent prompts
# use this value for {{base_branch}}. When not set, falls back to auto-detection
# (reads current HEAD branch).
base_branch = "master"
[[component]]
name = "frontend"
path = "frontend"
setup = ["npm ci", "npm run build"]
teardown = []
[[component]]
name = "server"
path = "."
setup = ["mkdir -p frontend/dist", "cargo check"]
teardown = []
[[agent]] [[agent]]
name = "coder-1" name = "coder-1"
stage = "coder" stage = "coder"
@@ -37,8 +5,8 @@ role = "Full-stack engineer. Implements features across all components."
model = "sonnet" model = "sonnet"
max_turns = 50 max_turns = 50
max_budget_usd = 5.00 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." 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 when your process exits and advance the pipeline based on the results. To verify before committing, use the run_tests MCP tool (it starts tests in the background — poll get_test_result to check completion) — never run script/test or cargo test directly via Bash.\n\n## Acceptance Criteria Tracking\nAs you complete each acceptance criterion, call the check_criterion MCP tool (story_id, criterion_index) to mark it done. Index 0 is the first unchecked criterion, 1 is the second, etc. Do this as you go — not all at once at the end.\n\n## Bug Workflow: Trust the Story, Act Fast\nWhen working on bugs:\n1. READ THE STORY DESCRIPTION FIRST. If it specifies exact files, functions, and line numbers — go directly there and make the fix. Do NOT explore git history, grep the whole codebase, or re-investigate the root cause when the story already tells you what to do.\n2. If the story does NOT specify the exact location, THEN investigate: use targeted grep to find the relevant code.\n3. Fix with a surgical, minimal change. Do NOT add new abstractions or workarounds.\n4. Commit early. If you've made the fix and tests pass, commit and exit. Do not spend turns verifying that master also has the same failures — that wastes budget.\n5. Write commit messages that explain what broke and why."
system_prompt = "You are a full-stack engineer working autonomously in a git worktree. Follow the Story-Driven Test Workflow strictly. Run cargo clippy --all-targets --all-features and biome checks before considering work complete. Commit all your work before finishing - use a descriptive commit message. Do not accept stories, move them to archived, or merge to master - a human will do that. Do not coordinate with other agents - focus on your assigned story. The server automatically runs acceptance gates when your process exits. For bugs, always find and fix the root cause. Use git bisect to find breaking commits. Do not layer new code on top of existing code when a surgical fix is possible. If root cause is unclear after investigation, document what you tried rather than guessing." system_prompt = "You are a full-stack engineer working autonomously in a git worktree. Follow the Story-Driven Test Workflow strictly. Use the run_tests MCP tool to verify your changes pass — it starts tests in the background, then poll get_test_result to check completion. Never run script/test or cargo test directly via Bash. As you complete each acceptance criterion, call check_criterion MCP tool to mark it done. Add //! module-level doc comments to any new modules and /// doc comments to any new public functions, structs, or enums. 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, trust the story description — if it specifies exact files and functions, go directly there. Do not explore git history or grep the whole codebase when the story already tells you where to look. Make surgical fixes, commit early."
[[agent]] [[agent]]
name = "coder-2" name = "coder-2"
@@ -47,8 +15,8 @@ role = "Full-stack engineer. Implements features across all components."
model = "sonnet" model = "sonnet"
max_turns = 50 max_turns = 50
max_budget_usd = 5.00 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." 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 when your process exits and advance the pipeline based on the results. To verify before committing, use the run_tests MCP tool (it starts tests in the background — poll get_test_result to check completion) — never run script/test or cargo test directly via Bash.\n\n## Acceptance Criteria Tracking\nAs you complete each acceptance criterion, call the check_criterion MCP tool (story_id, criterion_index) to mark it done. Index 0 is the first unchecked criterion, 1 is the second, etc. Do this as you go — not all at once at the end.\n\n## Bug Workflow: Trust the Story, Act Fast\nWhen working on bugs:\n1. READ THE STORY DESCRIPTION FIRST. If it specifies exact files, functions, and line numbers — go directly there and make the fix. Do NOT explore git history, grep the whole codebase, or re-investigate the root cause when the story already tells you what to do.\n2. If the story does NOT specify the exact location, THEN investigate: use targeted grep to find the relevant code.\n3. Fix with a surgical, minimal change. Do NOT add new abstractions or workarounds.\n4. Commit early. If you've made the fix and tests pass, commit and exit. Do not spend turns verifying that master also has the same failures — that wastes budget.\n5. Write commit messages that explain what broke and why."
system_prompt = "You are a full-stack engineer working autonomously in a git worktree. Follow the Story-Driven Test Workflow strictly. Run cargo clippy --all-targets --all-features and biome checks before considering work complete. Commit all your work before finishing - use a descriptive commit message. Do not accept stories, move them to archived, or merge to master - a human will do that. Do not coordinate with other agents - focus on your assigned story. The server automatically runs acceptance gates when your process exits. For bugs, always find and fix the root cause. Use git bisect to find breaking commits. Do not layer new code on top of existing code when a surgical fix is possible. If root cause is unclear after investigation, document what you tried rather than guessing." system_prompt = "You are a full-stack engineer working autonomously in a git worktree. Follow the Story-Driven Test Workflow strictly. Use the run_tests MCP tool to verify your changes pass — it starts tests in the background, then poll get_test_result to check completion. Never run script/test or cargo test directly via Bash. As you complete each acceptance criterion, call check_criterion MCP tool to mark it done. Add //! module-level doc comments to any new modules and /// doc comments to any new public functions, structs, or enums. 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, trust the story description — if it specifies exact files and functions, go directly there. Do not explore git history or grep the whole codebase when the story already tells you where to look. Make surgical fixes, commit early."
[[agent]] [[agent]]
name = "coder-3" name = "coder-3"
@@ -57,8 +25,8 @@ role = "Full-stack engineer. Implements features across all components."
model = "sonnet" model = "sonnet"
max_turns = 50 max_turns = 50
max_budget_usd = 5.00 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." 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 when your process exits and advance the pipeline based on the results. To verify before committing, use the run_tests MCP tool (it starts tests in the background — poll get_test_result to check completion) — never run script/test or cargo test directly via Bash.\n\n## Acceptance Criteria Tracking\nAs you complete each acceptance criterion, call the check_criterion MCP tool (story_id, criterion_index) to mark it done. Index 0 is the first unchecked criterion, 1 is the second, etc. Do this as you go — not all at once at the end.\n\n## Bug Workflow: Trust the Story, Act Fast\nWhen working on bugs:\n1. READ THE STORY DESCRIPTION FIRST. If it specifies exact files, functions, and line numbers — go directly there and make the fix. Do NOT explore git history, grep the whole codebase, or re-investigate the root cause when the story already tells you what to do.\n2. If the story does NOT specify the exact location, THEN investigate: use targeted grep to find the relevant code.\n3. Fix with a surgical, minimal change. Do NOT add new abstractions or workarounds.\n4. Commit early. If you've made the fix and tests pass, commit and exit. Do not spend turns verifying that master also has the same failures — that wastes budget.\n5. Write commit messages that explain what broke and why."
system_prompt = "You are a full-stack engineer working autonomously in a git worktree. Follow the Story-Driven Test Workflow strictly. Run cargo clippy --all-targets --all-features and biome checks before considering work complete. Commit all your work before finishing - use a descriptive commit message. Do not accept stories, move them to archived, or merge to master - a human will do that. Do not coordinate with other agents - focus on your assigned story. The server automatically runs acceptance gates when your process exits. For bugs, always find and fix the root cause. Use git bisect to find breaking commits. Do not layer new code on top of existing code when a surgical fix is possible. If root cause is unclear after investigation, document what you tried rather than guessing." system_prompt = "You are a full-stack engineer working autonomously in a git worktree. Follow the Story-Driven Test Workflow strictly. Use the run_tests MCP tool to verify your changes pass — it starts tests in the background, then poll get_test_result to check completion. Never run script/test or cargo test directly via Bash. As you complete each acceptance criterion, call check_criterion MCP tool to mark it done. Add //! module-level doc comments to any new modules and /// doc comments to any new public functions, structs, or enums. 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, trust the story description — if it specifies exact files and functions, go directly there. Do not explore git history or grep the whole codebase when the story already tells you where to look. Make surgical fixes, commit early."
[[agent]] [[agent]]
name = "qa-2" name = "qa-2"
@@ -74,18 +42,13 @@ Read CLAUDE.md first, then .story_kit/README.md to understand the dev process.
## Your Workflow ## Your Workflow
### 0. Read the Story ### 0. Read the Story
- Read the story file at `.storkit/work/3_qa/{{story_id}}.md` - Read the story file at `.huskies/work/3_qa/{{story_id}}.md`
- Extract every acceptance criterion (the `- [ ]` checkbox lines) - Extract every acceptance criterion (the `- [ ]` checkbox lines)
- Keep this list in mind for Step 3 - Keep this list in mind for Step 3
### 1. Deterministic Gates (Prerequisites) ### 1. Deterministic Gates (Prerequisites)
Run these first if any fail, reject immediately without proceeding to AC review: Run these first if any fail, reject immediately without proceeding to AC review:
- Run `cargo clippy --all-targets --all-features` must show 0 errors, 0 warnings - Call the `run_tests` MCP tool to start tests, then poll `get_test_result` until complete all gates must pass (0 lint errors/warnings, all tests green, frontend build clean if applicable). Do NOT run script/test via Bash.
- Run `cargo test` and verify all tests pass
- If a `frontend/` directory exists:
- Run `npm run build` and note any TypeScript errors
- Run `npx @biomejs/biome check src/` and note any linting issues
- Run `npm test` and verify all frontend tests pass
### 2. Code Change Review ### 2. Code Change Review
- Run `git diff master...HEAD --stat` to see what files changed - Run `git diff master...HEAD --stat` to see what files changed
@@ -109,13 +72,13 @@ An AC fails if:
- A test exists but doesn't actually assert the behaviour described - A test exists but doesn't actually assert the behaviour described
### 4. Manual Testing Support (only if all gates PASS and all ACs PASS) ### 4. Manual Testing Support (only if all gates PASS and all ACs PASS)
- Build the server: run `cargo build` and note success/failure - Build: run `script/build` and note success/failure
- If build succeeds: find a free port (try 3010-3020) and attempt to start the server - If build succeeds: find a free port (try 3010-3020), set `HUSKIES_PORT=<port>` and start the server with `script/server`
- Generate a testing plan including: - Generate a testing plan including:
- URL to visit in the browser - URL to visit in the browser
- Things to check in the UI - Things to check in the UI
- curl commands to exercise relevant API endpoints - curl commands to exercise relevant API endpoints
- Kill the test server when done: `pkill -f 'target.*storkit' || true` (NEVER use `pkill -f storkit` it kills the vite dev server) - Stop the test server when done: send SIGTERM to the `script/server` process (e.g. `kill <pid>`)
### 5. Produce Structured Report and Verdict ### 5. Produce Structured Report and Verdict
Print your QA report to stdout. Then call `approve_qa` or `reject_qa` via the MCP tool based on the overall result. Use this format: Print your QA report to stdout. Then call `approve_qa` or `reject_qa` via the MCP tool based on the overall result. Use this format:
@@ -124,11 +87,7 @@ Print your QA report to stdout. Then call `approve_qa` or `reject_qa` via the MC
## QA Report for {{story_id}} ## QA Report for {{story_id}}
### Code Quality ### Code Quality
- clippy: PASS/FAIL (details) - run_tests MCP tool: PASS/FAIL (details)
- TypeScript build: PASS/FAIL/SKIP (details)
- Biome lint: PASS/FAIL/SKIP (details)
- cargo test: PASS/FAIL (N tests)
- npm test: PASS/FAIL/SKIP (N tests)
- Incomplete implementations: (list any todo!/unimplemented!/stubs found, or "None") - Incomplete implementations: (list any todo!/unimplemented!/stubs found, or "None")
- Other code review findings: (list any issues found, or "None") - Other code review findings: (list any issues found, or "None")
@@ -167,8 +126,8 @@ role = "Senior full-stack engineer for complex tasks. Implements features across
model = "opus" model = "opus"
max_turns = 80 max_turns = 80
max_budget_usd = 20.00 max_budget_usd = 20.00
prompt = "You are working in a git worktree on story {{story_id}}. Read CLAUDE.md first, then .story_kit/README.md to understand the dev process. The story details are in your prompt above. Follow the SDTW process through implementation and verification (Steps 1-3). The worktree and feature branch already exist - do not create them. Check .mcp.json for MCP tools. Do NOT accept the story or merge - commit your work and stop. If the user asks to review your changes, tell them to run: cd \"{{worktree_path}}\" && git difftool {{base_branch}}...HEAD\n\nIMPORTANT: Commit all your work before your process exits. The server will automatically run acceptance gates (cargo clippy + tests) when your process exits and advance the pipeline based on the results.\n\n## Bug Workflow: Root Cause First\nWhen working on bugs:\n1. Investigate the root cause before writing any fix. Use `git bisect` to find the breaking commit or `git log` to trace history. Read the relevant code before touching anything.\n2. Fix the root cause with a surgical, minimal change. Do NOT add new abstractions, wrappers, or workarounds when a targeted fix to the original code is possible.\n3. Write commit messages that explain what broke and why, not just what was changed.\n4. If you cannot determine the root cause after thorough investigation, document what you tried and why it was inconclusive — do not guess and ship a speculative fix." 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 when your process exits and advance the pipeline based on the results. To verify before committing, use the run_tests MCP tool (it starts tests in the background — poll get_test_result to check completion) — never run script/test or cargo test directly via Bash.\n\n## Acceptance Criteria Tracking\nAs you complete each acceptance criterion, call the check_criterion MCP tool (story_id, criterion_index) to mark it done. Index 0 is the first unchecked criterion, 1 is the second, etc. Do this as you go — not all at once at the end.\n\n## Bug Workflow: Trust the Story, Act Fast\nWhen working on bugs:\n1. READ THE STORY DESCRIPTION FIRST. If it specifies exact files, functions, and line numbers — go directly there and make the fix. Do NOT explore git history, grep the whole codebase, or re-investigate the root cause when the story already tells you what to do.\n2. If the story does NOT specify the exact location, THEN investigate: use targeted grep to find the relevant code.\n3. Fix with a surgical, minimal change. Do NOT add new abstractions or workarounds.\n4. Commit early. If you've made the fix and tests pass, commit and exit. Do not spend turns verifying that master also has the same failures — that wastes budget.\n5. Write commit messages that explain what broke and why."
system_prompt = "You are a senior full-stack engineer working autonomously in a git worktree. You handle complex tasks requiring deep architectural understanding. Follow the Story-Driven Test Workflow strictly. Run cargo clippy --all-targets --all-features and biome checks before considering work complete. Commit all your work before finishing - use a descriptive commit message. Do not accept stories, move them to archived, or merge to master - a human will do that. Do not coordinate with other agents - focus on your assigned story. The server automatically runs acceptance gates when your process exits. For bugs, always find and fix the root cause. Use git bisect to find breaking commits. Do not layer new code on top of existing code when a surgical fix is possible. If root cause is unclear after investigation, document what you tried rather than guessing." system_prompt = "You are a senior full-stack engineer working autonomously in a git worktree. You handle complex tasks requiring deep architectural understanding. Follow the Story-Driven Test Workflow strictly. Use the run_tests MCP tool to verify your changes pass — it starts tests in the background, then poll get_test_result to check completion. Never run script/test or cargo test directly via Bash. As you complete each acceptance criterion, call check_criterion MCP tool to mark it done. Add //! module-level doc comments to any new modules and /// doc comments to any new public functions, structs, or enums. 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, trust the story description — if it specifies exact files and functions, go directly there. Do not explore git history or grep the whole codebase when the story already tells you where to look. Make surgical fixes, commit early."
[[agent]] [[agent]]
name = "qa" name = "qa"
@@ -184,18 +143,13 @@ Read CLAUDE.md first, then .story_kit/README.md to understand the dev process.
## Your Workflow ## Your Workflow
### 0. Read the Story ### 0. Read the Story
- Read the story file at `.storkit/work/3_qa/{{story_id}}.md` - Read the story file at `.huskies/work/3_qa/{{story_id}}.md`
- Extract every acceptance criterion (the `- [ ]` checkbox lines) - Extract every acceptance criterion (the `- [ ]` checkbox lines)
- Keep this list in mind for Step 3 - Keep this list in mind for Step 3
### 1. Deterministic Gates (Prerequisites) ### 1. Deterministic Gates (Prerequisites)
Run these first if any fail, reject immediately without proceeding to AC review: Run these first if any fail, reject immediately without proceeding to AC review:
- Run `cargo clippy --all-targets --all-features` must show 0 errors, 0 warnings - Call the `run_tests` MCP tool to start tests, then poll `get_test_result` until complete all gates must pass (0 lint errors/warnings, all tests green, frontend build clean if applicable). Do NOT run script/test via Bash.
- Run `cargo test` and verify all tests pass
- If a `frontend/` directory exists:
- Run `npm run build` and note any TypeScript errors
- Run `npx @biomejs/biome check src/` and note any linting issues
- Run `npm test` and verify all frontend tests pass
### 2. Code Change Review ### 2. Code Change Review
- Run `git diff master...HEAD --stat` to see what files changed - Run `git diff master...HEAD --stat` to see what files changed
@@ -219,13 +173,13 @@ An AC fails if:
- A test exists but doesn't actually assert the behaviour described - A test exists but doesn't actually assert the behaviour described
### 4. Manual Testing Support (only if all gates PASS and all ACs PASS) ### 4. Manual Testing Support (only if all gates PASS and all ACs PASS)
- Build the server: run `cargo build` and note success/failure - Build: run `script/build` and note success/failure
- If build succeeds: find a free port (try 3010-3020) and attempt to start the server - If build succeeds: find a free port (try 3010-3020), set `HUSKIES_PORT=<port>` and start the server with `script/server`
- Generate a testing plan including: - Generate a testing plan including:
- URL to visit in the browser - URL to visit in the browser
- Things to check in the UI - Things to check in the UI
- curl commands to exercise relevant API endpoints - curl commands to exercise relevant API endpoints
- Kill the test server when done: `pkill -f 'target.*storkit' || true` (NEVER use `pkill -f storkit` it kills the vite dev server) - Stop the test server when done: send SIGTERM to the `script/server` process (e.g. `kill <pid>`)
### 5. Produce Structured Report and Verdict ### 5. Produce Structured Report and Verdict
Print your QA report to stdout. Then call `approve_qa` or `reject_qa` via the MCP tool based on the overall result. Use this format: Print your QA report to stdout. Then call `approve_qa` or `reject_qa` via the MCP tool based on the overall result. Use this format:
@@ -234,11 +188,7 @@ Print your QA report to stdout. Then call `approve_qa` or `reject_qa` via the MC
## QA Report for {{story_id}} ## QA Report for {{story_id}}
### Code Quality ### Code Quality
- clippy: PASS/FAIL (details) - run_tests MCP tool: PASS/FAIL (details)
- TypeScript build: PASS/FAIL/SKIP (details)
- Biome lint: PASS/FAIL/SKIP (details)
- cargo test: PASS/FAIL (N tests)
- npm test: PASS/FAIL/SKIP (N tests)
- Incomplete implementations: (list any todo!/unimplemented!/stubs found, or "None") - Incomplete implementations: (list any todo!/unimplemented!/stubs found, or "None")
- Other code review findings: (list any issues found, or "None") - Other code review findings: (list any issues found, or "None")
@@ -282,7 +232,7 @@ prompt = """You are the mergemaster agent for story {{story_id}}. Your job is to
Read CLAUDE.md first, then .story_kit/README.md to understand the dev process. Read CLAUDE.md first, then .story_kit/README.md to understand the dev process.
## Your Workflow ## Your Workflow
1. Call merge_agent_work(story_id='{{story_id}}') via the MCP tool to trigger the full merge pipeline 1. Call merge_agent_work(story_id='{{story_id}}') this blocks until the merge completes and returns the result. Do NOT poll get_merge_status.
2. Review the result: check success, had_conflicts, conflicts_resolved, gates_passed, and gate_output 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 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 4. If conflicts were auto-resolved (conflicts_resolved=true) and gates passed: report success, noting which conflicts were resolved
@@ -300,36 +250,33 @@ When the auto-resolver fails, you have access to the merge worktree at `.story_k
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. 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. Resolve by integrating the feature's new functionality into master's code structure
5. Stage resolved files with `git add` 5. Stage resolved files with `git add`
6. Run `cargo check` (and `npm run build` if frontend changed) to verify compilation 6. Call the `run_tests` MCP tool to start tests, then poll `get_test_result` until complete
7. If it compiles, commit and re-trigger merge_agent_work 7. If it compiles, commit and re-trigger merge_agent_work
### Common conflict patterns in this project: ### Common conflict patterns:
**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. **Story file rename/rename conflicts:** Both branches moved the story .md file to different pipeline directories. Resolution: `git rm` both sides story files in pipeline directories 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. **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). **Formatting-only conflicts:** Both sides reformatted the same code differently. Resolution: pick either side (prefer master).
**IMPORTANT: After resolving ANY conflict or fixing ANY gate failure in the merge workspace, use the `run_lint` MCP tool to check formatting, then `run_tests` to verify everything passes before recommitting.** The auto-resolver frequently produces code that compiles but fails formatting or linting checks.
## Fixing Gate Failures ## 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. If quality gates fail, attempt to fix issues yourself in the merge workspace. Use the run_tests MCP tool to verify before recommitting.
**Fix yourself (up to 3 attempts total):** **Fix yourself (up to 3 attempts total):**
- Syntax errors (missing semicolons, brackets, commas) - Syntax errors
- Duplicate definitions from merge artifacts - Duplicate definitions from merge artifacts
- Simple type annotation errors - Unused import warnings
- Unused import warnings flagged by clippy - Formatting issues that block linting
- Mismatched braces from bad conflict resolution
- Trivial formatting issues that block compilation or linting
**Report to human without attempting a fix:** **Report to human without attempting a fix:**
- Logic errors or incorrect business logic - Logic errors or incorrect business logic
- Missing function implementations - Missing function implementations
- Architectural changes required - Architectural changes required
- Non-trivial refactoring needed
**Max retry limit:** If gates still fail after 3 fix attempts, call report_merge_failure to record the failure, then stop immediately and report the full gate output to the human. **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.
@@ -340,4 +287,4 @@ If quality gates fail (cargo clippy, cargo test, npm run build, npm test), attem
- Report conflict resolution outcomes clearly - Report conflict resolution outcomes clearly
- Report gate failures with full output so the human can act if needed - Report gate failures with full output so the human can act if needed
- The server automatically runs acceptance gates when your process exits""" - The server automatically runs acceptance gates when your process exits"""
system_prompt = "You are the mergemaster agent. Your primary job is to merge feature branches to master. First try the merge_agent_work MCP tool. If the auto-resolver fails on complex conflicts, resolve them yourself in the merge worktree — you are an opus-class agent capable of understanding both sides of a conflict and producing correct merged code. Common patterns: keep master's tokio::select! permission loop in bot.rs, discard story file rename conflicts (gitignored), remove duplicate definitions. After resolving, verify compilation before re-triggering merge. CRITICAL: Never manually move story files or call accept_story. After 3 failed fix attempts, call report_merge_failure and stop." 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 workspace. Common patterns: discard story file rename conflicts (gitignored), remove duplicate definitions/imports. After resolving, verify with run_tests MCP tool 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."
+28
View File
@@ -0,0 +1,28 @@
# Discord Transport
# Copy this file to bot.toml and fill in your values.
# Only one transport can be active at a time.
#
# Setup:
# 1. Create a Discord Application at discord.com/developers/applications
# 2. Go to Bot → create a bot and copy the token
# 3. Enable "Message Content Intent" under Privileged Gateway Intents
# 4. Go to OAuth2 → URL Generator, select "bot" scope with permissions:
# Send Messages, Read Message History, Manage Messages
# 5. Use the generated URL to invite the bot to your server
# 6. Right-click the channel(s) → Copy Channel ID (enable Developer Mode in settings)
enabled = true
transport = "discord"
discord_bot_token = "your-bot-token-here"
discord_channel_ids = ["123456789012345678"]
# Discord user IDs allowed to interact with the bot.
# When empty, all users in configured channels can interact.
# discord_allowed_users = ["111222333444555666"]
# Bot display name (used in formatted messages).
# display_name = "Assistant"
# Maximum conversation turns to remember per channel (default: 20).
# history_size = 20
+39
View File
@@ -0,0 +1,39 @@
# Project-wide default QA mode: "server", "agent", or "human".
# Per-story `qa` front matter overrides this setting.
default_qa = "server"
# Default model for coder agents. Only agents with this model are auto-assigned.
# Opus coders are reserved for explicit per-story `agent:` front matter requests.
default_coder_model = "sonnet"
# Maximum concurrent coder agents. Stories wait in 2_current/ when all slots are full.
max_coders = 3
# Maximum retries per story per pipeline stage before marking as blocked.
# Set to 0 to disable retry limits.
max_retries = 3
# Base branch name for this project. Worktree creation, merges, and agent prompts
# use this value for {{base_branch}}. When not set, falls back to auto-detection
# (reads current HEAD branch).
base_branch = "master"
# Suppress soft rate-limit warning notifications in chat.
# Hard blocks and story-blocked notifications are always sent.
rate_limit_notifications = false
# IANA timezone for timer scheduling (e.g. "Europe/London", "America/New_York").
# Timer HH:MM inputs are interpreted in this timezone.
timezone = "Europe/London"
[[component]]
name = "frontend"
path = "frontend"
setup = ["npm ci", "npm run build"]
teardown = []
[[component]]
name = "server"
path = "."
setup = ["mkdir -p frontend/dist", "cargo check"]
teardown = []
@@ -1,4 +1,4 @@
# Example project.toml — copy to .storkit/project.toml and customise. # Example project.toml — copy to .huskies/project.toml and customise.
# This file is checked in; project.toml itself is gitignored (it may contain # This file is checked in; project.toml itself is gitignored (it may contain
# instance-specific settings). # instance-specific settings).
@@ -37,7 +37,7 @@ max_turns = 50
max_budget_usd = 5.00 max_budget_usd = 5.00
prompt = """ prompt = """
You are working in a git worktree on story {{story_id}}. You are working in a git worktree on story {{story_id}}.
Read CLAUDE.md first, then .storkit/README.md to understand the dev process. Read CLAUDE.md first, then .huskies/README.md to understand the dev process.
Run: cd "{{worktree_path}}" && git difftool {{base_branch}}...HEAD Run: cd "{{worktree_path}}" && git difftool {{base_branch}}...HEAD
Commit all your work before your process exits. Commit all your work before your process exits.
""" """
@@ -6,7 +6,7 @@ Slack integration is configured via `bot.toml` in the project's `.story_kit/` di
```toml ```toml
transport = "slack" transport = "slack"
display_name = "Storkit" display_name = "Huskies"
slack_bot_token = "xoxb-..." slack_bot_token = "xoxb-..."
slack_signing_secret = "..." slack_signing_secret = "..."
slack_channel_ids = ["C01ABCDEF"] slack_channel_ids = ["C01ABCDEF"]
@@ -29,11 +29,11 @@ Slash commands provide quick access to pipeline commands without mentioning the
| Command | Description | | Command | Description |
|---------|-------------| |---------|-------------|
| `/storkit-status` | Show pipeline status and agent availability | | `/huskies-status` | Show pipeline status and agent availability |
| `/storkit-cost` | Show token spend: 24h total, top stories, and breakdown | | `/huskies-cost` | Show token spend: 24h total, top stories, and breakdown |
| `/storkit-show` | Display the full text of a work item (e.g. `/storkit-show 42`) | | `/huskies-show` | Display the full text of a work item (e.g. `/huskies-show 42`) |
| `/storkit-git` | Show git status: branch, changes, ahead/behind | | `/huskies-git` | Show git status: branch, changes, ahead/behind |
| `/storkit-htop` | Show system and agent process dashboard | | `/huskies-htop` | Show system and agent process dashboard |
All slash command responses are **ephemeral** — only the user who invoked the command sees the response. All slash command responses are **ephemeral** — only the user who invoked the command sees the response.
@@ -118,8 +118,8 @@ To support both Remote and Local models, the system implements a `ModelProvider`
Multiple instances can run simultaneously in different worktrees. To avoid port conflicts: Multiple instances can run simultaneously in different worktrees. To avoid port conflicts:
- **Backend:** Set `STORKIT_PORT` to a unique port (default is 3001). Example: `STORKIT_PORT=3002 cargo run` - **Backend:** Set `HUSKIES_PORT` to a unique port (default is 3001). Example: `HUSKIES_PORT=3002 cargo run`
- **Frontend:** Run `npm run dev` from `frontend/`. It auto-selects the next unused port. It reads `STORKIT_PORT` to know which backend to talk to, so export it before running: `export STORKIT_PORT=3002 && cd frontend && npm run dev` - **Frontend:** Run `npm run dev` from `frontend/`. It auto-selects the next unused port. It reads `HUSKIES_PORT` to know which backend to talk to, so export it before running: `export HUSKIES_PORT=3002 && cd frontend && npm run dev`
When running in a worktree, use a port that won't conflict with the main instance (3001). Ports 3002+ are good choices. When running in a worktree, use a port that won't conflict with the main instance (3001). Ports 3002+ are good choices.
@@ -1,8 +1,8 @@
--- ---
name: "Fly.io Machines API integration for multi-tenant storkit SaaS" name: "Fly.io Machines API integration for multi-tenant huskies SaaS"
--- ---
# Spike 408: Fly.io Machines API integration for multi-tenant storkit SaaS # Spike 408: Fly.io Machines API integration for multi-tenant huskies SaaS
## Question ## Question
@@ -28,7 +28,7 @@ A thin Rust service using `reqwest` for the Machines API and `axum` for the reve
- [ ] Test attaching a persistent volume to a machine and verify it persists across stop/start - [ ] Test attaching a persistent volume to a machine and verify it persists across stop/start
- [ ] Test secret injection — pass a dummy `credentials.json` as a Fly secret and verify it's readable inside the machine - [ ] Test secret injection — pass a dummy `credentials.json` as a Fly secret and verify it's readable inside the machine
- [ ] Sketch the auth proxy: JWT validation → machine lookup → reverse proxy to machine's private IP; verify WebSocket proxying works - [ ] Sketch the auth proxy: JWT validation → machine lookup → reverse proxy to machine's private IP; verify WebSocket proxying works
- [ ] Measure actual cold start time for a minimal storkit container image - [ ] Measure actual cold start time for a minimal huskies container image
- [ ] Document any API quirks, rate limits, or sharp edges discovered during testing - [ ] Document any API quirks, rate limits, or sharp edges discovered during testing
## Findings ## Findings
@@ -6,13 +6,13 @@ name: "Multi-account OAuth token rotation on rate limit"
## User Story ## User Story
As a storkit user with multiple Claude Max subscriptions, I want the system to automatically rotate to a different account when one gets rate limited, so that agents and chat don't stall out waiting for limits to reset. As a huskies user with multiple Claude Max subscriptions, I want the system to automatically rotate to a different account when one gets rate limited, so that agents and chat don't stall out waiting for limits to reset.
## Acceptance Criteria ## Acceptance Criteria
- [ ] OAuth login flow stores credentials per-account (keyed by email), not overwriting previous accounts - [ ] OAuth login flow stores credentials per-account (keyed by email), not overwriting previous accounts
- [ ] GET /oauth/status returns all stored accounts and their status (active, rate-limited, expired) - [ ] GET /oauth/status returns all stored accounts and their status (active, rate-limited, expired)
- [ ] When the active account hits a rate limit, storkit automatically swaps to the next available account's refresh token, refreshes, and retries - [ ] When the active account hits a rate limit, huskies automatically swaps to the next available account's refresh token, refreshes, and retries
- [ ] The bot sends a notification in Matrix/WhatsApp when it swaps accounts - [ ] The bot sends a notification in Matrix/WhatsApp when it swaps accounts
- [ ] If all accounts are rate limited, the bot surfaces a clear message with the time until the earliest reset - [ ] If all accounts are rate limited, the bot surfaces a clear message with the time until the earliest reset
- [ ] A new /oauth/authorize login adds to the account pool rather than replacing the current credentials - [ ] A new /oauth/authorize login adds to the account pool rather than replacing the current credentials
@@ -1,5 +1,7 @@
--- ---
name: "Unify story stuck states into a single status field" name: "Unify story stuck states into a single status field"
status: "superseded"
superseded_by: 520
--- ---
# Refactor 436: Unify story stuck states into a single status field # Refactor 436: Unify story stuck states into a single status field
@@ -0,0 +1,24 @@
---
name: "Build agent mode with CRDT-based work claiming"
agent: coder-opus
depends_on: [478]
---
# Story 479: Build agent mode with CRDT-based work claiming
## User Story
As a user with multiple laptops, I want to run huskies in build agent mode so it connects to the mesh, syncs state, and autonomously picks up and runs coding work.
## Acceptance Criteria
- [ ] New CLI mode: huskies agent --rendezvous ws://host:3001
- [ ] Agent mode: syncs CRDT state, runs coders, no web UI or chat interface
- [ ] Work claiming via CRDT: node writes claim (node ID) to CRDT doc, merge resolves conflicts deterministically, losing node stops work
- [ ] Agent picks up stories in current stage and runs Claude Code locally
- [ ] Agent pushes feature branch to Gitea when done, reports completion via CRDT
- [ ] Handles offline/reconnect: CRDT merges on reconnect, interrupted work is reclaimed after timeout
## Out of Scope
- TBD
@@ -0,0 +1,23 @@
---
name: "Cryptographic node auth for distributed mesh"
agent: coder-opus
depends_on: [479]
---
# Story 480: Cryptographic node auth for distributed mesh
## User Story
As a user running a distributed huskies mesh, I want nodes authenticated by Ed25519 keypairs so only trusted machines can join and see pipeline state.
## Acceptance Criteria
- [ ] Each node has an Ed25519 keypair (generated on first run or via CLI command)
- [ ] Trusted nodes defined by a list of known public keys in config
- [ ] Nodes authenticate on WebSocket connect by signing a challenge
- [ ] CRDT node ID derived from public key (already built into bft-json-crdt crate)
- [ ] Unauthorised nodes rejected on connect
## Out of Scope
- TBD
@@ -0,0 +1,25 @@
---
name: "create_worktree deletes all files from main branch git index"
---
# Bug 486: create_worktree deletes all files from main branch git index
## Description
On the reclaimer project, the create_worktree operation for story 34 produced a commit (cea0c48) with message "huskies: create 34_story_drawer_open_pushes_main_view_aside_with_animation" that removed 76 files (9853 deletions) from the main branch git index. All files remained on disk — nothing was lost — but every tracked file became untracked. Static analysis of the watcher code (watcher.rs:152-185) shows git add -A .huskies/work/ with correct current_dir, which should only affect files under .huskies/work/. No other code path in the server runs git add without a pathspec on the main branch. Root cause is unknown — may be related to the storkit→huskies migration leaving the git index in an inconsistent state, or a race condition during first-time scaffold on a project that previously used .storkit/.
## How to Reproduce
1. Uncertain — may require fresh huskies setup on a project that previously used storkit. 2. Create a story via the bot or MCP tool. 3. Observe that the auto-commit for story creation removes all tracked files from the git index.
## Actual Result
The commit for story creation deleted 76 files (9853 deletions) from the git index while leaving them on disk.
## Expected Result
The commit for story creation should only add the new story markdown file under .huskies/work/1_backlog/. No other files should be affected.
## Acceptance Criteria
- [ ] Bug is fixed and verified
@@ -0,0 +1,32 @@
---
name: "Stale merge job lock prevents new merges after agent dies"
---
# Bug 498: Stale merge job lock prevents new merges after agent dies
## Description
When the mergemaster agent is killed or stops while a merge is in progress, the in-memory `merge_jobs` map retains a `Running` status entry for that story. Subsequent attempts to call `merge_agent_work` get "Merge already in progress" and fail. The lock is never cleaned up.
This causes the mergemaster to loop: spawn, try merge, get "already in progress", waste turns, exit, respawn. The merge never completes.
The fix: clear the merge job entry when the mergemaster agent exits (whether cleanly or via kill/stop).
## How to Reproduce
1. Start mergemaster on a story in merge
2. Kill/stop the mergemaster agent before merge completes
3. Try to merge_agent_work again for the same story
4. Get "Merge already in progress" error
## Actual Result
Stale Running entry in merge_jobs map blocks all future merge attempts until server restart.
## Expected Result
Merge job lock is cleaned up when the agent exits, allowing retry.
## Acceptance Criteria
- [ ] Bug is fixed and verified
@@ -0,0 +1,72 @@
---
name: "CRDT lamport clock (inner.seq) resets to 1 on server restart instead of resuming from max(own_author_seq) + 1"
---
# Bug 511: CRDT lamport clock (inner.seq) resets to 1 on server restart instead of resuming from max(own_author_seq) + 1
## Description
When the huskies server restarts (e.g. via `rebuild_and_restart`), the local node's CRDT lamport clock — `inner.seq` on each `SignedOp` — appears to reset to 1 instead of resuming from `MAX(seq) + 1` for the local author's own previously-persisted ops.
**Discovered live on 2026-04-09** while inspecting the `crdt_ops` table after a `rebuild_and_restart`. Pre-restart ops were at seqs 485-492 (creation ops for stories 503-510). Post-restart ops were being persisted at seqs 1, 2, 3, 4, 5, 6, 7 — visible by sorting `crdt_ops` by `created_at DESC`:
```
created_at | seq
2026-04-09T18:49:56 → seq 7 ← post-restart
2026-04-09T18:38:22 → seq 6 ← post-restart
2026-04-09T18:37:45 → seq 492 ← pre-restart, last write before restart
2026-04-09T18:31:32 → seq 4 ← post-restart
2026-04-09T18:27:04 → seq 3 ← post-restart
```
So the local node, which had reached seq=492 before the restart, started writing new ops at seq=1 after the restart and is now climbing from there. This means **new ops have lower seqs than existing ops from the same author**.
## Why this matters
In a BFT JSON CRDT, `inner.seq` is the local lamport clock used for causality tracking. The library assumes per-author seqs are monotonically increasing — newer ops from the same author have higher seqs than older ops. Several things break when this invariant is violated:
1. **Causality / ordering on remote replay.** When a peer (or this same node after another restart) replays the persisted ops in `seq` order, the post-restart ops will be applied *before* the pre-restart ops, even though they happened later. This can produce a non-deterministic state and can cause field updates to "go backwards" — e.g. a story that was moved current → done pre-restart, then nothing post-restart, would correctly end at "done"; but if you also did a post-restart action, the seq ordering would re-play it in the wrong order.
2. **Op ID collisions.** The op id is a hash of the op contents (including author, seq, content). If a post-restart op happens to be structurally identical to a pre-restart op (e.g. "set stage to 1_backlog" with the same author and same seq=3), the op ids could collide. The persistence path uses `INSERT INTO crdt_ops ... ON CONFLICT(op_id) DO NOTHING`, which would *silently drop* the new op. (We have not yet observed this happen, but it's a latent risk.)
3. **Sync between nodes will desync.** Once the WebSocket sync layer (story 478, just merged) is exchanging ops between nodes, a restart on one node will produce ops with seqs that look "old" to the other node, and the receiving node may de-dupe or mis-order them. This will manifest as silent state divergence in multi-node deployments, which is exactly what the sync layer is supposed to prevent.
4. **Today's pipeline state confusion.** The 8 stories I created in this session (503-510) are at seqs 485-492 in the persisted CRDT. Their post-restart lifecycle moves are at seqs 1-15. If we replay the CRDT from disk in seq order, the lifecycle moves will be applied to *empty* state before the creation ops have run, and will silently no-op (because they reference content indexes that don't exist yet). On the next restart after this state, the in-memory view will show the stories in their *creation* state, not their post-restart-lifecycle state — i.e. all 8 stories will appear "stuck at 1_backlog" again. **This may well be the cause of bug 510's split-brain symptom**.
## Where the bug lives
`server/src/crdt_state.rs::init()` (around lines 80-115) replays persisted ops to reconstruct state, then constructs a fresh `CrdtState { crdt, keypair, index, persist_tx }`. The `BaseCrdt::new(&keypair)` call constructs a fresh CRDT with a fresh internal seq counter. The replay re-applies ops via `crdt.apply(signed_op)` which presumably updates the doc but does NOT advance the local seq counter (because `apply()` is for *remote* ops).
After replay, the local seq counter is at 0 (or wherever the BFT CRDT library defaults). The next call to `apply_and_persist` produces an op with `inner.seq = 1` (or whatever the next-counter value is) — even though there are already ops at seq 485+ from this author in the persisted state.
The fix is to inspect `MAX(inner.seq)` for ops where `author == local_keypair.public()` after the replay, and seed the BFT CRDT's local seq counter from `max + 1`. The exact API for "seed the seq counter" depends on the bft-json-crdt library — may need a small upstream change if not already exposed.
## How to Reproduce
1. Start a fresh huskies server with an empty database. Verify `crdt_ops` is empty.
2. Create several stories via `create_story` or similar — observe ops being persisted at incrementing seqs (1, 2, 3, ...).
3. Note the highest seq via `sqlite3 .huskies/pipeline.db "SELECT MAX(seq) FROM crdt_ops;"` — call this N.
4. Stop the server and start it again (or `rebuild_and_restart`).
5. Create another story via `create_story`.
6. Query `SELECT seq, created_at FROM crdt_ops ORDER BY created_at DESC LIMIT 5;`
## Actual Result
The new op (just created in step 5) is persisted with `seq = 1` (or some small value), NOT `seq = N + 1`. The lamport clock has been reset.
Concretely on 2026-04-09 we observed seqs in `crdt_ops` ordered by `created_at` DESC of: 7, 6, 492, 4, 3 — i.e. post-restart writes were at seqs 3, 4, 6, 7 even though the highest pre-restart seq was 492.
## Expected Result
After restart, the local node's seq counter must resume from `MAX(inner.seq)` across all persisted ops where `author == local_keypair.public()`, plus 1. The next op written by the local node should have `seq = N + 1` where N is the previous local high-water mark.
Equivalent stated: `inner.seq` on the local author's ops must be monotonically increasing across the entire lifetime of the local node's keypair, not just within a single process invocation.
## Acceptance Criteria
- [ ] After a server restart, the next CRDT op written by the local node has seq = MAX(local_author_seq from crdt_ops) + 1, not 1
- [ ] Regression test: seed crdt_ops with an op at seq=100 by the local author, restart the CRDT subsystem (or call init() in a test harness), trigger a write_item, assert the new op has seq=101
- [ ] Regression test: a brand-new node (no pre-existing ops) still starts at seq=1 (no off-by-one introduced by the fix)
- [ ] Inter-node test: simulate two nodes A and B, A writes ops up to seq=50, A restarts, A writes a new op which should be seq=51, broadcast to B, assert B applies it in the correct causal position
- [ ] If the fix requires changes to bft-json-crdt itself (to expose a way to seed the local seq), the upstream change is documented in the bug body and either landed or vendored
- [ ] After this fix is in place, replay-on-restart for the existing data (8 stories in pipeline_items at seqs 485-492 with lifecycle moves at seqs 1-15) is verified to produce the correct in-memory state — OR the existing broken-seq data is migrated as part of the fix
@@ -0,0 +1,58 @@
---
name: "Migrate chat commands from filesystem lookup to CRDT/DB"
---
# Story 512: Migrate chat commands from filesystem lookup to CRDT/DB
## User Story
**Depends on story 520** (typed pipeline state machine). This story is best implemented as a *consumer* of the typed transition API, not against the loose `PipelineItemView`. Wait for 520 to land first, then migrate the chat command lookups to use the typed `find_story_by_number → Result<PipelineItem, _>` helper from the new module.
---
**Note:** content stuffed into user_story per bug 509 workaround.
## Context
All the slash-style chat commands in `server/src/chat/commands/{move_story,show,depends,unblock}.rs` and `server/src/chat/transport/matrix/{start,assign,delete}.rs` look up stories by **searching for `.huskies/work/*/N_*.md` filesystem files**. After the 491/492 migration moved story content out of the filesystem and into `pipeline_items` + CRDT, these commands silently fail with `"No story, bug, or spike with number {N} found"` for any story whose filesystem shadow doesn't exist — *even when the story is fully present in the DB and CRDT*.
## Real user story
As a user typing chat commands in the web UI or the matrix bot, I want move/show/depends/unblock/start/assign/delete to find any story that's in the pipeline regardless of whether its filesystem shadow exists, so the chat workflow stays usable post-migration.
## Observed 2026-04-09
Master commit `41515e3b` had 503's code, the in-memory CRDT view had 503 at stage='merge', the `pipeline_items` row existed (post my-sqlite-update at `5_done`), but `move 503 done` in the web UI returned **`No story, bug, or spike with number 503 found`** because no `.huskies/work/4_merge/503_*.md` file existed.
## Implementation note
The MCP `move_story` tool already does this correctly: it goes through `lifecycle::move_item` which checks `crdt_state::read_item(story_id)` first. The chat commands need to use the same lookup helper. The fix should consolidate all "find story by number" logic into one shared function used by every command.
## Context
All the slash-style chat commands in `server/src/chat/commands/{move_story,show,depends,unblock}.rs` and `server/src/chat/transport/matrix/{start,assign,delete}.rs` look up stories by **searching for `.huskies/work/*/N_*.md` filesystem files**. After the 491/492 migration moved story content out of the filesystem and into `pipeline_items` + CRDT, these commands silently fail with `"No story, bug, or spike with number {N} found"` for any story whose filesystem shadow doesn't exist — *even when the story is fully present in the DB and CRDT*.
## Real user story
As a user typing chat commands in the web UI or the matrix bot, I want move/show/depends/unblock/start/assign/delete to find any story that's in the pipeline regardless of whether its filesystem shadow exists, so the chat workflow stays usable post-migration.
## Observed 2026-04-09
Master commit `41515e3b` had 503's code, the in-memory CRDT view had 503 at stage='merge', the `pipeline_items` row existed (post my-sqlite-update at `5_done`), but `move 503 done` in the web UI returned **`No story, bug, or spike with number 503 found`** because no `.huskies/work/4_merge/503_*.md` file existed.
## Implementation note
The MCP `move_story` tool already does this correctly: it goes through `lifecycle::move_item` which checks `crdt_state::read_item(story_id)` first. The chat commands need to use the same lookup helper. The fix should consolidate all "find story by number" logic into one shared function used by every command.
## Acceptance Criteria
- [ ] All seven chat commands (move_story, show, depends, unblock, start, assign, delete) successfully find stories that exist in CRDT but have no filesystem shadow
- [ ] Backward compat: commands still work for stories with only filesystem shadows (during the migration window)
- [ ] A single shared `find_story_by_number` helper is introduced and used by every chat command
- [ ] Lookup priority order is documented and consistent: CRDT first, then pipeline_items, then filesystem fallback
- [ ] Regression test per command covering CRDT-only, filesystem-only, both-present, and not-found cases
- [ ] Observed repro from 2026-04-09 (move 503 done failing even though 503 was fully present in CRDT and pipeline_items) is the canonical regression case
## Out of Scope
- TBD
@@ -0,0 +1,55 @@
---
name: "Startup reconcile pass that detects drift between CRDT, pipeline_items, and filesystem shadows"
---
# Story 513: Startup reconcile pass that detects drift between CRDT, pipeline_items, and filesystem shadows
## User Story
**Note:** content stuffed into user_story per bug 509 workaround.
---
## Context
Post-491/492, huskies has **four places state lives** that can drift apart:
1. `crdt_ops` table — the persisted CRDT operation log (intended source of truth)
2. In-memory CRDT view — `state.crdt.doc.items` reconstructed from `crdt_ops` on startup, mutated by `apply_and_persist` during runtime
3. `pipeline_items` table — a shadow / materialised view, written to as a shadow alongside CRDT writes
4. Filesystem shadows in `.huskies/work/N_stage/*.md` — legacy rendering, still written by some paths and read by others
There is currently **no reconcile pass** that detects drift between them. We've watched this drift bite repeatedly today: stories appear in some views and not others, lifecycle moves happen in one but not another, my direct sqlite UPDATE was invisible to the API, etc. Each individual view looks "fine" in isolation, but the drift only becomes visible when a user notices a story behaving inconsistently.
## Real user story
As a developer or operator running huskies, I want a startup reconcile pass that compares all four state sources and either reconciles them automatically (preferred) or logs structured warnings about the drift, so I can detect and diagnose state corruption before it causes user-visible bugs.
## Observed 2026-04-09
Throughout this session we observed: 478 in pipeline_items but missing from CRDT (after a direct sqlite insert), 503 in CRDT at stage=merge but pipeline_items at stage=5_done (after my UPDATE), filesystem shadows in `1_backlog/` for stories that were already in 5_done in the DB (bug 510), etc. None of these were detected by huskies itself — they were all only found by running ad-hoc `SELECT` queries during incident response.
## Scope
This is the *detection* story, not the *fix-the-drift* story. The reconcile pass should:
- Run at startup (after CRDT replay, before serving requests)
- Compare each story's stage across all four sources
- Emit structured log lines for each drift type (CRDT-only, FS-only, DB-only, stage mismatch, etc.)
- Optionally surface a count to the matrix bot startup announcement (e.g. "⚠️ 3 stories have CRDT/DB drift — see logs")
The actual *fix-the-drift* logic (what to do when drift is detected) is a separate, larger story.
## Acceptance Criteria
- [ ] At server startup, after CRDT replay, a reconcile_state() function runs that walks all four state sources and detects drift
- [ ] Each drift type is logged with a structured line: e.g. `[reconcile] DRIFT story=X crdt_stage=Y db_stage=Z fs_stage=W` (or `MISSING` for absent)
- [ ] If any drift is detected, the matrix bot startup announcement includes a count and a suggestion to check the server logs
- [ ] The reconcile pass completes in < 1 second for a typical pipeline (~100 stories) so it doesn't slow startup meaningfully
- [ ] Tests cover: no drift (clean state), CRDT-only story, DB-only story, FS-only story, stage mismatch between CRDT and DB
- [ ] Documentation in README.md explains the reconcile pass and what each drift type means
- [ ] The pass is opt-out via a config flag in case it produces noise during the migration window
## Out of Scope
- TBD
@@ -0,0 +1,94 @@
---
name: "delete_story should do a full cleanup (CRDT op + DB row + filesystem shadow + worktree + pending timers)"
---
# Story 514: delete_story should do a full cleanup (CRDT op + DB row + filesystem shadow + worktree + pending timers)
## User Story
**Depends on story 520** (typed pipeline state machine). With 520 in place, `delete_story` becomes a single typed transition (`* → Archived(Abandoned)` or a hard-delete CRDT op) followed by event subscribers that handle the worktree, timers, and filesystem cleanup. This story should be re-shaped as the consumer migration once 520 lands.
---
**Note:** content stuffed into user_story per bug 509 workaround.
## Context
The MCP `delete_story` tool currently only **removes the filesystem markdown** from `.huskies/work/N_stage/`. It does NOT:
- Remove the row from `pipeline_items`
- Write a CRDT delete op to `crdt_ops`
- Tear down the in-memory CRDT entry
- Remove the `.huskies/worktrees/N_…/` worktree
- Cancel any pending rate-limit retry timers in `.huskies/timers.json`
So after `delete_story`, the story keeps appearing in `get_pipeline_status` (because the in-memory CRDT still has it), the timer fires and re-spawns an agent, the agent runs in the still-existing worktree, and the user has no idea why the "deleted" story keeps coming back.
## Real user story
As a user calling `delete_story` (via MCP, web UI, or chat command), I want a complete tear-down of all state associated with that story across every layer, so the story is actually gone — no in-memory cache entries, no pending agents, no timers, no worktree, no shadow files, no future spawns.
## Observed 2026-04-09
Repeatedly throughout the session. The most concrete example was around 17:20: I called `delete_story 478_…`, the tool returned success, the markdown file at `.huskies/work/1_backlog/478_…md` was removed, but at 17:25:17 the rate-limit retry timer fired and **re-spawned a coder-1 on the deleted story** because the worktree still existed, the pipeline_items row still existed, and the timer entry still existed in `.huskies/timers.json`. We then had to do sqlite surgery + manual worktree removal + manual timers.json edit to actually kill 478.
## Implementation note
The current `delete_story` is on the legacy filesystem path. The fix needs to wrap it in a transaction that touches every layer:
1. Cancel any pending timers for this story_id (read timers.json, filter, write back)
2. Stop any running/pending agents for this story_id (call `agent_pool.stop_agent` for each)
3. Remove the worktree if it exists (`git worktree remove`)
4. Write a CRDT delete op (`apply_and_persist` with a delete op)
5. Wait for the persist task to confirm
6. Delete the row from `pipeline_items` directly (or trust the materialiser to drop it)
7. Remove the filesystem shadow
Each step should be best-effort with logging — partial failures should be visible, not silent.
## Context
The MCP `delete_story` tool currently only **removes the filesystem markdown** from `.huskies/work/N_stage/`. It does NOT:
- Remove the row from `pipeline_items`
- Write a CRDT delete op to `crdt_ops`
- Tear down the in-memory CRDT entry
- Remove the `.huskies/worktrees/N_…/` worktree
- Cancel any pending rate-limit retry timers in `.huskies/timers.json`
So after `delete_story`, the story keeps appearing in `get_pipeline_status` (because the in-memory CRDT still has it), the timer fires and re-spawns an agent, the agent runs in the still-existing worktree, and the user has no idea why the "deleted" story keeps coming back.
## Real user story
As a user calling `delete_story` (via MCP, web UI, or chat command), I want a complete tear-down of all state associated with that story across every layer, so the story is actually gone — no in-memory cache entries, no pending agents, no timers, no worktree, no shadow files, no future spawns.
## Observed 2026-04-09
Repeatedly throughout the session. The most concrete example was around 17:20: I called `delete_story 478_…`, the tool returned success, the markdown file at `.huskies/work/1_backlog/478_…md` was removed, but at 17:25:17 the rate-limit retry timer fired and **re-spawned a coder-1 on the deleted story** because the worktree still existed, the pipeline_items row still existed, and the timer entry still existed in `.huskies/timers.json`. We then had to do sqlite surgery + manual worktree removal + manual timers.json edit to actually kill 478.
## Implementation note
The current `delete_story` is on the legacy filesystem path. The fix needs to wrap it in a transaction that touches every layer:
1. Cancel any pending timers for this story_id (read timers.json, filter, write back)
2. Stop any running/pending agents for this story_id (call `agent_pool.stop_agent` for each)
3. Remove the worktree if it exists (`git worktree remove`)
4. Write a CRDT delete op (`apply_and_persist` with a delete op)
5. Wait for the persist task to confirm
6. Delete the row from `pipeline_items` directly (or trust the materialiser to drop it)
7. Remove the filesystem shadow
Each step should be best-effort with logging — partial failures should be visible, not silent.
## Acceptance Criteria
- [ ] delete_story returns success only when ALL of the following are true: no row in pipeline_items, no op in crdt_ops referencing the story_id (or a delete op present), no in-memory CRDT entry, no worktree directory, no timer entries, no filesystem shadow
- [ ] Each tear-down step has its own log line so partial failures are diagnosable
- [ ] If any tear-down step fails, the tool returns an error with which step failed and what was already torn down (so the user can finish the cleanup manually)
- [ ] After delete_story, the story does NOT appear in get_pipeline_status, the web UI, or list_agents
- [ ] After delete_story, no rate-limit retry timer can re-spawn an agent on the deleted story
- [ ] Regression test using the 2026-04-09 repro: schedule a rate-limit timer for the story, call delete_story, fast-forward 5 minutes, assert no agent spawned
## Out of Scope
- TBD
@@ -0,0 +1,54 @@
---
name: "Add a debug MCP tool to dump the in-memory CRDT state for inspection"
---
# Story 515: Add a debug MCP tool to dump the in-memory CRDT state for inspection
## User Story
**Note:** content stuffed into user_story per bug 509 workaround.
---
## Context
When diagnosing CRDT/state issues today, we had no way to look at the **in-memory** CRDT state directly. The closest available views were:
- `get_pipeline_status` — gives a summarised pipeline-shaped view (active/backlog/done) but hides the raw item structure, the index map, the lamport clock state, etc.
- Querying `crdt_ops` directly via sqlite — gives the *persisted* state, which can diverge from the in-memory state (we saw this with bug 511, where post-restart writes use reset seq counters)
- `read_item(story_id)` in `crdt_state.rs` — exists, returns a `PipelineItemView`, but is not exposed via MCP or HTTP
The result: every time I needed to check the CRDT state, I was either inferring it from `get_pipeline_status` (lossy) or querying the persisted ops (lagging the in-memory state). Neither gave me the ground truth.
## Real user story
As a developer debugging huskies state issues, I want an MCP tool (or HTTP debug endpoint) that returns a structured dump of the in-memory CRDT state, so I can see exactly what the running server thinks is true without inferring from summaries.
## Suggested API
- Tool name: `mcp__huskies__dump_crdt`
- Args: optional `story_id` filter (single story) or no args (dump everything)
- Returns: JSON with one entry per item containing: `story_id`, all field values (`stage`, `name`, `agent`, `retry_count`, `blocked`, `depends_on`), the CRDT path/index bytes (for cross-referencing with `crdt_ops`), the local lamport seq counter, and a flag indicating whether the item is `is_deleted`
- Returns metadata: total item count, current local seq counter value, count of pending ops in `persist_tx` channel (if observable)
## Observed 2026-04-09
This story would have saved us significant debugging time. Specific examples:
- When 478 was missing from `get_pipeline_status` after the manual sqlite insert, we had to infer "the API reads from in-memory CRDT, not from pipeline_items" by looking at source code. A `dump_crdt 478_…` call would have returned "not found" immediately, confirming the same conclusion.
- When 503 was showing at stage=merge in the API but only had a creation op at stage=1_backlog in `crdt_ops`, we had to manually search for content-indexed update ops to figure out where the post-restart updates went. A dump tool showing the current in-memory state vs the persisted op count would have made the divergence obvious.
## Acceptance Criteria
- [ ] New MCP tool `dump_crdt` is registered and callable
- [ ] With no args, returns all items in the in-memory CRDT as a structured JSON list
- [ ] With a story_id arg, returns just that one item (or null if not found)
- [ ] Each item entry includes: story_id, stage, name, agent, retry_count, blocked, depends_on, content_index (hex), is_deleted
- [ ] Returns top-level metadata: total_items, max_local_seq, pending_persist_ops_count (if available), in_memory_state_loaded (bool)
- [ ] Tool description is clear that this is a debug tool, not for normal pipeline introspection (those should use get_pipeline_status)
- [ ] Optional: also expose via HTTP at `/debug/crdt` for browser inspection
- [ ] Documented in README.md under a 'debugging' section
## Out of Scope
- TBD
@@ -0,0 +1,53 @@
---
name: "update_story.description should create the ## Description section if it doesn't exist (instead of erroring)"
---
# Story 516: update_story.description should create the ## Description section if it doesn't exist (instead of erroring)
## User Story
**Note:** content stuffed into user_story per bug 509 workaround.
---
## Context
The MCP `update_story` tool's `description` parameter "replaces the `## Description` section content". If the section doesn't exist in the story file, the call **errors out** with `Section '## Description' not found in story file.`
This becomes a real problem when:
1. A story was created via `create_story` (which is buggy per 509 and writes a stub template with no `## Description` section)
2. The user later wants to add a description via `update_story`
3. The update fails with the cryptic "section not found" error
We hit this exact scenario today: after bug 509 dropped the descriptions of 6 stories (500, 504, 505, 506, 507, 508), I tried to recover them by calling `update_story` with `description=...` — and the call errored out because the stub template the buggy `create_story` had written had no `## Description` section. We had to fall back to stuffing everything into the `user_story` field.
## Real user story
As a user calling `update_story.description` on any story (regardless of how it was originally created), I want the call to either replace the existing `## Description` section OR create one if it doesn't exist, so I never have to think about the template structure.
## Implementation note
The simplest fix is in the `update_story_in_file` (or equivalent) function: when looking for the `## Description` section, if not found, **insert it** at a sensible location — probably between `## User Story` and `## Acceptance Criteria` — and then write the description content there.
Related: this story partially covers the workaround for bug 509 (create_story drops description). If 509 is fixed first, the templates would always have a `## Description` section and this wouldn't matter. But this fix is still valuable for older stories created before 509 lands, AND for stories created via legacy paths that don't use the canonical template.
## Observed 2026-04-09
```
> update_story story_id=500_story_remove_duplicate_pty_debug_log_lines description="..."
Error: Section '## Description' not found in story file.
```
## Acceptance Criteria
- [ ] update_story.description succeeds whether or not the target story has a pre-existing ## Description section
- [ ] When the section is missing, it is created at a consistent location (between ## User Story and ## Acceptance Criteria)
- [ ] When the section exists, the existing replace-content behaviour is preserved (no regression)
- [ ] Unit test covering both: section-exists path AND section-missing path
- [ ] Symmetric fix for update_story.user_story (if it has the same brittleness)
- [ ] Error messages for genuine failure modes (file not found, write failed) are still distinct from the now-silent missing-section case
## Out of Scope
- TBD
@@ -0,0 +1,98 @@
---
name: "Remove filesystem-shadow fallback paths from lifecycle.rs (finish the migration to CRDT-only)"
---
# Story 517: Remove filesystem-shadow fallback paths from lifecycle.rs (finish the migration to CRDT-only)
## User Story
**Depends on story 520** (typed pipeline state machine). Once 520 lands and consumers are migrated to the typed transition API, the lifecycle module no longer needs filesystem fallbacks — all state changes go through the typed `transition` function and the event bus. This story becomes the natural cleanup pass after 520 + 512 + 514 land.
---
**Note:** content stuffed into user_story per bug 509 workaround.
## Context
`server/src/agents/lifecycle.rs::move_item` (the helper that backs `move_story_to_current`, `move_story_to_done`, `move_story_to_merge`, etc.) has **three execution paths**:
1. **CRDT-first path** (the "happy" post-migration path) — calls `crdt_state::read_item(story_id)`, then `db::move_item_stage`, which writes a CRDT op and broadcasts events
2. **Content-store fallback** — if the story isn't in CRDT but exists in the db's content store, import it via `db::write_item_with_content`
3. **Filesystem fallback** — if neither, scan `.huskies/work/N_stage/` for a markdown file, import it to the DB
Paths 2 and 3 are **migration scaffolding**. They were necessary while stories existed only on disk and the CRDT was empty, but post-491/492 they should be unnecessary. Worse, they actively *cause* drift today:
- The filesystem fallback can re-import stale shadow files into the DB, undoing intentional deletes
- The path 3 search is blind to which stage a story "should" be in per the DB — it picks whatever stage dir has the file, which can promote stale shadows
- This is the mechanism that makes bug 510 (split-brain shadow promotion) possible
`move_story_to_current` is hardcoded to read from `["1_backlog"]`, which is also part of the same legacy filesystem assumption.
## Real user story
As a developer maintaining huskies, I want the lifecycle code to operate exclusively on the CRDT/DB and never touch filesystem shadows, so state drift is eliminated and the post-migration architecture is consistent.
## Implementation plan
1. Inventory every code path in `lifecycle.rs` that touches the filesystem under `.huskies/work/`
2. For each, determine whether it's a *read* (legacy fallback — can be removed if we're confident all stories are in CRDT now) or a *write* (legacy mirror — can be deferred to a separate filesystem-renderer task that derives state from CRDT)
3. Remove the read fallbacks
4. Move the writes to a downstream materialiser task that writes the filesystem shadows from CRDT events (so they're strictly read-only renderings)
5. Run the bug-510 reconcile pass at startup (story TBD) before this lands, to ensure no story is stranded with only a filesystem shadow
## Observed 2026-04-09
We watched the filesystem fallback paths cause harm multiple times today:
- Bug 510 split-brain: filesystem shadows in `1_backlog/` got re-promoted by timer fires after the DB had already moved the story to `5_done`
- The 478 worktree's `move_story_to_current` no-op'd because there was no `1_backlog` shadow — even though 478 was in `4_merge` per the DB (this was actually correct behaviour given the function's narrow `from = ["1_backlog"]`, but it surfaces how filesystem-bound the function is)
- Lifecycle moves were happening on the filesystem without writing CRDT ops (we initially mis-diagnosed this as "no transition ops in CRDT" before finding bug 511)
## Context
`server/src/agents/lifecycle.rs::move_item` (the helper that backs `move_story_to_current`, `move_story_to_done`, `move_story_to_merge`, etc.) has **three execution paths**:
1. **CRDT-first path** (the "happy" post-migration path) — calls `crdt_state::read_item(story_id)`, then `db::move_item_stage`, which writes a CRDT op and broadcasts events
2. **Content-store fallback** — if the story isn't in CRDT but exists in the db's content store, import it via `db::write_item_with_content`
3. **Filesystem fallback** — if neither, scan `.huskies/work/N_stage/` for a markdown file, import it to the DB
Paths 2 and 3 are **migration scaffolding**. They were necessary while stories existed only on disk and the CRDT was empty, but post-491/492 they should be unnecessary. Worse, they actively *cause* drift today:
- The filesystem fallback can re-import stale shadow files into the DB, undoing intentional deletes
- The path 3 search is blind to which stage a story "should" be in per the DB — it picks whatever stage dir has the file, which can promote stale shadows
- This is the mechanism that makes bug 510 (split-brain shadow promotion) possible
`move_story_to_current` is hardcoded to read from `["1_backlog"]`, which is also part of the same legacy filesystem assumption.
## Real user story
As a developer maintaining huskies, I want the lifecycle code to operate exclusively on the CRDT/DB and never touch filesystem shadows, so state drift is eliminated and the post-migration architecture is consistent.
## Implementation plan
1. Inventory every code path in `lifecycle.rs` that touches the filesystem under `.huskies/work/`
2. For each, determine whether it's a *read* (legacy fallback — can be removed if we're confident all stories are in CRDT now) or a *write* (legacy mirror — can be deferred to a separate filesystem-renderer task that derives state from CRDT)
3. Remove the read fallbacks
4. Move the writes to a downstream materialiser task that writes the filesystem shadows from CRDT events (so they're strictly read-only renderings)
5. Run the bug-510 reconcile pass at startup (story TBD) before this lands, to ensure no story is stranded with only a filesystem shadow
## Observed 2026-04-09
We watched the filesystem fallback paths cause harm multiple times today:
- Bug 510 split-brain: filesystem shadows in `1_backlog/` got re-promoted by timer fires after the DB had already moved the story to `5_done`
- The 478 worktree's `move_story_to_current` no-op'd because there was no `1_backlog` shadow — even though 478 was in `4_merge` per the DB (this was actually correct behaviour given the function's narrow `from = ["1_backlog"]`, but it surfaces how filesystem-bound the function is)
- Lifecycle moves were happening on the filesystem without writing CRDT ops (we initially mis-diagnosed this as "no transition ops in CRDT" before finding bug 511)
## Acceptance Criteria
- [ ] Inventory of every filesystem touch in lifecycle.rs is documented in the story body or a follow-up comment
- [ ] All read fallbacks in lifecycle.rs (paths 2 and 3 above) are removed
- [ ] All write paths in lifecycle.rs that mirror to the filesystem are moved to a separate materialiser task driven by CRDT events
- [ ] After the change, lifecycle.rs has zero direct std::fs:: calls under .huskies/work/
- [ ] move_story_to_current no longer hardcodes from=['1_backlog'] — it reads the source stage from CRDT
- [ ] Regression: the existing 'try filesystem fallback' tests are updated to test the new CRDT-only path instead of being deleted
- [ ] A pre-flight script verifies all existing stories are in CRDT before this change lands (so nothing gets stranded)
- [ ] Bug 510 (split-brain shadows) no longer reproduces after this change
## Out of Scope
- TBD
@@ -0,0 +1,62 @@
---
name: "apply_and_persist should log when persist_tx send fails instead of silently dropping the op"
---
# Story 518: apply_and_persist should log when persist_tx send fails instead of silently dropping the op
## User Story
**Note:** content stuffed into user_story per bug 509 workaround.
---
## Context
`server/src/crdt_state.rs::apply_and_persist` updates the in-memory CRDT and then sends the signed op to the persistence task via a channel:
```rust
fn apply_and_persist<F>(state: &mut CrdtState, op_fn: F) {
let raw_op = op_fn(state);
let signed = raw_op.sign(&state.keypair);
state.crdt.apply(signed.clone()); // in-memory update
let _ = state.persist_tx.send(signed.clone()); // ← fire-and-forget, error dropped
...
}
```
The `let _ = ...` discards the return value of `send()`. If the channel is closed (because the persistence task panicked, was shut down, or has dropped its receiver), the op is silently dropped from persistence — but the in-memory CRDT is already updated. The next restart will replay only the persisted ops, and the in-memory state will quietly diverge from the persisted state.
This is also one of the candidate causes for some of the state drift we've been chasing. It's hard to rule out because there's no log line to confirm whether the persist task is still alive or whether sends are succeeding.
## Real user story
As a developer or operator, I want any failure of `persist_tx.send()` to be logged immediately at WARN or ERROR level, so silent persistence loss is detectable instead of invisible.
## Observed 2026-04-09
Spent significant time investigating whether persist sends were silently failing. Eventually ruled it out empirically (we found that ops WERE being persisted, just with reset seq counters per bug 511). But the diagnosis would have been minutes instead of an hour if there was a log line to check.
## Fix (small)
```rust
if let Err(e) = state.persist_tx.send(signed.clone()) {
crate::slog_error!(
"[crdt] Failed to send op to persist task: {e}; persist task may be dead. \
In-memory state is now ahead of persisted state."
);
}
```
Apply the same fix at every `let _ = state.persist_tx.send(...)` site in crdt_state.rs (there are at least 2 — one in apply_and_persist, one in apply_remote_op).
## Acceptance Criteria
- [ ] Every call site of `state.persist_tx.send(...)` in crdt_state.rs logs at ERROR level on send failure
- [ ] The error message includes the channel error and a clear note that 'in-memory and persisted state may have diverged'
- [ ] Unit test: shut down the persist receiver (drop the rx end), call write_item, assert an error is logged
- [ ] No regression in the happy path (no extra log lines on success)
- [ ] Consider: also expose a counter / metric for persist send failures so it can be monitored without grepping logs
## Out of Scope
- TBD
@@ -0,0 +1,107 @@
---
name: "mergemaster should detect no-commits-ahead-of-master and fail loudly instead of exiting silently"
---
# Story 519: mergemaster should detect no-commits-ahead-of-master and fail loudly instead of exiting silently
## User Story
**Depends on story 520** (typed pipeline state machine). Once 520 lands, this story largely *evaporates*: `Stage::Merge` is defined as `Merge { feature_branch: BranchName, commits_ahead: NonZeroU32 }`, so a merge state with zero commits ahead is **structurally unrepresentable**. The transition `Current → Merge` (or `Qa → Merge`) is required to provide a NonZeroU32 — the type system enforces it. This story remains useful as a *defensive runtime check* during the migration window before 520 lands; afterwards, it should be closed as redundant.
---
**Note:** content stuffed into user_story per bug 509 workaround.
## Context
When mergemaster runs on a story whose worktree has **zero commits ahead of master** (e.g. because `create_worktree` always creates from master and the original feature branch was never checked out into the worktree), it currently:
1. Spawns its claude session
2. Runs `merge_agent_work` MCP tool
3. Finds nothing to merge
4. Exits cleanly with `[agent:N:mergemaster] Done. Session: ...`
5. **Does not log any error or warning**
6. **Spends real money** on the empty session — we observed `cost=$0.82` for one such no-op run
The user has no signal that the merge didn't actually happen. The matrix bot fires a "QA → Merge" stage notification (because the story did move stages internally), then nothing — no `🎉 Merge → Done` notification follows. Master is unchanged.
## Real user story
As a user watching the pipeline, I want mergemaster to detect "this worktree has no commits ahead of master" *before* spending money on a Claude session, and fail loudly with a clear error so I know to investigate the upstream cause (probably the worktree got reset to master).
## Observed 2026-04-09
Around 18:31:51, mergemaster spawned for 478 in a worktree that had been reset to master by the orphan cleanup logic at 18:29:54. By the time mergemaster ran, the worktree was on master with zero commits ahead. It ran a session, spent $0.82, exited "Done", and didn't merge anything. We didn't notice for several minutes because the failure was completely silent. We had to manually `git log master..feature/story-478_…` to confirm there was no merge commit on master.
## Fix
In mergemaster's startup sequence (probably in advance.rs or wherever the mergemaster session is spawned), add a pre-flight check:
```rust
let commits_ahead = git_commits_ahead(worktree_path, "master")?;
if commits_ahead == 0 {
slog_error!(
"[mergemaster] worktree {worktree_path} has no commits ahead of master; \
refusing to spawn merge session. Likely cause: worktree was reset to \
master after the feature branch's commits were created. Investigate the \
worktree's git state before retrying."
);
return Err("no commits to merge".into());
}
```
This costs ~milliseconds (one git command) and saves the cost of an entire Claude session per false-positive.
## Context
When mergemaster runs on a story whose worktree has **zero commits ahead of master** (e.g. because `create_worktree` always creates from master and the original feature branch was never checked out into the worktree), it currently:
1. Spawns its claude session
2. Runs `merge_agent_work` MCP tool
3. Finds nothing to merge
4. Exits cleanly with `[agent:N:mergemaster] Done. Session: ...`
5. **Does not log any error or warning**
6. **Spends real money** on the empty session — we observed `cost=$0.82` for one such no-op run
The user has no signal that the merge didn't actually happen. The matrix bot fires a "QA → Merge" stage notification (because the story did move stages internally), then nothing — no `🎉 Merge → Done` notification follows. Master is unchanged.
## Real user story
As a user watching the pipeline, I want mergemaster to detect "this worktree has no commits ahead of master" *before* spending money on a Claude session, and fail loudly with a clear error so I know to investigate the upstream cause (probably the worktree got reset to master).
## Observed 2026-04-09
Around 18:31:51, mergemaster spawned for 478 in a worktree that had been reset to master by the orphan cleanup logic at 18:29:54. By the time mergemaster ran, the worktree was on master with zero commits ahead. It ran a session, spent $0.82, exited "Done", and didn't merge anything. We didn't notice for several minutes because the failure was completely silent. We had to manually `git log master..feature/story-478_…` to confirm there was no merge commit on master.
## Fix
In mergemaster's startup sequence (probably in advance.rs or wherever the mergemaster session is spawned), add a pre-flight check:
```rust
let commits_ahead = git_commits_ahead(worktree_path, "master")?;
if commits_ahead == 0 {
slog_error!(
"[mergemaster] worktree {worktree_path} has no commits ahead of master; \
refusing to spawn merge session. Likely cause: worktree was reset to \
master after the feature branch's commits were created. Investigate the \
worktree's git state before retrying."
);
return Err("no commits to merge".into());
}
```
This costs ~milliseconds (one git command) and saves the cost of an entire Claude session per false-positive.
## Acceptance Criteria
- [ ] Before mergemaster spawns its Claude session, it runs `git log master..HEAD --oneline` (or equivalent) on the worktree
- [ ] If the result is empty (zero commits ahead), mergemaster exits early with an ERROR log line and does NOT spawn the session
- [ ] The error message is specific enough that the user can diagnose the upstream cause (e.g. mentions 'worktree was reset' and suggests checking the worktree's branch)
- [ ] The matrix bot sends a clear failure notification (NOT a successful 🎉 emoji) when this happens
- [ ] The story does not advance to a 'done' state when mergemaster exits this way; it stays in 4_merge with a clear blocked status
- [ ] Regression test: create a worktree on master (no feature commits), invoke mergemaster, assert the early exit happens and no Claude session is spawned
- [ ] Cost saving observed in the 2026-04-09 incident ($0.82 per no-op session) is documented in the test as the motivation
## Out of Scope
- TBD
@@ -0,0 +1,192 @@
---
name: "Typed pipeline state machine in Rust (foundation: replaces stringly-typed CRDT views with strict enums, subsumes 436)"
---
# Story 520: Typed pipeline state machine in Rust (foundation: replaces stringly-typed CRDT views with strict enums, subsumes 436)
## User Story
**Note:** content stuffed into user_story per bug 509 workaround.
---
## Context
Today huskies represents pipeline state as a loose JSON document inside the BFT JSON CRDT. Each story has fields like `stage: String`, `agent: String`, `retry_count: f64`, `blocked: bool`, `depends_on: String` (JSON-encoded list, double-encoded). This stringly-typed representation allows **many impossible states** to be representable in the data model:
- `stage = "9_invalid"` — typo, no compile error
- `stage = "5_done"` + `blocked = true` — a done story is blocked? what does that mean?
- `stage = "4_merge"` with no commits ahead of master — the silent mergemaster failure mode (today's story 478)
- A coder agent assigned to a story in `4_merge` — bug 502, the loop we fought all day today
- `retry_count = 3.7` — fractional retry counts (it's an f64 because that's what JSON CRDTs do)
- `agent = "coder-1"` AND `stage = "1_backlog"` — backlog story has an agent? sentinel encoding via empty string
Multiple bugs filed today (501, 502, 510, 511) exist *because* the type system can't enforce the pipeline invariants. **Patching individual symptoms forever is the wrong strategy.** The right strategy is to make impossible states unrepresentable at the Rust type level, using a typed state machine layered on top of the loose CRDT. The CRDT can stay loose at the persistence layer (it has to be — that's what makes it merge correctly across nodes), but every consumer above the CRDT operates on strict typed enums.
## Real user story
As a developer working on huskies, I want the pipeline state to be expressed as a strict Rust state machine where impossible states and impossible transitions are compile-time errors, so future bugs in this category become structural rather than runtime drift.
## Design
### Two enum hierarchies
**Synced state (CRDT-backed, converges across nodes):**
```rust
enum Stage {
Backlog,
Current,
Qa,
Merge { feature_branch: BranchName, commits_ahead: NonZeroU32 },
Done { merged_at: DateTime<Utc>, merge_commit: GitSha },
Archived { archived_at: DateTime<Utc>, reason: ArchiveReason },
}
enum ArchiveReason {
Completed, // normal accept_story → archived
Abandoned, // user explicitly abandoned
Superseded { by: StoryId },
Blocked { reason: String }, // was bug 436's `blocked: true`
MergeFailed { reason: String }, // was bug 436's `merge_failure`
ReviewHeld { reason: String }, // was bug 436's `review_hold`
}
struct PipelineItem {
story_id: StoryId, // newtype, validated
name: String,
stage: Stage, // typed enum, all variants are valid by construction
depends_on: Vec<StoryId>, // parsed, not stringified
retry_count: u32, // not f64
// No more separate `blocked`, `merge_failure`, `review_hold` — folded into Stage::Archived
}
```
**Per-node execution state (CRDT-backed under node_id key, local-authored but globally-readable):**
```rust
enum ExecutionState {
Idle,
Pending { agent: AgentName, since: DateTime<Utc> },
Running { agent: AgentName, started_at: DateTime<Utc>, last_heartbeat: DateTime<Utc> },
RateLimited { agent: AgentName, resume_at: DateTime<Utc> },
Completed { agent: AgentName, exit_code: i32, completed_at: DateTime<Utc> },
}
// In the CRDT document, ExecutionState is stored under each node's pubkey:
// crdt.execution_state: { node_pubkey → { story_id → ExecutionState } }
```
The execution state lives in the CRDT under **each node's pubkey**. Each node only writes to entries where `node_pubkey == self.pubkey`, so there's no merge conflict — concurrent writes from the same author follow LWW, concurrent writes from different authors target different entries entirely. All nodes can READ all execution states across the mesh.
**This per-node-keyed CRDT pattern enables:**
- **Cross-node observability** — matrix bot can show "node A is running coder-1 on story X, node B is rate-limited on story Y"
- **Heartbeat detection** — if a node hasn't updated its execution_state in N minutes, the entry is "stale" (laptop closed, process crashed, oom kill, etc.)
- **Foundation for story 479** (CRDT work claiming) — a node knows what other nodes are doing *before* claiming work
- **Stuck job recovery** — if node A's heartbeat dies mid-run, node B can see the stuck state and decide whether to take over
- **Crash forensics** — the last persisted ExecutionState before a crash is preserved in CRDT, accessible from any node
### The transition function
```rust
fn transition(
state: PipelineItem,
event: PipelineEvent,
) -> Result<PipelineItem, TransitionError>
```
Pure function. Takes the current state and an event, returns either the new state or a TransitionError. The compiler enforces that the result of every transition is structurally valid — you can't construct a `Stage::Merge` without `commits_ahead: NonZeroU32`, you can't construct a `Stage::Done` without a `merge_commit: GitSha`, etc.
**The set of valid transitions is small** (roughly 10):
- `Backlog → Current` — deps met, auto-assign promotes
- `Current → Qa` — gates start
- `Current → Merge` — qa: server, gates auto-pass
- `Qa → Merge` — gates pass
- `Qa → Current` — gates fail, retry
- `Merge → Done` — mergemaster squash succeeds; *requires `Merge.commits_ahead > 0`*
- `Done → Archived(Completed)` — accept_story
- `* → Archived(Blocked / MergeFailed / ReviewHeld)` — stuck-state move
- `Archived(Blocked) → Backlog` — unblock
Anything else is a `TransitionError`. The compiler refuses to compile code that constructs invalid transitions.
### The event subscriber pattern
State changes fire events on a bus. Side-effect handlers subscribe independently:
```rust
type TransitionEvent = (PipelineItem /* before */, PipelineItem /* after */);
bus.subscribe("matrix-bot", |before, after| matrix_bot.notify_stage_change(before, after));
bus.subscribe("filesystem", |before, after| fs_renderer.update(after));
bus.subscribe("pipeline-table", |before, after| pipeline_items_table.upsert(after));
bus.subscribe("auto-assign", |before, after| auto_assign.poke_if_relevant(after));
bus.subscribe("web-ui-broadcast", |before, after| ws_clients.broadcast(after));
```
Each subscriber is independent and concerns itself only with its own dispatch. Adding a new side effect = adding a new subscriber, not editing the transition function. **The "many things happen on state changes" complexity moves out of the state machine and into the bus consumers**, where each piece is testable in isolation.
### Projection layer (loose CRDT ↔ typed Rust)
The bft-json-crdt JSON document is the persistence layer. The typed enums are the application layer. A projection function bridges them at one carefully-controlled boundary:
```rust
impl TryFrom<&PipelineItemCrdt> for PipelineItem {
type Error = ProjectionError;
fn try_from(crdt: &PipelineItemCrdt) -> Result<Self, ProjectionError> { ... }
}
impl From<&PipelineItem> for PipelineItemCrdt {
fn from(item: &PipelineItem) -> Self { ... }
}
```
When the CRDT contains data the typed layer can't parse (e.g. a stage value from a future huskies version, OR a merge that produces an inconsistent intermediate state), `try_from` returns a `ProjectionError`. The error surfaces to the caller — it doesn't silently propagate as garbage. The validation happens at exactly one point: the projection boundary.
## What this subsumes
**Story 436** ("Unify story stuck states into a single status field") is subsumed by the `Stage::Archived { reason: ArchiveReason }` variant. The unified status field IS the `ArchiveReason` enum. Story 436 is marked superseded by this story.
## What this enables (concrete bug eliminations)
- **Bug 502** becomes unrepresentable: there's no way to construct `Stage::Merge` with a Coder agent — Coder agents only attach to `Current` / `Qa` stages, and that constraint is in the type signature of the transition function.
- **Bug 510** becomes irrelevant: there's no "stage='1_backlog' filesystem shadow vs stage='5_done' DB" drift, because Stage is a typed enum with a single source of truth (the CRDT), and projections are derived deterministically.
- **Bug 519** (mergemaster silent on no-op merge) becomes unrepresentable: `Stage::Merge` requires `commits_ahead: NonZeroU32`. You can't construct a Merge state with zero commits.
- **Bug 511** (lamport seq reset) becomes detectable: the projection layer notices when CRDT data fails to parse cleanly and surfaces a ProjectionError instead of silently producing garbage in-memory state.
- **Story 479** (CRDT work claiming) has a clean foundation: ExecutionState gives every node visibility into what every other node is doing, including stale-heartbeat detection.
- **Future state machine bugs become compile errors**, not runtime drift.
## Implementation order
1. Define the `Stage`, `ArchiveReason`, `ExecutionState`, `PipelineItem` types in a new module (e.g. `server/src/pipeline_state.rs`).
2. Implement the projection layer (try_from / from for PipelineItemCrdt).
3. Implement the `transition` function with exhaustive valid transitions.
4. Implement the event bus.
5. Migrate consumers ONE AT A TIME — chat commands, lifecycle, API, auto-assign, matrix bot. Each migration is isolated; the compiler tells you when you've missed something.
6. Once nothing reads the loose `PipelineItemView` anymore, delete it.
7. Story 436 closes when this lands.
## Acceptance Criteria
- [ ] A new module (e.g. server/src/pipeline_state.rs) defines the Stage, ArchiveReason, ExecutionState, and PipelineItem types with the variants described in the design
- [ ] Stage::Merge has a NonZeroU32 commits_ahead field (so the bug 519 silent no-op merge is unrepresentable)
- [ ] Stage::Done has GitSha merge_commit and DateTime merged_at fields (so a 'done' story always has merge metadata)
- [ ] ArchiveReason enum subsumes the old blocked / merge_failure / review_hold front matter fields, with a sub-reason variant for each
- [ ] PipelineItem.depends_on is Vec<StoryId>, not String (no more JSON-as-string)
- [ ] ExecutionState lives in the CRDT under per-node-pubkey keys; each node only writes to its own subspace (validated by CRDT signature check)
- [ ] Last_heartbeat field is updated periodically by the running node so other nodes can detect stale entries
- [ ] A pure transition(state, event) -> Result<PipelineItem, TransitionError> function exists and is exhaustively pattern-matched
- [ ] Every valid transition listed in the design (~10) is implemented and unit-tested with both success and error cases
- [ ] The TryFrom<&PipelineItemCrdt> for PipelineItem projection function handles every currently-valid CRDT state and returns a structured ProjectionError for invalid ones (instead of silently propagating garbage)
- [ ] An event bus pattern is in place where matrix bot, filesystem renderer, pipeline_items materialiser, auto-assign, and web UI broadcaster are independent subscribers
- [ ] All call sites that previously read item.stage as a string or used the blocked / merge_failure / review_hold fields are migrated to the typed enum API
- [ ] Story 436 is closed as superseded by this story
- [ ] Bug 502 has a regression test that confirms the type system prevents the loop (the test should be a compile-fail test if possible)
- [ ] Bug 510 (filesystem shadow split-brain) no longer reproduces after this lands, because the typed state machine has a single source of truth
- [ ] Documentation in README.md or a new ARCHITECTURE.md explains the type hierarchy, the transition function, the event bus pattern, and the per-node ExecutionState convention
## Out of Scope
- TBD
@@ -0,0 +1,87 @@
---
name: "MCP/HTTP capability to write a CRDT tombstone (delete op) for a story, to clear it from in-memory state"
---
# Story 521: MCP/HTTP capability to write a CRDT tombstone (delete op) for a story, to clear it from in-memory state
## User Story
**Note:** content stuffed into user_story per bug 509 workaround.
---
## Context
Today (2026-04-09) we discovered the hard way that **there is no way to remove a story from huskies's running in-memory state without restarting the server process**. The state machines that keep stories alive include:
1. The persisted CRDT op log (`crdt_ops` table) — direct sqlite DELETE works
2. The in-memory CRDT view (`CRDT_STATE` global in `server/src/crdt_state.rs`) — **no eviction API**
3. The in-memory content store (`CONTENT_STORE` in `server/src/db/mod.rs:46`) — has `delete_content()` but no MCP / HTTP exposure
4. The shadow `pipeline_items` table — direct sqlite DELETE works
5. Filesystem shadows under `.huskies/work/``find -delete` works
6. `timers.json` — direct file edit works
If a story gets into a bad state (split-brain, ghost row, runaway timer respawning it), we can scrub all the *persistent* layers (1, 4, 5, 6) but the *in-memory* layers (2, 3) keep regenerating it because some periodic code reads in-memory state and writes new ops based on what it sees. The only way to clear in-memory state today is `docker restart huskies`, which is heavy and disrupts the matrix bot, web UI, and any in-flight agents.
We need a **scoped, surgical capability** to write a CRDT tombstone op for a single story_id, which:
- Marks the in-memory item as `is_deleted = true`
- Persists the tombstone op to `crdt_ops` so future replays don't resurrect the story
- Removes the story from `CONTENT_STORE`
- Cleans up any pending `timers.json` entries for the story
- Cancels any running agents on the story
…and exposes it as an MCP tool (e.g. `mcp__huskies__purge_story`) and ideally an HTTP endpoint, so an operator can "kill it with fire" without restarting the server.
## Real user story
As a huskies operator, I want a single MCP/HTTP call that completely removes a story from every layer of state — persistent AND in-memory — so I never have to restart the entire server just to clean up one stuck story.
## Observed 2026-04-09
We spent the last hour of this session whack-a-moling stories 503 and 478. Even after:
- `DELETE FROM pipeline_items WHERE id LIKE '503%'`
- `DELETE FROM crdt_ops WHERE op_json LIKE '%503_bug_depends_on%'`
- `mcp stop_agent + remove_worktree` for the running coders ✓
- `find .huskies/work -name '503_*' -delete`
- emptying `timers.json` (multiple times — kept getting re-populated) ✓
…503 kept reappearing in `current` with new agents being spawned. The root cause: the in-memory `CRDT_STATE` (loaded from `crdt_ops` at startup at 18:19) still had 503 and 478 as live items, and a periodic code path was reading `crdt_state::read_all_items()`, seeing them as live, and triggering the auto-assign / rate-limit-retry chain.
Final resolution: `docker restart huskies` to wipe the in-memory state. Worked, but it's a sledgehammer.
## Implementation note
The bft-json-crdt library appears to support per-item delete via the `is_deleted: bool` field on each CRDT item (visible in the persisted op JSON we inspected today). Writing a delete op should look something like:
```rust
crdt_state::apply_and_persist(&mut state, |s| {
s.crdt.doc.items[idx].delete() // or whatever the BFT JSON CRDT delete API is
})
```
The op gets signed, applied to the in-memory state (marking the item deleted), and persisted to crdt_ops via the existing channel. Then `read_all_items()` should filter out `is_deleted: true` entries (it may already do this — verify in `extract_item_view`).
## Why this is distinct from bug 514 (delete_story full cleanup)
Bug 514 is about making the existing `delete_story` MCP tool do a full cleanup across all the layers we know about. **This** story is specifically about acquiring the *capability* to write a CRDT tombstone — without that, bug 514 can't be implemented correctly because it has no way to clear in-memory state. So 521 is a prerequisite for 514.
It's also a prerequisite for properly handling the fix for bug 510 (split-brain shadows) — when the reconcile pass detects a stale story, it needs a way to actually evict it. That eviction is what this story provides.
## Acceptance Criteria
- [ ] A new MCP tool (e.g. `mcp__huskies__purge_story`) is registered and callable
- [ ] The tool takes a story_id and returns a structured result indicating which layers were cleared (CRDT op, content store, timers, agents, worktree, filesystem)
- [ ] The tool writes a signed CRDT tombstone op (is_deleted: true) for the item, applies it to the in-memory CRDT, and persists it to crdt_ops
- [ ] After the tool runs, `read_all_items()` does NOT return the purged story (verify the filter handles is_deleted)
- [ ] After the tool runs, `read_content(story_id)` returns None (CONTENT_STORE entry is removed)
- [ ] After the tool runs, `timers.json` has no entries for the story
- [ ] After the tool runs, no agents are running on the story (stop_agent is called for any active ones)
- [ ] After the tool runs, the worktree at `.huskies/worktrees/{story_id}/` is removed
- [ ] After the tool runs, the filesystem shadow at `.huskies/work/*/{story_id}.md` is removed
- [ ] Idempotent: calling purge_story twice on the same story_id is safe and doesn't error
- [ ] Bug 514 (delete_story full cleanup) is updated to use this purge capability internally
- [ ] Regression test: insert a story via the normal write path, call purge_story, restart the server, verify the story is still gone (i.e. the tombstone persisted correctly)
## Out of Scope
- TBD
@@ -10,7 +10,7 @@ The `prompt_permission` MCP tool returns plain text ("Permission granted for '..
## How to Reproduce ## How to Reproduce
1. Start the storkit server and open the web UI 1. Start the huskies server and open the web UI
2. Chat with the claude-code-pty model 2. Chat with the claude-code-pty model
3. Ask it to do something that requires a tool NOT in `.claude/settings.json` allow list (e.g. `wc -l /etc/hosts`, or WebFetch to a non-allowed domain) 3. Ask it to do something that requires a tool NOT in `.claude/settings.json` allow list (e.g. `wc -l /etc/hosts`, or WebFetch to a non-allowed domain)
4. The permission dialog appears — click Approve 4. The permission dialog appears — click Approve
@@ -6,7 +6,7 @@ name: "Retry limit for mergemaster and pipeline restarts"
## User Story ## User Story
As a developer using storkit, I want pipeline auto-restarts to have a configurable retry limit so that failing agents don't loop infinitely consuming CPU and API credits. As a developer using huskies, I want pipeline auto-restarts to have a configurable retry limit so that failing agents don't loop infinitely consuming CPU and API credits.
## Acceptance Criteria ## Acceptance Criteria

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