diff --git a/cmd/bd/create.go b/cmd/bd/create.go index acc4423d..f7bb8cd0 100644 --- a/cmd/bd/create.go +++ b/cmd/bd/create.go @@ -199,6 +199,7 @@ var createCmd = &cobra.Command{ ExternalRef: externalRefPtr, Ephemeral: wisp, CreatedBy: getActorWithGit(), + Owner: getOwner(), MolType: molType, RoleType: roleType, Rig: agentRig, @@ -423,6 +424,7 @@ var createCmd = &cobra.Command{ WaitsForGate: waitsForGate, Ephemeral: wisp, CreatedBy: getActorWithGit(), + Owner: getOwner(), MolType: string(molType), RoleType: roleType, Rig: agentRig, @@ -482,6 +484,7 @@ var createCmd = &cobra.Command{ EstimatedMinutes: estimatedMinutes, Ephemeral: wisp, CreatedBy: getActorWithGit(), + Owner: getOwner(), MolType: molType, RoleType: roleType, Rig: agentRig, @@ -808,6 +811,7 @@ func createInRig(cmd *cobra.Command, rigName, title, description, issueType stri ExternalRef: externalRefPtr, Ephemeral: wisp, CreatedBy: getActorWithGit(), + Owner: getOwner(), // Event fields (bd-xwvo fix) EventKind: eventCategory, Actor: eventActor, diff --git a/cmd/bd/main.go b/cmd/bd/main.go index ad0e78cc..c2eec253 100644 --- a/cmd/bd/main.go +++ b/cmd/bd/main.go @@ -143,6 +143,27 @@ func getActorWithGit() string { return "unknown" } +// getOwner returns the human owner for CV attribution. +// Priority: GIT_AUTHOR_EMAIL env > git config user.email > "" (empty) +// This is the foundation for HOP CV (curriculum vitae) chains per Decision 008. +// Unlike actor (which tracks who executed), owner tracks the human responsible. +func getOwner() string { + // Check GIT_AUTHOR_EMAIL first - this is set during git commit operations + if authorEmail := os.Getenv("GIT_AUTHOR_EMAIL"); authorEmail != "" { + return authorEmail + } + + // Fall back to git config user.email - the natural default + if out, err := exec.Command("git", "config", "user.email").Output(); err == nil { + if gitEmail := strings.TrimSpace(string(out)); gitEmail != "" { + return gitEmail + } + } + + // Return empty if no email found (owner is optional) + return "" +} + func init() { // Initialize viper configuration if err := config.Initialize(); err != nil { diff --git a/internal/rpc/protocol.go b/internal/rpc/protocol.go index 14612132..25c04513 100644 --- a/internal/rpc/protocol.go +++ b/internal/rpc/protocol.go @@ -98,6 +98,7 @@ type CreateArgs struct { // ID generation IDPrefix string `json:"id_prefix,omitempty"` // Override prefix for ID generation (mol, eph, etc.) CreatedBy string `json:"created_by,omitempty"` // Who created the issue + Owner string `json:"owner,omitempty"` // Human owner for CV attribution (git author email) // Molecule type (for swarm coordination) MolType string `json:"mol_type,omitempty"` // swarm, patrol, or work (default) // Agent identity fields (only valid when IssueType == "agent") diff --git a/internal/rpc/server_issues_epics.go b/internal/rpc/server_issues_epics.go index 5dd4b3f0..01f80791 100644 --- a/internal/rpc/server_issues_epics.go +++ b/internal/rpc/server_issues_epics.go @@ -306,6 +306,7 @@ func (s *Server) handleCreate(req *Request) Response { // ID generation IDPrefix: createArgs.IDPrefix, CreatedBy: createArgs.CreatedBy, + Owner: createArgs.Owner, // Molecule type MolType: types.MolType(createArgs.MolType), // Agent identity fields diff --git a/internal/storage/sqlite/dependencies.go b/internal/storage/sqlite/dependencies.go index fb516786..d9e3dbc4 100644 --- a/internal/storage/sqlite/dependencies.go +++ b/internal/storage/sqlite/dependencies.go @@ -247,7 +247,7 @@ func (s *SQLiteStorage) GetDependenciesWithMetadata(ctx context.Context, issueID rows, err := s.db.QueryContext(ctx, ` 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.created_at, i.created_by, i.updated_at, i.closed_at, i.external_ref, i.source_repo, + i.created_at, i.created_by, i.owner, i.updated_at, i.closed_at, i.external_ref, i.source_repo, i.deleted_at, i.deleted_by, i.delete_reason, i.original_type, i.sender, i.ephemeral, i.pinned, i.is_template, i.await_type, i.await_id, i.timeout_ns, i.waiters, @@ -270,7 +270,7 @@ func (s *SQLiteStorage) GetDependentsWithMetadata(ctx context.Context, issueID s rows, err := s.db.QueryContext(ctx, ` 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.created_at, i.created_by, i.updated_at, i.closed_at, i.external_ref, i.source_repo, + i.created_at, i.created_by, i.owner, i.updated_at, i.closed_at, i.external_ref, i.source_repo, i.deleted_at, i.deleted_by, i.delete_reason, i.original_type, i.sender, i.ephemeral, i.pinned, i.is_template, i.await_type, i.await_id, i.timeout_ns, i.waiters, @@ -864,6 +864,7 @@ func (s *SQLiteStorage) scanIssues(ctx context.Context, rows *sql.Rows) ([]*type var closedAt sql.NullTime var estimatedMinutes sql.NullInt64 var assignee sql.NullString + var owner sql.NullString var externalRef sql.NullString var sourceRepo sql.NullString var closeReason sql.NullString @@ -888,7 +889,7 @@ func (s *SQLiteStorage) scanIssues(ctx context.Context, rows *sql.Rows) ([]*type &issue.ID, &contentHash, &issue.Title, &issue.Description, &issue.Design, &issue.AcceptanceCriteria, &issue.Notes, &issue.Status, &issue.Priority, &issue.IssueType, &assignee, &estimatedMinutes, - &issue.CreatedAt, &issue.CreatedBy, &issue.UpdatedAt, &closedAt, &externalRef, &sourceRepo, &closeReason, + &issue.CreatedAt, &issue.CreatedBy, &owner, &issue.UpdatedAt, &closedAt, &externalRef, &sourceRepo, &closeReason, &deletedAt, &deletedBy, &deleteReason, &originalType, &sender, &wisp, &pinned, &isTemplate, &awaitType, &awaitID, &timeoutNs, &waiters, @@ -910,6 +911,9 @@ func (s *SQLiteStorage) scanIssues(ctx context.Context, rows *sql.Rows) ([]*type if assignee.Valid { issue.Assignee = assignee.String } + if owner.Valid { + issue.Owner = owner.String + } if externalRef.Valid { issue.ExternalRef = &externalRef.String } @@ -987,6 +991,7 @@ func (s *SQLiteStorage) scanIssuesWithDependencyType(ctx context.Context, rows * var closedAt sql.NullTime var estimatedMinutes sql.NullInt64 var assignee sql.NullString + var owner sql.NullString var externalRef sql.NullString var sourceRepo sql.NullString var deletedAt sql.NullString // TEXT column, not DATETIME - must parse manually @@ -1011,7 +1016,7 @@ func (s *SQLiteStorage) scanIssuesWithDependencyType(ctx context.Context, rows * &issue.ID, &contentHash, &issue.Title, &issue.Description, &issue.Design, &issue.AcceptanceCriteria, &issue.Notes, &issue.Status, &issue.Priority, &issue.IssueType, &assignee, &estimatedMinutes, - &issue.CreatedAt, &issue.CreatedBy, &issue.UpdatedAt, &closedAt, &externalRef, &sourceRepo, + &issue.CreatedAt, &issue.CreatedBy, &owner, &issue.UpdatedAt, &closedAt, &externalRef, &sourceRepo, &deletedAt, &deletedBy, &deleteReason, &originalType, &sender, &wisp, &pinned, &isTemplate, &awaitType, &awaitID, &timeoutNs, &waiters, @@ -1034,6 +1039,9 @@ func (s *SQLiteStorage) scanIssuesWithDependencyType(ctx context.Context, rows * if assignee.Valid { issue.Assignee = assignee.String } + if owner.Valid { + issue.Owner = owner.String + } if externalRef.Valid { issue.ExternalRef = &externalRef.String } diff --git a/internal/storage/sqlite/issues.go b/internal/storage/sqlite/issues.go index 92ef06de..1add9576 100644 --- a/internal/storage/sqlite/issues.go +++ b/internal/storage/sqlite/issues.go @@ -46,18 +46,18 @@ func insertIssue(ctx context.Context, conn *sql.Conn, issue *types.Issue) error INSERT OR IGNORE INTO issues ( id, content_hash, title, description, design, acceptance_criteria, notes, status, priority, issue_type, assignee, estimated_minutes, - created_at, created_by, updated_at, closed_at, external_ref, source_repo, close_reason, + created_at, created_by, owner, 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, mol_type, event_kind, actor, target, payload, due_at, defer_until - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, issue.ID, issue.ContentHash, issue.Title, issue.Description, issue.Design, issue.AcceptanceCriteria, issue.Notes, issue.Status, issue.Priority, issue.IssueType, issue.Assignee, - issue.EstimatedMinutes, issue.CreatedAt, issue.CreatedBy, issue.UpdatedAt, + issue.EstimatedMinutes, issue.CreatedAt, issue.CreatedBy, issue.Owner, issue.UpdatedAt, issue.ClosedAt, issue.ExternalRef, sourceRepo, issue.CloseReason, issue.DeletedAt, issue.DeletedBy, issue.DeleteReason, issue.OriginalType, issue.Sender, wisp, pinned, isTemplate, @@ -104,18 +104,18 @@ func insertIssueStrict(ctx context.Context, conn *sql.Conn, issue *types.Issue) INSERT INTO issues ( id, content_hash, title, description, design, acceptance_criteria, notes, status, priority, issue_type, assignee, estimated_minutes, - created_at, created_by, updated_at, closed_at, external_ref, source_repo, close_reason, + created_at, created_by, owner, 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, mol_type, event_kind, actor, target, payload, due_at, defer_until - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, issue.ID, issue.ContentHash, issue.Title, issue.Description, issue.Design, issue.AcceptanceCriteria, issue.Notes, issue.Status, issue.Priority, issue.IssueType, issue.Assignee, - issue.EstimatedMinutes, issue.CreatedAt, issue.CreatedBy, issue.UpdatedAt, + issue.EstimatedMinutes, issue.CreatedAt, issue.CreatedBy, issue.Owner, issue.UpdatedAt, issue.ClosedAt, issue.ExternalRef, sourceRepo, issue.CloseReason, issue.DeletedAt, issue.DeletedBy, issue.DeleteReason, issue.OriginalType, issue.Sender, wisp, pinned, isTemplate, @@ -136,13 +136,13 @@ func insertIssues(ctx context.Context, conn *sql.Conn, issues []*types.Issue) er INSERT OR IGNORE INTO issues ( id, content_hash, title, description, design, acceptance_criteria, notes, status, priority, issue_type, assignee, estimated_minutes, - created_at, created_by, updated_at, closed_at, external_ref, source_repo, close_reason, + created_at, created_by, owner, 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, mol_type, event_kind, actor, target, payload, due_at, defer_until - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `) if err != nil { return fmt.Errorf("failed to prepare statement: %w", err) @@ -172,7 +172,7 @@ func insertIssues(ctx context.Context, conn *sql.Conn, issues []*types.Issue) er issue.ID, issue.ContentHash, issue.Title, issue.Description, issue.Design, issue.AcceptanceCriteria, issue.Notes, issue.Status, issue.Priority, issue.IssueType, issue.Assignee, - issue.EstimatedMinutes, issue.CreatedAt, issue.CreatedBy, issue.UpdatedAt, + issue.EstimatedMinutes, issue.CreatedAt, issue.CreatedBy, issue.Owner, issue.UpdatedAt, issue.ClosedAt, issue.ExternalRef, sourceRepo, issue.CloseReason, issue.DeletedAt, issue.DeletedBy, issue.DeleteReason, issue.OriginalType, issue.Sender, wisp, pinned, isTemplate, diff --git a/internal/storage/sqlite/migrations.go b/internal/storage/sqlite/migrations.go index 4a62354f..3a2f3179 100644 --- a/internal/storage/sqlite/migrations.go +++ b/internal/storage/sqlite/migrations.go @@ -52,6 +52,7 @@ var migrationsList = []Migration{ {"event_fields", migrations.MigrateEventFields}, {"closed_by_session_column", migrations.MigrateClosedBySessionColumn}, {"due_defer_columns", migrations.MigrateDueDeferColumns}, + {"owner_column", migrations.MigrateOwnerColumn}, } // MigrationInfo contains metadata about a migration for inspection @@ -111,6 +112,7 @@ func getMigrationDescription(name string) string { "event_fields": "Adds event fields (event_kind, actor, target, payload) for operational state change beads", "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)", + "owner_column": "Adds owner column for human attribution in HOP CV chains (Decision 008)", } if desc, ok := descriptions[name]; ok { diff --git a/internal/storage/sqlite/migrations/036_owner_column.go b/internal/storage/sqlite/migrations/036_owner_column.go new file mode 100644 index 00000000..e4293b21 --- /dev/null +++ b/internal/storage/sqlite/migrations/036_owner_column.go @@ -0,0 +1,34 @@ +package migrations + +import ( + "database/sql" + "fmt" +) + +// MigrateOwnerColumn adds the owner column to the issues table. +// This tracks the human owner responsible for the issue, using git author email +// for HOP CV (curriculum vitae) attribution chains. See Decision 008. +func MigrateOwnerColumn(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 = 'owner' + `).Scan(&columnExists) + if err != nil { + return fmt.Errorf("failed to check owner column: %w", err) + } + + if columnExists { + return nil + } + + // Add the owner column + _, err = db.Exec(`ALTER TABLE issues ADD COLUMN owner TEXT DEFAULT ''`) + if err != nil { + return fmt.Errorf("failed to add owner column: %w", err) + } + + return nil +} diff --git a/internal/storage/sqlite/queries.go b/internal/storage/sqlite/queries.go index 87dbf0c5..691f73a8 100644 --- a/internal/storage/sqlite/queries.go +++ b/internal/storage/sqlite/queries.go @@ -298,10 +298,11 @@ func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue, var contentHash sql.NullString var compactedAtCommit sql.NullString + var owner sql.NullString err := s.db.QueryRowContext(ctx, ` SELECT id, content_hash, title, description, design, acceptance_criteria, notes, status, priority, issue_type, assignee, estimated_minutes, - created_at, created_by, updated_at, closed_at, external_ref, + created_at, created_by, owner, updated_at, closed_at, external_ref, compaction_level, compacted_at, compacted_at_commit, original_size, source_repo, close_reason, deleted_at, deleted_by, delete_reason, original_type, sender, ephemeral, pinned, is_template, @@ -315,7 +316,7 @@ func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue, &issue.ID, &contentHash, &issue.Title, &issue.Description, &issue.Design, &issue.AcceptanceCriteria, &issue.Notes, &issue.Status, &issue.Priority, &issue.IssueType, &assignee, &estimatedMinutes, - &issue.CreatedAt, &issue.CreatedBy, &issue.UpdatedAt, &closedAt, &externalRef, + &issue.CreatedAt, &issue.CreatedBy, &owner, &issue.UpdatedAt, &closedAt, &externalRef, &issue.CompactionLevel, &compactedAt, &compactedAtCommit, &originalSize, &sourceRepo, &closeReason, &deletedAt, &deletedBy, &deleteReason, &originalType, &sender, &wisp, &pinned, &isTemplate, @@ -345,6 +346,9 @@ func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue, if assignee.Valid { issue.Assignee = assignee.String } + if owner.Valid { + issue.Owner = owner.String + } if externalRef.Valid { issue.ExternalRef = &externalRef.String } @@ -560,10 +564,11 @@ func (s *SQLiteStorage) GetIssueByExternalRef(ctx context.Context, externalRef s var timeoutNs sql.NullInt64 var waiters sql.NullString + var owner sql.NullString err := s.db.QueryRowContext(ctx, ` SELECT id, content_hash, title, description, design, acceptance_criteria, notes, status, priority, issue_type, assignee, estimated_minutes, - created_at, created_by, updated_at, closed_at, external_ref, + created_at, created_by, owner, updated_at, closed_at, external_ref, compaction_level, compacted_at, compacted_at_commit, original_size, source_repo, close_reason, deleted_at, deleted_by, delete_reason, original_type, sender, ephemeral, pinned, is_template, @@ -574,7 +579,7 @@ func (s *SQLiteStorage) GetIssueByExternalRef(ctx context.Context, externalRef s &issue.ID, &contentHash, &issue.Title, &issue.Description, &issue.Design, &issue.AcceptanceCriteria, &issue.Notes, &issue.Status, &issue.Priority, &issue.IssueType, &assignee, &estimatedMinutes, - &issue.CreatedAt, &issue.CreatedBy, &issue.UpdatedAt, &closedAt, &externalRefCol, + &issue.CreatedAt, &issue.CreatedBy, &owner, &issue.UpdatedAt, &closedAt, &externalRefCol, &issue.CompactionLevel, &compactedAt, &compactedAtCommit, &originalSize, &sourceRepo, &closeReason, &deletedAt, &deletedBy, &deleteReason, &originalType, &sender, &wisp, &pinned, &isTemplate, @@ -601,6 +606,9 @@ func (s *SQLiteStorage) GetIssueByExternalRef(ctx context.Context, externalRef s if assignee.Valid { issue.Assignee = assignee.String } + if owner.Valid { + issue.Owner = owner.String + } if externalRefCol.Valid { issue.ExternalRef = &externalRefCol.String } @@ -1928,7 +1936,7 @@ func (s *SQLiteStorage) SearchIssues(ctx context.Context, query string, filter t querySQL := fmt.Sprintf(` SELECT id, content_hash, title, description, design, acceptance_criteria, notes, status, priority, issue_type, assignee, estimated_minutes, - created_at, created_by, updated_at, closed_at, external_ref, source_repo, close_reason, + created_at, created_by, owner, 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 diff --git a/internal/storage/sqlite/ready.go b/internal/storage/sqlite/ready.go index 60b05739..2ca5fb6f 100644 --- a/internal/storage/sqlite/ready.go +++ b/internal/storage/sqlite/ready.go @@ -152,7 +152,7 @@ func (s *SQLiteStorage) GetReadyWork(ctx context.Context, filter types.WorkFilte query := fmt.Sprintf(` 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.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.sender, i.ephemeral, i.pinned, i.is_template, i.await_type, i.await_id, i.timeout_ns, i.waiters @@ -744,7 +744,7 @@ func (s *SQLiteStorage) GetNewlyUnblockedByClose(ctx context.Context, closedIssu query := ` 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.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.sender, i.ephemeral, i.pinned, i.is_template, i.await_type, i.await_id, i.timeout_ns, i.waiters diff --git a/internal/storage/sqlite/schema.go b/internal/storage/sqlite/schema.go index 335df04d..43d4d6fa 100644 --- a/internal/storage/sqlite/schema.go +++ b/internal/storage/sqlite/schema.go @@ -17,6 +17,7 @@ CREATE TABLE IF NOT EXISTS issues ( estimated_minutes INTEGER, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by TEXT DEFAULT '', + owner TEXT DEFAULT '', updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, closed_at DATETIME, closed_by_session TEXT DEFAULT '', diff --git a/internal/storage/sqlite/transaction.go b/internal/storage/sqlite/transaction.go index 5b5adaac..68d894d0 100644 --- a/internal/storage/sqlite/transaction.go +++ b/internal/storage/sqlite/transaction.go @@ -320,7 +320,7 @@ func (t *sqliteTxStorage) GetIssue(ctx context.Context, id string) (*types.Issue row := t.conn.QueryRowContext(ctx, ` SELECT id, content_hash, title, description, design, acceptance_criteria, notes, status, priority, issue_type, assignee, estimated_minutes, - created_at, created_by, updated_at, closed_at, external_ref, + created_at, created_by, owner, updated_at, closed_at, external_ref, compaction_level, compacted_at, compacted_at_commit, original_size, source_repo, close_reason, deleted_at, deleted_by, delete_reason, original_type, sender, ephemeral, pinned, is_template, @@ -1249,7 +1249,7 @@ func (t *sqliteTxStorage) SearchIssues(ctx context.Context, query string, filter querySQL := fmt.Sprintf(` SELECT id, content_hash, title, description, design, acceptance_criteria, notes, status, priority, issue_type, assignee, estimated_minutes, - created_at, created_by, updated_at, closed_at, external_ref, + created_at, created_by, owner, updated_at, closed_at, external_ref, compaction_level, compacted_at, compacted_at_commit, original_size, source_repo, close_reason, deleted_at, deleted_by, delete_reason, original_type, sender, ephemeral, pinned, is_template, @@ -1283,6 +1283,7 @@ func scanIssueRow(row scanner) (*types.Issue, error) { var closedAt sql.NullTime var estimatedMinutes sql.NullInt64 var assignee sql.NullString + var owner sql.NullString var externalRef sql.NullString var compactedAt sql.NullTime var originalSize sql.NullInt64 @@ -1310,7 +1311,7 @@ func scanIssueRow(row scanner) (*types.Issue, error) { &issue.ID, &contentHash, &issue.Title, &issue.Description, &issue.Design, &issue.AcceptanceCriteria, &issue.Notes, &issue.Status, &issue.Priority, &issue.IssueType, &assignee, &estimatedMinutes, - &issue.CreatedAt, &issue.CreatedBy, &issue.UpdatedAt, &closedAt, &externalRef, + &issue.CreatedAt, &issue.CreatedBy, &owner, &issue.UpdatedAt, &closedAt, &externalRef, &issue.CompactionLevel, &compactedAt, &compactedAtCommit, &originalSize, &sourceRepo, &closeReason, &deletedAt, &deletedBy, &deleteReason, &originalType, &sender, &wisp, &pinned, &isTemplate, @@ -1333,6 +1334,9 @@ func scanIssueRow(row scanner) (*types.Issue, error) { if assignee.Valid { issue.Assignee = assignee.String } + if owner.Valid { + issue.Owner = owner.String + } if externalRef.Valid { issue.ExternalRef = &externalRef.String } diff --git a/internal/types/types.go b/internal/types/types.go index 5f3901c9..98d7552f 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -30,6 +30,7 @@ type Issue struct { // ===== Assignment ===== Assignee string `json:"assignee,omitempty"` + Owner string `json:"owner,omitempty"` // Human owner for CV attribution (git author email) EstimatedMinutes *int `json:"estimated_minutes,omitempty"` // ===== Timestamps ===== @@ -133,6 +134,7 @@ func (i *Issue) ComputeContentHash() string { w.int(i.Priority) w.str(string(i.IssueType)) w.str(i.Assignee) + w.str(i.Owner) w.str(i.CreatedBy) // Optional fields