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:
@@ -112,6 +112,14 @@ var createCmd = &cobra.Command{
|
|||||||
rigOverride, _ := cmd.Flags().GetString("rig")
|
rigOverride, _ := cmd.Flags().GetString("rig")
|
||||||
prefixOverride, _ := cmd.Flags().GetString("prefix")
|
prefixOverride, _ := cmd.Flags().GetString("prefix")
|
||||||
wisp, _ := cmd.Flags().GetBool("ephemeral")
|
wisp, _ := cmd.Flags().GetBool("ephemeral")
|
||||||
|
molTypeStr, _ := cmd.Flags().GetString("mol-type")
|
||||||
|
var molType types.MolType
|
||||||
|
if molTypeStr != "" {
|
||||||
|
molType = types.MolType(molTypeStr)
|
||||||
|
if !molType.IsValid() {
|
||||||
|
FatalError("invalid mol-type %q (must be swarm, patrol, or work)", molTypeStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle --rig or --prefix flag: create issue in a different rig
|
// Handle --rig or --prefix flag: create issue in a different rig
|
||||||
// Both flags use the same forgiving lookup (accepts rig names or prefixes)
|
// Both flags use the same forgiving lookup (accepts rig names or prefixes)
|
||||||
@@ -249,6 +257,7 @@ var createCmd = &cobra.Command{
|
|||||||
WaitsForGate: waitsForGate,
|
WaitsForGate: waitsForGate,
|
||||||
Ephemeral: wisp,
|
Ephemeral: wisp,
|
||||||
CreatedBy: getActorWithGit(),
|
CreatedBy: getActorWithGit(),
|
||||||
|
MolType: string(molType),
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := daemonClient.Create(createArgs)
|
resp, err := daemonClient.Create(createArgs)
|
||||||
@@ -295,6 +304,7 @@ var createCmd = &cobra.Command{
|
|||||||
EstimatedMinutes: estimatedMinutes,
|
EstimatedMinutes: estimatedMinutes,
|
||||||
Ephemeral: wisp,
|
Ephemeral: wisp,
|
||||||
CreatedBy: getActorWithGit(),
|
CreatedBy: getActorWithGit(),
|
||||||
|
MolType: molType,
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := rootCtx
|
ctx := rootCtx
|
||||||
@@ -476,6 +486,7 @@ func init() {
|
|||||||
createCmd.Flags().String("prefix", "", "Create issue in rig by prefix (e.g., --prefix bd- or --prefix bd or --prefix beads)")
|
createCmd.Flags().String("prefix", "", "Create issue in rig by prefix (e.g., --prefix bd- or --prefix bd or --prefix beads)")
|
||||||
createCmd.Flags().IntP("estimate", "e", 0, "Time estimate in minutes (e.g., 60 for 1 hour)")
|
createCmd.Flags().IntP("estimate", "e", 0, "Time estimate in minutes (e.g., 60 for 1 hour)")
|
||||||
createCmd.Flags().Bool("ephemeral", false, "Create as ephemeral (ephemeral, not exported to JSONL)")
|
createCmd.Flags().Bool("ephemeral", false, "Create as ephemeral (ephemeral, not exported to JSONL)")
|
||||||
|
createCmd.Flags().String("mol-type", "", "Molecule type: swarm (multi-polecat), patrol (recurring ops), work (default)")
|
||||||
// Note: --json flag is defined as a persistent flag in main.go, not here
|
// Note: --json flag is defined as a persistent flag in main.go, not here
|
||||||
rootCmd.AddCommand(createCmd)
|
rootCmd.AddCommand(createCmd)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -356,6 +356,18 @@ var listCmd = &cobra.Command{
|
|||||||
// Parent filtering
|
// Parent filtering
|
||||||
parentID, _ := cmd.Flags().GetString("parent")
|
parentID, _ := cmd.Flags().GetString("parent")
|
||||||
|
|
||||||
|
// Molecule type filtering
|
||||||
|
molTypeStr, _ := cmd.Flags().GetString("mol-type")
|
||||||
|
var molType *types.MolType
|
||||||
|
if molTypeStr != "" {
|
||||||
|
mt := types.MolType(molTypeStr)
|
||||||
|
if !mt.IsValid() {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: invalid mol-type %q (must be swarm, patrol, or work)\n", molTypeStr)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
molType = &mt
|
||||||
|
}
|
||||||
|
|
||||||
// Pretty and watch flags (GH#654)
|
// Pretty and watch flags (GH#654)
|
||||||
prettyFormat, _ := cmd.Flags().GetBool("pretty")
|
prettyFormat, _ := cmd.Flags().GetBool("pretty")
|
||||||
watchMode, _ := cmd.Flags().GetBool("watch")
|
watchMode, _ := cmd.Flags().GetBool("watch")
|
||||||
@@ -533,6 +545,11 @@ var listCmd = &cobra.Command{
|
|||||||
filter.ParentID = &parentID
|
filter.ParentID = &parentID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Molecule type filtering
|
||||||
|
if molType != nil {
|
||||||
|
filter.MolType = molType
|
||||||
|
}
|
||||||
|
|
||||||
// Check database freshness before reading
|
// Check database freshness before reading
|
||||||
// Skip check when using daemon (daemon auto-imports on staleness)
|
// Skip check when using daemon (daemon auto-imports on staleness)
|
||||||
ctx := rootCtx
|
ctx := rootCtx
|
||||||
@@ -906,6 +923,9 @@ func init() {
|
|||||||
// Parent filtering: filter children by parent issue
|
// Parent filtering: filter children by parent issue
|
||||||
listCmd.Flags().String("parent", "", "Filter by parent issue ID (shows children of specified issue)")
|
listCmd.Flags().String("parent", "", "Filter by parent issue ID (shows children of specified issue)")
|
||||||
|
|
||||||
|
// Molecule type filtering
|
||||||
|
listCmd.Flags().String("mol-type", "", "Filter by molecule type: swarm, patrol, or work")
|
||||||
|
|
||||||
// Pretty and watch flags (GH#654)
|
// Pretty and watch flags (GH#654)
|
||||||
listCmd.Flags().Bool("pretty", false, "Display issues in a tree format with status/priority symbols")
|
listCmd.Flags().Bool("pretty", false, "Display issues in a tree format with status/priority symbols")
|
||||||
listCmd.Flags().BoolP("watch", "w", false, "Watch for changes and auto-update display (implies --pretty)")
|
listCmd.Flags().BoolP("watch", "w", false, "Watch for changes and auto-update display (implies --pretty)")
|
||||||
|
|||||||
@@ -40,6 +40,16 @@ This is useful for agents executing molecules to see which steps can run next.`,
|
|||||||
labelsAny, _ := cmd.Flags().GetStringSlice("label-any")
|
labelsAny, _ := cmd.Flags().GetStringSlice("label-any")
|
||||||
issueType, _ := cmd.Flags().GetString("type")
|
issueType, _ := cmd.Flags().GetString("type")
|
||||||
parentID, _ := cmd.Flags().GetString("parent")
|
parentID, _ := cmd.Flags().GetString("parent")
|
||||||
|
molTypeStr, _ := cmd.Flags().GetString("mol-type")
|
||||||
|
var molType *types.MolType
|
||||||
|
if molTypeStr != "" {
|
||||||
|
mt := types.MolType(molTypeStr)
|
||||||
|
if !mt.IsValid() {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: invalid mol-type %q (must be swarm, patrol, or work)\n", molTypeStr)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
molType = &mt
|
||||||
|
}
|
||||||
// Use global jsonOutput set by PersistentPreRun (respects config.yaml + env vars)
|
// Use global jsonOutput set by PersistentPreRun (respects config.yaml + env vars)
|
||||||
|
|
||||||
// Normalize labels: trim, dedupe, remove empty
|
// Normalize labels: trim, dedupe, remove empty
|
||||||
@@ -73,6 +83,9 @@ This is useful for agents executing molecules to see which steps can run next.`,
|
|||||||
if parentID != "" {
|
if parentID != "" {
|
||||||
filter.ParentID = &parentID
|
filter.ParentID = &parentID
|
||||||
}
|
}
|
||||||
|
if molType != nil {
|
||||||
|
filter.MolType = molType
|
||||||
|
}
|
||||||
// Validate sort policy
|
// Validate sort policy
|
||||||
if !filter.SortPolicy.IsValid() {
|
if !filter.SortPolicy.IsValid() {
|
||||||
fmt.Fprintf(os.Stderr, "Error: invalid sort policy '%s'. Valid values: hybrid, priority, oldest\n", sortPolicy)
|
fmt.Fprintf(os.Stderr, "Error: invalid sort policy '%s'. Valid values: hybrid, priority, oldest\n", sortPolicy)
|
||||||
@@ -89,6 +102,7 @@ This is useful for agents executing molecules to see which steps can run next.`,
|
|||||||
Labels: labels,
|
Labels: labels,
|
||||||
LabelsAny: labelsAny,
|
LabelsAny: labelsAny,
|
||||||
ParentID: parentID,
|
ParentID: parentID,
|
||||||
|
MolType: molTypeStr,
|
||||||
}
|
}
|
||||||
if cmd.Flags().Changed("priority") {
|
if cmd.Flags().Changed("priority") {
|
||||||
priority, _ := cmd.Flags().GetInt("priority")
|
priority, _ := cmd.Flags().GetInt("priority")
|
||||||
@@ -421,6 +435,7 @@ func init() {
|
|||||||
readyCmd.Flags().StringP("type", "t", "", "Filter by issue type (task, bug, feature, epic, merge-request)")
|
readyCmd.Flags().StringP("type", "t", "", "Filter by issue type (task, bug, feature, epic, merge-request)")
|
||||||
readyCmd.Flags().String("mol", "", "Filter to steps within a specific molecule")
|
readyCmd.Flags().String("mol", "", "Filter to steps within a specific molecule")
|
||||||
readyCmd.Flags().String("parent", "", "Filter to descendants of this bead/epic")
|
readyCmd.Flags().String("parent", "", "Filter to descendants of this bead/epic")
|
||||||
|
readyCmd.Flags().String("mol-type", "", "Filter by molecule type: swarm, patrol, or work")
|
||||||
rootCmd.AddCommand(readyCmd)
|
rootCmd.AddCommand(readyCmd)
|
||||||
blockedCmd.Flags().String("parent", "", "Filter to descendants of this bead/epic")
|
blockedCmd.Flags().String("parent", "", "Filter to descendants of this bead/epic")
|
||||||
rootCmd.AddCommand(blockedCmd)
|
rootCmd.AddCommand(blockedCmd)
|
||||||
|
|||||||
@@ -95,6 +95,8 @@ type CreateArgs struct {
|
|||||||
// ID generation
|
// ID generation
|
||||||
IDPrefix string `json:"id_prefix,omitempty"` // Override prefix for ID generation (mol, eph, etc.)
|
IDPrefix string `json:"id_prefix,omitempty"` // Override prefix for ID generation (mol, eph, etc.)
|
||||||
CreatedBy string `json:"created_by,omitempty"` // Who created the issue
|
CreatedBy string `json:"created_by,omitempty"` // Who created the issue
|
||||||
|
// Molecule type (for swarm coordination)
|
||||||
|
MolType string `json:"mol_type,omitempty"` // swarm, patrol, or work (default)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateArgs represents arguments for the update operation
|
// UpdateArgs represents arguments for the update operation
|
||||||
@@ -203,6 +205,9 @@ type ListArgs struct {
|
|||||||
|
|
||||||
// Ephemeral filtering
|
// Ephemeral filtering
|
||||||
Ephemeral *bool `json:"ephemeral,omitempty"`
|
Ephemeral *bool `json:"ephemeral,omitempty"`
|
||||||
|
|
||||||
|
// Molecule type filtering
|
||||||
|
MolType string `json:"mol_type,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CountArgs represents arguments for the count operation
|
// CountArgs represents arguments for the count operation
|
||||||
@@ -264,6 +269,7 @@ type ReadyArgs struct {
|
|||||||
Labels []string `json:"labels,omitempty"`
|
Labels []string `json:"labels,omitempty"`
|
||||||
LabelsAny []string `json:"labels_any,omitempty"`
|
LabelsAny []string `json:"labels_any,omitempty"`
|
||||||
ParentID string `json:"parent_id,omitempty"` // Filter to descendants of this bead/epic
|
ParentID string `json:"parent_id,omitempty"` // Filter to descendants of this bead/epic
|
||||||
|
MolType string `json:"mol_type,omitempty"` // Filter by molecule type: swarm, patrol, or work
|
||||||
}
|
}
|
||||||
|
|
||||||
// BlockedArgs represents arguments for the blocked operation
|
// BlockedArgs represents arguments for the blocked operation
|
||||||
|
|||||||
@@ -196,6 +196,8 @@ func (s *Server) handleCreate(req *Request) Response {
|
|||||||
// ID generation
|
// ID generation
|
||||||
IDPrefix: createArgs.IDPrefix,
|
IDPrefix: createArgs.IDPrefix,
|
||||||
CreatedBy: createArgs.CreatedBy,
|
CreatedBy: createArgs.CreatedBy,
|
||||||
|
// Molecule type
|
||||||
|
MolType: types.MolType(createArgs.MolType),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if any dependencies are discovered-from type
|
// Check if any dependencies are discovered-from type
|
||||||
@@ -918,6 +920,12 @@ func (s *Server) handleList(req *Request) Response {
|
|||||||
// Ephemeral filtering
|
// Ephemeral filtering
|
||||||
filter.Ephemeral = listArgs.Ephemeral
|
filter.Ephemeral = listArgs.Ephemeral
|
||||||
|
|
||||||
|
// Molecule type filtering
|
||||||
|
if listArgs.MolType != "" {
|
||||||
|
molType := types.MolType(listArgs.MolType)
|
||||||
|
filter.MolType = &molType
|
||||||
|
}
|
||||||
|
|
||||||
// Guard against excessive ID lists to avoid SQLite parameter limits
|
// Guard against excessive ID lists to avoid SQLite parameter limits
|
||||||
const maxIDs = 1000
|
const maxIDs = 1000
|
||||||
if len(filter.IDs) > maxIDs {
|
if len(filter.IDs) > maxIDs {
|
||||||
@@ -1353,6 +1361,10 @@ func (s *Server) handleReady(req *Request) Response {
|
|||||||
if readyArgs.ParentID != "" {
|
if readyArgs.ParentID != "" {
|
||||||
wf.ParentID = &readyArgs.ParentID
|
wf.ParentID = &readyArgs.ParentID
|
||||||
}
|
}
|
||||||
|
if readyArgs.MolType != "" {
|
||||||
|
molType := types.MolType(readyArgs.MolType)
|
||||||
|
wf.MolType = &molType
|
||||||
|
}
|
||||||
|
|
||||||
ctx := s.reqCtx(req)
|
ctx := s.reqCtx(req)
|
||||||
issues, err := store.GetReadyWork(ctx, wf)
|
issues, err := store.GetReadyWork(ctx, wf)
|
||||||
|
|||||||
@@ -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,
|
created_at, created_by, updated_at, closed_at, external_ref, source_repo, close_reason,
|
||||||
deleted_at, deleted_by, delete_reason, original_type,
|
deleted_at, deleted_by, delete_reason, original_type,
|
||||||
sender, ephemeral, pinned, is_template,
|
sender, ephemeral, pinned, is_template,
|
||||||
await_type, await_id, timeout_ns, waiters
|
await_type, await_id, timeout_ns, waiters, mol_type
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`,
|
`,
|
||||||
issue.ID, issue.ContentHash, issue.Title, issue.Description, issue.Design,
|
issue.ID, issue.ContentHash, issue.Title, issue.Description, issue.Design,
|
||||||
issue.AcceptanceCriteria, issue.Notes, issue.Status,
|
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.DeletedAt, issue.DeletedBy, issue.DeleteReason, issue.OriginalType,
|
||||||
issue.Sender, wisp, pinned, isTemplate,
|
issue.Sender, wisp, pinned, isTemplate,
|
||||||
issue.AwaitType, issue.AwaitID, int64(issue.Timeout), formatJSONStringArray(issue.Waiters),
|
issue.AwaitType, issue.AwaitID, int64(issue.Timeout), formatJSONStringArray(issue.Waiters),
|
||||||
|
string(issue.MolType),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// INSERT OR IGNORE should handle duplicates, but driver may still return error
|
// 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,
|
created_at, created_by, updated_at, closed_at, external_ref, source_repo, close_reason,
|
||||||
deleted_at, deleted_by, delete_reason, original_type,
|
deleted_at, deleted_by, delete_reason, original_type,
|
||||||
sender, ephemeral, pinned, is_template,
|
sender, ephemeral, pinned, is_template,
|
||||||
await_type, await_id, timeout_ns, waiters
|
await_type, await_id, timeout_ns, waiters, mol_type
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`)
|
`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to prepare statement: %w", err)
|
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.DeletedAt, issue.DeletedBy, issue.DeleteReason, issue.OriginalType,
|
||||||
issue.Sender, wisp, pinned, isTemplate,
|
issue.Sender, wisp, pinned, isTemplate,
|
||||||
issue.AwaitType, issue.AwaitID, int64(issue.Timeout), formatJSONStringArray(issue.Waiters),
|
issue.AwaitType, issue.AwaitID, int64(issue.Timeout), formatJSONStringArray(issue.Waiters),
|
||||||
|
string(issue.MolType),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// INSERT OR IGNORE should handle duplicates, but driver may still return error
|
// INSERT OR IGNORE should handle duplicates, but driver may still return error
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ var migrationsList = []Migration{
|
|||||||
{"tombstone_closed_at", migrations.MigrateTombstoneClosedAt},
|
{"tombstone_closed_at", migrations.MigrateTombstoneClosedAt},
|
||||||
{"created_by_column", migrations.MigrateCreatedByColumn},
|
{"created_by_column", migrations.MigrateCreatedByColumn},
|
||||||
{"agent_fields", migrations.MigrateAgentFields},
|
{"agent_fields", migrations.MigrateAgentFields},
|
||||||
|
{"mol_type_column", migrations.MigrateMolTypeColumn},
|
||||||
}
|
}
|
||||||
|
|
||||||
// MigrationInfo contains metadata about a migration for inspection
|
// 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",
|
"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",
|
"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",
|
"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 {
|
if desc, ok := descriptions[name]; ok {
|
||||||
return desc
|
return desc
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
last_activity DATETIME,
|
||||||
role_type TEXT DEFAULT '',
|
role_type TEXT DEFAULT '',
|
||||||
rig TEXT DEFAULT '',
|
rig TEXT DEFAULT '',
|
||||||
|
mol_type TEXT DEFAULT '',
|
||||||
CHECK ((status = 'closed') = (closed_at IS NOT NULL))
|
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;
|
DROP TABLE issues_backup;
|
||||||
`)
|
`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -279,6 +279,8 @@ func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue,
|
|||||||
var lastActivity sql.NullTime
|
var lastActivity sql.NullTime
|
||||||
var roleType sql.NullString
|
var roleType sql.NullString
|
||||||
var rig sql.NullString
|
var rig sql.NullString
|
||||||
|
// Molecule type field
|
||||||
|
var molType sql.NullString
|
||||||
|
|
||||||
var contentHash sql.NullString
|
var contentHash sql.NullString
|
||||||
var compactedAtCommit 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,
|
deleted_at, deleted_by, delete_reason, original_type,
|
||||||
sender, ephemeral, pinned, is_template,
|
sender, ephemeral, pinned, is_template,
|
||||||
await_type, await_id, timeout_ns, waiters,
|
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
|
FROM issues
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`, id).Scan(
|
`, id).Scan(
|
||||||
@@ -302,7 +304,7 @@ func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue,
|
|||||||
&deletedAt, &deletedBy, &deleteReason, &originalType,
|
&deletedAt, &deletedBy, &deleteReason, &originalType,
|
||||||
&sender, &wisp, &pinned, &isTemplate,
|
&sender, &wisp, &pinned, &isTemplate,
|
||||||
&awaitType, &awaitID, &timeoutNs, &waiters,
|
&awaitType, &awaitID, &timeoutNs, &waiters,
|
||||||
&hookBead, &roleBead, &agentState, &lastActivity, &roleType, &rig,
|
&hookBead, &roleBead, &agentState, &lastActivity, &roleType, &rig, &molType,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
@@ -400,6 +402,10 @@ func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue,
|
|||||||
if rig.Valid {
|
if rig.Valid {
|
||||||
issue.Rig = rig.String
|
issue.Rig = rig.String
|
||||||
}
|
}
|
||||||
|
// Molecule type field
|
||||||
|
if molType.Valid {
|
||||||
|
issue.MolType = types.MolType(molType.String)
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch labels for this issue
|
// Fetch labels for this issue
|
||||||
labels, err := s.GetLabels(ctx, issue.ID)
|
labels, err := s.GetLabels(ctx, issue.ID)
|
||||||
@@ -652,6 +658,8 @@ var allowedUpdateFields = map[string]bool{
|
|||||||
"last_activity": true,
|
"last_activity": true,
|
||||||
"role_type": true,
|
"role_type": true,
|
||||||
"rig": true,
|
"rig": true,
|
||||||
|
// Molecule type field
|
||||||
|
"mol_type": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
// validatePriority validates a priority value
|
// 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)
|
args = append(args, *filter.ParentID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Molecule type filtering
|
||||||
|
if filter.MolType != nil {
|
||||||
|
whereClauses = append(whereClauses, "mol_type = ?")
|
||||||
|
args = append(args, string(*filter.MolType))
|
||||||
|
}
|
||||||
|
|
||||||
whereSQL := ""
|
whereSQL := ""
|
||||||
if len(whereClauses) > 0 {
|
if len(whereClauses) > 0 {
|
||||||
whereSQL = "WHERE " + strings.Join(whereClauses, " AND ")
|
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)
|
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
|
// Build WHERE clause properly
|
||||||
whereSQL := strings.Join(whereClauses, " AND ")
|
whereSQL := strings.Join(whereClauses, " AND ")
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ CREATE TABLE IF NOT EXISTS issues (
|
|||||||
pinned INTEGER DEFAULT 0,
|
pinned INTEGER DEFAULT 0,
|
||||||
-- Template field (beads-1ra)
|
-- Template field (beads-1ra)
|
||||||
is_template INTEGER DEFAULT 0,
|
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
|
-- NOTE: replies_to, relates_to, duplicate_of, superseded_by removed per Decision 004
|
||||||
-- These relationships are now stored in the dependencies table
|
-- These relationships are now stored in the dependencies table
|
||||||
-- closed_at constraint: closed issues must have it, tombstones may retain it from before deletion
|
-- closed_at constraint: closed issues must have it, tombstones may retain it from before deletion
|
||||||
|
|||||||
@@ -97,6 +97,9 @@ type Issue struct {
|
|||||||
LastActivity *time.Time `json:"last_activity,omitempty"` // Updated on each action (timeout detection)
|
LastActivity *time.Time `json:"last_activity,omitempty"` // Updated on each action (timeout detection)
|
||||||
RoleType string `json:"role_type,omitempty"` // Role: polecat|crew|witness|refinery|mayor|deacon
|
RoleType string `json:"role_type,omitempty"` // Role: polecat|crew|witness|refinery|mayor|deacon
|
||||||
Rig string `json:"rig,omitempty"` // Rig name (empty for town-level agents)
|
Rig string `json:"rig,omitempty"` // Rig name (empty for town-level agents)
|
||||||
|
|
||||||
|
// ===== Molecule Type Fields (swarm coordination) =====
|
||||||
|
MolType MolType `json:"mol_type,omitempty"` // Molecule type: swarm|patrol|work (empty = work)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ComputeContentHash creates a deterministic hash of the issue's content.
|
// ComputeContentHash creates a deterministic hash of the issue's content.
|
||||||
@@ -156,6 +159,9 @@ func (i *Issue) ComputeContentHash() string {
|
|||||||
w.str(i.RoleType)
|
w.str(i.RoleType)
|
||||||
w.str(i.Rig)
|
w.str(i.Rig)
|
||||||
|
|
||||||
|
// Molecule type
|
||||||
|
w.str(string(i.MolType))
|
||||||
|
|
||||||
return fmt.Sprintf("%x", h.Sum(nil))
|
return fmt.Sprintf("%x", h.Sum(nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,6 +428,25 @@ func (s AgentState) IsValid() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MolType categorizes the molecule type for swarm coordination
|
||||||
|
type MolType string
|
||||||
|
|
||||||
|
// MolType constants
|
||||||
|
const (
|
||||||
|
MolTypeSwarm MolType = "swarm" // Swarm molecule: coordinated multi-polecat work
|
||||||
|
MolTypePatrol MolType = "patrol" // Patrol molecule: recurring operational work (Witness, Deacon, etc.)
|
||||||
|
MolTypeWork MolType = "work" // Work molecule: regular polecat work (default)
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsValid checks if the mol type value is valid
|
||||||
|
func (m MolType) IsValid() bool {
|
||||||
|
switch m {
|
||||||
|
case MolTypeSwarm, MolTypePatrol, MolTypeWork, "":
|
||||||
|
return true // empty is valid (defaults to work)
|
||||||
|
}
|
||||||
|
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"`
|
||||||
@@ -680,6 +705,9 @@ type IssueFilter struct {
|
|||||||
|
|
||||||
// Parent filtering: filter children by parent issue ID
|
// Parent filtering: filter children by parent issue ID
|
||||||
ParentID *string // Filter by parent issue (via parent-child dependency)
|
ParentID *string // Filter by parent issue (via parent-child dependency)
|
||||||
|
|
||||||
|
// Molecule type filtering
|
||||||
|
MolType *MolType // Filter by molecule type (nil = any, swarm/patrol/work)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SortPolicy determines how ready work is ordered
|
// SortPolicy determines how ready work is ordered
|
||||||
@@ -724,6 +752,9 @@ type WorkFilter struct {
|
|||||||
|
|
||||||
// Parent filtering: filter to descendants of a bead/epic (recursive)
|
// Parent filtering: filter to descendants of a bead/epic (recursive)
|
||||||
ParentID *string // Show all descendants of this issue
|
ParentID *string // Show all descendants of this issue
|
||||||
|
|
||||||
|
// Molecule type filtering
|
||||||
|
MolType *MolType // Filter by molecule type (nil = any, swarm/patrol/work)
|
||||||
}
|
}
|
||||||
|
|
||||||
// StaleFilter is used to filter stale issue queries
|
// StaleFilter is used to filter stale issue queries
|
||||||
|
|||||||
Reference in New Issue
Block a user