Commit Graph

31 Commits

Author SHA1 Message Date
dave 13f7dab5f0 huskies: merge 1088 2026-05-15 02:03:30 +00:00
dave ee20e54d40 huskies: merge 1036 2026-05-14 15:13:25 +00:00
dave c7a7cb4281 huskies: merge 997 2026-05-14 11:06:27 +00:00
dave 309542cf2c huskies: merge 1018 2026-05-14 09:38:15 +00:00
dave 13ab97a615 huskies: merge 1010 2026-05-14 08:12:56 +00:00
dave 4e007bb770 huskies: merge 1009 2026-05-13 22:55:05 +00:00
Timmy c61f715878 fix(1001): stop create_* from half-writing onto tombstoned IDs
Root cause: db::next_item_number scanned the visible CRDT index and the
content store but not the tombstone set, so it would hand out a numeric
ID whose CRDT entry had been tombstoned. crdt_state::write_item then
silently no-op'd the insert (tombstone-match guard) while the content
store and SQLite shadow happily accepted the row, producing a split-
brain half-write that was invisible to every CRDT-driven read path and
couldn't be cleaned up by delete_story / purge_story.

This change closes the loop:

- crdt_state::read::{is_tombstoned, tombstoned_ids} expose the
  tombstone set so callers outside crdt_state can consult it.

- db::next_item_number now scans tombstoned_ids() too. The allocator
  skips past tombstoned numeric IDs instead of treating their slots as
  free.

- write_item logs a WARN when it rejects a write for a tombstoned ID
  (was silent). The warn is a tripwire — if the allocator ever lets one
  slip through again we'll see it in the log.

- create_item_in_backlog adds two defence-in-depth checks:
    (a) before any write, reject if the allocator returned a
        tombstoned ID;
    (b) after the writes, call read_item to confirm the CRDT entry
        materialised. If not, roll back the content-store + shadow-DB
        rows via db::delete_item and return Err.

Regression tests cover the allocator skip, the is_tombstoned accessor,
and the create_item_in_backlog rollback path.

Out of scope for this commit:
- Recovery of the already-half-written items currently in the running
  pipeline (989, 1000, 1001) — Stage 2/3 of the plan, handled
  separately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 19:05:48 +01:00
dave 580480094e huskies: merge 984 2026-05-13 16:47:51 +00:00
dave 430079ecbc huskies: merge 986 2026-05-13 16:01:51 +00:00
dave 91fbad568a huskies: merge 982 2026-05-13 15:34:41 +00:00
dave 184c214c34 huskies: merge 962 2026-05-13 12:05:01 +00:00
dave 8b53e20ca9 huskies: merge 961 2026-05-13 11:27:21 +00:00
dave a7840ea4b0 huskies: merge 946 2026-05-13 08:00:49 +00:00
dave 09a8edc0a1 huskies: merge 919 2026-05-13 06:27:10 +00:00
dave 9ce5a8df0c huskies: merge 945 2026-05-13 06:09:34 +00:00
dave 2f50e2198b huskies: merge 951 2026-05-13 04:34:06 +00:00
Timmy d78dd9e8f9 feat(934): typed Stage enum replaces directory-string state model
The state machine's `Stage` enum becomes the source of truth for pipeline
state. Six stages of work land together:

  1. Clean wire vocabulary (`coding`, `merge`, `merge_failure`, ...) replaces
     legacy directory-style strings (`2_current`, `4_merge`, ...) on the wire.
     `Stage::from_dir` accepted both during deployment; new writes always
     emit the clean form via `stage_dir_name`. Lexicographic `dir >= "5_done"`
     checks in lifecycle.rs become typed `matches!` checks since the new
     vocabulary doesn't sort in pipeline order.
  2. `crdt_state::write_item` takes typed `&Stage`, serialising via
     `stage_dir_name` at the CRDT boundary. `#[cfg(test)] write_item_str`
     parses legacy strings for test fixtures.
  3. `WorkItem::stage()` returns typed `crdt_state::Stage`; `stage_str()`
     is gone from the public API. Projection dispatches on the typed enum.
  4. `frozen` becomes an orthogonal CRDT register. `Stage::Frozen` and
     `PipelineEvent::Freeze`/`Unfreeze` are removed; `transition_to_frozen`/
     `unfrozen` set the flag directly without touching the stage register.
  5. Watcher sweep and `tool_update_story`'s `blocked` setter route through
     `apply_transition` so the typed transition table validates every
     stage change. `update_story` gains a `frozen` field for symmetry.
  6. One-shot startup migration rewrites pre-934 directory-style stage
     registers (and sets `frozen=true` on items previously at `7_frozen`).
     `Stage::from_dir` drops legacy aliases. The db boundary keeps a small
     normaliser so callers with legacy strings (MCP, tests) still work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 22:31:59 +01:00
