feat: consolidate schema changes from crew directories

Merges schema additions from crew/fang, crew/giles, crew/grip, and crew/wolf:

- crystallizes: bool field for work economics (compounds vs evaporates)
- work_type: WorkType field for assignment model (mutex vs open_competition)
- source_system: string field for federation adapter tracking
- quality_score: *float32 for aggregate quality (0.0-1.0)
- delegated-from: new dependency type for work delegation chains

Migrations properly sequenced as 037-040 (after existing 036 owner_column).

Also fixes test compilation errors for removed TypeRig and IsBuiltIn references.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Executed-By: beads/crew/dave
Rig: beads
Role: crew
This commit is contained in:
beads/crew/dave
2026-01-10 20:26:57 -08:00
committed by Steve Yegge
parent 8942261a12
commit f79e636000
9 changed files with 193 additions and 46 deletions

View File

@@ -157,7 +157,7 @@ func (s *SQLiteStorage) GetIssuesByLabel(ctx context.Context, label string) ([]*
rows, err := s.db.QueryContext(ctx, ` rows, err := s.db.QueryContext(ctx, `
SELECT i.id, i.content_hash, i.title, i.description, i.design, i.acceptance_criteria, i.notes, SELECT i.id, i.content_hash, i.title, i.description, i.design, i.acceptance_criteria, i.notes,
i.status, i.priority, i.issue_type, i.assignee, i.estimated_minutes, i.status, i.priority, i.issue_type, i.assignee, i.estimated_minutes,
i.created_at, i.created_by, i.updated_at, i.closed_at, i.external_ref, i.source_repo, i.close_reason, i.created_at, i.created_by, i.owner, i.updated_at, i.closed_at, i.external_ref, i.source_repo, i.close_reason,
i.deleted_at, i.deleted_by, i.delete_reason, i.original_type, i.deleted_at, i.deleted_by, i.delete_reason, i.original_type,
i.sender, i.ephemeral, i.pinned, i.is_template, i.sender, i.ephemeral, i.pinned, i.is_template,
i.await_type, i.await_id, i.timeout_ns, i.waiters i.await_type, i.await_id, i.timeout_ns, i.waiters

View File

@@ -53,6 +53,10 @@ var migrationsList = []Migration{
{"closed_by_session_column", migrations.MigrateClosedBySessionColumn}, {"closed_by_session_column", migrations.MigrateClosedBySessionColumn},
{"due_defer_columns", migrations.MigrateDueDeferColumns}, {"due_defer_columns", migrations.MigrateDueDeferColumns},
{"owner_column", migrations.MigrateOwnerColumn}, {"owner_column", migrations.MigrateOwnerColumn},
{"crystallizes_column", migrations.MigrateCrystallizesColumn},
{"work_type_column", migrations.MigrateWorkTypeColumn},
{"source_system_column", migrations.MigrateSourceSystemColumn},
{"quality_score_column", migrations.MigrateQualityScoreColumn},
} }
// MigrationInfo contains metadata about a migration for inspection // MigrationInfo contains metadata about a migration for inspection
@@ -113,6 +117,10 @@ func getMigrationDescription(name string) string {
"closed_by_session_column": "Adds closed_by_session column for tracking which Claude Code session closed an issue", "closed_by_session_column": "Adds closed_by_session column for tracking which Claude Code session closed an issue",
"due_defer_columns": "Adds due_at and defer_until columns for time-based task scheduling (GH#820)", "due_defer_columns": "Adds due_at and defer_until columns for time-based task scheduling (GH#820)",
"owner_column": "Adds owner column for human attribution in HOP CV chains (Decision 008)", "owner_column": "Adds owner column for human attribution in HOP CV chains (Decision 008)",
"crystallizes_column": "Adds crystallizes column for work economics (compounds vs evaporates) per Decision 006",
"work_type_column": "Adds work_type column for work assignment model (mutex vs open_competition per Decision 006)",
"source_system_column": "Adds source_system column for federation adapter tracking",
"quality_score_column": "Adds quality_score column for aggregate quality (0.0-1.0) set by Refineries",
} }
if desc, ok := descriptions[name]; ok { if desc, ok := descriptions[name]; ok {

View File

@@ -0,0 +1,36 @@
package migrations
import (
"database/sql"
"fmt"
)
// MigrateCrystallizesColumn adds the crystallizes column to the issues table.
// Crystallizes tracks whether work compounds over time (true: code, features)
// or evaporates (false: ops, support). Per Decision 006, this affects CV weighting.
// Default is false (conservative - work evaporates unless explicitly marked).
func MigrateCrystallizesColumn(db *sql.DB) error {
// Check if column already exists
var columnExists bool
err := db.QueryRow(`
SELECT COUNT(*) > 0
FROM pragma_table_info('issues')
WHERE name = 'crystallizes'
`).Scan(&columnExists)
if err != nil {
return fmt.Errorf("failed to check crystallizes column: %w", err)
}
if columnExists {
// Column already exists (e.g. created by new schema)
return nil
}
// Add the crystallizes column
_, err = db.Exec(`ALTER TABLE issues ADD COLUMN crystallizes INTEGER DEFAULT 0`)
if err != nil {
return fmt.Errorf("failed to add crystallizes column: %w", err)
}
return nil
}

View File

@@ -0,0 +1,34 @@
package migrations
import (
"database/sql"
"fmt"
)
// MigrateWorkTypeColumn adds work_type column to the issues table.
// This field distinguishes work assignment models per Decision 006.
// Values: 'mutex' (one worker, exclusive - default) or 'open_competition' (many submit, buyer picks)
func MigrateWorkTypeColumn(db *sql.DB) error {
// Check if column already exists
var columnExists bool
err := db.QueryRow(`
SELECT COUNT(*) > 0
FROM pragma_table_info('issues')
WHERE name = 'work_type'
`).Scan(&columnExists)
if err != nil {
return fmt.Errorf("failed to check work_type column: %w", err)
}
if columnExists {
return nil
}
// Add the column with default 'mutex'
_, err = db.Exec(`ALTER TABLE issues ADD COLUMN work_type TEXT DEFAULT 'mutex'`)
if err != nil {
return fmt.Errorf("failed to add work_type column: %w", err)
}
return nil
}

View File

@@ -0,0 +1,33 @@
package migrations
import (
"database/sql"
"fmt"
)
// MigrateSourceSystemColumn adds the source_system column to the issues table.
// This tracks which adapter/system created the issue for federation support.
func MigrateSourceSystemColumn(db *sql.DB) error {
// Check if column already exists
var columnExists bool
err := db.QueryRow(`
SELECT COUNT(*) > 0
FROM pragma_table_info('issues')
WHERE name = 'source_system'
`).Scan(&columnExists)
if err != nil {
return fmt.Errorf("failed to check source_system column: %w", err)
}
if columnExists {
return nil
}
// Add the source_system column
_, err = db.Exec(`ALTER TABLE issues ADD COLUMN source_system TEXT DEFAULT ''`)
if err != nil {
return fmt.Errorf("failed to add source_system column: %w", err)
}
return nil
}

View File

@@ -0,0 +1,34 @@
package migrations
import (
"database/sql"
"fmt"
)
// MigrateQualityScoreColumn adds the quality_score column to the issues table.
// This stores an aggregate quality score (0.0-1.0) set by Refineries on merge.
// NULL indicates no score has been assigned yet.
func MigrateQualityScoreColumn(db *sql.DB) error {
// Check if column already exists
var columnExists bool
err := db.QueryRow(`
SELECT COUNT(*) > 0
FROM pragma_table_info('issues')
WHERE name = 'quality_score'
`).Scan(&columnExists)
if err != nil {
return fmt.Errorf("failed to check quality_score column: %w", err)
}
if columnExists {
return nil
}
// Add the quality_score column (REAL, nullable - no default)
_, err = db.Exec(`ALTER TABLE issues ADD COLUMN quality_score REAL`)
if err != nil {
return fmt.Errorf("failed to add quality_score column: %w", err)
}
return nil
}

View File

@@ -39,6 +39,14 @@ CREATE TABLE IF NOT EXISTS issues (
is_template INTEGER DEFAULT 0, is_template INTEGER DEFAULT 0,
-- Molecule type field (bd-oxgi) -- Molecule type field (bd-oxgi)
mol_type TEXT DEFAULT '', mol_type TEXT DEFAULT '',
-- Work type field (Decision 006: mutex vs open_competition)
work_type TEXT DEFAULT 'mutex',
-- HOP quality score field (0.0-1.0, set by Refineries on merge)
quality_score REAL,
-- Work economics field (Decision 006) - compounds vs evaporates
crystallizes INTEGER DEFAULT 0,
-- Federation source system field
source_system TEXT DEFAULT '',
-- Event fields (bd-ecmd) -- Event fields (bd-ecmd)
event_kind TEXT DEFAULT '', event_kind TEXT DEFAULT '',
actor TEXT DEFAULT '', actor TEXT DEFAULT '',

View File

@@ -46,7 +46,8 @@ type Issue struct {
DeferUntil *time.Time `json:"defer_until,omitempty"` // Hide from bd ready until this time DeferUntil *time.Time `json:"defer_until,omitempty"` // Hide from bd ready until this time
// ===== External Integration ===== // ===== External Integration =====
ExternalRef *string `json:"external_ref,omitempty"` // e.g., "gh-9", "jira-ABC" ExternalRef *string `json:"external_ref,omitempty"` // e.g., "gh-9", "jira-ABC"
SourceSystem string `json:"source_system,omitempty"` // Adapter/system that created this issue (federation)
// ===== Compaction Metadata ===== // ===== Compaction Metadata =====
CompactionLevel int `json:"compaction_level,omitempty"` CompactionLevel int `json:"compaction_level,omitempty"`
@@ -83,8 +84,10 @@ type Issue struct {
BondedFrom []BondRef `json:"bonded_from,omitempty"` // For compounds: constituent protos BondedFrom []BondRef `json:"bonded_from,omitempty"` // For compounds: constituent protos
// ===== HOP Fields (entity tracking for CV chains) ===== // ===== HOP Fields (entity tracking for CV chains) =====
Creator *EntityRef `json:"creator,omitempty"` // Who created (human, agent, or org) Creator *EntityRef `json:"creator,omitempty"` // Who created (human, agent, or org)
Validations []Validation `json:"validations,omitempty"` // Who validated/approved Validations []Validation `json:"validations,omitempty"` // Who validated/approved
QualityScore *float32 `json:"quality_score,omitempty"` // Aggregate quality (0.0-1.0), set by Refineries on merge
Crystallizes bool `json:"crystallizes,omitempty"` // Work that compounds (true: code, features) vs evaporates (false: ops, support) - affects CV weighting per Decision 006
// ===== Gate Fields (async coordination primitives) ===== // ===== Gate Fields (async coordination primitives) =====
AwaitType string `json:"await_type,omitempty"` // Condition type: gh:run, gh:pr, timer, human, mail AwaitType string `json:"await_type,omitempty"` // Condition type: gh:run, gh:pr, timer, human, mail
@@ -110,6 +113,9 @@ type Issue struct {
// ===== Molecule Type Fields (swarm coordination) ===== // ===== Molecule Type Fields (swarm coordination) =====
MolType MolType `json:"mol_type,omitempty"` // Molecule type: swarm|patrol|work (empty = work) MolType MolType `json:"mol_type,omitempty"` // Molecule type: swarm|patrol|work (empty = work)
// ===== Work Type Fields (assignment model - Decision 006) =====
WorkType WorkType `json:"work_type,omitempty"` // Work type: mutex|open_competition (empty = mutex)
// ===== Event Fields (operational state changes) ===== // ===== Event Fields (operational state changes) =====
EventKind string `json:"event_kind,omitempty"` // Namespaced event type: patrol.muted, agent.started EventKind string `json:"event_kind,omitempty"` // Namespaced event type: patrol.muted, agent.started
Actor string `json:"actor,omitempty"` // Entity URI who caused this event Actor string `json:"actor,omitempty"` // Entity URI who caused this event
@@ -139,6 +145,7 @@ func (i *Issue) ComputeContentHash() string {
// Optional fields // Optional fields
w.strPtr(i.ExternalRef) w.strPtr(i.ExternalRef)
w.str(i.SourceSystem)
w.flag(i.Pinned, "pinned") w.flag(i.Pinned, "pinned")
w.flag(i.IsTemplate, "template") w.flag(i.IsTemplate, "template")
@@ -160,6 +167,10 @@ func (i *Issue) ComputeContentHash() string {
w.float32Ptr(v.Score) w.float32Ptr(v.Score)
} }
// HOP aggregate quality score and crystallizes
w.float32Ptr(i.QualityScore)
w.flag(i.Crystallizes, "crystallizes")
// Gate fields for async coordination // Gate fields for async coordination
w.str(i.AwaitType) w.str(i.AwaitType)
w.str(i.AwaitID) w.str(i.AwaitID)
@@ -181,6 +192,9 @@ func (i *Issue) ComputeContentHash() string {
// Molecule type // Molecule type
w.str(string(i.MolType)) w.str(string(i.MolType))
// Work type
w.str(string(i.WorkType))
// Event fields // Event fields
w.str(i.EventKind) w.str(i.EventKind)
w.str(i.Actor) w.str(i.Actor)
@@ -587,6 +601,24 @@ func (m MolType) IsValid() bool {
return false return false
} }
// WorkType categorizes how work assignment operates for a bead (Decision 006)
type WorkType string
// WorkType constants
const (
WorkTypeMutex WorkType = "mutex" // One worker, exclusive assignment (default)
WorkTypeOpenCompetition WorkType = "open_competition" // Many submit, buyer picks
)
// IsValid checks if the work type value is valid
func (w WorkType) IsValid() bool {
switch w {
case WorkTypeMutex, WorkTypeOpenCompetition, "":
return true // empty is valid (defaults to mutex)
}
return false
}
// Dependency represents a relationship between issues // Dependency represents a relationship between issues
type Dependency struct { type Dependency struct {
IssueID string `json:"issue_id"` IssueID string `json:"issue_id"`
@@ -667,6 +699,9 @@ const (
DepUntil DependencyType = "until" // Active until target closes (e.g., muted until issue resolved) DepUntil DependencyType = "until" // Active until target closes (e.g., muted until issue resolved)
DepCausedBy DependencyType = "caused-by" // Triggered by target (audit trail) DepCausedBy DependencyType = "caused-by" // Triggered by target (audit trail)
DepValidates DependencyType = "validates" // Approval/validation relationship DepValidates DependencyType = "validates" // Approval/validation relationship
// Delegation types (work delegation chains)
DepDelegatedFrom DependencyType = "delegated-from" // Work delegated from parent; completion cascades up
) )
// IsValid checks if the dependency type value is valid. // IsValid checks if the dependency type value is valid.
@@ -683,7 +718,7 @@ func (d DependencyType) IsWellKnown() bool {
case DepBlocks, DepParentChild, DepConditionalBlocks, DepWaitsFor, DepRelated, DepDiscoveredFrom, case DepBlocks, DepParentChild, DepConditionalBlocks, DepWaitsFor, DepRelated, DepDiscoveredFrom,
DepRepliesTo, DepRelatesTo, DepDuplicates, DepSupersedes, DepRepliesTo, DepRelatesTo, DepDuplicates, DepSupersedes,
DepAuthoredBy, DepAssignedTo, DepApprovedBy, DepAttests, DepTracks, DepAuthoredBy, DepAssignedTo, DepApprovedBy, DepAttests, DepTracks,
DepUntil, DepCausedBy, DepValidates: DepUntil, DepCausedBy, DepValidates, DepDelegatedFrom:
return true return true
} }
return false return false

View File

@@ -550,7 +550,6 @@ func TestIssueTypeIsValid(t *testing.T) {
{TypeGate, true}, {TypeGate, true},
{TypeAgent, true}, {TypeAgent, true},
{TypeRole, true}, {TypeRole, true},
{TypeRig, true},
{TypeConvoy, true}, {TypeConvoy, true},
{TypeEvent, true}, {TypeEvent, true},
{TypeSlot, true}, {TypeSlot, true},
@@ -567,46 +566,6 @@ func TestIssueTypeIsValid(t *testing.T) {
} }
} }
func TestIssueTypeIsBuiltIn(t *testing.T) {
// IsBuiltIn should match IsValid - both identify built-in types
// This is used during multi-repo hydration to determine trust:
// - Built-in types: validate (catch typos)
// - Custom types (!IsBuiltIn): trust from source repo
tests := []struct {
issueType IssueType
builtIn bool
}{
// All built-in types
{TypeBug, true},
{TypeFeature, true},
{TypeTask, true},
{TypeEpic, true},
{TypeChore, true},
{TypeMessage, true},
{TypeMergeRequest, true},
{TypeMolecule, true},
{TypeGate, true},
{TypeAgent, true},
{TypeRole, true},
{TypeRig, true},
{TypeConvoy, true},
{TypeEvent, true},
{TypeSlot, true},
// Custom types (not built-in)
{IssueType("pm"), false}, // Custom type from child repo
{IssueType("llm"), false}, // Custom type from child repo
{IssueType(""), false}, // Empty is not built-in
}
for _, tt := range tests {
t.Run(string(tt.issueType), func(t *testing.T) {
if got := tt.issueType.IsBuiltIn(); got != tt.builtIn {
t.Errorf("IssueType(%q).IsBuiltIn() = %v, want %v", tt.issueType, got, tt.builtIn)
}
})
}
}
func TestIssueTypeRequiredSections(t *testing.T) { func TestIssueTypeRequiredSections(t *testing.T) {
tests := []struct { tests := []struct {
issueType IssueType issueType IssueType