fix(ready): filter formula scaffolds from gt ready output (gt-579)
Formula scaffold beads (created when formulas are installed) were appearing as actionable work items in `gt ready`. These are template beads, not actual work. Add filtering to exclude issues whose ID: - Matches a formula name exactly (e.g., "mol-deacon-patrol") - Starts with "<formula-name>." (step scaffolds like "mol-deacon-patrol.inbox-check") The fix reads the formulas directory to get installed formula names and filters issues accordingly for both town and rig beads. Fixes: gt-579 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -72,13 +73,6 @@ type ReadySummary struct {
|
|||||||
P4Count int `json:"p4_count"`
|
P4Count int `json:"p4_count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// isFormulaScaffold returns true if the issue ID looks like a formula scaffold.
|
|
||||||
// Formula scaffolds are templates created when formulas are installed, not actual work.
|
|
||||||
// Pattern: "mol-<name>" or "mol-<name>.<step-id>"
|
|
||||||
func isFormulaScaffold(id string) bool {
|
|
||||||
return strings.HasPrefix(id, "mol-")
|
|
||||||
}
|
|
||||||
|
|
||||||
func runReady(cmd *cobra.Command, args []string) error {
|
func runReady(cmd *cobra.Command, args []string) error {
|
||||||
// Find town root
|
// Find town root
|
||||||
townRoot, err := workspace.FindFromCwdOrError()
|
townRoot, err := workspace.FindFromCwdOrError()
|
||||||
@@ -136,14 +130,9 @@ func runReady(cmd *cobra.Command, args []string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
src.Error = err.Error()
|
src.Error = err.Error()
|
||||||
} else {
|
} else {
|
||||||
// Filter out formula scaffolds
|
// Filter out formula scaffolds (gt-579)
|
||||||
var filtered []*beads.Issue
|
formulaNames := getFormulaNames(townBeadsPath)
|
||||||
for _, issue := range issues {
|
src.Issues = filterFormulaScaffolds(issues, formulaNames)
|
||||||
if !isFormulaScaffold(issue.ID) {
|
|
||||||
filtered = append(filtered, issue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
src.Issues = filtered
|
|
||||||
}
|
}
|
||||||
sources = append(sources, src)
|
sources = append(sources, src)
|
||||||
}()
|
}()
|
||||||
@@ -165,14 +154,9 @@ func runReady(cmd *cobra.Command, args []string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
src.Error = err.Error()
|
src.Error = err.Error()
|
||||||
} else {
|
} else {
|
||||||
// Filter out formula scaffolds
|
// Filter out formula scaffolds (gt-579)
|
||||||
var filtered []*beads.Issue
|
formulaNames := getFormulaNames(rigBeadsPath)
|
||||||
for _, issue := range issues {
|
src.Issues = filterFormulaScaffolds(issues, formulaNames)
|
||||||
if !isFormulaScaffold(issue.ID) {
|
|
||||||
filtered = append(filtered, issue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
src.Issues = filtered
|
|
||||||
}
|
}
|
||||||
sources = append(sources, src)
|
sources = append(sources, src)
|
||||||
}(r)
|
}(r)
|
||||||
@@ -310,3 +294,55 @@ func printReadyHuman(result ReadyResult) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getFormulaNames reads the formulas directory and returns a set of formula names.
|
||||||
|
// Formula names are derived from filenames by removing the ".formula.toml" suffix.
|
||||||
|
func getFormulaNames(beadsPath string) map[string]bool {
|
||||||
|
formulasDir := filepath.Join(beadsPath, "formulas")
|
||||||
|
entries, err := os.ReadDir(formulasDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
names := make(map[string]bool)
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := entry.Name()
|
||||||
|
if strings.HasSuffix(name, ".formula.toml") {
|
||||||
|
// Remove suffix to get formula name
|
||||||
|
formulaName := strings.TrimSuffix(name, ".formula.toml")
|
||||||
|
names[formulaName] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterFormulaScaffolds removes formula scaffold issues from the list.
|
||||||
|
// Formula scaffolds are issues whose ID matches a formula name exactly
|
||||||
|
// or starts with "<formula-name>." (step scaffolds).
|
||||||
|
func filterFormulaScaffolds(issues []*beads.Issue, formulaNames map[string]bool) []*beads.Issue {
|
||||||
|
if formulaNames == nil || len(formulaNames) == 0 {
|
||||||
|
return issues
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered := make([]*beads.Issue, 0, len(issues))
|
||||||
|
for _, issue := range issues {
|
||||||
|
// Check if this is a formula scaffold (exact match)
|
||||||
|
if formulaNames[issue.ID] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is a step scaffold (formula-name.step-id)
|
||||||
|
if idx := strings.Index(issue.ID, "."); idx > 0 {
|
||||||
|
prefix := issue.ID[:idx]
|
||||||
|
if formulaNames[prefix] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered = append(filtered, issue)
|
||||||
|
}
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
|
|||||||
150
internal/cmd/ready_test.go
Normal file
150
internal/cmd/ready_test.go
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/steveyegge/gastown/internal/beads"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetFormulaNames(t *testing.T) {
|
||||||
|
// Create temp directory structure
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
formulasDir := filepath.Join(tmpDir, "formulas")
|
||||||
|
if err := os.MkdirAll(formulasDir, 0755); err != nil {
|
||||||
|
t.Fatalf("creating formulas dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create some formula files
|
||||||
|
formulas := []string{
|
||||||
|
"mol-deacon-patrol.formula.toml",
|
||||||
|
"mol-witness-patrol.formula.toml",
|
||||||
|
"shiny.formula.toml",
|
||||||
|
}
|
||||||
|
for _, f := range formulas {
|
||||||
|
path := filepath.Join(formulasDir, f)
|
||||||
|
if err := os.WriteFile(path, []byte("# test"), 0644); err != nil {
|
||||||
|
t.Fatalf("writing %s: %v", f, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also create a non-formula file (should be ignored)
|
||||||
|
if err := os.WriteFile(filepath.Join(formulasDir, ".installed.json"), []byte("{}"), 0644); err != nil {
|
||||||
|
t.Fatalf("writing .installed.json: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test
|
||||||
|
names := getFormulaNames(tmpDir)
|
||||||
|
if names == nil {
|
||||||
|
t.Fatal("getFormulaNames returned nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := []string{"mol-deacon-patrol", "mol-witness-patrol", "shiny"}
|
||||||
|
for _, name := range expected {
|
||||||
|
if !names[name] {
|
||||||
|
t.Errorf("expected formula name %q not found", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should not include the .installed.json file
|
||||||
|
if names[".installed"] {
|
||||||
|
t.Error(".installed should not be in formula names")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(names) != len(expected) {
|
||||||
|
t.Errorf("got %d formula names, want %d", len(names), len(expected))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetFormulaNames_NonexistentDir(t *testing.T) {
|
||||||
|
names := getFormulaNames("/nonexistent/path")
|
||||||
|
if names != nil {
|
||||||
|
t.Error("expected nil for nonexistent directory")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilterFormulaScaffolds(t *testing.T) {
|
||||||
|
formulaNames := map[string]bool{
|
||||||
|
"mol-deacon-patrol": true,
|
||||||
|
"mol-witness-patrol": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
issues := []*beads.Issue{
|
||||||
|
{ID: "mol-deacon-patrol", Title: "mol-deacon-patrol"},
|
||||||
|
{ID: "mol-deacon-patrol.inbox-check", Title: "Handle callbacks"},
|
||||||
|
{ID: "mol-deacon-patrol.health-scan", Title: "Check health"},
|
||||||
|
{ID: "mol-witness-patrol", Title: "mol-witness-patrol"},
|
||||||
|
{ID: "mol-witness-patrol.loop-or-exit", Title: "Loop or exit"},
|
||||||
|
{ID: "hq-123", Title: "Real work item"},
|
||||||
|
{ID: "hq-wisp-abc", Title: "Actual wisp"},
|
||||||
|
{ID: "gt-456", Title: "Project issue"},
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered := filterFormulaScaffolds(issues, formulaNames)
|
||||||
|
|
||||||
|
// Should only have the non-scaffold issues
|
||||||
|
if len(filtered) != 3 {
|
||||||
|
t.Errorf("got %d filtered issues, want 3", len(filtered))
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedIDs := map[string]bool{
|
||||||
|
"hq-123": true,
|
||||||
|
"hq-wisp-abc": true,
|
||||||
|
"gt-456": true,
|
||||||
|
}
|
||||||
|
for _, issue := range filtered {
|
||||||
|
if !expectedIDs[issue.ID] {
|
||||||
|
t.Errorf("unexpected issue in filtered result: %s", issue.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilterFormulaScaffolds_NilFormulaNames(t *testing.T) {
|
||||||
|
issues := []*beads.Issue{
|
||||||
|
{ID: "hq-123", Title: "Real work"},
|
||||||
|
{ID: "mol-deacon-patrol", Title: "Would be filtered"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// With nil formula names, should return all issues unchanged
|
||||||
|
filtered := filterFormulaScaffolds(issues, nil)
|
||||||
|
if len(filtered) != len(issues) {
|
||||||
|
t.Errorf("got %d issues, want %d (nil formulaNames should return all)", len(filtered), len(issues))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilterFormulaScaffolds_EmptyFormulaNames(t *testing.T) {
|
||||||
|
issues := []*beads.Issue{
|
||||||
|
{ID: "hq-123", Title: "Real work"},
|
||||||
|
{ID: "mol-deacon-patrol", Title: "Would be filtered"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// With empty formula names, should return all issues unchanged
|
||||||
|
filtered := filterFormulaScaffolds(issues, map[string]bool{})
|
||||||
|
if len(filtered) != len(issues) {
|
||||||
|
t.Errorf("got %d issues, want %d (empty formulaNames should return all)", len(filtered), len(issues))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilterFormulaScaffolds_EmptyIssues(t *testing.T) {
|
||||||
|
formulaNames := map[string]bool{"mol-deacon-patrol": true}
|
||||||
|
filtered := filterFormulaScaffolds([]*beads.Issue{}, formulaNames)
|
||||||
|
if len(filtered) != 0 {
|
||||||
|
t.Errorf("got %d issues, want 0", len(filtered))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilterFormulaScaffolds_DotInNonScaffold(t *testing.T) {
|
||||||
|
// Issue ID has a dot but prefix is not a formula name
|
||||||
|
formulaNames := map[string]bool{"mol-deacon-patrol": true}
|
||||||
|
|
||||||
|
issues := []*beads.Issue{
|
||||||
|
{ID: "hq-cv.synthesis-step", Title: "Convoy synthesis"},
|
||||||
|
{ID: "some.other.thing", Title: "Random dotted ID"},
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered := filterFormulaScaffolds(issues, formulaNames)
|
||||||
|
if len(filtered) != 2 {
|
||||||
|
t.Errorf("got %d issues, want 2 (non-formula dots should not filter)", len(filtered))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user