feat: add owner field for human attribution in HOP CV chains

Add 'owner' field to Issue struct for tracking the human responsible
for the issue, distinct from 'created_by' which tracks the executor.
Owner is populated from git author email (GIT_AUTHOR_EMAIL or git
config user.email), per Decision 008 for CV accumulation.

Changes:
- Add Owner field to types.Issue with omitempty JSON tag
- Include Owner in content hash computation
- Add owner column migration (036_owner_column.go)
- Update all SQL queries to include owner field
- Add getOwner() helper using git author email fallback chain
- Populate owner in bd create command
- Add owner to RPC CreateArgs protocol

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Executed-By: beads/crew/dave
Rig: beads
Role: crew
This commit is contained in:
beads/crew/dave
2026-01-10 19:28:08 -08:00
committed by Steve Yegge
parent 1039a69186
commit ceb5769c75
13 changed files with 109 additions and 23 deletions

View File

@@ -199,6 +199,7 @@ var createCmd = &cobra.Command{
ExternalRef: externalRefPtr, ExternalRef: externalRefPtr,
Ephemeral: wisp, Ephemeral: wisp,
CreatedBy: getActorWithGit(), CreatedBy: getActorWithGit(),
Owner: getOwner(),
MolType: molType, MolType: molType,
RoleType: roleType, RoleType: roleType,
Rig: agentRig, Rig: agentRig,
@@ -423,6 +424,7 @@ var createCmd = &cobra.Command{
WaitsForGate: waitsForGate, WaitsForGate: waitsForGate,
Ephemeral: wisp, Ephemeral: wisp,
CreatedBy: getActorWithGit(), CreatedBy: getActorWithGit(),
Owner: getOwner(),
MolType: string(molType), MolType: string(molType),
RoleType: roleType, RoleType: roleType,
Rig: agentRig, Rig: agentRig,
@@ -482,6 +484,7 @@ var createCmd = &cobra.Command{
EstimatedMinutes: estimatedMinutes, EstimatedMinutes: estimatedMinutes,
Ephemeral: wisp, Ephemeral: wisp,
CreatedBy: getActorWithGit(), CreatedBy: getActorWithGit(),
Owner: getOwner(),
MolType: molType, MolType: molType,
RoleType: roleType, RoleType: roleType,
Rig: agentRig, Rig: agentRig,
@@ -808,6 +811,7 @@ func createInRig(cmd *cobra.Command, rigName, title, description, issueType stri
ExternalRef: externalRefPtr, ExternalRef: externalRefPtr,
Ephemeral: wisp, Ephemeral: wisp,
CreatedBy: getActorWithGit(), CreatedBy: getActorWithGit(),
Owner: getOwner(),
// Event fields (bd-xwvo fix) // Event fields (bd-xwvo fix)
EventKind: eventCategory, EventKind: eventCategory,
Actor: eventActor, Actor: eventActor,

View File

@@ -143,6 +143,27 @@ func getActorWithGit() string {
return "unknown" 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() { func init() {
// Initialize viper configuration // Initialize viper configuration
if err := config.Initialize(); err != nil { if err := config.Initialize(); err != nil {

View File

@@ -98,6 +98,7 @@ 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
Owner string `json:"owner,omitempty"` // Human owner for CV attribution (git author email)
// Molecule type (for swarm coordination) // Molecule type (for swarm coordination)
MolType string `json:"mol_type,omitempty"` // swarm, patrol, or work (default) MolType string `json:"mol_type,omitempty"` // swarm, patrol, or work (default)
// Agent identity fields (only valid when IssueType == "agent") // Agent identity fields (only valid when IssueType == "agent")

View File

@@ -306,6 +306,7 @@ func (s *Server) handleCreate(req *Request) Response {
// ID generation // ID generation
IDPrefix: createArgs.IDPrefix, IDPrefix: createArgs.IDPrefix,
CreatedBy: createArgs.CreatedBy, CreatedBy: createArgs.CreatedBy,
Owner: createArgs.Owner,
// Molecule type // Molecule type
MolType: types.MolType(createArgs.MolType), MolType: types.MolType(createArgs.MolType),
// Agent identity fields // Agent identity fields

View File

@@ -247,7 +247,7 @@ func (s *SQLiteStorage) GetDependenciesWithMetadata(ctx context.Context, issueID
rows, err := s.db.QueryContext(ctx, ` rows, err := s.db.QueryContext(ctx, `
SELECT i.id, i.content_hash, i.title, i.description, i.design, i.acceptance_criteria, i.notes, 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.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.deleted_at, i.deleted_by, i.delete_reason, i.original_type,
i.sender, i.ephemeral, i.pinned, i.is_template, i.sender, i.ephemeral, i.pinned, i.is_template,
i.await_type, i.await_id, i.timeout_ns, i.waiters, 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, ` rows, err := s.db.QueryContext(ctx, `
SELECT i.id, i.content_hash, i.title, i.description, i.design, i.acceptance_criteria, i.notes, 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.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.deleted_at, i.deleted_by, i.delete_reason, i.original_type,
i.sender, i.ephemeral, i.pinned, i.is_template, i.sender, i.ephemeral, i.pinned, i.is_template,
i.await_type, i.await_id, i.timeout_ns, i.waiters, 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 closedAt sql.NullTime
var estimatedMinutes sql.NullInt64 var estimatedMinutes sql.NullInt64
var assignee sql.NullString var assignee sql.NullString
var owner sql.NullString
var externalRef sql.NullString var externalRef sql.NullString
var sourceRepo sql.NullString var sourceRepo sql.NullString
var closeReason 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.ID, &contentHash, &issue.Title, &issue.Description, &issue.Design,
&issue.AcceptanceCriteria, &issue.Notes, &issue.Status, &issue.AcceptanceCriteria, &issue.Notes, &issue.Status,
&issue.Priority, &issue.IssueType, &assignee, &estimatedMinutes, &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, &deletedAt, &deletedBy, &deleteReason, &originalType,
&sender, &wisp, &pinned, &isTemplate, &sender, &wisp, &pinned, &isTemplate,
&awaitType, &awaitID, &timeoutNs, &waiters, &awaitType, &awaitID, &timeoutNs, &waiters,
@@ -910,6 +911,9 @@ func (s *SQLiteStorage) scanIssues(ctx context.Context, rows *sql.Rows) ([]*type
if assignee.Valid { if assignee.Valid {
issue.Assignee = assignee.String issue.Assignee = assignee.String
} }
if owner.Valid {
issue.Owner = owner.String
}
if externalRef.Valid { if externalRef.Valid {
issue.ExternalRef = &externalRef.String issue.ExternalRef = &externalRef.String
} }
@@ -987,6 +991,7 @@ func (s *SQLiteStorage) scanIssuesWithDependencyType(ctx context.Context, rows *
var closedAt sql.NullTime var closedAt sql.NullTime
var estimatedMinutes sql.NullInt64 var estimatedMinutes sql.NullInt64
var assignee sql.NullString var assignee sql.NullString
var owner sql.NullString
var externalRef sql.NullString var externalRef sql.NullString
var sourceRepo sql.NullString var sourceRepo sql.NullString
var deletedAt sql.NullString // TEXT column, not DATETIME - must parse manually 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.ID, &contentHash, &issue.Title, &issue.Description, &issue.Design,
&issue.AcceptanceCriteria, &issue.Notes, &issue.Status, &issue.AcceptanceCriteria, &issue.Notes, &issue.Status,
&issue.Priority, &issue.IssueType, &assignee, &estimatedMinutes, &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, &deletedAt, &deletedBy, &deleteReason, &originalType,
&sender, &wisp, &pinned, &isTemplate, &sender, &wisp, &pinned, &isTemplate,
&awaitType, &awaitID, &timeoutNs, &waiters, &awaitType, &awaitID, &timeoutNs, &waiters,
@@ -1034,6 +1039,9 @@ func (s *SQLiteStorage) scanIssuesWithDependencyType(ctx context.Context, rows *
if assignee.Valid { if assignee.Valid {
issue.Assignee = assignee.String issue.Assignee = assignee.String
} }
if owner.Valid {
issue.Owner = owner.String
}
if externalRef.Valid { if externalRef.Valid {
issue.ExternalRef = &externalRef.String issue.ExternalRef = &externalRef.String
} }

View File

@@ -46,18 +46,18 @@ func insertIssue(ctx context.Context, conn *sql.Conn, issue *types.Issue) error
INSERT OR IGNORE INTO issues ( INSERT OR IGNORE INTO issues (
id, content_hash, title, description, design, acceptance_criteria, notes, id, content_hash, title, description, design, acceptance_criteria, notes,
status, priority, issue_type, assignee, estimated_minutes, 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, 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, mol_type, await_type, await_id, timeout_ns, waiters, mol_type,
event_kind, actor, target, payload, event_kind, actor, target, payload,
due_at, defer_until due_at, defer_until
) 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,
issue.Priority, issue.IssueType, issue.Assignee, 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.ClosedAt, issue.ExternalRef, sourceRepo, issue.CloseReason,
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,
@@ -104,18 +104,18 @@ func insertIssueStrict(ctx context.Context, conn *sql.Conn, issue *types.Issue)
INSERT INTO issues ( INSERT INTO issues (
id, content_hash, title, description, design, acceptance_criteria, notes, id, content_hash, title, description, design, acceptance_criteria, notes,
status, priority, issue_type, assignee, estimated_minutes, 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, 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, mol_type, await_type, await_id, timeout_ns, waiters, mol_type,
event_kind, actor, target, payload, event_kind, actor, target, payload,
due_at, defer_until due_at, defer_until
) 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,
issue.Priority, issue.IssueType, issue.Assignee, 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.ClosedAt, issue.ExternalRef, sourceRepo, issue.CloseReason,
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,
@@ -136,13 +136,13 @@ func insertIssues(ctx context.Context, conn *sql.Conn, issues []*types.Issue) er
INSERT OR IGNORE INTO issues ( INSERT OR IGNORE INTO issues (
id, content_hash, title, description, design, acceptance_criteria, notes, id, content_hash, title, description, design, acceptance_criteria, notes,
status, priority, issue_type, assignee, estimated_minutes, 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, 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, mol_type, await_type, await_id, timeout_ns, waiters, mol_type,
event_kind, actor, target, payload, event_kind, actor, target, payload,
due_at, defer_until due_at, defer_until
) 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)
@@ -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.ID, issue.ContentHash, issue.Title, issue.Description, issue.Design,
issue.AcceptanceCriteria, issue.Notes, issue.Status, issue.AcceptanceCriteria, issue.Notes, issue.Status,
issue.Priority, issue.IssueType, issue.Assignee, 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.ClosedAt, issue.ExternalRef, sourceRepo, issue.CloseReason,
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,

View File

@@ -52,6 +52,7 @@ var migrationsList = []Migration{
{"event_fields", migrations.MigrateEventFields}, {"event_fields", migrations.MigrateEventFields},
{"closed_by_session_column", migrations.MigrateClosedBySessionColumn}, {"closed_by_session_column", migrations.MigrateClosedBySessionColumn},
{"due_defer_columns", migrations.MigrateDueDeferColumns}, {"due_defer_columns", migrations.MigrateDueDeferColumns},
{"owner_column", migrations.MigrateOwnerColumn},
} }
// MigrationInfo contains metadata about a migration for inspection // 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", "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", "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)", "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 { if desc, ok := descriptions[name]; ok {

View File

@@ -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
}

View File

@@ -298,10 +298,11 @@ func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue,
var contentHash sql.NullString var contentHash sql.NullString
var compactedAtCommit sql.NullString var compactedAtCommit sql.NullString
var owner sql.NullString
err := s.db.QueryRowContext(ctx, ` err := s.db.QueryRowContext(ctx, `
SELECT id, content_hash, title, description, design, acceptance_criteria, notes, SELECT id, content_hash, title, description, design, acceptance_criteria, notes,
status, priority, issue_type, assignee, estimated_minutes, 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, compaction_level, compacted_at, compacted_at_commit, original_size, 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,
@@ -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.ID, &contentHash, &issue.Title, &issue.Description, &issue.Design,
&issue.AcceptanceCriteria, &issue.Notes, &issue.Status, &issue.AcceptanceCriteria, &issue.Notes, &issue.Status,
&issue.Priority, &issue.IssueType, &assignee, &estimatedMinutes, &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, &issue.CompactionLevel, &compactedAt, &compactedAtCommit, &originalSize, &sourceRepo, &closeReason,
&deletedAt, &deletedBy, &deleteReason, &originalType, &deletedAt, &deletedBy, &deleteReason, &originalType,
&sender, &wisp, &pinned, &isTemplate, &sender, &wisp, &pinned, &isTemplate,
@@ -345,6 +346,9 @@ func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue,
if assignee.Valid { if assignee.Valid {
issue.Assignee = assignee.String issue.Assignee = assignee.String
} }
if owner.Valid {
issue.Owner = owner.String
}
if externalRef.Valid { if externalRef.Valid {
issue.ExternalRef = &externalRef.String issue.ExternalRef = &externalRef.String
} }
@@ -560,10 +564,11 @@ func (s *SQLiteStorage) GetIssueByExternalRef(ctx context.Context, externalRef s
var timeoutNs sql.NullInt64 var timeoutNs sql.NullInt64
var waiters sql.NullString var waiters sql.NullString
var owner sql.NullString
err := s.db.QueryRowContext(ctx, ` err := s.db.QueryRowContext(ctx, `
SELECT id, content_hash, title, description, design, acceptance_criteria, notes, SELECT id, content_hash, title, description, design, acceptance_criteria, notes,
status, priority, issue_type, assignee, estimated_minutes, 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, compaction_level, compacted_at, compacted_at_commit, original_size, 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,
@@ -574,7 +579,7 @@ func (s *SQLiteStorage) GetIssueByExternalRef(ctx context.Context, externalRef s
&issue.ID, &contentHash, &issue.Title, &issue.Description, &issue.Design, &issue.ID, &contentHash, &issue.Title, &issue.Description, &issue.Design,
&issue.AcceptanceCriteria, &issue.Notes, &issue.Status, &issue.AcceptanceCriteria, &issue.Notes, &issue.Status,
&issue.Priority, &issue.IssueType, &assignee, &estimatedMinutes, &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, &issue.CompactionLevel, &compactedAt, &compactedAtCommit, &originalSize, &sourceRepo, &closeReason,
&deletedAt, &deletedBy, &deleteReason, &originalType, &deletedAt, &deletedBy, &deleteReason, &originalType,
&sender, &wisp, &pinned, &isTemplate, &sender, &wisp, &pinned, &isTemplate,
@@ -601,6 +606,9 @@ func (s *SQLiteStorage) GetIssueByExternalRef(ctx context.Context, externalRef s
if assignee.Valid { if assignee.Valid {
issue.Assignee = assignee.String issue.Assignee = assignee.String
} }
if owner.Valid {
issue.Owner = owner.String
}
if externalRefCol.Valid { if externalRefCol.Valid {
issue.ExternalRef = &externalRefCol.String issue.ExternalRef = &externalRefCol.String
} }
@@ -1928,7 +1936,7 @@ func (s *SQLiteStorage) SearchIssues(ctx context.Context, query string, filter t
querySQL := fmt.Sprintf(` querySQL := fmt.Sprintf(`
SELECT id, content_hash, title, description, design, acceptance_criteria, notes, SELECT id, content_hash, title, description, design, acceptance_criteria, notes,
status, priority, issue_type, assignee, estimated_minutes, 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, 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

View File

@@ -152,7 +152,7 @@ func (s *SQLiteStorage) GetReadyWork(ctx context.Context, filter types.WorkFilte
query := fmt.Sprintf(` query := fmt.Sprintf(`
SELECT i.id, i.content_hash, i.title, i.description, i.design, i.acceptance_criteria, i.notes, 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.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.deleted_at, i.deleted_by, i.delete_reason, i.original_type,
i.sender, i.ephemeral, i.pinned, i.is_template, i.sender, i.ephemeral, i.pinned, i.is_template,
i.await_type, i.await_id, i.timeout_ns, i.waiters i.await_type, i.await_id, i.timeout_ns, i.waiters
@@ -744,7 +744,7 @@ func (s *SQLiteStorage) GetNewlyUnblockedByClose(ctx context.Context, closedIssu
query := ` query := `
SELECT i.id, i.content_hash, i.title, i.description, i.design, i.acceptance_criteria, i.notes, 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.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.deleted_at, i.deleted_by, i.delete_reason, i.original_type,
i.sender, i.ephemeral, i.pinned, i.is_template, i.sender, i.ephemeral, i.pinned, i.is_template,
i.await_type, i.await_id, i.timeout_ns, i.waiters i.await_type, i.await_id, i.timeout_ns, i.waiters

View File

@@ -17,6 +17,7 @@ CREATE TABLE IF NOT EXISTS issues (
estimated_minutes INTEGER, estimated_minutes INTEGER,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_by TEXT DEFAULT '', created_by TEXT DEFAULT '',
owner TEXT DEFAULT '',
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
closed_at DATETIME, closed_at DATETIME,
closed_by_session TEXT DEFAULT '', closed_by_session TEXT DEFAULT '',

View File

@@ -320,7 +320,7 @@ func (t *sqliteTxStorage) GetIssue(ctx context.Context, id string) (*types.Issue
row := t.conn.QueryRowContext(ctx, ` row := t.conn.QueryRowContext(ctx, `
SELECT id, content_hash, title, description, design, acceptance_criteria, notes, SELECT id, content_hash, title, description, design, acceptance_criteria, notes,
status, priority, issue_type, assignee, estimated_minutes, 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, compaction_level, compacted_at, compacted_at_commit, original_size, 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,
@@ -1249,7 +1249,7 @@ func (t *sqliteTxStorage) SearchIssues(ctx context.Context, query string, filter
querySQL := fmt.Sprintf(` querySQL := fmt.Sprintf(`
SELECT id, content_hash, title, description, design, acceptance_criteria, notes, SELECT id, content_hash, title, description, design, acceptance_criteria, notes,
status, priority, issue_type, assignee, estimated_minutes, 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, compaction_level, compacted_at, compacted_at_commit, original_size, 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,
@@ -1283,6 +1283,7 @@ func scanIssueRow(row scanner) (*types.Issue, error) {
var closedAt sql.NullTime var closedAt sql.NullTime
var estimatedMinutes sql.NullInt64 var estimatedMinutes sql.NullInt64
var assignee sql.NullString var assignee sql.NullString
var owner sql.NullString
var externalRef sql.NullString var externalRef sql.NullString
var compactedAt sql.NullTime var compactedAt sql.NullTime
var originalSize sql.NullInt64 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.ID, &contentHash, &issue.Title, &issue.Description, &issue.Design,
&issue.AcceptanceCriteria, &issue.Notes, &issue.Status, &issue.AcceptanceCriteria, &issue.Notes, &issue.Status,
&issue.Priority, &issue.IssueType, &assignee, &estimatedMinutes, &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, &issue.CompactionLevel, &compactedAt, &compactedAtCommit, &originalSize, &sourceRepo, &closeReason,
&deletedAt, &deletedBy, &deleteReason, &originalType, &deletedAt, &deletedBy, &deleteReason, &originalType,
&sender, &wisp, &pinned, &isTemplate, &sender, &wisp, &pinned, &isTemplate,
@@ -1333,6 +1334,9 @@ func scanIssueRow(row scanner) (*types.Issue, error) {
if assignee.Valid { if assignee.Valid {
issue.Assignee = assignee.String issue.Assignee = assignee.String
} }
if owner.Valid {
issue.Owner = owner.String
}
if externalRef.Valid { if externalRef.Valid {
issue.ExternalRef = &externalRef.String issue.ExternalRef = &externalRef.String
} }

View File

@@ -30,6 +30,7 @@ type Issue struct {
// ===== Assignment ===== // ===== Assignment =====
Assignee string `json:"assignee,omitempty"` Assignee string `json:"assignee,omitempty"`
Owner string `json:"owner,omitempty"` // Human owner for CV attribution (git author email)
EstimatedMinutes *int `json:"estimated_minutes,omitempty"` EstimatedMinutes *int `json:"estimated_minutes,omitempty"`
// ===== Timestamps ===== // ===== Timestamps =====
@@ -133,6 +134,7 @@ func (i *Issue) ComputeContentHash() string {
w.int(i.Priority) w.int(i.Priority)
w.str(string(i.IssueType)) w.str(string(i.IssueType))
w.str(i.Assignee) w.str(i.Assignee)
w.str(i.Owner)
w.str(i.CreatedBy) w.str(i.CreatedBy)
// Optional fields // Optional fields