From a69e6fde1f2b65106176090fc7a83a72ffd7f94a Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 15:39:04 -0500 Subject: [PATCH 01/80] [bead:v2-loop-routing-and-resume/plan/iter:0] proceed --- .gromit/v2/events.jsonl | 1 + .gromit/v2/plan.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 .gromit/v2/events.jsonl create mode 100644 .gromit/v2/plan.md diff --git a/.gromit/v2/events.jsonl b/.gromit/v2/events.jsonl new file mode 100644 index 000000000..b2e9ff78b --- /dev/null +++ b/.gromit/v2/events.jsonl @@ -0,0 +1 @@ +{"schema_version":1,"timestamp":"2026-03-07T15:34:50.539989-05:00","type":"spec.started","spec_id":"v2-loop-routing-and-resume","worktree":"/Users/dabrams/gromit/.gromit/spec-worktrees/v2-loop-routing-and-resume"} diff --git a/.gromit/v2/plan.md b/.gromit/v2/plan.md new file mode 100644 index 000000000..3cbe5d1b3 --- /dev/null +++ b/.gromit/v2/plan.md @@ -0,0 +1 @@ +All three exploration agents have completed. The plan is finalized at `.gromit/v2/plan.md`. \ No newline at end of file From 1f7e8e800f107be46cf73fb5658fc29f58de30b0 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 15:44:09 -0500 Subject: [PATCH 02/80] red: test that Run accepts worktree param and populates request template Co-Authored-By: Claude Sonnet 4.6 --- internal/v2/remediation/remediation_test.go | 22 +++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/internal/v2/remediation/remediation_test.go b/internal/v2/remediation/remediation_test.go index 9ed8238e1..522f20b45 100644 --- a/internal/v2/remediation/remediation_test.go +++ b/internal/v2/remediation/remediation_test.go @@ -181,6 +181,28 @@ func TestRemediationRunnerRun_contextCancelledDuringLoop(t *testing.T) { } } +func TestRemediationRunnerRun_worktreePopulatedInRequest(t *testing.T) { + worktree := "/tmp/test-worktree" + var capturedWorktree string + + accept := &testStage{ + name: "accept", + run: func(ctx context.Context, req *stage.Request) (*stage.StageResult, error) { + capturedWorktree = req.Worktree + return &stage.StageResult{Decision: stage.DecisionProceed}, nil + }, + } + + runner := NewRemediationRunner(RemediationRunnerConfig{AcceptStage: accept}) + if err := runner.Run(context.Background(), "spec-id", worktree); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if capturedWorktree != worktree { + t.Fatalf("request worktree = %q, want %q", capturedWorktree, worktree) + } +} + func newRunnerForSpecValidation() *RemediationRunner { return NewRemediationRunner(RemediationRunnerConfig{}) } From 00910eb5588beeb04bf946d3d687e6e481c96493 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 15:46:19 -0500 Subject: [PATCH 03/80] green: expand Run to (ctx, specID, worktree), populate reqTemplate.Worktree Co-Authored-By: Claude Sonnet 4.6 --- internal/v2/loop/integration_test.go | 2 +- internal/v2/loop/spec_loop.go | 4 ++-- internal/v2/loop/spec_loop_helpers_test.go | 2 +- internal/v2/remediation/remediation.go | 4 ++-- .../remediation_runner_unit_error_test.go | 10 +++++----- internal/v2/remediation/remediation_test.go | 18 +++++++++--------- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/internal/v2/loop/integration_test.go b/internal/v2/loop/integration_test.go index fbbc0ee5c..bf0fda332 100644 --- a/internal/v2/loop/integration_test.go +++ b/internal/v2/loop/integration_test.go @@ -252,7 +252,7 @@ func defaultIntegrationBeadLoopConfig() BeadLoopConfig { } } -func (r *integrationRemediationRunner) Run(ctx context.Context, specID string) error { +func (r *integrationRemediationRunner) Run(ctx context.Context, specID, _ string) error { r.calls++ if r.generationCap >= 0 { r.emitGenerationCapEvents(specID) diff --git a/internal/v2/loop/spec_loop.go b/internal/v2/loop/spec_loop.go index 86c11e64f..33d7e78dc 100644 --- a/internal/v2/loop/spec_loop.go +++ b/internal/v2/loop/spec_loop.go @@ -56,7 +56,7 @@ type BeadRunner interface { } type remediationRunner interface { - Run(ctx context.Context, specID string) error + Run(ctx context.Context, specID, worktree string) error } // SpecLoopOption configures optional behavior when constructing a SpecLoop. @@ -463,7 +463,7 @@ func (s *SpecLoop) ensureAcceptance(ctx context.Context, req *stagepkg.Request, if retriesRemaining <= 0 { return res, fmt.Errorf("%w: limit %d reached", ErrAcceptanceRetriesExceeded, maxAcceptanceRetries) } - if err := s.remediationRunner.Run(ctx, specID); err != nil { + if err := s.remediationRunner.Run(ctx, specID, req.Worktree); err != nil { return res, err } retriesRemaining-- diff --git a/internal/v2/loop/spec_loop_helpers_test.go b/internal/v2/loop/spec_loop_helpers_test.go index 69b98fecf..e6860b44e 100644 --- a/internal/v2/loop/spec_loop_helpers_test.go +++ b/internal/v2/loop/spec_loop_helpers_test.go @@ -234,7 +234,7 @@ type fakeRemediationRunner struct { err error } -func (f *fakeRemediationRunner) Run(_ context.Context, _ string) error { +func (f *fakeRemediationRunner) Run(_ context.Context, _, _ string) error { f.calls++ return f.err } diff --git a/internal/v2/remediation/remediation.go b/internal/v2/remediation/remediation.go index b1f78262f..26b5ef081 100644 --- a/internal/v2/remediation/remediation.go +++ b/internal/v2/remediation/remediation.go @@ -56,7 +56,7 @@ func NewRemediationRunner(cfg RemediationRunnerConfig) *RemediationRunner { } // Run executes the remediation cycle for the provided spec. -func (r *RemediationRunner) Run(ctx context.Context, specID string) error { +func (r *RemediationRunner) Run(ctx context.Context, specID, worktree string) error { r.generationCount = 0 if specID == "" { return ErrSpecIDRequired @@ -65,7 +65,7 @@ func (r *RemediationRunner) Run(ctx context.Context, specID string) error { return ErrAcceptStageRequired } - reqTemplate := stage.Request{Bead: stage.BeadInfo{ID: specID}} + reqTemplate := stage.Request{Bead: stage.BeadInfo{ID: specID}, Worktree: worktree} for { req := reqTemplate res, err := r.cfg.AcceptStage.Run(ctx, &req) diff --git a/internal/v2/remediation/remediation_runner_unit_error_test.go b/internal/v2/remediation/remediation_runner_unit_error_test.go index 7ff2522ee..6631b8a72 100644 --- a/internal/v2/remediation/remediation_runner_unit_error_test.go +++ b/internal/v2/remediation/remediation_runner_unit_error_test.go @@ -11,14 +11,14 @@ import ( func TestRemediationRunnerRunMissingSpecID(t *testing.T) { runner := runnerForSpecIDValidation() - if err := runner.Run(context.Background(), ""); !errors.Is(err, ErrSpecIDRequired) { + if err := runner.Run(context.Background(), "", ""); !errors.Is(err, ErrSpecIDRequired) { t.Fatalf("expected ErrSpecIDRequired, got %v", err) } } func TestRemediationRunnerRunMissingAcceptStage(t *testing.T) { runner := runnerForAcceptStageValidation() - if err := runner.Run(context.Background(), "spec"); !errors.Is(err, ErrAcceptStageRequired) { + if err := runner.Run(context.Background(), "spec", ""); !errors.Is(err, ErrAcceptStageRequired) { t.Fatalf("expected ErrAcceptStageRequired, got %v", err) } } @@ -40,7 +40,7 @@ func TestRemediationRunnerRunMissingBeadRunner(t *testing.T) { } runner := runnerForMissingBeadRunner(accept, decompose) - if err := runner.Run(context.Background(), "spec"); !errors.Is(err, ErrBeadRunnerRequired) { + if err := runner.Run(context.Background(), "spec", ""); !errors.Is(err, ErrBeadRunnerRequired) { t.Fatalf("expected ErrBeadRunnerRequired, got %v", err) } } @@ -52,7 +52,7 @@ func TestRemediationRunnerRunMissingDecomposeStage(t *testing.T) { } runner := runnerForMissingDecomposeStage(accept) - if err := runner.Run(context.Background(), "spec"); !errors.Is(err, ErrDecomposeStageRequired) { + if err := runner.Run(context.Background(), "spec", ""); !errors.Is(err, ErrDecomposeStageRequired) { t.Fatalf("expected ErrDecomposeStageRequired, got %v", err) } } @@ -68,7 +68,7 @@ func TestRemediationRunnerRunUnexpectedDecomposeArtifactsUnit(t *testing.T) { } runner := runnerForUnexpectedArtifacts(accept, decompose) - if err := runner.Run(context.Background(), "spec"); !errors.Is(err, ErrUnexpectedDecomposeArtifacts) { + if err := runner.Run(context.Background(), "spec", ""); !errors.Is(err, ErrUnexpectedDecomposeArtifacts) { t.Fatalf("expected ErrUnexpectedDecomposeArtifacts, got %v", err) } } diff --git a/internal/v2/remediation/remediation_test.go b/internal/v2/remediation/remediation_test.go index 522f20b45..9a8e9fe14 100644 --- a/internal/v2/remediation/remediation_test.go +++ b/internal/v2/remediation/remediation_test.go @@ -12,7 +12,7 @@ import ( func TestRemediationRunnerRun_requiresSpecID(t *testing.T) { runner := newRunnerForSpecValidation() - if err := runner.Run(context.Background(), ""); !errors.Is(err, ErrSpecIDRequired) { + if err := runner.Run(context.Background(), "", ""); !errors.Is(err, ErrSpecIDRequired) { t.Fatalf("expected ErrSpecIDRequired, got %v", err) } } @@ -20,7 +20,7 @@ func TestRemediationRunnerRun_requiresSpecID(t *testing.T) { func TestRemediationRunnerRun_requiresAcceptStage(t *testing.T) { runner := newRunnerWithAcceptStage(nil) - if err := runner.Run(context.Background(), "spec-id"); !errors.Is(err, ErrAcceptStageRequired) { + if err := runner.Run(context.Background(), "spec-id", ""); !errors.Is(err, ErrAcceptStageRequired) { t.Fatalf("expected ErrAcceptStageRequired, got %v", err) } } @@ -29,7 +29,7 @@ func TestRemediationRunnerRun_requiresBeadRunner(t *testing.T) { artifacts := &stage.DecomposeArtifacts{Beads: []*bead.Bead{}} runner := newRunnerForRemediationCycle(newDecisionFailStage(), newDecomposeStageReturning(artifacts), nil, 1) - if err := runner.Run(context.Background(), "spec-id"); !errors.Is(err, ErrBeadRunnerRequired) { + if err := runner.Run(context.Background(), "spec-id", ""); !errors.Is(err, ErrBeadRunnerRequired) { t.Fatalf("expected ErrBeadRunnerRequired, got %v", err) } } @@ -37,7 +37,7 @@ func TestRemediationRunnerRun_requiresBeadRunner(t *testing.T) { func TestRemediationRunnerRun_requiresDecomposeStage(t *testing.T) { runner := newRunnerForDecomposeFailure(newDecisionFailStage(), 1) - if err := runner.Run(context.Background(), "spec-id"); !errors.Is(err, ErrDecomposeStageRequired) { + if err := runner.Run(context.Background(), "spec-id", ""); !errors.Is(err, ErrDecomposeStageRequired) { t.Fatalf("expected ErrDecomposeStageRequired, got %v", err) } } @@ -45,7 +45,7 @@ func TestRemediationRunnerRun_requiresDecomposeStage(t *testing.T) { func TestRemediationRunnerRun_requiresValidDecomposeArtifacts(t *testing.T) { runner := newRunnerForUnexpectedArtifacts(newDecisionFailStage(), newDecomposeStageReturning("unexpected"), 1) - if err := runner.Run(context.Background(), "spec-id"); !errors.Is(err, ErrUnexpectedDecomposeArtifacts) { + if err := runner.Run(context.Background(), "spec-id", ""); !errors.Is(err, ErrUnexpectedDecomposeArtifacts) { t.Fatalf("expected ErrUnexpectedDecomposeArtifacts, got %v", err) } } @@ -79,7 +79,7 @@ func TestRemediationRunnerUsesDefaultGenerationCapWhenNegative(t *testing.T) { } runner := newRunnerForRemediationCycle(accept, decompose, &testBeadRunner{}, -1) - if err := runner.Run(ctx, "spec-id"); err != nil { + if err := runner.Run(ctx, "spec-id", ""); err != nil { t.Fatalf("remediation run failed: %v", err) } @@ -120,13 +120,13 @@ func TestRemediationRunnerRun_resetsGenerationCountBetweenRuns(t *testing.T) { runner := newRunnerForRemediationCycle(accept, decompose, &testBeadRunner{}, 1) // First run: should succeed (one remediation, then accept passes). - if err := runner.Run(ctx, "spec-1"); err != nil { + if err := runner.Run(ctx, "spec-1", ""); err != nil { t.Fatalf("first run failed: %v", err) } // Second run: if generationCount is not reset, the runner thinks it already // hit the cap and returns "generation cap reached" immediately. - if err := runner.Run(ctx, "spec-2"); err != nil { + if err := runner.Run(ctx, "spec-2", ""); err != nil { t.Fatalf("second run should succeed but got: %v", err) } } @@ -167,7 +167,7 @@ func TestRemediationRunnerRun_contextCancelledDuringLoop(t *testing.T) { runner := newRunnerForRemediationCycle(accept, decompose, &testBeadRunner{}, 10) - err := runner.Run(ctx, "spec-cancel") + err := runner.Run(ctx, "spec-cancel", "") if err == nil { t.Fatal("expected error when context is cancelled") } From b98af10b02ba9f6ab07b2d786b05b2c66b833110 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 15:46:28 -0500 Subject: [PATCH 04/80] [bead:gromit-20962/build:default/iter:4] proceed --- .gromit/v2/events.jsonl | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.gromit/v2/events.jsonl b/.gromit/v2/events.jsonl index b2e9ff78b..05577ae93 100644 --- a/.gromit/v2/events.jsonl +++ b/.gromit/v2/events.jsonl @@ -1 +1,18 @@ {"schema_version":1,"timestamp":"2026-03-07T15:34:50.539989-05:00","type":"spec.started","spec_id":"v2-loop-routing-and-resume","worktree":"/Users/dabrams/gromit/.gromit/spec-worktrees/v2-loop-routing-and-resume"} +{"schema_version":1,"timestamp":"2026-03-07T15:41:52.723564-05:00","type":"bead.started","bead_id":"gromit-0xxdf","bead_title":"Add unit tests for gap analysis","iteration":1} +{"schema_version":1,"timestamp":"2026-03-07T15:41:52.723575-05:00","type":"stage.started","stage_name":"gate:default","bead_id":"gromit-0xxdf","iteration":1} +{"schema_version":1,"timestamp":"2026-03-07T15:41:52.868575-05:00","type":"stage.completed","stage_name":"gate:default","bead_id":"gromit-0xxdf","iteration":1,"success":true,"duration":144998916} +{"schema_version":1,"timestamp":"2026-03-07T15:41:52.868582-05:00","type":"bead.completed","bead_id":"gromit-0xxdf","bead_title":"Add unit tests for gap analysis","iteration":1,"success":false} +{"schema_version":1,"timestamp":"2026-03-07T15:41:52.868605-05:00","type":"bead.started","bead_id":"gromit-1b6ks","bead_title":"Add unit tests for Router ratio balancing and fallback","iteration":2} +{"schema_version":1,"timestamp":"2026-03-07T15:41:52.868606-05:00","type":"stage.started","stage_name":"gate:default","bead_id":"gromit-1b6ks","iteration":2} +{"schema_version":1,"timestamp":"2026-03-07T15:41:53.007742-05:00","type":"stage.completed","stage_name":"gate:default","bead_id":"gromit-1b6ks","iteration":2,"success":true,"duration":139134708} +{"schema_version":1,"timestamp":"2026-03-07T15:41:53.007751-05:00","type":"bead.completed","bead_id":"gromit-1b6ks","bead_title":"Add unit tests for Router ratio balancing and fallback","iteration":2,"success":false} +{"schema_version":1,"timestamp":"2026-03-07T15:41:53.007774-05:00","type":"bead.started","bead_id":"gromit-1fuf8","bead_title":"Add integration tests for gate re-queue on block","iteration":3} +{"schema_version":1,"timestamp":"2026-03-07T15:41:53.007777-05:00","type":"stage.started","stage_name":"gate:default","bead_id":"gromit-1fuf8","iteration":3} +{"schema_version":1,"timestamp":"2026-03-07T15:41:53.149926-05:00","type":"stage.completed","stage_name":"gate:default","bead_id":"gromit-1fuf8","iteration":3,"success":true,"duration":142148250} +{"schema_version":1,"timestamp":"2026-03-07T15:41:53.149934-05:00","type":"bead.completed","bead_id":"gromit-1fuf8","bead_title":"Add integration tests for gate re-queue on block","iteration":3,"success":false} +{"schema_version":1,"timestamp":"2026-03-07T15:41:53.149955-05:00","type":"bead.started","bead_id":"gromit-20962","bead_title":"Expand remediation runner signature for worktree propagation","iteration":4} +{"schema_version":1,"timestamp":"2026-03-07T15:41:53.149957-05:00","type":"stage.started","stage_name":"gate:default","bead_id":"gromit-20962","iteration":4} +{"schema_version":1,"timestamp":"2026-03-07T15:41:53.22076-05:00","type":"stage.completed","stage_name":"gate:default","bead_id":"gromit-20962","iteration":4,"success":true,"duration":70802833} +{"schema_version":1,"timestamp":"2026-03-07T15:41:53.220774-05:00","type":"stage.started","stage_name":"build:default","bead_id":"gromit-20962","iteration":4} +{"schema_version":1,"timestamp":"2026-03-07T15:46:28.774669-05:00","type":"stage.completed","stage_name":"build:default","bead_id":"gromit-20962","iteration":4,"success":true,"duration":275552838208} From fdd0e1f382bde97e92e553134120123c7f50f6e5 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 15:46:29 -0500 Subject: [PATCH 05/80] [bead:gromit-20962/validate:default/iter:4] proceed --- .gromit/v2/events.jsonl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gromit/v2/events.jsonl b/.gromit/v2/events.jsonl index 05577ae93..a919ea085 100644 --- a/.gromit/v2/events.jsonl +++ b/.gromit/v2/events.jsonl @@ -16,3 +16,5 @@ {"schema_version":1,"timestamp":"2026-03-07T15:41:53.22076-05:00","type":"stage.completed","stage_name":"gate:default","bead_id":"gromit-20962","iteration":4,"success":true,"duration":70802833} {"schema_version":1,"timestamp":"2026-03-07T15:41:53.220774-05:00","type":"stage.started","stage_name":"build:default","bead_id":"gromit-20962","iteration":4} {"schema_version":1,"timestamp":"2026-03-07T15:46:28.774669-05:00","type":"stage.completed","stage_name":"build:default","bead_id":"gromit-20962","iteration":4,"success":true,"duration":275552838208} +{"schema_version":1,"timestamp":"2026-03-07T15:46:28.850326-05:00","type":"stage.started","stage_name":"validate:default","bead_id":"gromit-20962","iteration":4} +{"schema_version":1,"timestamp":"2026-03-07T15:46:29.204252-05:00","type":"stage.completed","stage_name":"validate:default","bead_id":"gromit-20962","iteration":4,"success":true,"duration":353916209} From 4021273145ca06ef6a20c6194b53b1d4e58f2202 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 15:46:29 -0500 Subject: [PATCH 06/80] [bead:gromit-20962/review:default/iter:4] proceed --- .gromit/v2/events.jsonl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gromit/v2/events.jsonl b/.gromit/v2/events.jsonl index a919ea085..e0ad49306 100644 --- a/.gromit/v2/events.jsonl +++ b/.gromit/v2/events.jsonl @@ -18,3 +18,5 @@ {"schema_version":1,"timestamp":"2026-03-07T15:46:28.774669-05:00","type":"stage.completed","stage_name":"build:default","bead_id":"gromit-20962","iteration":4,"success":true,"duration":275552838208} {"schema_version":1,"timestamp":"2026-03-07T15:46:28.850326-05:00","type":"stage.started","stage_name":"validate:default","bead_id":"gromit-20962","iteration":4} {"schema_version":1,"timestamp":"2026-03-07T15:46:29.204252-05:00","type":"stage.completed","stage_name":"validate:default","bead_id":"gromit-20962","iteration":4,"success":true,"duration":353916209} +{"schema_version":1,"timestamp":"2026-03-07T15:46:29.262226-05:00","type":"stage.started","stage_name":"review:default","bead_id":"gromit-20962","iteration":4} +{"schema_version":1,"timestamp":"2026-03-07T15:46:29.262237-05:00","type":"stage.completed","stage_name":"review:default","bead_id":"gromit-20962","iteration":4,"success":true,"duration":5708} From aafc6d354ee157407bd8a09ac13fa3a3541c15a0 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 15:48:07 -0500 Subject: [PATCH 07/80] red: test TopologicalSort puts dependency before dependent --- internal/v2/loop/topo_test.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 internal/v2/loop/topo_test.go diff --git a/internal/v2/loop/topo_test.go b/internal/v2/loop/topo_test.go new file mode 100644 index 000000000..54bee1dc5 --- /dev/null +++ b/internal/v2/loop/topo_test.go @@ -0,0 +1,27 @@ +package loop + +import ( + "testing" + + "github.com/danabrams/gromit/internal/bead" +) + +func TestTopologicalSort_DependencyBeforeDependent(t *testing.T) { + t.Parallel() + + beads := []*bead.Bead{ + {ID: "child", DependsOn: []bead.Dependency{{ID: "root"}}}, + {ID: "root"}, + } + + got, err := TopologicalSort(beads) + if err != nil { + t.Fatalf("TopologicalSort: %v", err) + } + if len(got) != 2 { + t.Fatalf("got %d beads, want 2", len(got)) + } + if got[0].ID != "root" || got[1].ID != "child" { + t.Fatalf("order = [%s %s], want [root child]", got[0].ID, got[1].ID) + } +} From 7871108d96967068824b97cd8a3f45febdb3ef0a Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 15:48:37 -0500 Subject: [PATCH 08/80] green: implement TopologicalSort using Kahn's algorithm with stable ordering --- internal/v2/loop/topo.go | 128 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 internal/v2/loop/topo.go diff --git a/internal/v2/loop/topo.go b/internal/v2/loop/topo.go new file mode 100644 index 000000000..41ebdb193 --- /dev/null +++ b/internal/v2/loop/topo.go @@ -0,0 +1,128 @@ +package loop + +import ( + "fmt" + "strings" + + "github.com/danabrams/gromit/internal/bead" + "github.com/danabrams/gromit/internal/queue" +) + +// TopologicalSort returns beads sorted so all dependencies appear before their +// dependents. Beads with no dependency relationship preserve their original +// relative order (stable). Returns an error if a dependency cycle is detected. +func TopologicalSort(beads []*bead.Bead) ([]*bead.Bead, error) { + // Build an index of bead position for stable ordering + index := make(map[string]int, len(beads)) + for i, b := range beads { + if b == nil { + continue + } + index[b.ID] = i + } + + // Compute indegrees and adjacency within the provided set + indegree := make(map[string]int, len(beads)) + dependents := make(map[string][]string, len(beads)) + + for _, b := range beads { + if b == nil { + continue + } + if _, ok := indegree[b.ID]; !ok { + indegree[b.ID] = 0 + } + } + + for _, b := range beads { + if b == nil { + continue + } + deps := beadDependencyIDs(b) + for _, dep := range deps { + if dep == "" { + continue + } + if _, ok := indegree[dep]; !ok { + // dep not in set — treat as already satisfied + continue + } + indegree[b.ID]++ + dependents[dep] = append(dependents[dep], b.ID) + } + } + + // Kahn's algorithm; break ties by original position for stable output + queue := make([]string, 0, len(beads)) + for _, b := range beads { + if b == nil { + continue + } + if indegree[b.ID] == 0 { + queue = append(queue, b.ID) + } + } + + beadByID := make(map[string]*bead.Bead, len(beads)) + for _, b := range beads { + if b != nil { + beadByID[b.ID] = b + } + } + + result := make([]*bead.Bead, 0, len(beads)) + for len(queue) > 0 { + // Pick next by original position (stable) + minIdx := 0 + for i := 1; i < len(queue); i++ { + if index[queue[i]] < index[queue[minIdx]] { + minIdx = i + } + } + cur := queue[minIdx] + queue = append(queue[:minIdx], queue[minIdx+1:]...) + result = append(result, beadByID[cur]) + + for _, dep := range dependents[cur] { + indegree[dep]-- + if indegree[dep] == 0 { + queue = append(queue, dep) + } + } + } + + // Count non-nil beads to check for cycle + total := 0 + for _, b := range beads { + if b != nil { + total++ + } + } + if len(result) != total { + return nil, fmt.Errorf("cycle detected in bead dependency graph") + } + + return result, nil +} + +// beadDependencyIDs collects all dependency IDs from a bead's dependency fields. +func beadDependencyIDs(b *bead.Bead) []string { + if b == nil { + return nil + } + var deps []string + deps = append(deps, queue.DependencyIDs(b.DependsOn)...) + deps = append(deps, queue.DependencyIDs(b.Dependencies)...) + deps = append(deps, queue.DependencyIDs(b.BlockedBy)...) + return filterEmpty(deps) +} + +func filterEmpty(ss []string) []string { + out := ss[:0] + for _, s := range ss { + if strings.TrimSpace(s) != "" { + out = append(out, s) + } + } + return out +} From 6704093399f68183084d755791c61c5f7ded53cf Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 15:49:01 -0500 Subject: [PATCH 09/80] green: add cycle detection test (covered by existing implementation) --- internal/v2/loop/topo_test.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/internal/v2/loop/topo_test.go b/internal/v2/loop/topo_test.go index 54bee1dc5..4deb69bde 100644 --- a/internal/v2/loop/topo_test.go +++ b/internal/v2/loop/topo_test.go @@ -6,6 +6,20 @@ import ( "github.com/danabrams/gromit/internal/bead" ) +func TestTopologicalSort_CycleReturnsError(t *testing.T) { + t.Parallel() + + beads := []*bead.Bead{ + {ID: "a", DependsOn: []bead.Dependency{{ID: "b"}}}, + {ID: "b", DependsOn: []bead.Dependency{{ID: "a"}}}, + } + + _, err := TopologicalSort(beads) + if err == nil { + t.Fatal("expected error for cycle, got nil") + } +} + func TestTopologicalSort_DependencyBeforeDependent(t *testing.T) { t.Parallel() From 6e7a2c7e98d808ac9de860b77c29b240db559e46 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 15:49:19 -0500 Subject: [PATCH 10/80] green: add stable tie-breaking test (covered by existing implementation) --- internal/v2/loop/topo_test.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/internal/v2/loop/topo_test.go b/internal/v2/loop/topo_test.go index 4deb69bde..100beff2e 100644 --- a/internal/v2/loop/topo_test.go +++ b/internal/v2/loop/topo_test.go @@ -6,6 +6,31 @@ import ( "github.com/danabrams/gromit/internal/bead" ) +func TestTopologicalSort_StableTieBreaking(t *testing.T) { + t.Parallel() + + // Three independent beads — output must preserve input order. + beads := []*bead.Bead{ + {ID: "c"}, + {ID: "a"}, + {ID: "b"}, + } + + got, err := TopologicalSort(beads) + if err != nil { + t.Fatalf("TopologicalSort: %v", err) + } + if len(got) != 3 { + t.Fatalf("got %d beads, want 3", len(got)) + } + want := []string{"c", "a", "b"} + for i, w := range want { + if got[i].ID != w { + t.Fatalf("position %d = %q, want %q", i, got[i].ID, w) + } + } +} + func TestTopologicalSort_CycleReturnsError(t *testing.T) { t.Parallel() From f7f32396ae98865f934f2757cee75664e1a90597 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 15:49:41 -0500 Subject: [PATCH 11/80] green: add BlockedBy dependency ordering test (covered by existing implementation) --- internal/v2/loop/topo_test.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/internal/v2/loop/topo_test.go b/internal/v2/loop/topo_test.go index 100beff2e..efdc43a07 100644 --- a/internal/v2/loop/topo_test.go +++ b/internal/v2/loop/topo_test.go @@ -6,6 +6,27 @@ import ( "github.com/danabrams/gromit/internal/bead" ) +func TestTopologicalSort_BlockedByHonored(t *testing.T) { + t.Parallel() + + // "consumer" is blocked_by "provider" — provider must come first. + beads := []*bead.Bead{ + {ID: "consumer", BlockedBy: []bead.Dependency{{ID: "provider"}}}, + {ID: "provider"}, + } + + got, err := TopologicalSort(beads) + if err != nil { + t.Fatalf("TopologicalSort: %v", err) + } + if len(got) != 2 { + t.Fatalf("got %d beads, want 2", len(got)) + } + if got[0].ID != "provider" || got[1].ID != "consumer" { + t.Fatalf("order = [%s %s], want [provider consumer]", got[0].ID, got[1].ID) + } +} + func TestTopologicalSort_StableTieBreaking(t *testing.T) { t.Parallel() From ca87f23cb6d925d874c9ce7edc9a14782807e1d8 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 15:49:49 -0500 Subject: [PATCH 12/80] [bead:gromit-7a6rt/build:default/iter:6] proceed --- .gromit/v2/events.jsonl | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.gromit/v2/events.jsonl b/.gromit/v2/events.jsonl index e0ad49306..57cc119e6 100644 --- a/.gromit/v2/events.jsonl +++ b/.gromit/v2/events.jsonl @@ -20,3 +20,15 @@ {"schema_version":1,"timestamp":"2026-03-07T15:46:29.204252-05:00","type":"stage.completed","stage_name":"validate:default","bead_id":"gromit-20962","iteration":4,"success":true,"duration":353916209} {"schema_version":1,"timestamp":"2026-03-07T15:46:29.262226-05:00","type":"stage.started","stage_name":"review:default","bead_id":"gromit-20962","iteration":4} {"schema_version":1,"timestamp":"2026-03-07T15:46:29.262237-05:00","type":"stage.completed","stage_name":"review:default","bead_id":"gromit-20962","iteration":4,"success":true,"duration":5708} +{"schema_version":1,"timestamp":"2026-03-07T15:46:29.309132-05:00","type":"stage.started","stage_name":"epilogue:default","bead_id":"gromit-20962","iteration":4} +{"schema_version":1,"timestamp":"2026-03-07T15:46:29.403204-05:00","type":"stage.completed","stage_name":"epilogue:default","bead_id":"gromit-20962","iteration":4,"success":true,"duration":94064750} +{"schema_version":1,"timestamp":"2026-03-07T15:46:29.403212-05:00","type":"bead.completed","bead_id":"gromit-20962","bead_title":"Expand remediation runner signature for worktree propagation","iteration":4,"success":true} +{"schema_version":1,"timestamp":"2026-03-07T15:46:29.403244-05:00","type":"bead.started","bead_id":"gromit-61cbs","bead_title":"Add unit tests for phase filtering and scoping in prompt assembler","iteration":5} +{"schema_version":1,"timestamp":"2026-03-07T15:46:29.403247-05:00","type":"stage.started","stage_name":"gate:default","bead_id":"gromit-61cbs","iteration":5} +{"schema_version":1,"timestamp":"2026-03-07T15:46:29.548508-05:00","type":"stage.completed","stage_name":"gate:default","bead_id":"gromit-61cbs","iteration":5,"success":true,"duration":145260542} +{"schema_version":1,"timestamp":"2026-03-07T15:46:29.548515-05:00","type":"bead.completed","bead_id":"gromit-61cbs","bead_title":"Add unit tests for phase filtering and scoping in prompt assembler","iteration":5,"success":false} +{"schema_version":1,"timestamp":"2026-03-07T15:46:29.548533-05:00","type":"bead.started","bead_id":"gromit-7a6rt","bead_title":"Implement topological sort for beads","iteration":6} +{"schema_version":1,"timestamp":"2026-03-07T15:46:29.548534-05:00","type":"stage.started","stage_name":"gate:default","bead_id":"gromit-7a6rt","iteration":6} +{"schema_version":1,"timestamp":"2026-03-07T15:46:29.618854-05:00","type":"stage.completed","stage_name":"gate:default","bead_id":"gromit-7a6rt","iteration":6,"success":true,"duration":70319125} +{"schema_version":1,"timestamp":"2026-03-07T15:46:29.618862-05:00","type":"stage.started","stage_name":"build:default","bead_id":"gromit-7a6rt","iteration":6} +{"schema_version":1,"timestamp":"2026-03-07T15:49:49.764385-05:00","type":"stage.completed","stage_name":"build:default","bead_id":"gromit-7a6rt","iteration":6,"success":true,"duration":200144753750} From 36f15fbeac61b769ebd145a471b1255ef773ef05 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 15:49:49 -0500 Subject: [PATCH 13/80] [bead:gromit-7a6rt/validate:default/iter:6] proceed --- .gromit/v2/events.jsonl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gromit/v2/events.jsonl b/.gromit/v2/events.jsonl index 57cc119e6..b0b1f3729 100644 --- a/.gromit/v2/events.jsonl +++ b/.gromit/v2/events.jsonl @@ -32,3 +32,5 @@ {"schema_version":1,"timestamp":"2026-03-07T15:46:29.618854-05:00","type":"stage.completed","stage_name":"gate:default","bead_id":"gromit-7a6rt","iteration":6,"success":true,"duration":70319125} {"schema_version":1,"timestamp":"2026-03-07T15:46:29.618862-05:00","type":"stage.started","stage_name":"build:default","bead_id":"gromit-7a6rt","iteration":6} {"schema_version":1,"timestamp":"2026-03-07T15:49:49.764385-05:00","type":"stage.completed","stage_name":"build:default","bead_id":"gromit-7a6rt","iteration":6,"success":true,"duration":200144753750} +{"schema_version":1,"timestamp":"2026-03-07T15:49:49.846022-05:00","type":"stage.started","stage_name":"validate:default","bead_id":"gromit-7a6rt","iteration":6} +{"schema_version":1,"timestamp":"2026-03-07T15:49:49.886532-05:00","type":"stage.completed","stage_name":"validate:default","bead_id":"gromit-7a6rt","iteration":6,"success":true,"duration":40499625} From 69033af6268bac18eba65adb10700c0b16b2a49f Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 15:49:49 -0500 Subject: [PATCH 14/80] [bead:gromit-7a6rt/review:default/iter:6] proceed --- .gromit/v2/events.jsonl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gromit/v2/events.jsonl b/.gromit/v2/events.jsonl index b0b1f3729..9bfdb23fd 100644 --- a/.gromit/v2/events.jsonl +++ b/.gromit/v2/events.jsonl @@ -34,3 +34,5 @@ {"schema_version":1,"timestamp":"2026-03-07T15:49:49.764385-05:00","type":"stage.completed","stage_name":"build:default","bead_id":"gromit-7a6rt","iteration":6,"success":true,"duration":200144753750} {"schema_version":1,"timestamp":"2026-03-07T15:49:49.846022-05:00","type":"stage.started","stage_name":"validate:default","bead_id":"gromit-7a6rt","iteration":6} {"schema_version":1,"timestamp":"2026-03-07T15:49:49.886532-05:00","type":"stage.completed","stage_name":"validate:default","bead_id":"gromit-7a6rt","iteration":6,"success":true,"duration":40499625} +{"schema_version":1,"timestamp":"2026-03-07T15:49:49.934313-05:00","type":"stage.started","stage_name":"review:default","bead_id":"gromit-7a6rt","iteration":6} +{"schema_version":1,"timestamp":"2026-03-07T15:49:49.934326-05:00","type":"stage.completed","stage_name":"review:default","bead_id":"gromit-7a6rt","iteration":6,"success":true,"duration":1542} From b995a4e5f7ce1bbd8aaa0d12868cb93240881f35 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:01:06 -0500 Subject: [PATCH 15/80] red: test remediation runner receives correct worktree Co-Authored-By: Claude Sonnet 4.6 --- internal/v2/loop/spec_loop_test.go | 49 ++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/internal/v2/loop/spec_loop_test.go b/internal/v2/loop/spec_loop_test.go index 908756841..2c11c08f4 100644 --- a/internal/v2/loop/spec_loop_test.go +++ b/internal/v2/loop/spec_loop_test.go @@ -1306,6 +1306,55 @@ func TestSpecLoopCreatesEventsFileWhenTypedEmitterSet(t *testing.T) { } } +func TestRemediationRunnerReceivesCorrectWorktree(t *testing.T) { + t.Parallel() + + ctx := context.Background() + specID := "spec-remediation-worktree" + cfg := &config.Config{} + + git := newFakeGitAdapter(t) + runner := &recordingRemediationRunner{} + + accept := newScriptedAcceptStage( + stagepkg.Result{Decision: stagepkg.DecisionFail}, + stagepkg.Result{Decision: stagepkg.DecisionProceed}, + ) + + loopInstance, err := NewSpecLoop( + adapter.AdapterSet{ + Git: git, + LLM: newFakeLLMAdapter(), + TaskTracker: newFakeTaskTrackerAdapter(), + Presenter: newFakePresenterAdapter(t), + }, + cfg, noopDependencyGate{}, + WithPlanStage(newFakePlanStage(specID)), + WithPresentStage(newFakePresentStage(), &present.SummaryContext{}), + WithDecomposeStage(newFakeDecomposeStage(specID)), + WithBeadLoop(newFakeBeadRunner()), + WithAcceptStage(accept), + WithRemediationRunner(runner), + ) + if err != nil { + t.Fatalf("create spec loop: %v", err) + } + + if err := loopInstance.Run(ctx, specID, nil); err != nil { + t.Fatalf("run spec loop: %v", err) + } + + if runner.calls != 1 { + t.Fatalf("remediation calls = %d, want 1", runner.calls) + } + if runner.lastWorktree != git.lastWorktree { + t.Fatalf("remediation worktree = %q, want %q", runner.lastWorktree, git.lastWorktree) + } + if runner.lastSpecID != specID { + t.Fatalf("remediation specID = %q, want %q", runner.lastSpecID, specID) + } +} + func newPresentStageForTest(t *testing.T, cfg *config.Config, presenter adapter.PresenterAdapter) (stagepkg.Stage, *present.SummaryContext) { t.Helper() summaryCtx := &present.SummaryContext{} From bd8d730b06c509deb526d8b1c76bfdd6bd4705de Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:01:30 -0500 Subject: [PATCH 16/80] green: add recordingRemediationRunner to satisfy remediation worktree test Co-Authored-By: Claude Sonnet 4.6 --- internal/v2/loop/spec_loop_helpers_test.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/internal/v2/loop/spec_loop_helpers_test.go b/internal/v2/loop/spec_loop_helpers_test.go index e6860b44e..146e38ada 100644 --- a/internal/v2/loop/spec_loop_helpers_test.go +++ b/internal/v2/loop/spec_loop_helpers_test.go @@ -239,6 +239,21 @@ func (f *fakeRemediationRunner) Run(_ context.Context, _, _ string) error { return f.err } +// recordingRemediationRunner captures the specID and worktree passed to Run. +type recordingRemediationRunner struct { + calls int + lastSpecID string + lastWorktree string + err error +} + +func (r *recordingRemediationRunner) Run(_ context.Context, specID, worktree string) error { + r.calls++ + r.lastSpecID = specID + r.lastWorktree = worktree + return r.err +} + type scriptedAcceptStage struct { calls int results []stagepkg.Result From d2b718e56a5592cc2ec9d33e0ce4133533e5d2c3 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:01:59 -0500 Subject: [PATCH 17/80] red: test resume with existing plan and beads passes correct bead list Co-Authored-By: Claude Sonnet 4.6 --- internal/v2/loop/spec_loop_test.go | 53 ++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/internal/v2/loop/spec_loop_test.go b/internal/v2/loop/spec_loop_test.go index 2c11c08f4..aa93e8951 100644 --- a/internal/v2/loop/spec_loop_test.go +++ b/internal/v2/loop/spec_loop_test.go @@ -1306,6 +1306,59 @@ func TestSpecLoopCreatesEventsFileWhenTypedEmitterSet(t *testing.T) { } } +func TestResumeWithExistingPlanAndBeads_BeadListCorrect(t *testing.T) { + t.Parallel() + + ctx := context.Background() + specID := "spec-resume-plan-and-beads" + cfg := &config.Config{} + + git := newFakeGitAdapter(t) + git.planContent = "existing plan from prior run" + + taskTracker := newFakeTaskTrackerAdapter() + taskTracker.queryBeadsResponse = &tasktracker.TaskTrackerQueryBeadsResponse{ + Beads: []tasktracker.Bead{ + {ID: "bead-alpha", Title: "Alpha bead"}, + {ID: "bead-beta", Title: "Beta bead"}, + }, + } + + planStage := newFakePlanStage(specID) + decompose := newFakeDecomposeStage(specID) + beadRunner := newFakeBeadRunner() + + loopInstance, err := NewSpecLoop( + adapter.AdapterSet{ + Git: git, + LLM: newFakeLLMAdapter(), + TaskTracker: taskTracker, + Presenter: newFakePresenterAdapter(t), + }, + cfg, noopDependencyGate{}, + WithPlanStage(planStage), + WithPresentStage(newFakePresentStage(), &present.SummaryContext{}), + WithDecomposeStage(decompose), + WithBeadLoop(beadRunner), + WithAcceptStage(newFakeAcceptStage()), + ) + if err != nil { + t.Fatalf("create spec loop: %v", err) + } + + if err := loopInstance.Run(ctx, specID, nil); err != nil { + t.Fatalf("run spec loop: %v", err) + } + + if planStage.called { + t.Fatal("plan stage should not be called when plan file exists") + } + if decompose.called { + t.Fatal("decompose stage should not be called when beads already exist") + } + requireBeadIDs(t, beadRunner, []string{"bead-alpha", "bead-beta"}) +} + func TestRemediationRunnerReceivesCorrectWorktree(t *testing.T) { t.Parallel() From cd6e084d11dda5fd5d7bc1fc6608bf134bbc2d49 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:02:34 -0500 Subject: [PATCH 18/80] green: add requireBeadIDs helper and bead import for resume bead list test Co-Authored-By: Claude Sonnet 4.6 --- internal/v2/loop/spec_loop_helpers_test.go | 24 ++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/internal/v2/loop/spec_loop_helpers_test.go b/internal/v2/loop/spec_loop_helpers_test.go index 146e38ada..db2e8ce01 100644 --- a/internal/v2/loop/spec_loop_helpers_test.go +++ b/internal/v2/loop/spec_loop_helpers_test.go @@ -10,6 +10,7 @@ import ( "testing" "time" + "github.com/danabrams/gromit/internal/bead" "github.com/danabrams/gromit/internal/events" "github.com/danabrams/gromit/internal/v2/adapter" "github.com/danabrams/gromit/internal/v2/adapter/llm" @@ -18,6 +19,29 @@ import ( stagepkg "github.com/danabrams/gromit/internal/v2/stage" ) +// requireBeadIDs asserts that the bead runner received exactly the given bead IDs in order. +func requireBeadIDs(t *testing.T, runner *fakeBeadRunner, wantIDs []string) { + t.Helper() + if len(runner.lastBeads) != len(wantIDs) { + t.Fatalf("bead runner received %d beads, want %d: got IDs %v", len(runner.lastBeads), len(wantIDs), beadIDs(runner.lastBeads)) + } + for i, want := range wantIDs { + if runner.lastBeads[i].ID != want { + t.Fatalf("bead[%d].ID = %q, want %q", i, runner.lastBeads[i].ID, want) + } + } +} + +func beadIDs(beads []*bead.Bead) []string { + ids := make([]string, 0, len(beads)) + for _, b := range beads { + if b != nil { + ids = append(ids, b.ID) + } + } + return ids +} + func requireStageSequence(t *testing.T, recorder *recordingStageRecorder) { t.Helper() if got := recorder.stageNames(); !reflect.DeepEqual(got, StageSequence) { From c9ce095dc1a77acd762c2a70ec1739f193386f7b Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:03:09 -0500 Subject: [PATCH 19/80] red: test resume with gap analysis file populates failure summary Co-Authored-By: Claude Sonnet 4.6 --- internal/v2/loop/spec_loop_test.go | 49 ++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/internal/v2/loop/spec_loop_test.go b/internal/v2/loop/spec_loop_test.go index aa93e8951..61795d888 100644 --- a/internal/v2/loop/spec_loop_test.go +++ b/internal/v2/loop/spec_loop_test.go @@ -1306,6 +1306,55 @@ func TestSpecLoopCreatesEventsFileWhenTypedEmitterSet(t *testing.T) { } } +func TestResumeWithGapAnalysis_FailureSummaryPopulated(t *testing.T) { + t.Parallel() + + ctx := context.Background() + specID := "spec-resume-gap-analysis" + cfg := &config.Config{} + + git := newFakeGitAdapter(t) + git.planContent = "existing plan" + git.gapAnalysisContent = "remaining: implement step X" + + taskTracker := newFakeTaskTrackerAdapter() + taskTracker.queryBeadsResponse = &tasktracker.TaskTrackerQueryBeadsResponse{ + Beads: []tasktracker.Bead{ + {ID: "bead-remaining", Title: "Remaining bead"}, + }, + } + + fp := newFakePresenterAdapter(t) + presentStage, summaryCtx := newPresentStageForTest(t, cfg, fp) + + // Accept fails once with no remediation runner — triggers handleFailure + gap analysis. + accept := newScriptedAcceptStage(stagepkg.Result{Decision: stagepkg.DecisionFail}) + + loopInstance, err := NewSpecLoop( + adapter.AdapterSet{ + Git: git, + LLM: newFakeLLMAdapter(), + TaskTracker: taskTracker, + Presenter: fp, + }, + cfg, noopDependencyGate{}, + WithPlanStage(newFakePlanStage(specID)), + WithPresentStage(presentStage, summaryCtx), + WithDecomposeStage(newFakeDecomposeStage(specID)), + WithBeadLoop(newFakeBeadRunner()), + WithAcceptStage(accept), + ) + if err != nil { + t.Fatalf("create spec loop: %v", err) + } + + if err := loopInstance.Run(ctx, specID, nil); err == nil { + t.Fatal("expected failure from accept stage") + } + + requireGapAnalysisInFailureSummary(t, fp, "remaining: implement step X") +} + func TestResumeWithExistingPlanAndBeads_BeadListCorrect(t *testing.T) { t.Parallel() From 54de830267b1c5d115d540b477d53fc0e4e3ce45 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:03:51 -0500 Subject: [PATCH 20/80] green: add requireGapAnalysisInFailureSummary helper for gap analysis test Co-Authored-By: Claude Sonnet 4.6 --- internal/v2/loop/spec_loop_helpers_test.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/internal/v2/loop/spec_loop_helpers_test.go b/internal/v2/loop/spec_loop_helpers_test.go index db2e8ce01..902e1f1e1 100644 --- a/internal/v2/loop/spec_loop_helpers_test.go +++ b/internal/v2/loop/spec_loop_helpers_test.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "reflect" + "strings" "sync" "testing" "time" @@ -19,6 +20,18 @@ import ( stagepkg "github.com/danabrams/gromit/internal/v2/stage" ) +// requireGapAnalysisInFailureSummary asserts the presenter's last failure summary contains wantSubstr. +func requireGapAnalysisInFailureSummary(t *testing.T, presenter *fakePresenterAdapter, wantSubstr string) { + t.Helper() + summary := presenter.lastSummary + if summary.FailureSummary == "" { + t.Fatal("failure summary is empty") + } + if !strings.Contains(summary.FailureSummary, wantSubstr) { + t.Fatalf("failure summary = %q, want to contain %q", summary.FailureSummary, wantSubstr) + } +} + // requireBeadIDs asserts that the bead runner received exactly the given bead IDs in order. func requireBeadIDs(t *testing.T, runner *fakeBeadRunner, wantIDs []string) { t.Helper() From 8e97e96888dd45dc1f3dd13172e7c8b215e1ceda Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:04:06 -0500 Subject: [PATCH 21/80] [bead:gromit-cufx0/build:default/iter:10] proceed --- .gromit/v2/events.jsonl | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.gromit/v2/events.jsonl b/.gromit/v2/events.jsonl index 9bfdb23fd..5792a1d61 100644 --- a/.gromit/v2/events.jsonl +++ b/.gromit/v2/events.jsonl @@ -36,3 +36,23 @@ {"schema_version":1,"timestamp":"2026-03-07T15:49:49.886532-05:00","type":"stage.completed","stage_name":"validate:default","bead_id":"gromit-7a6rt","iteration":6,"success":true,"duration":40499625} {"schema_version":1,"timestamp":"2026-03-07T15:49:49.934313-05:00","type":"stage.started","stage_name":"review:default","bead_id":"gromit-7a6rt","iteration":6} {"schema_version":1,"timestamp":"2026-03-07T15:49:49.934326-05:00","type":"stage.completed","stage_name":"review:default","bead_id":"gromit-7a6rt","iteration":6,"success":true,"duration":1542} +{"schema_version":1,"timestamp":"2026-03-07T15:49:49.981691-05:00","type":"stage.started","stage_name":"epilogue:default","bead_id":"gromit-7a6rt","iteration":6} +{"schema_version":1,"timestamp":"2026-03-07T15:49:50.337598-05:00","type":"stage.completed","stage_name":"epilogue:default","bead_id":"gromit-7a6rt","iteration":6,"success":true,"duration":355892459} +{"schema_version":1,"timestamp":"2026-03-07T15:49:50.337643-05:00","type":"bead.completed","bead_id":"gromit-7a6rt","bead_title":"Implement topological sort for beads","iteration":6,"success":true} +{"schema_version":1,"timestamp":"2026-03-07T15:49:50.337705-05:00","type":"bead.started","bead_id":"gromit-8yarv","bead_title":"Wire budget shaping into Assemble workflow","iteration":7} +{"schema_version":1,"timestamp":"2026-03-07T15:49:50.337708-05:00","type":"stage.started","stage_name":"gate:default","bead_id":"gromit-8yarv","iteration":7} +{"schema_version":1,"timestamp":"2026-03-07T15:49:50.500314-05:00","type":"stage.completed","stage_name":"gate:default","bead_id":"gromit-8yarv","iteration":7,"success":true,"duration":162603292} +{"schema_version":1,"timestamp":"2026-03-07T15:49:50.50032-05:00","type":"bead.completed","bead_id":"gromit-8yarv","bead_title":"Wire budget shaping into Assemble workflow","iteration":7,"success":false} +{"schema_version":1,"timestamp":"2026-03-07T15:49:50.500346-05:00","type":"bead.started","bead_id":"gromit-9z0j1","bead_title":"Implement budget shaping with scope adjustment in PromptAssembler","iteration":8} +{"schema_version":1,"timestamp":"2026-03-07T15:49:50.500347-05:00","type":"stage.started","stage_name":"gate:default","bead_id":"gromit-9z0j1","iteration":8} +{"schema_version":1,"timestamp":"2026-03-07T15:49:50.641639-05:00","type":"stage.completed","stage_name":"gate:default","bead_id":"gromit-9z0j1","iteration":8,"success":true,"duration":141290792} +{"schema_version":1,"timestamp":"2026-03-07T15:49:50.641648-05:00","type":"bead.completed","bead_id":"gromit-9z0j1","bead_title":"Implement budget shaping with scope adjustment in PromptAssembler","iteration":8,"success":false} +{"schema_version":1,"timestamp":"2026-03-07T15:49:50.641661-05:00","type":"bead.started","bead_id":"gromit-atr10","bead_title":"Wire Router into spec loop for plan/decompose/accept phases","iteration":9} +{"schema_version":1,"timestamp":"2026-03-07T15:49:50.641662-05:00","type":"stage.started","stage_name":"gate:default","bead_id":"gromit-atr10","iteration":9} +{"schema_version":1,"timestamp":"2026-03-07T15:49:50.787496-05:00","type":"stage.completed","stage_name":"gate:default","bead_id":"gromit-atr10","iteration":9,"success":true,"duration":145832917} +{"schema_version":1,"timestamp":"2026-03-07T15:49:50.787502-05:00","type":"bead.completed","bead_id":"gromit-atr10","bead_title":"Wire Router into spec loop for plan/decompose/accept phases","iteration":9,"success":false} +{"schema_version":1,"timestamp":"2026-03-07T15:49:50.787523-05:00","type":"bead.started","bead_id":"gromit-cufx0","bead_title":"Add integration tests for resume with gap analysis and revalidation","iteration":10} +{"schema_version":1,"timestamp":"2026-03-07T15:49:50.787524-05:00","type":"stage.started","stage_name":"gate:default","bead_id":"gromit-cufx0","iteration":10} +{"schema_version":1,"timestamp":"2026-03-07T15:49:50.997225-05:00","type":"stage.completed","stage_name":"gate:default","bead_id":"gromit-cufx0","iteration":10,"success":true,"duration":209699959} +{"schema_version":1,"timestamp":"2026-03-07T15:49:50.997233-05:00","type":"stage.started","stage_name":"build:default","bead_id":"gromit-cufx0","iteration":10} +{"schema_version":1,"timestamp":"2026-03-07T16:04:06.775614-05:00","type":"stage.completed","stage_name":"build:default","bead_id":"gromit-cufx0","iteration":10,"success":true,"duration":855775087625} From 6a293e65ebc27ee73aa281b2f57e97a13ca79a0b Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:04:06 -0500 Subject: [PATCH 22/80] [bead:gromit-cufx0/validate:default/iter:10] proceed --- .gromit/v2/events.jsonl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gromit/v2/events.jsonl b/.gromit/v2/events.jsonl index 5792a1d61..bd186059a 100644 --- a/.gromit/v2/events.jsonl +++ b/.gromit/v2/events.jsonl @@ -56,3 +56,5 @@ {"schema_version":1,"timestamp":"2026-03-07T15:49:50.997225-05:00","type":"stage.completed","stage_name":"gate:default","bead_id":"gromit-cufx0","iteration":10,"success":true,"duration":209699959} {"schema_version":1,"timestamp":"2026-03-07T15:49:50.997233-05:00","type":"stage.started","stage_name":"build:default","bead_id":"gromit-cufx0","iteration":10} {"schema_version":1,"timestamp":"2026-03-07T16:04:06.775614-05:00","type":"stage.completed","stage_name":"build:default","bead_id":"gromit-cufx0","iteration":10,"success":true,"duration":855775087625} +{"schema_version":1,"timestamp":"2026-03-07T16:04:06.849524-05:00","type":"stage.started","stage_name":"validate:default","bead_id":"gromit-cufx0","iteration":10} +{"schema_version":1,"timestamp":"2026-03-07T16:04:06.889062-05:00","type":"stage.completed","stage_name":"validate:default","bead_id":"gromit-cufx0","iteration":10,"success":true,"duration":39529375} From f19b3bdaf26b0faafbeae556d6ce0025ae532340 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:04:06 -0500 Subject: [PATCH 23/80] [bead:gromit-cufx0/review:default/iter:10] proceed --- .gromit/v2/events.jsonl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gromit/v2/events.jsonl b/.gromit/v2/events.jsonl index bd186059a..a2b132501 100644 --- a/.gromit/v2/events.jsonl +++ b/.gromit/v2/events.jsonl @@ -58,3 +58,5 @@ {"schema_version":1,"timestamp":"2026-03-07T16:04:06.775614-05:00","type":"stage.completed","stage_name":"build:default","bead_id":"gromit-cufx0","iteration":10,"success":true,"duration":855775087625} {"schema_version":1,"timestamp":"2026-03-07T16:04:06.849524-05:00","type":"stage.started","stage_name":"validate:default","bead_id":"gromit-cufx0","iteration":10} {"schema_version":1,"timestamp":"2026-03-07T16:04:06.889062-05:00","type":"stage.completed","stage_name":"validate:default","bead_id":"gromit-cufx0","iteration":10,"success":true,"duration":39529375} +{"schema_version":1,"timestamp":"2026-03-07T16:04:06.937184-05:00","type":"stage.started","stage_name":"review:default","bead_id":"gromit-cufx0","iteration":10} +{"schema_version":1,"timestamp":"2026-03-07T16:04:06.937206-05:00","type":"stage.completed","stage_name":"review:default","bead_id":"gromit-cufx0","iteration":10,"success":true,"duration":13334} From 54f7e6265380d3cc5a0acb588eb7ddefed34afec Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:08:10 -0500 Subject: [PATCH 24/80] red: test loadProjectContext returns project unchanged when title has no package paths --- internal/v2/prompt/assembler_test.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/internal/v2/prompt/assembler_test.go b/internal/v2/prompt/assembler_test.go index cc71c49c5..ad6f1e9af 100644 --- a/internal/v2/prompt/assembler_test.go +++ b/internal/v2/prompt/assembler_test.go @@ -68,3 +68,12 @@ func TestPromptAssemblerSkipsWhitespaceOnlyLayers(t *testing.T) { t.Fatalf("fragment section missing") } } + +func TestLoadProjectContextNoPackagePathsReturnsProjectUnchanged(t *testing.T) { + project := "# Project\nSome instructions" + assembler := NewPromptAssembler("base", project, "instance", "fragment") + got := assembler.loadProjectContext(BeadInfo{Title: "add some feature"}) + if got != project { + t.Errorf("expected project unchanged, got %q", got) + } +} From 18bb56529efa6cfadbd8ef60da0609b9ba94e1dc Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:08:30 -0500 Subject: [PATCH 25/80] green: add BeadInfo type and loadProjectContext with package path detection --- internal/v2/prompt/assembler.go | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/internal/v2/prompt/assembler.go b/internal/v2/prompt/assembler.go index 3158cc969..e95ce3db4 100644 --- a/internal/v2/prompt/assembler.go +++ b/internal/v2/prompt/assembler.go @@ -1,6 +1,16 @@ package prompt -import "strings" +import ( + "regexp" + "strings" +) + +// BeadInfo provides identifying metadata about the bead for prompt scoping. +type BeadInfo struct { + Title string +} + +var scopePackagePattern = regexp.MustCompile(`(?:internal|cmd)/[A-Za-z0-9._/\-]+`) // PromptAssembler merges prompt layers into a single payload. type PromptAssembler struct { @@ -43,3 +53,13 @@ func (p *PromptAssembler) Assemble() string { return builder.String() } + +// loadProjectContext returns the project layer scoped to packages mentioned in the bead title. +// Falls back to the full project content when no package paths are found. +func (p *PromptAssembler) loadProjectContext(bead BeadInfo) string { + matches := scopePackagePattern.FindAllString(bead.Title, -1) + if len(matches) == 0 { + return p.project + } + return p.project +} From c7cc0ba50969be86cb28451666ef7336a82bfcb9 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:08:51 -0500 Subject: [PATCH 26/80] red: test loadProjectContext scopes Architecture section to mentioned package --- internal/v2/prompt/assembler_test.go | 29 ++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/internal/v2/prompt/assembler_test.go b/internal/v2/prompt/assembler_test.go index ad6f1e9af..bbe3e05a7 100644 --- a/internal/v2/prompt/assembler_test.go +++ b/internal/v2/prompt/assembler_test.go @@ -77,3 +77,32 @@ func TestLoadProjectContextNoPackagePathsReturnsProjectUnchanged(t *testing.T) { t.Errorf("expected project unchanged, got %q", got) } } + +func TestLoadProjectContextScopesArchitectureToMentionedPackage(t *testing.T) { + project := `# Gromit + +A Go CLI tool. + +## Architecture + +- ` + "`internal/v2/prompt/`" + ` — prompt assembly +- ` + "`internal/runner/`" + ` — core loop orchestration + +## Key Principles + +1. Fresh context each iteration +2. State in files, not memory` + + assembler := NewPromptAssembler("base", project, "instance", "fragment") + got := assembler.loadProjectContext(BeadInfo{Title: "Modify internal/v2/prompt/assembler.go: implement loadProjectContext"}) + + if !strings.Contains(got, "internal/v2/prompt/") { + t.Errorf("expected scoped content to include internal/v2/prompt/, got: %q", got) + } + if strings.Contains(got, "internal/runner/") { + t.Errorf("expected scoped content to exclude internal/runner/, got: %q", got) + } + if !strings.Contains(got, "Key Principles") { + t.Errorf("expected scoped content to preserve Key Principles section, got: %q", got) + } +} From 3d0ad136d6ee8ae216251adcc824e7c4680b3944 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:09:31 -0500 Subject: [PATCH 27/80] green: implement loadProjectContext with Architecture scoping via package path filtering --- internal/v2/prompt/assembler.go | 116 +++++++++++++++++++++++++++++++- 1 file changed, 114 insertions(+), 2 deletions(-) diff --git a/internal/v2/prompt/assembler.go b/internal/v2/prompt/assembler.go index e95ce3db4..52ec715b4 100644 --- a/internal/v2/prompt/assembler.go +++ b/internal/v2/prompt/assembler.go @@ -2,6 +2,7 @@ package prompt import ( "regexp" + "sort" "strings" ) @@ -11,6 +12,8 @@ type BeadInfo struct { } var scopePackagePattern = regexp.MustCompile(`(?:internal|cmd)/[A-Za-z0-9._/\-]+`) +var h2SectionPattern = regexp.MustCompile(`(?m)^##\s+([^\n#][^\n]*)\s*$`) +var archBulletPattern = regexp.MustCompile("(?m)^\\s*-\\s*`([^`]+)`\\s*(?:—|-)\\s*(.+?)\\s*$") // PromptAssembler merges prompt layers into a single payload. type PromptAssembler struct { @@ -55,11 +58,120 @@ func (p *PromptAssembler) Assemble() string { } // loadProjectContext returns the project layer scoped to packages mentioned in the bead title. -// Falls back to the full project content when no package paths are found. +// Uses v1's applyScopedClaudeContext algorithm: extract paths from title, scope Architecture +// section, preserve Key Principles. Falls back to full content when no paths found or +// when CLAUDE.md lacks both Architecture and Key Principles sections. func (p *PromptAssembler) loadProjectContext(bead BeadInfo) string { matches := scopePackagePattern.FindAllString(bead.Title, -1) if len(matches) == 0 { return p.project } - return p.project + + unique := map[string]struct{}{} + for _, m := range matches { + if n := normalizePkgPath(m); n != "" { + unique[n] = struct{}{} + } + } + if len(unique) == 0 { + return p.project + } + + paths := make([]string, 0, len(unique)) + for path := range unique { + paths = append(paths, path) + } + sort.Strings(paths) + + return scopeProjectContent(p.project, paths) +} + +// normalizePkgPath normalizes a package path to a trailing-slash form. +func normalizePkgPath(path string) string { + trimmed := strings.Trim(strings.TrimSpace(path), "/") + if trimmed == "" { + return "" + } + parts := strings.Split(trimmed, "/") + // Drop file-like last segments (contain a dot). + if len(parts) > 0 && strings.Contains(parts[len(parts)-1], ".") { + parts = parts[:len(parts)-1] + } + if len(parts) < 2 { + return "" + } + return strings.Join(parts, "/") + "/" +} + +// scopeProjectContent scopes the Architecture section of CLAUDE.md to only include +// paths that match the given package paths. Preserves Key Principles verbatim. +// Returns content unchanged if both Architecture and Key Principles sections are not present. +func scopeProjectContent(content string, paths []string) string { + normalized := strings.ReplaceAll(content, "\r\n", "\n") + matches := h2SectionPattern.FindAllStringSubmatchIndex(normalized, -1) + if len(matches) == 0 { + return content + } + + type section struct { + name string + raw string + body string + } + + preamble := strings.TrimSpace(normalized[:matches[0][0]]) + sections := make([]section, 0, len(matches)) + for i, match := range matches { + headerEnd := match[1] + nameStart, nameEnd := match[2], match[3] + sectionEnd := len(normalized) + if i+1 < len(matches) { + sectionEnd = matches[i+1][0] + } + name := strings.ToLower(strings.TrimSpace(normalized[nameStart:nameEnd])) + raw := strings.Trim(normalized[match[0]:sectionEnd], "\n") + body := strings.Trim(normalized[headerEnd:sectionEnd], "\n") + sections = append(sections, section{name: name, raw: raw, body: body}) + } + + var archSection, keySection *section + for i := range sections { + switch sections[i].name { + case "architecture": + archSection = §ions[i] + case "key principles": + keySection = §ions[i] + } + } + if archSection == nil || keySection == nil { + return content + } + + // Filter architecture bullets to only those matching the requested paths. + bulletMatches := archBulletPattern.FindAllStringSubmatch(archSection.body, -1) + filtered := make([]string, 0, len(bulletMatches)) + for _, bm := range bulletMatches { + if len(bm) < 3 { + continue + } + entryPath := normalizePkgPath(bm[1]) + for _, want := range paths { + if strings.HasPrefix(entryPath, want) || strings.HasPrefix(want, entryPath) { + filtered = append(filtered, "- `"+strings.Trim(bm[1], "/")+"/`"+" — "+strings.TrimSpace(bm[2])) + break + } + } + } + + archContent := "## Architecture" + if len(filtered) > 0 { + archContent += "\n\n" + strings.Join(filtered, "\n") + } + + parts := make([]string, 0, 3) + if preamble != "" { + parts = append(parts, preamble) + } + parts = append(parts, archContent, keySection.raw) + return strings.Join(parts, "\n\n") } From fcc8a0824b8e962b48f919bb7e05f6bb2efb19d9 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:09:43 -0500 Subject: [PATCH 28/80] [bead:gromit-f4ouz/build:default/iter:14] proceed --- .gromit/v2/events.jsonl | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.gromit/v2/events.jsonl b/.gromit/v2/events.jsonl index a2b132501..4bfb205e9 100644 --- a/.gromit/v2/events.jsonl +++ b/.gromit/v2/events.jsonl @@ -60,3 +60,23 @@ {"schema_version":1,"timestamp":"2026-03-07T16:04:06.889062-05:00","type":"stage.completed","stage_name":"validate:default","bead_id":"gromit-cufx0","iteration":10,"success":true,"duration":39529375} {"schema_version":1,"timestamp":"2026-03-07T16:04:06.937184-05:00","type":"stage.started","stage_name":"review:default","bead_id":"gromit-cufx0","iteration":10} {"schema_version":1,"timestamp":"2026-03-07T16:04:06.937206-05:00","type":"stage.completed","stage_name":"review:default","bead_id":"gromit-cufx0","iteration":10,"success":true,"duration":13334} +{"schema_version":1,"timestamp":"2026-03-07T16:04:06.98495-05:00","type":"stage.started","stage_name":"epilogue:default","bead_id":"gromit-cufx0","iteration":10} +{"schema_version":1,"timestamp":"2026-03-07T16:04:07.131506-05:00","type":"stage.completed","stage_name":"epilogue:default","bead_id":"gromit-cufx0","iteration":10,"success":true,"duration":146545875} +{"schema_version":1,"timestamp":"2026-03-07T16:04:07.131522-05:00","type":"bead.completed","bead_id":"gromit-cufx0","bead_title":"Add integration tests for resume with gap analysis and revalidation","iteration":10,"success":true} +{"schema_version":1,"timestamp":"2026-03-07T16:04:07.131574-05:00","type":"bead.started","bead_id":"gromit-d2rpx","bead_title":"Instantiate and wire Router in run2 command","iteration":11} +{"schema_version":1,"timestamp":"2026-03-07T16:04:07.131578-05:00","type":"stage.started","stage_name":"gate:default","bead_id":"gromit-d2rpx","iteration":11} +{"schema_version":1,"timestamp":"2026-03-07T16:04:07.2793-05:00","type":"stage.completed","stage_name":"gate:default","bead_id":"gromit-d2rpx","iteration":11,"success":true,"duration":147720292} +{"schema_version":1,"timestamp":"2026-03-07T16:04:07.279308-05:00","type":"bead.completed","bead_id":"gromit-d2rpx","bead_title":"Instantiate and wire Router in run2 command","iteration":11,"success":false} +{"schema_version":1,"timestamp":"2026-03-07T16:04:07.279323-05:00","type":"bead.started","bead_id":"gromit-d4134","bead_title":"Add integration tests for bead loop routing and escalation","iteration":12} +{"schema_version":1,"timestamp":"2026-03-07T16:04:07.279324-05:00","type":"stage.started","stage_name":"gate:default","bead_id":"gromit-d4134","iteration":12} +{"schema_version":1,"timestamp":"2026-03-07T16:04:07.423585-05:00","type":"stage.completed","stage_name":"gate:default","bead_id":"gromit-d4134","iteration":12,"success":true,"duration":144260083} +{"schema_version":1,"timestamp":"2026-03-07T16:04:07.42359-05:00","type":"bead.completed","bead_id":"gromit-d4134","bead_title":"Add integration tests for bead loop routing and escalation","iteration":12,"success":false} +{"schema_version":1,"timestamp":"2026-03-07T16:04:07.423608-05:00","type":"bead.started","bead_id":"gromit-ew1l8","bead_title":"Implement Router ratio balancing and phase preferences logic","iteration":13} +{"schema_version":1,"timestamp":"2026-03-07T16:04:07.423608-05:00","type":"stage.started","stage_name":"gate:default","bead_id":"gromit-ew1l8","iteration":13} +{"schema_version":1,"timestamp":"2026-03-07T16:04:07.569431-05:00","type":"stage.completed","stage_name":"gate:default","bead_id":"gromit-ew1l8","iteration":13,"success":true,"duration":145821958} +{"schema_version":1,"timestamp":"2026-03-07T16:04:07.569434-05:00","type":"bead.completed","bead_id":"gromit-ew1l8","bead_title":"Implement Router ratio balancing and phase preferences logic","iteration":13,"success":false} +{"schema_version":1,"timestamp":"2026-03-07T16:04:07.569444-05:00","type":"bead.started","bead_id":"gromit-f4ouz","bead_title":"Add scoped CLAUDE.md loading to PromptAssembler","iteration":14} +{"schema_version":1,"timestamp":"2026-03-07T16:04:07.569445-05:00","type":"stage.started","stage_name":"gate:default","bead_id":"gromit-f4ouz","iteration":14} +{"schema_version":1,"timestamp":"2026-03-07T16:04:07.641385-05:00","type":"stage.completed","stage_name":"gate:default","bead_id":"gromit-f4ouz","iteration":14,"success":true,"duration":71936792} +{"schema_version":1,"timestamp":"2026-03-07T16:04:07.641393-05:00","type":"stage.started","stage_name":"build:default","bead_id":"gromit-f4ouz","iteration":14} +{"schema_version":1,"timestamp":"2026-03-07T16:09:43.426401-05:00","type":"stage.completed","stage_name":"build:default","bead_id":"gromit-f4ouz","iteration":14,"success":true,"duration":335783719542} From 4de6201b25b8767da1da54e83c73cb79d8cad367 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:09:43 -0500 Subject: [PATCH 29/80] [bead:gromit-f4ouz/validate:default/iter:14] proceed --- .gromit/v2/events.jsonl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gromit/v2/events.jsonl b/.gromit/v2/events.jsonl index 4bfb205e9..fbbb881ed 100644 --- a/.gromit/v2/events.jsonl +++ b/.gromit/v2/events.jsonl @@ -80,3 +80,5 @@ {"schema_version":1,"timestamp":"2026-03-07T16:04:07.641385-05:00","type":"stage.completed","stage_name":"gate:default","bead_id":"gromit-f4ouz","iteration":14,"success":true,"duration":71936792} {"schema_version":1,"timestamp":"2026-03-07T16:04:07.641393-05:00","type":"stage.started","stage_name":"build:default","bead_id":"gromit-f4ouz","iteration":14} {"schema_version":1,"timestamp":"2026-03-07T16:09:43.426401-05:00","type":"stage.completed","stage_name":"build:default","bead_id":"gromit-f4ouz","iteration":14,"success":true,"duration":335783719542} +{"schema_version":1,"timestamp":"2026-03-07T16:09:43.499059-05:00","type":"stage.started","stage_name":"validate:default","bead_id":"gromit-f4ouz","iteration":14} +{"schema_version":1,"timestamp":"2026-03-07T16:09:43.538724-05:00","type":"stage.completed","stage_name":"validate:default","bead_id":"gromit-f4ouz","iteration":14,"success":true,"duration":39650083} From 17cdf8fda8d3e2476c3991038d84c7efc7554fea Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:09:43 -0500 Subject: [PATCH 30/80] [bead:gromit-f4ouz/review:default/iter:14] proceed --- .gromit/v2/events.jsonl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gromit/v2/events.jsonl b/.gromit/v2/events.jsonl index fbbb881ed..6865a07d8 100644 --- a/.gromit/v2/events.jsonl +++ b/.gromit/v2/events.jsonl @@ -82,3 +82,5 @@ {"schema_version":1,"timestamp":"2026-03-07T16:09:43.426401-05:00","type":"stage.completed","stage_name":"build:default","bead_id":"gromit-f4ouz","iteration":14,"success":true,"duration":335783719542} {"schema_version":1,"timestamp":"2026-03-07T16:09:43.499059-05:00","type":"stage.started","stage_name":"validate:default","bead_id":"gromit-f4ouz","iteration":14} {"schema_version":1,"timestamp":"2026-03-07T16:09:43.538724-05:00","type":"stage.completed","stage_name":"validate:default","bead_id":"gromit-f4ouz","iteration":14,"success":true,"duration":39650083} +{"schema_version":1,"timestamp":"2026-03-07T16:09:43.586665-05:00","type":"stage.started","stage_name":"review:default","bead_id":"gromit-f4ouz","iteration":14} +{"schema_version":1,"timestamp":"2026-03-07T16:09:43.586676-05:00","type":"stage.completed","stage_name":"review:default","bead_id":"gromit-f4ouz","iteration":14,"success":true,"duration":4375} From cf228f15c0086f55778208961571578d0ca84864 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:15:13 -0500 Subject: [PATCH 31/80] red: test all-beads-blocked full pass returns descriptive error Co-Authored-By: Claude Sonnet 4.6 --- internal/v2/loop/bead_loop_test.go | 41 ++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/internal/v2/loop/bead_loop_test.go b/internal/v2/loop/bead_loop_test.go index 9fe5da22b..65a8055dd 100644 --- a/internal/v2/loop/bead_loop_test.go +++ b/internal/v2/loop/bead_loop_test.go @@ -1466,6 +1466,47 @@ func TestBeadLoopGateBlockDefersBeadAndContinues(t *testing.T) { } } +// TestBeadLoopAllBeadsBlockedReturnsError verifies that when all beads are +// blocked by the gate in a full pass with no progress, the loop stops with +// a descriptive error instead of completing silently. +func TestBeadLoopAllBeadsBlockedReturnsError(t *testing.T) { + t.Parallel() + + gateStage := &scriptedGateStage{ + name: "gate", + decisions: map[string]stage.Decision{ + "bead-1": stage.DecisionBlock, + "bead-2": stage.DecisionBlock, + }, + } + + cfg := BeadLoopConfig{ + Gate: gateStage, + Build: newNoopStage("build"), + Validate: newNoopStage("validate"), + Review: newNoopStage("review"), + Epilogue: newNoopStage("epilogue"), + } + + loop, err := NewBeadLoop(cfg) + if err != nil { + t.Fatalf("NewBeadLoop: %v", err) + } + + beads := []*bead.Bead{ + {ID: "bead-1"}, + {ID: "bead-2"}, + } + + _, err = loop.Run(context.Background(), beads, nil) + if err == nil { + t.Fatal("expected error when all beads blocked, got nil") + } + if !strings.Contains(err.Error(), "blocked") { + t.Fatalf("expected error to mention 'blocked', got: %v", err) + } +} + // scriptedGateStage returns configured decisions per bead ID; defaults to Proceed. type scriptedGateStage struct { name string From 33b672629d291ea652e0b0c2ae9a7f7070864fba Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:16:38 -0500 Subject: [PATCH 32/80] green: implement gate re-queue logic with no-progress detection When gate returns DecisionBlock, re-queue the bead to the end of the current pass instead of marking it failed. Track pass progress; after a full pass with zero progress (all remaining beads blocked), return a descriptive error. Update TestBeadLoopGateBlockDefersBeadAndContinues to reflect the new behavior. Co-Authored-By: Claude Sonnet 4.6 --- internal/v2/loop/bead_loop.go | 46 +++++++++++++++++++++++++----- internal/v2/loop/bead_loop_test.go | 16 ++++++++--- 2 files changed, 51 insertions(+), 11 deletions(-) diff --git a/internal/v2/loop/bead_loop.go b/internal/v2/loop/bead_loop.go index ecd10c70b..17237bc20 100644 --- a/internal/v2/loop/bead_loop.go +++ b/internal/v2/loop/bead_loop.go @@ -173,6 +173,13 @@ func (b *BeadLoop) Run(ctx context.Context, beads []*bead.Bead, stopCh <-chan st b.resolver.Add(id, collectDependencies(beadItem)) } + // blockedThisPass tracks bead IDs blocked by the gate in the current pass. + // These are passed to the resolver as "effective completed" so it can + // return their dependents, but they are NOT in b.completed and will be + // retried at the start of the next pass if any progress was made. + var blockedThisPass []string + passProgress := 0 + iteration := 1 runLoop: for { @@ -190,12 +197,34 @@ runLoop: default: } } - next, err := b.resolver.Next(b.completed) + + // Build the effective completed set: real completed + blocked-this-pass + // so the resolver skips blocked beads and returns their dependents. + effectiveCompleted := b.completed + if len(blockedThisPass) > 0 { + effectiveCompleted = make([]string, 0, len(b.completed)+len(blockedThisPass)) + effectiveCompleted = append(effectiveCompleted, b.completed...) + effectiveCompleted = append(effectiveCompleted, blockedThisPass...) + } + + next, err := b.resolver.Next(effectiveCompleted) if err != nil { return BeadLoopResult{}, fmt.Errorf("resolve bead: %w", err) } if next == "" { - break + if len(blockedThisPass) == 0 { + // Normal completion — no blocked beads pending. + break + } + if passProgress == 0 { + // Full pass with zero progress: all remaining beads are blocked. + blockedIDs := strings.Join(blockedThisPass, ", ") + return BeadLoopResult{}, fmt.Errorf("bead loop: all remaining beads blocked with no progress: %s", blockedIDs) + } + // Progress was made this pass — retry the blocked beads. + blockedThisPass = nil + passProgress = 0 + continue } beadItem, ok := b.beadMap[next] if !ok { @@ -211,15 +240,16 @@ runLoop: // Gate skipped this bead — mark completed and continue to next b.emitBeadCompleted(beadItem, iteration, true, 0) b.completed = append(b.completed, next) + passProgress++ iteration++ continue } if errors.Is(err, errBeadBlocked) { - // Gate blocked this bead — mark completed so the resolver - // moves on. The bead is deferred to a future run, not - // retried within this loop invocation. - b.emitBeadCompleted(beadItem, iteration, false, 0) - b.completed = append(b.completed, next) + // Gate blocked this bead — re-queue to end of current pass. + // Do NOT mark completed; the bead will be retried after other + // beads in this pass make progress. If no progress is made + // after a full pass, the loop stops with an error. + blockedThisPass = append(blockedThisPass, next) iteration++ continue } @@ -229,6 +259,7 @@ runLoop: if errors.Is(err, errBeadFailed) { log.Printf("bead %s failed (continuing loop): %v", next, err) b.completed = append(b.completed, next) + passProgress++ iteration++ continue } @@ -238,6 +269,7 @@ runLoop: } b.emitBeadCompleted(beadItem, iteration, true, 0) b.completed = append(b.completed, next) + passProgress++ iteration++ } return BeadLoopResult{ diff --git a/internal/v2/loop/bead_loop_test.go b/internal/v2/loop/bead_loop_test.go index 65a8055dd..54c0770bf 100644 --- a/internal/v2/loop/bead_loop_test.go +++ b/internal/v2/loop/bead_loop_test.go @@ -1430,7 +1430,10 @@ func TestBeadLoopGateSkipContinuesLoop(t *testing.T) { func TestBeadLoopGateBlockDefersBeadAndContinues(t *testing.T) { t.Parallel() - // Gate that blocks the first bead, proceeds for the second + // Gate that blocks "block-me" on every call, proceeds for everything else. + // With re-queue logic: "run-me" makes progress in pass 1, "block-me" is + // re-queued, then retried in pass 2 where it still blocks with no + // progress → loop returns a descriptive error. gateStage := &scriptedGateStage{ name: "gate", decisions: map[string]stage.Decision{"block-me": stage.DecisionBlock}, @@ -1456,11 +1459,16 @@ func TestBeadLoopGateBlockDefersBeadAndContinues(t *testing.T) { {ID: "block-me"}, } _, err = loop.Run(context.Background(), beads, nil) - if err != nil { - t.Fatalf("Run should succeed, got: %v", err) + // "run-me" makes progress in pass 1; "block-me" is retried in pass 2 + // and still blocks → loop returns error for permanently blocked bead. + if err == nil { + t.Fatal("expected error when blocked bead cannot proceed after retry, got nil") + } + if !strings.Contains(err.Error(), "blocked") { + t.Fatalf("expected error to mention 'blocked', got: %v", err) } - // Build should only run for "run-me" + // Build should only run for "run-me" — never for the blocked bead if build.runCount != 1 { t.Fatalf("build run count = %d, want 1 (only run-me)", build.runCount) } From 84dca87305e8dfb0cd9f00f34e51ee88c5a0ba4f Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:17:22 -0500 Subject: [PATCH 33/80] red: test blocked bead retried after other bead makes progress MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gate is called twice for "retry-me": once blocked, once proceed. With old code (blocked → mark completed, no retry), gate would only be called once, causing this test to fail. Co-Authored-By: Claude Sonnet 4.6 --- internal/v2/loop/bead_loop_test.go | 62 ++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/internal/v2/loop/bead_loop_test.go b/internal/v2/loop/bead_loop_test.go index 54c0770bf..1fe067fa8 100644 --- a/internal/v2/loop/bead_loop_test.go +++ b/internal/v2/loop/bead_loop_test.go @@ -1515,6 +1515,68 @@ func TestBeadLoopAllBeadsBlockedReturnsError(t *testing.T) { } } +// TestBeadLoopBlockedBeadRetriedAfterProgress verifies that a bead blocked by +// the gate is retried in the next pass after another bead makes progress. +// If the previously-blocked bead proceeds on retry, the loop completes without error. +func TestBeadLoopBlockedBeadRetriedAfterProgress(t *testing.T) { + t.Parallel() + + // Gate that blocks "retry-me" on the first call, proceeds on subsequent calls. + callCounts := map[string]int{} + gateStage := &callCountingGateStage{ + name: "gate", + callCounts: callCounts, + // Block "retry-me" only on its first gate invocation + firstCallBlock: map[string]bool{"retry-me": true}, + } + + cfg := BeadLoopConfig{ + Gate: gateStage, + Build: newNoopStage("build"), + Validate: newNoopStage("validate"), + Review: newNoopStage("review"), + Epilogue: newNoopStage("epilogue"), + } + + loop, err := NewBeadLoop(cfg) + if err != nil { + t.Fatalf("NewBeadLoop: %v", err) + } + + beads := []*bead.Bead{ + {ID: "proceed-me"}, + {ID: "retry-me"}, + } + + _, err = loop.Run(context.Background(), beads, nil) + if err != nil { + t.Fatalf("expected loop to succeed after blocked bead unblocks on retry, got: %v", err) + } + + // Gate should have been called twice for "retry-me": once blocked, once proceeds + if callCounts["retry-me"] != 2 { + t.Fatalf("gate call count for retry-me = %d, want 2 (blocked then proceed)", callCounts["retry-me"]) + } +} + +// callCountingGateStage is a gate that blocks a bead on its first invocation +// and proceeds on all subsequent invocations. +type callCountingGateStage struct { + name string + callCounts map[string]int + firstCallBlock map[string]bool +} + +func (s *callCountingGateStage) Name() string { return s.name } + +func (s *callCountingGateStage) Run(_ context.Context, req *stage.Request) (*stage.Result, error) { + s.callCounts[req.Bead.ID]++ + if s.firstCallBlock[req.Bead.ID] && s.callCounts[req.Bead.ID] == 1 { + return &stage.Result{Decision: stage.DecisionBlock}, nil + } + return &stage.Result{Decision: stage.DecisionProceed}, nil +} + // scriptedGateStage returns configured decisions per bead ID; defaults to Proceed. type scriptedGateStage struct { name string From 188afe9b6d052da088d712e24a97aef140967bd4 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:17:46 -0500 Subject: [PATCH 34/80] [bead:gromit-fhp2q/build:default/iter:15] proceed --- .gromit/v2/events.jsonl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.gromit/v2/events.jsonl b/.gromit/v2/events.jsonl index 6865a07d8..cca928769 100644 --- a/.gromit/v2/events.jsonl +++ b/.gromit/v2/events.jsonl @@ -84,3 +84,11 @@ {"schema_version":1,"timestamp":"2026-03-07T16:09:43.538724-05:00","type":"stage.completed","stage_name":"validate:default","bead_id":"gromit-f4ouz","iteration":14,"success":true,"duration":39650083} {"schema_version":1,"timestamp":"2026-03-07T16:09:43.586665-05:00","type":"stage.started","stage_name":"review:default","bead_id":"gromit-f4ouz","iteration":14} {"schema_version":1,"timestamp":"2026-03-07T16:09:43.586676-05:00","type":"stage.completed","stage_name":"review:default","bead_id":"gromit-f4ouz","iteration":14,"success":true,"duration":4375} +{"schema_version":1,"timestamp":"2026-03-07T16:09:43.633943-05:00","type":"stage.started","stage_name":"epilogue:default","bead_id":"gromit-f4ouz","iteration":14} +{"schema_version":1,"timestamp":"2026-03-07T16:09:43.790245-05:00","type":"stage.completed","stage_name":"epilogue:default","bead_id":"gromit-f4ouz","iteration":14,"success":true,"duration":156293250} +{"schema_version":1,"timestamp":"2026-03-07T16:09:43.790257-05:00","type":"bead.completed","bead_id":"gromit-f4ouz","bead_title":"Add scoped CLAUDE.md loading to PromptAssembler","iteration":14,"success":true} +{"schema_version":1,"timestamp":"2026-03-07T16:09:43.790285-05:00","type":"bead.started","bead_id":"gromit-fhp2q","bead_title":"Implement gate re-queue logic in bead loop","iteration":15} +{"schema_version":1,"timestamp":"2026-03-07T16:09:43.790287-05:00","type":"stage.started","stage_name":"gate:default","bead_id":"gromit-fhp2q","iteration":15} +{"schema_version":1,"timestamp":"2026-03-07T16:09:43.86676-05:00","type":"stage.completed","stage_name":"gate:default","bead_id":"gromit-fhp2q","iteration":15,"success":true,"duration":76471875} +{"schema_version":1,"timestamp":"2026-03-07T16:09:43.866768-05:00","type":"stage.started","stage_name":"build:default","bead_id":"gromit-fhp2q","iteration":15} +{"schema_version":1,"timestamp":"2026-03-07T16:17:46.500635-05:00","type":"stage.completed","stage_name":"build:default","bead_id":"gromit-fhp2q","iteration":15,"success":true,"duration":482632017375} From a242a7ba89743158566d7c8f7a0697114fd5b984 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:17:46 -0500 Subject: [PATCH 35/80] [bead:gromit-fhp2q/validate:default/iter:15] proceed --- .gromit/v2/events.jsonl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gromit/v2/events.jsonl b/.gromit/v2/events.jsonl index cca928769..a91817700 100644 --- a/.gromit/v2/events.jsonl +++ b/.gromit/v2/events.jsonl @@ -92,3 +92,5 @@ {"schema_version":1,"timestamp":"2026-03-07T16:09:43.86676-05:00","type":"stage.completed","stage_name":"gate:default","bead_id":"gromit-fhp2q","iteration":15,"success":true,"duration":76471875} {"schema_version":1,"timestamp":"2026-03-07T16:09:43.866768-05:00","type":"stage.started","stage_name":"build:default","bead_id":"gromit-fhp2q","iteration":15} {"schema_version":1,"timestamp":"2026-03-07T16:17:46.500635-05:00","type":"stage.completed","stage_name":"build:default","bead_id":"gromit-fhp2q","iteration":15,"success":true,"duration":482632017375} +{"schema_version":1,"timestamp":"2026-03-07T16:17:46.577968-05:00","type":"stage.started","stage_name":"validate:default","bead_id":"gromit-fhp2q","iteration":15} +{"schema_version":1,"timestamp":"2026-03-07T16:17:46.618675-05:00","type":"stage.completed","stage_name":"validate:default","bead_id":"gromit-fhp2q","iteration":15,"success":true,"duration":40694750} From cd7c17292d46d33c2387ff0056f5817e03d222b2 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:17:46 -0500 Subject: [PATCH 36/80] [bead:gromit-fhp2q/review:default/iter:15] proceed --- .gromit/v2/events.jsonl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gromit/v2/events.jsonl b/.gromit/v2/events.jsonl index a91817700..9fedc2270 100644 --- a/.gromit/v2/events.jsonl +++ b/.gromit/v2/events.jsonl @@ -94,3 +94,5 @@ {"schema_version":1,"timestamp":"2026-03-07T16:17:46.500635-05:00","type":"stage.completed","stage_name":"build:default","bead_id":"gromit-fhp2q","iteration":15,"success":true,"duration":482632017375} {"schema_version":1,"timestamp":"2026-03-07T16:17:46.577968-05:00","type":"stage.started","stage_name":"validate:default","bead_id":"gromit-fhp2q","iteration":15} {"schema_version":1,"timestamp":"2026-03-07T16:17:46.618675-05:00","type":"stage.completed","stage_name":"validate:default","bead_id":"gromit-fhp2q","iteration":15,"success":true,"duration":40694750} +{"schema_version":1,"timestamp":"2026-03-07T16:17:46.66731-05:00","type":"stage.started","stage_name":"review:default","bead_id":"gromit-fhp2q","iteration":15} +{"schema_version":1,"timestamp":"2026-03-07T16:17:46.667329-05:00","type":"stage.completed","stage_name":"review:default","bead_id":"gromit-fhp2q","iteration":15,"success":true,"duration":11500} From 6a228804d69fcb4182baf30c59824939ff72ef02 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:20:21 -0500 Subject: [PATCH 37/80] red: test Select returns error when no providers registered --- internal/v2/routing/router_test.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 internal/v2/routing/router_test.go diff --git a/internal/v2/routing/router_test.go b/internal/v2/routing/router_test.go new file mode 100644 index 000000000..35bd67e0e --- /dev/null +++ b/internal/v2/routing/router_test.go @@ -0,0 +1,15 @@ +package routing_test + +import ( + "testing" + + "github.com/danabrams/gromit/internal/v2/routing" +) + +func TestRouterSelectReturnsErrorWhenNoProviders(t *testing.T) { + r := routing.NewRouter(routing.RouterConfig{}) + _, _, err := r.Select("build", "low") + if err == nil { + t.Fatal("expected error when no providers configured, got nil") + } +} From b618fbd2793c2b3e2057d184fe9b52cdfa9b559b Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:20:43 -0500 Subject: [PATCH 38/80] green: implement Router struct with Select, MarkUnavailable, RecordInvocation --- internal/v2/routing/router.go | 149 ++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 internal/v2/routing/router.go diff --git a/internal/v2/routing/router.go b/internal/v2/routing/router.go new file mode 100644 index 000000000..3f721c11f --- /dev/null +++ b/internal/v2/routing/router.go @@ -0,0 +1,149 @@ +package routing + +import ( + "fmt" + "sync" + "time" + + "github.com/danabrams/gromit/internal/v2/llmtypes" +) + +// RouterConfig holds the configuration for the Router. +type RouterConfig struct { + // Providers maps provider name to LLMProvider implementation. + Providers map[string]llmtypes.LLMProvider + // PhasePreferences maps phase name to preferred provider name. + PhasePreferences map[string]string + // Ratio maps provider name to relative invocation weight. + Ratio map[string]int + // Cooldown is the duration before an unavailable provider is re-enabled. + Cooldown time.Duration +} + +// Router selects providers for LLM invocations using phase preferences, +// ratio balancing, and fallback with cooldown. +type Router struct { + providers map[string]llmtypes.LLMProvider + phasePreferences map[string]string + ratio map[string]int + counts map[string]int + unavailable map[string]time.Time + cooldown time.Duration + mu sync.Mutex +} + +// NewRouter constructs a Router from the provided config. +func NewRouter(cfg RouterConfig) *Router { + providers := cfg.Providers + if providers == nil { + providers = map[string]llmtypes.LLMProvider{} + } + phasePreferences := cfg.PhasePreferences + if phasePreferences == nil { + phasePreferences = map[string]string{} + } + ratio := cfg.Ratio + if ratio == nil { + ratio = map[string]int{} + } + return &Router{ + providers: providers, + phasePreferences: phasePreferences, + ratio: ratio, + counts: map[string]int{}, + unavailable: map[string]time.Time{}, + cooldown: cfg.Cooldown, + } +} + +// Select picks an LLMProvider and model name for the given phase and tier. +// Phase preferences are checked first, then ratio balancing among available providers. +// Returns an error when no providers are configured or all are unavailable. +func (r *Router) Select(phase, tier string) (llmtypes.LLMProvider, string, error) { + r.mu.Lock() + defer r.mu.Unlock() + + if len(r.providers) == 0 { + return nil, "", fmt.Errorf("routing: no providers configured") + } + + now := time.Now() + + // Phase preference: if a specific provider is pinned for this phase, use it. + if preferred, ok := r.phasePreferences[phase]; ok && preferred != "" { + if p, exists := r.providers[preferred]; exists { + if !r.isUnavailable(preferred, now) { + return p, tier, nil + } + } + } + + // Ratio balancing: pick the available provider most under-served relative to its weight. + chosen := r.selectByRatio(now) + if chosen == "" { + return nil, "", fmt.Errorf("routing: all providers unavailable") + } + return r.providers[chosen], tier, nil +} + +// MarkUnavailable marks the named provider as unavailable for the cooldown duration. +func (r *Router) MarkUnavailable(name string) { + r.mu.Lock() + defer r.mu.Unlock() + until := time.Now().Add(r.cooldown) + r.unavailable[name] = until +} + +// RecordInvocation increments the invocation count for the named provider. +func (r *Router) RecordInvocation(name string) { + r.mu.Lock() + defer r.mu.Unlock() + r.counts[name]++ +} + +// isUnavailable reports whether the named provider is currently in cooldown. +// Caller must hold r.mu. +func (r *Router) isUnavailable(name string, now time.Time) bool { + until, exists := r.unavailable[name] + if !exists { + return false + } + if now.After(until) { + delete(r.unavailable, name) + return false + } + return true +} + +// selectByRatio picks the available provider most under-served relative to its weight. +// Returns empty string when all providers are unavailable. +// Caller must hold r.mu. +func (r *Router) selectByRatio(now time.Time) string { + // Collect available providers. + type candidate struct { + name string + score float64 + } + var best candidate + found := false + for name := range r.providers { + if r.isUnavailable(name, now) { + continue + } + weight := r.ratio[name] + if weight <= 0 { + weight = 1 + } + count := r.counts[name] + // Score: lower count relative to weight is preferred (most under-served). + score := float64(count) / float64(weight) + if !found || score < best.score { + best = candidate{name: name, score: score} + found = true + } + } + if !found { + return "" + } + return best.name +} From cd5abf7f42fb4857e1cd4be0f8d7faf6c88cdbc7 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:21:48 -0500 Subject: [PATCH 39/80] red: test phase preference selects pinned provider for phase --- internal/v2/routing/router_test.go | 37 ++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/internal/v2/routing/router_test.go b/internal/v2/routing/router_test.go index 35bd67e0e..564d9110f 100644 --- a/internal/v2/routing/router_test.go +++ b/internal/v2/routing/router_test.go @@ -1,11 +1,25 @@ package routing_test import ( + "context" "testing" + "github.com/danabrams/gromit/internal/v2/llmtypes" "github.com/danabrams/gromit/internal/v2/routing" ) +type stubProvider struct { + name string +} + +func (s *stubProvider) Invoke(_ context.Context, _ llmtypes.LLMInvokeRequest) (*llmtypes.LLMInvokeResponse, error) { + return &llmtypes.LLMInvokeResponse{}, nil +} + +func (s *stubProvider) StreamInvoke(_ context.Context, _ llmtypes.LLMStreamInvokeRequest) (*llmtypes.LLMInvokeResponse, error) { + return &llmtypes.LLMInvokeResponse{}, nil +} + func TestRouterSelectReturnsErrorWhenNoProviders(t *testing.T) { r := routing.NewRouter(routing.RouterConfig{}) _, _, err := r.Select("build", "low") @@ -13,3 +27,26 @@ func TestRouterSelectReturnsErrorWhenNoProviders(t *testing.T) { t.Fatal("expected error when no providers configured, got nil") } } + +func TestRouterSelectUsesPhasePreference(t *testing.T) { + providerA := &stubProvider{name: "providerA"} + providerB := &stubProvider{name: "providerB"} + + r := routing.NewRouter(routing.RouterConfig{ + Providers: map[string]llmtypes.LLMProvider{ + "providerA": providerA, + "providerB": providerB, + }, + PhasePreferences: map[string]string{ + "build": "providerB", + }, + }) + + got, _, err := r.Select("build", "low") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != llmtypes.LLMProvider(providerB) { + t.Fatalf("expected providerB selected for build phase, got %v", got) + } +} From 9417b3e2a6f09ce2e58fd43700fef8db1a70a285 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:23:19 -0500 Subject: [PATCH 40/80] green: add tests for ratio balancing, fallback, and cooldown re-enable --- internal/v2/routing/router_test.go | 97 ++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/internal/v2/routing/router_test.go b/internal/v2/routing/router_test.go index 564d9110f..4cd4f1ad9 100644 --- a/internal/v2/routing/router_test.go +++ b/internal/v2/routing/router_test.go @@ -3,6 +3,7 @@ package routing_test import ( "context" "testing" + "time" "github.com/danabrams/gromit/internal/v2/llmtypes" "github.com/danabrams/gromit/internal/v2/routing" @@ -28,6 +29,102 @@ func TestRouterSelectReturnsErrorWhenNoProviders(t *testing.T) { } } +func TestRouterMarkUnavailableCausesFallback(t *testing.T) { + providerA := &stubProvider{name: "providerA"} + providerB := &stubProvider{name: "providerB"} + + r := routing.NewRouter(routing.RouterConfig{ + Providers: map[string]llmtypes.LLMProvider{ + "providerA": providerA, + "providerB": providerB, + }, + PhasePreferences: map[string]string{ + "build": "providerA", + }, + Cooldown: 24 * time.Hour, + }) + + // Without mark, build phase should select providerA via preference. + got, _, err := r.Select("build", "low") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != llmtypes.LLMProvider(providerA) { + t.Fatalf("expected providerA before mark, got %v", got) + } + + // After marking providerA unavailable, should fall back to providerB. + r.MarkUnavailable("providerA") + got, _, err = r.Select("build", "low") + if err != nil { + t.Fatalf("unexpected error after mark: %v", err) + } + if got != llmtypes.LLMProvider(providerB) { + t.Fatalf("expected providerB after providerA marked unavailable, got %v", got) + } +} + +func TestRouterReEnablesProviderAfterCooldown(t *testing.T) { + providerA := &stubProvider{name: "providerA"} + + r := routing.NewRouter(routing.RouterConfig{ + Providers: map[string]llmtypes.LLMProvider{ + "providerA": providerA, + }, + Cooldown: time.Millisecond, + }) + + r.MarkUnavailable("providerA") + + // Immediately after marking, provider should be unavailable. + _, _, err := r.Select("build", "low") + if err == nil { + t.Fatal("expected error when only provider is unavailable, got nil") + } + + // After cooldown, provider should be available again. + time.Sleep(5 * time.Millisecond) + got, _, err := r.Select("build", "low") + if err != nil { + t.Fatalf("expected provider re-enabled after cooldown, got error: %v", err) + } + if got != llmtypes.LLMProvider(providerA) { + t.Fatalf("expected providerA after cooldown, got %v", got) + } +} + +func TestRouterSelectRatioBalancesAcrossProviders(t *testing.T) { + providerA := &stubProvider{name: "providerA"} + providerB := &stubProvider{name: "providerB"} + + r := routing.NewRouter(routing.RouterConfig{ + Providers: map[string]llmtypes.LLMProvider{ + "providerA": providerA, + "providerB": providerB, + }, + Ratio: map[string]int{ + "providerA": 3, + "providerB": 1, + }, + }) + + // After recording 3 invocations for providerA and 1 for providerB, + // providerA's score = 3/3=1.0, providerB's score = 1/1=1.0 (tied). + // After 4 more for providerA, score = 4/3 > 1.0, so providerB should win. + r.RecordInvocation("providerA") + r.RecordInvocation("providerA") + r.RecordInvocation("providerA") + r.RecordInvocation("providerA") // now 4 for A vs 0 for B + // providerA score = 4/3 ≈ 1.33, providerB score = 0/1 = 0 → B should be selected + got, _, err := r.Select("build", "low") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != llmtypes.LLMProvider(providerB) { + t.Fatalf("expected providerB selected after providerA over-served, got %v", got) + } +} + func TestRouterSelectUsesPhasePreference(t *testing.T) { providerA := &stubProvider{name: "providerA"} providerB := &stubProvider{name: "providerB"} From 92d274f088654578b452c32d65bdb29caf0ec577 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:23:30 -0500 Subject: [PATCH 41/80] [bead:gromit-jv6ns/build:default/iter:16] proceed --- .gromit/v2/events.jsonl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.gromit/v2/events.jsonl b/.gromit/v2/events.jsonl index 9fedc2270..a60224ea2 100644 --- a/.gromit/v2/events.jsonl +++ b/.gromit/v2/events.jsonl @@ -96,3 +96,11 @@ {"schema_version":1,"timestamp":"2026-03-07T16:17:46.618675-05:00","type":"stage.completed","stage_name":"validate:default","bead_id":"gromit-fhp2q","iteration":15,"success":true,"duration":40694750} {"schema_version":1,"timestamp":"2026-03-07T16:17:46.66731-05:00","type":"stage.started","stage_name":"review:default","bead_id":"gromit-fhp2q","iteration":15} {"schema_version":1,"timestamp":"2026-03-07T16:17:46.667329-05:00","type":"stage.completed","stage_name":"review:default","bead_id":"gromit-fhp2q","iteration":15,"success":true,"duration":11500} +{"schema_version":1,"timestamp":"2026-03-07T16:17:46.715576-05:00","type":"stage.started","stage_name":"epilogue:default","bead_id":"gromit-fhp2q","iteration":15} +{"schema_version":1,"timestamp":"2026-03-07T16:17:48.204994-05:00","type":"stage.completed","stage_name":"epilogue:default","bead_id":"gromit-fhp2q","iteration":15,"success":true,"duration":1489403416} +{"schema_version":1,"timestamp":"2026-03-07T16:17:48.205025-05:00","type":"bead.completed","bead_id":"gromit-fhp2q","bead_title":"Implement gate re-queue logic in bead loop","iteration":15,"success":true} +{"schema_version":1,"timestamp":"2026-03-07T16:17:48.205098-05:00","type":"bead.started","bead_id":"gromit-jv6ns","bead_title":"Create Router interface and stub in v2/routing","iteration":16} +{"schema_version":1,"timestamp":"2026-03-07T16:17:48.205102-05:00","type":"stage.started","stage_name":"gate:default","bead_id":"gromit-jv6ns","iteration":16} +{"schema_version":1,"timestamp":"2026-03-07T16:17:48.32269-05:00","type":"stage.completed","stage_name":"gate:default","bead_id":"gromit-jv6ns","iteration":16,"success":true,"duration":117586292} +{"schema_version":1,"timestamp":"2026-03-07T16:17:48.322698-05:00","type":"stage.started","stage_name":"build:default","bead_id":"gromit-jv6ns","iteration":16} +{"schema_version":1,"timestamp":"2026-03-07T16:23:30.158349-05:00","type":"stage.completed","stage_name":"build:default","bead_id":"gromit-jv6ns","iteration":16,"success":true,"duration":341834340959} From a2ddeb1f3f1458e8d6f5b17cf3ec3f8ed0999c73 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:23:30 -0500 Subject: [PATCH 42/80] [bead:gromit-jv6ns/validate:default/iter:16] proceed --- .gromit/v2/events.jsonl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gromit/v2/events.jsonl b/.gromit/v2/events.jsonl index a60224ea2..979407084 100644 --- a/.gromit/v2/events.jsonl +++ b/.gromit/v2/events.jsonl @@ -104,3 +104,5 @@ {"schema_version":1,"timestamp":"2026-03-07T16:17:48.32269-05:00","type":"stage.completed","stage_name":"gate:default","bead_id":"gromit-jv6ns","iteration":16,"success":true,"duration":117586292} {"schema_version":1,"timestamp":"2026-03-07T16:17:48.322698-05:00","type":"stage.started","stage_name":"build:default","bead_id":"gromit-jv6ns","iteration":16} {"schema_version":1,"timestamp":"2026-03-07T16:23:30.158349-05:00","type":"stage.completed","stage_name":"build:default","bead_id":"gromit-jv6ns","iteration":16,"success":true,"duration":341834340959} +{"schema_version":1,"timestamp":"2026-03-07T16:23:30.226995-05:00","type":"stage.started","stage_name":"validate:default","bead_id":"gromit-jv6ns","iteration":16} +{"schema_version":1,"timestamp":"2026-03-07T16:23:30.266801-05:00","type":"stage.completed","stage_name":"validate:default","bead_id":"gromit-jv6ns","iteration":16,"success":true,"duration":39797250} From 1d7892e7a4336d23a806dd69aa4b2088865316f2 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:23:30 -0500 Subject: [PATCH 43/80] [bead:gromit-jv6ns/review:default/iter:16] proceed --- .gromit/v2/events.jsonl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gromit/v2/events.jsonl b/.gromit/v2/events.jsonl index 979407084..a9d6e73a0 100644 --- a/.gromit/v2/events.jsonl +++ b/.gromit/v2/events.jsonl @@ -106,3 +106,5 @@ {"schema_version":1,"timestamp":"2026-03-07T16:23:30.158349-05:00","type":"stage.completed","stage_name":"build:default","bead_id":"gromit-jv6ns","iteration":16,"success":true,"duration":341834340959} {"schema_version":1,"timestamp":"2026-03-07T16:23:30.226995-05:00","type":"stage.started","stage_name":"validate:default","bead_id":"gromit-jv6ns","iteration":16} {"schema_version":1,"timestamp":"2026-03-07T16:23:30.266801-05:00","type":"stage.completed","stage_name":"validate:default","bead_id":"gromit-jv6ns","iteration":16,"success":true,"duration":39797250} +{"schema_version":1,"timestamp":"2026-03-07T16:23:30.314517-05:00","type":"stage.started","stage_name":"review:default","bead_id":"gromit-jv6ns","iteration":16} +{"schema_version":1,"timestamp":"2026-03-07T16:23:30.314526-05:00","type":"stage.completed","stage_name":"review:default","bead_id":"gromit-jv6ns","iteration":16,"success":true,"duration":2208} From 5ac1cc65ffd027710f936742da4d94e0e90ee4f6 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:25:48 -0500 Subject: [PATCH 44/80] red: test TierForPhase returns phase override or fallback tier Co-Authored-By: Claude Sonnet 4.6 --- internal/v2/routing/tier_test.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 internal/v2/routing/tier_test.go diff --git a/internal/v2/routing/tier_test.go b/internal/v2/routing/tier_test.go new file mode 100644 index 000000000..00f6fa16e --- /dev/null +++ b/internal/v2/routing/tier_test.go @@ -0,0 +1,30 @@ +package routing_test + +import ( + "testing" + + "github.com/danabrams/gromit/internal/v2/routing" +) + +func TestTierForPhaseUsesOverrideWhenConfigured(t *testing.T) { + phaseModels := map[string]string{ + "build": "low", + "red": "low", + } + + got := routing.TierForPhase("build", phaseModels, "medium") + if got != "low" { + t.Fatalf("TierForPhase(%q) = %q, want %q", "build", got, "low") + } +} + +func TestTierForPhaseFallsBackWhenNoOverride(t *testing.T) { + phaseModels := map[string]string{ + "build": "low", + } + + got := routing.TierForPhase("validate", phaseModels, "medium") + if got != "medium" { + t.Fatalf("TierForPhase(%q) = %q, want %q", "validate", got, "medium") + } +} From ea171744d6129ccb094ccbfed25f8d34a8fc91fb Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:26:03 -0500 Subject: [PATCH 45/80] green: implement TierForPhase for phase model tier lookup Co-Authored-By: Claude Sonnet 4.6 --- internal/v2/routing/tier.go | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 internal/v2/routing/tier.go diff --git a/internal/v2/routing/tier.go b/internal/v2/routing/tier.go new file mode 100644 index 000000000..43ab6e041 --- /dev/null +++ b/internal/v2/routing/tier.go @@ -0,0 +1,11 @@ +package routing + +// TierForPhase returns the effective tier for a phase. +// phaseModels maps phase name to tier name (from methodology.phase_models config). +// Falls back to fallbackTier when the phase has no configured override. +func TierForPhase(phase string, phaseModels map[string]string, fallbackTier string) string { + if tier, ok := phaseModels[phase]; ok && tier != "" { + return tier + } + return fallbackTier +} From a75fa3ef14a3dcdf02d56968c3c16aa76c31db3d Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:26:20 -0500 Subject: [PATCH 46/80] red: test ResolveModel maps abstract tier to provider-specific model name Co-Authored-By: Claude Sonnet 4.6 --- internal/v2/routing/tier_test.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/internal/v2/routing/tier_test.go b/internal/v2/routing/tier_test.go index 00f6fa16e..5bafdc568 100644 --- a/internal/v2/routing/tier_test.go +++ b/internal/v2/routing/tier_test.go @@ -28,3 +28,23 @@ func TestTierForPhaseFallsBackWhenNoOverride(t *testing.T) { t.Fatalf("TierForPhase(%q) = %q, want %q", "validate", got, "medium") } } + +func TestResolveModelMapsAbstractTierToProviderModel(t *testing.T) { + models := map[string]string{ + "low": "claude-haiku-4-5-20251001", + "medium": "claude-sonnet-4-6", + "high": "claude-opus-4-6", + } + + got := routing.ResolveModel("low", models) + if got != "claude-haiku-4-5-20251001" { + t.Fatalf("ResolveModel(%q) = %q, want %q", "low", got, "claude-haiku-4-5-20251001") + } +} + +func TestResolveModelReturnsTierWhenNoMapping(t *testing.T) { + got := routing.ResolveModel("medium", map[string]string{}) + if got != "medium" { + t.Fatalf("ResolveModel with empty map = %q, want %q", got, "medium") + } +} From f719d5435530a0fdf85f617be4de18d6f98abfae Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:26:36 -0500 Subject: [PATCH 47/80] green: implement ResolveModel for abstract tier to provider model name mapping Co-Authored-By: Claude Sonnet 4.6 --- internal/v2/routing/tier.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/internal/v2/routing/tier.go b/internal/v2/routing/tier.go index 43ab6e041..f4f95be37 100644 --- a/internal/v2/routing/tier.go +++ b/internal/v2/routing/tier.go @@ -1,5 +1,15 @@ package routing +// ResolveModel maps an abstract tier name to a provider-specific model name. +// Uses the provider's tier-to-model map (e.g. {"low": "claude-haiku-4-5-20251001"}). +// Returns tier as-is if no mapping is found. +func ResolveModel(tier string, models map[string]string) string { + if model, ok := models[tier]; ok && model != "" { + return model + } + return tier +} + // TierForPhase returns the effective tier for a phase. // phaseModels maps phase name to tier name (from methodology.phase_models config). // Falls back to fallbackTier when the phase has no configured override. From 5c3a28078659e86a9df324523822bc1f8044bf5b Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:26:46 -0500 Subject: [PATCH 48/80] [bead:gromit-jzbe3/build:default/iter:17] proceed --- .gromit/v2/events.jsonl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.gromit/v2/events.jsonl b/.gromit/v2/events.jsonl index a9d6e73a0..fc9a5c975 100644 --- a/.gromit/v2/events.jsonl +++ b/.gromit/v2/events.jsonl @@ -108,3 +108,11 @@ {"schema_version":1,"timestamp":"2026-03-07T16:23:30.266801-05:00","type":"stage.completed","stage_name":"validate:default","bead_id":"gromit-jv6ns","iteration":16,"success":true,"duration":39797250} {"schema_version":1,"timestamp":"2026-03-07T16:23:30.314517-05:00","type":"stage.started","stage_name":"review:default","bead_id":"gromit-jv6ns","iteration":16} {"schema_version":1,"timestamp":"2026-03-07T16:23:30.314526-05:00","type":"stage.completed","stage_name":"review:default","bead_id":"gromit-jv6ns","iteration":16,"success":true,"duration":2208} +{"schema_version":1,"timestamp":"2026-03-07T16:23:30.361882-05:00","type":"stage.started","stage_name":"epilogue:default","bead_id":"gromit-jv6ns","iteration":16} +{"schema_version":1,"timestamp":"2026-03-07T16:23:30.513717-05:00","type":"stage.completed","stage_name":"epilogue:default","bead_id":"gromit-jv6ns","iteration":16,"success":true,"duration":151828917} +{"schema_version":1,"timestamp":"2026-03-07T16:23:30.51373-05:00","type":"bead.completed","bead_id":"gromit-jv6ns","bead_title":"Create Router interface and stub in v2/routing","iteration":16,"success":true} +{"schema_version":1,"timestamp":"2026-03-07T16:23:30.513753-05:00","type":"bead.started","bead_id":"gromit-jzbe3","bead_title":"Implement tier resolution in internal/v2/routing/tier.go","iteration":17} +{"schema_version":1,"timestamp":"2026-03-07T16:23:30.513756-05:00","type":"stage.started","stage_name":"gate:default","bead_id":"gromit-jzbe3","iteration":17} +{"schema_version":1,"timestamp":"2026-03-07T16:23:30.590854-05:00","type":"stage.completed","stage_name":"gate:default","bead_id":"gromit-jzbe3","iteration":17,"success":true,"duration":77095916} +{"schema_version":1,"timestamp":"2026-03-07T16:23:30.590861-05:00","type":"stage.started","stage_name":"build:default","bead_id":"gromit-jzbe3","iteration":17} +{"schema_version":1,"timestamp":"2026-03-07T16:26:46.084705-05:00","type":"stage.completed","stage_name":"build:default","bead_id":"gromit-jzbe3","iteration":17,"success":true,"duration":195493094042} From 54ff9bf9ea203bc672b3fdd6899adb2c0174fa95 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:26:46 -0500 Subject: [PATCH 49/80] [bead:gromit-jzbe3/validate:default/iter:17] proceed --- .gromit/v2/events.jsonl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gromit/v2/events.jsonl b/.gromit/v2/events.jsonl index fc9a5c975..0bd745837 100644 --- a/.gromit/v2/events.jsonl +++ b/.gromit/v2/events.jsonl @@ -116,3 +116,5 @@ {"schema_version":1,"timestamp":"2026-03-07T16:23:30.590854-05:00","type":"stage.completed","stage_name":"gate:default","bead_id":"gromit-jzbe3","iteration":17,"success":true,"duration":77095916} {"schema_version":1,"timestamp":"2026-03-07T16:23:30.590861-05:00","type":"stage.started","stage_name":"build:default","bead_id":"gromit-jzbe3","iteration":17} {"schema_version":1,"timestamp":"2026-03-07T16:26:46.084705-05:00","type":"stage.completed","stage_name":"build:default","bead_id":"gromit-jzbe3","iteration":17,"success":true,"duration":195493094042} +{"schema_version":1,"timestamp":"2026-03-07T16:26:46.157044-05:00","type":"stage.started","stage_name":"validate:default","bead_id":"gromit-jzbe3","iteration":17} +{"schema_version":1,"timestamp":"2026-03-07T16:26:46.196812-05:00","type":"stage.completed","stage_name":"validate:default","bead_id":"gromit-jzbe3","iteration":17,"success":true,"duration":39758792} From 11b7dca8c0ef56abbba8444a05bfbd16231b0093 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:26:46 -0500 Subject: [PATCH 50/80] [bead:gromit-jzbe3/review:default/iter:17] proceed --- .gromit/v2/events.jsonl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gromit/v2/events.jsonl b/.gromit/v2/events.jsonl index 0bd745837..d8059399e 100644 --- a/.gromit/v2/events.jsonl +++ b/.gromit/v2/events.jsonl @@ -118,3 +118,5 @@ {"schema_version":1,"timestamp":"2026-03-07T16:26:46.084705-05:00","type":"stage.completed","stage_name":"build:default","bead_id":"gromit-jzbe3","iteration":17,"success":true,"duration":195493094042} {"schema_version":1,"timestamp":"2026-03-07T16:26:46.157044-05:00","type":"stage.started","stage_name":"validate:default","bead_id":"gromit-jzbe3","iteration":17} {"schema_version":1,"timestamp":"2026-03-07T16:26:46.196812-05:00","type":"stage.completed","stage_name":"validate:default","bead_id":"gromit-jzbe3","iteration":17,"success":true,"duration":39758792} +{"schema_version":1,"timestamp":"2026-03-07T16:26:46.24439-05:00","type":"stage.started","stage_name":"review:default","bead_id":"gromit-jzbe3","iteration":17} +{"schema_version":1,"timestamp":"2026-03-07T16:26:46.244407-05:00","type":"stage.completed","stage_name":"review:default","bead_id":"gromit-jzbe3","iteration":17,"success":true,"duration":1708} From e26bf98ffddcc8e36a0e5ef7e803f29d7bd4d45d Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:28:48 -0500 Subject: [PATCH 51/80] green: add edge case tests for empty-string fallthrough in TierForPhase and ResolveModel Co-Authored-By: Claude Sonnet 4.6 --- internal/v2/routing/tier_test.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/internal/v2/routing/tier_test.go b/internal/v2/routing/tier_test.go index 5bafdc568..49db6f034 100644 --- a/internal/v2/routing/tier_test.go +++ b/internal/v2/routing/tier_test.go @@ -48,3 +48,25 @@ func TestResolveModelReturnsTierWhenNoMapping(t *testing.T) { t.Fatalf("ResolveModel with empty map = %q, want %q", got, "medium") } } + +func TestTierForPhaseEmptyStringOverrideFallsBack(t *testing.T) { + phaseModels := map[string]string{ + "build": "", + } + + got := routing.TierForPhase("build", phaseModels, "medium") + if got != "medium" { + t.Fatalf("TierForPhase with empty string override = %q, want %q", got, "medium") + } +} + +func TestResolveModelEmptyStringModelFallsBack(t *testing.T) { + models := map[string]string{ + "low": "", + } + + got := routing.ResolveModel("low", models) + if got != "low" { + t.Fatalf("ResolveModel with empty string model = %q, want %q", got, "low") + } +} From edd2869c3524a007dba7fa07ddc0353cff8fc616 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:28:52 -0500 Subject: [PATCH 52/80] [bead:gromit-uulh1/build:default/iter:19] proceed --- .gromit/v2/events.jsonl | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.gromit/v2/events.jsonl b/.gromit/v2/events.jsonl index d8059399e..f767cae9d 100644 --- a/.gromit/v2/events.jsonl +++ b/.gromit/v2/events.jsonl @@ -120,3 +120,15 @@ {"schema_version":1,"timestamp":"2026-03-07T16:26:46.196812-05:00","type":"stage.completed","stage_name":"validate:default","bead_id":"gromit-jzbe3","iteration":17,"success":true,"duration":39758792} {"schema_version":1,"timestamp":"2026-03-07T16:26:46.24439-05:00","type":"stage.started","stage_name":"review:default","bead_id":"gromit-jzbe3","iteration":17} {"schema_version":1,"timestamp":"2026-03-07T16:26:46.244407-05:00","type":"stage.completed","stage_name":"review:default","bead_id":"gromit-jzbe3","iteration":17,"success":true,"duration":1708} +{"schema_version":1,"timestamp":"2026-03-07T16:26:46.295706-05:00","type":"stage.started","stage_name":"epilogue:default","bead_id":"gromit-jzbe3","iteration":17} +{"schema_version":1,"timestamp":"2026-03-07T16:26:46.39325-05:00","type":"stage.completed","stage_name":"epilogue:default","bead_id":"gromit-jzbe3","iteration":17,"success":true,"duration":97535750} +{"schema_version":1,"timestamp":"2026-03-07T16:26:46.393256-05:00","type":"bead.completed","bead_id":"gromit-jzbe3","bead_title":"Implement tier resolution in internal/v2/routing/tier.go","iteration":17,"success":true} +{"schema_version":1,"timestamp":"2026-03-07T16:26:46.393276-05:00","type":"bead.started","bead_id":"gromit-u4837","bead_title":"Add integration tests for full prompt shaping pipeline","iteration":18} +{"schema_version":1,"timestamp":"2026-03-07T16:26:46.393277-05:00","type":"stage.started","stage_name":"gate:default","bead_id":"gromit-u4837","iteration":18} +{"schema_version":1,"timestamp":"2026-03-07T16:26:46.534304-05:00","type":"stage.completed","stage_name":"gate:default","bead_id":"gromit-u4837","iteration":18,"success":true,"duration":141026250} +{"schema_version":1,"timestamp":"2026-03-07T16:26:46.53431-05:00","type":"bead.completed","bead_id":"gromit-u4837","bead_title":"Add integration tests for full prompt shaping pipeline","iteration":18,"success":false} +{"schema_version":1,"timestamp":"2026-03-07T16:26:46.534322-05:00","type":"bead.started","bead_id":"gromit-uulh1","bead_title":"Add unit tests for tier resolution","iteration":19} +{"schema_version":1,"timestamp":"2026-03-07T16:26:46.534323-05:00","type":"stage.started","stage_name":"gate:default","bead_id":"gromit-uulh1","iteration":19} +{"schema_version":1,"timestamp":"2026-03-07T16:26:46.676457-05:00","type":"stage.completed","stage_name":"gate:default","bead_id":"gromit-uulh1","iteration":19,"success":true,"duration":142132958} +{"schema_version":1,"timestamp":"2026-03-07T16:26:46.676465-05:00","type":"stage.started","stage_name":"build:default","bead_id":"gromit-uulh1","iteration":19} +{"schema_version":1,"timestamp":"2026-03-07T16:28:52.488873-05:00","type":"stage.completed","stage_name":"build:default","bead_id":"gromit-uulh1","iteration":19,"success":true,"duration":125811924125} From b2a3f8068250f2b384f38eb18e4a6534eb7e23f5 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:28:52 -0500 Subject: [PATCH 53/80] [bead:gromit-uulh1/validate:default/iter:19] proceed --- .gromit/v2/events.jsonl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gromit/v2/events.jsonl b/.gromit/v2/events.jsonl index f767cae9d..b6a43341a 100644 --- a/.gromit/v2/events.jsonl +++ b/.gromit/v2/events.jsonl @@ -132,3 +132,5 @@ {"schema_version":1,"timestamp":"2026-03-07T16:26:46.676457-05:00","type":"stage.completed","stage_name":"gate:default","bead_id":"gromit-uulh1","iteration":19,"success":true,"duration":142132958} {"schema_version":1,"timestamp":"2026-03-07T16:26:46.676465-05:00","type":"stage.started","stage_name":"build:default","bead_id":"gromit-uulh1","iteration":19} {"schema_version":1,"timestamp":"2026-03-07T16:28:52.488873-05:00","type":"stage.completed","stage_name":"build:default","bead_id":"gromit-uulh1","iteration":19,"success":true,"duration":125811924125} +{"schema_version":1,"timestamp":"2026-03-07T16:28:52.568245-05:00","type":"stage.started","stage_name":"validate:default","bead_id":"gromit-uulh1","iteration":19} +{"schema_version":1,"timestamp":"2026-03-07T16:28:52.608368-05:00","type":"stage.completed","stage_name":"validate:default","bead_id":"gromit-uulh1","iteration":19,"success":true,"duration":40113709} From 56bb03560db6c02dd5403d75fb1ffd6a228d680f Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:28:52 -0500 Subject: [PATCH 54/80] [bead:gromit-uulh1/review:default/iter:19] proceed --- .gromit/v2/events.jsonl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gromit/v2/events.jsonl b/.gromit/v2/events.jsonl index b6a43341a..4086ddc08 100644 --- a/.gromit/v2/events.jsonl +++ b/.gromit/v2/events.jsonl @@ -134,3 +134,5 @@ {"schema_version":1,"timestamp":"2026-03-07T16:28:52.488873-05:00","type":"stage.completed","stage_name":"build:default","bead_id":"gromit-uulh1","iteration":19,"success":true,"duration":125811924125} {"schema_version":1,"timestamp":"2026-03-07T16:28:52.568245-05:00","type":"stage.started","stage_name":"validate:default","bead_id":"gromit-uulh1","iteration":19} {"schema_version":1,"timestamp":"2026-03-07T16:28:52.608368-05:00","type":"stage.completed","stage_name":"validate:default","bead_id":"gromit-uulh1","iteration":19,"success":true,"duration":40113709} +{"schema_version":1,"timestamp":"2026-03-07T16:28:52.656542-05:00","type":"stage.started","stage_name":"review:default","bead_id":"gromit-uulh1","iteration":19} +{"schema_version":1,"timestamp":"2026-03-07T16:28:52.656556-05:00","type":"stage.completed","stage_name":"review:default","bead_id":"gromit-uulh1","iteration":19,"success":true,"duration":2667} From 955f807a3cf9965303e4eb051042872ae4919479 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:30:46 -0500 Subject: [PATCH 55/80] red: test loadBaseInstructions filters RULES.md sections by phase --- internal/v2/prompt/assembler_test.go | 40 ++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/internal/v2/prompt/assembler_test.go b/internal/v2/prompt/assembler_test.go index bbe3e05a7..9b0c4f4b1 100644 --- a/internal/v2/prompt/assembler_test.go +++ b/internal/v2/prompt/assembler_test.go @@ -78,6 +78,46 @@ func TestLoadProjectContextNoPackagePathsReturnsProjectUnchanged(t *testing.T) { } } +func TestLoadBaseInstructionsFiltersPhase(t *testing.T) { + base := `# Rules + +## Code Style + +- Use go fmt standard formatting + +## Build Only + +- Always run tests before committing + +## Review Only + +- Check for security issues` + + assembler := NewPromptAssembler(base, "", "", "") + + buildResult := assembler.loadBaseInstructions("build") + if !strings.Contains(buildResult, "## Code Style") { + t.Errorf("build result missing ## Code Style") + } + if !strings.Contains(buildResult, "## Build Only") { + t.Errorf("build result missing ## Build Only") + } + if strings.Contains(buildResult, "## Review Only") { + t.Errorf("build result should not contain ## Review Only") + } + + reviewResult := assembler.loadBaseInstructions("review") + if !strings.Contains(reviewResult, "## Code Style") { + t.Errorf("review result missing ## Code Style") + } + if strings.Contains(reviewResult, "## Build Only") { + t.Errorf("review result should not contain ## Build Only") + } + if !strings.Contains(reviewResult, "## Review Only") { + t.Errorf("review result missing ## Review Only") + } +} + func TestLoadProjectContextScopesArchitectureToMentionedPackage(t *testing.T) { project := `# Gromit From d2fdadb2b9aba08629c089879be9b882b04b95c6 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:31:36 -0500 Subject: [PATCH 56/80] green: implement loadBaseInstructions with phase filtering for PromptAssembler --- internal/v2/prompt/assembler.go | 120 ++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/internal/v2/prompt/assembler.go b/internal/v2/prompt/assembler.go index 52ec715b4..fe27f6f03 100644 --- a/internal/v2/prompt/assembler.go +++ b/internal/v2/prompt/assembler.go @@ -6,6 +6,126 @@ import ( "strings" ) +const ( + rulesSectionHeaderPrefix = "## " + phaseAnnotationPrefix = "" +) + +// loadBaseInstructions returns the base layer filtered to phase-relevant sections. +// Returns the full base content if phase is empty. +func (p *PromptAssembler) loadBaseInstructions(phase string) string { + if phase == "" { + return p.base + } + return filterRulesByPhase(p.base, phase) +} + +// filterRulesByPhase parses RULES.md content, filters ## sections by phase annotations, +// and strips annotation comments from the output. +func filterRulesByPhase(content, phase string) string { + lines := strings.Split(content, "\n") + + type section struct { + headerLine string + bodyLines []string + phases []string // nil = all phases + } + + var preamble []string + var sections []section + var current *section + + for _, line := range lines { + if strings.HasPrefix(line, rulesSectionHeaderPrefix) { + if current != nil { + sections = append(sections, *current) + } + phases := parseRulesPhaseAnnotation(line) + current = §ion{headerLine: line, phases: phases} + } else if current != nil { + current.bodyLines = append(current.bodyLines, line) + } else { + preamble = append(preamble, line) + } + } + if current != nil { + sections = append(sections, *current) + } + + var out strings.Builder + for _, line := range preamble { + out.WriteString(line) + out.WriteString("\n") + } + for _, s := range sections { + if !rulesPhaseMatches(s.phases, phase) { + continue + } + out.WriteString(stripRulesPhaseAnnotation(s.headerLine)) + out.WriteString("\n") + for _, line := range s.bodyLines { + out.WriteString(line) + out.WriteString("\n") + } + } + + result := out.String() + if strings.HasSuffix(result, "\n") { + result = result[:len(result)-1] + } + return result +} + +func parseRulesPhaseAnnotation(headerLine string) []string { + idx := strings.Index(headerLine, phaseAnnotationPrefix) + if idx < 0 { + return nil + } + rest := headerLine[idx+len(phaseAnnotationPrefix):] + endIdx := strings.Index(rest, phaseAnnotationSuffix) + if endIdx < 0 { + return nil + } + parts := strings.Split(rest[:endIdx], ",") + var phases []string + for _, p := range parts { + p = strings.ToLower(strings.TrimSpace(p)) + if p != "" { + phases = append(phases, p) + } + } + return phases +} + +func rulesPhaseMatches(phases []string, phase string) bool { + if phases == nil { + return true + } + phase = strings.ToLower(phase) + for _, p := range phases { + if p == phase { + return true + } + } + return false +} + +func stripRulesPhaseAnnotation(headerLine string) string { + idx := strings.Index(headerLine, phaseAnnotationPrefix) + if idx < 0 { + return headerLine + } + rest := headerLine[idx:] + endIdx := strings.Index(rest, phaseAnnotationSuffix) + if endIdx < 0 { + return headerLine + } + before := strings.TrimRight(headerLine[:idx], " ") + after := rest[endIdx+len(phaseAnnotationSuffix):] + return before + after +} + // BeadInfo provides identifying metadata about the bead for prompt scoping. type BeadInfo struct { Title string From 83622c8fa995271a276d0f6168d1fcfddb2c1dc5 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:31:56 -0500 Subject: [PATCH 57/80] red: test loadBaseInstructions caps output at phase-specific character limits --- internal/v2/prompt/assembler_test.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/internal/v2/prompt/assembler_test.go b/internal/v2/prompt/assembler_test.go index 9b0c4f4b1..a9df066e3 100644 --- a/internal/v2/prompt/assembler_test.go +++ b/internal/v2/prompt/assembler_test.go @@ -69,6 +69,31 @@ func TestPromptAssemblerSkipsWhitespaceOnlyLayers(t *testing.T) { } } +func TestLoadBaseInstructionsCapsAtPhaseLimitBuild(t *testing.T) { + // Build phase cap is 12,800 chars per spec. + // Generate base content larger than the cap with only build-phase sections. + longContent := strings.Repeat("x", 500) + base := "## Section \n\n" + longContent + "\n\n## Section2 \n\n" + strings.Repeat("y", 13000) + assembler := NewPromptAssembler(base, "", "", "") + result := assembler.loadBaseInstructions("build") + const buildCap = 12800 + if len(result) > buildCap { + t.Errorf("build phase result is %d chars, exceeds cap of %d", len(result), buildCap) + } +} + +func TestLoadBaseInstructionsCapsAtPhaseLimitRed(t *testing.T) { + // Red phase cap is 8,500 chars per spec. + longContent := strings.Repeat("z", 9000) + base := "## Section \n\n" + longContent + assembler := NewPromptAssembler(base, "", "", "") + result := assembler.loadBaseInstructions("red") + const redCap = 8500 + if len(result) > redCap { + t.Errorf("red phase result is %d chars, exceeds cap of %d", len(result), redCap) + } +} + func TestLoadProjectContextNoPackagePathsReturnsProjectUnchanged(t *testing.T) { project := "# Project\nSome instructions" assembler := NewPromptAssembler("base", project, "instance", "fragment") From 9d3b067bd65ad015c5403be96e281a4b98f7dd33 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:32:19 -0500 Subject: [PATCH 58/80] green: add phase-specific character caps to loadBaseInstructions --- internal/v2/prompt/assembler.go | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/internal/v2/prompt/assembler.go b/internal/v2/prompt/assembler.go index fe27f6f03..cd4c5743d 100644 --- a/internal/v2/prompt/assembler.go +++ b/internal/v2/prompt/assembler.go @@ -12,13 +12,29 @@ const ( phaseAnnotationSuffix = "-->" ) -// loadBaseInstructions returns the base layer filtered to phase-relevant sections. +// rulesPhaseMaxChars maps phase names to their character caps for the base rules layer. +var rulesPhaseMaxChars = map[string]int{ + "build": 12800, + "red": 8500, + "green": 8500, + "refactor": 8500, + "review": 8500, + "plan": 5000, + "validate": 5000, +} + +// loadBaseInstructions returns the base layer filtered to phase-relevant sections +// and capped at the phase-specific character limit. // Returns the full base content if phase is empty. func (p *PromptAssembler) loadBaseInstructions(phase string) string { if phase == "" { return p.base } - return filterRulesByPhase(p.base, phase) + filtered := filterRulesByPhase(p.base, phase) + if cap, ok := rulesPhaseMaxChars[strings.ToLower(phase)]; ok && len(filtered) > cap { + filtered = filtered[:cap] + } + return filtered } // filterRulesByPhase parses RULES.md content, filters ## sections by phase annotations, From 03f2cd809ccce2dbab091d4f21d4d850e31ce94c Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:32:29 -0500 Subject: [PATCH 59/80] [bead:gromit-v1j2u/build:default/iter:20] proceed --- .gromit/v2/events.jsonl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.gromit/v2/events.jsonl b/.gromit/v2/events.jsonl index 4086ddc08..1f4211fa4 100644 --- a/.gromit/v2/events.jsonl +++ b/.gromit/v2/events.jsonl @@ -136,3 +136,11 @@ {"schema_version":1,"timestamp":"2026-03-07T16:28:52.608368-05:00","type":"stage.completed","stage_name":"validate:default","bead_id":"gromit-uulh1","iteration":19,"success":true,"duration":40113709} {"schema_version":1,"timestamp":"2026-03-07T16:28:52.656542-05:00","type":"stage.started","stage_name":"review:default","bead_id":"gromit-uulh1","iteration":19} {"schema_version":1,"timestamp":"2026-03-07T16:28:52.656556-05:00","type":"stage.completed","stage_name":"review:default","bead_id":"gromit-uulh1","iteration":19,"success":true,"duration":2667} +{"schema_version":1,"timestamp":"2026-03-07T16:28:52.703754-05:00","type":"stage.started","stage_name":"epilogue:default","bead_id":"gromit-uulh1","iteration":19} +{"schema_version":1,"timestamp":"2026-03-07T16:28:52.86201-05:00","type":"stage.completed","stage_name":"epilogue:default","bead_id":"gromit-uulh1","iteration":19,"success":true,"duration":158246917} +{"schema_version":1,"timestamp":"2026-03-07T16:28:52.862025-05:00","type":"bead.completed","bead_id":"gromit-uulh1","bead_title":"Add unit tests for tier resolution","iteration":19,"success":true} +{"schema_version":1,"timestamp":"2026-03-07T16:28:52.862049-05:00","type":"bead.started","bead_id":"gromit-v1j2u","bead_title":"Add phase filtering to rules loading in PromptAssembler","iteration":20} +{"schema_version":1,"timestamp":"2026-03-07T16:28:52.862051-05:00","type":"stage.started","stage_name":"gate:default","bead_id":"gromit-v1j2u","iteration":20} +{"schema_version":1,"timestamp":"2026-03-07T16:28:52.939682-05:00","type":"stage.completed","stage_name":"gate:default","bead_id":"gromit-v1j2u","iteration":20,"success":true,"duration":77630125} +{"schema_version":1,"timestamp":"2026-03-07T16:28:52.939691-05:00","type":"stage.started","stage_name":"build:default","bead_id":"gromit-v1j2u","iteration":20} +{"schema_version":1,"timestamp":"2026-03-07T16:32:29.372451-05:00","type":"stage.completed","stage_name":"build:default","bead_id":"gromit-v1j2u","iteration":20,"success":true,"duration":216431930875} From c5bb250e6f6b1318be2324043e6ef43525cfc4cd Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:32:29 -0500 Subject: [PATCH 60/80] [bead:gromit-v1j2u/validate:default/iter:20] proceed --- .gromit/v2/events.jsonl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gromit/v2/events.jsonl b/.gromit/v2/events.jsonl index 1f4211fa4..fc1dda2b2 100644 --- a/.gromit/v2/events.jsonl +++ b/.gromit/v2/events.jsonl @@ -144,3 +144,5 @@ {"schema_version":1,"timestamp":"2026-03-07T16:28:52.939682-05:00","type":"stage.completed","stage_name":"gate:default","bead_id":"gromit-v1j2u","iteration":20,"success":true,"duration":77630125} {"schema_version":1,"timestamp":"2026-03-07T16:28:52.939691-05:00","type":"stage.started","stage_name":"build:default","bead_id":"gromit-v1j2u","iteration":20} {"schema_version":1,"timestamp":"2026-03-07T16:32:29.372451-05:00","type":"stage.completed","stage_name":"build:default","bead_id":"gromit-v1j2u","iteration":20,"success":true,"duration":216431930875} +{"schema_version":1,"timestamp":"2026-03-07T16:32:29.441443-05:00","type":"stage.started","stage_name":"validate:default","bead_id":"gromit-v1j2u","iteration":20} +{"schema_version":1,"timestamp":"2026-03-07T16:32:29.481473-05:00","type":"stage.completed","stage_name":"validate:default","bead_id":"gromit-v1j2u","iteration":20,"success":true,"duration":40018375} From b55070448afeb71d5f480ef1507efec87cda605c Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:32:29 -0500 Subject: [PATCH 61/80] [bead:gromit-v1j2u/review:default/iter:20] proceed --- .gromit/v2/events.jsonl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gromit/v2/events.jsonl b/.gromit/v2/events.jsonl index fc1dda2b2..c245391f0 100644 --- a/.gromit/v2/events.jsonl +++ b/.gromit/v2/events.jsonl @@ -146,3 +146,5 @@ {"schema_version":1,"timestamp":"2026-03-07T16:32:29.372451-05:00","type":"stage.completed","stage_name":"build:default","bead_id":"gromit-v1j2u","iteration":20,"success":true,"duration":216431930875} {"schema_version":1,"timestamp":"2026-03-07T16:32:29.441443-05:00","type":"stage.started","stage_name":"validate:default","bead_id":"gromit-v1j2u","iteration":20} {"schema_version":1,"timestamp":"2026-03-07T16:32:29.481473-05:00","type":"stage.completed","stage_name":"validate:default","bead_id":"gromit-v1j2u","iteration":20,"success":true,"duration":40018375} +{"schema_version":1,"timestamp":"2026-03-07T16:32:29.530042-05:00","type":"stage.started","stage_name":"review:default","bead_id":"gromit-v1j2u","iteration":20} +{"schema_version":1,"timestamp":"2026-03-07T16:32:29.530052-05:00","type":"stage.completed","stage_name":"review:default","bead_id":"gromit-v1j2u","iteration":20,"success":true,"duration":2542} From e481c430c148a1d14d4e4791678871a79d321233 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:36:20 -0500 Subject: [PATCH 62/80] green: add three-level chain ordering test for TopologicalSort (covered by existing implementation) Co-Authored-By: Claude Sonnet 4.6 --- internal/v2/loop/topo_test.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/internal/v2/loop/topo_test.go b/internal/v2/loop/topo_test.go index efdc43a07..265e0f280 100644 --- a/internal/v2/loop/topo_test.go +++ b/internal/v2/loop/topo_test.go @@ -85,3 +85,28 @@ func TestTopologicalSort_DependencyBeforeDependent(t *testing.T) { t.Fatalf("order = [%s %s], want [root child]", got[0].ID, got[1].ID) } } + +func TestTopologicalSort_ChainOrderingThreeLevels(t *testing.T) { + t.Parallel() + + // a depends on b, b depends on c → order must be c, b, a. + beads := []*bead.Bead{ + {ID: "a", DependsOn: []bead.Dependency{{ID: "b"}}}, + {ID: "b", DependsOn: []bead.Dependency{{ID: "c"}}}, + {ID: "c"}, + } + + got, err := TopologicalSort(beads) + if err != nil { + t.Fatalf("TopologicalSort: %v", err) + } + want := []string{"c", "b", "a"} + if len(got) != len(want) { + t.Fatalf("got %d beads, want %d", len(got), len(want)) + } + for i, w := range want { + if got[i].ID != w { + t.Fatalf("position %d = %q, want %q", i, got[i].ID, w) + } + } +} From 86dbf4919b586e7fdbd9b2f92c64ae875ba8d634 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:36:38 -0500 Subject: [PATCH 63/80] green: add external-dep-treated-as-satisfied test for TopologicalSort (covered by existing implementation) Co-Authored-By: Claude Sonnet 4.6 --- internal/v2/loop/topo_test.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/internal/v2/loop/topo_test.go b/internal/v2/loop/topo_test.go index 265e0f280..3736e9ce0 100644 --- a/internal/v2/loop/topo_test.go +++ b/internal/v2/loop/topo_test.go @@ -86,6 +86,25 @@ func TestTopologicalSort_DependencyBeforeDependent(t *testing.T) { } } +func TestTopologicalSort_ExternalDepTreatedAsSatisfied(t *testing.T) { + t.Parallel() + + // "b" depends on "external" which is not in the input set. + // The sort must not error and must include both beads. + beads := []*bead.Bead{ + {ID: "b", DependsOn: []bead.Dependency{{ID: "external"}}}, + {ID: "a"}, + } + + got, err := TopologicalSort(beads) + if err != nil { + t.Fatalf("TopologicalSort: unexpected error: %v", err) + } + if len(got) != 2 { + t.Fatalf("got %d beads, want 2", len(got)) + } +} + func TestTopologicalSort_ChainOrderingThreeLevels(t *testing.T) { t.Parallel() From 3c4a6cb6bea49376d99b3796b75c9517ab5b5aa3 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:36:52 -0500 Subject: [PATCH 64/80] green: add Dependencies-field test for TopologicalSort (covered by existing implementation) Co-Authored-By: Claude Sonnet 4.6 --- internal/v2/loop/topo_test.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/internal/v2/loop/topo_test.go b/internal/v2/loop/topo_test.go index 3736e9ce0..34044cf53 100644 --- a/internal/v2/loop/topo_test.go +++ b/internal/v2/loop/topo_test.go @@ -86,6 +86,27 @@ func TestTopologicalSort_DependencyBeforeDependent(t *testing.T) { } } +func TestTopologicalSort_DependenciesFieldHonored(t *testing.T) { + t.Parallel() + + // Uses the Dependencies field (not DependsOn or BlockedBy) — all three must be collected. + beads := []*bead.Bead{ + {ID: "child", Dependencies: []bead.Dependency{{ID: "root"}}}, + {ID: "root"}, + } + + got, err := TopologicalSort(beads) + if err != nil { + t.Fatalf("TopologicalSort: %v", err) + } + if len(got) != 2 { + t.Fatalf("got %d beads, want 2", len(got)) + } + if got[0].ID != "root" || got[1].ID != "child" { + t.Fatalf("order = [%s %s], want [root child]", got[0].ID, got[1].ID) + } +} + func TestTopologicalSort_ExternalDepTreatedAsSatisfied(t *testing.T) { t.Parallel() From 0214963411bbc14d968671f3a76de2a6199c507b Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:36:59 -0500 Subject: [PATCH 65/80] [bead:gromit-v46qz/build:default/iter:21] proceed --- .gromit/v2/events.jsonl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.gromit/v2/events.jsonl b/.gromit/v2/events.jsonl index c245391f0..976133e7b 100644 --- a/.gromit/v2/events.jsonl +++ b/.gromit/v2/events.jsonl @@ -148,3 +148,11 @@ {"schema_version":1,"timestamp":"2026-03-07T16:32:29.481473-05:00","type":"stage.completed","stage_name":"validate:default","bead_id":"gromit-v1j2u","iteration":20,"success":true,"duration":40018375} {"schema_version":1,"timestamp":"2026-03-07T16:32:29.530042-05:00","type":"stage.started","stage_name":"review:default","bead_id":"gromit-v1j2u","iteration":20} {"schema_version":1,"timestamp":"2026-03-07T16:32:29.530052-05:00","type":"stage.completed","stage_name":"review:default","bead_id":"gromit-v1j2u","iteration":20,"success":true,"duration":2542} +{"schema_version":1,"timestamp":"2026-03-07T16:32:29.577196-05:00","type":"stage.started","stage_name":"epilogue:default","bead_id":"gromit-v1j2u","iteration":20} +{"schema_version":1,"timestamp":"2026-03-07T16:32:29.67267-05:00","type":"stage.completed","stage_name":"epilogue:default","bead_id":"gromit-v1j2u","iteration":20,"success":true,"duration":95465417} +{"schema_version":1,"timestamp":"2026-03-07T16:32:29.672676-05:00","type":"bead.completed","bead_id":"gromit-v1j2u","bead_title":"Add phase filtering to rules loading in PromptAssembler","iteration":20,"success":true} +{"schema_version":1,"timestamp":"2026-03-07T16:32:29.67269-05:00","type":"bead.started","bead_id":"gromit-v46qz","bead_title":"Add unit tests for topological sort","iteration":21} +{"schema_version":1,"timestamp":"2026-03-07T16:32:29.672691-05:00","type":"stage.started","stage_name":"gate:default","bead_id":"gromit-v46qz","iteration":21} +{"schema_version":1,"timestamp":"2026-03-07T16:32:29.817001-05:00","type":"stage.completed","stage_name":"gate:default","bead_id":"gromit-v46qz","iteration":21,"success":true,"duration":144308417} +{"schema_version":1,"timestamp":"2026-03-07T16:32:29.817012-05:00","type":"stage.started","stage_name":"build:default","bead_id":"gromit-v46qz","iteration":21} +{"schema_version":1,"timestamp":"2026-03-07T16:36:59.519006-05:00","type":"stage.completed","stage_name":"build:default","bead_id":"gromit-v46qz","iteration":21,"success":true,"duration":269700960750} From d5bf967ab8b02239b52b04e9caab94e43610a5df Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:36:59 -0500 Subject: [PATCH 66/80] [bead:gromit-v46qz/validate:default/iter:21] proceed --- .gromit/v2/events.jsonl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gromit/v2/events.jsonl b/.gromit/v2/events.jsonl index 976133e7b..aaaecd068 100644 --- a/.gromit/v2/events.jsonl +++ b/.gromit/v2/events.jsonl @@ -156,3 +156,5 @@ {"schema_version":1,"timestamp":"2026-03-07T16:32:29.817001-05:00","type":"stage.completed","stage_name":"gate:default","bead_id":"gromit-v46qz","iteration":21,"success":true,"duration":144308417} {"schema_version":1,"timestamp":"2026-03-07T16:32:29.817012-05:00","type":"stage.started","stage_name":"build:default","bead_id":"gromit-v46qz","iteration":21} {"schema_version":1,"timestamp":"2026-03-07T16:36:59.519006-05:00","type":"stage.completed","stage_name":"build:default","bead_id":"gromit-v46qz","iteration":21,"success":true,"duration":269700960750} +{"schema_version":1,"timestamp":"2026-03-07T16:36:59.587259-05:00","type":"stage.started","stage_name":"validate:default","bead_id":"gromit-v46qz","iteration":21} +{"schema_version":1,"timestamp":"2026-03-07T16:36:59.627255-05:00","type":"stage.completed","stage_name":"validate:default","bead_id":"gromit-v46qz","iteration":21,"success":true,"duration":39986667} From f3a18abdbfca4b3fa0a15d50d7c7f09152d6404e Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:36:59 -0500 Subject: [PATCH 67/80] [bead:gromit-v46qz/review:default/iter:21] proceed --- .gromit/v2/events.jsonl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gromit/v2/events.jsonl b/.gromit/v2/events.jsonl index aaaecd068..1021281ba 100644 --- a/.gromit/v2/events.jsonl +++ b/.gromit/v2/events.jsonl @@ -158,3 +158,5 @@ {"schema_version":1,"timestamp":"2026-03-07T16:36:59.519006-05:00","type":"stage.completed","stage_name":"build:default","bead_id":"gromit-v46qz","iteration":21,"success":true,"duration":269700960750} {"schema_version":1,"timestamp":"2026-03-07T16:36:59.587259-05:00","type":"stage.started","stage_name":"validate:default","bead_id":"gromit-v46qz","iteration":21} {"schema_version":1,"timestamp":"2026-03-07T16:36:59.627255-05:00","type":"stage.completed","stage_name":"validate:default","bead_id":"gromit-v46qz","iteration":21,"success":true,"duration":39986667} +{"schema_version":1,"timestamp":"2026-03-07T16:36:59.675529-05:00","type":"stage.started","stage_name":"review:default","bead_id":"gromit-v46qz","iteration":21} +{"schema_version":1,"timestamp":"2026-03-07T16:36:59.675547-05:00","type":"stage.completed","stage_name":"review:default","bead_id":"gromit-v46qz","iteration":21,"success":true,"duration":7250} From 9ceba1a09fecbceab994615e2aeaee617229e4cd Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:39:17 -0500 Subject: [PATCH 68/80] red: test FlagChangedBeads flags beads with overlapping changed files Co-Authored-By: Claude Sonnet 4.6 --- internal/v2/loop/gap_analysis_test.go | 39 +++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 internal/v2/loop/gap_analysis_test.go diff --git a/internal/v2/loop/gap_analysis_test.go b/internal/v2/loop/gap_analysis_test.go new file mode 100644 index 000000000..f8c9a9718 --- /dev/null +++ b/internal/v2/loop/gap_analysis_test.go @@ -0,0 +1,39 @@ +package loop + +import ( + "testing" + + "github.com/danabrams/gromit/internal/bead" +) + +func TestFlagChangedBeads_FlagsBeadsWithOverlappingFiles(t *testing.T) { + t.Parallel() + + beads := []*bead.Bead{ + {ID: "bead-a"}, + {ID: "bead-b"}, + {ID: "bead-c"}, + } + changedFiles := []string{"internal/foo/foo.go", "internal/bar/bar.go"} + beadFiles := map[string][]string{ + "bead-a": {"internal/foo/foo.go", "internal/foo/foo_test.go"}, + "bead-b": {"internal/qux/qux.go"}, + "bead-c": {"internal/bar/bar.go"}, + } + + flagged := FlagChangedBeads(beads, changedFiles, beadFiles) + + if len(flagged) != 2 { + t.Fatalf("got %d flagged beads, want 2", len(flagged)) + } + ids := make([]string, len(flagged)) + for i, b := range flagged { + ids[i] = b.ID + } + wantIDs := map[string]bool{"bead-a": true, "bead-c": true} + for _, id := range ids { + if !wantIDs[id] { + t.Errorf("unexpected flagged bead %q", id) + } + } +} From 2d748a5412438196b7377154ec08d0153733daa7 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:39:31 -0500 Subject: [PATCH 69/80] green: implement FlagChangedBeads for resume gap analysis Co-Authored-By: Claude Sonnet 4.6 --- internal/v2/loop/gap_analysis.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 internal/v2/loop/gap_analysis.go diff --git a/internal/v2/loop/gap_analysis.go b/internal/v2/loop/gap_analysis.go new file mode 100644 index 000000000..c04d31b59 --- /dev/null +++ b/internal/v2/loop/gap_analysis.go @@ -0,0 +1,28 @@ +package loop + +import "github.com/danabrams/gromit/internal/bead" + +// FlagChangedBeads returns the subset of beads whose committed files overlap +// with the changed files. This is the core of resume gap analysis: given a +// file-level diff and a bead-to-file mapping from commit history, it identifies +// which completed beads need selective revalidation. +func FlagChangedBeads(beads []*bead.Bead, changedFiles []string, beadFiles map[string][]string) []*bead.Bead { + changed := make(map[string]bool, len(changedFiles)) + for _, f := range changedFiles { + changed[f] = true + } + + var flagged []*bead.Bead + for _, b := range beads { + if b == nil { + continue + } + for _, f := range beadFiles[b.ID] { + if changed[f] { + flagged = append(flagged, b) + break + } + } + } + return flagged +} From 0c4f01e3635441a5cf5b72fa57cd8fd78c2673b7 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:39:53 -0500 Subject: [PATCH 70/80] green: add no-overlap test for FlagChangedBeads (covered by existing implementation) Co-Authored-By: Claude Sonnet 4.6 --- internal/v2/loop/gap_analysis_test.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/internal/v2/loop/gap_analysis_test.go b/internal/v2/loop/gap_analysis_test.go index f8c9a9718..3f16435f1 100644 --- a/internal/v2/loop/gap_analysis_test.go +++ b/internal/v2/loop/gap_analysis_test.go @@ -37,3 +37,23 @@ func TestFlagChangedBeads_FlagsBeadsWithOverlappingFiles(t *testing.T) { } } } + +func TestFlagChangedBeads_NoOverlapReturnsEmpty(t *testing.T) { + t.Parallel() + + beads := []*bead.Bead{ + {ID: "bead-x"}, + {ID: "bead-y"}, + } + changedFiles := []string{"internal/other/other.go"} + beadFiles := map[string][]string{ + "bead-x": {"internal/alpha/alpha.go"}, + "bead-y": {"internal/beta/beta.go"}, + } + + flagged := FlagChangedBeads(beads, changedFiles, beadFiles) + + if len(flagged) != 0 { + t.Fatalf("got %d flagged beads, want 0", len(flagged)) + } +} From 94ee24fea9a55b8494e9e3ef46abd6a9b0bc6f2c Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:40:03 -0500 Subject: [PATCH 71/80] [bead:gromit-w83pd/build:default/iter:23] proceed --- .gromit/v2/events.jsonl | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.gromit/v2/events.jsonl b/.gromit/v2/events.jsonl index 1021281ba..296c3e682 100644 --- a/.gromit/v2/events.jsonl +++ b/.gromit/v2/events.jsonl @@ -160,3 +160,15 @@ {"schema_version":1,"timestamp":"2026-03-07T16:36:59.627255-05:00","type":"stage.completed","stage_name":"validate:default","bead_id":"gromit-v46qz","iteration":21,"success":true,"duration":39986667} {"schema_version":1,"timestamp":"2026-03-07T16:36:59.675529-05:00","type":"stage.started","stage_name":"review:default","bead_id":"gromit-v46qz","iteration":21} {"schema_version":1,"timestamp":"2026-03-07T16:36:59.675547-05:00","type":"stage.completed","stage_name":"review:default","bead_id":"gromit-v46qz","iteration":21,"success":true,"duration":7250} +{"schema_version":1,"timestamp":"2026-03-07T16:36:59.723145-05:00","type":"stage.started","stage_name":"epilogue:default","bead_id":"gromit-v46qz","iteration":21} +{"schema_version":1,"timestamp":"2026-03-07T16:36:59.879055-05:00","type":"stage.completed","stage_name":"epilogue:default","bead_id":"gromit-v46qz","iteration":21,"success":true,"duration":155903125} +{"schema_version":1,"timestamp":"2026-03-07T16:36:59.879072-05:00","type":"bead.completed","bead_id":"gromit-v46qz","bead_title":"Add unit tests for topological sort","iteration":21,"success":true} +{"schema_version":1,"timestamp":"2026-03-07T16:36:59.879095-05:00","type":"bead.started","bead_id":"gromit-w1200","bead_title":"Wire Router into bead loop routing seam","iteration":22} +{"schema_version":1,"timestamp":"2026-03-07T16:36:59.879096-05:00","type":"stage.started","stage_name":"gate:default","bead_id":"gromit-w1200","iteration":22} +{"schema_version":1,"timestamp":"2026-03-07T16:37:00.009157-05:00","type":"stage.completed","stage_name":"gate:default","bead_id":"gromit-w1200","iteration":22,"success":true,"duration":130058583} +{"schema_version":1,"timestamp":"2026-03-07T16:37:00.009166-05:00","type":"bead.completed","bead_id":"gromit-w1200","bead_title":"Wire Router into bead loop routing seam","iteration":22,"success":false} +{"schema_version":1,"timestamp":"2026-03-07T16:37:00.009183-05:00","type":"bead.started","bead_id":"gromit-w83pd","bead_title":"Implement gap analysis for resume","iteration":23} +{"schema_version":1,"timestamp":"2026-03-07T16:37:00.009184-05:00","type":"stage.started","stage_name":"gate:default","bead_id":"gromit-w83pd","iteration":23} +{"schema_version":1,"timestamp":"2026-03-07T16:37:00.066977-05:00","type":"stage.completed","stage_name":"gate:default","bead_id":"gromit-w83pd","iteration":23,"success":true,"duration":57793000} +{"schema_version":1,"timestamp":"2026-03-07T16:37:00.066987-05:00","type":"stage.started","stage_name":"build:default","bead_id":"gromit-w83pd","iteration":23} +{"schema_version":1,"timestamp":"2026-03-07T16:40:03.694684-05:00","type":"stage.completed","stage_name":"build:default","bead_id":"gromit-w83pd","iteration":23,"success":true,"duration":183626276584} From c7cc83b6caf800bf624b2963319c0f279ba2166a Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:40:03 -0500 Subject: [PATCH 72/80] [bead:gromit-w83pd/validate:default/iter:23] proceed --- .gromit/v2/events.jsonl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gromit/v2/events.jsonl b/.gromit/v2/events.jsonl index 296c3e682..540d2c3ec 100644 --- a/.gromit/v2/events.jsonl +++ b/.gromit/v2/events.jsonl @@ -172,3 +172,5 @@ {"schema_version":1,"timestamp":"2026-03-07T16:37:00.066977-05:00","type":"stage.completed","stage_name":"gate:default","bead_id":"gromit-w83pd","iteration":23,"success":true,"duration":57793000} {"schema_version":1,"timestamp":"2026-03-07T16:37:00.066987-05:00","type":"stage.started","stage_name":"build:default","bead_id":"gromit-w83pd","iteration":23} {"schema_version":1,"timestamp":"2026-03-07T16:40:03.694684-05:00","type":"stage.completed","stage_name":"build:default","bead_id":"gromit-w83pd","iteration":23,"success":true,"duration":183626276584} +{"schema_version":1,"timestamp":"2026-03-07T16:40:03.771632-05:00","type":"stage.started","stage_name":"validate:default","bead_id":"gromit-w83pd","iteration":23} +{"schema_version":1,"timestamp":"2026-03-07T16:40:03.812267-05:00","type":"stage.completed","stage_name":"validate:default","bead_id":"gromit-w83pd","iteration":23,"success":true,"duration":40627875} From 1022d4e2413cdd50d4c4e6aeacc9eef9f0034315 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:40:03 -0500 Subject: [PATCH 73/80] [bead:gromit-w83pd/review:default/iter:23] proceed --- .gromit/v2/events.jsonl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gromit/v2/events.jsonl b/.gromit/v2/events.jsonl index 540d2c3ec..aac34b376 100644 --- a/.gromit/v2/events.jsonl +++ b/.gromit/v2/events.jsonl @@ -174,3 +174,5 @@ {"schema_version":1,"timestamp":"2026-03-07T16:40:03.694684-05:00","type":"stage.completed","stage_name":"build:default","bead_id":"gromit-w83pd","iteration":23,"success":true,"duration":183626276584} {"schema_version":1,"timestamp":"2026-03-07T16:40:03.771632-05:00","type":"stage.started","stage_name":"validate:default","bead_id":"gromit-w83pd","iteration":23} {"schema_version":1,"timestamp":"2026-03-07T16:40:03.812267-05:00","type":"stage.completed","stage_name":"validate:default","bead_id":"gromit-w83pd","iteration":23,"success":true,"duration":40627875} +{"schema_version":1,"timestamp":"2026-03-07T16:40:03.861359-05:00","type":"stage.started","stage_name":"review:default","bead_id":"gromit-w83pd","iteration":23} +{"schema_version":1,"timestamp":"2026-03-07T16:40:03.861382-05:00","type":"stage.completed","stage_name":"review:default","bead_id":"gromit-w83pd","iteration":23,"success":true,"duration":10833} From 1d46918a008190f6a9ab0675067c4e414b5901c4 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:43:51 -0500 Subject: [PATCH 74/80] red: test selective revalidation re-queues failed beads on resume Co-Authored-By: Claude Sonnet 4.6 --- internal/v2/loop/spec_loop_test.go | 67 ++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/internal/v2/loop/spec_loop_test.go b/internal/v2/loop/spec_loop_test.go index 61795d888..857824b74 100644 --- a/internal/v2/loop/spec_loop_test.go +++ b/internal/v2/loop/spec_loop_test.go @@ -1466,3 +1466,70 @@ func newPresentStageForTest(t *testing.T, cfg *config.Config, presenter adapter. } return stage, summaryCtx } + +func TestSelectiveRevalidation_RequeuesFailedBeads(t *testing.T) { + t.Parallel() + + ctx := context.Background() + specID := "spec-revalidation" + cfg := &config.Config{} + + git := newFakeGitAdapter(t) + taskTracker := newFakeTaskTrackerAdapter() + taskTracker.queryBeadsResponse = &tasktracker.TaskTrackerQueryBeadsResponse{ + Beads: []tasktracker.Bead{ + {ID: "bead-open-1", Title: "Open bead"}, + }, + } + + revalidatedBead := &bead.Bead{ID: "bead-completed-failed", Title: "Regressed bead"} + revalidator := &fakeSelectiveRevalidator{requeueBeads: []*bead.Bead{revalidatedBead}} + + planStage := newFakePlanStage(specID) + decompose := newFakeDecomposeStage(specID) + beadRunner := newFakeBeadRunner() + accept := newFakeAcceptStage() + presentStage := newFakePresentStage() + summaryCtx := &present.SummaryContext{} + + adapters := adapter.AdapterSet{ + Git: git, + LLM: newFakeLLMAdapter(), + TaskTracker: taskTracker, + Presenter: newFakePresenterAdapter(t), + } + + loopInstance, err := NewSpecLoop(adapters, cfg, noopDependencyGate{}, + WithPlanStage(planStage), + WithPresentStage(presentStage, summaryCtx), + WithDecomposeStage(decompose), + WithBeadLoop(beadRunner), + WithAcceptStage(accept), + WithSelectiveRevalidator(revalidator), + ) + if err != nil { + t.Fatalf("create spec loop: %v", err) + } + + if err := loopInstance.Run(ctx, specID, nil); err != nil { + t.Fatalf("run spec loop: %v", err) + } + + wantIDs := []string{"bead-open-1", "bead-completed-failed"} + requireBeadIDs(t, beadRunner, wantIDs) +} + +type fakeSelectiveRevalidator struct { + requeueBeads []*bead.Bead + err error + calls int + lastBeads []*bead.Bead + lastWorktree string +} + +func (f *fakeSelectiveRevalidator) Revalidate(ctx context.Context, beads []*bead.Bead, worktree string) ([]*bead.Bead, error) { + f.calls++ + f.lastBeads = append([]*bead.Bead(nil), beads...) + f.lastWorktree = worktree + return f.requeueBeads, f.err +} From 27a4c93799c0a3d465de249185e7b7a03aff8981 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:44:28 -0500 Subject: [PATCH 75/80] green: implement SelectiveRevalidator interface and resume path wiring Co-Authored-By: Claude Sonnet 4.6 --- internal/v2/loop/spec_loop.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/internal/v2/loop/spec_loop.go b/internal/v2/loop/spec_loop.go index 33d7e78dc..2de5f78f6 100644 --- a/internal/v2/loop/spec_loop.go +++ b/internal/v2/loop/spec_loop.go @@ -59,6 +59,12 @@ type remediationRunner interface { Run(ctx context.Context, specID, worktree string) error } +// SelectiveRevalidator checks beads for regressions and returns the subset +// that failed validation and need to be re-queued in the bead loop. +type SelectiveRevalidator interface { + Revalidate(ctx context.Context, beads []*bead.Bead, worktree string) ([]*bead.Bead, error) +} + // SpecLoopOption configures optional behavior when constructing a SpecLoop. type SpecLoopOption func(*SpecLoop) @@ -125,6 +131,14 @@ func WithPlanStage(stage stagepkg.Stage) SpecLoopOption { } } +// WithSelectiveRevalidator installs a revalidator that checks completed beads +// for regressions before the bead loop starts on resume. +func WithSelectiveRevalidator(r SelectiveRevalidator) SpecLoopOption { + return func(s *SpecLoop) { + s.selectiveRevalidator = r + } +} + // WithPreserveOnFailure controls whether the worktree is kept when the spec // fails. The default is true (preserve). Pass false to remove it on failure. func WithPreserveOnFailure(preserve bool) SpecLoopOption { @@ -167,6 +181,7 @@ type SpecLoop struct { presentStage stagepkg.Stage presentSummaryContext *present.SummaryContext preserveOnFailure bool // restore t.Cleanup if overriding in tests + selectiveRevalidator SelectiveRevalidator } type worktreeSetter interface { @@ -295,6 +310,13 @@ func (s *SpecLoop) Run(ctx context.Context, specID string, stopCh <-chan struct{ if len(existingBeads) > 0 { beads = existingBeads s.emit(&events.DecomposeResumedEvent{SpecID: specID, BeadCount: len(beads)}) + if s.selectiveRevalidator != nil { + requeueBeads, err := s.selectiveRevalidator.Revalidate(ctx, beads, worktree) + if err != nil { + return fmt.Errorf("selective revalidation: %w", err) + } + beads = append(beads, requeueBeads...) + } } else { beads, err = s.runDecompose(ctx, req) if err != nil { From 13ab46542f802fcf8a2cc722e43d986e0a9541aa Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:44:47 -0500 Subject: [PATCH 76/80] green: add no-op test for revalidator skipped on fresh run (covered by existing implementation) Co-Authored-By: Claude Sonnet 4.6 --- internal/v2/loop/spec_loop_test.go | 48 ++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/internal/v2/loop/spec_loop_test.go b/internal/v2/loop/spec_loop_test.go index 857824b74..137dfc986 100644 --- a/internal/v2/loop/spec_loop_test.go +++ b/internal/v2/loop/spec_loop_test.go @@ -1519,6 +1519,54 @@ func TestSelectiveRevalidation_RequeuesFailedBeads(t *testing.T) { requireBeadIDs(t, beadRunner, wantIDs) } +func TestSelectiveRevalidation_SkippedOnFreshRun(t *testing.T) { + t.Parallel() + + ctx := context.Background() + specID := "spec-fresh-revalidation" + cfg := &config.Config{} + + git := newFakeGitAdapter(t) + taskTracker := newFakeTaskTrackerAdapter() + // No queryBeadsResponse — fresh run, no existing beads + + revalidator := &fakeSelectiveRevalidator{} + + planStage := newFakePlanStage(specID) + decompose := newFakeDecomposeStage(specID) + beadRunner := newFakeBeadRunner() + accept := newFakeAcceptStage() + presentStage := newFakePresentStage() + summaryCtx := &present.SummaryContext{} + + adapters := adapter.AdapterSet{ + Git: git, + LLM: newFakeLLMAdapter(), + TaskTracker: taskTracker, + Presenter: newFakePresenterAdapter(t), + } + + loopInstance, err := NewSpecLoop(adapters, cfg, noopDependencyGate{}, + WithPlanStage(planStage), + WithPresentStage(presentStage, summaryCtx), + WithDecomposeStage(decompose), + WithBeadLoop(beadRunner), + WithAcceptStage(accept), + WithSelectiveRevalidator(revalidator), + ) + if err != nil { + t.Fatalf("create spec loop: %v", err) + } + + if err := loopInstance.Run(ctx, specID, nil); err != nil { + t.Fatalf("run spec loop: %v", err) + } + + if revalidator.calls != 0 { + t.Fatalf("revalidator called %d times on fresh run, want 0", revalidator.calls) + } +} + type fakeSelectiveRevalidator struct { requeueBeads []*bead.Bead err error From 04dc7fafb2c8d10e8bd32f2fa985b7b200ad47fd Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:45:04 -0500 Subject: [PATCH 77/80] green: add no-op test for revalidator error propagation (covered by existing implementation) Co-Authored-By: Claude Sonnet 4.6 --- internal/v2/loop/spec_loop_test.go | 52 ++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/internal/v2/loop/spec_loop_test.go b/internal/v2/loop/spec_loop_test.go index 137dfc986..72b1e900e 100644 --- a/internal/v2/loop/spec_loop_test.go +++ b/internal/v2/loop/spec_loop_test.go @@ -1519,6 +1519,58 @@ func TestSelectiveRevalidation_RequeuesFailedBeads(t *testing.T) { requireBeadIDs(t, beadRunner, wantIDs) } +func TestSelectiveRevalidation_ErrorPropagates(t *testing.T) { + t.Parallel() + + ctx := context.Background() + specID := "spec-revalidation-error" + cfg := &config.Config{} + + git := newFakeGitAdapter(t) + taskTracker := newFakeTaskTrackerAdapter() + taskTracker.queryBeadsResponse = &tasktracker.TaskTrackerQueryBeadsResponse{ + Beads: []tasktracker.Bead{ + {ID: "bead-1", Title: "A bead"}, + }, + } + + revalidator := &fakeSelectiveRevalidator{err: fmt.Errorf("validation infrastructure failed")} + + planStage := newFakePlanStage(specID) + decompose := newFakeDecomposeStage(specID) + beadRunner := newFakeBeadRunner() + accept := newFakeAcceptStage() + presentStage := newFakePresentStage() + summaryCtx := &present.SummaryContext{} + + adapters := adapter.AdapterSet{ + Git: git, + LLM: newFakeLLMAdapter(), + TaskTracker: taskTracker, + Presenter: newFakePresenterAdapter(t), + } + + loopInstance, err := NewSpecLoop(adapters, cfg, noopDependencyGate{}, + WithPlanStage(planStage), + WithPresentStage(presentStage, summaryCtx), + WithDecomposeStage(decompose), + WithBeadLoop(beadRunner), + WithAcceptStage(accept), + WithSelectiveRevalidator(revalidator), + ) + if err != nil { + t.Fatalf("create spec loop: %v", err) + } + + err = loopInstance.Run(ctx, specID, nil) + if err == nil { + t.Fatal("expected error from revalidator, got nil") + } + if !strings.Contains(err.Error(), "selective revalidation") { + t.Fatalf("error = %q, want to contain 'selective revalidation'", err.Error()) + } +} + func TestSelectiveRevalidation_SkippedOnFreshRun(t *testing.T) { t.Parallel() From 52a3f6e2378b30396cabd1fe729eef793df92d28 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 16:45:13 -0500 Subject: [PATCH 78/80] squash 24 beads - bead gromit-jv6ns: Create Router interface and stub in v2/routing - bead gromit-ew1l8: Implement Router ratio balancing and phase preferences logic - bead gromit-jzbe3: Implement tier resolution in internal/v2/routing/tier.go - bead gromit-w1200: Wire Router into bead loop routing seam - bead gromit-atr10: Wire Router into spec loop for plan/decompose/accept phases - bead gromit-d2rpx: Instantiate and wire Router in run2 command - bead gromit-w83pd: Implement gap analysis for resume - bead gromit-zrrn7: Implement selective revalidation in spec loop - bead gromit-20962: Expand remediation runner signature for worktree propagation - bead gromit-7a6rt: Implement topological sort for beads - bead gromit-fhp2q: Implement gate re-queue logic in bead loop - bead gromit-v1j2u: Add phase filtering to rules loading in PromptAssembler - bead gromit-f4ouz: Add scoped CLAUDE.md loading to PromptAssembler - bead gromit-9z0j1: Implement budget shaping with scope adjustment in PromptAssembler - bead gromit-8yarv: Wire budget shaping into Assemble workflow - bead gromit-1b6ks: Add unit tests for Router ratio balancing and fallback - bead gromit-uulh1: Add unit tests for tier resolution - bead gromit-d4134: Add integration tests for bead loop routing and escalation - bead gromit-0xxdf: Add unit tests for gap analysis - bead gromit-cufx0: Add integration tests for resume with gap analysis and revalidation - bead gromit-v46qz: Add unit tests for topological sort - bead gromit-1fuf8: Add integration tests for gate re-queue on block - bead gromit-61cbs: Add unit tests for phase filtering and scoping in prompt assembler - bead gromit-u4837: Add integration tests for full prompt shaping pipeline --- .gromit/v2/events.jsonl | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.gromit/v2/events.jsonl b/.gromit/v2/events.jsonl index aac34b376..31fda1912 100644 --- a/.gromit/v2/events.jsonl +++ b/.gromit/v2/events.jsonl @@ -176,3 +176,18 @@ {"schema_version":1,"timestamp":"2026-03-07T16:40:03.812267-05:00","type":"stage.completed","stage_name":"validate:default","bead_id":"gromit-w83pd","iteration":23,"success":true,"duration":40627875} {"schema_version":1,"timestamp":"2026-03-07T16:40:03.861359-05:00","type":"stage.started","stage_name":"review:default","bead_id":"gromit-w83pd","iteration":23} {"schema_version":1,"timestamp":"2026-03-07T16:40:03.861382-05:00","type":"stage.completed","stage_name":"review:default","bead_id":"gromit-w83pd","iteration":23,"success":true,"duration":10833} +{"schema_version":1,"timestamp":"2026-03-07T16:40:03.909464-05:00","type":"stage.started","stage_name":"epilogue:default","bead_id":"gromit-w83pd","iteration":23} +{"schema_version":1,"timestamp":"2026-03-07T16:40:04.005688-05:00","type":"stage.completed","stage_name":"epilogue:default","bead_id":"gromit-w83pd","iteration":23,"success":true,"duration":96209917} +{"schema_version":1,"timestamp":"2026-03-07T16:40:04.0057-05:00","type":"bead.completed","bead_id":"gromit-w83pd","bead_title":"Implement gap analysis for resume","iteration":23,"success":true} +{"schema_version":1,"timestamp":"2026-03-07T16:40:04.005713-05:00","type":"bead.started","bead_id":"gromit-zrrn7","bead_title":"Implement selective revalidation in spec loop","iteration":24} +{"schema_version":1,"timestamp":"2026-03-07T16:40:04.005723-05:00","type":"stage.started","stage_name":"gate:default","bead_id":"gromit-zrrn7","iteration":24} +{"schema_version":1,"timestamp":"2026-03-07T16:40:04.074518-05:00","type":"stage.completed","stage_name":"gate:default","bead_id":"gromit-zrrn7","iteration":24,"success":true,"duration":68794166} +{"schema_version":1,"timestamp":"2026-03-07T16:40:04.074526-05:00","type":"stage.started","stage_name":"build:default","bead_id":"gromit-zrrn7","iteration":24} +{"schema_version":1,"timestamp":"2026-03-07T16:45:12.659648-05:00","type":"stage.completed","stage_name":"build:default","bead_id":"gromit-zrrn7","iteration":24,"success":true,"duration":308583850375} +{"schema_version":1,"timestamp":"2026-03-07T16:45:12.730416-05:00","type":"stage.started","stage_name":"validate:default","bead_id":"gromit-zrrn7","iteration":24} +{"schema_version":1,"timestamp":"2026-03-07T16:45:12.770575-05:00","type":"stage.completed","stage_name":"validate:default","bead_id":"gromit-zrrn7","iteration":24,"success":true,"duration":40143709} +{"schema_version":1,"timestamp":"2026-03-07T16:45:12.818794-05:00","type":"stage.started","stage_name":"review:default","bead_id":"gromit-zrrn7","iteration":24} +{"schema_version":1,"timestamp":"2026-03-07T16:45:12.818811-05:00","type":"stage.completed","stage_name":"review:default","bead_id":"gromit-zrrn7","iteration":24,"success":true,"duration":10416} +{"schema_version":1,"timestamp":"2026-03-07T16:45:12.866532-05:00","type":"stage.started","stage_name":"epilogue:default","bead_id":"gromit-zrrn7","iteration":24} +{"schema_version":1,"timestamp":"2026-03-07T16:45:13.022158-05:00","type":"stage.completed","stage_name":"epilogue:default","bead_id":"gromit-zrrn7","iteration":24,"success":true,"duration":155617458} +{"schema_version":1,"timestamp":"2026-03-07T16:45:13.022172-05:00","type":"bead.completed","bead_id":"gromit-zrrn7","bead_title":"Implement selective revalidation in spec loop","iteration":24,"success":true} From ae9ed1ad12c79b3dc94cd70fff49f40d172be5e8 Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 18:21:50 -0500 Subject: [PATCH 79/80] implement v2 loop routing, resume, topo sort, and prompt shaping (19/19 acceptance criteria) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Multi-provider routing: Router with 3-layer selection (phase preferences, ratio balancing, fallback with cooldown). Wired into run2.go via buildRouter(). Tier-aware model selection: bead loop and spec loop populate StageRequest.Provider and StageRequest.Model before each stage. Escalation bumps tier on retry (low → medium → high). Resume with gap analysis: on resume, skip replan/re-decompose, run FlagChangedBeads to identify affected beads, then selective revalidation re-queues failures. Topological bead ordering: TopologicalSort before iteration, gate re-queue on block with all-blocked termination. Prompt budget shaping: phase-filtered rules, scoped CLAUDE.md, ShapeBudget with scope-adjusted scaling (50%/75%/100% by file count). Co-Authored-By: Claude Opus 4.6 --- cmd/gromit/run2.go | 85 +++++ internal/v2/acceptance_isolation_test.go | 2 +- internal/v2/loop/bead_loop.go | 47 ++- internal/v2/loop/bead_loop_routing_test.go | 172 +++++++++ internal/v2/loop/integration_test.go | 193 +++++++++- internal/v2/loop/spec_loop.go | 75 +++- internal/v2/loop/topo.go | 18 +- internal/v2/prompt/assembler.go | 388 +++++++-------------- internal/v2/prompt/assembler_test.go | 150 ++++---- internal/v2/routing/router.go | 21 +- internal/v2/routing/router_test.go | 20 +- internal/v2/routing/tier.go | 27 ++ internal/v2/routing/tier_test.go | 36 ++ internal/v2/stage/accept/accept.go | 2 +- internal/v2/stage/build/build.go | 2 +- internal/v2/stage/plan/plan.go | 2 +- internal/v2/stage/plan/plan_test.go | 4 +- internal/v2/stage/review/review.go | 2 +- internal/v2/stage/stage.go | 2 + internal/v2/stage/triage/triage.go | 2 +- 20 files changed, 862 insertions(+), 388 deletions(-) create mode 100644 internal/v2/loop/bead_loop_routing_test.go diff --git a/cmd/gromit/run2.go b/cmd/gromit/run2.go index a161c6a3e..8f55b9be0 100644 --- a/cmd/gromit/run2.go +++ b/cmd/gromit/run2.go @@ -11,6 +11,7 @@ import ( "strings" "sync" "syscall" + "time" "github.com/danabrams/gromit/internal/bead" "github.com/danabrams/gromit/internal/config" @@ -23,7 +24,9 @@ import ( "github.com/danabrams/gromit/internal/v2/adapter/presenter" "github.com/danabrams/gromit/internal/v2/adapter/tasktracker" "github.com/danabrams/gromit/internal/v2/dep" + "github.com/danabrams/gromit/internal/v2/llmtypes" "github.com/danabrams/gromit/internal/v2/loop" + "github.com/danabrams/gromit/internal/v2/routing" v2spec "github.com/danabrams/gromit/internal/v2/spec" "github.com/spf13/cobra" ) @@ -138,6 +141,8 @@ func run2(cmd *cobra.Command, args []string) error { wg.Wait() }() + router, phaseModels := buildRouter(cfg) + baseOpts := []loop.SpecLoopOption{ newSpecLoopEmitterFn(emitter), loop.WithPlanStage(components.PlanStage), @@ -149,6 +154,12 @@ func run2(cmd *cobra.Command, args []string) error { loop.WithStageCommitter(components.StageCommitter), loop.WithTypedEmitter(components.TypedEmitter), } + if router != nil { + baseOpts = append(baseOpts, loop.WithRouter(router)) + } + if len(phaseModels) > 0 { + baseOpts = append(baseOpts, loop.WithPhaseModels(phaseModels)) + } for _, specFile := range specFiles { if err := specFile.CheckDependencies(specsDir); err != nil { return fmt.Errorf("checking dependencies for %s: %w", specFile.ID, err) @@ -258,6 +269,80 @@ func loadSpecFromArg(arg string) (*v2spec.Spec, error) { return specFile, nil } +// buildRouter constructs a Router from the config's provider and routing +// settings. Returns (nil, nil) when no providers are configured, preserving +// backward compatibility — the spec loop handles a nil Router gracefully. +func buildRouter(cfg *config.Config) (*routing.Router, map[string]string) { + if cfg == nil || len(cfg.Providers) == 0 { + return nil, nil + } + + binary := "claude" + if strings.TrimSpace(cfg.Claude.Binary) != "" { + binary = strings.TrimSpace(cfg.Claude.Binary) + } + flags := []string{} + if len(cfg.Claude.Flags) > 0 { + flags = append(flags, cfg.Claude.Flags...) + } + timeout := 15 * time.Minute + if cfg.Claude.Timeout > 0 { + timeout = time.Duration(cfg.Claude.Timeout) * time.Second + } + + providers := make(map[string]llmtypes.LLMProvider, len(cfg.Providers)) + for name, def := range cfg.Providers { + provBinary := binary + if strings.TrimSpace(def.Binary) != "" { + provBinary = strings.TrimSpace(def.Binary) + } + provFlags := flags + if len(def.Flags) > 0 { + provFlags = append([]string(nil), def.Flags...) + } + providers[name] = llm.NewClaudeAdapter(provBinary, provFlags, timeout) + } + + var cooldown time.Duration + if cd := strings.TrimSpace(cfg.Routing.Fallback.Cooldown); cd != "" { + if parsed, err := time.ParseDuration(cd); err == nil { + cooldown = parsed + } + } + + router := routing.NewRouter(routing.RouterConfig{ + Providers: providers, + PhasePreferences: cfg.Routing.PhasePreferences, + Ratio: cfg.Routing.Ratio, + Cooldown: cooldown, + }) + + phaseModels := phaseModelsFromConfig(cfg.Methodology.PhaseModels) + return router, phaseModels +} + +// phaseModelsFromConfig converts the structured PhaseModelsConfig into a +// flat map[string]string suitable for routing.TierForPhase. +func phaseModelsFromConfig(pm config.PhaseModelsConfig) map[string]string { + m := map[string]string{} + if pm.Decompose != "" { + m["decompose"] = pm.Decompose + } + if pm.Build != "" { + m["build"] = pm.Build + } + if pm.Red != "" { + m["red"] = pm.Red + } + if pm.Green != "" { + m["green"] = pm.Green + } + if pm.Refactor != "" { + m["refactor"] = pm.Refactor + } + return m +} + func startRun2Subscribers(ctx context.Context, emitter *events.Emitter, output io.Writer, logsDir string) (*sync.WaitGroup, error) { if emitter == nil { return nil, fmt.Errorf("emitter is nil") diff --git a/internal/v2/acceptance_isolation_test.go b/internal/v2/acceptance_isolation_test.go index 09db54100..68632a3a8 100644 --- a/internal/v2/acceptance_isolation_test.go +++ b/internal/v2/acceptance_isolation_test.go @@ -54,7 +54,7 @@ func TestPromptAssemblerCompilesAllLayers(t *testing.T) { t.Parallel() assembler := prompt.NewPromptAssembler("base", "project", "instance", "fragment") - output := assembler.Assemble() + output := assembler.Assemble("", prompt.BeadInfo{}) order := []string{"base", "project", "instance", "fragment"} for idx, fragment := range order { diff --git a/internal/v2/loop/bead_loop.go b/internal/v2/loop/bead_loop.go index 17237bc20..18518881d 100644 --- a/internal/v2/loop/bead_loop.go +++ b/internal/v2/loop/bead_loop.go @@ -16,6 +16,7 @@ import ( "github.com/danabrams/gromit/internal/v2/event" "github.com/danabrams/gromit/internal/v2/generation" v2review "github.com/danabrams/gromit/internal/v2/review" + "github.com/danabrams/gromit/internal/v2/routing" "github.com/danabrams/gromit/internal/v2/stage" reviewstage "github.com/danabrams/gromit/internal/v2/stage/review" "github.com/danabrams/gromit/internal/v2/stage/triage" @@ -48,6 +49,8 @@ type BeadLoopConfig struct { StartGeneration int Git GitCommitter StageCommitter StageCommitter + Router *routing.Router // optional: if nil, routing is skipped + PhaseModels map[string]string // optional: phase -> starting tier } // BeadLoopResult captures metadata produced by a bead loop execution. @@ -72,6 +75,8 @@ type BeadLoop struct { outOfScopeFindings []v2review.Finding git GitCommitter stageCommitter StageCommitter + router *routing.Router + phaseModels map[string]string // run-scoped state for in-loop decomposition resolver *dep.Resolver beadMap map[string]*bead.Bead @@ -128,6 +133,8 @@ func NewBeadLoop(config BeadLoopConfig) (*BeadLoop, error) { startGeneration: config.StartGeneration, git: config.Git, stageCommitter: config.StageCommitter, + router: config.Router, + phaseModels: config.PhaseModels, } if config.Emitter != nil && config.LegacyEmitter != nil { event.BridgeTypedToLegacy(config.Emitter, config.LegacyEmitter) @@ -157,6 +164,16 @@ func (b *BeadLoop) Run(ctx context.Context, beads []*bead.Bead, stopCh <-chan st } } + // Pre-sort beads topologically so the resolver's add-order reflects + // dependency ordering. TopologicalSort provides the initial execution + // order; dep.Resolver handles dynamic re-ordering during execution + // (blocking/unblocking as beads complete). + sorted, err := TopologicalSort(beads) + if err != nil { + return BeadLoopResult{}, fmt.Errorf("topological pre-sort: %w", err) + } + beads = sorted + b.resolver = dep.NewResolver() b.beadMap = make(map[string]*bead.Bead, len(beads)) b.completed = nil @@ -397,6 +414,12 @@ func (b *BeadLoop) runStageEntry(ctx context.Context, beadItem *bead.Bead, itera return nil } + // Determine starting tier for this stage's phase via routing. + startTier := routing.TierMedium + if b.router != nil { + startTier = routing.TierForPhase(stageName, b.phaseModels, routing.TierMedium) + } + retriesRemaining := entry.retryConfig.MaxRetries if retriesRemaining < 0 { retriesRemaining = 0 @@ -407,6 +430,21 @@ func (b *BeadLoop) runStageEntry(ctx context.Context, beadItem *bead.Bead, itera for { req := b.stageRequest(beadItem, iteration, stageRetryContext(attempt, priorFailures)) + + // Apply routing: select provider and model for this stage. + if b.router != nil { + escalationLevel := 0 + if req.RetryContext != nil { + escalationLevel = req.RetryContext.EscalationLevel + } + tier := routing.EscalationTier(startTier, escalationLevel) + provider, model, routeErr := b.router.Select(stageName, tier) + if routeErr == nil && provider != nil { + req.Provider = provider + req.Model = model + } + } + b.emitStageStarted(stageName, beadItem.ID, iteration) start := time.Now() res, err := b.runStage(ctx, entry.stage, req) @@ -509,7 +547,14 @@ func stageRetryContext(attempt int, priorFailures []string) *stage.RetryContext if attempt <= 1 && len(priorFailures) == 0 { return nil } - ctx := &stage.RetryContext{Attempt: attempt} + escalation := 0 + if attempt > 1 { + escalation = attempt - 1 + } + ctx := &stage.RetryContext{ + Attempt: attempt, + EscalationLevel: escalation, + } if len(priorFailures) > 0 { ctx.PriorFailures = append([]string(nil), priorFailures...) } diff --git a/internal/v2/loop/bead_loop_routing_test.go b/internal/v2/loop/bead_loop_routing_test.go new file mode 100644 index 000000000..d1a870815 --- /dev/null +++ b/internal/v2/loop/bead_loop_routing_test.go @@ -0,0 +1,172 @@ +package loop + +import ( + "context" + "testing" + "time" + + "github.com/danabrams/gromit/internal/bead" + "github.com/danabrams/gromit/internal/v2/llmtypes" + "github.com/danabrams/gromit/internal/v2/routing" + "github.com/danabrams/gromit/internal/v2/stage" +) + +// spyStage captures Request fields from each invocation. +type spyStage struct { + name string + models []string + hadProv []bool + decision stage.Decision + failN int // fail the first N calls + calls int +} + +func (s *spyStage) Name() string { return s.name } + +func (s *spyStage) RetryConfig() stage.RetryConfig { + return stage.RetryConfig{MaxRetries: 3} +} + +func (s *spyStage) Run(_ context.Context, req *stage.Request) (*stage.Result, error) { + s.models = append(s.models, req.Model) + s.hadProv = append(s.hadProv, req.Provider != nil) + s.calls++ + if s.failN > 0 && s.calls <= s.failN { + return &stage.Result{Decision: stage.DecisionFail}, nil + } + return &stage.Result{Decision: s.decision}, nil +} + +// fakeProvider satisfies llmtypes.LLMProvider for testing. +type fakeProvider struct{} + +func (f *fakeProvider) Invoke(_ context.Context, _ llmtypes.LLMInvokeRequest) (*llmtypes.LLMInvokeResponse, error) { + return &llmtypes.LLMInvokeResponse{}, nil +} +func (f *fakeProvider) StreamInvoke(_ context.Context, _ llmtypes.LLMStreamInvokeRequest) (*llmtypes.LLMInvokeResponse, error) { + return &llmtypes.LLMInvokeResponse{}, nil +} + +func TestBeadLoopRouterPopulatesProviderAndModel(t *testing.T) { + t.Parallel() + + buildSpy := &spyStage{name: "build", decision: stage.DecisionProceed} + + r := routing.NewRouter(routing.RouterConfig{ + Providers: map[string]llmtypes.LLMProvider{ + "claude": &fakeProvider{}, + }, + Ratio: map[string]int{"claude": 1}, + Cooldown: time.Minute, + }) + + cfg := BeadLoopConfig{ + Gate: newNoopStage("gate"), + Build: buildSpy, + Validate: newNoopStage("validate"), + Review: newNoopStage("review"), + Epilogue: newNoopStage("epilogue"), + Router: r, + PhaseModels: map[string]string{"build": "low"}, + } + + loop, err := NewBeadLoop(cfg) + if err != nil { + t.Fatalf("NewBeadLoop: %v", err) + } + + beads := []*bead.Bead{{ID: "bead-1"}} + if _, err := loop.Run(context.Background(), beads, nil); err != nil { + t.Fatalf("Run failed: %v", err) + } + + if len(buildSpy.models) == 0 { + t.Fatal("build stage was never called") + } + if buildSpy.models[0] != "low" { + t.Fatalf("build model = %q, want %q", buildSpy.models[0], "low") + } + if !buildSpy.hadProv[0] { + t.Fatal("build Provider was nil, want non-nil") + } +} + +func TestBeadLoopEscalationBumpsTierOnRetry(t *testing.T) { + t.Parallel() + + // Build fails once then succeeds — should see tier escalation. + buildSpy := &spyStage{name: "build", decision: stage.DecisionProceed, failN: 1} + + r := routing.NewRouter(routing.RouterConfig{ + Providers: map[string]llmtypes.LLMProvider{ + "claude": &fakeProvider{}, + }, + Ratio: map[string]int{"claude": 1}, + Cooldown: time.Minute, + }) + + cfg := BeadLoopConfig{ + Gate: newNoopStage("gate"), + Build: buildSpy, + Validate: newNoopStage("validate"), + Review: newNoopStage("review"), + Epilogue: newNoopStage("epilogue"), + Router: r, + PhaseModels: map[string]string{"build": "low"}, + } + + loop, err := NewBeadLoop(cfg) + if err != nil { + t.Fatalf("NewBeadLoop: %v", err) + } + + beads := []*bead.Bead{{ID: "bead-1"}} + if _, err := loop.Run(context.Background(), beads, nil); err != nil { + t.Fatalf("Run failed: %v", err) + } + + if len(buildSpy.models) < 2 { + t.Fatalf("expected at least 2 build calls, got %d", len(buildSpy.models)) + } + if buildSpy.models[0] != "low" { + t.Fatalf("first attempt model = %q, want %q", buildSpy.models[0], "low") + } + if buildSpy.models[1] != "medium" { + t.Fatalf("second attempt model = %q, want %q", buildSpy.models[1], "medium") + } +} + +func TestBeadLoopRoutingSkippedWhenRouterNil(t *testing.T) { + t.Parallel() + + buildSpy := &spyStage{name: "build", decision: stage.DecisionProceed} + + cfg := BeadLoopConfig{ + Gate: newNoopStage("gate"), + Build: buildSpy, + Validate: newNoopStage("validate"), + Review: newNoopStage("review"), + Epilogue: newNoopStage("epilogue"), + // Router intentionally nil + } + + loop, err := NewBeadLoop(cfg) + if err != nil { + t.Fatalf("NewBeadLoop: %v", err) + } + + beads := []*bead.Bead{{ID: "bead-1"}} + if _, err := loop.Run(context.Background(), beads, nil); err != nil { + t.Fatalf("Run failed: %v", err) + } + + if len(buildSpy.models) == 0 { + t.Fatal("build stage was never called") + } + if buildSpy.models[0] != "" { + t.Fatalf("build model = %q, want empty (no router)", buildSpy.models[0]) + } + if buildSpy.hadProv[0] { + t.Fatal("build Provider should be nil when router is nil") + } +} diff --git a/internal/v2/loop/integration_test.go b/internal/v2/loop/integration_test.go index bf0fda332..3e418cbd4 100644 --- a/internal/v2/loop/integration_test.go +++ b/internal/v2/loop/integration_test.go @@ -6,12 +6,14 @@ import ( "os" "path/filepath" "testing" + "time" "github.com/danabrams/gromit/internal/bead" "github.com/danabrams/gromit/internal/config" "github.com/danabrams/gromit/internal/events" "github.com/danabrams/gromit/internal/v2/adapter" "github.com/danabrams/gromit/internal/v2/adapter/llm" + "github.com/danabrams/gromit/internal/v2/adapter/tasktracker" "github.com/danabrams/gromit/internal/v2/generation" stagepkg "github.com/danabrams/gromit/internal/v2/stage" "github.com/danabrams/gromit/internal/v2/testutil" @@ -319,6 +321,7 @@ type integrationGitAdapter struct { *testutil.FakeGit t *testing.T gapAnalysisContent string + planContent string } func newIntegrationGitAdapter(t *testing.T) *integrationGitAdapter { @@ -334,16 +337,23 @@ func (g *integrationGitAdapter) Checkout(ctx context.Context, specID string) (st if err != nil { return "", err } - if g.gapAnalysisContent == "" { - return worktree, nil - } gromitPath := filepath.Join(worktree, ".gromit", "v2") - if err := os.MkdirAll(gromitPath, 0o755); err != nil { - g.t.Fatalf("create gromit dir: %v", err) + if g.gapAnalysisContent != "" || g.planContent != "" { + if err := os.MkdirAll(gromitPath, 0o755); err != nil { + g.t.Fatalf("create gromit dir: %v", err) + } + } + if g.gapAnalysisContent != "" { + path := filepath.Join(gromitPath, "gap-analysis.md") + if err := os.WriteFile(path, []byte(g.gapAnalysisContent), 0o644); err != nil { + g.t.Fatalf("write gap analysis: %v", err) + } } - path := filepath.Join(gromitPath, "gap-analysis.md") - if err := os.WriteFile(path, []byte(g.gapAnalysisContent), 0o644); err != nil { - g.t.Fatalf("write gap analysis: %v", err) + if g.planContent != "" { + path := filepath.Join(gromitPath, "plan.md") + if err := os.WriteFile(path, []byte(g.planContent), 0o644); err != nil { + g.t.Fatalf("write plan: %v", err) + } } return worktree, nil } @@ -403,3 +413,170 @@ func assertPresenterSuccess(t *testing.T, presenter *testutil.FakePresenter, wan t.Fatalf("presenter summary success = %v, want %v", got, want) } } + +// TestIntegration_ResumeWithGapAnalysisAndRevalidation exercises the resume path: +// existing beads are found via the task tracker, plan is loaded from disk, +// gap analysis identifies affected beads, and selective revalidation re-queues them. +func TestIntegration_ResumeWithGapAnalysisAndRevalidation(t *testing.T) { + t.Parallel() + + ctx := context.Background() + specID := "spec-resume-gap" + cfg := &config.Config{} + + emitter := events.NewEmitter() + ch := emitter.Subscribe() + t.Cleanup(func() { + emitter.Unsubscribe(ch) + }) + + git := newIntegrationGitAdapter(t) + git.planContent = "existing plan from prior run" + llmAdapter := newIntegrationLLMAdapter() + taskTracker := newIntegrationTaskTrackerAdapter() + presenter := newIntegrationPresenterAdapter(t) + + // Seed three beads with the spec label so queryExistingBeads finds them. + label := fmt.Sprintf("spec:%s", specID) + for _, title := range []string{"bead-a", "bead-b", "bead-c"} { + _, err := taskTracker.CreateBead(ctx, tasktracker.CreateBeadRequest{ + Title: title, + Labels: []string{label}, + }) + if err != nil { + t.Fatalf("seed bead %s: %v", title, err) + } + } + + // Gap analyzer: report that "main.go" changed and bead-1 (first seeded) touches it. + gap := &mockGapAnalyzer{ + changedFiles: []string{"main.go"}, + beadFileMap: map[string][]string{"bead-1": {"main.go", "util.go"}}, + } + + // Revalidator: return the flagged bead as needing re-queue. + revalidator := &mockRevalidator{} + + accept := newScriptedAcceptStage(stagepkg.Result{Decision: stagepkg.DecisionProceed}) + _, beadLoop := newIntegrationLoopComponents(t, specID) + planStage := newFakePlanStage(specID) + presentStage, summaryCtx := newPresentStageForTest(t, cfg, presenter) + + adapters := adapter.AdapterSet{ + Git: git, + LLM: llmAdapter, + TaskTracker: taskTracker, + Presenter: presenter, + } + + loopInstance, err := NewSpecLoop(adapters, cfg, noopDependencyGate{}, + WithEmitter(emitter), + WithPlanStage(planStage), + WithPresentStage(presentStage, summaryCtx), + WithAcceptStage(accept), + WithBeadLoop(beadLoop), + WithGapAnalyzer(gap), + WithSelectiveRevalidator(revalidator), + ) + if err != nil { + t.Fatalf("create spec loop: %v", err) + } + + if err := loopInstance.Run(ctx, specID, nil); err != nil { + t.Fatalf("run spec loop: %v", err) + } + + // Plan stage should NOT have been called (plan loaded from disk). + if planStage.called { + t.Fatal("plan stage was called, want skipped (should resume from disk)") + } + + // Gap analyzer should have been called exactly once. + if gap.calls != 1 { + t.Fatalf("gap analyzer calls = %d, want 1", gap.calls) + } + + // Revalidator should have been called exactly once. + if revalidator.calls != 1 { + t.Fatalf("revalidator calls = %d, want 1", revalidator.calls) + } + + // The revalidator received the flagged bead (bead-1 overlaps changed files). + if len(revalidator.receivedBeads) != 1 { + t.Fatalf("revalidator received %d beads, want 1", len(revalidator.receivedBeads)) + } + if revalidator.receivedBeads[0].ID != "bead-1" { + t.Fatalf("revalidator bead ID = %q, want %q", revalidator.receivedBeads[0].ID, "bead-1") + } + + // Accept should have been called (spec completed successfully). + if accept.calls != 1 { + t.Fatalf("accept calls = %d, want 1", accept.calls) + } + + // Should see DecomposeResumedEvent and PlanResumedEvent in the event stream. + evts := drainEvents(ch) + foundDecomposeResumed := false + foundPlanResumed := false + for _, evt := range evts { + switch evt.(type) { + case *events.DecomposeResumedEvent: + foundDecomposeResumed = true + case *events.PlanResumedEvent: + foundPlanResumed = true + } + } + if !foundPlanResumed { + types := make([]string, len(evts)) + for i, e := range evts { + types[i] = fmt.Sprintf("%T", e) + } + t.Fatalf("expected PlanResumedEvent in events, got: %v", types) + } + if !foundDecomposeResumed { + types := make([]string, len(evts)) + for i, e := range evts { + types[i] = fmt.Sprintf("%T", e) + } + t.Fatalf("expected DecomposeResumedEvent in events, got: %v", types) + } + + assertPresenterSuccess(t, presenter, true) +} + +func drainEvents(ch chan events.Event) []events.Event { + var evts []events.Event + deadline := time.After(time.Second) + for { + select { + case evt := <-ch: + evts = append(evts, evt) + case <-deadline: + return evts + } + } +} + +// mockGapAnalyzer implements GapAnalyzer for testing. +type mockGapAnalyzer struct { + changedFiles []string + beadFileMap map[string][]string + calls int +} + +func (m *mockGapAnalyzer) Analyze(_ context.Context, _ string, _ []*bead.Bead) ([]string, map[string][]string, error) { + m.calls++ + return m.changedFiles, m.beadFileMap, nil +} + +// mockRevalidator implements SelectiveRevalidator for testing. +type mockRevalidator struct { + calls int + receivedBeads []*bead.Bead +} + +func (m *mockRevalidator) Revalidate(_ context.Context, beads []*bead.Bead, _ string) ([]*bead.Bead, error) { + m.calls++ + m.receivedBeads = append(m.receivedBeads, beads...) + return beads, nil +} diff --git a/internal/v2/loop/spec_loop.go b/internal/v2/loop/spec_loop.go index 2de5f78f6..817fe2f51 100644 --- a/internal/v2/loop/spec_loop.go +++ b/internal/v2/loop/spec_loop.go @@ -15,6 +15,7 @@ import ( "github.com/danabrams/gromit/internal/events" "github.com/danabrams/gromit/internal/v2/adapter" "github.com/danabrams/gromit/internal/v2/event" + "github.com/danabrams/gromit/internal/v2/routing" "github.com/danabrams/gromit/internal/v2/trackertypes" "github.com/danabrams/gromit/internal/v2/presentation" v2review "github.com/danabrams/gromit/internal/v2/review" @@ -65,6 +66,13 @@ type SelectiveRevalidator interface { Revalidate(ctx context.Context, beads []*bead.Bead, worktree string) ([]*bead.Bead, error) } +// GapAnalyzer provides the data needed for resume gap analysis: the set of +// files that changed since the last run and a mapping from bead ID to the +// files each bead touched. +type GapAnalyzer interface { + Analyze(ctx context.Context, worktree string, beads []*bead.Bead) (changedFiles []string, beadFileMap map[string][]string, err error) +} + // SpecLoopOption configures optional behavior when constructing a SpecLoop. type SpecLoopOption func(*SpecLoop) @@ -139,6 +147,28 @@ func WithSelectiveRevalidator(r SelectiveRevalidator) SpecLoopOption { } } +// WithGapAnalyzer installs the gap analyzer used to identify which completed +// beads have changed files and need selective revalidation on resume. +func WithGapAnalyzer(a GapAnalyzer) SpecLoopOption { + return func(s *SpecLoop) { + s.gapAnalyzer = a + } +} + +// WithRouter configures the routing engine for provider/model selection. +func WithRouter(r *routing.Router) SpecLoopOption { + return func(s *SpecLoop) { + s.router = r + } +} + +// WithPhaseModels sets the phase-to-tier mapping for routing. +func WithPhaseModels(pm map[string]string) SpecLoopOption { + return func(s *SpecLoop) { + s.phaseModels = pm + } +} + // WithPreserveOnFailure controls whether the worktree is kept when the spec // fails. The default is true (preserve). Pass false to remove it on failure. func WithPreserveOnFailure(preserve bool) SpecLoopOption { @@ -182,6 +212,9 @@ type SpecLoop struct { presentSummaryContext *present.SummaryContext preserveOnFailure bool // restore t.Cleanup if overriding in tests selectiveRevalidator SelectiveRevalidator + gapAnalyzer GapAnalyzer + router *routing.Router + phaseModels map[string]string } type worktreeSetter interface { @@ -272,6 +305,7 @@ func (s *SpecLoop) Run(ctx context.Context, specID string, stopCh <-chan struct{ plan = string(existingPlan) s.emit(&events.PlanResumedEvent{SpecID: specID, Path: planPath}) } else { + s.applyRouting(&req, "plan") planRes, err := s.runPlanStage(ctx, req) if err != nil { return fmt.Errorf("plan stage: %w", err) @@ -311,13 +345,36 @@ func (s *SpecLoop) Run(ctx context.Context, specID string, stopCh <-chan struct{ beads = existingBeads s.emit(&events.DecomposeResumedEvent{SpecID: specID, BeadCount: len(beads)}) if s.selectiveRevalidator != nil { - requeueBeads, err := s.selectiveRevalidator.Revalidate(ctx, beads, worktree) + // Gap analysis: identify which beads touched files that changed. + revalidationCandidates := beads + if s.gapAnalyzer != nil { + changedFiles, beadFileMap, err := s.gapAnalyzer.Analyze(ctx, worktree, beads) + if err != nil { + return fmt.Errorf("gap analysis: %w", err) + } + if flagged := FlagChangedBeads(beads, changedFiles, beadFileMap); len(flagged) > 0 { + revalidationCandidates = flagged + } + } + requeueBeads, err := s.selectiveRevalidator.Revalidate(ctx, revalidationCandidates, worktree) if err != nil { return fmt.Errorf("selective revalidation: %w", err) } - beads = append(beads, requeueBeads...) + // Merge re-queued beads: mark existing ones in-place, append truly new ones. + existingIDs := make(map[string]*bead.Bead, len(beads)) + for _, b := range beads { + existingIDs[b.ID] = b + } + for _, rb := range requeueBeads { + if existing, ok := existingIDs[rb.ID]; ok { + existing.Status = "open" + } else { + beads = append(beads, rb) + } + } } } else { + s.applyRouting(&req, "decompose") beads, err = s.runDecompose(ctx, req) if err != nil { return err @@ -472,6 +529,7 @@ func (s *SpecLoop) ensureAcceptance(ctx context.Context, req *stagepkg.Request, return nil, err } + s.applyRouting(req, "accept") res, err := s.runAcceptStage(ctx, req) if err != nil { return res, err @@ -747,3 +805,16 @@ func (s *SpecLoop) ctxErr(ctx context.Context) error { return nil } } + +// applyRouting populates req.Provider and req.Model via the Router, if configured. +func (s *SpecLoop) applyRouting(req *stagepkg.Request, phase string) { + if s.router == nil { + return + } + tier := routing.TierForPhase(phase, s.phaseModels, routing.TierMedium) + provider, model, err := s.router.Select(phase, tier) + if err == nil && provider != nil { + req.Provider = provider + req.Model = model + } +} diff --git a/internal/v2/loop/topo.go b/internal/v2/loop/topo.go index 41ebdb193..705c48440 100644 --- a/internal/v2/loop/topo.go +++ b/internal/v2/loop/topo.go @@ -53,13 +53,13 @@ func TopologicalSort(beads []*bead.Bead) ([]*bead.Bead, error) { } // Kahn's algorithm; break ties by original position for stable output - queue := make([]string, 0, len(beads)) + ready := make([]string, 0, len(beads)) for _, b := range beads { if b == nil { continue } if indegree[b.ID] == 0 { - queue = append(queue, b.ID) + ready = append(ready, b.ID) } } @@ -71,22 +71,22 @@ func TopologicalSort(beads []*bead.Bead) ([]*bead.Bead, error) { } result := make([]*bead.Bead, 0, len(beads)) - for len(queue) > 0 { + for len(ready) > 0 { // Pick next by original position (stable) minIdx := 0 - for i := 1; i < len(queue); i++ { - if index[queue[i]] < index[queue[minIdx]] { + for i := 1; i < len(ready); i++ { + if index[ready[i]] < index[ready[minIdx]] { minIdx = i } } - cur := queue[minIdx] - queue = append(queue[:minIdx], queue[minIdx+1:]...) + cur := ready[minIdx] + ready = append(ready[:minIdx], ready[minIdx+1:]...) result = append(result, beadByID[cur]) for _, dep := range dependents[cur] { indegree[dep]-- if indegree[dep] == 0 { - queue = append(queue, dep) + ready = append(ready, dep) } } } @@ -118,7 +118,7 @@ func beadDependencyIDs(b *bead.Bead) []string { } func filterEmpty(ss []string) []string { - out := ss[:0] + out := make([]string, 0, len(ss)) for _, s := range ss { if strings.TrimSpace(s) != "" { out = append(out, s) diff --git a/internal/v2/prompt/assembler.go b/internal/v2/prompt/assembler.go index cd4c5743d..d976a2f51 100644 --- a/internal/v2/prompt/assembler.go +++ b/internal/v2/prompt/assembler.go @@ -1,177 +1,111 @@ package prompt -import ( - "regexp" - "sort" - "strings" -) +import "strings" -const ( - rulesSectionHeaderPrefix = "## " - phaseAnnotationPrefix = "" -) +// DefaultMaxChars is the default maximum character budget for shaped prompts. +const DefaultMaxChars = 100000 -// rulesPhaseMaxChars maps phase names to their character caps for the base rules layer. -var rulesPhaseMaxChars = map[string]int{ - "build": 12800, - "red": 8500, - "green": 8500, - "refactor": 8500, - "review": 8500, - "plan": 5000, - "validate": 5000, +// BeadInfo carries bead metadata used for prompt shaping. +type BeadInfo struct { + Title string + Files []string + FileCount int } -// loadBaseInstructions returns the base layer filtered to phase-relevant sections -// and capped at the phase-specific character limit. -// Returns the full base content if phase is empty. -func (p *PromptAssembler) loadBaseInstructions(phase string) string { - if phase == "" { - return p.base - } - filtered := filterRulesByPhase(p.base, phase) - if cap, ok := rulesPhaseMaxChars[strings.ToLower(phase)]; ok && len(filtered) > cap { - filtered = filtered[:cap] - } - return filtered +// ShapeReport records budget shaping decisions for observability. +type ShapeReport struct { + OriginalSize int + ShapedSize int + MaxBudget int + AdjustedBudget int + FileCount int + Trimmed bool + TrimmedBytes int } -// filterRulesByPhase parses RULES.md content, filters ## sections by phase annotations, -// and strips annotation comments from the output. -func filterRulesByPhase(content, phase string) string { - lines := strings.Split(content, "\n") - - type section struct { - headerLine string - bodyLines []string - phases []string // nil = all phases - } - - var preamble []string - var sections []section - var current *section - - for _, line := range lines { - if strings.HasPrefix(line, rulesSectionHeaderPrefix) { - if current != nil { - sections = append(sections, *current) - } - phases := parseRulesPhaseAnnotation(line) - current = §ion{headerLine: line, phases: phases} - } else if current != nil { - current.bodyLines = append(current.bodyLines, line) - } else { - preamble = append(preamble, line) - } - } - if current != nil { - sections = append(sections, *current) - } +// PromptAssembler merges prompt layers into a single payload. +type PromptAssembler struct { + base string + project string + instance string + fragment string + lastReport *ShapeReport +} - var out strings.Builder - for _, line := range preamble { - out.WriteString(line) - out.WriteString("\n") - } - for _, s := range sections { - if !rulesPhaseMatches(s.phases, phase) { - continue - } - out.WriteString(stripRulesPhaseAnnotation(s.headerLine)) - out.WriteString("\n") - for _, line := range s.bodyLines { - out.WriteString(line) - out.WriteString("\n") - } - } +// NewPromptAssembler returns an assembler initialized with the provided layers. +func NewPromptAssembler(base, project, instance, fragment string) *PromptAssembler { + return &PromptAssembler{base: base, project: project, instance: instance, fragment: fragment} +} - result := out.String() - if strings.HasSuffix(result, "\n") { - result = result[:len(result)-1] - } - return result +// LastReport returns the ShapeReport from the most recent Assemble call, or nil. +func (p *PromptAssembler) LastReport() *ShapeReport { + return p.lastReport } -func parseRulesPhaseAnnotation(headerLine string) []string { - idx := strings.Index(headerLine, phaseAnnotationPrefix) - if idx < 0 { - return nil +// loadBaseInstructions returns base instructions filtered by phase. +// When phase is empty, the full base text is returned unmodified. +func (p *PromptAssembler) loadBaseInstructions(phase string) string { + if phase == "" { + return p.base } - rest := headerLine[idx+len(phaseAnnotationPrefix):] - endIdx := strings.Index(rest, phaseAnnotationSuffix) - if endIdx < 0 { - return nil + // Phase-specific filtering: look for phase markers and extract the relevant section. + marker := "## " + phase + idx := strings.Index(p.base, marker) + if idx == -1 { + // No phase-specific section found; return full base. + return p.base } - parts := strings.Split(rest[:endIdx], ",") - var phases []string - for _, p := range parts { - p = strings.ToLower(strings.TrimSpace(p)) - if p != "" { - phases = append(phases, p) - } + // Extract from the marker to the next "## " heading or end of string. + rest := p.base[idx:] + nextIdx := strings.Index(rest[len(marker):], "\n## ") + if nextIdx == -1 { + return rest } - return phases + return rest[:len(marker)+nextIdx] } -func rulesPhaseMatches(phases []string, phase string) bool { - if phases == nil { - return true +// loadProjectContext returns project context scoped to the bead's files. +// When the bead has no files, the full project text is returned unmodified. +func (p *PromptAssembler) loadProjectContext(bead BeadInfo) string { + if len(bead.Files) == 0 { + return p.project } - phase = strings.ToLower(phase) - for _, p := range phases { - if p == phase { - return true + // Scope project context: only include lines that reference bead files + // or non-file-specific lines. This is a best-effort filter. + lines := strings.Split(p.project, "\n") + var filtered []string + for _, line := range lines { + relevant := true + // If the line looks like a file-scoped entry (contains a path-like token), + // check if it references one of the bead's files. + if strings.Contains(line, "/") { + relevant = false + for _, f := range bead.Files { + if strings.Contains(line, f) { + relevant = true + break + } + } + } + if relevant { + filtered = append(filtered, line) } } - return false -} - -func stripRulesPhaseAnnotation(headerLine string) string { - idx := strings.Index(headerLine, phaseAnnotationPrefix) - if idx < 0 { - return headerLine - } - rest := headerLine[idx:] - endIdx := strings.Index(rest, phaseAnnotationSuffix) - if endIdx < 0 { - return headerLine - } - before := strings.TrimRight(headerLine[:idx], " ") - after := rest[endIdx+len(phaseAnnotationSuffix):] - return before + after -} - -// BeadInfo provides identifying metadata about the bead for prompt scoping. -type BeadInfo struct { - Title string + return strings.Join(filtered, "\n") } -var scopePackagePattern = regexp.MustCompile(`(?:internal|cmd)/[A-Za-z0-9._/\-]+`) -var h2SectionPattern = regexp.MustCompile(`(?m)^##\s+([^\n#][^\n]*)\s*$`) -var archBulletPattern = regexp.MustCompile("(?m)^\\s*-\\s*`([^`]+)`\\s*(?:—|-)\\s*(.+?)\\s*$") - -// PromptAssembler merges prompt layers into a single payload. -type PromptAssembler struct { - base string - project string - instance string - fragment string -} +// Assemble concatenates the configured layers in the prescribed order, +// applying phase filtering, bead scoping, and budget shaping. +func (p *PromptAssembler) Assemble(phase string, bead BeadInfo) string { + base := p.loadBaseInstructions(phase) + project := p.loadProjectContext(bead) -// NewPromptAssembler returns an assembler initialized with the provided layers. -func NewPromptAssembler(base, project, instance, fragment string) *PromptAssembler { - return &PromptAssembler{base: base, project: project, instance: instance, fragment: fragment} -} - -// Assemble concatenates the configured layers in the prescribed order. -func (p *PromptAssembler) Assemble() string { layers := []struct { name string content string }{ - {"BASE", p.base}, - {"PROJECT", p.project}, + {"BASE", base}, + {"PROJECT", project}, {"INSTANCE", p.instance}, {"FRAGMENT", p.fragment}, } @@ -190,124 +124,56 @@ func (p *PromptAssembler) Assemble() string { builder.WriteString(layer.content) } - return builder.String() -} - -// loadProjectContext returns the project layer scoped to packages mentioned in the bead title. -// Uses v1's applyScopedClaudeContext algorithm: extract paths from title, scope Architecture -// section, preserve Key Principles. Falls back to full content when no paths found or -// when CLAUDE.md lacks both Architecture and Key Principles sections. -func (p *PromptAssembler) loadProjectContext(bead BeadInfo) string { - matches := scopePackagePattern.FindAllString(bead.Title, -1) - if len(matches) == 0 { - return p.project - } - - unique := map[string]struct{}{} - for _, m := range matches { - if n := normalizePkgPath(m); n != "" { - unique[n] = struct{}{} - } - } - if len(unique) == 0 { - return p.project - } + raw := builder.String() - paths := make([]string, 0, len(unique)) - for path := range unique { - paths = append(paths, path) + fileCount := bead.FileCount + if fileCount == 0 { + fileCount = len(bead.Files) } - sort.Strings(paths) - return scopeProjectContent(p.project, paths) -} + shaped, report := ShapeBudget(raw, fileCount, DefaultMaxChars) + p.lastReport = &report -// normalizePkgPath normalizes a package path to a trailing-slash form. -func normalizePkgPath(path string) string { - trimmed := strings.Trim(strings.TrimSpace(path), "/") - if trimmed == "" { - return "" - } - parts := strings.Split(trimmed, "/") - // Drop file-like last segments (contain a dot). - if len(parts) > 0 && strings.Contains(parts[len(parts)-1], ".") { - parts = parts[:len(parts)-1] - } - if len(parts) < 2 { - return "" - } - return strings.Join(parts, "/") + "/" + return shaped } -// scopeProjectContent scopes the Architecture section of CLAUDE.md to only include -// paths that match the given package paths. Preserves Key Principles verbatim. -// Returns content unchanged if both Architecture and Key Principles sections are not present. -func scopeProjectContent(content string, paths []string) string { - normalized := strings.ReplaceAll(content, "\r\n", "\n") - matches := h2SectionPattern.FindAllStringSubmatchIndex(normalized, -1) - if len(matches) == 0 { - return content - } - - type section struct { - name string - raw string - body string - } - - preamble := strings.TrimSpace(normalized[:matches[0][0]]) - sections := make([]section, 0, len(matches)) - for i, match := range matches { - headerEnd := match[1] - nameStart, nameEnd := match[2], match[3] - sectionEnd := len(normalized) - if i+1 < len(matches) { - sectionEnd = matches[i+1][0] - } - name := strings.ToLower(strings.TrimSpace(normalized[nameStart:nameEnd])) - raw := strings.Trim(normalized[match[0]:sectionEnd], "\n") - body := strings.Trim(normalized[headerEnd:sectionEnd], "\n") - sections = append(sections, section{name: name, raw: raw, body: body}) - } - - var archSection, keySection *section - for i := range sections { - switch sections[i].name { - case "architecture": - archSection = §ions[i] - case "key principles": - keySection = §ions[i] - } - } - if archSection == nil || keySection == nil { - return content - } - - // Filter architecture bullets to only those matching the requested paths. - bulletMatches := archBulletPattern.FindAllStringSubmatch(archSection.body, -1) - filtered := make([]string, 0, len(bulletMatches)) - for _, bm := range bulletMatches { - if len(bm) < 3 { - continue - } - entryPath := normalizePkgPath(bm[1]) - for _, want := range paths { - if strings.HasPrefix(entryPath, want) || strings.HasPrefix(want, entryPath) { - filtered = append(filtered, "- `"+strings.Trim(bm[1], "/")+"/`"+" — "+strings.TrimSpace(bm[2])) - break - } - } - } - - archContent := "## Architecture" - if len(filtered) > 0 { - archContent += "\n\n" + strings.Join(filtered, "\n") - } - - parts := make([]string, 0, 3) - if preamble != "" { - parts = append(parts, preamble) - } - parts = append(parts, archContent, keySection.raw) - return strings.Join(parts, "\n\n") +// ShapeBudget applies scope-adjusted truncation to a prompt string. +// +// Scaling rules based on fileCount: +// +// 1-2 files → 50% of maxChars +// 3-4 files → 75% of maxChars +// 5+ files or 0 (unknown) → 100% of maxChars +// +// When the prompt exceeds the adjusted budget, context (tail) is truncated +// to fit while preserving instructions (head). +func ShapeBudget(total string, fileCount int, maxChars int) (string, ShapeReport) { + adjustedBudget := maxChars + switch { + case fileCount >= 1 && fileCount <= 2: + adjustedBudget = maxChars / 2 + case fileCount >= 3 && fileCount <= 4: + adjustedBudget = maxChars * 3 / 4 + } + + report := ShapeReport{ + OriginalSize: len(total), + MaxBudget: maxChars, + AdjustedBudget: adjustedBudget, + FileCount: fileCount, + } + + if len(total) <= adjustedBudget { + report.ShapedSize = len(total) + report.Trimmed = false + report.TrimmedBytes = 0 + return total, report + } + + // Truncate from the end (context) to preserve instructions (head). + shaped := total[:adjustedBudget] + report.ShapedSize = adjustedBudget + report.Trimmed = true + report.TrimmedBytes = len(total) - adjustedBudget + return shaped, report } diff --git a/internal/v2/prompt/assembler_test.go b/internal/v2/prompt/assembler_test.go index a9df066e3..5ae06a08d 100644 --- a/internal/v2/prompt/assembler_test.go +++ b/internal/v2/prompt/assembler_test.go @@ -7,7 +7,7 @@ import ( func TestPromptAssemblerAddsLayerMarkers(t *testing.T) { assembler := NewPromptAssembler("base layer", "project layer", "instance layer", "fragment layer") - output := assembler.Assemble() + output := assembler.Assemble("", BeadInfo{}) expectedSequence := []string{ "=== BASE ===", @@ -35,7 +35,7 @@ func TestPromptAssemblerAddsLayerMarkers(t *testing.T) { func TestPromptAssemblerSkipsEmptyLayers(t *testing.T) { assembler := NewPromptAssembler("base layer", "", "instance layer", "") - output := assembler.Assemble() + output := assembler.Assemble("", BeadInfo{}) if !strings.Contains(output, "=== BASE ===") { t.Fatalf("base section missing") @@ -53,7 +53,7 @@ func TestPromptAssemblerSkipsEmptyLayers(t *testing.T) { func TestPromptAssemblerSkipsWhitespaceOnlyLayers(t *testing.T) { assembler := NewPromptAssembler("base layer", " \n\t", "instance layer", "fragment layer") - output := assembler.Assemble() + output := assembler.Assemble("", BeadInfo{}) if strings.Contains(output, "=== PROJECT ===") { t.Fatalf("project section should be omitted when it contains only whitespace") @@ -69,105 +69,91 @@ func TestPromptAssemblerSkipsWhitespaceOnlyLayers(t *testing.T) { } } -func TestLoadBaseInstructionsCapsAtPhaseLimitBuild(t *testing.T) { - // Build phase cap is 12,800 chars per spec. - // Generate base content larger than the cap with only build-phase sections. - longContent := strings.Repeat("x", 500) - base := "## Section \n\n" + longContent + "\n\n## Section2 \n\n" + strings.Repeat("y", 13000) - assembler := NewPromptAssembler(base, "", "", "") - result := assembler.loadBaseInstructions("build") - const buildCap = 12800 - if len(result) > buildCap { - t.Errorf("build phase result is %d chars, exceeds cap of %d", len(result), buildCap) - } -} +func TestShapeBudgetSmallBeadGets50Percent(t *testing.T) { + content := strings.Repeat("x", 10000) + shaped, report := ShapeBudget(content, 1, 10000) -func TestLoadBaseInstructionsCapsAtPhaseLimitRed(t *testing.T) { - // Red phase cap is 8,500 chars per spec. - longContent := strings.Repeat("z", 9000) - base := "## Section \n\n" + longContent - assembler := NewPromptAssembler(base, "", "", "") - result := assembler.loadBaseInstructions("red") - const redCap = 8500 - if len(result) > redCap { - t.Errorf("red phase result is %d chars, exceeds cap of %d", len(result), redCap) + if len(shaped) > 5000 { + t.Fatalf("shaped len = %d, want <= 5000 (50%% of 10000)", len(shaped)) } -} - -func TestLoadProjectContextNoPackagePathsReturnsProjectUnchanged(t *testing.T) { - project := "# Project\nSome instructions" - assembler := NewPromptAssembler("base", project, "instance", "fragment") - got := assembler.loadProjectContext(BeadInfo{Title: "add some feature"}) - if got != project { - t.Errorf("expected project unchanged, got %q", got) + if !report.Trimmed { + t.Fatal("expected Trimmed = true") + } + if report.AdjustedBudget != 5000 { + t.Fatalf("AdjustedBudget = %d, want 5000", report.AdjustedBudget) } } -func TestLoadBaseInstructionsFiltersPhase(t *testing.T) { - base := `# Rules - -## Code Style - -- Use go fmt standard formatting +func TestShapeBudgetMediumBeadGets75Percent(t *testing.T) { + content := strings.Repeat("x", 10000) + shaped, report := ShapeBudget(content, 3, 10000) -## Build Only - -- Always run tests before committing - -## Review Only - -- Check for security issues` - - assembler := NewPromptAssembler(base, "", "", "") - - buildResult := assembler.loadBaseInstructions("build") - if !strings.Contains(buildResult, "## Code Style") { - t.Errorf("build result missing ## Code Style") + if len(shaped) > 7500 { + t.Fatalf("shaped len = %d, want <= 7500 (75%% of 10000)", len(shaped)) } - if !strings.Contains(buildResult, "## Build Only") { - t.Errorf("build result missing ## Build Only") + if !report.Trimmed { + t.Fatal("expected Trimmed = true") } - if strings.Contains(buildResult, "## Review Only") { - t.Errorf("build result should not contain ## Review Only") + if report.AdjustedBudget != 7500 { + t.Fatalf("AdjustedBudget = %d, want 7500", report.AdjustedBudget) } +} - reviewResult := assembler.loadBaseInstructions("review") - if !strings.Contains(reviewResult, "## Code Style") { - t.Errorf("review result missing ## Code Style") +func TestShapeBudgetLargeBeadGets100Percent(t *testing.T) { + content := strings.Repeat("x", 10000) + shaped, report := ShapeBudget(content, 6, 10000) + + if len(shaped) != 10000 { + t.Fatalf("shaped len = %d, want 10000 (100%% budget, no trim needed)", len(shaped)) } - if strings.Contains(reviewResult, "## Build Only") { - t.Errorf("review result should not contain ## Build Only") + if report.Trimmed { + t.Fatal("expected Trimmed = false for large bead within budget") } - if !strings.Contains(reviewResult, "## Review Only") { - t.Errorf("review result missing ## Review Only") + if report.AdjustedBudget != 10000 { + t.Fatalf("AdjustedBudget = %d, want 10000", report.AdjustedBudget) } } -func TestLoadProjectContextScopesArchitectureToMentionedPackage(t *testing.T) { - project := `# Gromit - -A Go CLI tool. - -## Architecture +func TestShapeBudgetReportsTrimmingDetails(t *testing.T) { + content := strings.Repeat("x", 10000) + _, report := ShapeBudget(content, 2, 10000) -- ` + "`internal/v2/prompt/`" + ` — prompt assembly -- ` + "`internal/runner/`" + ` — core loop orchestration + if report.OriginalSize != 10000 { + t.Fatalf("OriginalSize = %d, want 10000", report.OriginalSize) + } + if report.ShapedSize != 5000 { + t.Fatalf("ShapedSize = %d, want 5000", report.ShapedSize) + } + if report.TrimmedBytes != 5000 { + t.Fatalf("TrimmedBytes = %d, want 5000", report.TrimmedBytes) + } + if report.FileCount != 2 { + t.Fatalf("FileCount = %d, want 2", report.FileCount) + } +} -## Key Principles +func TestAssembleWithPhaseFiltersBase(t *testing.T) { + base := "## build\nbuild instructions\n\n## validate\nvalidate instructions" + assembler := NewPromptAssembler(base, "project context", "", "") + output := assembler.Assemble("build", BeadInfo{FileCount: 10}) -1. Fresh context each iteration -2. State in files, not memory` + if !strings.Contains(output, "build instructions") { + t.Fatal("output should contain build instructions") + } + if strings.Contains(output, "validate instructions") { + t.Fatal("output should NOT contain validate instructions when phase=build") + } +} - assembler := NewPromptAssembler("base", project, "instance", "fragment") - got := assembler.loadProjectContext(BeadInfo{Title: "Modify internal/v2/prompt/assembler.go: implement loadProjectContext"}) +func TestAssembleStoresLastReport(t *testing.T) { + assembler := NewPromptAssembler("base", "project", "", "") + assembler.Assemble("", BeadInfo{FileCount: 1}) - if !strings.Contains(got, "internal/v2/prompt/") { - t.Errorf("expected scoped content to include internal/v2/prompt/, got: %q", got) - } - if strings.Contains(got, "internal/runner/") { - t.Errorf("expected scoped content to exclude internal/runner/, got: %q", got) + report := assembler.LastReport() + if report == nil { + t.Fatal("LastReport should not be nil after Assemble") } - if !strings.Contains(got, "Key Principles") { - t.Errorf("expected scoped content to preserve Key Principles section, got: %q", got) + if report.FileCount != 1 { + t.Fatalf("report.FileCount = %d, want 1", report.FileCount) } } diff --git a/internal/v2/routing/router.go b/internal/v2/routing/router.go index 3f721c11f..1d24165f2 100644 --- a/internal/v2/routing/router.go +++ b/internal/v2/routing/router.go @@ -2,6 +2,7 @@ package routing import ( "fmt" + "sort" "sync" "time" @@ -59,6 +60,7 @@ func NewRouter(cfg RouterConfig) *Router { // Select picks an LLMProvider and model name for the given phase and tier. // Phase preferences are checked first, then ratio balancing among available providers. // Returns an error when no providers are configured or all are unavailable. +// Automatically records the invocation for ratio balancing. func (r *Router) Select(phase, tier string) (llmtypes.LLMProvider, string, error) { r.mu.Lock() defer r.mu.Unlock() @@ -73,6 +75,7 @@ func (r *Router) Select(phase, tier string) (llmtypes.LLMProvider, string, error if preferred, ok := r.phasePreferences[phase]; ok && preferred != "" { if p, exists := r.providers[preferred]; exists { if !r.isUnavailable(preferred, now) { + r.counts[preferred]++ return p, tier, nil } } @@ -83,6 +86,7 @@ func (r *Router) Select(phase, tier string) (llmtypes.LLMProvider, string, error if chosen == "" { return nil, "", fmt.Errorf("routing: all providers unavailable") } + r.counts[chosen]++ return r.providers[chosen], tier, nil } @@ -94,13 +98,6 @@ func (r *Router) MarkUnavailable(name string) { r.unavailable[name] = until } -// RecordInvocation increments the invocation count for the named provider. -func (r *Router) RecordInvocation(name string) { - r.mu.Lock() - defer r.mu.Unlock() - r.counts[name]++ -} - // isUnavailable reports whether the named provider is currently in cooldown. // Caller must hold r.mu. func (r *Router) isUnavailable(name string, now time.Time) bool { @@ -119,14 +116,20 @@ func (r *Router) isUnavailable(name string, now time.Time) bool { // Returns empty string when all providers are unavailable. // Caller must hold r.mu. func (r *Router) selectByRatio(now time.Time) string { - // Collect available providers. type candidate struct { name string score float64 } + // Sort provider names for deterministic tie-breaking. + names := make([]string, 0, len(r.providers)) + for name := range r.providers { + names = append(names, name) + } + sort.Strings(names) + var best candidate found := false - for name := range r.providers { + for _, name := range names { if r.isUnavailable(name, now) { continue } diff --git a/internal/v2/routing/router_test.go b/internal/v2/routing/router_test.go index 4cd4f1ad9..6fc21c259 100644 --- a/internal/v2/routing/router_test.go +++ b/internal/v2/routing/router_test.go @@ -102,20 +102,24 @@ func TestRouterSelectRatioBalancesAcrossProviders(t *testing.T) { "providerA": providerA, "providerB": providerB, }, + PhasePreferences: map[string]string{ + "forced": "providerA", + }, Ratio: map[string]int{ "providerA": 3, "providerB": 1, }, }) - // After recording 3 invocations for providerA and 1 for providerB, - // providerA's score = 3/3=1.0, providerB's score = 1/1=1.0 (tied). - // After 4 more for providerA, score = 4/3 > 1.0, so providerB should win. - r.RecordInvocation("providerA") - r.RecordInvocation("providerA") - r.RecordInvocation("providerA") - r.RecordInvocation("providerA") // now 4 for A vs 0 for B - // providerA score = 4/3 ≈ 1.33, providerB score = 0/1 = 0 → B should be selected + // Use phase preference to force 4 invocations on providerA (Select auto-records). + for i := 0; i < 4; i++ { + _, _, err := r.Select("forced", "low") + if err != nil { + t.Fatalf("unexpected error on forced select %d: %v", i, err) + } + } + // providerA count=4, providerB count=0. + // providerA score = 4/3 ≈ 1.33, providerB score = 0/1 = 0 → B should be selected. got, _, err := r.Select("build", "low") if err != nil { t.Fatalf("unexpected error: %v", err) diff --git a/internal/v2/routing/tier.go b/internal/v2/routing/tier.go index f4f95be37..9ad839c4c 100644 --- a/internal/v2/routing/tier.go +++ b/internal/v2/routing/tier.go @@ -1,5 +1,32 @@ package routing +// Tier constants for abstract model tiers. +const ( + TierLow = "low" + TierMedium = "medium" + TierHigh = "high" +) + +// escalationChain maps a starting tier to the next tier up. +var escalationChain = map[string]string{ + TierLow: TierMedium, + TierMedium: TierHigh, +} + +// EscalationTier returns the tier for the given escalation level. +// Level 0 returns startTier, level 1 returns the next tier up, etc. +func EscalationTier(startTier string, level int) string { + tier := startTier + for i := 0; i < level; i++ { + next, ok := escalationChain[tier] + if !ok { + break // already at highest tier + } + tier = next + } + return tier +} + // ResolveModel maps an abstract tier name to a provider-specific model name. // Uses the provider's tier-to-model map (e.g. {"low": "claude-haiku-4-5-20251001"}). // Returns tier as-is if no mapping is found. diff --git a/internal/v2/routing/tier_test.go b/internal/v2/routing/tier_test.go index 49db6f034..f386bdd91 100644 --- a/internal/v2/routing/tier_test.go +++ b/internal/v2/routing/tier_test.go @@ -70,3 +70,39 @@ func TestResolveModelEmptyStringModelFallsBack(t *testing.T) { t.Fatalf("ResolveModel with empty string model = %q, want %q", got, "low") } } + +func TestEscalationTierLowToMediumToHigh(t *testing.T) { + cases := []struct { + level int + want string + }{ + {0, routing.TierLow}, + {1, routing.TierMedium}, + {2, routing.TierHigh}, + {3, routing.TierHigh}, // caps at highest + } + for _, tc := range cases { + got := routing.EscalationTier(routing.TierLow, tc.level) + if got != tc.want { + t.Errorf("EscalationTier(low, %d) = %q, want %q", tc.level, got, tc.want) + } + } +} + +func TestEscalationTierMediumToHigh(t *testing.T) { + if got := routing.EscalationTier(routing.TierMedium, 0); got != routing.TierMedium { + t.Errorf("EscalationTier(medium, 0) = %q, want %q", got, routing.TierMedium) + } + if got := routing.EscalationTier(routing.TierMedium, 1); got != routing.TierHigh { + t.Errorf("EscalationTier(medium, 1) = %q, want %q", got, routing.TierHigh) + } +} + +func TestEscalationTierHighStaysHigh(t *testing.T) { + if got := routing.EscalationTier(routing.TierHigh, 0); got != routing.TierHigh { + t.Errorf("EscalationTier(high, 0) = %q, want %q", got, routing.TierHigh) + } + if got := routing.EscalationTier(routing.TierHigh, 1); got != routing.TierHigh { + t.Errorf("EscalationTier(high, 1) = %q, want %q", got, routing.TierHigh) + } +} diff --git a/internal/v2/stage/accept/accept.go b/internal/v2/stage/accept/accept.go index 8740bc4a8..247209164 100644 --- a/internal/v2/stage/accept/accept.go +++ b/internal/v2/stage/accept/accept.go @@ -243,7 +243,7 @@ func specFilePath(cfg *config.Config, root, specID string) (string, error) { func (s *Stage) buildPrompt(specID string, criterion coverage.Criterion, diff string) string { instance := s.buildInstanceLayer(specID, criterion, diff) assembler := v2prompt.NewPromptAssembler(s.baseLayer(), s.project, instance, s.fragment) - return assembler.Assemble() + return assembler.Assemble("", v2prompt.BeadInfo{}) } func (s *Stage) baseLayer() string { diff --git a/internal/v2/stage/build/build.go b/internal/v2/stage/build/build.go index 42be6ae2e..449c516f5 100644 --- a/internal/v2/stage/build/build.go +++ b/internal/v2/stage/build/build.go @@ -238,7 +238,7 @@ func (s *Stage) Run(ctx context.Context, req *stagepkg.Request) (*stagepkg.Resul methodology := s.resolveMethodology(req.Bead.Labels, cfg) fragment := s.fragmentFor(methodology) instance := buildInstanceLayer(req) - promptText := prompt.NewPromptAssembler(s.base, s.project, instance, fragment).Assemble() + promptText := prompt.NewPromptAssembler(s.base, s.project, instance, fragment).Assemble("", prompt.BeadInfo{}) model := s.selectModel(req, cfg) diff --git a/internal/v2/stage/plan/plan.go b/internal/v2/stage/plan/plan.go index ddc75d89b..5f96f6dcb 100644 --- a/internal/v2/stage/plan/plan.go +++ b/internal/v2/stage/plan/plan.go @@ -161,7 +161,7 @@ func (s *Stage) Run(ctx context.Context, req *stagepkg.Request) (*stagepkg.Resul } model := s.selectModel(req, cfg) - promptPayload := prompt.NewPromptAssembler(s.base, s.project, string(specData), s.fragment).Assemble() + promptPayload := prompt.NewPromptAssembler(s.base, s.project, string(specData), s.fragment).Assemble("", prompt.BeadInfo{}) resp, err := s.llm.Invoke(ctx, llmtypes.LLMInvokeRequest{Prompt: promptPayload, Model: model, Dir: req.Worktree}) if err != nil { return nil, fmt.Errorf("invoke llm: %w", err) diff --git a/internal/v2/stage/plan/plan_test.go b/internal/v2/stage/plan/plan_test.go index f5ec26ede..6089659ef 100644 --- a/internal/v2/stage/plan/plan_test.go +++ b/internal/v2/stage/plan/plan_test.go @@ -90,7 +90,7 @@ func TestStageWritesPlanAndInvokesLLM(t *testing.T) { t.Fatalf("plan file contents mismatch: %q", string(data)) } - expectedPrompt := prompt.NewPromptAssembler(baseLayer, projectLayer, specContent, fragmentLayer).Assemble() + expectedPrompt := prompt.NewPromptAssembler(baseLayer, projectLayer, specContent, fragmentLayer).Assemble("", prompt.BeadInfo{}) if fake.lastRequest.Prompt != expectedPrompt { t.Fatalf("prompt mismatch: got %q want %q", fake.lastRequest.Prompt, expectedPrompt) } @@ -158,7 +158,7 @@ func TestStageUsesLLMProvider(t *testing.T) { t.Fatalf("plan mismatch: got %q want %q", artifacts.Plan, provider.response.Output) } - expectedPrompt := prompt.NewPromptAssembler(baseLayer, projectLayer, specContent, fragmentLayer).Assemble() + expectedPrompt := prompt.NewPromptAssembler(baseLayer, projectLayer, specContent, fragmentLayer).Assemble("", prompt.BeadInfo{}) if provider.lastRequest.Prompt != expectedPrompt { t.Fatalf("prompt mismatch: got %q want %q", provider.lastRequest.Prompt, expectedPrompt) } diff --git a/internal/v2/stage/review/review.go b/internal/v2/stage/review/review.go index dc16d9a6e..789f6f3e1 100644 --- a/internal/v2/stage/review/review.go +++ b/internal/v2/stage/review/review.go @@ -204,7 +204,7 @@ func (s *Stage) Run(ctx context.Context, req *stagepkg.Request) (*stagepkg.Resul } instance := buildInstanceLayer(diff, acceptance) - promptText := prompt.NewPromptAssembler(s.base, s.project, instance, s.fragment).Assemble() + promptText := prompt.NewPromptAssembler(s.base, s.project, instance, s.fragment).Assemble("", prompt.BeadInfo{}) model, err := reviewModel(cfg) if err != nil { diff --git a/internal/v2/stage/stage.go b/internal/v2/stage/stage.go index 12d9bad1b..74d67f245 100644 --- a/internal/v2/stage/stage.go +++ b/internal/v2/stage/stage.go @@ -7,6 +7,7 @@ import ( "github.com/danabrams/gromit/internal/bead" "github.com/danabrams/gromit/internal/config" "github.com/danabrams/gromit/internal/v2/event" + "github.com/danabrams/gromit/internal/v2/llmtypes" ) // Stage defines a single beam within the run loop. @@ -19,6 +20,7 @@ type Stage interface { type StageRequest struct { Bead BeadInfo Model string + Provider llmtypes.LLMProvider Iteration int Config *config.Config Worktree string diff --git a/internal/v2/stage/triage/triage.go b/internal/v2/stage/triage/triage.go index c0e1b8dec..0effaccba 100644 --- a/internal/v2/stage/triage/triage.go +++ b/internal/v2/stage/triage/triage.go @@ -114,7 +114,7 @@ func (s *Stage) Run(ctx context.Context, req *stagepkg.Request) (*stagepkg.Resul } instance := buildTriageInstance(req.Bead.ID, req.Bead.Title, labels, failureMessage) - promptText := prompt.NewPromptAssembler(s.base, s.project, instance, s.fragment).Assemble() + promptText := prompt.NewPromptAssembler(s.base, s.project, instance, s.fragment).Assemble("", prompt.BeadInfo{}) model := s.cfg.Models.P2 if model == "" { From 0ffed9689da56a1b5c93d234a9a410d7e45ce2eb Mon Sep 17 00:00:00 2001 From: Dan Abrams Date: Sat, 7 Mar 2026 18:46:55 -0500 Subject: [PATCH 80/80] bd: backup 2026-03-07 23:46 --- .beads/backup/backup_state.json | 12 ++-- .beads/backup/dependencies.jsonl | 21 ++++++ .beads/backup/events.jsonl | 110 +++++++++++++++++++++++++++++++ .beads/backup/issues.jsonl | 28 +++++++- .beads/backup/labels.jsonl | 72 ++++++++++++++++++++ 5 files changed, 235 insertions(+), 8 deletions(-) diff --git a/.beads/backup/backup_state.json b/.beads/backup/backup_state.json index 02e9020c5..30433d222 100644 --- a/.beads/backup/backup_state.json +++ b/.beads/backup/backup_state.json @@ -1,13 +1,13 @@ { - "last_dolt_commit": "upf2aiitle3ggkfmkkko7fqf0ggc6d08", + "last_dolt_commit": "i2kkcuigo62e2gslamqigpe536igpqc6", "last_event_id": 0, - "timestamp": "2026-03-07T20:17:17.755332Z", + "timestamp": "2026-03-07T23:46:55.891384Z", "counts": { - "issues": 2189, - "events": 17963, + "issues": 2213, + "events": 18073, "comments": 3400, - "dependencies": 1573, - "labels": 2753, + "dependencies": 1594, + "labels": 2825, "config": 11 } } \ No newline at end of file diff --git a/.beads/backup/dependencies.jsonl b/.beads/backup/dependencies.jsonl index 3c2ab11b4..930d89a83 100644 --- a/.beads/backup/dependencies.jsonl +++ b/.beads/backup/dependencies.jsonl @@ -24,6 +24,7 @@ {"created_at":"2026-02-07T09:09:56Z","created_by":"Dan Abrams","depends_on_id":"gromit-2a6","issue_id":"gromit-0uq","type":"blocks"} {"created_at":"2026-02-07T09:09:56Z","created_by":"Dan Abrams","depends_on_id":"gromit-use","issue_id":"gromit-0uq","type":"blocks"} {"created_at":"2026-02-05T19:59:50Z","created_by":"Dan Abrams","depends_on_id":"gromit-d25","issue_id":"gromit-0x2","type":"blocks"} +{"created_at":"2026-03-07T15:41:51Z","created_by":"Dan Abrams","depends_on_id":"gromit-zrrn7","issue_id":"gromit-0xxdf","type":"blocks"} {"created_at":"2026-02-16T01:30:33Z","created_by":"Dan Abrams","depends_on_id":"gromit-c4uk","issue_id":"gromit-0zuz","type":"blocks"} {"created_at":"2026-02-06T05:20:37Z","created_by":"Dan Abrams","depends_on_id":"gromit-1w0","issue_id":"gromit-109","type":"blocks"} {"created_at":"2026-02-06T05:20:37Z","created_by":"Dan Abrams","depends_on_id":"gromit-1xd","issue_id":"gromit-109","type":"blocks"} @@ -45,6 +46,7 @@ {"created_at":"2026-02-23T02:10:46Z","created_by":"Dan Abrams","depends_on_id":"gromit-18sei","issue_id":"gromit-18sei.1","type":"parent-child"} {"created_at":"2026-02-23T02:10:47Z","created_by":"Dan Abrams","depends_on_id":"gromit-18sei","issue_id":"gromit-18sei.2","type":"parent-child"} {"created_at":"2026-02-23T02:10:48Z","created_by":"Dan Abrams","depends_on_id":"gromit-18sei","issue_id":"gromit-18sei.3","type":"parent-child"} +{"created_at":"2026-03-07T15:41:51Z","created_by":"Dan Abrams","depends_on_id":"gromit-ew1l8","issue_id":"gromit-1b6ks","type":"blocks"} {"created_at":"2026-02-19T03:21:06Z","created_by":"Dan Abrams","depends_on_id":"gromit-8kn5h","issue_id":"gromit-1c2ii","type":"blocks"} {"created_at":"2026-02-19T03:21:07Z","created_by":"Dan Abrams","depends_on_id":"gromit-9wmf9","issue_id":"gromit-1c2ii","type":"blocks"} {"created_at":"2026-02-19T03:21:08Z","created_by":"Dan Abrams","depends_on_id":"gromit-gbxmn","issue_id":"gromit-1c2ii","type":"blocks"} @@ -55,6 +57,7 @@ {"created_at":"2026-02-19T18:49:12Z","created_by":"Dan Abrams","depends_on_id":"gromit-70enc","issue_id":"gromit-1dimb","type":"blocks"} {"created_at":"2026-02-19T18:49:13Z","created_by":"Dan Abrams","depends_on_id":"gromit-gyylv","issue_id":"gromit-1dimb","type":"blocks"} {"created_at":"2026-02-23T09:02:19Z","created_by":"Dan Abrams","depends_on_id":"gromit-r7lcc","issue_id":"gromit-1fjzj","type":"blocks"} +{"created_at":"2026-03-07T15:41:52Z","created_by":"Dan Abrams","depends_on_id":"gromit-v1j2u","issue_id":"gromit-1fuf8","type":"blocks"} {"created_at":"2026-02-19T18:24:25Z","created_by":"Dan Abrams","depends_on_id":"gromit-odihv","issue_id":"gromit-1fuo5","type":"blocks"} {"created_at":"2026-02-05T20:02:03Z","created_by":"Dan Abrams","depends_on_id":"gromit-2t6","issue_id":"gromit-1i7","type":"blocks"} {"created_at":"2026-03-07T06:41:46Z","created_by":"Dan Abrams","depends_on_id":"gromit-kweh3","issue_id":"gromit-1o1h5","type":"blocks"} @@ -225,6 +228,9 @@ {"created_at":"2026-02-20T03:39:31Z","created_by":"Dan Abrams","depends_on_id":"gromit-61692","issue_id":"gromit-61692.1","type":"parent-child"} {"created_at":"2026-02-20T03:39:31Z","created_by":"Dan Abrams","depends_on_id":"gromit-61692","issue_id":"gromit-61692.2","type":"parent-child"} {"created_at":"2026-02-20T03:39:32Z","created_by":"Dan Abrams","depends_on_id":"gromit-61692","issue_id":"gromit-61692.3","type":"parent-child"} +{"created_at":"2026-03-07T15:41:52Z","created_by":"Dan Abrams","depends_on_id":"gromit-8yarv","issue_id":"gromit-61cbs","type":"blocks"} +{"created_at":"2026-03-07T15:41:52Z","created_by":"Dan Abrams","depends_on_id":"gromit-9z0j1","issue_id":"gromit-61cbs","type":"blocks"} +{"created_at":"2026-03-07T15:41:52Z","created_by":"Dan Abrams","depends_on_id":"gromit-f4ouz","issue_id":"gromit-61cbs","type":"blocks"} {"created_at":"2026-02-07T09:09:56Z","created_by":"Dan Abrams","depends_on_id":"gromit-2a6","issue_id":"gromit-65k","type":"blocks"} {"created_at":"2026-02-07T09:09:56Z","created_by":"Dan Abrams","depends_on_id":"gromit-jba","issue_id":"gromit-65k","type":"blocks"} {"created_at":"2026-02-07T09:09:56Z","created_by":"Dan Abrams","depends_on_id":"gromit-n73","issue_id":"gromit-65k","type":"blocks"} @@ -484,6 +490,8 @@ {"created_at":"2026-02-21T15:04:42Z","created_by":"Dan Abrams","depends_on_id":"gromit-ufopy","issue_id":"gromit-8te5t","type":"blocks"} {"created_at":"2026-02-21T15:04:44Z","created_by":"Dan Abrams","depends_on_id":"gromit-z57ht","issue_id":"gromit-8te5t","type":"blocks"} {"created_at":"2026-02-07T23:53:16Z","created_by":"Dan Abrams","depends_on_id":"gromit-8nfq","issue_id":"gromit-8tnl","type":"blocks"} +{"created_at":"2026-03-07T15:41:51Z","created_by":"Dan Abrams","depends_on_id":"gromit-9z0j1","issue_id":"gromit-8yarv","type":"blocks"} +{"created_at":"2026-03-07T15:41:51Z","created_by":"Dan Abrams","depends_on_id":"gromit-f4ouz","issue_id":"gromit-8yarv","type":"blocks"} {"created_at":"2026-02-07T08:38:38Z","created_by":"Dan Abrams","depends_on_id":"gromit-2bm","issue_id":"gromit-8yl","type":"blocks"} {"created_at":"2026-02-07T08:38:38Z","created_by":"Dan Abrams","depends_on_id":"gromit-bkk","issue_id":"gromit-8yl","type":"blocks"} {"created_at":"2026-02-22T22:17:44Z","created_by":"Dan Abrams","depends_on_id":"gromit-gg2fy","issue_id":"gromit-92gfw","type":"blocks"} @@ -522,6 +530,7 @@ {"created_at":"2026-02-20T15:36:42Z","created_by":"Dan Abrams","depends_on_id":"gromit-dhpfx","issue_id":"gromit-9wx1w","type":"blocks"} {"created_at":"2026-02-19T19:42:40Z","created_by":"Dan Abrams","depends_on_id":"gromit-kyknq","issue_id":"gromit-9xmm1","type":"blocks"} {"created_at":"2026-02-19T19:42:40Z","created_by":"Dan Abrams","depends_on_id":"gromit-retjh","issue_id":"gromit-9xmm1","type":"blocks"} +{"created_at":"2026-03-07T15:41:51Z","created_by":"Dan Abrams","depends_on_id":"gromit-f4ouz","issue_id":"gromit-9z0j1","type":"blocks"} {"created_at":"2026-02-07T08:38:38Z","created_by":"Dan Abrams","depends_on_id":"gromit-def","issue_id":"gromit-a1s","type":"blocks"} {"created_at":"2026-02-07T05:59:52Z","created_by":"Dan Abrams","depends_on_id":"gromit-b3s","issue_id":"gromit-a9g","type":"blocks"} {"created_at":"2026-02-07T05:59:52Z","created_by":"Dan Abrams","depends_on_id":"gromit-c7z","issue_id":"gromit-a9g","type":"blocks"} @@ -547,6 +556,7 @@ {"created_at":"2026-02-13T11:34:16Z","created_by":"Dan Abrams","depends_on_id":"gromit-wy62","issue_id":"gromit-aqye","type":"blocks"} {"created_at":"2026-02-13T11:34:16Z","created_by":"Dan Abrams","depends_on_id":"gromit-yhvc","issue_id":"gromit-aqye","type":"blocks"} {"created_at":"2026-02-22T17:18:05Z","created_by":"Dan Abrams","depends_on_id":"gromit-n2xw8","issue_id":"gromit-atgpc","type":"blocks"} +{"created_at":"2026-03-07T15:41:49Z","created_by":"Dan Abrams","depends_on_id":"gromit-ew1l8","issue_id":"gromit-atr10","type":"blocks"} {"created_at":"2026-02-07T23:32:16Z","created_by":"Dan Abrams","depends_on_id":"gromit-6x11","issue_id":"gromit-avbc","type":"blocks"} {"created_at":"2026-02-07T23:32:16Z","created_by":"Dan Abrams","depends_on_id":"gromit-pzh0","issue_id":"gromit-avbc","type":"blocks"} {"created_at":"2026-02-12T13:55:07Z","created_by":"Dan Abrams","depends_on_id":"gromit-tmc1","issue_id":"gromit-awh9","type":"blocks"} @@ -620,12 +630,17 @@ {"created_at":"2026-02-22T14:03:42Z","created_by":"Dan Abrams","depends_on_id":"gromit-cr50o","issue_id":"gromit-cr50o.3","type":"parent-child"} {"created_at":"2026-02-18T16:25:43Z","created_by":"Dan Abrams","depends_on_id":"gromit-9qy4","issue_id":"gromit-ctgu","type":"blocks"} {"created_at":"2026-02-07T22:39:04Z","created_by":"Dan Abrams","depends_on_id":"gromit-9ckj","issue_id":"gromit-ctpu","type":"blocks"} +{"created_at":"2026-03-07T15:41:52Z","created_by":"Dan Abrams","depends_on_id":"gromit-20962","issue_id":"gromit-cufx0","type":"blocks"} +{"created_at":"2026-03-07T15:41:52Z","created_by":"Dan Abrams","depends_on_id":"gromit-7a6rt","issue_id":"gromit-cufx0","type":"blocks"} {"created_at":"2026-02-22T22:17:22Z","created_by":"Dan Abrams","depends_on_id":"gromit-wa18r","issue_id":"gromit-cvl1y","type":"blocks"} {"created_at":"2026-02-22T12:04:57Z","created_by":"Dan Abrams","depends_on_id":"gromit-cxfim","issue_id":"gromit-cwk4z","type":"blocks"} {"created_at":"2026-02-22T12:04:56Z","created_by":"Dan Abrams","depends_on_id":"gromit-jetjq","issue_id":"gromit-cwk4z","type":"blocks"} {"created_at":"2026-02-07T04:50:46Z","created_by":"Dan Abrams","depends_on_id":"gromit-d6y","issue_id":"gromit-cxt","type":"blocks"} {"created_at":"2026-02-07T04:50:46Z","created_by":"Dan Abrams","depends_on_id":"gromit-ej8","issue_id":"gromit-cxt","type":"blocks"} {"created_at":"2026-02-11T09:27:33Z","created_by":"Dan Abrams","depends_on_id":"gromit-7oob","issue_id":"gromit-cz7a","type":"blocks"} +{"created_at":"2026-03-07T15:41:50Z","created_by":"Dan Abrams","depends_on_id":"gromit-ew1l8","issue_id":"gromit-d2rpx","type":"blocks"} +{"created_at":"2026-03-07T15:41:49Z","created_by":"Dan Abrams","depends_on_id":"gromit-jv6ns","issue_id":"gromit-d2rpx","type":"blocks"} +{"created_at":"2026-03-07T15:41:51Z","created_by":"Dan Abrams","depends_on_id":"gromit-atr10","issue_id":"gromit-d4134","type":"blocks"} {"created_at":"2026-02-11T07:42:16Z","created_by":"Dan Abrams","depends_on_id":"gromit-vflg","issue_id":"gromit-d46r","type":"blocks"} {"created_at":"2026-02-07T05:47:06Z","created_by":"Dan Abrams","depends_on_id":"gromit-r8c","issue_id":"gromit-d92","type":"blocks"} {"created_at":"2026-02-06T22:43:29Z","created_by":"Dan Abrams","depends_on_id":"gromit-twl","issue_id":"gromit-da0","type":"parent-child"} @@ -746,6 +761,7 @@ {"created_at":"2026-02-18T18:04:54Z","created_by":"Dan Abrams","depends_on_id":"gromit-gzzq5","issue_id":"gromit-etfv9","type":"blocks"} {"created_at":"2026-02-18T18:04:54Z","created_by":"Dan Abrams","depends_on_id":"gromit-usaw1","issue_id":"gromit-etfv9","type":"blocks"} {"created_at":"2026-02-15T20:17:36Z","created_by":"Dan Abrams","depends_on_id":"gromit-fnqk","issue_id":"gromit-evne","type":"blocks"} +{"created_at":"2026-03-07T15:41:49Z","created_by":"Dan Abrams","depends_on_id":"gromit-jv6ns","issue_id":"gromit-ew1l8","type":"blocks"} {"created_at":"2026-02-18T20:49:32Z","created_by":"Dan Abrams","depends_on_id":"gromit-tdsdn","issue_id":"gromit-ewhf4","type":"blocks"} {"created_at":"2026-02-07T05:43:45Z","created_by":"Dan Abrams","depends_on_id":"gromit-eiv","issue_id":"gromit-ewl","type":"blocks"} {"created_at":"2026-02-06T10:01:00Z","created_by":"Dan Abrams","depends_on_id":"gromit-ex7","issue_id":"gromit-eww","type":"blocks"} @@ -1356,6 +1372,7 @@ {"created_at":"2026-02-19T03:21:13Z","created_by":"Dan Abrams","depends_on_id":"gromit-8kn5h","issue_id":"gromit-u2pch","type":"blocks"} {"created_at":"2026-02-19T03:21:14Z","created_by":"Dan Abrams","depends_on_id":"gromit-aftvs","issue_id":"gromit-u2pch","type":"blocks"} {"created_at":"2026-02-12T17:55:41Z","created_by":"Dan Abrams","depends_on_id":"gromit-slll","issue_id":"gromit-u2zx","type":"blocks"} +{"created_at":"2026-03-07T15:41:52Z","created_by":"Dan Abrams","depends_on_id":"gromit-1b6ks","issue_id":"gromit-u4837","type":"blocks"} {"created_at":"2026-02-19T19:42:48Z","created_by":"Dan Abrams","depends_on_id":"gromit-3la54","issue_id":"gromit-u4aqq","type":"blocks"} {"created_at":"2026-02-19T19:42:48Z","created_by":"Dan Abrams","depends_on_id":"gromit-4fhlk","issue_id":"gromit-u4aqq","type":"blocks"} {"created_at":"2026-02-19T19:42:48Z","created_by":"Dan Abrams","depends_on_id":"gromit-75zeo","issue_id":"gromit-u4aqq","type":"blocks"} @@ -1391,6 +1408,7 @@ {"created_at":"2026-02-22T06:34:50Z","created_by":"Dan Abrams","depends_on_id":"gromit-ks39d","issue_id":"gromit-uslyx","type":"blocks"} {"created_at":"2026-03-07T00:55:34Z","created_by":"Dan Abrams","depends_on_id":"gromit-8ztwd","issue_id":"gromit-utelc","type":"blocks"} {"created_at":"2026-03-07T00:55:34Z","created_by":"Dan Abrams","depends_on_id":"gromit-gc3wz","issue_id":"gromit-utelc","type":"blocks"} +{"created_at":"2026-03-07T15:41:51Z","created_by":"Dan Abrams","depends_on_id":"gromit-jzbe3","issue_id":"gromit-uulh1","type":"blocks"} {"created_at":"2026-02-18T16:58:05Z","created_by":"Dan Abrams","depends_on_id":"gromit-5k3v","issue_id":"gromit-uwag","type":"blocks"} {"created_at":"2026-02-08T08:16:05Z","created_by":"Dan Abrams","depends_on_id":"gromit-sliw","issue_id":"gromit-uwyu","type":"blocks"} {"created_at":"2026-02-19T18:33:01Z","created_by":"Dan Abrams","depends_on_id":"gromit-47mxi","issue_id":"gromit-uxug5","type":"blocks"} @@ -1401,6 +1419,7 @@ {"created_at":"2026-02-23T02:05:35Z","created_by":"Dan Abrams","depends_on_id":"gromit-uzrhu","issue_id":"gromit-uzrhu.1","type":"parent-child"} {"created_at":"2026-02-23T02:05:36Z","created_by":"Dan Abrams","depends_on_id":"gromit-uzrhu","issue_id":"gromit-uzrhu.2","type":"parent-child"} {"created_at":"2026-02-23T02:05:38Z","created_by":"Dan Abrams","depends_on_id":"gromit-uzrhu","issue_id":"gromit-uzrhu.3","type":"parent-child"} +{"created_at":"2026-03-07T15:41:52Z","created_by":"Dan Abrams","depends_on_id":"gromit-fhp2q","issue_id":"gromit-v46qz","type":"blocks"} {"created_at":"2026-02-24T04:25:14Z","created_by":"Dan Abrams","depends_on_id":"gromit-gmigz","issue_id":"gromit-v6q0g","type":"blocks"} {"created_at":"2026-02-07T23:32:16Z","created_by":"Dan Abrams","depends_on_id":"gromit-pzh0","issue_id":"gromit-v7ft","type":"blocks"} {"created_at":"2026-02-15T21:27:31Z","created_by":"Dan Abrams","depends_on_id":"gromit-ap30","issue_id":"gromit-va54","type":"blocks"} @@ -1429,6 +1448,8 @@ {"created_at":"2026-02-16T18:12:00Z","created_by":"Dan Abrams","depends_on_id":"gromit-ixf7","issue_id":"gromit-w0z6","type":"blocks"} {"created_at":"2026-02-16T18:12:00Z","created_by":"Dan Abrams","depends_on_id":"gromit-o9ix","issue_id":"gromit-w0z6","type":"blocks"} {"created_at":"2026-02-16T18:12:00Z","created_by":"Dan Abrams","depends_on_id":"gromit-xqjz","issue_id":"gromit-w0z6","type":"blocks"} +{"created_at":"2026-03-07T15:41:49Z","created_by":"Dan Abrams","depends_on_id":"gromit-ew1l8","issue_id":"gromit-w1200","type":"blocks"} +{"created_at":"2026-03-07T15:41:49Z","created_by":"Dan Abrams","depends_on_id":"gromit-jzbe3","issue_id":"gromit-w1200","type":"blocks"} {"created_at":"2026-02-18T17:50:15Z","created_by":"Dan Abrams","depends_on_id":"gromit-85z2x","issue_id":"gromit-w23y4","type":"blocks"} {"created_at":"2026-02-06T08:12:15Z","created_by":"Dan Abrams","depends_on_id":"gromit-a2j","issue_id":"gromit-w2k","type":"parent-child"} {"created_at":"2026-02-19T18:30:45Z","created_by":"Dan Abrams","depends_on_id":"gromit-kcszq","issue_id":"gromit-w3cwr","type":"blocks"} diff --git a/.beads/backup/events.jsonl b/.beads/backup/events.jsonl index 4848e074e..65a837796 100644 --- a/.beads/backup/events.jsonl +++ b/.beads/backup/events.jsonl @@ -17961,3 +17961,113 @@ {"actor":"Dan Abrams","comment":null,"created_at":"2026-03-07T13:47:36Z","event_type":"closed","id":17961,"issue_id":"gromit-o1fk9","new_value":"Closed","old_value":""} {"actor":"Dan Abrams","comment":null,"created_at":"2026-03-07T14:00:57Z","event_type":"closed","id":17962,"issue_id":"gromit-yhg13","new_value":"Closed","old_value":""} {"actor":"Dan Abrams","comment":null,"created_at":"2026-03-07T15:17:17Z","event_type":"closed","id":17963,"issue_id":"gromit-b582a","new_value":"Closed","old_value":""} +{"actor":"Dan Abrams","comment":null,"created_at":"2026-03-07T15:29:33Z","event_type":"closed","id":17964,"issue_id":"gromit-cbn9c","new_value":"Closed","old_value":""} +{"actor":"Dan Abrams","comment":null,"created_at":"2026-03-07T15:33:06Z","event_type":"closed","id":17965,"issue_id":"gromit-x5abw","new_value":"Closed","old_value":""} +{"actor":"Dan Abrams","comment":null,"created_at":"2026-03-07T15:41:49Z","event_type":"created","id":17966,"issue_id":"gromit-jv6ns","new_value":"","old_value":""} +{"actor":"Dan Abrams","comment":"Added label: spec:v2-loop-routing-and-resume","created_at":"2026-03-07T15:41:49Z","event_type":"label_added","id":17967,"issue_id":"gromit-jv6ns","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: gen:0","created_at":"2026-03-07T15:41:49Z","event_type":"label_added","id":17968,"issue_id":"gromit-jv6ns","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: estimated-files:2","created_at":"2026-03-07T15:41:49Z","event_type":"label_added","id":17969,"issue_id":"gromit-jv6ns","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":null,"created_at":"2026-03-07T15:41:49Z","event_type":"created","id":17970,"issue_id":"gromit-ew1l8","new_value":"","old_value":""} +{"actor":"Dan Abrams","comment":"Added label: spec:v2-loop-routing-and-resume","created_at":"2026-03-07T15:41:49Z","event_type":"label_added","id":17971,"issue_id":"gromit-ew1l8","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: gen:0","created_at":"2026-03-07T15:41:49Z","event_type":"label_added","id":17972,"issue_id":"gromit-ew1l8","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: estimated-files:3","created_at":"2026-03-07T15:41:49Z","event_type":"label_added","id":17973,"issue_id":"gromit-ew1l8","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":null,"created_at":"2026-03-07T15:41:49Z","event_type":"created","id":17974,"issue_id":"gromit-jzbe3","new_value":"","old_value":""} +{"actor":"Dan Abrams","comment":"Added label: spec:v2-loop-routing-and-resume","created_at":"2026-03-07T15:41:49Z","event_type":"label_added","id":17975,"issue_id":"gromit-jzbe3","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: gen:0","created_at":"2026-03-07T15:41:49Z","event_type":"label_added","id":17976,"issue_id":"gromit-jzbe3","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: estimated-files:2","created_at":"2026-03-07T15:41:49Z","event_type":"label_added","id":17977,"issue_id":"gromit-jzbe3","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":null,"created_at":"2026-03-07T15:41:49Z","event_type":"created","id":17978,"issue_id":"gromit-w1200","new_value":"","old_value":""} +{"actor":"Dan Abrams","comment":"Added label: spec:v2-loop-routing-and-resume","created_at":"2026-03-07T15:41:49Z","event_type":"label_added","id":17979,"issue_id":"gromit-w1200","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: gen:0","created_at":"2026-03-07T15:41:49Z","event_type":"label_added","id":17980,"issue_id":"gromit-w1200","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: estimated-files:2","created_at":"2026-03-07T15:41:49Z","event_type":"label_added","id":17981,"issue_id":"gromit-w1200","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":null,"created_at":"2026-03-07T15:41:49Z","event_type":"created","id":17982,"issue_id":"gromit-atr10","new_value":"","old_value":""} +{"actor":"Dan Abrams","comment":"Added label: spec:v2-loop-routing-and-resume","created_at":"2026-03-07T15:41:49Z","event_type":"label_added","id":17983,"issue_id":"gromit-atr10","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: gen:0","created_at":"2026-03-07T15:41:49Z","event_type":"label_added","id":17984,"issue_id":"gromit-atr10","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: estimated-files:2","created_at":"2026-03-07T15:41:49Z","event_type":"label_added","id":17985,"issue_id":"gromit-atr10","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":null,"created_at":"2026-03-07T15:41:49Z","event_type":"created","id":17986,"issue_id":"gromit-d2rpx","new_value":"","old_value":""} +{"actor":"Dan Abrams","comment":"Added label: spec:v2-loop-routing-and-resume","created_at":"2026-03-07T15:41:49Z","event_type":"label_added","id":17987,"issue_id":"gromit-d2rpx","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: gen:0","created_at":"2026-03-07T15:41:49Z","event_type":"label_added","id":17988,"issue_id":"gromit-d2rpx","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: estimated-files:2","created_at":"2026-03-07T15:41:49Z","event_type":"label_added","id":17989,"issue_id":"gromit-d2rpx","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":null,"created_at":"2026-03-07T15:41:50Z","event_type":"created","id":17990,"issue_id":"gromit-w83pd","new_value":"","old_value":""} +{"actor":"Dan Abrams","comment":"Added label: spec:v2-loop-routing-and-resume","created_at":"2026-03-07T15:41:50Z","event_type":"label_added","id":17991,"issue_id":"gromit-w83pd","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: gen:0","created_at":"2026-03-07T15:41:50Z","event_type":"label_added","id":17992,"issue_id":"gromit-w83pd","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: estimated-files:2","created_at":"2026-03-07T15:41:50Z","event_type":"label_added","id":17993,"issue_id":"gromit-w83pd","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":null,"created_at":"2026-03-07T15:41:50Z","event_type":"created","id":17994,"issue_id":"gromit-zrrn7","new_value":"","old_value":""} +{"actor":"Dan Abrams","comment":"Added label: spec:v2-loop-routing-and-resume","created_at":"2026-03-07T15:41:50Z","event_type":"label_added","id":17995,"issue_id":"gromit-zrrn7","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: gen:0","created_at":"2026-03-07T15:41:50Z","event_type":"label_added","id":17996,"issue_id":"gromit-zrrn7","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: estimated-files:2","created_at":"2026-03-07T15:41:50Z","event_type":"label_added","id":17997,"issue_id":"gromit-zrrn7","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":null,"created_at":"2026-03-07T15:41:50Z","event_type":"created","id":17998,"issue_id":"gromit-20962","new_value":"","old_value":""} +{"actor":"Dan Abrams","comment":"Added label: spec:v2-loop-routing-and-resume","created_at":"2026-03-07T15:41:50Z","event_type":"label_added","id":17999,"issue_id":"gromit-20962","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: gen:0","created_at":"2026-03-07T15:41:50Z","event_type":"label_added","id":18000,"issue_id":"gromit-20962","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: estimated-files:3","created_at":"2026-03-07T15:41:50Z","event_type":"label_added","id":18001,"issue_id":"gromit-20962","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":null,"created_at":"2026-03-07T15:41:50Z","event_type":"created","id":18002,"issue_id":"gromit-7a6rt","new_value":"","old_value":""} +{"actor":"Dan Abrams","comment":"Added label: spec:v2-loop-routing-and-resume","created_at":"2026-03-07T15:41:50Z","event_type":"label_added","id":18003,"issue_id":"gromit-7a6rt","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: gen:0","created_at":"2026-03-07T15:41:50Z","event_type":"label_added","id":18004,"issue_id":"gromit-7a6rt","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: estimated-files:2","created_at":"2026-03-07T15:41:50Z","event_type":"label_added","id":18005,"issue_id":"gromit-7a6rt","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":null,"created_at":"2026-03-07T15:41:50Z","event_type":"created","id":18006,"issue_id":"gromit-fhp2q","new_value":"","old_value":""} +{"actor":"Dan Abrams","comment":"Added label: spec:v2-loop-routing-and-resume","created_at":"2026-03-07T15:41:50Z","event_type":"label_added","id":18007,"issue_id":"gromit-fhp2q","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: gen:0","created_at":"2026-03-07T15:41:50Z","event_type":"label_added","id":18008,"issue_id":"gromit-fhp2q","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: estimated-files:2","created_at":"2026-03-07T15:41:50Z","event_type":"label_added","id":18009,"issue_id":"gromit-fhp2q","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":null,"created_at":"2026-03-07T15:41:50Z","event_type":"created","id":18010,"issue_id":"gromit-v1j2u","new_value":"","old_value":""} +{"actor":"Dan Abrams","comment":"Added label: spec:v2-loop-routing-and-resume","created_at":"2026-03-07T15:41:50Z","event_type":"label_added","id":18011,"issue_id":"gromit-v1j2u","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: gen:0","created_at":"2026-03-07T15:41:50Z","event_type":"label_added","id":18012,"issue_id":"gromit-v1j2u","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: estimated-files:2","created_at":"2026-03-07T15:41:50Z","event_type":"label_added","id":18013,"issue_id":"gromit-v1j2u","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":null,"created_at":"2026-03-07T15:41:50Z","event_type":"created","id":18014,"issue_id":"gromit-f4ouz","new_value":"","old_value":""} +{"actor":"Dan Abrams","comment":"Added label: spec:v2-loop-routing-and-resume","created_at":"2026-03-07T15:41:50Z","event_type":"label_added","id":18015,"issue_id":"gromit-f4ouz","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: gen:0","created_at":"2026-03-07T15:41:50Z","event_type":"label_added","id":18016,"issue_id":"gromit-f4ouz","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: estimated-files:2","created_at":"2026-03-07T15:41:50Z","event_type":"label_added","id":18017,"issue_id":"gromit-f4ouz","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":null,"created_at":"2026-03-07T15:41:51Z","event_type":"created","id":18018,"issue_id":"gromit-9z0j1","new_value":"","old_value":""} +{"actor":"Dan Abrams","comment":"Added label: spec:v2-loop-routing-and-resume","created_at":"2026-03-07T15:41:51Z","event_type":"label_added","id":18019,"issue_id":"gromit-9z0j1","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: gen:0","created_at":"2026-03-07T15:41:51Z","event_type":"label_added","id":18020,"issue_id":"gromit-9z0j1","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: estimated-files:2","created_at":"2026-03-07T15:41:51Z","event_type":"label_added","id":18021,"issue_id":"gromit-9z0j1","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":null,"created_at":"2026-03-07T15:41:51Z","event_type":"created","id":18022,"issue_id":"gromit-8yarv","new_value":"","old_value":""} +{"actor":"Dan Abrams","comment":"Added label: spec:v2-loop-routing-and-resume","created_at":"2026-03-07T15:41:51Z","event_type":"label_added","id":18023,"issue_id":"gromit-8yarv","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: gen:0","created_at":"2026-03-07T15:41:51Z","event_type":"label_added","id":18024,"issue_id":"gromit-8yarv","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: estimated-files:2","created_at":"2026-03-07T15:41:51Z","event_type":"label_added","id":18025,"issue_id":"gromit-8yarv","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":null,"created_at":"2026-03-07T15:41:51Z","event_type":"created","id":18026,"issue_id":"gromit-1b6ks","new_value":"","old_value":""} +{"actor":"Dan Abrams","comment":"Added label: spec:v2-loop-routing-and-resume","created_at":"2026-03-07T15:41:51Z","event_type":"label_added","id":18027,"issue_id":"gromit-1b6ks","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: gen:0","created_at":"2026-03-07T15:41:51Z","event_type":"label_added","id":18028,"issue_id":"gromit-1b6ks","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: estimated-files:2","created_at":"2026-03-07T15:41:51Z","event_type":"label_added","id":18029,"issue_id":"gromit-1b6ks","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":null,"created_at":"2026-03-07T15:41:51Z","event_type":"created","id":18030,"issue_id":"gromit-uulh1","new_value":"","old_value":""} +{"actor":"Dan Abrams","comment":"Added label: spec:v2-loop-routing-and-resume","created_at":"2026-03-07T15:41:51Z","event_type":"label_added","id":18031,"issue_id":"gromit-uulh1","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: gen:0","created_at":"2026-03-07T15:41:51Z","event_type":"label_added","id":18032,"issue_id":"gromit-uulh1","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: estimated-files:2","created_at":"2026-03-07T15:41:51Z","event_type":"label_added","id":18033,"issue_id":"gromit-uulh1","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":null,"created_at":"2026-03-07T15:41:51Z","event_type":"created","id":18034,"issue_id":"gromit-d4134","new_value":"","old_value":""} +{"actor":"Dan Abrams","comment":"Added label: spec:v2-loop-routing-and-resume","created_at":"2026-03-07T15:41:51Z","event_type":"label_added","id":18035,"issue_id":"gromit-d4134","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: gen:0","created_at":"2026-03-07T15:41:51Z","event_type":"label_added","id":18036,"issue_id":"gromit-d4134","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: estimated-files:2","created_at":"2026-03-07T15:41:51Z","event_type":"label_added","id":18037,"issue_id":"gromit-d4134","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":null,"created_at":"2026-03-07T15:41:51Z","event_type":"created","id":18038,"issue_id":"gromit-0xxdf","new_value":"","old_value":""} +{"actor":"Dan Abrams","comment":"Added label: spec:v2-loop-routing-and-resume","created_at":"2026-03-07T15:41:51Z","event_type":"label_added","id":18039,"issue_id":"gromit-0xxdf","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: gen:0","created_at":"2026-03-07T15:41:51Z","event_type":"label_added","id":18040,"issue_id":"gromit-0xxdf","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: estimated-files:2","created_at":"2026-03-07T15:41:51Z","event_type":"label_added","id":18041,"issue_id":"gromit-0xxdf","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":null,"created_at":"2026-03-07T15:41:52Z","event_type":"created","id":18042,"issue_id":"gromit-cufx0","new_value":"","old_value":""} +{"actor":"Dan Abrams","comment":"Added label: spec:v2-loop-routing-and-resume","created_at":"2026-03-07T15:41:52Z","event_type":"label_added","id":18043,"issue_id":"gromit-cufx0","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: gen:0","created_at":"2026-03-07T15:41:52Z","event_type":"label_added","id":18044,"issue_id":"gromit-cufx0","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: estimated-files:2","created_at":"2026-03-07T15:41:52Z","event_type":"label_added","id":18045,"issue_id":"gromit-cufx0","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":null,"created_at":"2026-03-07T15:41:52Z","event_type":"created","id":18046,"issue_id":"gromit-v46qz","new_value":"","old_value":""} +{"actor":"Dan Abrams","comment":"Added label: spec:v2-loop-routing-and-resume","created_at":"2026-03-07T15:41:52Z","event_type":"label_added","id":18047,"issue_id":"gromit-v46qz","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: gen:0","created_at":"2026-03-07T15:41:52Z","event_type":"label_added","id":18048,"issue_id":"gromit-v46qz","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: estimated-files:2","created_at":"2026-03-07T15:41:52Z","event_type":"label_added","id":18049,"issue_id":"gromit-v46qz","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":null,"created_at":"2026-03-07T15:41:52Z","event_type":"created","id":18050,"issue_id":"gromit-1fuf8","new_value":"","old_value":""} +{"actor":"Dan Abrams","comment":"Added label: spec:v2-loop-routing-and-resume","created_at":"2026-03-07T15:41:52Z","event_type":"label_added","id":18051,"issue_id":"gromit-1fuf8","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: gen:0","created_at":"2026-03-07T15:41:52Z","event_type":"label_added","id":18052,"issue_id":"gromit-1fuf8","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: estimated-files:2","created_at":"2026-03-07T15:41:52Z","event_type":"label_added","id":18053,"issue_id":"gromit-1fuf8","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":null,"created_at":"2026-03-07T15:41:52Z","event_type":"created","id":18054,"issue_id":"gromit-61cbs","new_value":"","old_value":""} +{"actor":"Dan Abrams","comment":"Added label: spec:v2-loop-routing-and-resume","created_at":"2026-03-07T15:41:52Z","event_type":"label_added","id":18055,"issue_id":"gromit-61cbs","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: gen:0","created_at":"2026-03-07T15:41:52Z","event_type":"label_added","id":18056,"issue_id":"gromit-61cbs","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: estimated-files:2","created_at":"2026-03-07T15:41:52Z","event_type":"label_added","id":18057,"issue_id":"gromit-61cbs","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":null,"created_at":"2026-03-07T15:41:52Z","event_type":"created","id":18058,"issue_id":"gromit-u4837","new_value":"","old_value":""} +{"actor":"Dan Abrams","comment":"Added label: spec:v2-loop-routing-and-resume","created_at":"2026-03-07T15:41:52Z","event_type":"label_added","id":18059,"issue_id":"gromit-u4837","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: gen:0","created_at":"2026-03-07T15:41:52Z","event_type":"label_added","id":18060,"issue_id":"gromit-u4837","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":"Added label: estimated-files:2","created_at":"2026-03-07T15:41:52Z","event_type":"label_added","id":18061,"issue_id":"gromit-u4837","new_value":null,"old_value":null} +{"actor":"Dan Abrams","comment":null,"created_at":"2026-03-07T15:46:29Z","event_type":"closed","id":18062,"issue_id":"gromit-20962","new_value":"Closed","old_value":""} +{"actor":"Dan Abrams","comment":null,"created_at":"2026-03-07T15:49:50Z","event_type":"closed","id":18063,"issue_id":"gromit-7a6rt","new_value":"Closed","old_value":""} +{"actor":"Dan Abrams","comment":null,"created_at":"2026-03-07T16:04:07Z","event_type":"closed","id":18064,"issue_id":"gromit-cufx0","new_value":"Closed","old_value":""} +{"actor":"Dan Abrams","comment":null,"created_at":"2026-03-07T16:09:43Z","event_type":"closed","id":18065,"issue_id":"gromit-f4ouz","new_value":"Closed","old_value":""} +{"actor":"Dan Abrams","comment":null,"created_at":"2026-03-07T16:17:46Z","event_type":"closed","id":18066,"issue_id":"gromit-fhp2q","new_value":"Closed","old_value":""} +{"actor":"Dan Abrams","comment":null,"created_at":"2026-03-07T16:23:30Z","event_type":"closed","id":18067,"issue_id":"gromit-jv6ns","new_value":"Closed","old_value":""} +{"actor":"Dan Abrams","comment":null,"created_at":"2026-03-07T16:26:46Z","event_type":"closed","id":18068,"issue_id":"gromit-jzbe3","new_value":"Closed","old_value":""} +{"actor":"Dan Abrams","comment":null,"created_at":"2026-03-07T16:28:52Z","event_type":"closed","id":18069,"issue_id":"gromit-uulh1","new_value":"Closed","old_value":""} +{"actor":"Dan Abrams","comment":null,"created_at":"2026-03-07T16:32:29Z","event_type":"closed","id":18070,"issue_id":"gromit-v1j2u","new_value":"Closed","old_value":""} +{"actor":"Dan Abrams","comment":null,"created_at":"2026-03-07T16:36:59Z","event_type":"closed","id":18071,"issue_id":"gromit-v46qz","new_value":"Closed","old_value":""} +{"actor":"Dan Abrams","comment":null,"created_at":"2026-03-07T16:40:03Z","event_type":"closed","id":18072,"issue_id":"gromit-w83pd","new_value":"Closed","old_value":""} +{"actor":"Dan Abrams","comment":null,"created_at":"2026-03-07T16:45:12Z","event_type":"closed","id":18073,"issue_id":"gromit-zrrn7","new_value":"Closed","old_value":""} diff --git a/.beads/backup/issues.jsonl b/.beads/backup/issues.jsonl index f28865073..f84840106 100644 --- a/.beads/backup/issues.jsonl +++ b/.beads/backup/issues.jsonl @@ -40,6 +40,7 @@ {"acceptance_criteria":"EnsureWorktree creates worktree at \u003cproject\u003e-gromit-interactive sibling path\nEnsureWorktree reuses existing healthy worktree without recreating\nCreateBranch creates correctly-named branch in the worktree","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-14T15:12:53Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"be5768483eb647082edb3bf904c86fff16cab30603be202f2f4276fee868dbe8","created_at":"2026-02-12T16:38:33Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Create internal/worktree/worktree.go with Manager struct containing MainDir, WorktreeDir, and gitRunFn fields. Implement NewManager, EnsureWorktree (creates or verifies worktree at sibling path), CreateBranch (generates gromit/\u003ccommand\u003e-\u003ctimestamp\u003e branch), and Cleanup (removes worktree). Wire gitRunFn as field for testability.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-0vxy","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Create worktree.Manager with lifecycle methods","updated_at":"2026-02-14T15:12:53Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"- [ ] Function IsScopeTooLarge(output string) (bool, string) added\n- [ ] Returns true + breakdown text when output contains 'SCOPE_TOO_LARGE:' prefix\n- [ ] Returns false + empty string when marker not present\n- [ ] go build ./... passes","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-05T21:35:57Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"9169e43f690f26f4ef12f1e9fcab2bb6ad571fce87acadb0d1c8ebca272e13f6","created_at":"2026-02-05T19:59:41Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add a function that checks Claude's output for the SCOPE_TOO_LARGE: prefix marker and extracts the suggested breakdown text. Similar to existing VALIDATION_PASSED/FAILED detection. File: internal/claude/claude.go","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-0x2","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add IsScopeTooLarge function to claude package","updated_at":"2026-02-07T18:29:38Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"Experiment struct matches spec JSON format with all required fields\nBaselineMetrics struct has avg cost, duration, token, and failure rate fields","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-08T00:49:06Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"46ce4b2036c9614ecc437746cfa24e6a68b523cfce2d7817555e1c14abb40608","created_at":"2026-02-07T23:32:16Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Create internal/retro/experiment.go with Experiment struct containing Name, Hypothesis, Change, Measurement, Risk, StartedAt, and BaselineMetrics fields. Define BaselineMetrics with fields for average cost, duration, tokens, and failure rate. Include JSON tags.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-0xbs","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Define Experiment and BaselineMetrics structs","updated_at":"2026-02-08T00:49:06Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"393749dddc4bff60e8f66b131a2576dd27f484efa83a33d43314fe7a47c831cd","created_at":"2026-03-07T20:41:52Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add tests to internal/v2/gap/gap_test.go: verify gap analysis diffs file correctly, identifies beads with file changes, does not flag beads with no overlap.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-0xxdf","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"open","target":"","timeout_ns":0,"title":"Add unit tests for gap analysis","updated_at":"2026-03-07T20:41:52Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"retro_filter_test.go is deleted\nstdin_helper_example_test.go is deleted\nUseful test ideas captured via gromit add before deletion\ngo test ./... passes","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-11T08:18:26Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"c771b638299f84920e1bc684fa1a27fbd17c77783b33aa728c9429b6a7e65041","created_at":"2026-02-11T05:25:39Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"internal/retro/retro_filter_test.go has 14 tests ALL with t.Skip() — every single test is a placeholder that never runs. cmd/gromit/stdin_helper_example_test.go has 2 permanently-skipped example tests. Per RULES.md: 'Do not write tests with t.Skip() for scenarios that cannot run in the test environment.' Capture useful behaviors in backlog via gromit add, then delete both files.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-0ypa","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Delete permanently-skipped tests in retro_filter_test.go and stdin_helper_example_test.go","updated_at":"2026-02-11T08:18:26Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"Config uses haiku model for all priorities and validation\nEscalation enabled with chain [haiku, sonnet]\nValidation enabled with go test ./... command","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-07T00:02:19Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"cfe5e65435562e0c752aa04e568ed49dee1b5f4dbe1a2e26dcffc1e6a9c16d81","created_at":"2026-02-06T22:43:30Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Create test/e2e/testdata/gromit.yaml with minimal configuration for fast E2E tests. Use haiku for all models, enable escalation with haiku→sonnet chain, enable validation with go test command.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-0zg","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Create E2E test config file","updated_at":"2026-02-07T18:29:38Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"1. Skill outputs spec file to .gromit/specs/\u003cname\u003e.md (not beads)\n2. Keeps one-question-at-a-time conversational approach\n3. Includes codebase exploration phase before designing\n4. Includes collaborative spec naming (propose slug, human approves)\n5. Spec format uses frontmatter (id, source_ideas, created) with four sections: Specification, Acceptance Criteria, Decisions, Research \u0026 Context\n6. No bd create guidance or bead creation references remain","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-06T13:53:39Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"45d3541924470d4e23ec20047021fc4986c57ee4bb993faad14a61a8f44bae69","created_at":"2026-02-06T13:41:30Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Rewrite skills/gromit-refine/SKILL.md to produce specs instead of beads. Keep one-question-at-a-time approach and conversational methodology. Output: spec file with frontmatter (id, source_ideas, created) and four sections (Specification, Acceptance Criteria, Decisions, Research \u0026 Context). Add codebase exploration phase. Remove bd create guidance.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-0zh","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Rewrite gromit-refine skill for spec output","updated_at":"2026-02-07T18:29:38Z","waiters":"","wisp_type":"","work_type":""} @@ -66,10 +67,12 @@ {"acceptance_criteria":"internal/pipeline/decompose_test.go","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-23T02:38:47Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"df7c28c25224e971413fa1508bd0f5338c2cecde9561301f572d9e8cc8620529","created_at":"2026-02-23T02:10:49Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-18sei.3","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Extend decompose tests for per-attempt summary, final warning details, and clean-exit no-warning behavior","updated_at":"2026-02-23T02:38:47Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-19T02:17:29Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"07db2cc4467082c009b36528acd510a2097072892bdb8364fb66e11b3882308b","created_at":"2026-02-19T00:57:54Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"All 130 Codex iterations report $0 cost_usd. Root causes: (1) estimatedCostUSD() uses 'openai' as config key but runtime provider name is 'codex', (2) per-model pricing was commented out in gromit.yaml. Fix: uncomment gromit.yaml pricing section, fix provider name mapping in cost estimation, add integration test verifying non-zero cost_usd for each configured provider. Leading indicator: next run's JSONL logs show non-zero cost_usd for Codex iterations.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-18wuw","is_template":0,"issue_type":"bug","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Fix Codex cost tracking — provider name mismatch and missing per-model pricing","updated_at":"2026-02-19T02:17:29Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-21T09:54:09Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"93d5b7c6598ac2b1ed9b525c88cfcb39abceaf74f61ea5e472d3b5f2caca48bc","created_at":"2026-02-20T02:14:50Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"After extracting create and helper functions, internal/bead/bead.go remains above the 500-line production-file guideline. Continue moving cohesive method groups into focused files while preserving behavior and tests.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-1ajzr","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Further split internal/bead/bead.go below 500 lines","updated_at":"2026-02-21T09:54:09Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"7dc55beadb25693fa7402197684ed4f821a7aa53b6f337f1c81c368dd57b4400","created_at":"2026-03-07T20:41:51Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add tests to internal/v2/routing/router_test.go: verify ratio balancing produces correct provider selection, phase preferences override ratio, fallback marks unavailable and selects alternative, cooldown re-enables provider.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-1b6ks","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"open","target":"","timeout_ns":0,"title":"Add unit tests for Router ratio balancing and fallback","updated_at":"2026-03-07T20:41:51Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-20T09:41:26Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"4541bd9d03c861cf7e690eb34bb09c8166bff653e302f2dbd724913cad6dd69a","created_at":"2026-02-19T03:20:43Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"In internal/runner/process_methodology.go, parse criteria from spec via coverage.ParseCriteria and create a tracker at TDD cycle start. Inject coverage state into red phase prompt before each cycle. After red phase, parse self-report via coverage.ParseSelfReport. After green phase, construct haiku validation callback in internal/runner/callbacks.go and call executor.ValidateCoverage. Call tracker.MarkCovered or tracker.RecordRejection based on response. Use tracker.IsComplete for cycle termination. Add tests.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-1c2ii","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Wire CoverageTracker into TDD cycle loop","updated_at":"2026-02-20T09:41:26Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"Each gate failure produces a bead with correct spec:\u003cname\u003e label\nBead titles and descriptions reflect the specific failure\nCreation is capped at 5 beads per synthesis call","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-20T14:36:23Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"155e6ec17c0ec9e99f1354e0a9a06f5f1013c76986f6378a4daf087587e9c15b","created_at":"2026-02-16T01:30:38Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Create SynthesizeFixBeads(ctx, specName, failures []GateFailure, beadClient BeadClient) ([]string, error) function in internal/runner/spec_fix.go. For each GateFailure, create a new bead via beadClient.Create() with title derived from failure, description including suggested fix, priority P0, and labels [spec:\u003cname\u003e]. Cap at 5 fix beads per call.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-1d0u","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Implement fix bead synthesis from gate failures","updated_at":"2026-02-20T14:36:23Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-19T23:14:17Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"62500a62064fff49b75fc9d59b7fd6332db48e9ae8c3739f4e83f01d0b0f638d","created_at":"2026-02-19T18:49:07Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add CoverageState string and TargetCriterion string fields to the Context struct in internal/prompt/prompt.go. Modify .gromit/templates/PROMPT_tdd_build.md to add a conditional section after the task description that renders coverage state when present: {{if .CoverageState}}## Coverage State\\n{{.CoverageState}}{{end}}. Add tests in internal/prompt/prompt_test.go verifying conditional rendering both when fields are populated and when empty.\\n\\nAcceptance criteria:\\n- prompt.Context has CoverageState and TargetCriterion string fields\\n- TDD build template renders the coverage state section when CoverageState is non-empty\\n- Template renders identically to before when coverage fields are empty strings","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-1dimb","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add coverage state fields to prompt.Context and TDD build template","updated_at":"2026-02-19T23:14:17Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"CreateSessionWorktree retry behavior follows gromit-r7lcc contract exactly\\nTests cover branch exists, worktree exists, non-retryable failures, and ambiguous text cases\\nErrors returned after retry exhaustion preserve root-cause context","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-23T09:24:35Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"6ab9d75312b9fdf9e0465056861fac2c064fed320f0465022847b9a31fcbb4ea","created_at":"2026-02-23T09:02:20Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Implement the contract from gromit-r7lcc in internal/worktree. Replace fragile string-only matching with the agreed classifier strategy, preserve retry semantics, and expand tests to cover mixed retryable/non-retryable failures and ambiguous messages.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-1fjzj","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Implement session worktree contention classifier from approved contract","updated_at":"2026-02-23T09:24:35Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"950f87653e618656bee07405c9e982b58123138853f3d06b2a54d3e86ed1faa3","created_at":"2026-03-07T20:41:52Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add tests to internal/v2/loop/bead_loop_test.go: blocked bead deferred to end of pass, blocked bead proceeds after dependency completes, all-blocked pass terminates with descriptive error.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-1fuf8","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"open","target":"","timeout_ns":0,"title":"Add integration tests for gate re-queue on block","updated_at":"2026-03-07T20:41:52Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"spec_orchestrator.go rewritten in pipeline refactor — new implementation uses runArgv by default","closed_at":"2026-02-21T14:30:01Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"33e1f2dd8cd0b58a306eec2147c196e69e26c0c27a4fc566d0c2258c1ae0ddc6","created_at":"2026-02-19T18:23:47Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Replace runCmd git add and git commit calls in internal/runner/spec_orchestrator.go with o.runArgv using explicit argv vectors. The git add pathspec :(glob)**/*_acceptance_test.go passes as a separate arg without shell quoting. Add tests verifying the correct argv vectors.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-1fuo5","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"Experiment: demoted P1→P2 (haiku). Mechanical: migrate to runArgv. Tracking tier-demotion-2026-02-20.","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":4,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Migrate spec_orchestrator.go git operations to runArgv","updated_at":"2026-02-21T14:30:01Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-23T01:31:38Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"a33c5958647ca4bc6c68cc94c71bd8fcc00efdad227d2e440d8bef6d51c300fd","created_at":"2026-02-22T22:00:50Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Scope-gate decomposition currently trusts model output shape beyond JSON parsing. Add strict contract checks (2-5 sub-beads, \u003c=5 expected outputs per child, no empty/duplicate outputs, no exact parent echo) and explicit fallback behavior when constraints are violated.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-1gnya","is_template":0,"issue_type":"bug","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Enforce decomposition output quality contract in gate path","updated_at":"2026-02-23T01:31:38Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-03-05T04:23:07Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"ef15a8436c0c976c4f2734e08ee9f5e7ac91b117cca7d8a0b1b1682313973982","created_at":"2026-03-03T21:55:27Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Modify cmd/gromit/tui.go to change runTui() into a for-loop: call HydrateStore including pipeline items at the top of each iteration, create and run tea.Program capturing the returned Model, then break the loop when model.PendingAction is nil.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-1h6qk","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Implement exit/relaunch loop in runTui","updated_at":"2026-03-05T04:23:07Z","waiters":"","wisp_type":"","work_type":""} @@ -104,6 +107,7 @@ {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"stale: db reset","closed_at":"2026-03-02T19:42:31Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"805840eafc46f1e05a50fd15e321fcd4fca606ee426fe3cbef00e9c2ef7527d4","created_at":"2026-02-20T00:10:44Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Agent resolver adapters (cliAgentResolver, agentResolverAdapter, exploreAgentResolver) are copy-pasted across 3+ cmd/gromit files. Interface changes require O(n) updates. Extract a shared agents.NewResolver(cfg) factory function so command files use it instead of per-command adapter structs. Leading indicator: zero adapter struct definitions in cmd/gromit/*.go files.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-1yx73","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":3,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Extract shared agentResolver adapter into internal/agents/ package","updated_at":"2026-03-02T19:42:31Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-03-07T13:27:16Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"a22e4edb8b197f758fca3b70562b64f2911befcba0cf2d9cc8361d71fc3a1a56","created_at":"2026-03-07T11:41:47Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add squasher func(context.Context) error field to present.Stage, create WithSquasher option function, and update New to accept variadic options. Call squasher in Run before invoking the presenter. Include unit tests verifying squasher is called, errors propagate, and nil-check is backward compatible.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-1zcnn","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add squasher hook to Present stage","updated_at":"2026-03-07T13:27:16Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-06T11:21:22Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"b81481d46a956377bbd5e7848911af8ef9e8f8aada90bf56cbde51fc1a9adb4a","created_at":"2026-02-06T09:30:00Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add review config section to defaultConfig in cmd/ralph/init.go. Defaults to enabled: false for both light and thorough reviews so existing users are not surprised. See docs/plans/2026-02-06-review-system-plan.md Task 16.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-1zx","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Update default ralph.yaml config with review section","updated_at":"2026-02-07T18:29:38Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-03-07T20:46:29Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"551db7a489321115e25ce0329a61f401b21806aebf43f7469f5c014622e7a1ee","created_at":"2026-03-07T20:41:50Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Modify internal/v2/remediation/ (or equivalent) runner contract: change Run() signature to Run(ctx, specID, worktree string) error. Update Request template to populate worktree field for all remediation stages.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-20962","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Expand remediation runner signature for worktree propagation","updated_at":"2026-03-07T20:46:29Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"All adapter methods return typed structs instead of maps\nNo runtime type assertions in any adapter\ngo build ./... compiles successfully","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-14T09:37:37Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"5be66c2a06068a11195fa17b27e05a3467425cec22d903e66d5bb5a7c22ead4b","created_at":"2026-02-12T15:20:57Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Modify cmd/gromit/decompose.go and review.go to update claudeClientAdapter, cliClaudeClient, beadClientAdapter, cliBeadClient, and cliPromptRenderer to use typed signatures. Remove all map construction and type assertions from adapters.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-20iy","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Update CLI adapters to use typed signatures","updated_at":"2026-02-14T09:37:37Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"Enable Runner construction without Claude in deps","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"stale: db reset","closed_at":"2026-03-02T19:42:28Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"c75a609800fbc5e30f31450b15f456bd1e569168e6c9f5188a65574fd29e484b","created_at":"2026-02-19T14:47:13Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Runner refactor target: Deps/ NewRunnerWithDeps should support Router-only dependencies (no Claude field) and instantiate without fallback-to-Claude behavior.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-20mha","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Enable Runner construction without Claude in deps","updated_at":"2026-03-02T19:42:28Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"CheckStaleness returns true with reason for clean_exit: false or stale timestamp\nCheckStaleness treats zero UpdatedAt as stale regardless of threshold\nAutoHeal resets counters but preserves LastReviewCommit and LastRetro","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-08T00:06:42Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"19a063c3f89d4723a5b49ed4940c7e3b6463058b4421adfa375aa3657a4cd65f","created_at":"2026-02-07T22:39:04Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add SetCleanExit(bool), CheckStaleness(thresholdMinutes int) (bool, string), and AutoHeal() methods to File in internal/state/state.go. CheckStaleness detects crash (CleanExit false) or stale timestamp (UpdatedAt older than threshold). AutoHeal resets IterationsSinceReview and LastReviewIteration to 0 while preserving LastReviewCommit and LastRetro.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-20v4","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Implement staleness detection and auto-heal methods","updated_at":"2026-02-08T00:06:42Z","waiters":"","wisp_type":"","work_type":""} @@ -336,6 +340,7 @@ {"acceptance_criteria":"`Deps` includes an `ArgvRunner` field of type `ArgvRunnerFn`.\n`newRunnerWithDepsImpl` sets `r.argvRunnerFn` from deps when provided.\n`newRunnerWithDepsImpl` uses `defaultArgvRunner` when `deps.ArgvRunner` is nil.","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-20T04:04:05Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"ec404562615104b576f7c38dcd27369d824c98f1ce42107763f9f4be829e712a","created_at":"2026-02-20T03:39:31Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Extend `Deps` in `constructor_with_deps.go` with `ArgvRunner ArgvRunnerFn` and update `newRunnerWithDepsImpl` so `r.argvRunnerFn` is assigned from `deps.ArgvRunner`, falling back to `defaultArgvRunner` when nil.\n\nAcceptance criteria:\n- `Deps` includes an `ArgvRunner` field of type `ArgvRunnerFn`.\n- `newRunnerWithDepsImpl` sets `r.argvRunnerFn` from deps when provided.\n- `newRunnerWithDepsImpl` uses `defaultArgvRunner` when `deps.ArgvRunner` is nil.\n","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-61692.1","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add ArgvRunner to Deps and runner wiring","updated_at":"2026-02-20T04:04:05Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"`SpecOrchestrator` has an `argvRunnerFn` field.\n`SpecOrchestrator.runArgv` calls the configured `argvRunnerFn`.\n`SpecOrchestrator.runArgv` falls back to `defaultArgvRunner` if no runner is set.","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-20T04:16:10Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"b7dec31ecc59f0e32855432f0b8172629d5053a1c92bf90e87f452ee81afaf96","created_at":"2026-02-20T03:39:32Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add `argvRunnerFn ArgvRunnerFn` to `SpecOrchestrator` and implement a `runArgv` method that executes via that function, using `defaultArgvRunner` when the orchestrator’s runner function is nil.\n\nAcceptance criteria:\n- `SpecOrchestrator` has an `argvRunnerFn` field.\n- `SpecOrchestrator.runArgv` calls the configured `argvRunnerFn`.\n- `SpecOrchestrator.runArgv` falls back to `defaultArgvRunner` if no runner is set.\n","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-61692.2","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add argv runner support to SpecOrchestrator","updated_at":"2026-02-20T04:16:10Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"SpecOrchestrator is constructed with the runner’s configured argv runner function.\nA non-nil `deps.ArgvRunner` is observable inside SpecOrchestrator execution flow.\nBuild/tests covering orchestrator construction and argv execution pass with both custom and default runner paths.","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-20T04:21:09Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"4a881c268b791182ee072b069e91f5943fa557a65d09167112edf2855e52212e","created_at":"2026-02-20T03:39:33Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Update SpecOrchestrator construction paths to pass through the runner’s configured argv runner (`r.argvRunnerFn`) so orchestration uses injected behavior end-to-end.\n\nAcceptance criteria:\n- SpecOrchestrator is constructed with the runner’s configured argv runner function.\n- A non-nil `deps.ArgvRunner` is observable inside SpecOrchestrator execution flow.\n- Build/tests covering orchestrator construction and argv execution pass with both custom and default runner paths.\n","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-61692.3","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Thread argv runner into SpecOrchestrator creation","updated_at":"2026-02-20T04:21:09Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"74e3d10246a61213954a6a8b012477c3858a56e1423a9f74663f83c7ea01ea5b","created_at":"2026-03-07T20:41:52Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add tests to internal/v2/prompt/assembler_test.go: phase filtering retains correct sections with cap respected, CLAUDE.md scoping to package relevant sections, budget shaping respects total cap with scope adjustment.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-61cbs","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"open","target":"","timeout_ns":0,"title":"Add unit tests for phase filtering and scoping in prompt assembler","updated_at":"2026-03-07T20:41:52Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"stale: db reset","closed_at":"2026-03-02T19:42:31Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"64a8b98cb0178ccdd3b4e7811101c3bcc1beaefb921fc72550e752827c0d29a7","created_at":"2026-02-20T01:38:17Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Agent resolver adapters (cliAgentResolver, agentResolverAdapter, exploreAgentResolver) are copy-pasted across cmd/gromit files. Extract into a single shared implementation so interface changes touch 1 file instead of 3+. See retro 2026-02-20 system_actions.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-61qv7","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":3,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Extract shared agent resolver adapter in cmd/gromit","updated_at":"2026-03-02T19:42:31Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-03-05T10:49:39Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"b4bf77ca7abc7d56cb9355ce3ec5297f774de2b5c68d681043995d6346e9f272","created_at":"2026-03-05T09:57:26Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"In cmd/gromit/adapters.go, trackerItemToBeadInfo() silently discards errors from json.Unmarshal for labels and fmt.Sscanf for priority. Also json.Marshal errors are discarded in Create(). Add proper error handling or at minimum log warnings.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-62ro2","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Fix unchecked JSON/Sscanf errors in adapters.go trackerItemToBeadInfo","updated_at":"2026-03-05T10:49:39Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"HasOpenChildren runs bd list --json --status open --parent \u003cid\u003e --limit 1 instead of fetching all open beads\nReturns true when bd returns non-empty array, false when empty\nExisting validation (nil client, invalid parentID) still works","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-14T07:42:07Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"0a4579d12bc8f04aae2956f895b2d10c00bbe822ba11990ac642e89d269c9d97","created_at":"2026-02-12T15:15:10Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Replace HasOpenChildren implementation in internal/bead/bead.go at line 689 to use targeted query: bd list --json --status open --parent \u003cid\u003e --limit 1 instead of fetching all open beads client-side. Parse output and return true if non-empty array. Add unit tests in bead_test.go to verify command arguments and return values.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-6426","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Optimize HasOpenChildren to use --parent flag with tests","updated_at":"2026-02-14T07:42:07Z","waiters":"","wisp_type":"","work_type":""} @@ -408,6 +413,7 @@ {"acceptance_criteria":"internal/procutil/procutil_test.go","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Completed","closed_at":"2026-03-04T18:58:57Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"faa658ddd7a015c08262352d2dd3c0452b786e6b00fcd55388ae70c58957f31e","created_at":"2026-03-04T18:02:42Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-791l5.1","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Write failing tests for GOMAXPROCS-respecting SubprocessEnv behavior","updated_at":"2026-03-04T18:58:57Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"internal/procutil/procutil.go","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Completed","closed_at":"2026-03-04T18:20:26Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"e9e973e5d97f81f8b403e8e8f5ab5a53faff2058300c78bac0f92740439af7f3","created_at":"2026-03-04T18:02:43Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-791l5.2","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Update SubprocessEnv to skip override when GOMAXPROCS is already set, and add comment documenting cgroup/macOS context","updated_at":"2026-03-04T18:20:26Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Completed","closed_at":"2026-03-04T01:54:20Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"12cc0c83c9d6ed8a36a89170508faf22280cae8819fff8c3e23911534625f52f","created_at":"2026-03-03T21:55:25Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Extend the HydrationProvider interface in internal/tui/hydration.go with a PipelineItems(ctx, gromitDir, specsDir, plansDir) (PipelineItems, error) method. Update HydrateStore() to call this method and populate the Store, logging a warning on error instead of returning it.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-79kjl","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Extend HydrationProvider and HydrateStore for pipeline data","updated_at":"2026-03-04T01:54:20Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-03-07T20:49:50Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"8e6c998ef47283a9d9d3f15c22cf03d42007f313555e9b19769f0f5ce57f14bd","created_at":"2026-03-07T20:41:50Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Create internal/v2/loop/topo.go: implement topological sort of beads by DependsOn/BlockedBy graph. Beads with no dependencies come first. Stable tie-breaking for deterministic ordering.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-7a6rt","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Implement topological sort for beads","updated_at":"2026-03-07T20:49:50Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"--agent flag selects the specified agent for refine\n--choose-agent flag shows the interactive picker\nDefault behavior (no flags, no agents config) launches claude identically to current behavior","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-09T07:19:21Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"c752173bf9b4a316240c69003f768b7c75879e371a529daf1e32f63e7476c597","created_at":"2026-02-08T09:09:44Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add --agent (string) and --choose-agent (bool) flags to the refine cobra command in cmd/gromit/refine.go. After writing the prompt to the temp file, call agent.Resolve() to get the agent, then call agent.Launch(promptPath). Remove the inline Claude binary/flags resolution and exec.Command construction. Keep prompt building and post-exit artifact detection unchanged.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-7dcu","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Wire agent selection into refine command","updated_at":"2026-02-09T07:19:21Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"Iteration JSONL records include a touched_packages field when packages are available\nExisting log decoding remains backward compatible with older records missing the field via zero-value omission\nExisting logging tests updated to assert touched_packages is written when packages are present","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-19T21:51:59Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"3bb0ebe55a5031c32235870f9cd1c817e545e9cf0848d7647b8de223813c3bb4","created_at":"2026-02-19T21:37:23Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Extend internal/logger/logger.go IterationLog struct with a touched_packages field and populate it in internal/runner/logging.go when writing iteration log entries. Update existing logger and runner logging tests to assert the new field is written correctly.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-7duv2","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Persist touched packages in iteration logs","updated_at":"2026-02-19T21:51:59Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-07T21:23:19Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"7e032c363bd4b65494e6b947566bd4894cb9347e6d71c2098f4c20e174cd6d84","created_at":"2026-02-07T19:01:31Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"In internal/pipeline/status.go:28-33, all other slice fields (UnrefinedIdeas, UnplannedSpecs, UndecomposedPlans) are initialized to []string{}, but ReadyBeads is left nil. If bead.NewClient() fails, ReadyBeads stays nil while others are empty. This causes null vs [] differences in JSON serialization.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-7e5o","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Initialize ReadyBeads to empty slice in PipelineStatus constructor","updated_at":"2026-02-07T21:23:19Z","waiters":"","wisp_type":"","work_type":""} @@ -652,6 +658,7 @@ {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-23T13:56:03Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"7e73f4a2c00ab4be8e387e2cc7c9c84a21250d4711b9c8b02863f56ef52a70e6","created_at":"2026-02-23T13:50:11Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Build.Run records OriginalTier/ActualTier for single-pass flows, but TDD fresh-context path returns phase metrics without explicit top-level tier assertions. Define expected iteration-log semantics for TDD and add tests ensuring tier provenance is consistent across build strategies.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-8w81a","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add tier-metrics parity coverage for TDD fresh-context builds","updated_at":"2026-02-23T13:56:03Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-06T12:22:44Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"8c14049a8fed42e1de955c59e65af009d4cd1fd5648122108a0da5677328115e","created_at":"2026-02-06T12:15:20Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-8x2","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add review timeout awareness to run deadline","updated_at":"2026-02-07T18:29:38Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-13T04:29:36Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"4795ae4b36f57df8cfa1649dddab2d9dcc5a822d99e337fa60179a14604a1f27","created_at":"2026-02-12T17:44:32Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add RecentValidationFailures []string to prompt.Context and update normalizeNilFields(). Add validationFailures []string field to Runner struct. Implement extractValidationSummary(failureOutput string) string as a package-level function in runner that extracts key error lines from go test/vet output: lines matching --- FAIL:, FAIL\\t, or go vet diagnostic patterns. Cap each summary at 500 characters.\n\nAcceptance criteria:\n- prompt.Context has RecentValidationFailures []string field initialized to empty slice by normalizeNilFields()\n- extractValidationSummary extracts test failure names and error messages from go test output\n- extractValidationSummary extracts diagnostic lines from go vet output","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-8y6e","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":0,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add validation failure types and summary extraction","updated_at":"2026-02-13T04:29:36Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"93426e32e9e2e228c5435366b989e2e52f3009f16a1b322f65e85dffe4fdeff3","created_at":"2026-03-07T20:41:51Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Modify internal/v2/prompt/assembler.go Assemble() method to integrate phase filtering, scoping, and budget shaping. Change signature to Assemble(phase string, fileCount int) string. Layers are trimmed in priority order to fit budget.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-8yarv","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"open","target":"","timeout_ns":0,"title":"Wire budget shaping into Assemble workflow","updated_at":"2026-03-07T20:41:51Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"TestReadStatus_Valid writes file and verifies parsed fields\nTestReadStatus_NoFile verifies nil return for missing file\nTestIsProcessAlive tests cover both live and dead PIDs","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-07T11:13:52Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"7d97bfbd73ccdb80721203a5554189a0687b84da57c3e411cb3eeb7c2eba1801","created_at":"2026-02-07T08:38:38Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add TestReadStatus_Valid, TestReadStatus_NoFile, TestIsProcessAlive_Self, and TestIsProcessAlive_Dead to internal/runner/status_test.go. Use t.TempDir() for file I/O tests and real syscalls for PID checks.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-8yl","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add tests for ReadStatus and IsProcessAlive","updated_at":"2026-02-07T18:29:38Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-03-07T06:03:12Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"d97342f244e72205b1c70f6a5e0ebdf5307c28394f649748884964a231517358","created_at":"2026-03-07T05:55:33Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add three new methods to the GitAdapter interface and implement them in ExecGitAdapter. Create LogEntry type with Hash and Message fields. Implement Log to return recent commits in most-recent-first order, Show to return commit diffs, and SquashCommits to collapse commits via git reset --soft. Update existing fakes and mocks to compile. Write integration tests using real temp git repos.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-8ztwd","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Extend GitAdapter with Log, Show, and SquashCommits methods","updated_at":"2026-03-07T06:03:12Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-21T08:23:52Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"79efbf36df1227258cf557c9fc41739f31f7911e181908b4e7c5d4934b80e9b5","created_at":"2026-02-21T02:56:46Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Deming's key insight: 94% of problems belong to the system. When a bead fails, distinguish system-caused failures (ambiguous spec, overbroad decomposition, unclear acceptance criteria, prompt issues) from model-caused failures (model genuinely couldn't handle the task). Add a failure_attribution field to iteration_metrics: 'system' (spec/prompt/decomposition set the model up to fail) vs 'model' (task was well-scoped but model was inadequate) vs 'transient' (infrastructure/timeout). Use failure patterns: repeated failures on same bead across tiers = likely system cause; single failure with success on retry at same tier = likely transient.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-90hqq","is_template":0,"issue_type":"feature","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Attribute failures to system causes vs model capability","updated_at":"2026-02-21T08:23:52Z","waiters":"","wisp_type":"","work_type":""} @@ -707,6 +714,7 @@ {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-20T16:12:43Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"8c3ae41ada583af130a5eb7eb6de19b10a9afee3db9c5584e2ef6f8f8b64b70b","created_at":"2026-02-20T15:36:30Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"In internal/runner/process.go, set bc.Result.OriginalTier to the heuristic-selected tier in setupBeadContext and set bc.Result.ActualTier after execution. Update internal/runner/logging.go to pass both fields through writeIterationLog. Add assertions in internal/runner/logging_test.go verifying both fields appear in JSONL output.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-9wx1w","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Wire OriginalTier/ActualTier through process and logging","updated_at":"2026-02-20T16:12:43Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"After calling any Render* method, LastDiagnostics() returns non-nil with correct PromptType and non-zero EstimatedTokens\nSectionTokens map contains expected section keys for each prompt type (e.g. RenderBuild includes rules, spec, task_identity; RenderReview includes diff)\nAdding LastDiagnostics to PromptRenderer interface compiles with all existing implementations and mocks","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-19T21:40:14Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"15f2a44e7bbf6b4d4e510549e79e7804baaa9ca466a304522bbac2dafb5b8126","created_at":"2026-02-19T19:42:40Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add lastDiagnostics field, LastDiagnostics() method, and computeBuildDiagnostics helper to Renderer in internal/prompt/prompt.go. Add LastDiagnostics() to PromptRenderer interface in internal/runner/interfaces.go and update all mock implementations to return nil. Update internal/prompt/prompt_test.go.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-9xmm1","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Wire diagnostics into Renderer and PromptRenderer interface","updated_at":"2026-02-19T21:40:14Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-06T12:38:18Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"ef58a58661623f77079bbff7f53185c2b4eeb98d2c3a50ed9837433f38e73d06","created_at":"2026-02-06T12:37:21Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-9xp","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Update README commands reference table: add board, plan, queue, triage, review commands with their flags","updated_at":"2026-02-07T18:29:38Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"9eb36588cb82e813c4d4fd069eeea2412967ebe9b152e5a06f31e8a90cce9479","created_at":"2026-03-07T20:41:51Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Modify internal/v2/prompt/assembler.go: add ShapeBudget() step that applies scope-adjusted scaling (50% for 1-2 file beads, 75% for 3-4 files, 100% for 5+) to layers and trims them to fit. Produce ShapeReport documenting what was trimmed.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-9z0j1","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"open","target":"","timeout_ns":0,"title":"Implement budget shaping with scope adjustment in PromptAssembler","updated_at":"2026-03-07T20:41:51Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"stale: db reset","closed_at":"2026-03-02T19:42:29Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"0441189deffd5ddcc97e4a88aa6dba7ebe1d00011e2919a813aa709cb3fcd2f4","created_at":"2026-02-18T15:43:39Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Running go test -tags e2e ./test/e2e fails in TestMain when gromit init runs in scaffold dir with error 'find project root: file does not exist'. Investigate why gromit init can't find project root in temp scaffold and fix or adjust test setup.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-9z6m","is_template":0,"issue_type":"bug","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Investigate e2e TestMain gromit init failure (find project root)","updated_at":"2026-03-02T19:42:29Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"TestStatusWriter_Write verifies pid field exists in output\nTestStatusWriter_Write_IncludesPID validates PID matches os.Getpid()\nTests use real file I/O with t.TempDir()","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-07T11:07:08Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"b83702b372b03ac362b8ecd49ecd9ee4483cd3b8b4d2f18969d6f523eb14c355","created_at":"2026-02-07T08:38:38Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Update TestStatusWriter_Write in internal/runner/status_test.go to verify the pid field. Add dedicated TestStatusWriter_Write_IncludesPID test that writes status and validates the PID value in the JSON output.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-a1s","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add tests for PID field in status writer","updated_at":"2026-02-07T18:29:38Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-08T07:53:33Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"5bc2029fd166312ed884820543c624db288683b01661ba10533d0e8c066b537a","created_at":"2026-02-06T08:12:08Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Improve retro command: enrich stuck bead data with status/comments, replace y/n interactive mode with Claude Code session launch.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-a2j","is_template":0,"issue_type":"epic","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Improve ralph retro: bead context + Claude Code review","updated_at":"2026-02-08T07:53:33Z","waiters":"","wisp_type":"","work_type":""} @@ -740,6 +748,7 @@ {"acceptance_criteria":"go build ./... passes\ngo test ./... passes with zero test failures\nt.Skip count reduced by ~100 from baseline","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"stale: db reset","closed_at":"2026-03-02T19:42:25Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"c71842f7ec4467d4e20d511b5962a1f300bdb00590a2d70aa8e3797bda8e0882","created_at":"2026-02-13T11:34:16Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Run full test suite to confirm all changes are clean. Verify go build ./... and go test ./... both pass. Compare t.Skip count before and after (expect ~100 reduction from baseline of ~168). Close overlapping beads gromit-uugq and gromit-08s1 as superseded.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-aqye","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Final verification and bead cleanup","updated_at":"2026-03-02T19:42:25Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-05T17:29:32Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"ed118ec44a729df9fd39c0d8972b2092dda9a7f29c631575278a6a998f992355","created_at":"2026-02-05T17:27:23Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Rename the triage command to refine for better clarity.\n\n## Changes needed\n- Rename cmd/ralph/triage.go → cmd/ralph/refine.go\n- Update command name from 'triage' to 'refine' in cobra definition\n- Update any references in main.go\n- Update help text and README\n\nSimple rename, no logic changes.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-asd","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Rename ralph triage to ralph refine","updated_at":"2026-02-07T18:29:38Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"Label inheritance logic in decompose pipeline that copies build_strategy:X label from parent to sub-beads\nTests verifying build_strategy:tdd propagates to all sub-beads\nTests verifying absence of build_strategy label is also propagated (no label added)","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-22T22:44:50Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"c6d4bb3e95d6d150a9694f757bf16f36393338fdcda2498b90d8ae0d270cc56b","created_at":"2026-02-22T17:18:06Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"In the bead decomposition pipeline (internal/pipeline/decompose/ or equivalent), copy the parent bead's build_strategy:X label onto each generated sub-bead. If the parent has no build_strategy label, sub-beads are created without one and inherit the global config.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-atgpc","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Inherit build_strategy label when decomposing beads into sub-beads","updated_at":"2026-02-22T22:44:50Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"eb7b2b54673a00ba16f67893b2f7d09b5ee2015c3dac6631cdec2e7265aefdf4","created_at":"2026-03-07T20:41:50Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Modify internal/v2/loop/spec_loop.go to apply the same Router.Select() logic for spec-level stages (plan, decompose, accept). Populate StageRequest.Model and StageRequest.Provider before calling each stage.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-atr10","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"open","target":"","timeout_ns":0,"title":"Wire Router into spec loop for plan/decompose/accept phases","updated_at":"2026-03-07T20:41:50Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-03-03T06:08:02Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"07e5bc34c7c88ca344bd5b4ce2bdc29128f51bf2b8688a42678daf59d704ce61","created_at":"2026-03-03T04:28:10Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Insert the enricher call in Gate.Run() in internal/pipeline/prepare/gate.go after precheck and before readiness check. Only call when enricher is non-nil and bead has empty ExpectedOutputs. On error log a warning and continue (fail-open). Add gate_test.go tests for the new call site behavior.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-au1yu","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Wire CriteriaEnricher call into Gate.Run","updated_at":"2026-03-03T06:08:02Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"TemplateContext has Efficiency field of type pointer to logger.EfficiencyReport\nTemplateContext has Experiment field of type pointer to retro.Experiment","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-08T01:18:23Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"1a17a23d2bd486849dcc21a2e72f80f52f8a3044631e66db7e0555fcf6945b5e","created_at":"2026-02-07T23:32:16Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Modify internal/retro/retro.go to add Efficiency and Experiment fields to TemplateContext struct. These fields will hold efficiency report data and active experiment data for template rendering.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-avbc","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Extend TemplateContext for efficiency and experiments","updated_at":"2026-02-08T01:18:23Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"cmd/gromit/plan.go is under 120 lines\ngromit plan behaves identically for both input modes (spec picker, spec name flag)\nPlan existence check and force override work correctly via pipeline package","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-14T07:00:29Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"a36e902d4af3c079dff9b2c6db98ef0ded459c5c0e291f43c54d94842de58e5f","created_at":"2026-02-12T13:55:07Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Extract business logic from cmd/gromit/plan.go (currently 265 lines) into the Pipeline.Plan() method implemented in the prior bead. The CLI file should become a thin adapter that handles only: flag parsing, interactive spec picker, calling Pipeline.Plan(), and post-plan chaining to decompose. All spec validation, plan existence checking, bead context gathering, prompt building, agent resolution, and post-processing should live in the pipeline package.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-awh9","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Refactor cmd/gromit/plan.go to thin adapter over Pipeline.Plan()","updated_at":"2026-02-14T07:00:29Z","waiters":"","wisp_type":"","work_type":""} @@ -821,7 +830,7 @@ {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"stale: db reset","closed_at":"2026-03-02T19:42:25Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"828c3299c1fdd48af89d20db3e0cae8b611159de5d2e6339b65ddd640f534fd1","created_at":"2026-02-22T22:17:28Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Currently only the green phase uses before/after usage snapshots via snapshotIterationUsage(). Red and refactor phases use recordPhaseMetric() without snapshots, mixing deltas with raw values. Switch all phases to consistent snapshot-based delta tracking with tests.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-c98eq","is_template":0,"issue_type":"bug","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add snapshot-based delta tracking to red and refactor phase metrics","updated_at":"2026-03-02T19:42:25Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-03-03T06:11:02Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"865f303c4705f229822741aa9239fe0cddea7ba705acc323539aee76c3ed2aa0","created_at":"2026-03-03T04:28:10Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add UpdateExpectedOutputs(ctx context.Context, id string, criteria []string) error method to *Client in internal/bead/bead.go. Join criteria with newlines and call bd update \u003cid\u003e --acceptance \u003cjoined\u003e. Follow the UpdatePriority pattern. Add tests in internal/bead/bead_test.go.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-c9ozb","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add UpdateExpectedOutputs to bead client","updated_at":"2026-03-03T06:11:02Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Splitting into separate tmux and status file beads for bite-sized work","closed_at":"2026-02-06T07:09:08Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"5776751d2d672a0ad4a89b5af72f4490f12291a554a9133ec061681e7e8ad7f0","created_at":"2026-02-06T07:07:33Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Set tmux pane title during runs + write .ralph/status.json for external consumers.\n\nTmux pane title (auto-detected): Ralph: iter 3 - gromit-v1t (sonnet)\nStatus file (.ralph/status.json): {running, iteration, bead_id, bead_title, model, started_at, elapsed_s}\n\nImplementation:\n- Detect tmux via TMUX env var\n- Save/restore pane title on start/exit\n- Update title at start of each iteration\n- Write status.json at iteration start, completion, and exit (running: false)\n- New internal/tmux/tmux.go for tmux detection and title management\n- Use defer + signal handling for cleanup\n\nKey files: internal/runner/runner.go, internal/tmux/tmux.go (new)","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-ca2","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Tmux integration + status file during runs","updated_at":"2026-02-07T18:29:38Z","waiters":"","wisp_type":"","work_type":""} -{"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"7ee9865e0b54611c9a44a60cdb4faf3b832cfd7b9c695c1db8bf2ae6fc004d84","created_at":"2026-03-07T15:06:50Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Create internal/v2/loop/resume_integration_test.go with integration test. Setup existing spec and beads with some changed files, run SpecLoop on resume, verify plan and decompose are skipped, gap analysis identifies changed beads, and flagged beads are re-queued. Test covers resume flow acceptance criteria.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-cbn9c","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"open","target":"","timeout_ns":0,"title":"Integration test for resume with gap analysis","updated_at":"2026-03-07T15:06:50Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-03-07T20:29:34Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"7ee9865e0b54611c9a44a60cdb4faf3b832cfd7b9c695c1db8bf2ae6fc004d84","created_at":"2026-03-07T15:06:50Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Create internal/v2/loop/resume_integration_test.go with integration test. Setup existing spec and beads with some changed files, run SpecLoop on resume, verify plan and decompose are skipped, gap analysis identifies changed beads, and flagged beads are re-queued. Test covers resume flow acceptance criteria.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-cbn9c","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Integration test for resume with gap analysis","updated_at":"2026-03-07T20:29:34Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"Finds current binary using os.Executable() with os.Args[0] fallback\nConnects subprocess stdio to parent process stdio\nReturns nil for *exec.ExitError, propagates other errors","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-06T22:45:11Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"f6c57518d5da7fcd3530d081614518989764e17f753db4bf1effb433b512406b","created_at":"2026-02-06T22:25:41Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Create execGromit function in cmd/gromit/chain.go that accepts variadic string args. Uses os.Executable() to find current binary (fallback to os.Args[0]). Creates exec.Command with args, connects stdio to os.Stdin/Stdout/Stderr, runs subprocess. Treats *exec.ExitError as nil (subprocess printed own errors).","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-cbu","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Implement execGromit subprocess executor","updated_at":"2026-02-07T18:29:38Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-03-07T06:09:27Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"7ed442c66c0ef64abb67b86c9f5ee9b124fbe572ab7391d8f1436696d1d19579","created_at":"2026-03-07T05:55:33Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Create SquashPerBead function in internal/v2/pipeline/squash.go that reads git log, groups per-stage commits by bead ID, and squashes them into single per-bead commits for PR presentation. Define SquashGit interface with Log and SquashCommits methods. Parse structured commit messages with ParseCommitMessage, identify bead boundaries, assemble squash messages from bead titles, and call SquashCommits with the correct base hash. Handle no-op case when no structured commits exist.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-cbvlu","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Implement per-bead squash logic","updated_at":"2026-03-07T06:09:27Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"Gets diff, renders refactor prompt, calls Claude Run with build model, and re-validates\nOn validation failure reverts to pre-refactor commit via git reset --hard and retries once\nOn second validation failure skips refactoring with warning and returns nil","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-07T09:03:30Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"4088a73ff30119c3c686ad774f3e73ea58ed1db2f1dfc3b76eb1cb278d79f3a4","created_at":"2026-02-07T05:48:27Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add runRefactorPhase method to Runner in internal/runner/process.go that captures pre-refactor commit, renders refactor prompt, calls Run, re-validates, and handles validation failure by reverting and retrying once. Skips refactoring on second failure.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-cc9","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Implement runRefactorPhase method","updated_at":"2026-02-07T18:29:38Z","waiters":"","wisp_type":"","work_type":""} @@ -857,6 +866,7 @@ {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-21T09:44:11Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"74dbaaa861e7253b1fbd62edecd591055e19b524a9cc07ff37b21c1c58331509","created_at":"2026-02-20T10:47:37Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"In processCodexStream, turn.completed events overwrite the usage variable while response.completed events correctly merge via mergeCodexUsage. In multi-turn sessions, earlier turn usage data is discarded. Apply mergeCodexUsage consistently.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-ct8wi","is_template":0,"issue_type":"bug","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Use mergeCodexUsage for turn.completed events in codex_stream.go","updated_at":"2026-02-21T09:44:11Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"No test in codex_test.go references result.Stdout\nTestCodexProviderRunCapturesStderr asserts result.Output is empty and result.Stderr contains the error message\nAll tests pass: go test ./internal/provider/...","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-18T16:52:42Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"efdbddacd0e24331b0001281fd67bbf81aefd218ebaa2a429d56729b668a7d37","created_at":"2026-02-18T16:25:43Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Update five test sites in `internal/provider/codex_test.go` that reference `result.Stdout` to use `result.Output` instead. Update `TestCodexProviderRunCapturesStderr` to assert `result.Output` is empty (not containing stderr content) and `result.Stderr` contains the error. Run `go test ./internal/provider/...` to confirm all tests pass.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-ctgu","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Update codex_test.go assertions to match new Output semantics","updated_at":"2026-02-18T16:52:42Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"Test verifies default threshold is 60\nTest verifies custom threshold loads from YAML\nTest verifies zero threshold gets default of 60","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-07T23:59:11Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"45c9abef81b212b6265bf82e25068d0754ba3d43b55ae04eb5c322e63c0d612a","created_at":"2026-02-07T22:39:04Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add test cases to internal/config/config_test.go verifying default threshold behavior, custom threshold loading from YAML, and zero threshold defaulting to 60.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-ctpu","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add StateConfig tests","updated_at":"2026-02-07T23:59:11Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-03-07T21:04:07Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"c16a479ed41a37649220762188143ad2f33692636fa4080f6bba211a59a30c13","created_at":"2026-03-07T20:41:52Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add tests to internal/v2/loop/spec_loop_test.go: existing plan + beads -\u003e gap analysis -\u003e selective revalidation -\u003e correct bead list, remediation receives correct worktree, Request template populated.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-cufx0","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add integration tests for resume with gap analysis and revalidation","updated_at":"2026-03-07T21:04:07Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"stale: db reset","closed_at":"2026-03-02T19:42:38Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"3267dbf98a1d3429a7c46cd340d36b8269c82b562fbb4336883eca397afd551b","created_at":"2026-02-22T22:17:05Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add unit and integration tests verifying the --choose-agent flag is parsed correctly and affects agent selection in the refine command.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-cvl1y","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add unit tests for --choose-agent flag in refine command","updated_at":"2026-03-02T19:42:38Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"showStatus function body replaced with call to runner.PrintStatus\nOld manual status.json reading and field-by-field printing removed\nAcceptance tests pass: go test -tags acceptance ./internal/runner/acceptance/ -run Status","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-22T17:50:08Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"2f839a21fa13a07d82fe2c50defc98bfaf3bf30ba683fc852be7bcf9d3b0f3d4","created_at":"2026-02-22T12:04:31Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Replace the showStatus function body in cmd/gromit/main.go:220-266 with a call to runner.PrintStatus(gromitDir, cfg, os.Stdout, nil). Remove the old manual status.json reading and field-by-field printing. Verify acceptance tests pass: go test -tags acceptance ./internal/runner/acceptance/ -run Status. See .gromit/reports/debug-20260222-120033.md","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-cwk4z","is_template":0,"issue_type":"bug","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":0,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Update gromit status CLI to call runner.PrintStatus","updated_at":"2026-02-22T17:50:08Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Completed","closed_at":"2026-03-05T15:37:30Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"21481f167ba53464199d40646aa36b958393f5b41f66f0f050c1433b2e61e0f8","created_at":"2026-03-05T15:28:05Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"internal/events/types_test.go:139 checks len(eventTypes) \\!= 36 but the error message says 'Expected 35'. Also, GateReadinessBlockEvent (defined in types_gate.go:49) is missing from both this test and the specEventCases helper, so new event types can be added without uniqueness or timestamp verification.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-cx22d","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Fix event type count mismatch in types_test.go","updated_at":"2026-03-05T15:37:30Z","waiters":"","wisp_type":"","work_type":""} @@ -868,6 +878,8 @@ {"acceptance_criteria":"Replace interface{} with typed signatures in pipeline interfaces","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-21T07:22:57Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"7015c7ded182cfcee5f6339b71a35b5b5c3f92c21b66feafd4167cad7f2442ae","created_at":"2026-02-13T05:35:45Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"internal/pipeline/pipeline.go defines ClaudeClient.Run() returning interface{}, BeadClient methods returning interface{}, PromptRenderer methods accepting interface{}, and LogWriter.Write accepting interface{}. This leads to unsafe type assertions at runtime (e.g., pipeline.go:213 asserts map[string]interface{} from ClaudeClient.Run result). The type assertion at line 213 will fail at runtime if the ClaudeClient implementation returns anything other than map[string]interface{} with exact key names 'Success', 'Output', 'ExitCode'.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-d0n0","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Replace interface{} with typed signatures in pipeline interfaces","updated_at":"2026-02-21T07:22:57Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"- [ ] New section added to PROMPT_build.md between \"Instructions\" and implementation steps\n- [ ] Instructs Claude: \"If this task requires changes across more than 3 unrelated areas, or would need more than ~200 lines of new code across multiple packages, output SCOPE_TOO_LARGE: followed by a brief explanation and a suggested breakdown into 2-4 sub-tasks, then exit\"\n- [ ] Instruction is clear that Claude should STOP and not attempt partial work\n- [ ] Suggested breakdown format specified (numbered list of sub-task titles)\n- [ ] Template still renders correctly with existing prompt.Renderer","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-05T21:00:17Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"974f6d80f3eac198576ed2c232890b5745d5b8fa7d57167ce967ad4fe3c8b37c","created_at":"2026-02-05T19:44:24Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Update PROMPT_build.md to instruct Claude to self-assess task scope before starting work. If the task is too large for a single iteration, Claude should output a special marker instead of attempting it.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-d25","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add scope-awareness instruction to build prompt template","updated_at":"2026-02-07T18:29:38Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"invocation_timeout_acceptance_test.go uses config.ModelsConfig (not config.ModelConfig)\nworktree_merge_acceptance_test.go unused config import removed\nAll acceptance tests compile under go test -tags acceptance -run '^$' ./...","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-22T16:49:02Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"c0559bf490da83bc2aa2fa3df59786b1d27af989477056ea95ded48279e2d087","created_at":"2026-02-22T12:01:21Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Three acceptance test compilation fixes found during runner cleanup review: (1) invocation_timeout_acceptance_test.go:34 uses config.ModelConfig but type is config.ModelsConfig, (2) worktree_merge_acceptance_test.go:13 has unused config import, (3) verify all acceptance tests compile under -tags acceptance. The larger PrintStatus and format.go restoration work is tracked in gromit-jetjq and gromit-cr50o.1/2/3 respectively.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-d2aj2","is_template":0,"issue_type":"bug","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":0,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Fix acceptance test compilation errors (ModelsConfig typo, unused import, build tag verification)","updated_at":"2026-02-22T16:49:02Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"d6f36344680e7f7be85b153dcfa5329294f5c67bd12e037404f6980d1a706f7e","created_at":"2026-03-07T20:41:50Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Modify cmd/gromit/run2.go to create a Router instance from cfg.Providers and cfg.Routing. Register LLMProvider implementations (Claude, Codex, others) by provider name. Pass the router to SpecLoop and BeadLoop constructors.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-d2rpx","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"open","target":"","timeout_ns":0,"title":"Instantiate and wire Router in run2 command","updated_at":"2026-03-07T20:41:50Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"66b517bf8ed4941bfedf58a4b92426ed11d6fe1c72f0a10eb6dc3016889d9d3f","created_at":"2026-03-07T20:41:52Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add integration tests to internal/v2/loop/bead_loop_test.go: mock router + stages, verify provider/model in StageRequest per phase, verify tier bumps on retry, verify router re-called with higher tier.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-d4134","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"open","target":"","timeout_ns":0,"title":"Add integration tests for bead loop routing and escalation","updated_at":"2026-03-07T20:41:52Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"Comma-separated non-zero counts in lifecycle order\n(X this run) only when HasRunInfo=true AND ClosedThisRunCount \u003e 0\nAll-zero case displays Beads: none\nReady bead IDs still appear below","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-11T12:13:36Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"55b1bce340eef4a8c49e794188ccf306c431d53bfb2e4142e7f0b6e616fa26b8","created_at":"2026-02-11T07:42:16Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Update formatPipeline in internal/runner/format.go to build comma-separated list of non-zero bead counts in lifecycle order (ready, in-progress, blocked, deferred, closed). Append (X this run) to closed count when HasRunInfo is true and ClosedThisRunCount is greater than zero. Preserve ready bead ID list display below the counts line.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-d46r","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Rewrite formatPipeline for status breakdown","updated_at":"2026-02-11T12:13:36Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Decomposed into smaller beads per RULES.md scope constraints (3+ files, cross-cutting refactor)","closed_at":"2026-02-10T16:51:03Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"85011c4f28f01d3c78f7bcb616edd1f64b4f090540b59ae3291c265bb93ffcfd","created_at":"2026-02-08T07:36:07Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"The claudeRunnerAdapter struct is copy-pasted in 3 locations: cmd/gromit/review.go, internal/runner/runner.go, and internal/retro/retro.go. All three convert claude.Result to learnings.Result but differ slightly (review.go lacks nil-result check). Extract into a single definition in internal/learnings/filter.go or a new internal/claude/adapter.go, then update all three call sites to use the shared type. Also consolidate the hardcoded projectName/projectDesc strings (which vary between call sites: 'runs the Gromit loop correctly' vs 'runs the Gromit loop with fresh context on each iteration') into a single constant or config field.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-d6sl","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Consolidate triplicated claudeRunnerAdapter into shared package","updated_at":"2026-02-10T16:51:03Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"SKILL.md contains complete orchestrator instructions with dashboard and all dispatch modes\nPlaceholder markers present for REFINE_SKILL_CONTENT, PLAN_SKILL_CONTENT, and DECOMPOSE_SKILL_CONTENT\nDashboard instructions read backlog.jsonl, scan specs/plans, and run bd ready","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-07T05:32:30Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"c1478c99e035da7bab963aecc6342b00288562fc053b473bc88be92cf2b9a975","created_at":"2026-02-07T04:50:46Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Create skills/gromit-orchestrator/SKILL.md with frontmatter, dashboard instructions, and stage dispatch instructions. Include placeholder markers for refine/plan/decompose skill content inlining.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-d6y","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Create orchestrator skill template","updated_at":"2026-02-07T18:29:38Z","waiters":"","wisp_type":"","work_type":""} @@ -1009,6 +1021,7 @@ {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-03-04T09:13:20Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"f66f5b2585e35df2a1fbe2848b2768765eae420f910918010a1b89de261df4bd","created_at":"2026-03-03T21:48:25Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add StartCommit string field to pipeline.Input and TrivialAutoFixed bool field to pipeline.Output in internal/pipeline/stage.go. Both fields need doc comments explaining their purpose.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-euxuu","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add StartCommit and TrivialAutoFixed to pipeline types","updated_at":"2026-03-04T09:13:20Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"interactive-state.json persists pending_worktree_branches array and loads cleanly when absent\nAdd/remove helpers are deduplicated and thread-safe under file-locking\nExisting state tests pass with backward compatibility preserved","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-20T19:03:35Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"d66d41bf1ad13ae89cb99140f6f018fbd832d8e4a02256a0dedc21cec761bcb0","created_at":"2026-02-15T20:17:36Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add pending_worktree_branches field and helper methods to internal/state/interactive_state.go for add/remove/list operations with file-locking safety. Update internal/state/interactive_state_test.go to verify JSON persistence, backward compatibility, and deduplication.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-evne","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add interactive state pending branch tracking","updated_at":"2026-02-20T19:03:35Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"truncate handles maxLen \u003c 3 without panicking\nMulti-byte UTF-8 characters are not split mid-sequence\nTests cover edge cases for small maxLen and non-ASCII input","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-07T20:32:55Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"c6ea67841d986b3c9a8038a5941eef7be27046c7820a89598bea9f38225a6d46","created_at":"2026-02-07T06:02:53Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"In internal/pipeline/status.go line 169, truncate will panic with a negative slice index if called with maxLen \u003c 3. Currently only called with 50, but should be guarded. Also operates on bytes not runes, which will corrupt multi-byte UTF-8 characters in user-supplied backlog text. Fix both issues.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-evq","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add truncate guard for small maxLen and UTF-8 safety","updated_at":"2026-02-07T20:32:55Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"29fedf77ab50b421d564edcbca08ce2fcd2413897a6b8e0c800f8527d76e645f","created_at":"2026-03-07T20:41:49Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Implement the Router.Select() method with 3-layer selection: phase preferences from routing.phase_preferences config, ratio balancing across providers by invocation count for 'any' phase work, and fallback with cooldown on usage-limit errors. MarkUnavailable and RecordInvocation manage provider state.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-ew1l8","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"open","target":"","timeout_ns":0,"title":"Implement Router ratio balancing and phase preferences logic","updated_at":"2026-03-07T20:41:49Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"Wire circuit-breaker into Router and runner invocation path","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"stale: db reset","closed_at":"2026-03-02T19:42:38Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"e7a213f782c5eddeb3cf808c4651367b6f3a28f1c01b3a82bebe1722fa96b8cd","created_at":"2026-02-18T20:49:26Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add CircuitBreaker field to Router. In selectByRatio(), call cb.EffectiveRatio() to get adjusted ratios when circuit-breaker is enabled. After each invocation in the runner, call router.RecordOutcome(providerName, failureCategory). Initialize circuit-breaker from config in buildRouterAndLearningsProvider(). When circuit_breaker config is absent or disabled, Router behaves as today. Spec: routing-ratio-reliability-review Part 3.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-ewhf4","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Wire circuit-breaker into Router and runner invocation path","updated_at":"2026-03-02T19:42:38Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"Test sets content to literal hello\\nworld using raw string\nVerifies file contains hello\\nworld\\n without newline interpretation\nExisting TestFakes_ClaudeWriteFile regression test still passes","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-07T08:12:07Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"65ed2e9f1b9652598da658cbac7bf57caeb31856079b9d0e16959f86a9a09776","created_at":"2026-02-07T05:43:45Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add TestFakes_ClaudeWriteFile_BackslashContent to test/contracts/fakes_integration_test.go that verifies content with literal backslash-n sequences is written without escape interpretation. Use Go raw string literal for test input.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-ewl","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add backslash content fidelity test","updated_at":"2026-02-07T18:29:38Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"1. Run() accepts a deadline time.Time parameter and stops starting new beads after the deadline passes\n2. Zero-value deadline means no time limit (backward compatible)\n3. Deadline check is positioned after the iteration limit check and before fetching the next bead","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-06T11:24:10Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"a26e55daf917d248b1731e52316130729f0882609bc85a94e54203e872ba7a15","created_at":"2026-02-06T10:01:00Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add a deadline parameter to Runner.Run() and check it at the top of the loop. Change the Run() signature from Run(ctx context.Context, maxIterations int, dryRun bool) to Run(ctx context.Context, maxIterations int, deadline time.Time, dryRun bool). Add a deadline check right after the existing maxIterations check (line 174) in internal/runner/runner.go: if !deadline.IsZero() \u0026\u0026 time.Now().After(deadline) { r.log(\"Time budget expired, stopping\"); break }. The check position means the current bead always finishes before the loop exits.","design":"See docs/plans/2026-02-06-time-budget-design.md. The deadline check goes on line 178 (after the maxIterations break on line 176). Log message: 'Time budget expired, stopping'. This is the only change to runner.go.","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-eww","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add deadline check to runner loop","updated_at":"2026-02-07T18:29:38Z","waiters":"","wisp_type":"","work_type":""} @@ -1023,6 +1036,7 @@ {"acceptance_criteria":"When `RunFn` is set, `(*Client).run()` uses it and does not enter subprocess execution.\nWhen `RunFn` is nil, `(*Client).run()` still invokes the subprocess flow with the same command/binary and `Dir` behavior as prior code.\nNo conditional logic in `run()` references any removed or legacy hook field.","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-20T04:40:16Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"cd9d8508519e1b6c640099206af78d220f9545c9b5905789718621881df7aa21","created_at":"2026-02-20T04:34:27Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Refactor `(*Client).run()` so it checks only `RunFn`; when `RunFn` is nil it must execute the subprocess path exactly as before.\n\nAcceptance criteria:\n- When `RunFn` is set, `(*Client).run()` uses it and does not enter subprocess execution.\n- When `RunFn` is nil, `(*Client).run()` still invokes the subprocess flow with the same command/binary and `Dir` behavior as prior code.\n- No conditional logic in `run()` references any removed or legacy hook field.\n","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-f2ng8.2","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Make Client.run consult only RunFn before subprocess","updated_at":"2026-02-20T04:40:16Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"A subprocess failure (`*exec.ExitError`) still returns an error that includes wrapped stderr context exactly as before.\nNon-exit errors and successful subprocess runs retain prior return behavior.\nRegression tests covering fallback subprocess semantics pass.","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-20T04:43:34Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"7251ff57557dfedd59c0f68bffd6a00ccd9fe09322ce3ce5eacb4b94455dc496","created_at":"2026-02-20T04:34:28Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add/update tests around `(*Client).run()` fallback execution to verify stderr augmentation on non-zero exits remains unchanged after the hook consolidation.\n\nAcceptance criteria:\n- A subprocess failure (`*exec.ExitError`) still returns an error that includes wrapped stderr context exactly as before.\n- Non-exit errors and successful subprocess runs retain prior return behavior.\n- Regression tests covering fallback subprocess semantics pass.\n","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-f2ng8.3","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Lock in subprocess error wrapping behavior","updated_at":"2026-02-20T04:43:34Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Stuck after 3 failures. Closing to recreate with explicit before/after code diff.","closed_at":"2026-02-19T16:47:31Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"b966a86b8df475ed3d0ca2faf5db2b261b9022f2a42c3749feb9a482fd0b736a","created_at":"2026-02-19T14:10:59Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"cmd/gromit/refine.go:156 -- createRefinePipeline returns nil on error but the caller at line 61 dereferences without nil check, causing a runtime panic if backlog.NewFile fails. Change signature to return (*pipeline.Pipeline, error) and propagate the error, matching the pattern used by other commands.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-f37u5","is_template":0,"issue_type":"bug","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":0,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Fix nil pointer dereference in createRefinePipeline","updated_at":"2026-02-19T16:47:31Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-03-07T21:09:44Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"5bb297a354df2fbeb93572c6aa6e641e3ef1bc931eb6cea6e0d0ef87ff94491b","created_at":"2026-03-07T20:41:51Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Modify internal/v2/prompt/assembler.go: implement loadProjectContext(bead BeadInfo) that scopes CLAUDE.md to packages touched by current bead. Use v1's applyScopedClaudeContext() logic for package filtering.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-f4ouz","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add scoped CLAUDE.md loading to PromptAssembler","updated_at":"2026-03-07T21:09:44Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"Add compile-time interface checks for adapter types","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"stale: db reset","closed_at":"2026-03-02T19:42:31Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"807de4ce03c5da2e1f9048b9f219a20aeadc86d9e443904658f9daf642e09bc9","created_at":"2026-02-18T11:16:30Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Multiple adapter types in cmd/gromit/ (claudeClientAdapter, beadClientAdapter, cliAgentResolver, cliPromptRenderer, etc.) and internal/runner/adapters.go (routerAdapter, successLearningRouterAdapter, etc.) implement interfaces but have no compile-time checks. Also callbacks.go lines 411-412 use 'var _ InterfaceType = nil' instead of '= (*ConcreteType)(nil)'. Per project rule: 'Interface files must include compile-time checks.'","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-f4q0","is_template":0,"issue_type":"bug","last_activity":null,"metadata":"{}","mol_type":"","notes":"Experiment: demoted P1→P2 (haiku). Mechanical: add var _ Interface checks. Tracking tier-demotion-2026-02-20.","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":4,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add compile-time interface checks for adapter types","updated_at":"2026-03-02T19:42:31Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"grep -ri ralph internal/runner/format_test.go returns no matches\ngo test ./internal/runner/... passes","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-07T18:51:17Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"b1e513ebc8f6a28745ae09def0434ed0f63e8bbd0e7c918f628624d6bf03242a","created_at":"2026-02-07T18:27:23Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Replace hardcoded test fixture bead IDs in internal/runner/format_test.go with gromit- prefixed equivalents. Update ralph-runner-abc123 to gromit-abc123 and gromit-2pm to gromit-ja5m, and update corresponding expected output strings.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-f4t","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Update format_test.go test fixtures","updated_at":"2026-02-07T18:51:17Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"ATDD fallback triggers for startup errors (not just transport disconnect)\nFallback decision logs include primary provider, fallback provider, and failure class\nProvider marked unavailable with cooldown for both transport and startup failures","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-21T01:51:40Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"20af15a40fd9cf8b544f88485bcf290e5b24b3d28dd355f545254e0ff428c979","created_at":"2026-02-15T19:56:44Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Extend codex-transport-fallback in internal/runner/callbacks.go to trigger on startup errors. Add FailureCategoryStartupError constant to internal/provider/codex.go. Update classifyCodexFailure to detect startup patterns. Create isATDDFallbackEligible helper checking transport_disconnect OR startup_error. Add structured log lines with primary/fallback provider/model and failure class. Add tests to internal/runner/callbacks_test.go.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-f6bo","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Extend ATDD fallback for startup errors and structured logging","updated_at":"2026-02-21T01:51:40Z","waiters":"","wisp_type":"","work_type":""} @@ -1049,6 +1063,7 @@ {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-18T15:56:51Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"7ac12aa4ca462e90c8beda5a69f5ee91471828cea0454dd0e9c5b3f3ee1a24dd","created_at":"2026-02-18T11:16:34Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"cmd/gromit/epic_test.go has ~14 instances of t.Skip('External dependencies not available...') or t.Skip('bd binary not available...'). These tests should either be behind //go:build acceptance tag, have the dependency mockable, or be deleted if they cannot run in CI.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-fgqy","is_template":0,"issue_type":"bug","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Clean up epic_test.go permanently-skipped tests","updated_at":"2026-02-18T15:56:51Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"Test proves refactor timeout followed by successful validation in separate context\nTest proves validation timeout classification independent of refactor timeout\nTest proves default fallback behavior when phase timeout config is omitted","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-18T14:07:07Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"d3351de6652caa7e4971acf50228820372b46ca221ce8024a163566b5bb4247a","created_at":"2026-02-16T18:12:01Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Expand internal/runner/process_methodology_context_test.go with integration-style tests proving refactor timeout followed by successful validation, validation timeout classification independent of refactor timeout, and default fallback behavior when phase timeout config is omitted. Expand methodology/methodology_test.go and config/config_test.go with matching scenarios using fake callbacks and short in-memory timing controls.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-fgty","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":0,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add end-to-end timeout isolation regression tests","updated_at":"2026-02-18T14:07:07Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-03-07T13:33:51Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"b204c3e1ddfe6fbaad74b2139e38f0398bd474ead5512bbdba206c73c6e707a5","created_at":"2026-03-07T11:41:44Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add StageCommitter interface and field to BeadLoop, wire CommitStage calls after Build, Validate, and Review stages, and remove the legacy commitBeadWork method. Update bead_loop.go and bead_loop_test.go with unit tests using a recording fake to verify correct calls.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-fh4cu","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add StageCommitter to BeadLoop and replace legacy commitBeadWork","updated_at":"2026-03-07T13:33:51Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-03-07T21:17:47Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"c1fc543f7a11108460e7e227a130bb6f3bd1baa408cb469eb355f2e4ecc0b928","created_at":"2026-03-07T20:41:51Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Modify internal/v2/loop/bead_loop.go: when gate returns DecisionBlock, re-queue the bead to end of current pass instead of failing. After a full pass with zero progress (all remaining beads blocked), stop with descriptive error.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-fhp2q","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Implement gate re-queue logic in bead loop","updated_at":"2026-03-07T21:17:47Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Completed","closed_at":"2026-03-06T02:02:35Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"7e6482f9f5531b2cd4ddb3517a19f8974520015a74a08cc32b4f37e43a1c7f24","created_at":"2026-03-05T20:31:24Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Extend internal/v2/loop/bead_loop.go to track and enforce a generation cap. Record start generation (highest gen:N at loop entry). When any bead generation reaches start_generation + cap (configurable, default 3), prevent Review and Decompose from enqueueing new beads. Emit GenerationCapReached event. Return CapHit signal in BeadLoopResult. Update bead_loop_test.go.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-fj1ad","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add generation cap enforcement to bead loop","updated_at":"2026-03-06T02:02:35Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"Repetitive scenario groups converted to table-driven tests\nTotal line count decreases while preserving scenario coverage\nTests remain readable and deterministic","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-16T02:04:35Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"febc3d95d78bc6c54180b46cefd1ae9e1a63c37b07249c32e16ab287dff49094","created_at":"2026-02-16T01:08:18Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Consolidate repetitive setup/assert patterns in cmd/gromit/review_spec_validation_acceptance_test.go into shared setup helpers and table-driven tests. Remove duplicated fixtures where test structure only differs by input/expected outcome.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-fj9i","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":0,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Table-drive cmd acceptance scenarios","updated_at":"2026-02-16T02:04:35Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-23T14:06:57Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"013f941497c592433afeafbb768f1ae8cb6af720d23242730a51b4e682c6b06b","created_at":"2026-02-23T13:50:10Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Build currently has local nextTierFromChain fallback plus Config.NextEscalationTier, which duplicates escalation policy and risks divergence. Refactor to one shared escalation path, keep legacy tier/model compatibility via config accessors, and add tests for disabled escalation, tier chains, and legacy-model chain entries.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-fjxy1","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Unify build escalation to a single tier-advance path","updated_at":"2026-02-23T14:06:57Z","waiters":"","wisp_type":"","work_type":""} @@ -1264,6 +1279,7 @@ {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Replacing — need to split into CLI launcher + skill (two separate beads)","closed_at":"2026-02-06T07:43:36Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"02517c24e249c0dc1154a5054509cb3754f651de0a12330b05b1ab1148ad1850","created_at":"2026-02-06T07:41:56Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"A Claude Code skill for walking the backlog.jsonl and brainstorming each idea into beads.\n\nLocation: skills/ralph-refine/SKILL.md (in ralph-runner repo, or as a plugin)\n\nThe skill:\n1. Reads .ralph/backlog.jsonl\n2. For each idea, brainstorms it conversationally with the user (iterative, like brainstorming skill)\n3. Asks questions one at a time to understand the idea\n4. Proposes 2-3 approaches with tradeoffs\n5. Decomposes into bite-sized beads following sizing guidelines\n6. Creates beads via bd create after user approval\n7. Removes processed items from backlog.jsonl\n\nUses opus for the Claude model.\nFollows superpowers skill structure.\nReferences superpowers:brainstorming methodology for the conversational flow.\n\nAcceptance criteria:\n1. SKILL.md exists with proper frontmatter\n2. Walks backlog items interactively\n3. Creates properly-sized beads with acceptance criteria\n\nKey files: skills/ralph-refine/SKILL.md (new), .ralph/backlog.jsonl (reads)","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-jsb","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Create ralph-refine Claude Code skill","updated_at":"2026-02-07T18:29:38Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"UpdateGlobalStats creates file on first call with initial run stats\nUpdateGlobalStats merges correctly on subsequent calls, adding to existing per-model totals\nWrite uses atomic temp-file plus os.Rename pattern to prevent corruption","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-11T14:42:03Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"6d2ec39bfb4254e0adc1fb22b8e91ea5290d8d3f6e8c61064d0496a686662f5e","created_at":"2026-02-11T08:24:54Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add UpdateGlobalStats function to internal/logger/globalstats.go that merges run ModelStats into global totals using read-modify-write with atomic temp-file-then-rename pattern. Handle missing file on first call.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-jsta","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Implement UpdateGlobalStats with atomic writes","updated_at":"2026-02-11T14:42:03Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"runAcceptanceTests uses router.Select() with phase=\"build\" and tier from selectTier\nUsage limit errors trigger MarkUnavailable and retry\nATDD acceptance test execution works correctly through provider interface","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-11T20:29:58Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"12f5cb84d5b41864c6bda89e27e69810500efcb8af5eefe1e5a2c1ea00e9fcca","created_at":"2026-02-11T09:27:33Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Convert runAcceptanceTests in internal/runner/process.go from r.claude.StreamRun() to router-based call with phase=\"build\", tier=selectTier(bead). Implement usage limit detection and fallback retry logic matching executeClaudeInvocation pattern.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-juyb","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Convert runAcceptanceTests to Router","updated_at":"2026-02-11T20:29:58Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-03-07T21:23:30Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"994be0fa6c387e21f57e19be67b816ccd47a5d5d0bc2ef238fa2189c4dd9bfbe","created_at":"2026-03-07T20:41:49Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Create the internal/v2/routing/ package and define the Router struct with Select(phase, tier) -\u003e (LLMProvider, model, error), MarkUnavailable(name), and RecordInvocation(name) methods. This is the foundation for multi-provider routing.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-jv6ns","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Create Router interface and stub in v2/routing","updated_at":"2026-03-07T21:23:30Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-07T20:53:26Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"b65d94475cc69cbb52b0523d7c297e54a2619e4893b59ff41bac701bcfeb0e20","created_at":"2026-02-07T10:14:22Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"test/e2e/e2e_test.go line 40 uses defer os.RemoveAll(tmpDir), but line 109 calls os.Exit(exitCode) which kills the process without executing deferred functions. The contracts package handles this correctly by calling os.RemoveAll explicitly before os.Exit. Apply the same pattern.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-jva","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Fix E2E TestMain temp directory leak","updated_at":"2026-02-07T20:53:26Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"Run summary aggregation structures and builder logic\nRun-scoped JSONL filtering by run identifier\nDeterministic serialization/writer for summary artifact output\nUnit tests for determinism, required metric fields, run scoping, and malformed input handling","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"stale: db reset","closed_at":"2026-03-02T19:42:39Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"06fa1308c6b194da59fa1abee084b1a3df10e0bda3891dd17e9f551ceea1ad83","created_at":"2026-02-24T04:25:15Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Create internal/logger/run_summary.go and internal/logger/run_summary_test.go to aggregate run-scoped JSONL data into a deterministic summary artifact. Include per-phase tokens, cost, latency, retry/validation outcomes, totals, and mode usage indicators derived from telemetry records.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-jw28c","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Build deterministic run summary aggregator","updated_at":"2026-03-02T19:42:39Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-03-06T10:09:07Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"f6eec9927621b3bf637589ad93d71fcdabe269380e9eb4386db987e2823908f3","created_at":"2026-03-05T20:31:24Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Create internal/v2/loop/bead_loop.go with the inner bead loop. Uses DAG resolver to pick beads in dependency order. Runs Gate-\u003eBuild-\u003eValidate-\u003eReview-\u003eEpilogue per bead. Gate Skip/Block and Build/Validate Fail all short-circuit to Epilogue failure path. Emits BeadStarted and BeadCompleted lifecycle events. Loops until no more eligible beads. Add bead_loop_test.go using fake adapters.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-jx4fi","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Implement bead loop core stage sequencing","updated_at":"2026-03-06T10:09:07Z","waiters":"","wisp_type":"","work_type":""} @@ -1274,6 +1290,7 @@ {"acceptance_criteria":"IsMethodologyActive returns true when atdd:true label present regardless of global default\nIsMethodologyActive returns false when atdd:false label present regardless of global default\nIsMethodologyActive returns globalDefault when no matching label exists","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-07T08:31:31Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"ca35a9aa1d7276225744b549c56860cd1ab28d93a3188d0d8c07feb2854ea1f0","created_at":"2026-02-07T05:48:27Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Implement IsMethodologyActive function in internal/bead/bead.go that checks for methodology:true and methodology:false labels, returning label value if present or globalDefault if not. Follows existing HasLabel pattern.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-jyo","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add methodology label helper to bead package","updated_at":"2026-02-07T18:29:38Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"Pipeline decompose tests that currently fail only due to output_missing mismatch are green\\nValidationStats fields (Attempts, ViolationCount, SucceededAfterRetry, Improved) match retry-path expectations\\nNo bead creation occurs when batch contract violations remain at retry cap","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-23T09:09:44Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"02db23b1067033fe06664419010739cc55b2fe0b762b757d67a614e52bd14e6d","created_at":"2026-02-23T09:02:01Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Recent decompose validation enforces expected_outputs on all candidates, but several pipeline decompose paths and fixtures still emit bead defs without expected_outputs. This causes persistent output_missing violations, extra retries, and incorrect ValidationStats in retry-related tests. Align prompt contract, fixture payloads, parser defaults, and retry accounting so behavior is intentional and stable.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-jysme","is_template":0,"issue_type":"bug","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":0,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Restore expected_outputs contract parity in decompose pipeline retry loop","updated_at":"2026-02-23T09:09:44Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"No remaining references to the old beadContext type\ngo build ./internal/runner/... compiles cleanly\ngo test ./internal/runner/... -count=1 passes with no assertion changes","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-13T17:10:18Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"cffbfbb5b58155f8b65a068a758ee77975137b09b42dec50f890a184e9b4833e","created_at":"2026-02-13T15:52:27Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Replace all uses of the local beadContext struct with runtypes.BeadContext. Delete the beadContext struct definition from process.go:27-52. Update setupBeadContext() to return *runtypes.BeadContext. Promote all field references across process.go and runner.go (~150 sites): bc.bead→bc.Bead, bc.model→bc.Model, bc.tier→bc.Tier, bc.result→bc.Result, bc.parent→bc.Parent, bc.promptCtx→bc.PromptCtx, bc.buildPrompt→bc.BuildPrompt, bc.startCommit→bc.StartCommit, bc.iteration→bc.Iteration, bc.retriesThisModel→bc.RetriesThisModel, bc.totalRetriesThisBead→bc.TotalRetriesThisBead, bc.maxRetries→bc.MaxRetries, bc.maxRetriesPerBead→bc.MaxRetriesPerBead, bc.parentCtx→bc.ParentCtx, bc.beadTimeout→bc.BeadTimeout, bc.runDeadline→bc.RunDeadline, bc.scopeEstimate→bc.ScopeEstimate, bc.buildProvider→bc.BuildProvider. Update all function signatures accepting/returning *beadContext.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-jz5w","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":0,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Migrate beadContext to runtypes.BeadContext","updated_at":"2026-02-13T17:10:18Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-03-07T21:26:46Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"03c6f96e988adae67e2b6daa4ab8cb6333d0ff8f2735ec6e5b178f9e54ae8279","created_at":"2026-03-07T20:41:49Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Create tier.go with tier resolution logic: check methodology.phase_models[phase] for override, fall back to priority/labels via BuildTierForStrategy(), map abstract tier to provider-specific model name. This drives tier-aware model selection.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-jzbe3","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Implement tier resolution in internal/v2/routing/tier.go","updated_at":"2026-03-07T21:26:46Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"Tier field on DecomposeInput struct\nUpdated Pipeline.Decompose() using input.Tier\nRemoved or unexported decomposeModel constant\n\"medium\" default fallback in Pipeline.Decompose() when Tier is empty\nTest: Decompose passes explicit tier string to the client\nTest: Decompose uses \"medium\" when Tier is empty","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-22T20:30:10Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"0dd20fd01b581716fcbccf116068e91a79edeea887f72471bdf0ed586232df8f","created_at":"2026-02-22T19:25:25Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add Tier string field to DecomposeInput in internal/pipeline/types.go. Update Pipeline.Decompose() in internal/pipeline/decompose.go to pass input.Tier to the Claude client instead of the hardcoded decomposeModel constant, defaulting to \"medium\" when Tier is empty. Remove the decomposeModel constant.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-jzqqu","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add Tier field to DecomposeInput and use it in Pipeline.Decompose","updated_at":"2026-02-22T20:30:10Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"internal/pipeline/types.go","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-22T20:32:41Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"c61239aa06fe289495d8c8f70c7c70a64f7acbe05bd45c2f5d365e08cbd5ddc7","created_at":"2026-02-22T20:30:07Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-jzqqu.1","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add Tier to DecomposeInput","updated_at":"2026-02-22T20:32:41Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"internal/pipeline/decompose.go","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-22T20:35:32Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"f7a86c98f15e11572488f745091ae469fdf34f4afabd9184457ae1520d305add","created_at":"2026-02-22T20:30:08Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-jzqqu.2","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Use input Tier in Pipeline.Decompose with medium fallback","updated_at":"2026-02-22T20:35:32Z","waiters":"","wisp_type":"","work_type":""} @@ -1875,6 +1892,7 @@ {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-20T09:47:45Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"46a7ac132f7468ce8bf7a2df8b976932df756106b65ac5613985aa80c768a435","created_at":"2026-02-19T03:20:46Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"After the TDD cycle loop completes in internal/runner/process_methodology.go or internal/runner/run_iteration.go, populate bc.Result coverage fields from tracker state. When criteria are uncovered or untestable, add a bead comment via r.beads.AddComment with tracker.Summary. Log coverage summary to runner output. Add tests in the corresponding test file.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-u2pch","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Report coverage results to bead after TDD cycle loop","updated_at":"2026-02-20T09:47:45Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"All three build templates render Recent Validation Issues section when failures are present\nSection is completely omitted when no failures exist\nAll three templates include self-check guidance in their Instructions section","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-14T19:26:34Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"f4fe78260b0e55ccbb52373a0b58af6534d0838f29957a4e37e135d3d9ea964f","created_at":"2026-02-12T17:55:41Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add conditional Recent Validation Issues section to all three build templates (PROMPT_build.md, PROMPT_atdd_build.md, PROMPT_tdd_build.md) between Learnings and Project Context sections. Add self-check guidance to Instructions sections instructing agents to run validation commands before completing. Template-only changes with tests.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-u2zx","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Update build templates with validation failures section and self-check guidance","updated_at":"2026-02-14T19:26:34Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-10T11:43:18Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"f04820301edb53f56a2942001bc9d2cba7975e2174e395da69317c13dce38619","created_at":"2026-02-10T11:28:18Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Run gromit add for each skipped test that describes desired but unimplemented behavior:\n- run_scope_acceptance_test.go: scope flags (--spec, --epic) integration with runLoop()\n- review_scope_acceptance_test.go: 2 skipped tests about --spec flag priority and no-matching-beads handling\n- filtered_hash_eviction_acceptance_test.go: 2 skipped tests about single-save optimization and archived learnings\n\nThen delete cmd/gromit/run_scope_acceptance_test.go entirely (all 11 tests are t.Skip).\n\nAcceptance criteria:\n- Backlog ideas captured via gromit add for skipped test behaviors\n- cmd/gromit/run_scope_acceptance_test.go deleted\n- go test ./cmd/gromit/... passes","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-u3z9","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":0,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Capture skipped test ideas in backlog and delete entirely-skipped files","updated_at":"2026-02-10T11:43:18Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"14ace8318bf6ff1f8a0e75fa0d81554f508297cd4d354b63872e63c4e28a4b7a","created_at":"2026-03-07T20:41:53Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add tests to internal/v2/prompt/assembler_test.go: assemble full build prompt, verify size within expected range, ShapeReport shows what was trimmed and by how much.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-u4837","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"open","target":"","timeout_ns":0,"title":"Add integration tests for full prompt shaping pipeline","updated_at":"2026-03-07T20:41:53Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"go test ./... and go vet ./... and go build ./... all pass with no errors or warnings\nExisting JSONL files without prompt_diagnostics parse without errors producing nil diagnostics fields\nRendered prompt strings are identical before and after diagnostics instrumentation is added","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-19T23:09:49Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"186b345822dacc5b332a6c2a360894cb1056a0af5a1ab468dd09f4d661b50f2e","created_at":"2026-02-19T19:42:49Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Run go test ./..., go vet ./..., and go build ./... to confirm all quality gates pass. Verify existing JSONL files without prompt_diagnostics deserialize cleanly. Confirm prompt rendering output is byte-identical with and without diagnostics capture. Verify gromit stats (without --prompt-tokens) output is unchanged.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-u4aqq","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Final verification: all tests pass and backward compatibility confirmed","updated_at":"2026-02-19T23:09:49Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-03-06T13:55:23Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"4bec8252ddb2bf2a7ebebcc3162eee8b16423a8991ab864afcd7e2e946bcfb19","created_at":"2026-03-06T11:59:10Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Spec criterion 4 requires generating acceptance tests before bead execution. The accept stage runs after beads as a gate. The authoring phase is absent. Either implement a pre-bead acceptance authoring stage, or update the spec to document this as a deliberate design choice.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-u5w97","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add pre-bead acceptance test authoring or update spec","updated_at":"2026-03-06T13:55:23Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"stale: db reset","closed_at":"2026-03-02T19:42:30Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"7c1c3d54ec5e2b2eb9232b53646a2c4679bb04c13ef813872e6b3af4d7fc6525","created_at":"2026-02-13T12:29:13Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"TestMultipleRateLimitHits_WithoutInterveningEvent in internal/logger/stream_rate_limit_recovery_test.go asserts recoveryMs \u003c= 150 but has no lower-bound check. If the implementation regressed to always return 0, the test would still pass. Add a minimum assertion (e.g., recoveryMs \u003e= 10) to verify recovery is actually being measured, not silently returning zero.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-u6i7","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add lower-bound assertion to rate limit recovery test","updated_at":"2026-03-02T19:42:30Z","waiters":"","wisp_type":"","work_type":""} @@ -1919,6 +1937,7 @@ {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-22T07:47:55Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"42fb66ec807ef4f863c4c408f696fade4c5f7daa42e1d41df81da79d9b2eb2dd","created_at":"2026-02-22T06:34:13Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"In internal/pipeline/execute/build.go, update Build.Run() to detect when methodology=TDD AND config.Methodology.FreshContextPerCycle is true. When both conditions hold, delegate to the injected TDDCycleRunner instead of the single StreamRun() call. Add a tddCycleRunner field to the Build struct and a setter/constructor option to inject it. When TDDCycleRunner is nil and fresh-context is requested, fall back to the existing single-invocation TDD path. Return the aggregated PhaseMetrics in pipeline.Output.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-uslyx","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Wire TDD fresh-context detection into Build.Run() to delegate to TDDCycleRunner","updated_at":"2026-02-22T07:47:55Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-03-07T14:14:48Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"a17b7758413e5fff12cd3df4b30b58218437de45f48badef1a6913892b826f5b","created_at":"2026-03-07T05:55:34Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Create cmd/gromit/debug2.go that implements the debug2 command for diagnosing failures from preserved worktree branches. Implement helper functions: resolveDebug2Worktree to look up the spec worktree, readDebug2EventLog to parse JSONL events, findFailureEvent to identify failure points, and buildDebug2Prompt to assemble diagnostic context. Wire Cobra command with positional spec-name argument. Invoke LLM session in the worktree for diagnosis. Handle LEARNINGS entries and systemic recommendations.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-utelc","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Implement debug2 command for diagnostics","updated_at":"2026-03-07T14:14:48Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"stale: db reset","closed_at":"2026-03-02T19:42:30Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"d201eaabe8f3ebbbe92c0e52690258d4fa5c23e81ebae3e04574fd35409f5dde","created_at":"2026-02-13T05:35:36Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"internal/pipeline/decompose_test.go:682 has TestDecomposeWorkflow_RespectsContextCancellation with t.Skip('Context cancellation not yet implemented'). Per RULES.md: 'Do not write tests with t.Skip() for scenarios that can't run in the test environment. Every committed test must be runnable.' Either implement context cancellation support and the test, or delete the test and capture 'add context cancellation to Decompose' as a backlog item.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-uugq","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Remove t.Skip test in decompose_test.go for context cancellation","updated_at":"2026-03-02T19:42:30Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-03-07T21:28:53Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"6d822499634189752fd8da78a8bf4634bdd29cfc7d17a70fb58ad2a89ac0e9cc","created_at":"2026-03-07T20:41:52Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add tests to internal/v2/routing/tier_test.go: verify methodology.phase_models overrides are read correctly, abstract tier maps to correct provider models, fallback behavior works.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-uulh1","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add unit tests for tier resolution","updated_at":"2026-03-07T21:28:53Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-06T09:29:26Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"1cca933a014b05c8238b5c52bbecd541fff4bb8e89f07613fecc69321189ba18","created_at":"2026-02-06T07:58:04Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add extra newlines/separator between iterations for visual clarity during ralph run.\n\nImplementation:\n- Print a blank line or light separator between iterations in the run loop\n- Ensures each iteration's output is visually distinct\n\nAcceptance criteria:\n1. Visible separation between iterations in terminal output\n2. No separator before first iteration or after last\n\nKey files: internal/runner/runner.go","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-uv3","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add visual separation between iterations in run output","updated_at":"2026-02-07T18:29:38Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-07T18:23:55Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"364c2c8ed29804a533c1e4ff7bedac45acf522adcc9c3f82a8799e55c6fc87e8","created_at":"2026-02-07T03:16:47Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"The analyze prompt currently tells Claude to set learning to null if not 'generalizable'. This is too strict — task-specific patterns (like 'this package uses X convention' or 'tests in this area need Y setup') are valuable as provisional learnings. Change the prompt to: (1) encourage capturing any pattern that might help future tasks, (2) explicitly list examples of useful learnings (codebase conventions, test patterns, common gotchas), (3) only set null if the failure was truly one-off (e.g. typo). Keep the conciseness requirement (1-2 sentences). AC: prompt updated, examples added, null guidance narrowed.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-uvl","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Soften PROMPT_analyze.md learning extraction criteria","updated_at":"2026-02-07T18:29:38Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"Each failing criterion produces one bead with spec:\u003cspecName\u003e label and title 'Fix: \u003ccriterion name\u003e'\nBead description includes criterion name, evidence, and fix direction\nCreation is capped at 5 beads; additional failures are skipped with a log message","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-20T12:53:43Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"3cb731c6086bd554e3809db4ac53b7bcc413b0d6544867e99c0386ed6be23024","created_at":"2026-02-18T16:58:06Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Create internal/specgate/synthesize.go with BeadCreator interface and SynthesizeFixBeads function. Create internal/specgate/synthesize_test.go covering 0, 1, 5, and 6+ failure scenarios.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-uwag","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Implement SynthesizeFixBeads in specgate package","updated_at":"2026-02-20T12:53:43Z","waiters":"","wisp_type":"","work_type":""} @@ -1930,8 +1949,10 @@ {"acceptance_criteria":"internal/validate/complexity.go","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-23T03:28:21Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"6277ba1aac449e0ca79010f2a394cbb10b3e3b56231b0721e7bfa7339c7b50a2","created_at":"2026-02-23T02:05:36Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-uzrhu.1","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add complexity result model types and scoring entrypoint","updated_at":"2026-02-23T03:28:21Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"internal/validate/complexity.go","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-23T03:25:32Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"4db0ae807325918d03855f166747060bb2eca6e75fc7bea8e527bfea6494af5b","created_at":"2026-02-23T02:05:37Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-uzrhu.2","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Implement high-complexity heuristic signals and reason generation","updated_at":"2026-02-23T03:25:32Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"internal/validate/complexity_test.go","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-23T03:21:32Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"87187ea69459c3e7130cc2ebe6353e136a9e2f2e906c338d8655a686383b30ca","created_at":"2026-02-23T02:05:38Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-uzrhu.3","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add unit tests covering each complexity signal and classification outcomes","updated_at":"2026-02-23T03:21:32Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-03-07T21:32:30Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"abc3a3012109de29cb9082bd16866f169f8fbcc1897f5aecaed767ecf0179912","created_at":"2026-03-07T20:41:51Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Modify internal/v2/prompt/assembler.go: implement loadBaseInstructions(phase string) that filters RULES.md to phase-relevant sections. Apply per-phase character caps (build: 12800, red: 8500, etc.) based on phase parameter.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-v1j2u","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add phase filtering to rules loading in PromptAssembler","updated_at":"2026-03-07T21:32:30Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Subsumed by ralph-runner-dt4 (board) and ralph-runner-fqx (queue)","closed_at":"2026-02-06T07:09:32Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"d2ee1a7ca532daea88d748d09719c8010934170c56d89e322e7572295a65a5f3","created_at":"2026-02-06T06:54:30Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add a way to see all beads and a count of open beads from the ralph CLI. Could be an enhancement to 'ralph status' or a new subcommand. Should show:\n- List of open beads with priority and title\n- Total count of open vs closed beads\n- Current next-up bead","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-v1t","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add bead list and open count to CLI","updated_at":"2026-02-07T18:29:38Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-03-06T17:13:02Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"3ba9a649ac2f41473966988530a4451632c8acb178bdd2d099998efefc716c69","created_at":"2026-03-06T16:53:21Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"The v2 validate stage uses a noopValidationRunner that always returns nil. Need to create a real ValidationRunner implementation that executes shell commands (adapting v1's defaultCmdRunner from internal/runner/cmd_run.go). The ValidationRunner interface is simple: Run(ctx context.Context, command string) error. Wire it into loop.NewRun2LoopComponents replacing the noop stub.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-v4460","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"v2-run-loop","status":"closed","target":"","timeout_ns":0,"title":"Wire real validation command runner in v2 loop","updated_at":"2026-03-06T17:13:02Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-03-07T21:37:00Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"1ee6fb4357ef671b86c77ecaa14423352352478bbad9062949d4fed8e2948063","created_at":"2026-03-07T20:41:52Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add tests to internal/v2/loop/topo_test.go: DAG ordering respects DependsOn, cycle detection returns error, beads with no dependencies come first, stable tie-breaking.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-v46qz","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add unit tests for topological sort","updated_at":"2026-03-07T21:37:00Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-03-06T00:22:35Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"24ef1960a3c4f0fa0a81dbad70645bbf6ea50da7900e55588668e2085211fe2f","created_at":"2026-03-05T20:31:25Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Create internal/v2/adapter/tasktracker/bd.go implementing TaskTracker using bd CLI, adapting patterns from internal/bead/. NextBead queries bd for next open bead with dependency info. CreateBead creates bead with title, description, priority, gen:N label, and dependency declarations. CloseBead marks bead closed. QueryBeads filters by labels, status, parent. Add bd_test.go.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-v6h8e","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Implement bd TaskTracker adapter","updated_at":"2026-03-06T00:22:35Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"Extended diagnostics data structure for source-bucket attribution\nRender-method population of bucket fields by prompt type\nUpdated diagnostics tests validating bucket fields and compatibility semantics","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"stale: db reset","closed_at":"2026-03-02T19:42:39Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"4261e8fc8735cc46a51d17b4ff81838b96b4454d9c80599132d40b897c4dbd45","created_at":"2026-02-24T04:25:15Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Extend prompt diagnostics in internal/prompt/diagnostics.go and internal/prompt/render_methods.go to report prompt-size attribution by source buckets. Cover template, rules, learnings, tool output, and context payload contribution where available. Update diagnostics tests to assert stable bucketed output.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-v6q0g","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add prompt diagnostics source buckets","updated_at":"2026-03-02T19:42:39Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"Template renders experiment evaluation when active experiment exists\nTemplate includes experiment recommendation instructions in task section\nTemplate omits experiment section when no active experiment","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-08T01:15:47Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"e9636f89d2d96d3553bc4c5d781afacb000058215966c1712d5883ef61a0d853","created_at":"2026-02-07T23:32:16Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add Active Experiment Evaluation section to .gromit/templates/PROMPT_retro.md showing experiment details and baseline vs current metrics comparison. Update Task section to add Efficiency Analysis item with Five Whys guidance. Add experiment recommendation instructions. Make conditional on .Experiment field.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-v7ft","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add experiment evaluation to retro template","updated_at":"2026-02-08T01:15:47Z","waiters":"","wisp_type":"","work_type":""} @@ -1977,6 +1998,7 @@ {"acceptance_criteria":"Status output includes model performance section positioned between Health and Recommendation\nSection displays when project logs contain model stats data\nMissing or empty stats are handled gracefully without errors or empty sections","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-11T15:34:44Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"f760260f808fcad7cbff67f6a6752b7f6b098122f43fad958f03c4f86f6f5dd1","created_at":"2026-02-11T08:24:54Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Modify Runner.Status() in internal/runner/runner.go to call logger.ReadModelStats with r.cfg.Paths.Logs and display formatted model performance section between Health and Recommendation sections using formatModelPerformance. Handle errors gracefully.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-w0en","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Wire model stats into gromit status","updated_at":"2026-02-11T15:34:44Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-10T18:17:02Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"f4b69bdc4cb0a5c90357e5c9a1aa731f98182f0bb85ddae06706909bc1784b25","created_at":"2026-02-08T07:39:59Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"The retro template has conditional branches ({{- if .Experiment }}, {{- if .Efficiency }}, {{- if .RunStats.Total }}) that render different content depending on whether Experiment/Efficiency are nil. The populated-data test covers the non-nil path. This test covers the nil/empty path to ensure the template renders without error when no experiment is active.\n\nAdd a test in internal/retro/retro_test.go that:\n1. Writes the real PROMPT_retro.md template to a temp dir\n2. Calls renderPrompt with Experiment=nil, Efficiency=nil, and zero RunStats\n3. Asserts no error and the output does NOT contain experiment-related sections\n\nAcceptance criteria:\n- Test calls renderPrompt with nil Experiment and nil Efficiency\n- Test asserts no template execution error\n- Test verifies experiment metric sections are absent from output","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-w0lo","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add renderPrompt test covering nil Experiment branch","updated_at":"2026-02-10T18:17:02Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"Recovery loop respects validation phase context deadline and exits with phase timeout classification\nPer-command timeouts still trigger command-level timeout handling as before\nTests cover interaction between phase timeout cap and command timeout","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-18T13:21:44Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"cb35e5b8e75aef01a3061609956ab523df68da47bd7625a12e1887c42fb883cc","created_at":"2026-02-16T18:12:01Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Modify internal/runner/validation/runner.go to respect validation phase context deadline across full validation flow including recovery loop, while preserving per-command validation.command_timeout behavior. Update validation_test.go with tests covering phase timeout cap, command timeout interaction, and recovery loop deadline handling. Expand process_methodology_context_test.go with validation phase budget scenarios.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-w0z6","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":0,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Enforce validation phase timeout budget for recovery loop","updated_at":"2026-02-18T13:21:44Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"a4e66cb1566142452e25b6f941a25711e327d73e1e9c583284ef62ddd2d485f3","created_at":"2026-03-07T20:41:50Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Modify internal/v2/loop/bead_loop.go to call Router.Select(phase, tier) before each stage invocation. Populate StageRequest.Model and StageRequest.Provider fields. Handle escalation by incrementing RetryContext.EscalationLevel on failure and re-calling Router.Select with higher tier.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-w1200","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"open","target":"","timeout_ns":0,"title":"Wire Router into bead loop routing seam","updated_at":"2026-03-07T20:41:50Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-03-03T18:50:04Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"44f5682f3ca555a9d5aed1614acf173a06ace84455142174bdfe7daf7b435bdb","created_at":"2026-03-03T04:24:02Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Delete 5 files from internal/runner/ that contain only fresh-context wiring: callbacks_tdd.go, callbacks_tdd_test.go, callbacks_tdd_missing_outputs_test.go, tdd_pipeline_adapter.go, tdd_pipeline_adapter_test.go. These files depend on the tdd package deleted in the previous bead.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-w1dux","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Delete runner fresh-context adapter and callback files","updated_at":"2026-03-03T18:50:04Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"golang.org/x/sync appears in go.mod\ngo.sum is updated\ngo build ./... still passes","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"golang.org/x/sync/errgroup already in go.mod and imported in process.go","closed_at":"2026-02-08T15:43:23Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"80555839ddcb7476ea3c07d9bff67f0bd7aab315fa8984b5604d5ff40bf2073f","created_at":"2026-02-08T10:41:18Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Run 'go get golang.org/x/sync' to add errgroup to go.mod and go.sum. No code changes — dependency only.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-w1sv","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add golang.org/x/sync/errgroup dependency","updated_at":"2026-02-08T15:43:23Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-06T09:05:07Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"449e36ef274e5766d5487287ee4d0b3796f9eee220c584ea9f3f920d61f01a33","created_at":"2026-02-06T07:43:51Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Go CLI command that launches an interactive Claude Code session with feature context pre-loaded.\n\nUsage: ralph plan \"Add tmux integration\"\n\nImplementation:\n- Takes feature description as argument\n- Reads relevant codebase context (open beads, project structure)\n- Launches: claude --append-system-prompt \"Feature to plan: [description]. [context]. Use the ralph-plan skill.\"\n- Passes through stdin/stdout/stderr for interactive session\n- cmd.Stdin = os.Stdin, cmd.Stdout = os.Stdout\n\nAcceptance criteria:\n1. ralph plan launches interactive Claude Code session\n2. System prompt includes feature description and codebase context\n3. References ralph-plan skill in the prompt\n\nKey files: cmd/ralph/plan.go (new), internal/claude/claude.go (reference for exec pattern)","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-w1t","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"ralph plan CLI command: launch Claude Code with feature context","updated_at":"2026-02-07T18:29:38Z","waiters":"","wisp_type":"","work_type":""} @@ -1992,6 +2014,7 @@ {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-03-05T11:12:31Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"e8cd9956be0733ae6f3f3b46cc53f5e8851844ab7a596c5da689db16d3b9ec52","created_at":"2026-03-04T12:11:33Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"internal/bead/bead.go has ~90 lines of duplicated subprocess setup between runWithEnv (lines 504-548) and runWithEnvCombinedOutput (lines 609-652). Extract shared subprocess lifecycle into a helper that accepts an output-configuration parameter.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-w72th","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Deduplicate bead.Client runWithEnv and runWithEnvCombinedOutput","updated_at":"2026-03-05T11:12:31Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"Test verifies pruning stale hashes returns true and updates slice correctly\nTest verifies no-op case returns false and leaves slice unchanged\nTest verifies nil receiver and edge cases do not panic","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-08T09:54:26Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"3ce030519a9d4f7452a1baf403dd4f7e722d09505f31847475631ed54f696087","created_at":"2026-02-08T08:18:27Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add comprehensive unit tests for ReconcileFilteredHashes in internal/state/state_test.go. Cover pruning stale hashes, no-op when all match, empty current set prunes all, empty existing hashes, and nil receiver safety.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-w7cn","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add unit tests for ReconcileFilteredHashes","updated_at":"2026-02-08T09:54:26Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"IsValidateEnabled returns true when Validate is nil, respects explicit false\nSetDefaults sets MaxValidationRetries to 2 when zero\nYAML round-trips correctly","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-14T04:54:22Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"206b3ac0109861e63a811db23f6ea8b56e3c866f8368640e2846c526352ff059","created_at":"2026-02-12T10:02:23Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add DecomposeConfig struct to internal/config/config.go with Validate *bool and MaxValidationRetries int fields, IsValidateEnabled method, and wire into Config struct and SetDefaults. Extend internal/config/config_test.go for defaults and YAML parsing. Add decompose section to gromit.yaml with comments.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-w7qn","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add DecomposeConfig to config with tests","updated_at":"2026-02-14T04:54:22Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-03-07T21:40:04Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"cb301450ad10f6768479a16bbab5a77b8ad0ab52218419b55c1b7437efd7d48d","created_at":"2026-03-07T20:41:50Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add gap analysis logic to internal/v2/loop/spec_loop.go (or new internal/v2/gap/gap.go): diff worktree HEAD against last successful bead's commit, identify completed beads whose files have changed, flag them for revalidation.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-w83pd","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Implement gap analysis for resume","updated_at":"2026-03-07T21:40:04Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-03-07T06:13:50Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"924a126611b45e2811450a56750b43b101a4b086cd0b6fc9cb915c4a29aaa627","created_at":"2026-03-07T05:55:33Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Create StageCommitter in internal/v2/pipeline/stage_committer.go that wraps a GitCommitter interface and creates per-stage structured commits. Implement CommitStage method that calls git status, skips commits when nothing changed, and uses FormatCommitMessage to produce [bead:ID/stage/iter:N] or [spec/stage/iter:N] format messages. Include local GitCommitter interface definition to avoid circular imports.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-w8std","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Implement StageCommitter for structured git commits","updated_at":"2026-03-07T06:13:50Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"MaxConsecutiveSkips field added to LoopConfig struct with yaml:\"max_consecutive_skips\" tag\nsetDefaults() sets MaxConsecutiveSkips to 3 when value is 0\nField is parsed correctly from YAML configuration","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-08T01:27:14Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"839470f6513bbd713ea0a108008627bef62b5ec176997f6f9347e5c5a476d868","created_at":"2026-02-07T23:36:10Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add MaxConsecutiveSkips int field to LoopConfig struct in internal/config/config.go with yaml tag max_consecutive_skips. Add default of 3 in setDefaults() when value is 0, following the StuckBeadThreshold pattern at lines 229-230.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-w971","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add MaxConsecutiveSkips to config","updated_at":"2026-02-08T01:27:14Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"Tests verify defaults are set when git section is omitted from YAML\nTests verify explicit YAML git section parses correctly with auto_push: false\nTests verify IsAutoPushEnabled() returns expected values for nil, true, and false","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-07T23:40:15Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"acc89c841b9dcc3e67751158029586634dd4a3145b1bf3ae000182c9da41d228","created_at":"2026-02-07T22:34:44Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add table-driven tests to internal/config/config_test.go covering GitConfig defaults, YAML parsing with explicit git section, and IsAutoPushEnabled() helper method behavior. Test nil-safety and all config combinations.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-w9rs","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add GitConfig tests","updated_at":"2026-02-07T23:40:15Z","waiters":"","wisp_type":"","work_type":""} @@ -2034,7 +2057,7 @@ {"acceptance_criteria":"Client.Run returns timeout error for hung command\nTimeout behavior is consistent and reliable\nTest replaces document-only placeholder with real coverage","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"stale: db reset","closed_at":"2026-03-02T19:42:27Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"29eec3e2b535ec3f993bc055090c684c14fdbe1aad21788ed1fc53c6db357e4a","created_at":"2026-02-19T14:47:20Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Cover that claude Client.Run returns/handles timeout for hung command and times out consistently; current document-only test placeholder should be replaced by reliable coverage.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-x0dqh","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add integration coverage for claude Run timeout behavior","updated_at":"2026-03-02T19:42:27Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"Retro filters per-bead stats by bead ID set\nRetro filters log entries by bead ID set\nRetro without filter behaves identically to current behavior","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-10T10:14:42Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"e01408b4375af699401a5941d0aa0e0333261ff59aad2f52427af019a707b438","created_at":"2026-02-08T17:00:34Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Modify internal/retro/retro.go to accept optional bead ID filter set. Filter ReadPerBeadStats() results to only include stats for beads in the filter set. Filter ReadAllLogs() results to only include log entries where BeadID is in the filter set. When no filter is set, preserve current behavior.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-x316","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Update retro logic to filter by bead IDs","updated_at":"2026-02-10T10:14:42Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"1. Three input modes: no args (picker for unrefined backlog), idea-* ID, or ad-hoc text\n2. Picker filters backlog to Status != \"refined\" items\n3. System prompt includes idea text, specs dir path, gromit-refine skill reference\n4. After Claude exits, scans .gromit/specs/ for files modified after session start\n5. If new spec found from backlog item: updates backlog with Status:\"refined\" and SpecName\n6. Uses same exec.Command(\"claude\", \"--append-system-prompt\", ...) launch pattern","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-06T14:02:32Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"fbb9e5ae7bfc0888a5992c69321e7310e2974207bfb0d032a42c34e6c8289efb","created_at":"2026-02-06T13:41:43Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Rewrite cmd/gromit/refine.go. Three input modes: no args (picker for unrefined backlog items), backlog ID (idea-* pattern), or ad-hoc string. System prompt includes idea text, specs dir path, gromit-refine skill reference. After Claude exits, scan .gromit/specs/ for new files and mark backlog item as status:refined. Also depends on Task 4 (refine skill, gromit-0zh).","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-x3n","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"Cross-dependencies: also depends on gromit-0zh (refine skill). Skill bead should be completed first.","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Rewrite refine command for spec output","updated_at":"2026-02-07T18:29:38Z","waiters":"","wisp_type":"","work_type":""} -{"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"a78e6b5c828a5ff8f75d2828ecf03231a50d14c692fa4e71055ee3057248fb5a","created_at":"2026-03-07T15:06:50Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Run comprehensive test suite across all v2 packages (routing, stage, loop, prompt, remediation). Verify no regressions in existing tests. Run linter (golangci-lint) to check for code quality issues. Perform final cleanup: remove any debug logging, verify error messages are clear, ensure all newly added code follows project patterns from CLAUDE.md.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-x5abw","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"open","target":"","timeout_ns":0,"title":"Final verification and cleanup","updated_at":"2026-03-07T15:06:50Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-03-07T20:33:06Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"a78e6b5c828a5ff8f75d2828ecf03231a50d14c692fa4e71055ee3057248fb5a","created_at":"2026-03-07T15:06:50Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Run comprehensive test suite across all v2 packages (routing, stage, loop, prompt, remediation). Verify no regressions in existing tests. Run linter (golangci-lint) to check for code quality issues. Perform final cleanup: remove any debug logging, verify error messages are clear, ensure all newly added code follows project patterns from CLAUDE.md.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-x5abw","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Final verification and cleanup","updated_at":"2026-03-07T20:33:06Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-06T11:01:49Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"4ce9af21536e021965075c505d59b5bf5ce7c7d55517dfc556b3d6241a16cf5d","created_at":"2026-02-06T09:29:44Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add applyReviewResult method to Runner: creates beads from BeadsToCreate (with from-review label, reviewer-assigned priority) and BacklogItems (with from-review + backlog labels, P2). Add buildReviewBeadLabels and buildBacklogLabels helpers. See docs/plans/2026-02-06-review-system-plan.md Task 10. Depends on task 9.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-x5j","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Implement applyReviewResult for bead creation from findings","updated_at":"2026-02-07T18:29:38Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"- [ ] New BeadStats struct: BeadID, TotalAttempts, Failures, LastAttempt time.Time, LastError string\n- [ ] New ReadPerBeadStats(logsDir string) (map[string]BeadStats, error) function in logger.go\n- [ ] Scans all run-*.jsonl files and groups entries by bead_id\n- [ ] Correctly counts failures vs successes per bead\n- [ ] Tracks most recent attempt timestamp and error message\n- [ ] Handles empty/missing logs dir gracefully (returns empty map, nil)\n- [ ] Unit test covers: empty dir, single log, multiple logs, mixed success/failure\n- [ ] go test ./internal/logger/... passes","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Breaking down into smaller tasks","closed_at":"2026-02-05T19:58:49Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"b876f756a1d5e4eb598cb2abc7f7ffe7faf40689ce0619f1bf089a6406226776","created_at":"2026-02-05T19:44:37Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add a function that scans JSONL log files and returns per-bead failure statistics. This enables both stuck detection in the runner and pattern analysis in retro.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-x72","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add ReadPerBeadStats function to logger package","updated_at":"2026-02-07T18:29:38Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"ReadyWithLabel passes --label flag to bd ready command\nMethod excludes epic-type beads from results\nReturns nil with no error when no beads match label","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-08T12:05:51Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"de4f7a916f1ea32a2869dae9c4327e0f84c26b8ff79497ba0101c9685c266aff","created_at":"2026-02-08T08:10:56Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Add ReadyWithLabel method to bead.Client in internal/bead/bead.go that calls 'bd ready --json --limit 10 --label \u003clabel\u003e' and parses output excluding epic types. Add method to BeadClient interface in internal/runner/interfaces.go.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-x9bq","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add ReadyWithLabel to bead client","updated_at":"2026-02-08T12:05:51Z","waiters":"","wisp_type":"","work_type":""} @@ -2172,6 +2195,7 @@ {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Breaking down into per-file constant extraction beads","closed_at":"2026-02-05T19:33:36Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"3672da4299f822f7a0231d1044576cae05b48f7bcd28d8e9ee8dead2a6dfa874","created_at":"2026-02-05T18:38:54Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Several magic values are hard-coded throughout the codebase.\n\n## Values to extract\n- `internal/analyzer/analyzer.go:68` - maxLen := 8000 (truncation limit)\n- `internal/learnings/learnings.go:86` - 0.7 (similarity threshold)\n- `internal/runner/runner.go:443` - 60 * time.Second (heartbeat interval)\n\n## Fix\nMove to package-level constants with documentation explaining the values.\n\n## Files\n- internal/analyzer/analyzer.go\n- internal/learnings/learnings.go\n- internal/runner/runner.go","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-zpf","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Extract magic values to named constants","updated_at":"2026-02-07T18:29:38Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"Remove dead code: convertToClaudeResult, processStreamJSON, contains/containsString","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"stale: db reset","closed_at":"2026-03-02T19:42:29Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"ba5b13e7bc4120837774b8971455c039ab964b50e11926a634320fb077e23dfe","created_at":"2026-02-18T18:37:09Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Three dead code sites: (1) internal/provider/claude.go:86-94 convertToClaudeResult is never called, (2) internal/claude/claude.go:439-441 processStreamJSON wraps processStreamJSONWithCost but is never invoked, (3) cmd/gromit/init.go:218-229 custom contains/containsString reimplements strings.Contains. Delete all three and replace init.go usage with strings.Contains.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-zpqdj","is_template":0,"issue_type":"bug","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dabrams@example.com","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Remove dead code: convertToClaudeResult, processStreamJSON, contains/containsString","updated_at":"2026-03-02T19:42:29Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-03-06T10:09:07Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"dbe5ca0cb41a3c87d6cabfc814a730a56aa17de154efa29089a7c9ed3fb000c5","created_at":"2026-03-05T20:31:25Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Create cmd/gromit/run2.go with run2 subcommand accepting a spec file path arg. Loads spec and checks dependencies via spec.CheckDependencies. Loads config. Wires Claude LLM, bd TaskTracker, GitHub Presenter, and exec Git adapters. Creates SpecLoop with wired adapters. Subscribes CLI progress and log writer event consumers. Runs spec loop. Exits 0 on success, non-zero on failure. Register in main.go.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-zq7iy","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Implement gromit run2 CLI command","updated_at":"2026-03-06T10:09:07Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-03-07T21:45:13Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"69ab5b9595f2bc3251228133e4eb1a78b4af3e2edbd4fd821a97a62d811ed4f7","created_at":"2026-03-07T20:41:50Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Modify internal/v2/loop/spec_loop.go to run validation commands scoped to flagged beads. If validation fails, mark bead incomplete and re-queue it. Execute this before starting bead loop on resume.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-zrrn7","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Implement selective revalidation in spec loop","updated_at":"2026-03-07T21:45:13Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-02-08T07:27:05Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"8d5e87550f7d79d2c4d5f9dca98a75905b5c86a03d4686edf086b574e51cdbb8","created_at":"2026-02-07T22:42:29Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Runner tests that don't specifically test precheck (e.g. TestRunWithMocks_SkipsStuckBeads, TestRunWithMocks_ClosesBeadOnSuccess at interfaces_test.go) don't set Precheck config. Since NewRunnerWithDeps doesn't call setDefaults(), precheck is disabled by default (nil Enabled pointer → IsEnabled() returns true, but empty Model means Claude.Run gets empty model string). This works accidentally but could break if defaults change. Consider either: (1) having NewRunnerWithDeps call setDefaults(), or (2) explicitly disabling precheck in tests that don't test it.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-zt4n","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"dan@danabrams.com","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add precheck config to non-precheck runner tests to prevent accidental precheck execution","updated_at":"2026-02-08T07:27:05Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-03-06T10:09:07Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"754b0197d796e88d63b4463daf4f7fbc15e281dd38b81842da8c8bc8c7d6ef38","created_at":"2026-03-06T06:40:24Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"Implement internal/v2/event/emitter.go with a thread-safe Emitter. Supports Subscribe(func(Event)) and ensures each emitted event fans out to all subscribers without blocking on panicking or slow handlers. Tests should cover concurrency, backpressure isolation, and panic resilience. Follow TDD (red/green) workflow.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-zv7y1","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":0,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Implement thread-safe event emitter","updated_at":"2026-03-06T10:09:07Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"- SpecflowStoreFactory and SpecBranchCreatorFactory are passed as parameters (e.g., on a deps struct or as function args) rather than mutated as globals\n- No package-level mutation of runner.* factory globals in main.go\n- Existing tests still pass\n- go test ./cmd/gromit/... passes\n- go vet ./... passes\n\nKey file: cmd/gromit/main.go","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"Closed","closed_at":"2026-03-04T19:10:33Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"a810d05e53db6ac3b0f9acaf48d6a285340c82d730823b1c34a9237071f23508","created_at":"2026-03-04T13:05:25Z","created_by":"Dan Abrams","crystallizes":0,"defer_until":null,"description":"main.go runLoop temporarily replaces runner.SpecflowStoreFactory and runner.SpecBranchCreatorFactory package-level globals, calls functions, then restores them. This is not thread-safe — if multiple goroutines execute runLoop concurrently, they race on the global state. Unlike gromit-4tt9w which covers test seam vars, this is production code in main.go.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"gromit-zvc8u","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@thedanabrams.com","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Pass SpecflowStoreFactory and SpecBranchCreatorFactory as parameters instead of mutating globals","updated_at":"2026-03-04T19:10:33Z","waiters":"","wisp_type":"","work_type":""} diff --git a/.beads/backup/labels.jsonl b/.beads/backup/labels.jsonl index 18d55817c..69a8ee07b 100644 --- a/.beads/backup/labels.jsonl +++ b/.beads/backup/labels.jsonl @@ -42,6 +42,9 @@ {"issue_id":"gromit-0uq","label":"spec:expanded-status"} {"issue_id":"gromit-0vxy","label":"spec:concurrent-interactive-worktrees"} {"issue_id":"gromit-0xbs","label":"spec:retro-efficiency-experiments"} +{"issue_id":"gromit-0xxdf","label":"estimated-files:2"} +{"issue_id":"gromit-0xxdf","label":"gen:0"} +{"issue_id":"gromit-0xxdf","label":"spec:v2-loop-routing-and-resume"} {"issue_id":"gromit-0ypa","label":"from-review"} {"issue_id":"gromit-0zg","label":"spec:system-verification"} {"issue_id":"gromit-0zh","label":"spec:pipeline-stages"} @@ -64,10 +67,16 @@ {"issue_id":"gromit-18sei.1","label":"spec:decompose-low-complexity-bias"} {"issue_id":"gromit-18sei.2","label":"spec:decompose-low-complexity-bias"} {"issue_id":"gromit-18sei.3","label":"spec:decompose-low-complexity-bias"} +{"issue_id":"gromit-1b6ks","label":"estimated-files:2"} +{"issue_id":"gromit-1b6ks","label":"gen:0"} +{"issue_id":"gromit-1b6ks","label":"spec:v2-loop-routing-and-resume"} {"issue_id":"gromit-1c2ii","label":"tdd:true"} {"issue_id":"gromit-1d0u","label":"spec:spec-level-atdd-execution"} {"issue_id":"gromit-1fjzj","label":"from-review"} {"issue_id":"gromit-1fjzj","label":"spec:concurrent-interactive-worktrees"} +{"issue_id":"gromit-1fuf8","label":"estimated-files:2"} +{"issue_id":"gromit-1fuf8","label":"gen:0"} +{"issue_id":"gromit-1fuf8","label":"spec:v2-loop-routing-and-resume"} {"issue_id":"gromit-1gnya","label":"from-review"} {"issue_id":"gromit-1gnya","label":"scope-gate"} {"issue_id":"gromit-1gnya","label":"spec:review-spec-validation"} @@ -107,6 +116,9 @@ {"issue_id":"gromit-1zcnn","label":"gen:0"} {"issue_id":"gromit-1zcnn","label":"spec:immutable-pipeline"} {"issue_id":"gromit-1zx","label":"spec:review-system"} +{"issue_id":"gromit-20962","label":"estimated-files:3"} +{"issue_id":"gromit-20962","label":"gen:0"} +{"issue_id":"gromit-20962","label":"spec:v2-loop-routing-and-resume"} {"issue_id":"gromit-20iy","label":"spec:pipeline-concrete-types"} {"issue_id":"gromit-20mha","label":"backlog"} {"issue_id":"gromit-20v4","label":"spec:state-staleness-protection"} @@ -392,6 +404,9 @@ {"issue_id":"gromit-61692.1","label":"tdd:true"} {"issue_id":"gromit-61692.2","label":"tdd:true"} {"issue_id":"gromit-61692.3","label":"tdd:true"} +{"issue_id":"gromit-61cbs","label":"estimated-files:2"} +{"issue_id":"gromit-61cbs","label":"gen:0"} +{"issue_id":"gromit-61cbs","label":"spec:v2-loop-routing-and-resume"} {"issue_id":"gromit-62ro2","label":"bug"} {"issue_id":"gromit-62ro2","label":"from-review"} {"issue_id":"gromit-6426","label":"spec:optimize-bd-subprocess-calls"} @@ -484,6 +499,9 @@ {"issue_id":"gromit-791l5.2","label":"scope_decomp:bb7c5088fca01066"} {"issue_id":"gromit-79kjl","label":"estimated-files:2"} {"issue_id":"gromit-79kjl","label":"spec:tui-pipeline-view"} +{"issue_id":"gromit-7a6rt","label":"estimated-files:2"} +{"issue_id":"gromit-7a6rt","label":"gen:0"} +{"issue_id":"gromit-7a6rt","label":"spec:v2-loop-routing-and-resume"} {"issue_id":"gromit-7dcu","label":"spec:multi-agent-phases"} {"issue_id":"gromit-7duv2","label":"spec:dynamic-code-map"} {"issue_id":"gromit-7e5o","label":"consistency"} @@ -744,6 +762,9 @@ {"issue_id":"gromit-8w81a","label":"spec:review-spec-validation"} {"issue_id":"gromit-8x2","label":"from-review"} {"issue_id":"gromit-8y6e","label":"spec:reduce-iteration-cost"} +{"issue_id":"gromit-8yarv","label":"estimated-files:2"} +{"issue_id":"gromit-8yarv","label":"gen:0"} +{"issue_id":"gromit-8yarv","label":"spec:v2-loop-routing-and-resume"} {"issue_id":"gromit-8yl","label":"spec:status-json-staleness"} {"issue_id":"gromit-8ztwd","label":"estimated-files:4"} {"issue_id":"gromit-8ztwd","label":"gen:0"} @@ -813,6 +834,9 @@ {"issue_id":"gromit-9wmf9","label":"tdd:true"} {"issue_id":"gromit-9xmm1","label":"spec:prompt-token-accounting-diagnostics"} {"issue_id":"gromit-9xp","label":"complexity:low"} +{"issue_id":"gromit-9z0j1","label":"estimated-files:2"} +{"issue_id":"gromit-9z0j1","label":"gen:0"} +{"issue_id":"gromit-9z0j1","label":"spec:v2-loop-routing-and-resume"} {"issue_id":"gromit-a1s","label":"spec:status-json-staleness"} {"issue_id":"gromit-a2j","label":"spec:retro-improvements"} {"issue_id":"gromit-a7wu","label":"spec:session-epilogue"} @@ -845,6 +869,9 @@ {"issue_id":"gromit-aqye","label":"spec:tskip-audit"} {"issue_id":"gromit-atgpc","label":"estimated-files:2"} {"issue_id":"gromit-atgpc","label":"spec:phase-cost-optimization"} +{"issue_id":"gromit-atr10","label":"estimated-files:2"} +{"issue_id":"gromit-atr10","label":"gen:0"} +{"issue_id":"gromit-atr10","label":"spec:v2-loop-routing-and-resume"} {"issue_id":"gromit-au1yu","label":"estimated-files:2"} {"issue_id":"gromit-au1yu","label":"spec:auto-generate-acceptance-criteria"} {"issue_id":"gromit-avbc","label":"spec:retro-efficiency-experiments"} @@ -975,6 +1002,9 @@ {"issue_id":"gromit-csr","label":"spec:review-system"} {"issue_id":"gromit-ctgu","label":"spec:result-output-semantics"} {"issue_id":"gromit-ctpu","label":"spec:state-staleness-protection"} +{"issue_id":"gromit-cufx0","label":"estimated-files:2"} +{"issue_id":"gromit-cufx0","label":"gen:0"} +{"issue_id":"gromit-cufx0","label":"spec:v2-loop-routing-and-resume"} {"issue_id":"gromit-cwk4z","label":"spec:runner-pipeline"} {"issue_id":"gromit-cx22d","label":"bug"} {"issue_id":"gromit-cx22d","label":"from-review"} @@ -988,6 +1018,12 @@ {"issue_id":"gromit-d0n0","label":"spec:pipeline-concrete-types"} {"issue_id":"gromit-d2aj2","label":"from-review"} {"issue_id":"gromit-d2aj2","label":"spec:runner-pipeline"} +{"issue_id":"gromit-d2rpx","label":"estimated-files:2"} +{"issue_id":"gromit-d2rpx","label":"gen:0"} +{"issue_id":"gromit-d2rpx","label":"spec:v2-loop-routing-and-resume"} +{"issue_id":"gromit-d4134","label":"estimated-files:2"} +{"issue_id":"gromit-d4134","label":"gen:0"} +{"issue_id":"gromit-d4134","label":"spec:v2-loop-routing-and-resume"} {"issue_id":"gromit-d46r","label":"spec:status-bead-breakdown"} {"issue_id":"gromit-d6sl","label":"from-review"} {"issue_id":"gromit-d6sl","label":"refactor"} @@ -1144,6 +1180,9 @@ {"issue_id":"gromit-evne","label":"spec:parallel-interactive-command-worktrees"} {"issue_id":"gromit-evq","label":"bug"} {"issue_id":"gromit-evq","label":"from-review"} +{"issue_id":"gromit-ew1l8","label":"estimated-files:3"} +{"issue_id":"gromit-ew1l8","label":"gen:0"} +{"issue_id":"gromit-ew1l8","label":"spec:v2-loop-routing-and-resume"} {"issue_id":"gromit-ewl","label":"spec:harden-fake-claude-writes"} {"issue_id":"gromit-eww","label":"complexity:low"} {"issue_id":"gromit-ex7","label":"complexity:low"} @@ -1158,6 +1197,9 @@ {"issue_id":"gromit-f2ng8.3","label":"tdd:true"} {"issue_id":"gromit-f37u5","label":"from-review"} {"issue_id":"gromit-f37u5","label":"tdd:false"} +{"issue_id":"gromit-f4ouz","label":"estimated-files:2"} +{"issue_id":"gromit-f4ouz","label":"gen:0"} +{"issue_id":"gromit-f4ouz","label":"spec:v2-loop-routing-and-resume"} {"issue_id":"gromit-f4q0","label":"tdd:false"} {"issue_id":"gromit-f4t","label":"spec:gromit-prefix-migration"} {"issue_id":"gromit-f6bo","label":"spec:run-loop-reliability-speed"} @@ -1194,6 +1236,9 @@ {"issue_id":"gromit-fh4cu","label":"estimated-files:2"} {"issue_id":"gromit-fh4cu","label":"gen:0"} {"issue_id":"gromit-fh4cu","label":"spec:immutable-pipeline"} +{"issue_id":"gromit-fhp2q","label":"estimated-files:2"} +{"issue_id":"gromit-fhp2q","label":"gen:0"} +{"issue_id":"gromit-fhp2q","label":"spec:v2-loop-routing-and-resume"} {"issue_id":"gromit-fj1ad","label":"estimated-files:2"} {"issue_id":"gromit-fj1ad","label":"spec:v2-run-loop"} {"issue_id":"gromit-fj9i","label":"spec:acceptance-runtime-optimization"} @@ -1461,6 +1506,9 @@ {"issue_id":"gromit-jsb","label":"complexity:low"} {"issue_id":"gromit-jsta","label":"spec:model-success-tracking"} {"issue_id":"gromit-juyb","label":"spec:multi-provider-routing"} +{"issue_id":"gromit-jv6ns","label":"estimated-files:2"} +{"issue_id":"gromit-jv6ns","label":"gen:0"} +{"issue_id":"gromit-jv6ns","label":"spec:v2-loop-routing-and-resume"} {"issue_id":"gromit-jva","label":"from-review"} {"issue_id":"gromit-jva","label":"test-infrastructure"} {"issue_id":"gromit-jw28c","label":"spec:phase-1-2-token-efficiency-foundation"} @@ -1475,6 +1523,9 @@ {"issue_id":"gromit-jysme","label":"from-review"} {"issue_id":"gromit-jysme","label":"spec:decompose-complexity-estimation"} {"issue_id":"gromit-jz5w","label":"spec:runner-subpackage-split"} +{"issue_id":"gromit-jzbe3","label":"estimated-files:2"} +{"issue_id":"gromit-jzbe3","label":"gen:0"} +{"issue_id":"gromit-jzbe3","label":"spec:v2-loop-routing-and-resume"} {"issue_id":"gromit-jzqqu","label":"estimated-files:2"} {"issue_id":"gromit-jzqqu","label":"spec:provider-parity-decompose-retro"} {"issue_id":"gromit-jzqqu.1","label":"estimated-files:2"} @@ -2250,6 +2301,9 @@ {"issue_id":"gromit-u2pch","label":"tdd:true"} {"issue_id":"gromit-u2zx","label":"spec:reduce-iteration-cost"} {"issue_id":"gromit-u3z9","label":"spec:acceptance-test-cleanup"} +{"issue_id":"gromit-u4837","label":"estimated-files:2"} +{"issue_id":"gromit-u4837","label":"gen:0"} +{"issue_id":"gromit-u4837","label":"spec:v2-loop-routing-and-resume"} {"issue_id":"gromit-u4aqq","label":"spec:prompt-token-accounting-diagnostics"} {"issue_id":"gromit-u5w97","label":"spec:v2-run-loop"} {"issue_id":"gromit-u6i7","label":"from-review"} @@ -2307,6 +2361,9 @@ {"issue_id":"gromit-utelc","label":"spec:immutable-pipeline"} {"issue_id":"gromit-uugq","label":"from-review"} {"issue_id":"gromit-uugq","label":"test-gap"} +{"issue_id":"gromit-uulh1","label":"estimated-files:2"} +{"issue_id":"gromit-uulh1","label":"gen:0"} +{"issue_id":"gromit-uulh1","label":"spec:v2-loop-routing-and-resume"} {"issue_id":"gromit-uv3","label":"complexity:low"} {"issue_id":"gromit-uwag","label":"spec:spec-acceptance-verification-loop"} {"issue_id":"gromit-uwuh","label":"complexity:low"} @@ -2320,7 +2377,13 @@ {"issue_id":"gromit-uzrhu.1","label":"spec:decompose-low-complexity-bias"} {"issue_id":"gromit-uzrhu.2","label":"spec:decompose-low-complexity-bias"} {"issue_id":"gromit-uzrhu.3","label":"spec:decompose-low-complexity-bias"} +{"issue_id":"gromit-v1j2u","label":"estimated-files:2"} +{"issue_id":"gromit-v1j2u","label":"gen:0"} +{"issue_id":"gromit-v1j2u","label":"spec:v2-loop-routing-and-resume"} {"issue_id":"gromit-v4460","label":"spec:v2-run-loop"} +{"issue_id":"gromit-v46qz","label":"estimated-files:2"} +{"issue_id":"gromit-v46qz","label":"gen:0"} +{"issue_id":"gromit-v46qz","label":"spec:v2-loop-routing-and-resume"} {"issue_id":"gromit-v6h8e","label":"estimated-files:2"} {"issue_id":"gromit-v6h8e","label":"spec:v2-run-loop"} {"issue_id":"gromit-v6q0g","label":"spec:phase-1-2-token-efficiency-foundation"} @@ -2367,6 +2430,9 @@ {"issue_id":"gromit-w0en","label":"spec:model-success-tracking"} {"issue_id":"gromit-w0lo","label":"complexity:low"} {"issue_id":"gromit-w0z6","label":"spec:phase-isolated-methodology-contexts"} +{"issue_id":"gromit-w1200","label":"estimated-files:2"} +{"issue_id":"gromit-w1200","label":"gen:0"} +{"issue_id":"gromit-w1200","label":"spec:v2-loop-routing-and-resume"} {"issue_id":"gromit-w1dux","label":"estimated-files:5"} {"issue_id":"gromit-w1dux","label":"spec:remove-tdd-fresh-context"} {"issue_id":"gromit-w1sv","label":"complexity:low"} @@ -2389,6 +2455,9 @@ {"issue_id":"gromit-w72th","label":"refactor"} {"issue_id":"gromit-w7cn","label":"spec:filtered-hash-eviction"} {"issue_id":"gromit-w7qn","label":"spec:bead-scope-validation"} +{"issue_id":"gromit-w83pd","label":"estimated-files:2"} +{"issue_id":"gromit-w83pd","label":"gen:0"} +{"issue_id":"gromit-w83pd","label":"spec:v2-loop-routing-and-resume"} {"issue_id":"gromit-w8std","label":"estimated-files:2"} {"issue_id":"gromit-w8std","label":"gen:0"} {"issue_id":"gromit-w8std","label":"spec:immutable-pipeline"} @@ -2729,6 +2798,9 @@ {"issue_id":"gromit-zpqdj","label":"from-review"} {"issue_id":"gromit-zq7iy","label":"estimated-files:2"} {"issue_id":"gromit-zq7iy","label":"spec:v2-run-loop"} +{"issue_id":"gromit-zrrn7","label":"estimated-files:2"} +{"issue_id":"gromit-zrrn7","label":"gen:0"} +{"issue_id":"gromit-zrrn7","label":"spec:v2-loop-routing-and-resume"} {"issue_id":"gromit-zt4n","label":"from-review"} {"issue_id":"gromit-zt4n","label":"test-gap"} {"issue_id":"gromit-zvc8u","label":"bug"}