Skip to content

Commit aee1235

Browse files
authored
Merge branch 'main' into feat/daemon-supervisor-vm-bootstrap
2 parents 6d02b99 + 167de96 commit aee1235

16 files changed

Lines changed: 4631 additions & 41 deletions

docs/architecture/ADR-001-meta-orchestrator-extension.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,48 @@ CHITTYOS/chittycommand
118118
surfaces the meta-orchestrator can route to. No code change required this PR.
119119
- The cluster-daemon runtime depends on Neon reachability for leader election;
120120
the "park the node" fallback is acceptable for MVP and will be revisited.
121+
122+
---
123+
124+
## Delta: Roux/Triage Carry-Through (2026-06-03)
125+
126+
> @canon: chittycanon://gov/governance#classification-axes STATUS:PENDING
127+
128+
Ratified by chittycanon-code-cardinal.
129+
130+
### Q1 — Where do `privilege` / `space` live?
131+
**(c) ratified.** Add to `cc_intents` AND `cc_disputes` directly as first-class
132+
columns (text, NOT NULL, defaults `public` / `business`), backed by indexes on
133+
`(privilege, status)` and `(space, status)`. CHECK constraints deferred because
134+
the Roux spec URI is `STATUS:PENDING` certification. App layer enforces the
135+
enum in `meta/intent.ts` and `src/routes/triage.ts`.
136+
137+
### Q2 — Migration semantics on existing rows?
138+
**(a) pass-through-with-warn.** The migration applies `DEFAULT 'public'` /
139+
`'business'` so existing rows are valid. `pushUnlinkedDisputesToNotion` emits
140+
a one-time per-row log when it encounters a row sitting on those defaults
141+
recommending an explicit tag. No backfill writes.
142+
143+
### Q3 — How does Triage claim work?
144+
**Both modes.** Specific-by-ID claim (`POST /api/triage/:id/claim`, atomic,
145+
409 if not pending) for human triagers; bucket-ordered claim
146+
(`POST /api/triage/claim-next`) for autonomous agents, parameterised on
147+
`privilege`, `space`, `priority_lte`. Routes are MCP-exposed as
148+
`triage_list_intents`, `triage_claim_intent`, `triage_claim_next`,
149+
`triage_complete_intent`.
150+
151+
### Q4 — Vocabulary alignment with sovereignty.ts?
152+
**Orthogonal axes — DO NOT TOUCH `decide()`.** The pre-existing
153+
`sensitivity ∈ {low, normal, sensitive, critical}` on
154+
`IntentForSovereignty` is the trust-tier axis the sovereignty matrix consumes.
155+
`privilege ∈ {privileged, pii, hoa_evidentiary, public}` is the Roux
156+
classification axis — informational on `IntentForSovereignty`, persisted on
157+
the intent row, but never an input to the autonomous/human/blocked decision.
158+
159+
### Notion mirror gate
160+
`linkDisputeToNotion` refuses to mirror any dispute where the effective
161+
`privilege ∈ {privileged, pii}` OR `space === 'legalink'`. Effective values
162+
resolve as `explicit > deriveRouxFromType(dispute_type)`. `legal`
163+
`(privileged, legalink)`; `insurance``(pii, business)`; everything else
164+
defaults to `(public, business)`. This prevents privileged work-product and
165+
PII from being mirrored into the operations Notion workspace.

meta/intent.ts

Lines changed: 61 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
2331
export 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.
234257
export 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.
291318
export 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),

meta/sovereignty.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,19 @@ export interface IntentForSovereignty {
2828
* decision. Optional — defaults to 'normal'.
2929
*/
3030
sensitivity?: 'low' | 'normal' | 'sensitive' | 'critical';
31+
/**
32+
* @canon: chittycanon://gov/governance#classification-axes STATUS:PENDING
33+
*
34+
* ChittyRoux privilege class — orthogonal to `sensitivity` (which is the
35+
* trust-tier axis the decide() matrix consumes). `privilege` is informational
36+
* here so callers can persist it on the intent; it does NOT feed decide().
37+
*
38+
* - privileged — attorney-client / work-product
39+
* - pii — personally identifiable info
40+
* - hoa_evidentiary — HOA-relevant evidentiary material
41+
* - public — no privilege class applies
42+
*/
43+
privilege?: 'privileged' | 'pii' | 'hoa_evidentiary' | 'public';
3144
/** Optional human-readable summary for audit trail. */
3245
summary?: string;
3346
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
ALTER TABLE "cc_disputes" ADD COLUMN "privilege" text DEFAULT 'public' NOT NULL;--> statement-breakpoint
2+
ALTER TABLE "cc_disputes" ADD COLUMN "space" text DEFAULT 'business' NOT NULL;--> statement-breakpoint
3+
ALTER TABLE "cc_intents" ADD COLUMN "privilege" text DEFAULT 'public' NOT NULL;--> statement-breakpoint
4+
ALTER TABLE "cc_intents" ADD COLUMN "space" text DEFAULT 'business' NOT NULL;--> statement-breakpoint
5+
CREATE INDEX "idx_cc_disputes_privilege" ON "cc_disputes" USING btree ("privilege","status");--> statement-breakpoint
6+
CREATE INDEX "idx_cc_disputes_space" ON "cc_disputes" USING btree ("space","status");--> statement-breakpoint
7+
CREATE INDEX "idx_cc_intents_privilege" ON "cc_intents" USING btree ("privilege","status");--> statement-breakpoint
8+
CREATE INDEX "idx_cc_intents_space" ON "cc_intents" USING btree ("space","status");

0 commit comments

Comments
 (0)