feat: Add 'hooked' status for GUPP work assignment (bd-s00m)

Separates semantics of 'pinned' (identity records) from work-on-hook:
- 'pinned' = domain table / identity record (agents, roles) - non-blocking
- 'hooked' = work on agent's hook (GUPP-driven) - blocks dependents

Changes:
- Add StatusHooked constant to types.go
- Update all blocking queries to include 'hooked' status
- Add cyan styling for 'hooked' in UI output
- Create migration 032 to convert pinned work items to hooked

Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-28 22:36:46 -08:00
parent 4a6d942fad
commit 7d1ee6d3e9
10 changed files with 61 additions and 15 deletions

View File

@@ -146,7 +146,7 @@ func (s *SQLiteStorage) rebuildBlockedCache(ctx context.Context, exec execer) er
FROM dependencies d
JOIN issues blocker ON d.depends_on_id = blocker.id
WHERE d.type = 'blocks'
AND blocker.status IN ('open', 'in_progress', 'blocked', 'deferred')
AND blocker.status IN ('open', 'in_progress', 'blocked', 'deferred', 'hooked')
UNION

View File

@@ -78,7 +78,7 @@ func (s *SQLiteStorage) GetTier1Candidates(ctx context.Context) ([]*CompactionCa
COUNT(DISTINCT dt.dependent_id) as dependent_count
FROM issues i
LEFT JOIN dependent_tree dt ON i.id = dt.issue_id
AND dt.dependent_status IN ('open', 'in_progress', 'blocked', 'deferred')
AND dt.dependent_status IN ('open', 'in_progress', 'blocked', 'deferred', 'hooked')
AND dt.depth <= ?
WHERE i.status = 'closed'
AND i.closed_at IS NOT NULL
@@ -163,7 +163,7 @@ func (s *SQLiteStorage) GetTier2Candidates(ctx context.Context) ([]*CompactionCa
JOIN issues dep ON d.issue_id = dep.id
WHERE d.depends_on_id = i.id
AND d.type = 'blocks'
AND dep.status IN ('open', 'in_progress', 'blocked', 'deferred')
AND dep.status IN ('open', 'in_progress', 'blocked', 'deferred', 'hooked')
)
ORDER BY i.closed_at ASC
`

View File

@@ -134,9 +134,9 @@ func (s *SQLiteStorage) GetStatistics(ctx context.Context) (*types.Statistics, e
FROM issues i
JOIN dependencies d ON i.id = d.issue_id
JOIN issues blocker ON d.depends_on_id = blocker.id
WHERE i.status IN ('open', 'in_progress', 'blocked', 'deferred')
WHERE i.status IN ('open', 'in_progress', 'blocked', 'deferred', 'hooked')
AND d.type = 'blocks'
AND blocker.status IN ('open', 'in_progress', 'blocked', 'deferred')
AND blocker.status IN ('open', 'in_progress', 'blocked', 'deferred', 'hooked')
`).Scan(&stats.BlockedIssues)
if err != nil {
return nil, fmt.Errorf("failed to get blocked count: %w", err)
@@ -152,7 +152,7 @@ func (s *SQLiteStorage) GetStatistics(ctx context.Context) (*types.Statistics, e
JOIN issues blocker ON d.depends_on_id = blocker.id
WHERE d.issue_id = i.id
AND d.type = 'blocks'
AND blocker.status IN ('open', 'in_progress', 'blocked', 'deferred')
AND blocker.status IN ('open', 'in_progress', 'blocked', 'deferred', 'hooked')
)
`).Scan(&stats.ReadyIssues)
if err != nil {

View File

@@ -48,6 +48,7 @@ var migrationsList = []Migration{
{"created_by_column", migrations.MigrateCreatedByColumn},
{"agent_fields", migrations.MigrateAgentFields},
{"mol_type_column", migrations.MigrateMolTypeColumn},
{"hooked_status_migration", migrations.MigrateHookedStatus},
}
// MigrationInfo contains metadata about a migration for inspection

View File

@@ -0,0 +1,37 @@
package migrations
import (
"database/sql"
"fmt"
)
// MigrateHookedStatus converts pinned work items to hooked status.
// 'pinned' now means identity/domain records (agents, roles).
// 'hooked' means work actively attached to an agent's hook (GUPP).
func MigrateHookedStatus(db *sql.DB) error {
// Migrate pinned issues that represent work (not identity records) to hooked.
// Agent/role beads stay pinned; molecules and regular issues become hooked.
result, err := db.Exec(`
UPDATE issues
SET status = 'hooked'
WHERE status = 'pinned'
AND issue_type NOT IN ('agent', 'role')
`)
if err != nil {
return fmt.Errorf("failed to migrate pinned to hooked: %w", err)
}
rowsAffected, _ := result.RowsAffected()
if rowsAffected > 0 {
// Log migration for audit trail (optional - no-op if table doesn't exist)
_, _ = db.Exec(`
INSERT INTO events (issue_id, event_type, actor, old_value, new_value, comment)
SELECT id, 'migration', 'system', 'pinned', 'hooked', 'bd-s00m: Semantic split of pinned vs hooked'
FROM issues
WHERE status = 'hooked'
AND issue_type NOT IN ('agent', 'role')
`)
}
return nil
}

View File

@@ -494,12 +494,12 @@ func (s *SQLiteStorage) GetBlockedIssues(ctx context.Context, filter types.WorkF
EXISTS (
SELECT 1 FROM issues blocker
WHERE blocker.id = d.depends_on_id
AND blocker.status IN ('open', 'in_progress', 'blocked', 'deferred')
AND blocker.status IN ('open', 'in_progress', 'blocked', 'deferred', 'hooked')
)
-- External refs: always included (resolution happens at query time)
OR d.depends_on_id LIKE 'external:%%'
)
WHERE i.status IN ('open', 'in_progress', 'blocked', 'deferred')
WHERE i.status IN ('open', 'in_progress', 'blocked', 'deferred', 'hooked')
AND i.pinned = 0
AND (
i.status = 'blocked'
@@ -510,7 +510,7 @@ func (s *SQLiteStorage) GetBlockedIssues(ctx context.Context, filter types.WorkF
JOIN issues blocker ON d2.depends_on_id = blocker.id
WHERE d2.issue_id = i.id
AND d2.type = 'blocks'
AND blocker.status IN ('open', 'in_progress', 'blocked', 'deferred')
AND blocker.status IN ('open', 'in_progress', 'blocked', 'deferred', 'hooked')
)
-- Has external blockers (always considered blocking until resolved)
OR EXISTS (

View File

@@ -214,7 +214,7 @@ WITH RECURSIVE
FROM dependencies d
JOIN issues blocker ON d.depends_on_id = blocker.id
WHERE d.type = 'blocks'
AND blocker.status IN ('open', 'in_progress', 'blocked', 'deferred')
AND blocker.status IN ('open', 'in_progress', 'blocked', 'deferred', 'hooked')
),
-- Propagate blockage to all descendants via parent-child
blocked_transitively AS (
@@ -245,8 +245,8 @@ SELECT
FROM issues i
JOIN dependencies d ON i.id = d.issue_id
JOIN issues blocker ON d.depends_on_id = blocker.id
WHERE i.status IN ('open', 'in_progress', 'blocked', 'deferred')
WHERE i.status IN ('open', 'in_progress', 'blocked', 'deferred', 'hooked')
AND d.type = 'blocks'
AND blocker.status IN ('open', 'in_progress', 'blocked', 'deferred')
AND blocker.status IN ('open', 'in_progress', 'blocked', 'deferred', 'hooked')
GROUP BY i.id;
`