Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
18e071e
feat(db): add Roux privilege+space columns to cc_intents and cc_disputes
chitcommit Jun 3, 2026
3803db9
feat(meta): thread Roux privilege+space through Intent + createIntent…
chitcommit Jun 3, 2026
9ab2b9b
feat(routes): ChittyTriage queue routes (list/claim/claim-next/complete)
chitcommit Jun 3, 2026
a728871
feat(mcp): add 4 ChittyTriage tools to the MCP surface
chitcommit Jun 3, 2026
0d7d06f
feat(dispute-sync): Roux derive + Notion suppression gate + backfill …
chitcommit Jun 3, 2026
a100b66
docs(adr-001): append Roux/Triage carry-through delta
chitcommit Jun 3, 2026
43da5bd
test: real-Neon coverage for ChittyTriage + Roux dispute-sync gate
chitcommit Jun 3, 2026
2cc4499
fix(tests): bump MCP tool count to 54 + guard dispute-sync-roux neon …
chitcommit Jun 3, 2026
395af7a
fix(migration): regenerate 0004 via drizzle-kit so journal/snapshot r…
chitcommit Jun 4, 2026
fdf7ac2
fix(dispute-sync): normalize dispute_type + re-derive on default back…
chitcommit Jun 4, 2026
ffc4e28
fix(triage): reject invalid claim-next filters with 400
chitcommit Jun 4, 2026
498cb6a
fix(triage): exclude future scheduled_for from direct claim
chitcommit Jun 4, 2026
b3438e6
fix(intent): allow completeIntent/failIntent from claimed or running
chitcommit Jun 4, 2026
e0579a1
fix(auth): require chittytriage:write scope on triage routes
chitcommit Jun 4, 2026
aa2f2d0
fix(mcp): scope-gate triage tools, filter from tools/list when unscoped
chitcommit Jun 4, 2026
9da3df6
test(intent): pin reclaim → re-claim → stale-token-A race rejection
chitcommit Jun 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions docs/architecture/ADR-001-meta-orchestrator-extension.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,48 @@ CHITTYOS/chittycommand
surfaces the meta-orchestrator can route to. No code change required this PR.
- The cluster-daemon runtime depends on Neon reachability for leader election;
the "park the node" fallback is acceptable for MVP and will be revisited.

---

## Delta: Roux/Triage Carry-Through (2026-06-03)

> @canon: chittycanon://gov/governance#classification-axes STATUS:PENDING

Ratified by chittycanon-code-cardinal.

### Q1 — Where do `privilege` / `space` live?
**(c) ratified.** Add to `cc_intents` AND `cc_disputes` directly as first-class
columns (text, NOT NULL, defaults `public` / `business`), backed by indexes on
`(privilege, status)` and `(space, status)`. CHECK constraints deferred because
the Roux spec URI is `STATUS:PENDING` certification. App layer enforces the
enum in `meta/intent.ts` and `src/routes/triage.ts`.

### Q2 — Migration semantics on existing rows?
**(a) pass-through-with-warn.** The migration applies `DEFAULT 'public'` /
`'business'` so existing rows are valid. `pushUnlinkedDisputesToNotion` emits
a one-time per-row log when it encounters a row sitting on those defaults
recommending an explicit tag. No backfill writes.

### Q3 — How does Triage claim work?
**Both modes.** Specific-by-ID claim (`POST /api/triage/:id/claim`, atomic,
409 if not pending) for human triagers; bucket-ordered claim
(`POST /api/triage/claim-next`) for autonomous agents, parameterised on
`privilege`, `space`, `priority_lte`. Routes are MCP-exposed as
`triage_list_intents`, `triage_claim_intent`, `triage_claim_next`,
`triage_complete_intent`.

### Q4 — Vocabulary alignment with sovereignty.ts?
**Orthogonal axes — DO NOT TOUCH `decide()`.** The pre-existing
`sensitivity ∈ {low, normal, sensitive, critical}` on
`IntentForSovereignty` is the trust-tier axis the sovereignty matrix consumes.
`privilege ∈ {privileged, pii, hoa_evidentiary, public}` is the Roux
classification axis — informational on `IntentForSovereignty`, persisted on
the intent row, but never an input to the autonomous/human/blocked decision.

### Notion mirror gate
`linkDisputeToNotion` refuses to mirror any dispute where the effective
`privilege ∈ {privileged, pii}` OR `space === 'legalink'`. Effective values
resolve as `explicit > deriveRouxFromType(dispute_type)`. `legal` ⇒
`(privileged, legalink)`; `insurance` ⇒ `(pii, business)`; everything else
defaults to `(public, business)`. This prevents privileged work-product and
PII from being mirrored into the operations Notion workspace.
91 changes: 61 additions & 30 deletions meta/intent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ export type IntentStatus =
| 'failed'
| 'blocked_human';

