feat: Distinct prefixes for protos, molecules, wisps (bd-hobo)
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 <noreply@anthropic.com>
This commit is contained in:
@@ -66,12 +66,14 @@ See also:
|
|||||||
// This instantiates a proto (template) into a molecule (real issues).
|
// This instantiates a proto (template) into a molecule (real issues).
|
||||||
// Wraps cloneSubgraph from template.go and returns InstantiateResult.
|
// Wraps cloneSubgraph from template.go and returns InstantiateResult.
|
||||||
// If ephemeral is true, spawned issues are marked for bulk deletion when closed.
|
// 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{
|
opts := CloneOptions{
|
||||||
Vars: vars,
|
Vars: vars,
|
||||||
Assignee: assignee,
|
Assignee: assignee,
|
||||||
Actor: actorName,
|
Actor: actorName,
|
||||||
Wisp: ephemeral,
|
Wisp: ephemeral,
|
||||||
|
Prefix: prefix,
|
||||||
}
|
}
|
||||||
return cloneSubgraph(ctx, s, subgraph, opts)
|
return cloneSubgraph(ctx, s, subgraph, opts)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -824,7 +824,7 @@ func TestSpawnWithBasicAttach(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
vars := map[string]string{"feature": "auth"}
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("Failed to spawn primary: %v", err)
|
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)
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("Failed to spawn primary: %v", err)
|
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)
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("Failed to spawn primary: %v", err)
|
t.Fatalf("Failed to spawn primary: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1212,7 +1212,7 @@ func TestSpawnVariableAggregation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Spawn primary with variables
|
// 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 {
|
if err != nil {
|
||||||
t.Fatalf("Failed to spawn primary: %v", err)
|
t.Fatalf("Failed to spawn primary: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -181,7 +181,8 @@ func runPour(cmd *cobra.Command, args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Spawn as persistent mol (ephemeral=false)
|
// 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 {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error pouring proto: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error pouring proto: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ type CloneOptions struct {
|
|||||||
Assignee string // Assign the root epic to this agent/user
|
Assignee string // Assign the root epic to this agent/user
|
||||||
Actor string // Actor performing the operation
|
Actor string // Actor performing the operation
|
||||||
Wisp bool // If true, spawned issues are marked for bulk deletion
|
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)
|
// Dynamic bonding fields (for Christmas Ornament pattern)
|
||||||
ParentID string // Parent molecule ID to bond under (e.g., "patrol-x7k")
|
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,
|
Assignee: issueAssignee,
|
||||||
EstimatedMinutes: oldIssue.EstimatedMinutes,
|
EstimatedMinutes: oldIssue.EstimatedMinutes,
|
||||||
Wisp: opts.Wisp,
|
Wisp: opts.Wisp,
|
||||||
|
IDPrefix: opts.Prefix, // bd-hobo: distinct prefixes for mols/wisps
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate custom ID for dynamic bonding if ParentID is set
|
// 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,
|
IssueType: oldIssue.IssueType,
|
||||||
Assignee: issueAssignee,
|
Assignee: issueAssignee,
|
||||||
EstimatedMinutes: oldIssue.EstimatedMinutes,
|
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(),
|
CreatedAt: time.Now(),
|
||||||
UpdatedAt: time.Now(),
|
UpdatedAt: time.Now(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
// 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 {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error creating wisp: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error creating wisp: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
@@ -89,6 +89,8 @@ type CreateArgs struct {
|
|||||||
Sender string `json:"sender,omitempty"` // Who sent this (for messages)
|
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
|
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
|
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
|
// UpdateArgs represents arguments for the update operation
|
||||||
|
|||||||
@@ -179,6 +179,8 @@ func (s *Server) handleCreate(req *Request) Response {
|
|||||||
Sender: createArgs.Sender,
|
Sender: createArgs.Sender,
|
||||||
Wisp: createArgs.Wisp,
|
Wisp: createArgs.Wisp,
|
||||||
// NOTE: RepliesTo now handled via replies-to dependency (Decision 004)
|
// 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
|
// Check if any dependencies are discovered-from type
|
||||||
|
|||||||
@@ -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)
|
// Get prefix from config (needed for both ID generation and validation)
|
||||||
var prefix string
|
var configPrefix string
|
||||||
err = conn.QueryRowContext(ctx, `SELECT value FROM config WHERE key = ?`, "issue_prefix").Scan(&prefix)
|
err = conn.QueryRowContext(ctx, `SELECT value FROM config WHERE key = ?`, "issue_prefix").Scan(&configPrefix)
|
||||||
if err == sql.ErrNoRows || prefix == "" {
|
if err == sql.ErrNoRows || configPrefix == "" {
|
||||||
// CRITICAL: Reject operation if issue_prefix config is missing (bd-166)
|
// CRITICAL: Reject operation if issue_prefix config is missing (bd-166)
|
||||||
// This prevents duplicate issues with wrong prefix
|
// This prevents duplicate issues with wrong prefix
|
||||||
return fmt.Errorf("database not initialized: issue_prefix config is missing (run 'bd init --prefix <prefix>' first)")
|
return fmt.Errorf("database not initialized: issue_prefix config is missing (run 'bd init --prefix <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)
|
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
|
// Generate or validate ID
|
||||||
if issue.ID == "" {
|
if issue.ID == "" {
|
||||||
// Generate hash-based ID with adaptive length based on database size (bd-ea2a13)
|
// Generate hash-based ID with adaptive length based on database size (bd-ea2a13)
|
||||||
|
|||||||
@@ -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)
|
// Get prefix from config (needed for both ID generation and validation)
|
||||||
var prefix string
|
var configPrefix string
|
||||||
err = t.conn.QueryRowContext(ctx, `SELECT value FROM config WHERE key = ?`, "issue_prefix").Scan(&prefix)
|
err = t.conn.QueryRowContext(ctx, `SELECT value FROM config WHERE key = ?`, "issue_prefix").Scan(&configPrefix)
|
||||||
if err == sql.ErrNoRows || prefix == "" {
|
if err == sql.ErrNoRows || configPrefix == "" {
|
||||||
// CRITICAL: Reject operation if issue_prefix config is missing (bd-166)
|
// 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 <prefix>' first)")
|
return fmt.Errorf("database not initialized: issue_prefix config is missing (run 'bd init --prefix <prefix>' first)")
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return fmt.Errorf("failed to get config: %w", err)
|
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
|
// Generate or validate ID
|
||||||
if issue.ID == "" {
|
if issue.ID == "" {
|
||||||
// Generate hash-based ID with adaptive length based on database size (bd-ea2a13)
|
// Generate hash-based ID with adaptive length based on database size (bd-ea2a13)
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ type Issue struct {
|
|||||||
CompactedAtCommit *string `json:"compacted_at_commit,omitempty"` // Git commit hash when compacted
|
CompactedAtCommit *string `json:"compacted_at_commit,omitempty"` // Git commit hash when compacted
|
||||||
OriginalSize int `json:"original_size,omitempty"`
|
OriginalSize int `json:"original_size,omitempty"`
|
||||||
SourceRepo string `json:"-"` // Internal: Which repo owns this issue (multi-repo support) - NOT exported to JSONL
|
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
|
Labels []string `json:"labels,omitempty"` // Populated only for export/import
|
||||||
Dependencies []*Dependency `json:"dependencies,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
|
Comments []*Comment `json:"comments,omitempty"` // Populated only for export/import
|
||||||
|
|||||||
Reference in New Issue
Block a user