@@ -49,6 +49,7 @@ export interface Task {
4949 retryCount : number ;
5050 maxRetries : number ;
5151 templateId : string | null ;
52+ externalRef : string | null ;
5253 children : Task [ ] ;
5354}
5455
@@ -63,6 +64,7 @@ export interface TaskCreateInput {
6364 gateConfig ?: GateConfig ;
6465 maxRetries ?: number ;
6566 templateId ?: string ;
67+ externalRef ?: string ;
6668}
6769
6870export interface TaskUpdateInput {
@@ -106,6 +108,7 @@ interface TaskRow {
106108 retry_count : number ;
107109 max_retries : number ;
108110 template_id : string | null ;
111+ external_ref : string | null ;
109112}
110113
111114const SCHEMA = `
@@ -137,11 +140,13 @@ const SCHEMA = `
137140 artifacts TEXT DEFAULT NULL,
138141 retry_count INTEGER NOT NULL DEFAULT 0,
139142 max_retries INTEGER NOT NULL DEFAULT 0,
140- template_id TEXT DEFAULT NULL
143+ template_id TEXT DEFAULT NULL,
144+ external_ref TEXT DEFAULT NULL
141145 );
142146 CREATE INDEX IF NOT EXISTS idx_tasks_parent ON tasks(parent_id);
143147 CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
144148 CREATE INDEX IF NOT EXISTS idx_tasks_session ON tasks(session_id);
149+ CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_external_ref ON tasks(external_ref) WHERE external_ref IS NOT NULL;
145150` ;
146151
147152const MIGRATIONS = [
@@ -158,6 +163,14 @@ const MIGRATIONS = [
158163 ALTER TABLE tasks ADD COLUMN template_id TEXT DEFAULT NULL;
159164 ` ,
160165 } ,
166+ // Migration 2: Add external_ref for idempotent task creation
167+ {
168+ check : "SELECT COUNT(*) as cnt FROM pragma_table_info('tasks') WHERE name = 'external_ref'" ,
169+ sql : `
170+ ALTER TABLE tasks ADD COLUMN external_ref TEXT DEFAULT NULL;
171+ CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_external_ref ON tasks(external_ref) WHERE external_ref IS NOT NULL;
172+ ` ,
173+ } ,
161174] ;
162175
163176function rowToTask ( row : TaskRow ) : Task {
@@ -194,6 +207,7 @@ function rowToTask(row: TaskRow): Task {
194207 retryCount : row . retry_count ?? 0 ,
195208 maxRetries : row . max_retries ?? 0 ,
196209 templateId : row . template_id ?? null ,
210+ externalRef : row . external_ref ?? null ,
197211 children : [ ] ,
198212 } ;
199213}
@@ -259,8 +273,8 @@ export class TaskStore {
259273 const gateConfig = input . gateConfig ? JSON . stringify ( input . gateConfig ) : null ;
260274
261275 db . prepare (
262- `INSERT INTO tasks (id, parent_id, title, description, status, session_policy, priority, depth, annotations, stage_type, gate_config, max_retries, template_id, created_at, updated_at)
263- VALUES (?, ?, ?, ?, 'pending', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` ,
276+ `INSERT INTO tasks (id, parent_id, title, description, status, session_policy, priority, depth, annotations, stage_type, gate_config, max_retries, template_id, external_ref, created_at, updated_at)
277+ VALUES (?, ?, ?, ?, 'pending', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )` ,
264278 ) . run (
265279 id ,
266280 input . parentId ?? null ,
@@ -274,6 +288,7 @@ export class TaskStore {
274288 gateConfig ,
275289 input . maxRetries ?? 0 ,
276290 input . templateId ?? null ,
291+ input . externalRef ?? null ,
277292 now ,
278293 now ,
279294 ) ;
@@ -288,6 +303,14 @@ export class TaskStore {
288303 return row ? rowToTask ( row ) : null ;
289304 }
290305
306+ /** Look up a task by its external reference (e.g. "pr_shepherd:dimakis/centaur#42"). */
307+ getByExternalRef ( ref : string ) : Task | null {
308+ const row = this . getDb ( )
309+ . prepare ( 'SELECT * FROM tasks WHERE external_ref = ?' )
310+ . get ( ref ) as TaskRow | undefined ;
311+ return row ? rowToTask ( row ) : null ;
312+ }
313+
291314 update ( id : string , fields : TaskUpdateInput ) : Task | null {
292315 const existing = this . get ( id ) ;
293316 if ( ! existing ) return null ;
0 commit comments