@@ -20,6 +20,14 @@ export type IntentStatus =
2020 | 'failed'
2121 | 'blocked_human' ;
2222
23+ // @canon : chittycanon://gov/governance#classification-axes STATUS:PENDING
24+ // ChittyRoux privilege class — orthogonal to sovereignty trust-tier sensitivity.
25+ export type IntentPrivilege = 'privileged' | 'pii' | 'hoa_evidentiary' | 'public' ;
26+
27+ // @canon : chittycanon://gov/governance#classification-axes STATUS:PENDING
28+ // ChittyRoux two-Space partition.
29+ export type IntentSpace = 'business' | 'legalink' ;
30+
2331export interface SovereigntyAssessmentSnapshot {
2432 decision : 'autonomous' | 'requires_human' | 'blocked' ;
2533 trustScore : number ;
@@ -75,6 +83,10 @@ export interface Intent {
7583 scheduledFor : Date | null ;
7684 completedAt : Date | null ;
7785 errorMessage : string | null ;
86+ // @canon : chittycanon://gov/governance#classification-axes STATUS:PENDING
87+ privilege : IntentPrivilege ;
88+ // @canon : chittycanon://gov/governance#classification-axes STATUS:PENDING
89+ space : IntentSpace ;
7890 metadata : Record < string , unknown > ;
7991 createdAt : Date ;
8092 updatedAt : Date ;
@@ -192,6 +204,10 @@ export interface CreateIntentInput {
192204 sovereigntyAssessment ?: SovereigntyAssessmentSnapshot ;
193205 humanGateReason ?: string ;
194206 scheduledFor ?: Date ;
207+ // @canon : chittycanon://gov/governance#classification-axes STATUS:PENDING
208+ privilege ?: IntentPrivilege ;
209+ // @canon : chittycanon://gov/governance#classification-axes STATUS:PENDING
210+ space ?: IntentSpace ;
195211 metadata ?: Record < string , unknown > ;
196212}
197213
@@ -206,16 +222,20 @@ export async function createIntent(env: IntentEnv, input: CreateIntentInput): Pr
206222 ? 'failed'
207223 : 'pending' ;
208224
225+ // @canon : chittycanon://gov/governance#classification-axes STATUS:PENDING
226+ // privilege/space default to public/business at the column level; pass through
227+ // explicit caller values so high-privilege intents are tagged at creation.
209228 const rows = await sql `
210229 INSERT INTO cc_intents
211230 (plan_id, goal_id, intent_type, target_channel, payload, status, priority,
212- sovereignty_assessment, human_gate_reason, scheduled_for, metadata)
231+ sovereignty_assessment, human_gate_reason, scheduled_for, privilege, space, metadata)
213232 VALUES
214233 (${ input . planId } , ${ input . goalId } , ${ input . intentType } ,
215234 ${ input . targetChannel ?? null } , ${ JSON . stringify ( input . payload ) } ::jsonb,
216235 ${ initialStatus } , ${ input . priority ?? 5 } ,
217236 ${ input . sovereigntyAssessment ? JSON . stringify ( input . sovereigntyAssessment ) : null } ::jsonb,
218237 ${ input . humanGateReason ?? null } , ${ input . scheduledFor ?? null } ,
238+ ${ input . privilege ?? 'public' } , ${ input . space ?? 'business' } ,
219239 ${ JSON . stringify ( input . metadata ?? { } ) } ::jsonb)
220240 RETURNING *` ;
221241 return rowToIntent ( rows [ 0 ] ) ;
@@ -231,37 +251,39 @@ export async function getIntent(env: IntentEnv, id: string): Promise<Intent | nu
231251 * Claim the next pending intent for execution. Atomic via UPDATE...RETURNING.
232252 * Mirrors the lease pattern in chittyentity/workers/shared/agent-tasks.ts.
233253 */
254+ // @canon : chittycanon://gov/governance#classification-axes STATUS:PENDING
255+ // privilege/space filters use the null-passthrough pattern (param IS NULL OR
256+ // col = param) so a single prepared statement covers all four filter combos.
234257export async function claimNextIntent (
235258 env : IntentEnv ,
236- options : { channel ?: string } = { } ,
259+ options : {
260+ channel ?: string ;
261+ privilege ?: IntentPrivilege ;
262+ space ?: IntentSpace ;
263+ priorityLte ?: number ;
264+ } = { } ,
237265) : Promise < Intent | null > {
238266 const sql = getSql ( env ) ;
239- const rows = options . channel
240- ? await sql `
241- UPDATE cc_intents
242- SET status = 'claimed', updated_at = NOW()
243- WHERE id = (
244- SELECT id FROM cc_intents
245- WHERE status = 'pending'
246- AND target_channel = ${ options . channel }
247- AND (scheduled_for IS NULL OR scheduled_for <= NOW())
248- ORDER BY priority ASC, created_at ASC
249- FOR UPDATE SKIP LOCKED
250- LIMIT 1
251- )
252- RETURNING *`
253- : await sql `
254- UPDATE cc_intents
255- SET status = 'claimed', updated_at = NOW()
256- WHERE id = (
257- SELECT id FROM cc_intents
258- WHERE status = 'pending'
259- AND (scheduled_for IS NULL OR scheduled_for <= NOW())
260- ORDER BY priority ASC, created_at ASC
261- FOR UPDATE SKIP LOCKED
262- LIMIT 1
263- )
264- RETURNING *` ;
267+ const channel = options . channel ?? null ;
268+ const privilege = options . privilege ?? null ;
269+ const space = options . space ?? null ;
270+ const priorityLte = options . priorityLte ?? null ;
271+ const rows = await sql `
272+ UPDATE cc_intents
273+ SET status = 'claimed', updated_at = NOW()
274+ WHERE id = (
275+ SELECT id FROM cc_intents
276+ WHERE status = 'pending'
277+ AND (${ channel } ::text IS NULL OR target_channel = ${ channel } )
278+ AND (${ privilege } ::text IS NULL OR privilege = ${ privilege } )
279+ AND (${ space } ::text IS NULL OR space = ${ space } )
280+ AND (${ priorityLte } ::int IS NULL OR priority <= ${ priorityLte } )
281+ AND (scheduled_for IS NULL OR scheduled_for <= NOW())
282+ ORDER BY priority ASC, created_at ASC
283+ FOR UPDATE SKIP LOCKED
284+ LIMIT 1
285+ )
286+ RETURNING *` ;
265287 return rows [ 0 ] ? rowToIntent ( rows [ 0 ] ) : null ;
266288}
267289
@@ -288,6 +310,11 @@ export async function markIntentDispatched(
288310// returns after a fresher leader has reclaimed + redispatched the intent, the
289311// stale dispatched_task_id will no longer match and the UPDATE will affect 0
290312// rows. Pass `undefined` to skip the token check (legacy / non-leader paths).
313+ // fixes codex-p2 PR#104 finding-4 — accept 'claimed' as well as 'running'.
314+ // The triage routes expose claim (→'claimed') but no explicit transition to
315+ // 'running', so an autonomous agent that does work and then calls complete
316+ // always hit 409. The token gate from P1-B still prevents stale completions
317+ // when a token is supplied. Failing from terminal states is still rejected.
291318export async function completeIntent (
292319 env : IntentEnv ,
293320 intentId : string ,
@@ -299,13 +326,13 @@ export async function completeIntent(
299326 ? await sql `
300327 UPDATE cc_intents
301328 SET status = 'done', completed_at = NOW(), updated_at = NOW()
302- WHERE id = ${ intentId } AND status = ' running'
329+ WHERE id = ${ intentId } AND status IN ('claimed', ' running')
303330 RETURNING *`
304331 : await sql `
305332 UPDATE cc_intents
306333 SET status = 'done', completed_at = NOW(), updated_at = NOW()
307334 WHERE id = ${ intentId }
308- AND status = ' running'
335+ AND status IN ('claimed', ' running')
309336 AND dispatched_task_id = ${ expectedDispatchedTaskId }
310337 RETURNING *` ;
311338 return rows [ 0 ] ? rowToIntent ( rows [ 0 ] ) : null ;
@@ -434,6 +461,10 @@ function rowToIntent(row: Record<string, unknown>): Intent {
434461 scheduledFor : row . scheduled_for ? new Date ( row . scheduled_for as string ) : null ,
435462 completedAt : row . completed_at ? new Date ( row . completed_at as string ) : null ,
436463 errorMessage : ( row . error_message as string ) ?? null ,
464+ // @canon : chittycanon://gov/governance#classification-axes STATUS:PENDING
465+ privilege : ( ( row . privilege as IntentPrivilege | undefined ) ?? 'public' ) ,
466+ // @canon : chittycanon://gov/governance#classification-axes STATUS:PENDING
467+ space : ( ( row . space as IntentSpace | undefined ) ?? 'business' ) ,
437468 metadata : ( row . metadata as Record < string , unknown > ) ?? { } ,
438469 createdAt : new Date ( row . created_at as string ) ,
439470 updatedAt : new Date ( row . updated_at as string ) ,
0 commit comments