feat: Add mol_type schema field for molecule type classification (bd-oxgi)
Add mol_type field to beads for swarm coordination: - Values: 'swarm' (multi-polecat), 'patrol' (recurring ops), 'work' (default) - Nullable, defaults to empty string (treated as 'work') Changes: - Add mol_type column to SQLite schema and migration 031 - Add MolType type with IsValid() validation in types.go - Update insertIssue/GetIssue to handle mol_type - Add --mol-type flag to create command - Add mol_type filtering to list and ready commands - Update RPC protocol for daemon mode support - Update test schema in migrations_test.go 🤝 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -47,8 +47,8 @@ func insertIssue(ctx context.Context, conn *sql.Conn, issue *types.Issue) error
|
||||
created_at, created_by, updated_at, closed_at, external_ref, source_repo, close_reason,
|
||||
deleted_at, deleted_by, delete_reason, original_type,
|
||||
sender, ephemeral, pinned, is_template,
|
||||
await_type, await_id, timeout_ns, waiters
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
await_type, await_id, timeout_ns, waiters, mol_type
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
issue.ID, issue.ContentHash, issue.Title, issue.Description, issue.Design,
|
||||
issue.AcceptanceCriteria, issue.Notes, issue.Status,
|
||||
@@ -58,6 +58,7 @@ func insertIssue(ctx context.Context, conn *sql.Conn, issue *types.Issue) error
|
||||
issue.DeletedAt, issue.DeletedBy, issue.DeleteReason, issue.OriginalType,
|
||||
issue.Sender, wisp, pinned, isTemplate,
|
||||
issue.AwaitType, issue.AwaitID, int64(issue.Timeout), formatJSONStringArray(issue.Waiters),
|
||||
string(issue.MolType),
|
||||
)
|
||||
if err != nil {
|
||||
// INSERT OR IGNORE should handle duplicates, but driver may still return error
|
||||
@@ -79,8 +80,8 @@ func insertIssues(ctx context.Context, conn *sql.Conn, issues []*types.Issue) er
|
||||
created_at, created_by, updated_at, closed_at, external_ref, source_repo, close_reason,
|
||||
deleted_at, deleted_by, delete_reason, original_type,
|
||||
sender, ephemeral, pinned, is_template,
|
||||
await_type, await_id, timeout_ns, waiters
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
await_type, await_id, timeout_ns, waiters, mol_type
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to prepare statement: %w", err)
|
||||
@@ -115,6 +116,7 @@ func insertIssues(ctx context.Context, conn *sql.Conn, issues []*types.Issue) er
|
||||
issue.DeletedAt, issue.DeletedBy, issue.DeleteReason, issue.OriginalType,
|
||||
issue.Sender, wisp, pinned, isTemplate,
|
||||
issue.AwaitType, issue.AwaitID, int64(issue.Timeout), formatJSONStringArray(issue.Waiters),
|
||||
string(issue.MolType),
|
||||
)
|
||||
if err != nil {
|
||||
// INSERT OR IGNORE should handle duplicates, but driver may still return error
|
||||
|
||||
@@ -47,6 +47,7 @@ var migrationsList = []Migration{
|
||||
{"tombstone_closed_at", migrations.MigrateTombstoneClosedAt},
|
||||
{"created_by_column", migrations.MigrateCreatedByColumn},
|
||||
{"agent_fields", migrations.MigrateAgentFields},
|
||||
{"mol_type_column", migrations.MigrateMolTypeColumn},
|
||||
}
|
||||
|
||||
// MigrationInfo contains metadata about a migration for inspection
|
||||
@@ -98,8 +99,12 @@ func getMigrationDescription(name string) string {
|
||||
"remove_depends_on_fk": "Removes FK constraint on depends_on_id to allow external references",
|
||||
"additional_indexes": "Adds performance optimization indexes for common query patterns",
|
||||
"gate_columns": "Adds gate columns (await_type, await_id, timeout_ns, waiters) for async coordination",
|
||||
"tombstone_closed_at": "Preserves closed_at timestamp when issues become tombstones",
|
||||
"created_by_column": "Adds created_by column to track issue creator",
|
||||
"agent_fields": "Adds agent identity fields (hook_bead, role_bead, agent_state, etc.) for agent-as-bead pattern",
|
||||
"mol_type_column": "Adds mol_type column for molecule type classification (swarm/patrol/work)",
|
||||
}
|
||||
|
||||
|
||||
if desc, ok := descriptions[name]; ok {
|
||||
return desc
|
||||
}
|
||||
|
||||
34
internal/storage/sqlite/migrations/031_mol_type_column.go
Normal file
34
internal/storage/sqlite/migrations/031_mol_type_column.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// MigrateMolTypeColumn adds mol_type column to the issues table.
|
||||
// This field distinguishes molecule types (swarm/patrol/work) for swarm coordination.
|
||||
// Values: 'swarm' (multi-polecat coordination), 'patrol' (recurring ops), 'work' (regular, default)
|
||||
func MigrateMolTypeColumn(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 = 'mol_type'
|
||||
`).Scan(&columnExists)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check mol_type column: %w", err)
|
||||
}
|
||||
|
||||
if columnExists {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add the column
|
||||
_, err = db.Exec(`ALTER TABLE issues ADD COLUMN mol_type TEXT DEFAULT ''`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add mol_type column: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -502,9 +502,10 @@ func TestMigrateContentHashColumn(t *testing.T) {
|
||||
last_activity DATETIME,
|
||||
role_type TEXT DEFAULT '',
|
||||
rig TEXT DEFAULT '',
|
||||
mol_type TEXT DEFAULT '',
|
||||
CHECK ((status = 'closed') = (closed_at IS NOT NULL))
|
||||
);
|
||||
INSERT INTO issues SELECT id, title, description, design, acceptance_criteria, notes, status, priority, issue_type, assignee, estimated_minutes, created_at, '', updated_at, closed_at, external_ref, compaction_level, compacted_at, original_size, compacted_at_commit, source_repo, '', NULL, '', '', '', '', 0, 0, 0, '', '', '', '', '', '', 0, '', '', '', '', NULL, '', '' FROM issues_backup;
|
||||
INSERT INTO issues SELECT id, title, description, design, acceptance_criteria, notes, status, priority, issue_type, assignee, estimated_minutes, created_at, '', updated_at, closed_at, external_ref, compaction_level, compacted_at, original_size, compacted_at_commit, source_repo, '', NULL, '', '', '', '', 0, 0, 0, '', '', '', '', '', '', 0, '', '', '', '', NULL, '', '', '' FROM issues_backup;
|
||||
DROP TABLE issues_backup;
|
||||
`)
|
||||
if err != nil {
|
||||
|
||||
@@ -279,6 +279,8 @@ func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue,
|
||||
var lastActivity sql.NullTime
|
||||
var roleType sql.NullString
|
||||
var rig sql.NullString
|
||||
// Molecule type field
|
||||
var molType sql.NullString
|
||||
|
||||
var contentHash sql.NullString
|
||||
var compactedAtCommit sql.NullString
|
||||
@@ -290,7 +292,7 @@ func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue,
|
||||
deleted_at, deleted_by, delete_reason, original_type,
|
||||
sender, ephemeral, pinned, is_template,
|
||||
await_type, await_id, timeout_ns, waiters,
|
||||
hook_bead, role_bead, agent_state, last_activity, role_type, rig
|
||||
hook_bead, role_bead, agent_state, last_activity, role_type, rig, mol_type
|
||||
FROM issues
|
||||
WHERE id = ?
|
||||
`, id).Scan(
|
||||
@@ -302,7 +304,7 @@ func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue,
|
||||
&deletedAt, &deletedBy, &deleteReason, &originalType,
|
||||
&sender, &wisp, &pinned, &isTemplate,
|
||||
&awaitType, &awaitID, &timeoutNs, &waiters,
|
||||
&hookBead, &roleBead, &agentState, &lastActivity, &roleType, &rig,
|
||||
&hookBead, &roleBead, &agentState, &lastActivity, &roleType, &rig, &molType,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
@@ -400,6 +402,10 @@ func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue,
|
||||
if rig.Valid {
|
||||
issue.Rig = rig.String
|
||||
}
|
||||
// Molecule type field
|
||||
if molType.Valid {
|
||||
issue.MolType = types.MolType(molType.String)
|
||||
}
|
||||
|
||||
// Fetch labels for this issue
|
||||
labels, err := s.GetLabels(ctx, issue.ID)
|
||||
@@ -652,6 +658,8 @@ var allowedUpdateFields = map[string]bool{
|
||||
"last_activity": true,
|
||||
"role_type": true,
|
||||
"rig": true,
|
||||
// Molecule type field
|
||||
"mol_type": true,
|
||||
}
|
||||
|
||||
// validatePriority validates a priority value
|
||||
@@ -1719,6 +1727,12 @@ func (s *SQLiteStorage) SearchIssues(ctx context.Context, query string, filter t
|
||||
args = append(args, *filter.ParentID)
|
||||
}
|
||||
|
||||
// Molecule type filtering
|
||||
if filter.MolType != nil {
|
||||
whereClauses = append(whereClauses, "mol_type = ?")
|
||||
args = append(args, string(*filter.MolType))
|
||||
}
|
||||
|
||||
whereSQL := ""
|
||||
if len(whereClauses) > 0 {
|
||||
whereSQL = "WHERE " + strings.Join(whereClauses, " AND ")
|
||||
|
||||
@@ -106,6 +106,12 @@ func (s *SQLiteStorage) GetReadyWork(ctx context.Context, filter types.WorkFilte
|
||||
args = append(args, *filter.ParentID)
|
||||
}
|
||||
|
||||
// Molecule type filtering
|
||||
if filter.MolType != nil {
|
||||
whereClauses = append(whereClauses, "i.mol_type = ?")
|
||||
args = append(args, string(*filter.MolType))
|
||||
}
|
||||
|
||||
// Build WHERE clause properly
|
||||
whereSQL := strings.Join(whereClauses, " AND ")
|
||||
|
||||
|
||||
@@ -35,6 +35,8 @@ CREATE TABLE IF NOT EXISTS issues (
|
||||
pinned INTEGER DEFAULT 0,
|
||||
-- Template field (beads-1ra)
|
||||
is_template INTEGER DEFAULT 0,
|
||||
-- Molecule type field (bd-oxgi)
|
||||
mol_type TEXT DEFAULT '',
|
||||
-- NOTE: replies_to, relates_to, duplicate_of, superseded_by removed per Decision 004
|
||||
-- These relationships are now stored in the dependencies table
|
||||
-- closed_at constraint: closed issues must have it, tombstones may retain it from before deletion
|
||||
|
||||
Reference in New Issue
Block a user