Skip to content

PAN-3511 - #3529

Merged
overdeck-agent[bot] merged 43 commits into
mainfrom
feature/pan-3511
Aug 5, 2026
Merged

PAN-3511#3529
overdeck-agent[bot] merged 43 commits into
mainfrom
feature/pan-3511

Conversation

@eltmon

@eltmon eltmon commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Issue: #3511

Acceptance Criteria

  • Add the shared artifact-restore helper that predicts the head-mismatch rejection
  • Route the landed orphan-reset restore through the shared helper
  • Consult the artifact before marking a feedback-delivery failure stuck
  • Consult the artifact before both review-infrastructure breaker trips
  • Consult the artifact in the sweeper's stuck-flag orbit before any clear or re-drive
  • Let a corroborating artifact lift a stale-journal refusal, memoized off the hot read path
  • Document the verdict-of-record contract and the sweeper's artifact consult

Summary by CodeRabbit

  • New Features

    • Signed review artifacts are now the authoritative source for restoring verdicts.
    • Added verification of artifact freshness, run identity, and reviewed commit.
    • Added review-agent attestation for completed reports.
    • Added protection to keep attestation secrets out of agent workspaces.
    • Added reporting when restoration is blocked by conflicting evidence.
  • Bug Fixes

    • Fresh approved artifacts automatically unpark stuck reviews without unnecessary agent messages.
    • Blocked or failed verdicts preserve reviewer feedback when requesting rework.
    • Missing or invalid evidence continues to prevent accidental approval.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds host-attested review artifacts, trusted verdict reads, bounded memoization, safe verdict restoration, and artifact-first recovery across review completion, journal reconciliation, Deacon recovery, feedback delivery, and stall sweeping.

Changes

Attested verdict recovery

Layer / File(s) Summary
Attestation and artifact contracts
src/lib/review-attestation-key.ts, src/lib/cloister/review-artifact-attestation.ts, src/lib/overdeck/agent-review-provenance.ts, packages/contracts/src/events.ts
Adds signed context and report artifacts, run-bound tokens, reviewed-HEAD validation, provenance lookup, secret boundaries, and blocked-restore events.
Review run and completion flow
src/lib/cloister/review-agent.ts, src/cli/commands/specialists/done.ts, src/dashboard/server/routes/specialists/legacy-routes.ts, src/lib/agents/*, src/lib/child-env.ts
Review runs use attested identifiers and host-issued tokens. Completion submits verdicts for attestation and records the returned reviewed HEAD.
Trusted reads and restoration
src/lib/cloister/synthesis-verdict.ts, src/lib/review-status-read.ts, src/lib/cloister/verdict-restore.ts, src/lib/cloister/verdict-head-guard.ts
Readers validate fresh artifacts and memoize results. Restoration applies matching verdicts, preserves head mismatches, and reports blocked restores.
Recovery integrations and validation
src/lib/cloister/deacon-review-status.ts, src/lib/cloister/stall-sweeper.ts, src/lib/cloister/feedback-target.ts, src/lib/cloister/__tests__/*, src/lib/__tests__/*, tests/*
Recovery paths consult artifacts before stuck transitions or re-drive. Tests cover restoration, freshness, attestation, head guards, missing evidence, and environment handling.
Documentation and support
docs/PARKED-POPULATION.md, docs/REVIEW-AGENT-ARCHITECTURE.md, roles/review.md, scripts/file-size-allowlist.txt
Documents artifact-first recovery, verdict-of-record rules, review-mode resolution, and the audited file-size allowance.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.89% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title contains only the issue identifier and does not describe the pull request's main change. Replace "PAN-3511" with a concise title that describes the shared artifact-verdict restoration and review recovery changes.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch feature/pan-3511
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/pan-3511

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.

@coderabbitai coderabbitai 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.

Actionable comments posted: 7

🧹 Nitpick comments (2)
src/lib/cloister/__tests__/verdict-restore-breaker.test.ts (2)

111-112: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Import the real breaker threshold instead of duplicating it.

BREAKER_THRESHOLD = 3 duplicates REVIEW_INFRA_BREAKER_THRESHOLD. If the production constant changes, status() no longer builds a row that reaches the breaker, and these tests pass while covering a different branch. Import the exported constant.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/cloister/__tests__/verdict-restore-breaker.test.ts` around lines 111
- 112, Replace the local BREAKER_THRESHOLD constant in the
verdict-restore-breaker tests with the exported REVIEW_INFRA_BREAKER_THRESHOLD
production constant, importing it from its existing module and using it wherever
the test constructs the breaker threshold.

218-257: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add a breaker case for a head-mismatched artifact.

No artifact in this suite writes context.json, so every artifact carries no head evidence and restoreWouldTripHeadGuard() can never trip. The blocked-by-head-guard branch of artifactSupersededBreaker is therefore untested at both breaker sites, and that branch decides whether the stuck mark is skipped. Add a case that writes context.json with a head that differs from the row's lastVerifiedCommit, then assert the intended markWorkspaceStuck behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/cloister/__tests__/verdict-restore-breaker.test.ts` around lines 218
- 257, The ac3 breaker suite does not cover the head-mismatch path that produces
the blocked-by-head-guard result. Add a test case using writeArtifact to create
context.json with a head different from the row’s lastVerifiedCommit, exercise
the relevant coordinator-death or orphan-status function, and assert the
intended markWorkspaceStuck behavior for artifactSupersededBreaker; cover the
breaker site applicable to the fixture while preserving existing assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/PARKED-POPULATION.md`:
- Around line 47-54: Update the “stuck-flag” orbit documentation to state that a
fresh passed artifact restores the verdict and clears the stuck flag only when
its recorded head matches the row anchor. Document that a head mismatch leaves
the row parked with the stuck flag set, while preserving the existing behavior
for matching passed artifacts and other verdict states.

In `@src/lib/cloister/deacon-review-status.ts`:
- Around line 346-366: Update src/lib/cloister/deacon-review-status.ts lines
346-366 in artifactSupersededBreaker so it returns true only when
restore.outcome is restored; blocked restores must return false so both breaker
call sites still mark the workspace stuck. Update
src/lib/cloister/feedback-target.ts lines 297-306 to use the same restored-only
check, ensuring blocked restores continue applying
FEEDBACK_DELIVERY_STUCK_REASON.
- Around line 616-623: Update the blocked-by-head-guard branch in
checkOrphanedReviewStatuses so repeated blocked patrols cannot leave the
reviewing row permanently unreachable. Track or increment reviewRetryCount and,
once the existing bounded escalation threshold is reached, discharge or
transition the row through the established safe write path; preserve the current
report-only behavior before that limit and do not reset the finished artifact.

In `@src/lib/cloister/stall-sweeper.ts`:
- Around line 377-389: Update the artifact flow in the passed-artifact branch
around readArtifact and restoreVerdict so the decision and restoration use the
same artifact snapshot. Pass the selected artifact into restoreVerdict if
supported, or require restore.artifact.verdict to remain passed before clearing
the stuck flag and emitting sweep.unparked, using that artifact for event data;
otherwise retain the parked outcome. Add coverage for differing verdicts across
the two reads.

In `@src/lib/cloister/synthesis-verdict.ts`:
- Around line 124-130: Update the cache-hit logic in the verdict memoization
flow around artifactVerdictMemo and readLatestSynthesisVerdict so non-null
cached verdicts expire at the earlier of ARTIFACT_VERDICT_MEMO_TTL_MS and the
artifact freshness boundary; preserve null-entry behavior as appropriate. Add a
boundary test covering an artifact that becomes stale while still within the
memo TTL.

In `@src/lib/cloister/verdict-restore.ts`:
- Around line 148-155: Update the default dependency wiring used by
attemptArtifactVerdictRestore to read artifacts through
readMemoizedArtifactVerdict instead of readLatestSynthesisVerdict, and ensure
the deacon-review-status override uses that memoized reader rather than
replacing it with the non-memoized implementation.

In `@src/lib/review-status-read.ts`:
- Around line 52-62: Update the stale-journal branch in the review-status read
flow to require the matching artifact’s headSha to pass the same live-status
head-anchor validation used by attemptArtifactVerdictRestore before calling
reconcileJournalIntoCacheSync. Preserve the existing verdict-match requirement,
and add a test covering an artifact with a mismatched headSha to ensure
reconciliation is not performed.

---

Nitpick comments:
In `@src/lib/cloister/__tests__/verdict-restore-breaker.test.ts`:
- Around line 111-112: Replace the local BREAKER_THRESHOLD constant in the
verdict-restore-breaker tests with the exported REVIEW_INFRA_BREAKER_THRESHOLD
production constant, importing it from its existing module and using it wherever
the test constructs the breaker threshold.
- Around line 218-257: The ac3 breaker suite does not cover the head-mismatch
path that produces the blocked-by-head-guard result. Add a test case using
writeArtifact to create context.json with a head different from the row’s
lastVerifiedCommit, exercise the relevant coordinator-death or orphan-status
function, and assert the intended markWorkspaceStuck behavior for
artifactSupersededBreaker; cover the breaker site applicable to the fixture
while preserving existing assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3264fbbe-ed99-468a-9e54-3a20a585eb88

📥 Commits

Reviewing files that changed from the base of the PR and between e5bbf0d and 3eec512.

📒 Files selected for processing (17)
  • docs/PARKED-POPULATION.md
  • docs/REVIEW-AGENT-ARCHITECTURE.md
  • packages/contracts/src/events.ts
  • roles/review.md
  • scripts/file-size-allowlist.txt
  • src/lib/__tests__/review-status-read-artifact.test.ts
  • src/lib/cloister/__tests__/feedback-target.test.ts
  • src/lib/cloister/__tests__/stall-sweeper.test.ts
  • src/lib/cloister/__tests__/verdict-restore-breaker.test.ts
  • src/lib/cloister/__tests__/verdict-restore.test.ts
  • src/lib/cloister/deacon-review-status.ts
  • src/lib/cloister/feedback-target.ts
  • src/lib/cloister/stall-sweeper.ts
  • src/lib/cloister/synthesis-verdict.ts
  • src/lib/cloister/verdict-restore.ts
  • src/lib/review-status-read.ts
  • tests/unit/lib/head-anchor-write-sites.test.ts

Comment thread docs/PARKED-POPULATION.md Outdated
Comment thread src/lib/cloister/deacon-review-status.ts Outdated
Comment thread src/lib/cloister/deacon-review-status.ts Outdated
Comment thread src/lib/cloister/stall-sweeper.ts Outdated
Comment thread src/lib/cloister/synthesis-verdict.ts Outdated
Comment thread src/lib/cloister/verdict-restore.ts Outdated
Comment thread src/lib/review-status-read.ts
panopticon-agent[bot] and others added 8 commits August 3, 2026 22:17
Adds src/lib/cloister/verdict-restore.ts — the single read-side decision
point every recovery path will call before it marks, resets, or re-drives a
review row. No call sites yet; this item is the contract the next four items
adopt.

attemptArtifactVerdictRestore() reads the freshest verdict artifact through
the already-landed readLatestSynthesisVerdict(), predicts whether writing it
would be rejected for disagreeing with the row's anchor, and either restores
the verdict through setReviewStatusSync or reports the refusal. It writes
nothing on 'no-artifact' and nothing on 'blocked-by-head-guard', so an absent
artifact never invents approval (NFR-4).

restoreWouldTripHeadGuard() is the prediction, kept as one exported pure
function so a future guard change has exactly one place to land (hazard H3).

Blocked restores report through emitActivityEntryOnce with an id keyed on the
exact condition, so a persistently-rejected restore tells the operator once
rather than once per ~60s patrol (hazard H7), while the domain event still
appends every time.

Extra file beyond files_scope, and why:

  packages/contracts/src/events.ts — CloisterEventStore.append() is typed to
  the contracts DomainEvent union, so review.verdict_restore_blocked has to be
  registered there to be a first-class domain event rather than an unvalidated
  blob. This mirrors what PAN-3512 did for review.verdict_rejected and
  review.verdict_dispatched.

Deviations from the item text, both deliberate:

1. The item cites "the review arm of findVerdictEvidenceHeadMismatch at
   src/lib/review-verdict-guards.ts:42-54". That arm no longer exists —
   PAN-3512 (fd48e3a, already on main and in this branch's history)
   deleted it, and the head-anchoring semantic now lives in the verdict write
   door at review-verdict-writer.ts:151-213. The module documents the live
   location rather than the stale one. The predicate itself is unchanged from
   what decision D3 specifies, and is deliberately CONSERVATIVE: it blocks on
   any two differing heads, where the write door would additionally classify a
   differing head as fresh/indeterminate and land it. Making the loss visible
   is this issue; making it land is PAN-3512.

2. The helper restores whatever verdict the artifact carries, not just
   'passed', per the item's "reviewStatus: artifact.verdict". The landed
   orphan restore at deacon-review-status.ts:594 currently gates on 'passed'
   only, so the orphan-restore-adopt item must decide whether to preserve that
   gate at its call site — the helper does not impose it.

Verification: 20 new tests pass, typecheck clean, eslint clean, file-size
guard clean. Fixtures exercise BOTH artifact filenames through the real
reader — synthesis.md (convoy) and review.md (quick self-review, the fleet
default) — per hazard H5, including the no-head-evidence path that most
production restores take, and CHANGES REQUESTED as quick mode's blocked
vocabulary.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C2nHLZQDncBvcnH7VicyLC
…fusal (PAN-3511)

The advancing-journal reconcile is the fourth recovery path that acted on a
review row without consulting the verdict of record. A reviewer writes its
artifact seconds to minutes before that verdict syncs into the row, so a
journal replay that merely LOOKS stale against the live cycle may in fact be
the finished review — and the resolver refused it.

Two changes, both sized by the fact that the resolver is the hot read path:

1. synthesis-verdict.ts gains readMemoizedArtifactVerdict() — a 60s per-issue
   TTL over readLatestSynthesisVerdict, with an explicit `now` so the TTL is
   testable without wall-clock drift, and __resetArtifactVerdictMemo() so the
   module-level Map cannot leak between tests (hazard H4). Absence is memoized
   too, since "no artifact" is the common case and re-scanning for a missing
   file every read is the exact cost this avoids.

2. review-status-read.ts consults it ONLY inside the branch that already
   refuses the replay. When a fresh artifact carries the SAME terminal verdict
   the journal is replaying, the artifact independently corroborates it and the
   reconcile proceeds; an absent or disagreeing artifact leaves the refusal
   exactly as it was.

The consult is strictly one-directional — it can only lift a refusal the
resolver was already making, never invent an approval, which is what keeps
NFR-4 true. Because it sits behind an already-rare branch and is memoized, the
common read path performs zero filesystem work and a refusing path at most one
scan per issue per minute.

Import direction verified acyclic: review-status-read.ts -> cloister/
synthesis-verdict.ts -> projects.ts, which reaches errors/paths/issue-id only
and never re-enters review-status. This matters because the dashboard runs
strict ESM and rejects circular imports at runtime, where typecheck would not.

Verification: 9 new tests pass (29 across both PAN-3511 suites), typecheck
clean, eslint clean, file-size guard clean. The TTL cases prove the no-restat
property by deleting the artifact between reads and asserting the memo still
serves it, rather than by spying on fs.

Pre-existing red observed while checking for regressions, NOT caused by this
change and deliberately not fixed here:

  tests/unit/lib/review-status.test.ts > "rejects a review verdict whose
  evidence HEAD differs from the verified target HEAD" fails on main. It
  asserts the review arm of findVerdictEvidenceHeadMismatch, which PAN-3512
  deleted in fd48e3a without updating the test (last touched by PAN-3377).
  Related reds in tests/cli/commands/specialists/done.test.ts ("Review verdict
  rejected: issue-not-found") and review-verdict-writer.test.ts (0 tests
  collected) are the same PAN-3512 surface. None of those files import the
  modules changed here. Left for PAN-3512 to finish rather than patched into
  this item's scoped diff.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C2nHLZQDncBvcnH7VicyLC
… door (PAN-3511)

Replaces the inline race-guard block landed by 93b88ee with a call to
attemptArtifactVerdictRestore. Behavior-preserving on the writable path, plus
the AC-READ-GUARD emit the inline block had no way to produce.

The orphan patrol installs a passed-only reader through the helper's injection
seam, so ONLY an approved artifact short-circuits the reset — exactly as
before. A blocked or failed artifact still yields 'no-artifact' and falls
through to the unchanged pending reset. Restoring a non-approved verdict here
would be a real behavior change this item does not carry; the helper itself
stays general and the gate lives at the call site.

On 'blocked-by-head-guard' the patrol now returns instead of resetting. The
artifact proves a review FINISHED, so falling through to pending would be the
PAN-1577 wipe by another name — the exact failure this issue exists to close.

Line count: 978 before, 978 after (AC4 — no more lines than before), so the
file stays at its 1000-line ratchet with no allowlist row needed. The inline
block collapsed by 2 lines, which paid for the new import and the
blocked-branch logging.

Test scope, stated plainly: reconcileReviewStatusOrphan is not exported and
needs the whole patrol surface (agents, tmux, status) to drive end-to-end, and
no existing suite covers it — it was untested machinery, which is part of why
the wipe went unnoticed. The four new cases therefore pin the decision the call
site delegates (passed restores and clears the infra gate, mismatched head
reports and writes nothing, blocked/failed and absent both fall through), not
the patrol function itself. End-to-end orphan-patrol coverage remains a gap.

Verification: 25 tests pass in verdict-restore.test.ts, typecheck clean, eslint
clean, file-size guard clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C2nHLZQDncBvcnH7VicyLC
…ck mark (PAN-3511)

surfaceIssueFeedbackNeedsYou marked an issue feedback_delivery_needs_you
whenever no live feedback target could be resurrected — including when the
review had already FINISHED and only its delivery failed. That strands a
completed review behind an operator gate for a transport problem the delivery
machinery retries on its own.

The consult now runs before the mark. On 'restored' or 'blocked-by-head-guard'
the stuck mark is skipped, because either outcome means an artifact exists and
a verdict was reached; only 'no-artifact' falls through to the existing
markWorkspaceStuck call with its arguments unchanged.

The consult fails TOWARD the stuck mark. It sits in its own try/catch that
degrades a reader error to 'no-artifact' rather than letting it escape to the
outer catch, which would have swallowed the mark entirely and silently removed
the protection that exists today (ac4 pins this).

Tests cover both artifact shapes through the real reader — synthesis.md for a
convoy and review.md for quick self-review, which is the fleet default — plus
the no-artifact, reader-throws, and head-mismatch paths. The suite's existing
existsSync mock is registered with each real artifact path so the reader's
findVerdictReport resolves it.

Verification: 14 tests pass (9 pre-existing, 5 new), typecheck clean, eslint
clean, file-size guard clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C2nHLZQDncBvcnH7VicyLC
… trips (PAN-3511)

Both review-infrastructure breaker sites in deacon-review-status.ts marked a row
stuck once reviewRetryCount crossed the threshold, without ever asking whether a
verdict already existed on disk. Retries exhausted while the reviewer's artifact
is sitting there means recovery was chasing a review that had already FINISHED —
stranding it behind an operator gate is the PAN-1577 wipe by another name.

A shared artifactSupersededBreaker() helper runs the restore before
markWorkspaceStuck at both sites (handleReviewCoordinatorDied and the orphan
re-dispatch path). It fails toward the mark: a reader error returns false so
today's protection survives unchanged.

verdict-restore-breaker.test.ts drives both sites end to end through their real
exported entry points, with a real artifact on disk read by the real reader, so
ac4's quick-mode review.md claim is genuinely proven rather than mocked. The
no-artifact cases assert the unchanged markWorkspaceStuck details payload at each
site, which is what makes the artifact cases a real differential.

Two fixes to fallout from 2efeea3 in the same commit:

- verdict-restore.ts bound emitActivityEntryOnce at import time, so importing it
  forced every transitive importer's test to mock an export only the blocked path
  uses. Nine suites across cloister/ and tests/lib/ died at module load. Calling
  through instead of binding removes the import-time coupling; no test file needed
  patching once the root cause was gone.
- The HeadAnchor write-site inventory listed the artifact stamp under
  deacon-review-status.ts, where it no longer lives. Re-registered at its real
  home in verdict-restore.ts, and restoreWouldTripHeadGuard's row parameter is
  renamed lastVerifiedCommit -> rowHead so a READ stops registering as a write in
  an inventory whose whole job is to be truthful about writes.

deacon-review-status.ts lands at 1001 lines against an origin/main baseline of
978, so scripts/file-size-allowlist.txt carries a matching PAN-3511 row.

Two inventory violations remain and are NOT from this branch: legacy-routes.ts
and review-verdict-writer.ts are PAN-3512 sites that origin/main has already
registered. This branch is simply behind main; a sync clears them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C2nHLZQDncBvcnH7VicyLC
…g orbit (PAN-3511)

The stall sweeper's stuck-flag orbit re-drove work agents and re-dispatched
reviews without ever asking whether a verdict already existed on disk. The
consult now runs BEFORE every branch — infra-failure re-dispatch, the
feedback-delivery / verification rework re-drive, and the operator resurface.
Ordering is the whole point: consulting later would let a passed artifact still
push a rework message at the agent before the restore short-circuited.

On a fresh passed artifact the sweeper restores, clears the stuck flag, emits
sweep.unparked with action 'verdict-restored', and returns having sent zero
messages. A passed artifact the head guard blocks also stands the sweeper down —
the artifact still proves a review finished, and the restore door already
reported the mismatch. On a blocked/failed artifact the existing re-drive stays,
but the feedback body now carries the reviewer's actual blocker instead of
pointing the agent at a directory to go read. No artifact changes nothing.

23 cases pass (17 existing, unchanged, plus 6 new).

Also fixes an import cycle this branch introduced in af3a9fc, which
npm run lint caught here and which Node's strict ESM would reject at runtime:
review-status.ts reaches up into cloister/feedback-target.ts, and feedback-target
now calls the restore door, so verdict-restore's own review-status import closed
the loop six ways. verdict-restore no longer imports review-status at all — it
declares the two row fields it reads and the update it writes structurally, and
getStatus/setStatus become required deps that each caller lends from its own
import. That is also the honest contract: the door is a decision function over
injected state access, not a store client. madge counts type-only and dynamic
imports alike, so nothing short of removing the edge would have worked.

deacon-review-status.ts grows 1001 -> 1005 for the lent accessors; its PAN-3511
allowlist row moves with it in this commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C2nHLZQDncBvcnH7VicyLC
docs/REVIEW-AGENT-ARCHITECTURE.md gains a "Verdict of record" section carrying
the four clauses, the two doors (readLatestSynthesisVerdict to read,
attemptArtifactVerdictRestore to decide), the one-directional rule that an absent
artifact is never an approval, and a table of the five recovery sites with what
each one did before the consult existed. A review-mode table follows, because the
fleet default is quick self-review and a reader that only understood synthesis.md
would be blind to most production reviews.

The AC-READ-GUARD limitation is stated plainly rather than left implicit:
restoreWouldTripHeadGuard is deliberately stricter than the write door, so a
restore it blocks is surfaced through review.verdict_restore_blocked and never
dropped. Landing those is PAN-3512's write door, which is already shipped — the
plan for this item described that clause as unshipped, which was true when the
plan was written and is not true now, so it is documented as PAN-3512's scope
rather than as future work.

docs/PARKED-POPULATION.md records the stuck-flag orbit's consult-first ordering.

roles/review.md's SELF-REVIEW banner now carries a staleness warning. This is
verified, not assumed: resolveReviewMode() defaults to quick, but the operator's
resolved config sets roles.review.mode: full, so convoys really do run while the
banner tells the reviewer they are disabled. The note tells the reviewer to trust
the spawn prompt over the banner and flags reconciling mode policy as a separate
follow-up this issue deliberately does not take on.

Prompt-Change: flag the stale SELF-REVIEW banner; trust the spawn prompt over it

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C2nHLZQDncBvcnH7VicyLC
…ranch (PAN-3511)

Both failures predate this branch — the frontend tree here is byte-identical to
origin/main — but they blocked PAN-3511's verification gate, so they are fixed
rather than waited out.

system-health-ui-no-loss.test.tsx: three assertions failed with "Found multiple
elements", not "Unable to find". Every audited affordance is present; the queries
just could not survive normal DOM nesting. The summary line renders into a leaf
div inside a bordered wrapper whose only child it is, so both nodes match even
when the regex is anchored, and the consumer and attention rows legitimately
match several elements each. Switched to getAllBy* asserting presence, which is
the property the no-loss audit is actually about — getBy* was failing on the
section being MORE populated, never less. The audit is not weakened: each
assertion still requires the named content to be visible in the dialog.

tiered-crews.test.ts: blendedCost expected 6.525, which was the weighted blend
while gpt-5.6-terra was priced 8.75. PAN-3388 (e5da1b5) repriced it to 7 for
the 272K billing tier and did not update this literal. Recomputed against the
catalog — haiku 1x10 + sonnet 6x40 + terra 7x30 + gemini 7x20 = 600 over weight
100 — so the expectation is 6. The catalog is the source of truth for price and
this test asserts the blending math, so the literal tracks it; the reasoning is
recorded inline so the next repricing is a one-line update, not an excavation.

Frontend suite is now fully green: 306 files, 2993 tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C2nHLZQDncBvcnH7VicyLC
@eltmon
eltmon force-pushed the feature/pan-3511 branch from 3eec512 to 71c3951 Compare August 4, 2026 02:17
@eltmon

eltmon commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

Review CHANGES REQUESTED for PAN-3511

Review Synthesis — PAN-3511 — 2026-08-04T02:44:36Z

Verdict: CHANGES REQUESTED — workspace-writable verdict artifacts can forge review approval

Context

  • Manifest: /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-71c39512/context.json
  • Branch: feature/pan-3511
  • Workspace: /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511
  • HEAD reviewed: 71c3951
  • Cycle number: 1
  • Prior cycle SHA: none

Convoy Status

Sub-role Signal Output Blocking findings
security ready /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-71c39512/security.md 1
correctness ready /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-71c39512/correctness.md 3
performance ready /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-71c39512/performance.md 2
requirements ready /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-71c39512/requirements.md 3

Blocking Findings

[security] Workspace-writable verdict artifacts can forge review approval — src/lib/cloister/verdict-restore.ts:170

The new restore door treats the workspace-local artifact returned by readLatestSynthesisVerdict() as authoritative and writes it directly to reviewStatus, including passed. Work and verification processes can write the same workspace, so they can create a newest .pan/review/<run>/review.md containing ## Verdict: APPROVED and omit optional context.json head evidence. Recovery then persists an approval without a review-role producer. Bind accepted artifacts to host-owned review-run provenance that implementation code cannot forge, such as a server-recorded run identity plus signed/nonce-bearing context or a host-owned artifact location; filename, mtime, and optional head evidence are not authorization.

[correctness, requirements] Memoized verdicts outlive the artifact freshness boundary — src/lib/cloister/synthesis-verdict.ts:118

The memo expires entries only from checkedAt, so an artifact read at 29:59 remains eligible until roughly 30:59 even though the underlying reader rejects it after 30 minutes. The stale-journal reader and sweeper can consequently use evidence from an older review cycle. Expire non-null entries at the earlier of the 60-second memo TTL and mtimeMs + SYNTHESIS_ARTIFACT_FRESH_MS, preserve the null-entry TTL, and add a boundary regression test.

[correctness, requirements] The stale-journal exception bypasses the artifact head guard — src/lib/review-status-read.ts:52

The refusal is lifted solely when the artifact verdict equals the journal verdict; differing non-empty artifact and live-row heads are ignored. That lets a terminal verdict from an older head overwrite the active review cycle, violating the explicit dispatch-not-drop constraint that a guard-rejected verdict is surfaced but not landed in PAN-3511. Apply the shared head-mismatch predicate before reconciliation and retain the existing refusal on mismatch, with a focused regression test.

[correctness, performance] The sweeper decides and restores from different artifact snapshots — src/lib/cloister/stall-sweeper.ts:377

The sweeper branches on one memoized artifact and then asks the restore helper to read the filesystem again. If the first artifact is passed and the second is blocked, the helper writes blocked, but the sweeper clears the stuck gate and emits that a passed verdict was restored. Pass the selected immutable artifact through the restore operation, or validate and use restore.artifact for the branch, event, and feedback; add a two-snapshot regression test.

[performance] The artifact memo retains every issue key for the process lifetime — src/lib/cloister/synthesis-verdict.ts:118

TTL expiry refreshes values but never evicts keys, so the dashboard and deacon retain every distinct issue that reaches these paths, including null results and completed issues. Use a bounded TTL/LRU cache or enforce capacity plus expired-entry deletion, and test eviction across more issue IDs than the capacity.

[performance] Deacon recovery paths synchronously rescan review directories — src/lib/cloister/verdict-restore.ts:148

The restore helper defaults to the synchronous non-memoized reader, orphan recovery explicitly installs it, and the sweeper performs a memoized read followed by a second synchronous scan. These scans run per issue on the roughly 60-second deacon patrol and can serially delay later recovery and dispatch phases. Default the helper to the memoized reader, remove the orphan override, and reuse the already-selected artifact snapshot in the sweeper without moving filesystem work onto the common review-status read path.

[requirements] Parked-population documentation omits the head-guard exception — docs/PARKED-POPULATION.md:47

The document says every fresh passed artifact restores the verdict and clears the stuck flag, while the implementation deliberately leaves the row parked when non-empty heads disagree. Document that restoration and clearing require a permitted/matching head, and that a mismatch remains parked, emits blocked-restore evidence, and does not re-drive the work agent.

Non-blocking Findings

None after deduplication; the requirements review's snapshot-consistency advisory is promoted by the correctness and performance evidence above.

Clean Sub-roles

None; every sub-role reported at least one blocking finding.

Source: /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-71c39512/synthesis.md

Required action

Fix every blocking review finding, commit the fixes, then re-request review with:

pan review request PAN-3511 -m "Fixed review issues"

1 similar comment
@eltmon

eltmon commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

Review CHANGES REQUESTED for PAN-3511

Review Synthesis — PAN-3511 — 2026-08-04T02:44:36Z

Verdict: CHANGES REQUESTED — workspace-writable verdict artifacts can forge review approval

Context

  • Manifest: /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-71c39512/context.json
  • Branch: feature/pan-3511
  • Workspace: /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511
  • HEAD reviewed: 71c3951
  • Cycle number: 1
  • Prior cycle SHA: none

Convoy Status

Sub-role Signal Output Blocking findings
security ready /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-71c39512/security.md 1
correctness ready /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-71c39512/correctness.md 3
performance ready /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-71c39512/performance.md 2
requirements ready /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-71c39512/requirements.md 3

Blocking Findings

[security] Workspace-writable verdict artifacts can forge review approval — src/lib/cloister/verdict-restore.ts:170

The new restore door treats the workspace-local artifact returned by readLatestSynthesisVerdict() as authoritative and writes it directly to reviewStatus, including passed. Work and verification processes can write the same workspace, so they can create a newest .pan/review/<run>/review.md containing ## Verdict: APPROVED and omit optional context.json head evidence. Recovery then persists an approval without a review-role producer. Bind accepted artifacts to host-owned review-run provenance that implementation code cannot forge, such as a server-recorded run identity plus signed/nonce-bearing context or a host-owned artifact location; filename, mtime, and optional head evidence are not authorization.

[correctness, requirements] Memoized verdicts outlive the artifact freshness boundary — src/lib/cloister/synthesis-verdict.ts:118

The memo expires entries only from checkedAt, so an artifact read at 29:59 remains eligible until roughly 30:59 even though the underlying reader rejects it after 30 minutes. The stale-journal reader and sweeper can consequently use evidence from an older review cycle. Expire non-null entries at the earlier of the 60-second memo TTL and mtimeMs + SYNTHESIS_ARTIFACT_FRESH_MS, preserve the null-entry TTL, and add a boundary regression test.

[correctness, requirements] The stale-journal exception bypasses the artifact head guard — src/lib/review-status-read.ts:52

The refusal is lifted solely when the artifact verdict equals the journal verdict; differing non-empty artifact and live-row heads are ignored. That lets a terminal verdict from an older head overwrite the active review cycle, violating the explicit dispatch-not-drop constraint that a guard-rejected verdict is surfaced but not landed in PAN-3511. Apply the shared head-mismatch predicate before reconciliation and retain the existing refusal on mismatch, with a focused regression test.

[correctness, performance] The sweeper decides and restores from different artifact snapshots — src/lib/cloister/stall-sweeper.ts:377

The sweeper branches on one memoized artifact and then asks the restore helper to read the filesystem again. If the first artifact is passed and the second is blocked, the helper writes blocked, but the sweeper clears the stuck gate and emits that a passed verdict was restored. Pass the selected immutable artifact through the restore operation, or validate and use restore.artifact for the branch, event, and feedback; add a two-snapshot regression test.

[performance] The artifact memo retains every issue key for the process lifetime — src/lib/cloister/synthesis-verdict.ts:118

TTL expiry refreshes values but never evicts keys, so the dashboard and deacon retain every distinct issue that reaches these paths, including null results and completed issues. Use a bounded TTL/LRU cache or enforce capacity plus expired-entry deletion, and test eviction across more issue IDs than the capacity.

[performance] Deacon recovery paths synchronously rescan review directories — src/lib/cloister/verdict-restore.ts:148

The restore helper defaults to the synchronous non-memoized reader, orphan recovery explicitly installs it, and the sweeper performs a memoized read followed by a second synchronous scan. These scans run per issue on the roughly 60-second deacon patrol and can serially delay later recovery and dispatch phases. Default the helper to the memoized reader, remove the orphan override, and reuse the already-selected artifact snapshot in the sweeper without moving filesystem work onto the common review-status read path.

[requirements] Parked-population documentation omits the head-guard exception — docs/PARKED-POPULATION.md:47

The document says every fresh passed artifact restores the verdict and clears the stuck flag, while the implementation deliberately leaves the row parked when non-empty heads disagree. Document that restoration and clearing require a permitted/matching head, and that a mismatch remains parked, emits blocked-restore evidence, and does not re-drive the work agent.

Non-blocking Findings

None after deduplication; the requirements review's snapshot-consistency advisory is promoted by the correctness and performance evidence above.

Clean Sub-roles

None; every sub-role reported at least one blocking finding.

Source: /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-71c39512/synthesis.md

Required action

Fix every blocking review finding, commit the fixes, then re-request review with:

pan review request PAN-3511 -m "Fixed review issues"

panopticon-agent[bot] and others added 2 commits August 4, 2026 09:23

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/lib/cloister/__tests__/synthesis-artifact-verdict.test.ts (1)

83-108: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add cases for the two remaining rejection branches.

The suite covers the forged capability, the mismatched context, and the freshness boundary. Two new rejection branches in resolveTrustedReviewRun and readRunContext have no test:

  • A runId that fails basename(runId) !== runId or the agent-<issue>-review prefix check.
  • A run directory with a report but no context.json.

Both branches guard artifact authorization, so a regression in either would silently widen what recovery accepts. Also consider a case where the marker appears on the second line, which pins the first-line-only comparison.

💚 Proposed additional cases
   it('returns null for a run without a verdict line', () => {
     const runId = 'agent-pan-1-review-empty';
     runDir(runId, '# no verdict here\n');
     expect(read(runId)).toBeNull();
   });
+
+  it('rejects a runId that escapes the review directory or misses the run prefix', () => {
+    expect(read('../../../etc')).toBeNull();
+    expect(read('agent-pan-2-review-other')).toBeNull();
+  });
+
+  it('rejects a run directory with no context.json', () => {
+    const runId = 'agent-pan-1-review-nocontext';
+    runDir(runId, '## Verdict: APPROVED\n');
+    rmSync(join(root, '.pan', 'review', runId, 'context.json'));
+    expect(read(runId)).toBeNull();
+  });
+
+  it('rejects a marker that is not the first line', () => {
+    const runId = 'agent-pan-1-review-latemarker';
+    const dir = join(root, '.pan', 'review', runId);
+    mkdirSync(dir, { recursive: true });
+    writeFileSync(
+      join(dir, 'synthesis.md'),
+      `# heading\n${reviewArtifactCapabilityMarker(CAPABILITY)}\n## Verdict: APPROVED\n`,
+    );
+    writeFileSync(join(dir, 'context.json'), JSON.stringify({ issueId: ISSUE, runId }));
+    expect(read(runId)).toBeNull();
+  });
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/cloister/__tests__/synthesis-artifact-verdict.test.ts` around lines
83 - 108, Add three test cases to cover untested rejection branches: add a test
for a runId that fails the basename or agent-<issue>-review prefix validation in
resolveTrustedReviewRun, add a test for a run directory with a report file but
no context.json file in readRunContext, and add a test where the verdict marker
appears on the second line instead of the first line. Each case should verify
that read returns null, consistent with the rejection behavior for the other
branches already tested above these additions.
src/lib/cloister/synthesis-verdict.ts (1)

67-84: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Make artifact provenance resolution fail safe on the review-status hot path.

resolveJournalReconciledReviewStatusSync calls readMemoizedArtifactVerdict(issueId) without a try. That calls resolveTrustedReviewRun, which invokes both getReviewArtifactProvenanceSync and resolveProjectFromIssueSync before the try in readLatestSynthesisVerdict. Make readMemoizedArtifactVerdict return null when provenance resolution fails, or wrap the artifact consult with an equivalent boundary catch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/cloister/synthesis-verdict.ts` around lines 67 - 84, Make the
artifact lookup fail safe by catching errors from resolveTrustedReviewRun within
readMemoizedArtifactVerdict and returning null when provenance resolution fails.
Ensure resolveJournalReconciledReviewStatusSync cannot propagate these errors
before readLatestSynthesisVerdict’s existing try boundary.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/overdeck/infra.ts`:
- Around line 196-197: Update the v54→v55 migration so the agents table includes
review_artifact_capability before backfillAgentsFromStateJsonSync() prepares its
query. Make this part of the v55 table definition or perform an idempotent ALTER
TABLE before the backfill, while retaining the existing runSchemaTopUp call only
as a repair path rather than the primary migration.

---

Nitpick comments:
In `@src/lib/cloister/__tests__/synthesis-artifact-verdict.test.ts`:
- Around line 83-108: Add three test cases to cover untested rejection branches:
add a test for a runId that fails the basename or agent-<issue>-review prefix
validation in resolveTrustedReviewRun, add a test for a run directory with a
report file but no context.json file in readRunContext, and add a test where the
verdict marker appears on the second line instead of the first line. Each case
should verify that read returns null, consistent with the rejection behavior for
the other branches already tested above these additions.

In `@src/lib/cloister/synthesis-verdict.ts`:
- Around line 67-84: Make the artifact lookup fail safe by catching errors from
resolveTrustedReviewRun within readMemoizedArtifactVerdict and returning null
when provenance resolution fails. Ensure
resolveJournalReconciledReviewStatusSync cannot propagate these errors before
readLatestSynthesisVerdict’s existing try boundary.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d3e54f53-afc8-42f4-b83d-a5c15418fe54

📥 Commits

Reviewing files that changed from the base of the PR and between 3eec512 and ec9f518.

📒 Files selected for processing (33)
  • docs/PARKED-POPULATION.md
  • docs/REVIEW-AGENT-ARCHITECTURE.md
  • drizzle/overdeck/0000_overdeck_init.sql
  • packages/contracts/src/events.ts
  • roles/review.md
  • scripts/file-size-allowlist.txt
  • src/lib/__tests__/review-status-read-artifact.test.ts
  • src/lib/agents/agent-state.ts
  • src/lib/cloister/__tests__/feedback-target.test.ts
  • src/lib/cloister/__tests__/stall-sweeper.test.ts
  • src/lib/cloister/__tests__/synthesis-artifact-verdict.test.ts
  • src/lib/cloister/__tests__/verdict-restore-breaker.test.ts
  • src/lib/cloister/__tests__/verdict-restore.test.ts
  • src/lib/cloister/deacon-review-signals.ts
  • src/lib/cloister/deacon-review-status.ts
  • src/lib/cloister/feedback-target.ts
  • src/lib/cloister/review-agent.ts
  • src/lib/cloister/review-artifact-capability.ts
  • src/lib/cloister/stall-sweeper.ts
  • src/lib/cloister/synthesis-verdict.ts
  • src/lib/cloister/verdict-head-guard.ts
  • src/lib/cloister/verdict-restore.ts
  • src/lib/database/agent-backfill.ts
  • src/lib/database/agent-mappers.ts
  • src/lib/database/agents-db.ts
  • src/lib/database/schema.ts
  • src/lib/overdeck/agent-review-provenance.ts
  • src/lib/overdeck/agent-state-sync.ts
  • src/lib/overdeck/agents.ts
  • src/lib/overdeck/infra.ts
  • src/lib/review-status-read.ts
  • tests/unit/lib/head-anchor-write-sites.test.ts
  • tests/unit/lib/overdeck/agent-discovery-columns.test.ts
🚧 Files skipped from review as they are similar to previous changes (14)
  • scripts/file-size-allowlist.txt
  • tests/unit/lib/head-anchor-write-sites.test.ts
  • src/lib/cloister/tests/verdict-restore-breaker.test.ts
  • packages/contracts/src/events.ts
  • src/lib/cloister/feedback-target.ts
  • src/lib/cloister/tests/feedback-target.test.ts
  • src/lib/cloister/tests/stall-sweeper.test.ts
  • src/lib/cloister/tests/verdict-restore.test.ts
  • docs/PARKED-POPULATION.md
  • src/lib/cloister/stall-sweeper.ts
  • roles/review.md
  • src/lib/review-status-read.ts
  • src/lib/tests/review-status-read-artifact.test.ts
  • docs/REVIEW-AGENT-ARCHITECTURE.md

Comment thread src/lib/overdeck/infra.ts Outdated
Comment on lines +196 to +197
// PAN-3511: host-issued capability binds verdict recovery to the active review run.
runSchemaTopUp(db, 'ALTER TABLE `agents` ADD COLUMN `review_artifact_capability` text');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Add the capability column before the v55 backfill runs.

The v54→v55 migration creates agents without review_artifact_capability. It then calls backfillAgentsFromStateJsonSync(). The changed backfill query includes this column, so statement preparation fails before this runtime top-up executes. The migration catches the error and does not retry the backfill. Existing agent state can therefore be missing after upgrade.

Add the column to the v55 table definition or run an idempotent ALTER TABLE before the backfill. Keep this top-up as a repair path, not as the primary migration. As per coding guidelines, fix broken behavior at its root cause; do not add a downstream workaround that masks the symptom.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/overdeck/infra.ts` around lines 196 - 197, Update the v54→v55
migration so the agents table includes review_artifact_capability before
backfillAgentsFromStateJsonSync() prepares its query. Make this part of the v55
table definition or perform an idempotent ALTER TABLE before the backfill, while
retaining the existing runSchemaTopUp call only as a repair path rather than the
primary migration.

Source: Coding guidelines

@eltmon

eltmon commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

Review CHANGES REQUESTED for PAN-3511

Review Synthesis — PAN-3511 — 2026-08-04T15:22:00Z

Verdict: CHANGES REQUESTED — the persisted capability remains forgeable by the agents it is meant to exclude

Context

  • Manifest: /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-ec9f5180/context.json
  • Branch: feature/pan-3511
  • Workspace: /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511
  • HEAD reviewed: ec9f518
  • Cycle number: 2
  • Prior cycle SHA: 71c3951

Convoy Status

Sub-role Signal Output Blocking findings
security ready /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-ec9f5180/security.md 1
correctness ready /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-ec9f5180/correctness.md 3
performance ready /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-ec9f5180/performance.md 0
requirements ready /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-ec9f5180/requirements.md 0

Blocking Findings

[security] The review capability is readable by the agents it is supposed to exclude — src/lib/cloister/review-agent.ts:696

The attempted fix for the prior forged-artifact blocker stores the bearer capability in the review agent's ordinary AgentState, then projects it into shared SQLite and rollback state.json. Work and test agents run under the same Unix identity and can read those stores, so a malicious or prompt-injected agent can recover the active run ID and capability, write a matching context.json plus an approved artifact, omit optional head evidence, and satisfy every new recovery check. The authorization must be host-only attestation over the verdict artifact and reviewed head, with signing material unavailable to coding-agent processes; a bearer token persisted in shared agent state does not establish producer authenticity.

[correctness] Existing panopticon.db databases never receive the capability column — src/lib/database/agent-backfill.ts:78

The changed backfill and all agent reads/upserts now reference review_artifact_capability, but the migration ladder only adds it to the fresh initSchema definition. The v54→v55 agents table definition omits the column and immediately runs the changed backfill, so upgrades fail with no such column, swallow that backfill error, and still advance the schema version; already-current databases also receive no repair migration. Add the column idempotently before the v55 backfill and add a current-version migration/repair path so existing databases retain a usable agent registry.

[correctness] A review artifact suppresses unrelated feedback-delivery gates — src/lib/cloister/feedback-target.ts:297

surfaceIssueFeedbackNeedsYou is shared by review, test, and verification failure delivery, but it now returns for any restored or head-blocked review artifact. A prior passed review does not prove that later test or verification feedback reached the work agent, so the normal review-passed → test-failed sequence can lose the feedback_delivery_needs_you operator gate and park silently. Restore the review verdict as a separate fact, but preserve the delivery-failure stuck condition unless delivery itself has been resolved.

[correctness] Polyrepo verdict artifacts always trip the head guard — src/lib/cloister/synthesis-verdict.ts:98

The reader exposes the review manifest's top-level headSha, which is only the primary changed sub-repository SHA in a polyrepo, then compares it with lastVerifiedCommit, whose canonical format is the ordered composite repoKey@sha .... Those formats never match even when every repository is unchanged, so polyrepo orphan restore, sweeper recovery, and stale-journal reconciliation reject every valid artifact. Derive the same canonical composite anchor from context.repos for polyrepos and add fixtures proving unchanged composites restore while a moved sub-repository blocks.

Non-blocking Findings

[correctness] A stale synthesis file can mask the active quick-review artifact — src/lib/cloister/synthesis-verdict.ts:126

findVerdictReport prefers synthesis.md; when a same-HEAD run rotates capability and changes from convoy to quick mode, an old synthesis file fails the marker check before the valid current review.md is considered. Examine both supported files and select the fresh report carrying the active capability.

[correctness] Provenance lookup failures escape the fail-safe artifact boundary — src/lib/cloister/synthesis-verdict.ts:195

readMemoizedArtifactVerdict resolves DB/project provenance before any catch, so optional recovery evidence can make the canonical review-status read throw during schema drift or configuration failures. Enclose trusted-run resolution and the memoized read in the same fail-safe boundary that returns null for other unreadable evidence.

[performance] The memo TTL does not suppress persistent reads across patrol cycles — src/lib/cloister/synthesis-verdict.ts:161

The 60-second cache TTL approximately matches the deacon patrol cadence, so a long-lived head mismatch usually re-reads one small artifact each minute. The cost is bounded and does not block this PR; non-null evidence could remain cached until its 30-minute freshness boundary while null results keep the short TTL.

Clean Sub-roles

  • requirements

Source: /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-ec9f5180/synthesis.md

Required action

Fix every blocking review finding, commit the fixes, then re-request review with:

pan review request PAN-3511 -m "Fixed review issues"

panopticon-agent[bot] and others added 2 commits August 4, 2026 12:51

@coderabbitai coderabbitai 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.

Actionable comments posted: 6

🧹 Nitpick comments (11)
src/lib/cloister/__tests__/feedback-target.test.ts (1)

261-272: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the absence of a redundant review write.

The test name claims the delivery gate survives when review already passed. The body only checks markWorkspaceStuck. Add an assertion that setReviewStatusSync was not called, so the test actually proves that an already-passed row is not rewritten from the artifact.

💚 Proposed addition
     expect(reviewStatus.markWorkspaceStuck).toHaveBeenCalledWith(
       ISSUE,
       'feedback_delivery_needs_you',
       { reason: 'test feedback could not be delivered', phase: 'test' },
     );
+    expect(reviewStatus.setReviewStatusSync).not.toHaveBeenCalled();
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/cloister/__tests__/feedback-target.test.ts` around lines 261 - 272,
Extend the test case “preserves a test-feedback delivery gate after review
already passed” to assert that reviewStatus.setReviewStatusSync is not called,
while retaining the existing markWorkspaceStuck expectation.
src/lib/cloister/synthesis-verdict.ts (2)

88-91: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Log the swallowed failure before returning null.

This catch covers resolveTrustedReviewRun, which performs a SQLite read and a project resolution. A database or configuration failure is indistinguishable from "no fresh artifact exists". Recovery then stays blocked with no signal. Emit a warning with the issue id and the error, and keep returning null.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/cloister/synthesis-verdict.ts` around lines 88 - 91, Update the catch
handling around resolveTrustedReviewRun to capture the thrown error and emit a
warning containing the issue id and error details before returning null.
Preserve the existing null return behavior and do not alter successful
resolution.

16-24: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Rename mtimeMs or correct its documentation.

The field now carries Date.parse(valid.issuedAt) from the attestation, not the file mtime. readAttestedReviewReports sets it that way in src/lib/cloister/review-artifact-attestation.ts Line 234. The comment at Line 22 still says "mtime (ms) of the verdict artifact". A caller that compares this value against a real file mtime gets a wrong result. Rename the field to attestedAtMs, or update the comment to state that it is the attestation issue time.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/cloister/synthesis-verdict.ts` around lines 16 - 24, Rename
SynthesisArtifactVerdict.mtimeMs to attestedAtMs and update all references,
including readAttestedReviewReports, to reflect that the value is
Date.parse(valid.issuedAt) from the attestation rather than the artifact file
mtime.
src/cli/commands/specialists/done.ts (1)

176-177: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add braces to the guarded try.

if (!workspaceHead) try { ... } catch { ... } is valid, but the brace-less if makes the guarded region hard to see and easy to break during a later edit. Wrap the try in a block.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/commands/specialists/done.ts` around lines 176 - 177, Wrap the
try/catch guarded by if (!workspaceHead) in braces in the workspaceHead
initialization flow, preserving the existing try/catch behavior and all
statements within its guarded region.
src/lib/cloister/__tests__/review-artifact-attestation.test.ts (1)

21-28: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Restore the attestation key environment variable after the suite.

installTestReviewAttestationKey writes OVERDECK_REVIEW_ATTESTATION_KEY into process.env and nothing removes it. The value then persists for other suites in the same Vitest worker. Use vi.stubEnv with vi.unstubAllEnvs, or delete the variable in afterEach.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/cloister/__tests__/review-artifact-attestation.test.ts` around lines
21 - 28, Update the setup and teardown around installTestReviewAttestationKey in
the review attestation test suite to restore OVERDECK_REVIEW_ATTESTATION_KEY
after each test. Prefer vi.stubEnv with vi.unstubAllEnvs, or explicitly delete
the environment variable in afterEach while preserving the existing temporary
workspace cleanup.
tests/cli/commands/specialists/done.test.ts (1)

137-177: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add negative coverage for a mismatched identity and a failed attestation.

The new tests cover a missing token and the success path. Two rejection branches in done.ts Lines 80-98 stay untested: a callerAgentId that does not equal agent-<issue>-review, and a non-ok attestation response. Both must throw before any review status write. Add a case for each.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/cli/commands/specialists/done.test.ts` around lines 137 - 177, Add two
negative tests alongside the existing attestation tests for doneCommand: one
with a callerAgentId that does not match the expected agent-<issue>-review
identity, and one where the attestation fetch returns a non-OK response. Assert
each rejects and verify mockSetReviewStatus is not called in both cases.
src/lib/cloister/__tests__/review-artifact-test-helpers.ts (1)

32-37: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Do not write headSha when repos is supplied.

The fixture always writes headSha, and adds repos on top. canonicalReviewedHeadFromContext prefers repos, so the headSha value is silently ignored. A polyrepo fixture then carries a monorepo anchor that no production context would contain. Omit headSha when repos is present, so the fixture matches the real manifest shape.

♻️ Proposed refactor
   writeFileSync(contextPath, JSON.stringify({
     issueId: options.issueId,
     runId: options.runId,
-    headSha: options.headSha ?? TEST_REVIEW_HEAD,
-    ...(options.repos ? { repos: options.repos } : {}),
+    ...(options.repos
+      ? { repos: options.repos }
+      : { headSha: options.headSha ?? TEST_REVIEW_HEAD }),
   }));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/cloister/__tests__/review-artifact-test-helpers.ts` around lines 32 -
37, Update the fixture construction in the writeFileSync call so headSha is
included only when options.repos is absent, while preserving the existing
default TEST_REVIEW_HEAD for monorepo fixtures and the repos field for polyrepo
fixtures.
src/lib/cloister/review-artifact-attestation.ts (1)

164-186: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Record why a candidate report was skipped.

The catch block at Line 180 discards every failure, including a corrupt read or a statSync error. When attestReviewReport then throws no ${expectedVerdict} review report exists, the operator has no evidence about the real cause. Log the filename and the error at debug level before continuing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/cloister/review-artifact-attestation.ts` around lines 164 - 186,
Update the catch block in the candidate-report loop to log a debug-level message
containing the skipped filename and caught error before continuing to the next
report. Preserve the existing fallback behavior and final no-report error in the
surrounding review report selection flow.
src/lib/review-attestation-key.ts (1)

57-68: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse verifyReviewAttestationSignature for token verification.

verifyReviewAgentAttestationToken repeats the length check and timingSafeEqual compare from verifyReviewAttestationSignature. Delegate instead, so one compare implementation stays authoritative.

♻️ Proposed refactor
 export function verifyReviewAgentAttestationToken(
   agentId: string,
   runId: string,
   token: string,
   env: NodeJS.ProcessEnv = process.env,
 ): boolean {
-  const expected = createReviewAgentAttestationToken(agentId, runId, env);
-  if (!expected) return false;
-  const expectedBytes = Buffer.from(expected);
-  const actualBytes = Buffer.from(token);
-  return expectedBytes.length === actualBytes.length && timingSafeEqual(expectedBytes, actualBytes);
+  return verifyReviewAttestationSignature(reviewAgentTokenPayload(agentId, runId), token, env);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/review-attestation-key.ts` around lines 57 - 68, Update
verifyReviewAgentAttestationToken to delegate token comparison to
verifyReviewAttestationSignature after generating the expected token, preserving
the existing false result when token creation fails. Remove the duplicated
length check and timingSafeEqual logic so verifyReviewAttestationSignature
remains the single authoritative comparison implementation.
src/dashboard/server/main.ts (1)

117-119: 🩺 Stability & Availability | 🔵 Trivial

Note the restart behavior of the in-memory key.

ensureReviewAttestationKey generates a new key on every dashboard start unless the environment already supplies one. After a restart, every attestation signed by the previous process fails verification, so in-flight review artifacts stop being recovery authority. The 30 minute freshness window limits the exposure, but a restart during an active review cycle discards otherwise valid evidence silently.

Consider logging at startup when a key is generated rather than inherited, so an operator can correlate a restart with lost artifact authority.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/dashboard/server/main.ts` around lines 117 - 119, Update the startup flow
around ensureReviewAttestationKey to log when the signing key is generated
locally rather than inherited from the environment. Preserve the existing key
selection behavior, and emit the startup message only for newly generated
in-memory keys so operators can correlate restarts with invalidated prior
artifacts.
src/lib/cloister/__tests__/verdict-restore.test.ts (1)

370-379: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move the duplicated writeArtifact wrapper into the shared helper.

The same wrapper body exists in src/lib/cloister/__tests__/verdict-restore-breaker.test.ts, src/lib/cloister/__tests__/synthesis-artifact-verdict.test.ts, and src/lib/cloister/__tests__/feedback-target.test.ts. Export a factory from review-artifact-test-helpers.ts that binds workspacePath, issueId, and runId once, then call it from each suite.

♻️ Proposed helper addition
// src/lib/cloister/__tests__/review-artifact-test-helpers.ts
export function attestedArtifactWriter(binding: {
  workspacePath: () => string;
  issueId: string;
  runId: string;
}) {
  return (filename: VerdictReportFilename, body: string, headSha?: string): string =>
    writeAttestedReviewArtifact({
      workspacePath: binding.workspacePath(),
      issueId: binding.issueId,
      runId: binding.runId,
      filename,
      body,
      ...(headSha ? { headSha } : {}),
    });
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/cloister/__tests__/verdict-restore.test.ts` around lines 370 - 379,
Move the duplicated writeArtifact wrapper into review-artifact-test-helpers.ts
by exporting an attestedArtifactWriter factory that binds workspacePath,
issueId, and runId and preserves optional headSha handling and the writer’s
return value. Update writeArtifact usage in verdict-restore.test.ts,
verdict-restore-breaker.test.ts, synthesis-artifact-verdict.test.ts, and
feedback-target.test.ts to use the shared factory.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/cli/commands/specialists/done.ts`:
- Around line 99-101: Validate attested.reviewedHead against the expected
HeadAnchor shape before passing it to rehydrateHeadAnchor in the done command.
Reject malformed values, not merely missing or empty strings, and only assign
the validated anchor to attestedEvidenceHead so reviewedAtCommit and subsequent
comparisons cannot receive an unchecked value.
- Around line 84-98: The attestation fetch in
src/cli/commands/specialists/done.ts lines 84-98 needs a wall-clock timeout: add
an AbortSignal.timeout(...) signal to the request options. Update the exact
request assertion in tests/cli/commands/specialists/done.test.ts lines 160-171
to include the signal property, or use expect.objectContaining while preserving
the existing assertions.

In `@src/lib/cloister/review-agent.ts`:
- Around line 591-596: The context manifest failure catch block currently clears
the path and continues dispatching agents. In the review-agent flow around
attestReviewContextManifest and the existing outer failure handler, rethrow or
otherwise propagate ctxErr after logging so launch aborts before agent spawning;
preserve successful manifest handling and add a regression test covering
attestation failure.
- Around line 578-579: Update the run-ID initialization around runNonce/runId
and the later reviewRunId assignment to capture the previously persisted ID
before mutation, then detect and reuse it when recovering an active review cycle
instead of generating a new nonce-based ID. Preserve new nonce generation for
genuine re-reviews so the full-review comparison can select
spawnConvoyReviewers() only for a new cycle, and add coverage for both recovery
and re-review paths.

In `@src/lib/cloister/review-artifact-attestation.ts`:
- Around line 99-101: Update the SHA validation in the entry-processing logic
and the single-repository headSha validation to accept only exactly 40 or 64
hexadecimal characters, rejecting lengths from 41 through 63 while preserving
the existing canonical anchor behavior.

In `@tests/unit/dashboard/server/routes/specialists-review-attestation.test.ts`:
- Around line 65-78: Update the test setup around beforeEach and afterEach to
use vi.stubEnv for REVIEW_ATTESTATION_KEY_ENV instead of directly mutating
process.env, and call vi.unstubAllEnvs() during teardown so any pre-existing
OVERDECK_REVIEW_ATTESTATION_KEY value is restored.

---

Nitpick comments:
In `@src/cli/commands/specialists/done.ts`:
- Around line 176-177: Wrap the try/catch guarded by if (!workspaceHead) in
braces in the workspaceHead initialization flow, preserving the existing
try/catch behavior and all statements within its guarded region.

In `@src/dashboard/server/main.ts`:
- Around line 117-119: Update the startup flow around ensureReviewAttestationKey
to log when the signing key is generated locally rather than inherited from the
environment. Preserve the existing key selection behavior, and emit the startup
message only for newly generated in-memory keys so operators can correlate
restarts with invalidated prior artifacts.

In `@src/lib/cloister/__tests__/feedback-target.test.ts`:
- Around line 261-272: Extend the test case “preserves a test-feedback delivery
gate after review already passed” to assert that
reviewStatus.setReviewStatusSync is not called, while retaining the existing
markWorkspaceStuck expectation.

In `@src/lib/cloister/__tests__/review-artifact-attestation.test.ts`:
- Around line 21-28: Update the setup and teardown around
installTestReviewAttestationKey in the review attestation test suite to restore
OVERDECK_REVIEW_ATTESTATION_KEY after each test. Prefer vi.stubEnv with
vi.unstubAllEnvs, or explicitly delete the environment variable in afterEach
while preserving the existing temporary workspace cleanup.

In `@src/lib/cloister/__tests__/review-artifact-test-helpers.ts`:
- Around line 32-37: Update the fixture construction in the writeFileSync call
so headSha is included only when options.repos is absent, while preserving the
existing default TEST_REVIEW_HEAD for monorepo fixtures and the repos field for
polyrepo fixtures.

In `@src/lib/cloister/__tests__/verdict-restore.test.ts`:
- Around line 370-379: Move the duplicated writeArtifact wrapper into
review-artifact-test-helpers.ts by exporting an attestedArtifactWriter factory
that binds workspacePath, issueId, and runId and preserves optional headSha
handling and the writer’s return value. Update writeArtifact usage in
verdict-restore.test.ts, verdict-restore-breaker.test.ts,
synthesis-artifact-verdict.test.ts, and feedback-target.test.ts to use the
shared factory.

In `@src/lib/cloister/review-artifact-attestation.ts`:
- Around line 164-186: Update the catch block in the candidate-report loop to
log a debug-level message containing the skipped filename and caught error
before continuing to the next report. Preserve the existing fallback behavior
and final no-report error in the surrounding review report selection flow.

In `@src/lib/cloister/synthesis-verdict.ts`:
- Around line 88-91: Update the catch handling around resolveTrustedReviewRun to
capture the thrown error and emit a warning containing the issue id and error
details before returning null. Preserve the existing null return behavior and do
not alter successful resolution.
- Around line 16-24: Rename SynthesisArtifactVerdict.mtimeMs to attestedAtMs and
update all references, including readAttestedReviewReports, to reflect that the
value is Date.parse(valid.issuedAt) from the attestation rather than the
artifact file mtime.

In `@src/lib/review-attestation-key.ts`:
- Around line 57-68: Update verifyReviewAgentAttestationToken to delegate token
comparison to verifyReviewAttestationSignature after generating the expected
token, preserving the existing false result when token creation fails. Remove
the duplicated length check and timingSafeEqual logic so
verifyReviewAttestationSignature remains the single authoritative comparison
implementation.

In `@tests/cli/commands/specialists/done.test.ts`:
- Around line 137-177: Add two negative tests alongside the existing attestation
tests for doneCommand: one with a callerAgentId that does not match the expected
agent-<issue>-review identity, and one where the attestation fetch returns a
non-OK response. Assert each rejects and verify mockSetReviewStatus is not
called in both cases.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4deb48a9-e660-47a2-b47e-7207efb6f8db

📥 Commits

Reviewing files that changed from the base of the PR and between ec9f518 and 0451d9f.

📒 Files selected for processing (35)
  • docs/REVIEW-AGENT-ARCHITECTURE.md
  • src/cli/commands/specialists/done.ts
  • src/dashboard/server/main.ts
  • src/dashboard/server/routes/specialists/legacy-routes.ts
  • src/lib/__tests__/agent-state-role.test.ts
  • src/lib/__tests__/review-status-read-artifact.test.ts
  • src/lib/agents/resume.ts
  • src/lib/agents/spawn-prep.ts
  • src/lib/agents/spawn.ts
  • src/lib/child-env.ts
  • src/lib/cloister/__tests__/deacon-stash-janitor.test.ts
  • src/lib/cloister/__tests__/feedback-target.test.ts
  • src/lib/cloister/__tests__/review-agent.test.ts
  • src/lib/cloister/__tests__/review-artifact-attestation.test.ts
  • src/lib/cloister/__tests__/review-artifact-test-helpers.ts
  • src/lib/cloister/__tests__/synthesis-artifact-verdict.test.ts
  • src/lib/cloister/__tests__/verdict-restore-breaker.test.ts
  • src/lib/cloister/__tests__/verdict-restore.test.ts
  • src/lib/cloister/deacon-review-signals.ts
  • src/lib/cloister/feedback-target.ts
  • src/lib/cloister/review-agent.ts
  • src/lib/cloister/review-artifact-attestation.ts
  • src/lib/cloister/synthesis-verdict.ts
  • src/lib/cloister/verdict-restore.ts
  • src/lib/overdeck/agent-review-provenance.ts
  • src/lib/review-attestation-key.ts
  • tests/cli/commands/specialists/done.test.ts
  • tests/integration/post-review-rebase-scenario.test.ts
  • tests/lib/child-env.test.ts
  • tests/lib/cloister/review-agent.test.ts
  • tests/unit/dashboard/deacon-supervisor.test.ts
  • tests/unit/dashboard/server/routes/specialists-review-attestation.test.ts
  • tests/unit/lib/cloister/guard-advancing-dispatch.test.ts
  • tests/unit/lib/head-anchor-write-sites.test.ts
  • tests/unit/lib/overdeck/agent-discovery-columns.test.ts
💤 Files with no reviewable changes (2)
  • tests/unit/lib/overdeck/agent-discovery-columns.test.ts
  • src/lib/cloister/verdict-restore.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/lib/cloister/tests/verdict-restore-breaker.test.ts
  • tests/unit/lib/head-anchor-write-sites.test.ts
  • docs/REVIEW-AGENT-ARCHITECTURE.md
  • src/lib/tests/review-status-read-artifact.test.ts
  • src/lib/cloister/tests/synthesis-artifact-verdict.test.ts

Comment thread src/cli/commands/specialists/done.ts Outdated
Comment thread src/cli/commands/specialists/done.ts Outdated
Comment thread src/lib/cloister/review-agent.ts Outdated
Comment thread src/lib/cloister/review-agent.ts Outdated
Comment on lines +591 to +596
attestReviewContextManifest(contextManifestPath);
tier1Summary = formatTier1Summary(manifest);
console.log(`[review-agent] Context manifest built: ${contextManifestPath} (${manifest.changedFiles.length} files)`);
console.log(`[review-agent] Context manifest built and attested: ${contextManifestPath} (${manifest.changedFiles.length} files)`);
} catch (ctxErr) {
console.warn(`[review-agent] Context manifest build failed for ${opts.issueId} — reviewers will block on missing shared context:`, ctxErr);
contextManifestPath = undefined;
console.warn(`[review-agent] Context manifest build or attestation failed for ${opts.issueId} — reviewers will block on missing trusted context:`, ctxErr);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Abort dispatch if context attestation fails.

Lines 591-596 clear the error and continue to launch a review. attestReviewReport() rejects a run without a valid signed context manifest, so the Deacon fallback cannot persist trusted terminal evidence for this launch. Propagate the error to the existing outer failure handler before spawning agents. Add a regression test for manifest-attestation failure.

As per coding guidelines, fix broken behavior at its root cause; never add workarounds, hacks, fallback chains, or downstream defensive handling that merely masks symptoms.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/cloister/review-agent.ts` around lines 591 - 596, The context
manifest failure catch block currently clears the path and continues dispatching
agents. In the review-agent flow around attestReviewContextManifest and the
existing outer failure handler, rethrow or otherwise propagate ctxErr after
logging so launch aborts before agent spawning; preserve successful manifest
handling and add a regression test covering attestation failure.

Source: Coding guidelines

Comment on lines +99 to +101
if (typeof repoKey !== 'string' || !repoKey || /[\s@]/.test(repoKey)) return undefined;
if (typeof headSha !== 'string' || !/^[0-9a-f]{40,64}$/i.test(headSha)) return undefined;
entries.push(`${repoKey}@${headSha}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Tighten the SHA validation to real git lengths.

/^[0-9a-f]{40,64}$/i accepts 41 to 63 character values, which are not valid git object IDs. Accept only 40 or 64 characters. This keeps the canonical anchor free of malformed values.

🛡️ Proposed fix
-      if (typeof headSha !== 'string' || !/^[0-9a-f]{40,64}$/i.test(headSha)) return undefined;
+      if (typeof headSha !== 'string' || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(headSha)) return undefined;

Apply the same pattern at Line 105 for the single-repository headSha.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/cloister/review-artifact-attestation.ts` around lines 99 - 101,
Update the SHA validation in the entry-processing logic and the
single-repository headSha validation to accept only exactly 40 or 64 hexadecimal
characters, rejecting lengths from 41 through 63 while preserving the existing
canonical anchor behavior.

Comment on lines +65 to +78
beforeEach(() => {
vi.clearAllMocks();
process.env[REVIEW_ATTESTATION_KEY_ENV] = 'route-test-review-attestation-key-material-123456789';
mocks.getProvenance.mockReturnValue({ workspace: '/workspace', reviewRunId: RUN_ID });
mocks.attestReport.mockReturnValue({
filename: 'synthesis.md',
verdict: 'passed',
reviewedHead: 'a'.repeat(40),
});
});

afterEach(() => {
delete process.env[REVIEW_ATTESTATION_KEY_ENV];
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate file =="
fd -a 'specialists-review-attestation.test.ts' . || true

echo "== File excerpt =="
file="$(fd 'specialists-review-attestation.test.ts' . | head -n1 || true)"
if [ -n "${file:-}" ]; then
  wc -l "$file"
  sed -n '1,130p' "$file" | cat -n
fi

echo "== Search REEVIEW_ATTESTATION_KEY_ENV usages =="
rg -n "REVIEW_ATTESTATION_KEY_ENV|review-attestation|reviewAttestation|attestation" -S . \
  -g '!node_modules' -g '!dist' -g '!build' | head -n 120

echo "== Search vi.stubEnv/unstubAllEnvs usage =="
rg -n "vi\.stubEnv|vi\.unstubAllEnvs" . -g '!node_modules' -g '!dist' -g '!build' | head -n 80

Repository: eltmon/overdeck

Length of output: 23950


🌐 Web query:

Vitest vi.stubEnv vi.unstubAllEnvs documentation

💡 Result:

In Vitest, vi.stubEnv and vi.unstubAllEnvs are utilities used to modify and restore environment variables during testing [1][2]. vi.stubEnv(name, value) This function changes the value of an environment variable on both process.env and import.meta.env [1][3]. - Parameters: Takes the name of the environment variable (string) and the value to set (string, boolean, or undefined) [1][2]. - Note: If you modify environment variables by direct assignment (e.g., import.meta.env.MODE = 'test'), Vitest will not be able to use vi.unstubAllEnvs to restore the original value [1][2]. vi.unstubAllEnvs This function restores all environment variables changed via vi.stubEnv to their original values that existed before the first stub was created [1][3]. - Usage: It is typically used manually in hooks like beforeEach or afterEach [4][5]. Configuration Option: unstubEnvs You can enable automatic restoration of environment variables by setting the unstubEnvs configuration option to true in your vitest.config.ts file [4][6]. When enabled, Vitest automatically calls vi.unstubAllEnvs before each test [6]. - Caution: This setting may cause issues with concurrent tests, as the completion of one test will restore environment variables that might still be in use by other concurrent tests [6]. Example of configuration: // vitest.config.ts import { defineConfig } from 'vitest/config' export default defineConfig({ test: { unstubEnvs: true, }, })

Citations:


Restore the original attestation-key value with vi.stubEnv.

Direct mutation and delete process.env[...] do not restore a pre-existing OVERDECK_REVIEW_ATTESTATION_KEY. Use vi.stubEnv(REVIEW_ATTESTATION_KEY_ENV, ...) in beforeEach and vi.unstubAllEnvs() in afterEach.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/dashboard/server/routes/specialists-review-attestation.test.ts`
around lines 65 - 78, Update the test setup around beforeEach and afterEach to
use vi.stubEnv for REVIEW_ATTESTATION_KEY_ENV instead of directly mutating
process.env, and call vi.unstubAllEnvs() during teardown so any pre-existing
OVERDECK_REVIEW_ATTESTATION_KEY value is restored.

@eltmon

eltmon commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

Review CHANGES REQUESTED for PAN-3511

Review Synthesis — PAN-3511 — 2026-08-04T22:51:46Z

Verdict: CHANGES REQUESTED — all four convoy reviewers timed out without producing reports

Context

  • Manifest: /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-a9b92991/context.json
  • Branch: feature/pan-3511
  • Workspace: /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511
  • HEAD reviewed: a9b9299
  • Cycle number: 4
  • Prior cycle SHA: 71c3951

Convoy Status

Sub-role Signal Output Blocking findings
security timeout infrastructure failure
correctness timeout infrastructure failure
performance timeout infrastructure failure
requirements timeout infrastructure failure

Blocking Findings

[security] Reviewer timed out without a usable report

The security reviewer exceeded its deadline and exhausted retries. No report exists for this run, so the security dimension is unreviewed and the change cannot be approved.

[correctness] Reviewer timed out without a usable report

The correctness reviewer exceeded its deadline and exhausted retries. No report exists for this run, so the correctness dimension is unreviewed and the change cannot be approved.

[performance] Reviewer timed out without a usable report

The performance reviewer exceeded its deadline and exhausted retries. No report exists for this run, so the performance dimension is unreviewed and the change cannot be approved.

[requirements] Reviewer timed out without a usable report

The requirements reviewer exceeded its deadline and exhausted retries. No report exists for this run, so the acceptance-criteria dimension is unreviewed and the change cannot be approved.

Non-blocking Findings

None.

Clean Sub-roles

None; all four sub-roles timed out before producing usable reports.

Source: /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-a9b92991/synthesis.md

Required action

Fix every blocking review finding, commit the fixes, then re-request review with:

pan review request PAN-3511 -m "Fixed review issues"

@eltmon

eltmon commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

Review CHANGES REQUESTED for PAN-3511

Review Synthesis — PAN-3511 — 2026-08-04T23:00:27Z

Verdict: CHANGES REQUESTED — dashboard restarts invalidate active review attestation authority

Context

  • Manifest: /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-a9b92991/context.json
  • Branch: feature/pan-3511
  • Workspace: /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511
  • HEAD reviewed: a9b9299
  • Cycle number: 4
  • Prior cycle SHA: ec9f518 (last complete synthesis)

Convoy Status

Sub-role Signal Output Blocking findings
security ready /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-a9b92991/security.md 0
correctness ready /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-a9b92991/correctness.md 1
performance ready /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-a9b92991/performance.md 0
requirements ready /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-a9b92991/requirements.md 0

Blocking Findings

[correctness] Dashboard restarts discard the key needed to verify review evidence — src/dashboard/server/main.ts:117

ensureReviewAttestationKey() creates a random HMAC key only in the dashboard process environment. A dashboard restart replaces it, so both an already-running review agent's run-bound completion token and any report attestations signed before the restart fail verification. The recovery readers then see no authoritative artifact and can reset or stick a review that actually finished, recreating the verdict-loss condition this change is intended to prevent.

Persist the host-only signing key under getOverdeckHome() with mode 0600, load it at boot, and generate plus atomically persist it only when absent. Keep that key stripped from agent environments, and add restart-boundary coverage for previously issued review-agent tokens and signed report artifacts. This line is in both the PR diff and the cycle diff from ec9f5180dd50309c0be73b78dac2dfdf6981b438, so it remains promotable in cycle 4.

Non-blocking Findings

[security] Host attestation key can reach workspace-controlled child processes — src/lib/child-env.ts:93

~ [demoted: pre-existing] buildChildEnvWithoutTmuxSync() only removes terminal variables, so it does not inherit the new PROVIDER_ENV_KEYS filtering. That can expose the host signing key to workspace-controlled ./dev processes. The concern is serious, but the reviewer cited line 93, which is outside both the PR and cycle diffs; the mandatory scope gate prevents it from blocking this review as reported. The PR-added key entries are at src/lib/child-env.ts:43-45.

Scope Note

The security reviewer identified an environment-isolation gap, but its cited implementation line is pre-existing and outside the PR/cycle diffs, so the scope gate demotes it rather than treating it as a blocker. The dashboard-restart correctness finding is independently blocking and is in the cycle diff.

Clean Sub-roles

  • performance
  • requirements

Source: /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-a9b92991/synthesis.md

Required action

Fix every blocking review finding, commit the fixes, then re-request review with:

pan review request PAN-3511 -m "Fixed review issues"

@eltmon

eltmon commented Aug 5, 2026

Copy link
Copy Markdown
Owner Author

Review CHANGES REQUESTED for PAN-3511

Review Synthesis — PAN-3511 — 2026-08-05

Verdict: CHANGES REQUESTED — the persisted attestation key remains readable by workspace-controlled processes

Context

  • Manifest: /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-ac6d2295/context.json
  • Branch: feature/pan-3511
  • Workspace: /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511
  • HEAD reviewed: ac6d229
  • Cycle number: 5
  • Prior cycle SHA: a9b9299 (last complete synthesis)

Convoy Status

Sub-role Signal Output Blocking findings
security ready /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-ac6d2295/security.md 1
correctness ready /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-ac6d2295/correctness.md 1
performance ready /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-ac6d2295/performance.md 0
requirements ready /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-ac6d2295/requirements.md 0

Blocking Findings

[security] Persisting the signing key at a same-UID path does not make it host-only — src/lib/review-attestation-key.ts:31

The new restart-safe key is read from <OVERDECK_HOME>/review-attestation-key with mode 0600. Review agents and repository-controlled subprocesses run as the same Unix account as the dashboard, so they can read that file directly despite environment stripping. With the active run ID from workspace context.json, such a process can derive the HMAC completion token, write an APPROVED report, and call the attestation endpoint; the host then signs attacker-authored content as a valid verdict of record.

The key persistence code is in both the PR diff and the cycle diff from a9b929918604303a61ec22b0189528eb8928c778, so this remains promotable in cycle 5. Put attestation behind an actual privilege or mount boundary unavailable to review-workspace processes, and prove a workspace-owned subprocess cannot read the signing material, derive a current-run token, or obtain a passed attestation.

[correctness] A head-guard refusal suppresses the review-infrastructure breaker without restoring a verdict — src/lib/cloister/deacon-review-status.ts:359

artifactSupersededBreaker() returns true for every result other than no-artifact. On blocked-by-head-guard, attemptArtifactVerdictRestore() intentionally writes no status, but both exhausted-retry breaker sites return before their existing markWorkspaceStuck('review_infrastructure_failure', ...) call. A dead review that completed on a different HEAD is consequently left pending or reviewing with no live reviewer and no durable operator-visible recovery state; subsequent patrols repeat the failed restore without advancing or escalating it.

This helper and both callers were introduced in the cycle diff. Treat only restored as superseding the breaker. Keep the blocked-restore event and activity evidence, then execute the established infrastructure-failure stuck mark for a guard-rejected artifact so the unresolved verdict is visible without claiming that it landed.

Scope Note

Both blockers cite lines added after the previous complete synthesis and remain inside the overall PR diff, so neither is demoted by the mandatory PR-scope or cycle convergence gates. The requirements report marks all seven traced work items Scope: in_pr_scope and complete; it does not override the independently found security and recovery-state failures.

Clean Sub-roles

  • performance
  • requirements

Source: /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-ac6d2295/synthesis.md

Required action

Fix every blocking review finding, commit the fixes, then re-request review with:

pan review request PAN-3511 -m "Fixed review issues"

panopticon-agent[bot] added 4 commits August 4, 2026 22:33
@eltmon

eltmon commented Aug 5, 2026

Copy link
Copy Markdown
Owner Author

Review CHANGES REQUESTED for PAN-3511

Review Synthesis — PAN-3511 — 2026-08-05

Verdict: CHANGES REQUESTED — Workspace-writable artifacts can independently set a passed review verdict

Context

  • Manifest: /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-e8840c4f/context.json
  • Branch: feature/pan-3511
  • Workspace: /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511
  • HEAD reviewed: e8840c4f28f445db550a0d2d2d41e9f56f46d135
  • Cycle number: 9
  • Prior cycle SHA: 250ced115b9940dea6a8ac02b6e35f7c5a4bd539

Convoy Status

Sub-role Signal Output Blocking findings
security ready security.md 1
correctness ready correctness.md 1
performance ready performance.md 2
requirements ready requirements.md 1

Blocking Findings

[security] Workspace-controlled artifacts can bypass independent review — src/lib/cloister/synthesis-verdict.ts:76

readLatestSynthesisVerdict() selects the newest parseable synthesis.md or review.md in the workspace-owned .pan/review tree and attemptArtifactVerdictRestore() writes that verdict through the review-status door. A work agent can create a fresh review.md containing ## Verdict: APPROVED; it can omit context.json, so the head guard does not apply. A recovery path can then persist reviewStatus: 'passed' without a reviewer-issued artifact or host-owned provenance. This defeats the independent-review boundary.

[correctness] Artifact freshness is not scoped to the active review cycle — src/lib/cloister/synthesis-verdict.ts:77

The reader accepts the most recently modified artifact from any run when it is younger than 30 minutes, without matching it to the active row's reviewSpawnedAt or review run. If review A writes an approved quick-review artifact and review B begins within that window, a recovery path before B emits a verdict can restore A's passed state onto B's reviewing row. Quick-review artifacts often have no head evidence, so the head guard does not prevent this stale-cycle restore.

[performance] A cache miss synchronously scans workspace artifacts from the canonical status-read path — src/lib/review-status-read.ts:53

The stale-journal branch calls readMemoizedArtifactVerdict() from resolveJournalReconciledReviewStatusSync(). A cache miss invokes synchronous statSync, readdirSync, statSync, and readFileSync operations over .pan/review in src/lib/cloister/synthesis-verdict.ts:69-89. The status resolver is reached by dashboard/API status reads, so a batch of stale-journal rows serializes filesystem walks on the Node event loop. This also violates the explicit no-per-read filesystem-artifact-scan constraint.

[performance] The artifact verdict memo grows for the lifetime of the dashboard process — src/lib/cloister/synthesis-verdict.ts:121

The module-level artifactVerdictMemo adds an entry for every consulted issue, including absent artifacts, but never evicts entries. Its 60-second TTL controls refresh timing rather than retention, so a long-lived server retains historical issue keys, verdict metadata, and notes indefinitely. Restore the prior bounded/LRU cache behavior or otherwise bound eviction to the active-review population.

[requirements] Sweeper documentation promises behavior the implementation deliberately does not perform — docs/PARKED-POPULATION.md:47

The documentation says the observability-only stall sweeper restores a verdict, clears the stuck flag, and emits sweep.unparked. The implementation in src/lib/cloister/stall-sweeper.ts:284-315 only emits sweep.recommendation; it neither restores review status nor clears a flag. The required operator-facing contract is inaccurate and could leave an issue parked while an operator expects it to advance automatically.

Non-blocking Findings

None.

Scope Note

All five blocking findings are in the PR diff and in the cycle diff since 250ced115b9940dea6a8ac02b6e35f7c5a4bd539. The security, active-cycle, and memory-boundary regressions were introduced when this cycle reverted host-attested, active-run artifact selection and bounded memoization; they are promotable under the cycle-9 convergence gate. The requirements finding is explicitly classified Scope: in_pr_scope by the requirements reviewer.

Clean Sub-roles

None. Every completed sub-role reported at least one blocking finding.

Source: /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-e8840c4f/synthesis.md

Required action

Fix every blocking review finding, commit the fixes, then re-request review with:

pan review request PAN-3511 -m "Fixed review issues"

panopticon-agent[bot] and others added 2 commits August 5, 2026 03:43
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
@eltmon

eltmon commented Aug 5, 2026

Copy link
Copy Markdown
Owner Author

Review CHANGES REQUESTED for PAN-3511

Review Synthesis — PAN-3511 — 2026-08-05

Verdict: CHANGES REQUESTED — The revision removes three required verdict-recovery behaviors

Context

  • Manifest: /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-152565c9/context.json
  • Branch: feature/pan-3511
  • Workspace: /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511
  • HEAD reviewed: 152565c99eb789dc0434cef9884716a094b12cb6
  • Cycle number: 10
  • Prior cycle SHA: e8840c4f28f445db550a0d2d2d41e9f56f46d135

Convoy Status

Sub-role Signal Output Blocking findings
security ready security.md 0
correctness ready correctness.md 0
performance ready performance.md 1
requirements ready requirements.md 3

Blocking Findings

[requirements] The shared restore helper and head-guard visibility path are absent — src/lib/cloister/verdict-restore.ts:1

The current file only reads active-run evidence. It no longer exposes the required restore outcome contract, head-mismatch prediction, safe restoration path, or producer for review.verdict_restore_blocked, although the event schema remains declared. This fails the in-scope restore-helper requirement and leaves a safe terminal artifact unable to take the required recovery path.

[requirements] Orphan recovery resets a reviewing row after finding active-run evidence — src/lib/cloister/deacon-review-status.ts:597

The orphan recovery branch logs active-run evidence and then unconditionally sets reviewStatus: 'pending'. It does not delegate its decision to the restore helper or preserve a guard-refused artifact, so the exact recovery path the issue targets can still overwrite recoverable terminal evidence. Keep the existing reset only for the helper's no-evidence outcome.

[requirements] The stale-journal refusal never consults corroborating artifact evidence — src/lib/review-status-read.ts:36

The artifact-based stale-journal recovery block was removed. The resolver now always refuses the stale journal snapshot, even where active-run evidence corroborates its terminal verdict. FR-7 requires the memoized consult exclusively after the existing stale-refusal predicate, so the common path remains filesystem-free while that recovery remains available.

[performance] Active-run artifact reads synchronously block Deacon recovery and feedback handling — src/lib/cloister/synthesis-verdict.ts:74

readActiveReviewArtifact() uses synchronous existence checks, statSync, and readFileSync, and it is called from serial orphan patrol and feedback-delivery recovery paths. A slow or contended workspace filesystem blocks the dashboard's Node event loop for each eligible recovery item. Provide an async artifact reader for these callers, or ensure synchronous consumers use only evidence populated asynchronously in a bounded cache.

Non-blocking Findings

[correctness] Cached artifact evidence can outlive its 30-minute freshness bound — src/lib/cloister/synthesis-verdict.ts:131

The 60-second memo TTL can return a non-null artifact for up to one minute after SYNTHESIS_ARTIFACT_FRESH_MS expires. This only affects an observability recommendation in the current revision and does not write a terminal verdict, so it is advisory. Cap a non-null cache entry's expiry at the earlier of its memo TTL and artifact freshness deadline, with a boundary regression test.

Scope Note

All four blockers are in the PR diff and in the cycle diff since e8840c4f28f445db550a0d2d2d41e9f56f46d135, so they remain promotable under the cycle-10 convergence gate. The requirements review classifies each missing work item as Scope: in_pr_scope; the security reviewer confirms that the replacement active-run-evidence model closes the prior artifact-forgery authorization bypass, but that does not satisfy the missing recovery requirements.

Clean Sub-roles

  • security

Source: /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-152565c9/synthesis.md

Required action

Fix every blocking review finding, commit the fixes, then re-request review with:

pan review request PAN-3511 -m "Fixed review issues"

@eltmon

eltmon commented Aug 5, 2026

Copy link
Copy Markdown
Owner Author

Review CHANGES REQUESTED for PAN-3511

Review Synthesis — PAN-3511 — 2026-08-05

Verdict: CHANGES REQUESTED — recovery again promotes workspace-controlled evidence and reintroduces forbidden synchronous status-read I/O

Context

  • Manifest: /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-baf9b820/context.json
  • Branch: feature/pan-3511
  • Workspace: /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511
  • HEAD reviewed: baf9b8206648c2c6c4ced324e58e2299bd1bb7d0
  • Cycle number: 11
  • Prior cycle SHA: 152565c99eb789dc0434cef9884716a094b12cb6

Convoy Status

Sub-role Signal Output Blocking findings
security ready security.md 1
correctness ready correctness.md 2
performance ready performance.md 1
requirements ready requirements.md 1

Blocking Findings

[security] A workspace-controlled active-run artifact can forge a terminal approval — src/lib/cloister/verdict-restore.ts:121

attemptArtifactVerdictRestore() parses the workspace artifact and passes its verdict to recordReviewVerdict() at src/lib/cloister/verdict-restore.ts:139-150. Active-run binding and freshness constrain which directory is read, but they do not authenticate its contents. In particular, a forged review.md without context.json supplies no headSha, so the writer takes its no-evidence path and records passed without an anchor comparison. A work agent that can write the workspace can therefore create a fresh approval in the host-recorded run directory and have orphan or breaker recovery advance an unreviewed change. Preserve this content as diagnostic evidence until a reviewer/host-issued attestation, stored outside the workspace and bound to the issue, run, verdict, and reviewed head, can authorize a recovery write.

[correctness] A head-guard refusal is treated as a completed recovery and leaves the review ownerless — src/lib/cloister/deacon-review-status.ts:360

restoreArtifactAtBreaker() returns true for both restored and blocked-by-head-guard, and its breaker callers return without marking the established review_infrastructure_failure stuck state. The orphan path likewise returns on a guard refusal at src/lib/cloister/deacon-review-status.ts:616-619 while its row remains reviewing and no review agent is live. A blocked-by-head-guard result writes no terminal verdict, so it is diagnostic evidence rather than recovery success; preserve and report it, but continue the existing bounded retry/escalation behavior.

[correctness] Stale-journal reconciliation bypasses the same head guard — src/lib/review-status-read.ts:45

The stale-refusal branch accepts any active-run artifact whose verdict string matches the journal and replays the stale terminal journal snapshot at src/lib/review-status-read.ts:49-53. It never compares artifact.headSha with dbStatus.lastVerifiedCommit, even though the restore helper refuses that mismatch. A stale passed journal and a current-run passed artifact with a different head can therefore mark the new cycle passed; require the same anchor policy before lifting the refusal and retain the current DB row on a mismatch.

[performance] Cold memo misses synchronously scan workspace files from canonical status reads — src/lib/review-status-read.ts:45

resolveJournalReconciledReviewStatusSync() calls readMemoizedArtifactVerdict() on the stale-refusal path. On a cold or expired key, that function reaches findVerdictReport(), statSync, and readFileSync at src/lib/cloister/synthesis-verdict.ts:181-185 and :104-117. The TTL and LRU bound limit repeats but do not move initial I/O out of the dashboard/API read path, violating the explicit no-per-read filesystem-scan constraint and allowing slow workspace storage to block the Node event loop. The synchronous resolver must consume only pre-populated cache entries and preserve the existing refusal on a miss; an asynchronous lifecycle or bounded background task must populate/refresh the cache.

[requirements] PAN-3511 modifies and depends on the explicitly excluded verdict write door — src/lib/cloister/review-verdict-writer.ts:41

The issue explicitly excludes recordReviewVerdict and src/lib/cloister/review-verdict-writer.ts as PAN-3512 work. This cycle adds clearStuckReason and its terminal-row mutation to the writer, then makes the new recovery helper import and invoke that writer at src/lib/cloister/verdict-restore.ts:12-15 and :139-150. Remove the writer changes and terminal-writer integration from this issue; PAN-3511 remains read-side evidence, visible guard refusal, and recommendation behavior until the separate write-door work owns an authenticated transition.

Scope and Convergence Gate

All five findings are in the current PR diff and in the cycle diff from 152565c99eb789dc0434cef9884716a094b12cb6 to baf9b8206648c2c6c4ced324e58e2299bd1bb7d0, so they are promotable in cycle 11. The functional recovery additions address the prior cycle's missing-helper and async-Deacon findings, but they do not override the original security, no-write-door, and no-per-read-filesystem constraints.

Clean Areas

  • Deacon orphan, breaker, and feedback recovery now use the asynchronous artifact reader rather than synchronous workspace I/O.
  • Artifact lookup is bound to the host-recorded active run, rejects absent run identity, uses a 256-entry LRU key space, and caps positive memo lifetime at the artifact freshness boundary.
  • The sweeper remains recommendation-only; it does not directly write review status, dispatch, stop, resume, or clear a stuck flag.

Source: /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-baf9b820/synthesis.md

Required action

Fix every blocking review finding, commit the fixes, then re-request review with:

pan review request PAN-3511 -m "Fixed review issues"

@eltmon

eltmon commented Aug 5, 2026

Copy link
Copy Markdown
Owner Author

Review CHANGES REQUESTED for PAN-3511

Review Synthesis — PAN-3511 — 2026-08-05

Verdict: CHANGES REQUESTED — the stale-journal guard rejects the default headless quick-review artifact shape

Context

  • Manifest: /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-59501a37/context.json
  • Branch: feature/pan-3511
  • Workspace: /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511
  • HEAD reviewed: 59501a371249fe5dfaf36a0c110b989086a3e0ee
  • Cycle number: 12
  • Prior cycle SHA: baf9b8206648c2c6c4ced324e58e2299bd1bb7d0

Convoy Status

Sub-role Signal Output Blocking findings
security ready security.md 0
correctness ready correctness.md 1
performance ready performance.md 0
requirements ready requirements.md 0

Blocking Findings

[correctness] Headless quick-review artifacts cannot corroborate a terminal journal verdict — src/lib/review-status-read.ts:50

The stale-journal exception now requires artifact.headSha, liveStatus.lastVerifiedCommit, and equality between them before it reconciles the terminal journal result. That turns the review head into a required condition even though readHeadEvidenceAsync() deliberately treats a missing context.json as normal for quick/self-review, and restoreWouldTripHeadGuard() only identifies a conflict when both anchors exist. A fresh host-bound quick review.md with the same terminal verdict as the journal therefore cannot lift the refusal when it has no optional head evidence, leaving the default quick-review path stuck at reviewing or pending. Preserve the active-run and matching-verdict requirements, but use the same two-present-anchor policy as the observation helper: reject only a known conflicting pair of anchors, and add coverage for matching headless quick artifacts and a live row with no anchor.

Scope and Convergence Gate

The cited condition was added in the cycle diff from baf9b8206648c2c6c4ced324e58e2299bd1bb7d0 to 59501a371249fe5dfaf36a0c110b989086a3e0ee, and it is within the PR diff, so it is promotable in cycle 12. The new condition fixes the preceding cycle's mismatched-head replay defect, but it also narrows the documented optional-anchor contract for the fleet-default quick mode.

Clean Sub-roles

  • security — workspace artifacts remain diagnostic evidence and no longer write terminal review status.
  • performance — status reads are cache-only, while Deacon and feedback artifact reads use asynchronous filesystem operations.
  • requirements — all seven traced work items and explicit no-write-door/no-per-read-scan constraints are otherwise covered.

Source: /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-59501a37/synthesis.md

Required action

Fix every blocking review finding, commit the fixes, then re-request review with:

pan review request PAN-3511 -m "Fixed review issues"

@eltmon

eltmon commented Aug 5, 2026

Copy link
Copy Markdown
Owner Author

Review CHANGES REQUESTED for PAN-3511

Review Synthesis — PAN-3511 — 2026-08-05

Verdict: CHANGES REQUESTED — headless workspace evidence can replay a stale approval

Context

  • Manifest: /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-09638451/context.json
  • Branch: feature/pan-3511
  • Workspace: /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511
  • HEAD reviewed: 09638451ffa2dc04aed07d6d8738df79e2469514
  • Cycle number: 13
  • Prior cycle SHA: 59501a371249fe5dfaf36a0c110b989086a3e0ee

Convoy Status

Sub-role Signal Output Blocking findings
security ready security.md 1
correctness ready correctness.md 0
performance ready performance.md 0
requirements ready requirements.md 0

Blocking Findings

[security] Headless workspace artifact can authorize stale verdict replay — src/lib/review-status-read.ts:50

The cycle changes the stale-journal exception from requiring a matching artifact and live review-head anchor to accepting an artifact whenever either anchor is absent. A work agent can write a fresh APPROVED review.md in the host-selected active-run directory and omit workspace-controlled context.json; after the asynchronous reader populates its memo, the status resolver uses that headless artifact to lift the stale-journal refusal and replays an approval from a prior review cycle. The resulting terminal state can recompute readyForMerge without a current review verdict. Keep the stale-journal refusal unless immutable host-recorded or authenticated review-head evidence binds the artifact to the live row; missing workspace evidence must not satisfy that authorization condition.

Scope and Convergence Gate

This finding is in the PR diff and in cycle 13's diff from 59501a371249fe5dfaf36a0c110b989086a3e0ee to 09638451ffa2dc04aed07d6d8738df79e2469514, so it remains promotable. Correctness and requirements confirm that the change restores the documented headless quick-review behavior, but that behavior conflicts with the security boundary that workspace-writable evidence cannot authorize a terminal state.

Clean Sub-roles

  • correctness — optional-anchor handling, active-run binding, bounded memo use, and the updated regression cases are otherwise internally consistent.
  • performance — all artifact filesystem reads remain asynchronous, and the status resolver consumes cache-only evidence.
  • requirements — all seven delivery items and the explicit non-goals are implemented as written.

Source: /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-09638451/synthesis.md

Required action

Fix every blocking review finding, commit the fixes, then re-request review with:

pan review request PAN-3511 -m "Fixed review issues"

@eltmon

eltmon commented Aug 5, 2026

Copy link
Copy Markdown
Owner Author

Review CHANGES REQUESTED for PAN-3511

Review Synthesis — PAN-3511 — 2026-08-05

Verdict: CHANGES REQUESTED — a workspace-provided head can still replay a stale approval

Context

  • Manifest: /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-63d419b9/context.json
  • Branch: feature/pan-3511
  • Workspace: /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511
  • HEAD reviewed: 63d419b952bb04353dd4a5864493e94151828311
  • Cycle number: 14
  • Prior cycle SHA: 09638451ffa2dc04aed07d6d8738df79e2469514

Convoy Status

Sub-role Signal Output Blocking findings
security ready security.md 1
correctness ready correctness.md 0
performance ready performance.md 0
requirements ready requirements.md 0

Blocking Findings

[security] Workspace-controlled artifact head can still authorize stale verdict replay — src/lib/review-status-read.ts:50

The new host-run binding is used only for a headless artifact. An artifact that includes .pan/review/<run>/context.json instead follows artifactHeadMatchesLive, which compares a workspace-controlled headSha directly to the live row and does not require the host-recorded roleRunHead. A work agent can write a fresh matching artifact and context file for the active run, prime the asynchronous memo through a recovery reader, and cause the stale-journal resolver to replay a prior passed verdict. Require the host-recorded run anchor for every artifact-based stale-journal exception: it must be present and match lastVerifiedCommit; an artifact-provided head, when present, must also match that same host anchor. Missing or mismatched host evidence must retain the stale-journal refusal.

Scope and Convergence Gate

This is a security finding in src/lib/review-status-read.ts, which remains inside the PR diff and changed in cycle 14 from 09638451ffa2dc04aed07d6d8738df79e2469514 to 63d419b952bb04353dd4a5864493e94151828311. The cycle's host-anchor addition closes the previous headless-artifact variant, but its artifact-head branch leaves the same authorization boundary open for artifacts that supply workspace-controlled context.json.

Clean Sub-roles

  • correctness — the host-run-head projection, memo-only status lookup, and current regression coverage are internally consistent.
  • performance — the host scalar comparison preserves the bounded, asynchronous artifact-read design and keeps the status resolver filesystem-free.
  • requirements — all seven delivery requirements and the explicit no-write-door/no-hot-path-scan constraints are implemented.

Source: /home/eltmon/Projects/overdeck/workspaces/feature-pan-3511/.pan/review/agent-pan-3511-review-63d419b9/synthesis.md

Required action

Fix every blocking review finding, commit the fixes, then re-request review with:

pan review request PAN-3511 -m "Fixed review issues"

panopticon-agent[bot] and others added 5 commits August 5, 2026 06:48
Co-Authored-By: Claude <noreply@anthropic.com>
Main independently greened itself against the PAN-3551 guard fallout
(e752688): it baselined the old introspecting stall-sweeper test and
added its own review_parent_stalled_needs_you park copy. This branch had
already solved both differently (test rewritten to runtime behavior, copy
entry added earlier), so the merge produced a duplicate STUCK_REASON_COPY
key and a stale introspection-baseline entry. Keep the branch's versions:
drop the duplicated copy entry and the stale baseline line.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@overdeck-agent
overdeck-agent Bot merged commit 299d98d into main Aug 5, 2026
11 checks passed
@eltmon
eltmon deleted the feature/pan-3511 branch August 5, 2026 17:01
This was referenced Aug 5, 2026
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