Timmy 7d7ab85994 feat(933): add item_type + epic CRDT registers + migrate epic mechanism
Replaces the YAML-only `type: epic` / `epic: <id>` front-matter fields with
typed CRDT registers on PipelineItemCrdt. The epic-mechanism MCP tools
(`tool_list_epics`, `tool_show_epic`), the epic-context injection in agent
spawn, and the type-classifier helpers (`item_type_from_id`, `is_bug_item`,
`is_refactor_item`) now all read from the CRDT.

Schema:
- PipelineItemCrdt: `item_type: LwwRegisterCrdt<String>` and
  `epic: LwwRegisterCrdt<String>` registers.
- WorkItem: typed `item_type()` and `epic()` accessors returning `Option<&str>`.
- crdt_state::set_item_type(story_id, Option<&str>) and
  crdt_state::set_epic(story_id, Option<&str>) typed setters.

Write paths populate the new registers:
- create_story_file / create_bug_file / create_spike_file /
  create_refactor_file / create_epic_file — each calls set_item_type after
  write_story_content.
- tool_update_story intercepts `epic` and `type` fields and routes them to
  the typed setters (same pattern as qa / depends_on).

Read paths migrated off yaml_legacy:
- http/mcp/story_tools/epic.rs: tool_list_epics + tool_show_epic.
- agents/lifecycle.rs::item_type_from_id (numeric-only IDs).
- agents/pool/start/spawn.rs epic-context injection.
- http/workflow/bug_ops/bug.rs::is_bug_item, refactor.rs::is_refactor_item.
- http/workflow/pipeline.rs::load_pipeline_state — review_hold/qa/epic_id
  all come from the CRDT now; only merge_failure is still YAML (sweep in
  929 stage 10).

All `yaml_residue(...)` wraps for item_type / epic are removed; the
remaining residue marker doc no longer references 933.

cargo fmt --check, clippy --all-targets -- -D warnings, and the 2857-test
suite all pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 19:58:43 +01:00
Timmy aadbb1b2af feat(932): add review_hold CRDT register + migrate callers off yaml_legacy
review_hold is now a typed bool register on PipelineItemCrdt alongside
blocked / mergemaster_attempted. Exposed via the typed setter
`crdt_state::set_review_hold(story_id, value)` and the
`WorkItem::review_hold()` accessor. Replaces the legacy
`review_hold: true` YAML front-matter field.

Migrated callers:
- http/mcp/qa_tools.rs::tool_approve_qa  — clear via set_review_hold(false)
- agents/lifecycle.rs::reject_story_from_qa  — clear via set_review_hold(false)
- agents/pool/pipeline/advance/helpers.rs::write_review_hold_to_store
  — set via set_review_hold(true), no more content rewrite
- agents/pool/auto_assign/reconcile.rs (two callsites) — set via
  set_review_hold(true) instead of FS YAML write
- agents/pool/auto_assign/story_checks.rs::has_review_hold — reads the
  typed register instead of conflating with Stage::Frozen (real bug fix:
  the legacy implementation returned `stage.is_frozen()`, which made
  the auto-assigner treat *every* held-for-review item as frozen even
  when it wasn't actually parked at the freeze stage).

Dead yaml_legacy helpers removed:
- write_review_hold(path), write_review_hold_in_content(content)
- clear_front_matter_field(path) — last caller was the qa_tools wrap

The yaml_residue marker doc now only mentions 933; the 932 line is gone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 19:49:36 +01:00
dave 148ce37beb huskies: merge 891 2026-05-12 17:09:01 +00:00
dave ce07c4d7b7 huskies: merge 917 2026-05-12 16:22:33 +00:00
Timmy 8421104645 fix(914): thread-local ALL_OPS/VECTOR_CLOCK in cfg(test) so compaction tests don't race
Root cause was not the persist channel (the test-mode channel is unbounded
and its receiver is leaked, so sends never fail). It was that `ALL_OPS` and
`VECTOR_CLOCK` were process-wide `OnceLock` globals while `CRDT_STATE` was
already thread-local — so one test thread's `apply_compaction` would prune
another test thread's freshly-written ops out of the shared journal, and
the subsequent `all_ops_json()` read in `compaction_reduces_ops` would
return fewer than the 5 it had just written.

Mirror the pattern already used for `CRDT_STATE` and `SnapshotState`: in
`cfg(test)` use thread-local `OnceLock<Mutex<...>>`s for the op journal and
vector clock, accessed via new `all_ops_lock()` / `vector_clock_lock()`
helpers. Production code path is unchanged (still the global statics set
during `init()`).

Touches ops/read/snapshot call sites to go through the helpers. Note in
passing that this overlaps backlog story 518; that story is about the
production-side persist path, this is the cfg(test)-only journal-isolation
slice.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 16:09:38 +01:00
dave f8a295eaec huskies: merge 889 2026-05-01 15:02:40 +00:00
dave c84786364a huskies: merge 874 2026-04-29 17:00:28 +00:00
dave 8a7e1aa036 huskies: merge 873 2026-04-29 16:11:34 +00:00
dave 2655288412 huskies: merge 870 2026-04-29 15:26:57 +00:00
dave f3e4d5d072 huskies: merge 869 2026-04-29 14:58:11 +00:00
dave 88f9e5dd54 huskies: merge 731_refactor_migrate_existing_stories_from_slug_based_ids_to_numeric_only 2026-04-27 20:42:21 +00:00
dave 4a0f57478c huskies: merge 671_refactor_migrate_pipeline_state_consumers_from_string_comparisons_to_typed_pipelinestage_enum 2026-04-27 16:39:39 +00:00
dave b340aa97b0 fix: clean up clippy warnings + cargo fmt across post-refactor surface
The 13-file refactor pass (commits db00a5d4 through eca15b4e) introduced
~89 clippy errors and 38 cargo fmt issues — every agent in every worktree
hit them on script/test, burning their turn budget on cleanup before doing
real story work. This is the silent kill behind 644, 652, 655, 664, 667
all hitting watchdog limits this round.

Changes:
- cargo fmt --all across 37 files (formatting normalisation only)
- #![allow(unused_imports, dead_code)] on 24 split modules where the
  python-script splitter imported liberally to be safe; tighter cleanup
  per-import will happen as agents touch each module
- Removed truly-dead re-exports (cleanup_merge_workspace, slog_warn from
  http/mcp/mod.rs, CliArgs/print_help from main.rs)
- Prefixed _auth_msg in crdt_sync/server.rs (handshake helper return is
  bound but not consumed)
- Converted dangling /// doc block in crdt_sync/mod.rs to //! so it
  attaches to the module
- Removed empty lines after doc comments in 4 spots (clippy lint)

All 2636 tests pass; clippy --all-targets -- -D warnings clean.
2026-04-27 01:32:08 +00:00
dave 23e22ba49c refactor: split crdt_state.rs into 6 sub-modules with co-located tests
The 2122-line crdt_state.rs is split into a sub-module directory:

- types.rs: CRDT/view types + CrdtEvent (247 lines)
- state.rs: CrdtState struct, statics, init, apply_and_persist (531 lines)
- ops.rs: sync API + apply_remote_op + delta-sync tests (455 lines)
- write.rs: write_item + bug_511 test (273 lines)
- read.rs: read API + dump + dep helpers (469 lines)
- presence.rs: node identity + claim API + heartbeat (176 lines)
- mod.rs: doc, sub-module decls, re-exports, hex helper (53 lines)

Tests are co-located with the code they primarily exercise per Rust convention.

No behaviour change. All 26 crdt_state tests pass; full suite green
(2635 tests with --test-threads=1).
2026-04-26 20:54:15 +00:00