From f78fe883d0b9f0642c6c263605a5b3f47db9465f Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Thu, 25 Dec 2025 02:05:06 -0800 Subject: [PATCH] feat: Distinct prefixes for protos, molecules, wisps (bd-hobo) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added type-specific ID prefixes for better visual recognition: - `bd pour` now generates IDs with "mol-" prefix - `bd wisp create` now generates IDs with "wisp-" prefix - Regular issues continue using the configured prefix Implementation: - Added IDPrefix field to types.Issue (internal, not exported to JSONL) - Added Prefix field to CloneOptions for spawning operations - Added IDPrefix to RPC CreateArgs for daemon communication - Updated storage layer to use issue.IDPrefix when generating IDs - Updated pour.go and wisp.go to pass appropriate prefixes This enables instant visual recognition of entity types and prevents accidental modification of templates. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- cmd/bd/mol.go | 4 +++- cmd/bd/mol_test.go | 8 ++++---- cmd/bd/pour.go | 3 ++- cmd/bd/template.go | 5 ++++- cmd/bd/wisp.go | 3 ++- internal/rpc/protocol.go | 2 ++ internal/rpc/server_issues_epics.go | 2 ++ internal/storage/sqlite/queries.go | 12 +++++++++--- internal/storage/sqlite/transaction.go | 12 +++++++++--- internal/types/types.go | 1 + 10 files changed, 38 insertions(+), 14 deletions(-) diff --git a/cmd/bd/mol.go b/cmd/bd/mol.go index 01c9cf65..5cbc673a 100644 --- a/cmd/bd/mol.go +++ b/cmd/bd/mol.go @@ -66,12 +66,14 @@ See also: // This instantiates a proto (template) into a molecule (real issues). // Wraps cloneSubgraph from template.go and returns InstantiateResult. // If ephemeral is true, spawned issues are marked for bulk deletion when closed. -func spawnMolecule(ctx context.Context, s storage.Storage, subgraph *MoleculeSubgraph, vars map[string]string, assignee string, actorName string, ephemeral bool) (*InstantiateResult, error) { +// The prefix parameter overrides the default issue prefix (bd-hobo: distinct prefixes). +func spawnMolecule(ctx context.Context, s storage.Storage, subgraph *MoleculeSubgraph, vars map[string]string, assignee string, actorName string, ephemeral bool, prefix string) (*InstantiateResult, error) { opts := CloneOptions{ Vars: vars, Assignee: assignee, Actor: actorName, Wisp: ephemeral, + Prefix: prefix, } return cloneSubgraph(ctx, s, subgraph, opts) } diff --git a/cmd/bd/mol_test.go b/cmd/bd/mol_test.go index 61fc6084..cf57a18b 100644 --- a/cmd/bd/mol_test.go +++ b/cmd/bd/mol_test.go @@ -824,7 +824,7 @@ func TestSpawnWithBasicAttach(t *testing.T) { } vars := map[string]string{"feature": "auth"} - spawnResult, err := spawnMolecule(ctx, s, primarySubgraph, vars, "", "test", true) + spawnResult, err := spawnMolecule(ctx, s, primarySubgraph, vars, "", "test", true, "") if err != nil { t.Fatalf("Failed to spawn primary: %v", err) } @@ -934,7 +934,7 @@ func TestSpawnWithMultipleAttachments(t *testing.T) { t.Fatalf("Failed to load primary subgraph: %v", err) } - spawnResult, err := spawnMolecule(ctx, s, primarySubgraph, nil, "", "test", true) + spawnResult, err := spawnMolecule(ctx, s, primarySubgraph, nil, "", "test", true, "") if err != nil { t.Fatalf("Failed to spawn primary: %v", err) } @@ -1052,7 +1052,7 @@ func TestSpawnAttachTypes(t *testing.T) { t.Fatalf("Failed to load primary subgraph: %v", err) } - spawnResult, err := spawnMolecule(ctx, s, primarySubgraph, nil, "", "test", true) + spawnResult, err := spawnMolecule(ctx, s, primarySubgraph, nil, "", "test", true, "") if err != nil { t.Fatalf("Failed to spawn primary: %v", err) } @@ -1212,7 +1212,7 @@ func TestSpawnVariableAggregation(t *testing.T) { } // Spawn primary with variables - spawnResult, err := spawnMolecule(ctx, s, primarySubgraph, vars, "", "test", true) + spawnResult, err := spawnMolecule(ctx, s, primarySubgraph, vars, "", "test", true, "") if err != nil { t.Fatalf("Failed to spawn primary: %v", err) } diff --git a/cmd/bd/pour.go b/cmd/bd/pour.go index 2a2cbe61..6ca6d3d9 100644 --- a/cmd/bd/pour.go +++ b/cmd/bd/pour.go @@ -181,7 +181,8 @@ func runPour(cmd *cobra.Command, args []string) { } // Spawn as persistent mol (ephemeral=false) - result, err := spawnMolecule(ctx, store, subgraph, vars, assignee, actor, false) + // bd-hobo: Use "mol" prefix for distinct visual recognition + result, err := spawnMolecule(ctx, store, subgraph, vars, assignee, actor, false, "mol") if err != nil { fmt.Fprintf(os.Stderr, "Error pouring proto: %v\n", err) os.Exit(1) diff --git a/cmd/bd/template.go b/cmd/bd/template.go index de46f4f8..cd06290c 100644 --- a/cmd/bd/template.go +++ b/cmd/bd/template.go @@ -44,6 +44,7 @@ type CloneOptions struct { Assignee string // Assign the root epic to this agent/user Actor string // Actor performing the operation Wisp bool // If true, spawned issues are marked for bulk deletion + Prefix string // Override prefix for ID generation (bd-hobo: distinct prefixes) // Dynamic bonding fields (for Christmas Ornament pattern) ParentID string // Parent molecule ID to bond under (e.g., "patrol-x7k") @@ -711,6 +712,7 @@ func cloneSubgraphViaDaemon(client *rpc.Client, subgraph *TemplateSubgraph, opts Assignee: issueAssignee, EstimatedMinutes: oldIssue.EstimatedMinutes, Wisp: opts.Wisp, + IDPrefix: opts.Prefix, // bd-hobo: distinct prefixes for mols/wisps } // Generate custom ID for dynamic bonding if ParentID is set @@ -905,7 +907,8 @@ func cloneSubgraph(ctx context.Context, s storage.Storage, subgraph *TemplateSub IssueType: oldIssue.IssueType, Assignee: issueAssignee, EstimatedMinutes: oldIssue.EstimatedMinutes, - Wisp: opts.Wisp, // bd-2vh3: mark for cleanup when closed + Wisp: opts.Wisp, // bd-2vh3: mark for cleanup when closed + IDPrefix: opts.Prefix, // bd-hobo: distinct prefixes for mols/wisps CreatedAt: time.Now(), UpdatedAt: time.Now(), } diff --git a/cmd/bd/wisp.go b/cmd/bd/wisp.go index f03c9692..6f1d92c6 100644 --- a/cmd/bd/wisp.go +++ b/cmd/bd/wisp.go @@ -206,7 +206,8 @@ func runWispCreate(cmd *cobra.Command, args []string) { } // Spawn as wisp in main database (ephemeral=true sets Wisp flag, skips JSONL export) - result, err := spawnMolecule(ctx, store, subgraph, vars, "", actor, true) + // bd-hobo: Use "wisp" prefix for distinct visual recognition + result, err := spawnMolecule(ctx, store, subgraph, vars, "", actor, true, "wisp") if err != nil { fmt.Fprintf(os.Stderr, "Error creating wisp: %v\n", err) os.Exit(1) diff --git a/internal/rpc/protocol.go b/internal/rpc/protocol.go index 130d9938..e1e97b02 100644 --- a/internal/rpc/protocol.go +++ b/internal/rpc/protocol.go @@ -89,6 +89,8 @@ type CreateArgs struct { Sender string `json:"sender,omitempty"` // Who sent this (for messages) Wisp bool `json:"wisp,omitempty"` // Wisp = ephemeral vapor from the Steam Engine; bulk-deleted when closed RepliesTo string `json:"replies_to,omitempty"` // Issue ID for conversation threading + // ID generation (bd-hobo) + IDPrefix string `json:"id_prefix,omitempty"` // Override prefix for ID generation (mol, wisp, etc.) } // UpdateArgs represents arguments for the update operation diff --git a/internal/rpc/server_issues_epics.go b/internal/rpc/server_issues_epics.go index 89fabe21..0a10c1fe 100644 --- a/internal/rpc/server_issues_epics.go +++ b/internal/rpc/server_issues_epics.go @@ -179,6 +179,8 @@ func (s *Server) handleCreate(req *Request) Response { Sender: createArgs.Sender, Wisp: createArgs.Wisp, // NOTE: RepliesTo now handled via replies-to dependency (Decision 004) + // ID generation (bd-hobo) + IDPrefix: createArgs.IDPrefix, } // Check if any dependencies are discovered-from type diff --git a/internal/storage/sqlite/queries.go b/internal/storage/sqlite/queries.go index a0e251ce..bf792d1d 100644 --- a/internal/storage/sqlite/queries.go +++ b/internal/storage/sqlite/queries.go @@ -153,9 +153,9 @@ func (s *SQLiteStorage) CreateIssue(ctx context.Context, issue *types.Issue, act }() // Get prefix from config (needed for both ID generation and validation) - var prefix string - err = conn.QueryRowContext(ctx, `SELECT value FROM config WHERE key = ?`, "issue_prefix").Scan(&prefix) - if err == sql.ErrNoRows || prefix == "" { + var configPrefix string + err = conn.QueryRowContext(ctx, `SELECT value FROM config WHERE key = ?`, "issue_prefix").Scan(&configPrefix) + if err == sql.ErrNoRows || configPrefix == "" { // CRITICAL: Reject operation if issue_prefix config is missing (bd-166) // This prevents duplicate issues with wrong prefix return fmt.Errorf("database not initialized: issue_prefix config is missing (run 'bd init --prefix ' first)") @@ -163,6 +163,12 @@ func (s *SQLiteStorage) CreateIssue(ctx context.Context, issue *types.Issue, act return fmt.Errorf("failed to get config: %w", err) } + // Use IDPrefix override if set, otherwise use config prefix (bd-hobo) + prefix := configPrefix + if issue.IDPrefix != "" { + prefix = issue.IDPrefix + } + // Generate or validate ID if issue.ID == "" { // Generate hash-based ID with adaptive length based on database size (bd-ea2a13) diff --git a/internal/storage/sqlite/transaction.go b/internal/storage/sqlite/transaction.go index 297a064b..81fd72df 100644 --- a/internal/storage/sqlite/transaction.go +++ b/internal/storage/sqlite/transaction.go @@ -138,15 +138,21 @@ func (t *sqliteTxStorage) CreateIssue(ctx context.Context, issue *types.Issue, a } // Get prefix from config (needed for both ID generation and validation) - var prefix string - err = t.conn.QueryRowContext(ctx, `SELECT value FROM config WHERE key = ?`, "issue_prefix").Scan(&prefix) - if err == sql.ErrNoRows || prefix == "" { + var configPrefix string + err = t.conn.QueryRowContext(ctx, `SELECT value FROM config WHERE key = ?`, "issue_prefix").Scan(&configPrefix) + if err == sql.ErrNoRows || configPrefix == "" { // CRITICAL: Reject operation if issue_prefix config is missing (bd-166) return fmt.Errorf("database not initialized: issue_prefix config is missing (run 'bd init --prefix ' first)") } else if err != nil { return fmt.Errorf("failed to get config: %w", err) } + // Use IDPrefix override if set, otherwise use config prefix (bd-hobo) + prefix := configPrefix + if issue.IDPrefix != "" { + prefix = issue.IDPrefix + } + // Generate or validate ID if issue.ID == "" { // Generate hash-based ID with adaptive length based on database size (bd-ea2a13) diff --git a/internal/types/types.go b/internal/types/types.go index a762134c..cae710a7 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -32,6 +32,7 @@ type Issue struct { CompactedAtCommit *string `json:"compacted_at_commit,omitempty"` // Git commit hash when compacted OriginalSize int `json:"original_size,omitempty"` SourceRepo string `json:"-"` // Internal: Which repo owns this issue (multi-repo support) - NOT exported to JSONL + IDPrefix string `json:"-"` // Internal: Override prefix for ID generation (bd-hobo) - NOT exported to JSONL Labels []string `json:"labels,omitempty"` // Populated only for export/import Dependencies []*Dependency `json:"dependencies,omitempty"` // Populated only for export/import Comments []*Comment `json:"comments,omitempty"` // Populated only for export/import