// @canon: chittycanon://gov/governance#classification-axes STATUS:PENDING
// ChittyRoux privilege class — orthogonal to sovereignty trust-tier sensitivity.
export type IntentPrivilege = 'privileged' | 'pii' | 'hoa_evidentiary' | 'public';

// @canon: chittycanon://gov/governance#classification-axes STATUS:PENDING
// ChittyRoux two-Space partition.
export type IntentSpace = 'business' | 'legalink';

export interface SovereigntyAssessmentSnapshot {
decision: 'autonomous' | 'requires_human' | 'blocked';
trustScore: number;
Expand Down Expand Up @@ -75,6 +83,10 @@ export interface Intent {
scheduledFor: Date | null;
completedAt: Date | null;
errorMessage: string | null;
// @canon: chittycanon://gov/governance#classification-axes STATUS:PENDING
privilege: IntentPrivilege;
// @canon: chittycanon://gov/governance#classification-axes STATUS:PENDING
space: IntentSpace;
metadata: Record<string, unknown>;
createdAt: Date;
updatedAt: Date;
Expand Down Expand Up @@ -192,6 +204,10 @@ export interface CreateIntentInput {
sovereigntyAssessment?: SovereigntyAssessmentSnapshot;
humanGateReason?: string;
scheduledFor?: Date;
// @canon: chittycanon://gov/governance#classification-axes STATUS:PENDING
privilege?: IntentPrivilege;
// @canon: chittycanon://gov/governance#classification-axes STATUS:PENDING
space?: IntentSpace;
metadata?: Record<string, unknown>;
}

Expand All @@ -206,16 +222,20 @@ export async function createIntent(env: IntentEnv, input: CreateIntentInput): Pr
? 'failed'
: 'pending';

// @canon: chittycanon://gov/governance#classification-axes STATUS:PENDING
// privilege/space default to public/business at the column level; pass through
// explicit caller values so high-privilege intents are tagged at creation.
const rows = await sql`
INSERT INTO cc_intents
(plan_id, goal_id, intent_type, target_channel, payload, status, priority,
sovereignty_assessment, human_gate_reason, scheduled_for, metadata)
sovereignty_assessment, human_gate_reason, scheduled_for, privilege, space, metadata)
VALUES
(${input.planId}, ${input.goalId}, ${input.intentType},
${input.targetChannel ?? null}, ${JSON.stringify(input.payload)}::jsonb,
${initialStatus}, ${input.priority ?? 5},
${input.sovereigntyAssessment ? JSON.stringify(input.sovereigntyAssessment) : null}::jsonb,
${input.humanGateReason ?? null}, ${input.scheduledFor ?? null},
${input.privilege ?? 'public'}, ${input.space ?? 'business'},
${JSON.stringify(input.metadata ?? {})}::jsonb)
RETURNING *`;
return rowToIntent(rows[0]);
Expand All @@ -231,37 +251,39 @@ export async function getIntent(env: IntentEnv, id: string): Promise<Intent | nu
* Claim the next pending intent for execution. Atomic via UPDATE...RETURNING.
* Mirrors the lease pattern in chittyentity/workers/shared/agent-tasks.ts.
*/
// @canon: chittycanon://gov/governance#classification-axes STATUS:PENDING
// privilege/space filters use the null-passthrough pattern (param IS NULL OR
// col = param) so a single prepared statement covers all four filter combos.
export async function claimNextIntent(
env: IntentEnv,
options: { channel?: string } = {},
options: {
channel?: string;
privilege?: IntentPrivilege;
space?: IntentSpace;
priorityLte?: number;
} = {},
): Promise<Intent | null> {
const sql = getSql(env);
const rows = options.channel
? await sql`
UPDATE cc_intents
SET status = 'claimed', updated_at = NOW()
WHERE id = (
SELECT id FROM cc_intents
WHERE status = 'pending'
AND target_channel = ${options.channel}
AND (scheduled_for IS NULL OR scheduled_for <= NOW())
ORDER BY priority ASC, created_at ASC
FOR UPDATE SKIP LOCKED
LIMIT 1
)
RETURNING *`
: await sql`
UPDATE cc_intents
SET status = 'claimed', updated_at = NOW()
WHERE id = (
SELECT id FROM cc_intents
WHERE status = 'pending'
AND (scheduled_for IS NULL OR scheduled_for <= NOW())
ORDER BY priority ASC, created_at ASC
FOR UPDATE SKIP LOCKED
LIMIT 1
)
RETURNING *`;
const channel = options.channel ?? null;
const privilege = options.privilege ?? null;
const space = options.space ?? null;
const priorityLte = options.priorityLte ?? null;
const rows = await sql`
UPDATE cc_intents
SET status = 'claimed', updated_at = NOW()
WHERE id = (
SELECT id FROM cc_intents
WHERE status = 'pending'
AND (${channel}::text IS NULL OR target_channel = ${channel})
AND (${privilege}::text IS NULL OR privilege = ${privilege})
AND (${space}::text IS NULL OR space = ${space})
AND (${priorityLte}::int IS NULL OR priority <= ${priorityLte})
AND (scheduled_for IS NULL OR scheduled_for <= NOW())
ORDER BY priority ASC, created_at ASC
FOR UPDATE SKIP LOCKED
LIMIT 1
)
RETURNING *`;
return rows[0] ? rowToIntent(rows[0]) : null;
}

Expand All @@ -288,6 +310,11 @@ export async function markIntentDispatched(
// returns after a fresher leader has reclaimed + redispatched the intent, the
// stale dispatched_task_id will no longer match and the UPDATE will affect 0
// rows. Pass `undefined` to skip the token check (legacy / non-leader paths).
// fixes codex-p2 PR#104 finding-4 — accept 'claimed' as well as 'running'.
// The triage routes expose claim (→'claimed') but no explicit transition to
// 'running', so an autonomous agent that does work and then calls complete
// always hit 409. The token gate from P1-B still prevents stale completions
// when a token is supplied. Failing from terminal states is still rejected.
export async function completeIntent(
env: IntentEnv,
intentId: string,
Expand All @@ -299,13 +326,13 @@ export async function completeIntent(
? await sql`
UPDATE cc_intents
SET status = 'done', completed_at = NOW(), updated_at = NOW()
WHERE id = ${intentId} AND status = 'running'
WHERE id = ${intentId} AND status IN ('claimed', 'running')
Comment thread
chitcommit marked this conversation as resolved.
RETURNING *`
: await sql`
UPDATE cc_intents
SET status = 'done', completed_at = NOW(), updated_at = NOW()
WHERE id = ${intentId}
AND status = 'running'
AND status IN ('claimed', 'running')
AND dispatched_task_id = ${expectedDispatchedTaskId}
RETURNING *`;
return rows[0] ? rowToIntent(rows[0]) : null;
Expand Down Expand Up @@ -434,6 +461,10 @@ function rowToIntent(row: Record<string, unknown>): Intent {
scheduledFor: row.scheduled_for ? new Date(row.scheduled_for as string) : null,
completedAt: row.completed_at ? new Date(row.completed_at as string) : null,
errorMessage: (row.error_message as string) ?? null,
// @canon: chittycanon://gov/governance#classification-axes STATUS:PENDING
privilege: ((row.privilege as IntentPrivilege | undefined) ?? 'public'),
// @canon: chittycanon://gov/governance#classification-axes STATUS:PENDING
space: ((row.space as IntentSpace | undefined) ?? 'business'),
metadata: (row.metadata as Record<string, unknown>) ?? {},
createdAt: new Date(row.created_at as string),
updatedAt: new Date(row.updated_at as string),
Expand Down
13 changes: 13 additions & 0 deletions meta/sovereignty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,19 @@ export interface IntentForSovereignty {
* decision. Optional — defaults to 'normal'.
*/
sensitivity?: 'low' | 'normal' | 'sensitive' | 'critical';
/**
* @canon: chittycanon://gov/governance#classification-axes STATUS:PENDING
*
* ChittyRoux privilege class — orthogonal to `sensitivity` (which is the
* trust-tier axis the decide() matrix consumes). `privilege` is informational
* here so callers can persist it on the intent; it does NOT feed decide().
*
* - privileged — attorney-client / work-product
* - pii — personally identifiable info
* - hoa_evidentiary — HOA-relevant evidentiary material
* - public — no privilege class applies
*/
privilege?: 'privileged' | 'pii' | 'hoa_evidentiary' | 'public';
/** Optional human-readable summary for audit trail. */
summary?: string;
}
Expand Down
8 changes: 8 additions & 0 deletions migrations/0004_premium_toad_men.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
ALTER TABLE "cc_disputes" ADD COLUMN "privilege" text DEFAULT 'public' NOT NULL;--> statement-breakpoint
ALTER TABLE "cc_disputes" ADD COLUMN "space" text DEFAULT 'business' NOT NULL;--> statement-breakpoint
ALTER TABLE "cc_intents" ADD COLUMN "privilege" text DEFAULT 'public' NOT NULL;--> statement-breakpoint
ALTER TABLE "cc_intents" ADD COLUMN "space" text DEFAULT 'business' NOT NULL;--> statement-breakpoint
CREATE INDEX "idx_cc_disputes_privilege" ON "cc_disputes" USING btree ("privilege","status");--> statement-breakpoint
CREATE INDEX "idx_cc_disputes_space" ON "cc_disputes" USING btree ("space","status");--> statement-breakpoint
CREATE INDEX "idx_cc_intents_privilege" ON "cc_intents" USING btree ("privilege","status");--> statement-breakpoint
CREATE INDEX "idx_cc_intents_space" ON "cc_intents" USING btree ("space","status");
Loading
Loading