Skip to content

fix: resolve 8 Codex P2 findings on PR #101 - #103

Merged
chitcommit merged 8 commits into
mainfrom
fix/codex-p2-comments
Jun 4, 2026
Merged

chitcommit merged 8 commits into
mainfrom
fix/codex-p2-comments

Conversation

@chitcommit

@chitcommit chitcommit commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Resolves the 8 P2 findings Codex raised on #101.

Findings → resolution

# File:line Finding Resolution
F1 meta/intent.ts:255 No reclaim path for intents stuck in running New reclaimStuckIntents(maxRunningSeconds); called once per leader tick from daemon/loop.ts before claim. Adds cc_intents.reclaim_count column.
F2 daemon/leader.ts:166 releaseLeadership matches only node_id — a stale process can release a newer leader's lease UPDATE now also requires session_id IS NOT DISTINCT FROM $sessionId.
F3 daemon/loop.ts:203 No heartbeat during executor.execute(intent) — slow executors can let the lease lapse setInterval ticker at leaseSeconds/2 (min 1s) wraps the execute span; cleared in finally; uses current session.
F4 src/db/schema.ts:456 cc_intents.plan_id and cc_intents.goal_id had independent FKs, allowing intent.goal_id ≠ plan.goal_id Adds UNIQUE(id, goal_id) on cc_plans; replaces plan_id FK with composite (plan_id, goal_id) → cc_plans(id, goal_id) ON DELETE CASCADE.
F5 daemon/leader.ts:140 heartbeat matches only node_id — a restarted process clobbers a newer leader UPDATE now also requires session_id IS NOT DISTINCT FROM $sessionId.
F6 meta/intent.ts:287 completeIntent overwrites any state, including failed/blocked_human/done Adds WHERE status = 'running'; failIntent mirrors with WHERE status IN ('claimed','running'). Returns null when guarded.
F7 meta/context.ts:131 getEcosystemAwareness had no AGENT_CONNECT fallback Falls through to the existing fallback() helper (service binding) when the HTTPS path is unavailable.
F8 meta/context.ts:63 Re-implemented token retrieval instead of reusing the canonical CHITTY_CONNECT_TOKEN binding ContextEnv now reads CHITTY_CONNECT_TOKEN (canonical, used in src/lib/cron.ts, src/routes/bridge/*) with legacy CHITTYCONNECT_TOKEN as fallback.

Schema validation (F4)

Validated on disposable Neon branch br-delicate-mode-akkgde73 of project cool-bar-13270800 BEFORE committing:

  • Migration applied cleanly: drop old FK, add UNIQUE(id, goal_id), add composite FK, add reclaim_count column.
  • Matching (plan_id, goal_id) insert succeeded.
  • Mismatched goal_id insert failed with: insert or update on table "cc_intents" violates foreign key constraint "cc_intents_plan_goal_cc_plans_fk"

Tests

  • tests/meta/intent-lifecycle.spec.ts — F1 reclaim round-trip (incl. reclaim_count bump, fresh-running rows untouched) + F6 state guards.
  • tests/daemon/leader-session.spec.ts — F2 + F5: old session cannot heartbeat or release after a new session of the same node reclaims.
  • Existing tests/daemon/leader.spec.ts updated to pass sessionId on heartbeat/releaseLeadership calls.

All tests follow the existing real-Neon pattern: skip cleanly when DATABASE_URL is absent.

Deviations from spec (call-outs for the next architect)

  • Spec referenced session_token; the real schema column is session_id. Used session_id.
  • Spec referenced clearing claimed_by / claimed_at in the reclaim path; cc_intents has neither. Staleness measured via updated_at (bumped by dispatch + heartbeat) and dispatched_task_id is cleared instead.
  • src/middleware/auth.ts had no CHITTYCONNECT_TOKEN reference; the canonical binding in this worker is CHITTY_CONNECT_TOKEN (see src/lib/cron.ts:378). Used that.

Verification

  • npm run typecheck — clean.
  • 4 logical commits (schema, intent, daemon, context) + 1 test commit.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Session-scoped leadership mechanism prevents stale processes from interfering with active leaders.
    • Automatic recovery for intents stuck during execution.
  • Bug Fixes

    • Improved intent completion logic to prevent stale executions from overwriting newer operations.
    • Enhanced token binding with fallback support for configuration flexibility.
  • Database

    • Updated schema with reclaim counters and composite foreign keys for stronger consistency.

@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f68215a0-b87b-4b19-8aea-05c2f9972b88

📥 Commits

Reviewing files that changed from the base of the PR and between 03e8fc9 and 053b158.

📒 Files selected for processing (11)
  • daemon/leader.ts
  • daemon/loop.ts
  • meta/context.ts
  • meta/intent.ts
  • migrations/0003_foamy_king_cobra.sql
  • migrations/meta/0003_snapshot.json
  • migrations/meta/_journal.json
  • src/db/schema.ts
  • tests/daemon/leader-session.spec.ts
  • tests/daemon/leader.spec.ts
  • tests/meta/intent-lifecycle.spec.ts

📝 Walkthrough

Walkthrough

This PR adds session ownership guards to prevent stale leaders from interfering with active sessions and execution tokens to prevent stale leaders from incorrectly completing work. The database schema adds composite foreign keys and reclaim tracking. The leader loop integrates session-scoped heartbeats, intent reclamation, and token-gated completion. New tests validate leadership and intent lifecycle guards independently.

Changes

Session Ownership and Execution Token Guards

Layer / File(s) Summary
Database schema for composite FK and reclaim tracking
src/db/schema.ts, migrations/0003_foamy_king_cobra.sql, migrations/meta/_journal.json
Drizzle schema adds composite uniqueness on cc_plans(id, goalId) and composite FK on cc_intents(plan_id, goal_id). Migration heals legacy data mismatches, replaces single-column FK with composite FK, adds reclaim_count column (default 0), and updates journal.
Leader session ownership guards for heartbeat and release
daemon/leader.ts
heartbeat and releaseLeadership now accept optional sessionId. Heartbeat only updates lease when stored session_id matches via IS NOT DISTINCT FROM. Release requires both node_id and session_id to match, preventing restarted processes from extending newer sessions' leases.
Intent completion and failure guarded by execution tokens
meta/intent.ts
completeIntent and failIntent accept optional expectedDispatchedTaskId and only succeed if dispatched_task_id matches the provided token. reclaimStuckIntents resets stale running/claimed intents to pending, clearing execution tokens and incrementing reclaim_count. Prevents stale leaders from overwriting newer executions.
Canonical bearer token with legacy fallback
meta/context.ts
ContextEnv adds canonical CHITTY_CONNECT_TOKEN binding with CHITTYCONNECT_TOKEN as legacy fallback via new resolveConnectToken helper. Primary HTTPS request path resolves token and validates both URL and token presence. getEcosystemAwareness attempts fallback through AGENT_CONNECT service binding on primary failure.
Leader loop session-scoped heartbeats and token-gated execution
daemon/loop.ts
Integrates session/token guards: includes sessionId in best-effort release on exit, computes innerHeartbeatMs cadence, calls reclaimStuckIntents before dispatch, runs background heartbeat during executor (scoped with sessionId), tracks dispatchedTaskId, gates completion/failure with token validation, logs stale-status cases, clears interval in finally.
Session ownership integration tests for leader heartbeat and release
tests/daemon/leader-session.spec.ts
New integration test suite validating leadership session guards: heartbeat test (F5) verifies stale session cannot renew expired lease; release test (F2) verifies stale session cannot release newer session's lease. Both create unique identifiers, clean up prior leases, and assert ownership invariants.
Update existing leader tests to use sessionId
tests/daemon/leader.spec.ts
Updates existing tests to pass sessionId in heartbeat and releaseLeadership calls: holder test uses same sessionId, non-holder test uses competing node's sessionId, release test uses holder's sessionId.
Execution token and intent reclamation integration tests
tests/meta/intent-lifecycle.spec.ts
New integration test suite validating intent lifecycle guards: completeIntent idempotence, failIntent terminal-state protection, reclaimStuckIntents recovery (stale intents reset to pending with token cleared and count incremented), and execution token rejection (stale token prevents completion, matching token succeeds).

Sequence Diagram

sequenceDiagram
  participant LeaderA as Leader Session A
  participant Lease as cc_node_leases
  participant Intent as cc_intents
  participant LeaderB as Leader Session B
  participant Exec as executor(intent)

  LeaderA->>Lease: claim leadership (sessionId_A)
  Lease-->>LeaderA: lease granted
  Note over LeaderA: process restarts
  LeaderB->>Lease: claim leadership (sessionId_B, same nodeId)
  Lease-->>LeaderB: lease granted (newer session)
  
  LeaderA->>Lease: heartbeat (sessionId_A)
  Lease-->>LeaderA: null (session mismatch)
  
  LeaderB->>Intent: claim intent, dispatch
  Intent-->>LeaderB: token_1
  LeaderB->>Exec: call with background heartbeat (sessionId_B)
  Exec-->>LeaderB: completes with result
  
  LeaderA->>Intent: complete with stale token
  Intent-->>LeaderA: null (wrong sessionId in context)
  
  LeaderB->>Intent: complete with token_1
  Intent-->>LeaderB: done (token matches)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 Sessions now own their leases bright,
Execution tokens guard the night,
Stale leaders cannot steal the work,
While fresh sessions claim their perch,
With reclaimed intents set right! 🌙

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/codex-p2-comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 3, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
chittycommand 053b158 Jun 04 2026, 02:08 AM

@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
  1. @coderabbitai review
  2. @copilot review
  3. @codex review
  4. @claude review
    Adversarial review request: evaluate security, policy bypass paths, regression risk, and merge-gating bypass attempts.

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

Base automatically changed from feat/meta-orchestrator-extension to main June 3, 2026 19:33
@chitcommit
chitcommit enabled auto-merge (squash) June 3, 2026 19:34

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1258ac13d4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread migrations/0003_codex_p2_fixes.sql Outdated
Comment thread daemon/loop.ts
Comment thread migrations/0003_codex_p2_fixes.sql Outdated
chitcommit and others added 5 commits June 3, 2026 22:45
Codex P2 PR#101 findings 1 and 4.

- F4: cc_intents now references cc_plans via composite FK
  (plan_id, goal_id) -> cc_plans(id, goal_id) so an intent's goal_id
  MUST match its plan's goal_id. Adds UNIQUE(id, goal_id) on cc_plans
  to back the composite reference.
- F1: adds cc_intents.reclaim_count for stuck-intent bookkeeping
  surfaced by reclaimStuckIntents() in the next commit.

Validated on disposable Neon branch br-delicate-mode-akkgde73 off
project cool-bar-13270800: matching (plan_id, goal_id) insert succeeds;
mismatched goal_id insert is rejected with
"violates foreign key constraint cc_intents_plan_goal_cc_plans_fk".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codex P2 PR#101 findings 1 and 6.

- F1: adds reclaimStuckIntents(maxRunningSeconds) — atomically resets
  intents whose status='claimed'|'running' updated_at is older than the
  threshold back to 'pending', increments reclaim_count, and clears
  dispatched_task_id + error_message. Idempotent; returns rowcount.
  cc_intents has no claimed_by/claimed_at; staleness is measured via
  updated_at, which dispatch and (next-commit) heartbeat paths bump.
- F6: completeIntent now requires status='running'; failIntent allows
  ('claimed','running') but never overwrites a terminal state. Prevents
  parallel cancellation paths from being silently clobbered.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codex P2 PR#101 findings 2, 3, and 5.

- F5: heartbeat() now matches WHERE session_id IS NOT DISTINCT FROM
  $sessionId. A restarted process with the same nodeId cannot extend
  a lease that already belongs to a newer leader.
- F2: releaseLeadership() applies the same session-ownership guard.
- F3: runLeaderLoop wraps each executor.execute(intent) in a
  setInterval heartbeat ticker at lease/2 cadence (min 1s). The
  interval is cleared in finally. Ticker uses the same session token
  so stale executors cannot keep a foreign lease alive.
- Also: calls reclaimStuckIntents(leaseSeconds * 2) once per tick
  before claiming new work (wires up F1 from the previous commit).
- Updates tests/daemon/leader.spec.ts to pass sessionId on heartbeat
  and release so it remains green under the new ownership guard.

Schema column is session_id; the original task spec said
"session_token". Used the real column.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…llback

Codex P2 PR#101 findings 7 and 8.

- F8: ContextEnv now prefers CHITTY_CONNECT_TOKEN — the binding already
  used by src/lib/cron.ts, src/routes/bridge/credentials.ts, and the
  rest of the worker. Legacy CHITTYCONNECT_TOKEN is kept as a
  back-compat fallback via resolveConnectToken().
- F7: getEcosystemAwareness now falls back to the AGENT_CONNECT service
  binding when the HTTPS path has no URL/token or the upstream fetch
  fails — matching the persist/recall fallback pattern already in
  place. Avoids hard failure when the worker only has the binding.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- tests/meta/intent-lifecycle.spec.ts — exercises F1 (reclaim round-
  trip, reclaim_count increment, fresh-running rows untouched) and F6
  (completeIntent rejects non-running; failIntent cannot overwrite a
  terminal state).
- tests/daemon/leader-session.spec.ts — exercises F2 + F5 by letting
  an old session's lease expire, having a new session reclaim, then
  proving the old session can neither heartbeat nor release.

Both tests follow the existing pattern in tests/daemon/leader.spec.ts:
real Neon, skip with `describe.skipIf(!DATABASE_URL)` so CI without a
Neon branch URL doesn't fail. Realistic ChittyOS-shaped IDs only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@chitcommit
chitcommit force-pushed the fix/codex-p2-comments branch from 1258ac1 to 5ce549f Compare June 3, 2026 22:46
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
  1. @coderabbitai review
  2. @copilot review
  3. @codex review
  4. @claude review
    Adversarial review request: evaluate security, policy bypass paths, regression risk, and merge-gating bypass attempts.

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

Codex P1-A — the hand-written migrations/0003_codex_p2_fixes.sql was not
listed in migrations/meta/_journal.json. drizzle-kit migrate iterates the
journal entries, so the file would have been silently skipped at deploy
time: reclaim_count column missing, composite FK uninstalled,
reclaimStuckIntents() crashing at runtime on the production schema.

Ran drizzle-kit generate against the updated schema in src/db/schema.ts
to produce migrations/0003_foamy_king_cobra.sql plus the matching
migrations/meta/0003_snapshot.json and the journal entry. The generated
SQL preserves every change from the hand-written file: reclaim_count
column, cc_plans UNIQUE(id, goal_id), drop of the old plan_id FK, and
the new composite cc_intents_plan_goal_cc_plans_fk.

Validated end-to-end on Neon branch br-soft-moon-aki26rfn off
cool-bar-13270800: after applying 0002 + 0003 the schema reflects the
new column and composite FK, mismatched (plan_id, goal_id) inserts are
rejected with the expected constraint name.
…de fresh runs

Codex P1-B — daemon/loop.ts:199 had a race where a leader L1 that loses
its lease mid-executor() could still return and call completeIntent /
failIntent for the intent that a fresher leader L2 has already reclaimed
and is currently re-driving. The stale call would flip L2's running
execution to 'done' (or 'failed') before L2 finishes.

Use dispatched_task_id as a per-execution token. markIntentDispatched
sets it and reclaimStuckIntents clears it, so it's monotonic within an
execution attempt. completeIntent and failIntent gain an optional
expectedDispatchedTaskId argument; when provided, the WHERE clause also
requires dispatched_task_id = token. UPDATE 0 rows means a fresher
leader has taken over — we log the stale event and return null.

daemon/loop.ts captures the token returned from markIntentDispatched
(itself null when status is no longer 'claimed') and passes it through
both the success path and the catch branch. Failure before dispatch
keeps the original unguarded behaviour.

Tests/meta/intent-lifecycle.spec.ts gains a stale-token scenario that
seeds an intent running under token-T2, confirms a completeIntent /
failIntent call with token-T1 is rejected (status unchanged), and that
the matching token-T2 call succeeds.

Validated against Neon branch br-soft-moon-aki26rfn (cool-bar-13270800):
the gated UPDATE returns 0 rows when the token mismatches and flips to
'done' when it matches. npm run typecheck passes.
…composite FK

Codex P2-A — before the new composite FK existed, cc_intents.plan_id and
cc_intents.goal_id had independent FKs, so a row could have plan_id
pointing at goal_X while goal_id was goal_Y. Adding the composite FK
cc_intents_plan_goal_cc_plans_fk validates every existing row; any such
mismatched legacy row aborts the migration.

Insert a preflight UPDATE inside 0003_foamy_king_cobra.sql, run after
the cc_plans UNIQUE constraint is added and before the composite FK is
added, that rewrites cc_intents.goal_id to match its plan's goal_id for
any row where they disagree. The plan's goal_id is the authoritative
source — the composite FK enforces exactly this invariant going forward.

Validated on Neon branch br-soft-moon-aki26rfn (cool-bar-13270800):
seeded a row with mismatched goal_id, ran 0003 in full, confirmed the
preflight UPDATE healed the row (goal_id flipped from goal_B to
plan->goal_A) and the composite FK installed cleanly. Post-migration,
a fresh mismatched insert is rejected with the new constraint name.
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
  1. @coderabbitai review
  2. @copilot review
  3. @codex review
  4. @claude review
    Adversarial review request: evaluate security, policy bypass paths, regression risk, and merge-gating bypass attempts.

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@chitcommit
chitcommit merged commit d11f195 into main Jun 4, 2026
17 of 18 checks passed
@chitcommit
chitcommit deleted the fix/codex-p2-comments branch June 4, 2026 02:09

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 053b15871a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread daemon/loop.ts
await completeIntent(env, intent.id);
intentsProcessed += 1;
log('intent_completed', { intentId: intent.id, dispatchedTaskId: result.dispatchedTaskId });
const dispatched = await markIntentDispatched(env, intent.id, result.dispatchedTaskId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add ownership guard before dispatching reclaimed claims

When a leader loses its lease while still inside executor(intent) before markIntentDispatched() runs, this patch's new reclaim path can reset that stale claimed row to pending, and the next leader can claim it back to claimed; the old executor then reaches this line and the status-only dispatch update can succeed against the new leader's claim, completing (or, in the catch path, failing) work that another executor is currently driving. Fresh evidence beyond the prior stale-completion issue is that reclaimStuckIntents() now reclaims claimed rows too, but there is still no per-claim token checked here before writing dispatched_task_id.

Useful? React with 👍 / 👎.

chitcommit added a commit that referenced this pull request Jun 4, 2026
…egister the Roux columns

Hand-written 0004_roux_privilege_space.sql was on disk but never registered
in migrations/meta/_journal.json, so drizzle-kit migrate would silently skip
it. Regenerated via `drizzle-kit generate` after rebasing onto the merged
PR #103 baseline; auto-named 0004_premium_toad_men.sql now carries the
same 4 ALTER COLUMN + 4 CREATE INDEX statements (cc_intents/cc_disputes
privilege+space + composite indexes) and is properly journaled with a
matching meta/0004_snapshot.json.

Validated on disposable Neon branch br-green-mode-akxyxiz5 of project
cool-bar-13270800: 0002 + 0003 + 0004 sequence applied cleanly; all 4
columns and 4 indexes verified via information_schema and pg_indexes.

Rebase also dropped 5 commits (f4cb131, 7027daa, 117d77d, 001e157,
5ce549f) that were the pre-squash form of merged PR #103.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
chitcommit added a commit that referenced this pull request Jun 4, 2026
* feat(db): add Roux privilege+space columns to cc_intents and cc_disputes

Migration 0004 adds privilege ∈ {privileged,pii,hoa_evidentiary,public} and
space ∈ {business,legalink} as first-class text columns on cc_intents and
cc_disputes, with composite indexes on (axis, status). CHECK constraints are
deferred — the Roux spec URI is STATUS:PENDING and enum is enforced in the
app layer.

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

Validated on Neon branch br-broad-mud-ak4k790p of project cool-bar-13270800:
ADD COLUMN + CREATE INDEX succeed on top of the post-0003 schema; defaults
applied to existing rows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(meta): thread Roux privilege+space through Intent + createIntent + claimNextIntent

- Intent interface gains privilege/space (typed unions, sourced from canon).
- CreateIntentInput accepts optional privilege/space; INSERT defaults to
  public/business when caller omits.
- claimNextIntent grows null-passthrough filters for privilege, space, and
  priority_lte so a single prepared statement covers the autonomous-bucket
  and human-channel paths.
- rowToIntent maps the new columns.
- sovereignty.ts: add optional privilege field to IntentForSovereignty for
  audit-trail persistence. decide() is intentionally NOT modified — privilege
  is orthogonal to the trust-tier sensitivity axis the gate consumes.

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(routes): ChittyTriage queue routes (list/claim/claim-next/complete)

GET  /api/triage              — list pending intents, filter by privilege/space
POST /api/triage/:id/claim    — atomic claim-by-ID, 409 if not pending
POST /api/triage/claim-next   — bucket-ordered claim for autonomous agents
POST /api/triage/:id/complete — terminal transition (done|failed) with the
                                same status guards used in meta/intent.ts

Mounted after /api/tasks; covered by the existing global /api/* authMiddleware.

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(mcp): add 4 ChittyTriage tools to the MCP surface

- triage_list_intents({privilege?, space?, limit?})
- triage_claim_intent({id})
- triage_claim_next({privilege?, space?, priority_lte?})
- triage_complete_intent({id, outcome, error?})

Each tool wraps the matching /api/triage route's semantics so Claude Code
sessions can drive the queue without HTTP plumbing. Validation reuses the
ratified privilege/space enums from meta/intent.ts.

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(dispute-sync): Roux derive + Notion suppression gate + backfill warn

- deriveRouxFromType(disputeType) maps dispute_type → ratified Roux defaults:
    'legal'      ⇒ {privilege:'privileged', space:'legalink'}
    'insurance'  ⇒ {privilege:'pii',        space:'business'}
    others       ⇒ {privilege:'public',     space:'business'}
- linkDisputeToNotion now gates: if effective privilege ∈ {privileged, pii}
  OR space = 'legalink', the function logs the suppression and returns false
  before touching Notion. Effective values resolve as explicit > derived.
- pushUnlinkedDisputesToNotion now SELECTs privilege/space and emits a
  per-row warn when the row sits on the migration defaults — surfaces
  legacy untagged disputes for explicit triage (Q2=(a) pass-through-warn).
- linkDisputeToNotion is now exported for direct integration testing.

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(adr-001): append Roux/Triage carry-through delta

Records the four ratified decisions from chittycanon-code-cardinal:

  Q1 — privilege+space land as first-class columns on cc_intents AND
       cc_disputes; CHECK deferred until Roux spec is CERTIFIED.
  Q2 — pass-through-with-warn on legacy rows (column defaults).
  Q3 — both specific-by-ID and bucket-ordered claim modes.
  Q4 — privilege is orthogonal to sensitivity; decide() is untouched.

Also documents the Notion mirror gate and explicit > derived resolution.

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: real-Neon coverage for ChittyTriage + Roux dispute-sync gate

tests/routes/triage-roux.spec.ts
  - createIntent privilege+space round-trip
  - claimNextIntent honors bucket filter (won't grab a higher-priority intent
    in the wrong bucket)
  - second atomic claim of the same intent returns 0 rows (the 409 source)
  - bucket-scoped claim still respects priority within the bucket

tests/lib/dispute-sync-roux.spec.ts
  - deriveRouxFromType: legal/insurance/property/vendor + unknown safe default
  - linkDisputeToNotion suppresses on explicit privilege=privileged
  - linkDisputeToNotion suppresses on explicit space=legalink
  - linkDisputeToNotion suppresses 'legal' via derived defaults
  - explicit (public, business) override beats derived (privileged, legalink)
    for a 'legal' dispute_type — gate passes (Notion call then no-ops in test)

Skip pattern mirrors tests/meta/intent-lifecycle.spec.ts — disabled without
DATABASE_URL or with SKIP_INTEGRATION=1.

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(tests): bump MCP tool count to 54 + guard dispute-sync-roux neon init

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(migration): regenerate 0004 via drizzle-kit so journal/snapshot register the Roux columns

Hand-written 0004_roux_privilege_space.sql was on disk but never registered
in migrations/meta/_journal.json, so drizzle-kit migrate would silently skip
it. Regenerated via `drizzle-kit generate` after rebasing onto the merged
PR #103 baseline; auto-named 0004_premium_toad_men.sql now carries the
same 4 ALTER COLUMN + 4 CREATE INDEX statements (cc_intents/cc_disputes
privilege+space + composite indexes) and is properly journaled with a
matching meta/0004_snapshot.json.

Validated on disposable Neon branch br-green-mode-akxyxiz5 of project
cool-bar-13270800: 0002 + 0003 + 0004 sequence applied cleanly; all 4
columns and 4 indexes verified via information_schema and pg_indexes.

Rebase also dropped 5 commits (f4cb131, 7027daa, 117d77d, 001e157,
5ce549f) that were the pre-squash form of merged PR #103.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(dispute-sync): normalize dispute_type + re-derive on default backfill

Finding 6 (P1): deriveRouxFromType only matched exact strings, so free-text
dispute_type values like "Legal" or "legal dispute" fell through to the
default public/business mapping and leaked into the Notion business bucket.
Lowercase + trim, then route any string containing "legal" to
privileged/legalink and any containing "insurance" to pii/business. This is
intentionally over-broad on the privileged side as a fail-safe.

Finding 1 (P1): pushUnlinkedDisputesToNotion treated rows that landed on the
column defaults (public/business) as explicit values, so a row with
dispute_type='legal' but no explicit privilege/space was passed through the
gate. Now, when both axes sit on defaults, re-derive from dispute_type for
the gate decision (DB row is untouched — retag is a separate concern).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(triage): reject invalid claim-next filters with 400

Finding 2 (P2): claim-next called parsePrivilege/parseSpace and treated null
as "no filter", so an unrecognized value (e.g. "pi" instead of "pii") would
silently fall through and claim the highest-priority pending intent from any
bucket — including privileged/legalink. Mirror the GET /api/triage pattern:
when a filter is provided but doesn't parse, return 400 with the valid enum.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(triage): exclude future scheduled_for from direct claim

Finding 5 (P2): list + claim-next correctly exclude future-scheduled intents,
but the direct claim path (POST /api/triage/:id/claim and the
triage_claim_intent MCP tool) only gated on status='pending'. A client with
the ID could short-circuit the schedule and claim tomorrow's intent today.
Add scheduled_for guard to both paths and distinguish 409 with the
scheduled_for value in the response for client UX.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(intent): allow completeIntent/failIntent from claimed or running

Finding 4 (P2): the triage routes expose claim (pending→claimed) but no
explicit claimed→running transition, so an autonomous agent that did its
work and called complete always got 409. Extend completeIntent's WHERE to
accept status IN ('claimed', 'running'), matching failIntent's existing
guard. The P1-B execution-token gate still rejects stale completions when a
token is supplied — so concurrent leaders are still protected.

Update error messages in the HTTP and MCP wrappers to reflect the relaxed
guard. Add three lifecycle tests: claimed→done, claimed→failed, and the
token-gate rejection on a claimed (not running) intent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(auth): require chittytriage:write scope on triage routes

Mounting /api/triage under the generic authMiddleware let any valid
ChittyAuth user token list, claim, and complete privileged/legalink
intents. Add requireTriageScope middleware that runs after authMiddleware
and rejects callers without chittytriage:write, chittytriage:admin,
admin, or wildcard scope.

Also exports hasTriageScope() for reuse by the MCP dispatcher (next
commit) since that path runs inside JSON-RPC, not Hono middleware.

fixes codex-p2 PR#104 P1 — elevated scope on triage routes

* fix(mcp): scope-gate triage tools, filter from tools/list when unscoped

The 4 triage_* MCP tools were reachable by any caller accepted by
mcpAuthMiddleware with the broad 'mcp' scope or the legacy KV shared
service token. That lets generic MCP integrations enumerate and mutate
privileged/legalink orchestration intents.

Two layers:
 - tools/list now filters triage_* tools out of the catalog unless the
   caller has chittytriage:write (don't advertise what they can't call).
 - tools/call enforces the same scope as a defense-in-depth gate so a
   caller that knows the tool name can't bypass the list filter.

Uses hasTriageScope() helper exported from src/middleware/auth.ts.

fixes codex-p2 PR#104 P1 — elevated scope on triage MCP tools

* test(intent): pin reclaim → re-claim → stale-token-A race rejection

Codex flagged a possible race where a stale client A could complete a
re-claimed intent owned by client B with token A. After audit, the
existing token gate from PR#103 P1-B already prevents this:

  meta/intent.ts:332-337,367-373 — completeIntent/failIntent UPDATE
  predicate is `status IN ('claimed','running') AND dispatched_task_id
  = $expectedDispatchedTaskId`, covering BOTH claimed and running.

  meta/intent.ts:402 — reclaimStuckIntents sets dispatched_task_id =
  NULL when resetting to pending, so any subsequent re-claim writes a
  fresh token and the stale A token can never match.

Add a regression test that drives the exact race the reviewer
described (claim with tokenA → updated_at past reclaim window →
reclaimStuckIntents → re-claim with tokenB → tokenA tries complete +
fail) and asserts the row stays B's. No production-code change.

fixes codex-p2 PR#104 P2 — pin reclaim claim-token race

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant