PR #1: approve the scoped safety pass

Approved within PR #1 safety scope

202 repository tests pass · 30 previous reproductions pass · 4 completion checks pass

0 current blockers · F17 recovery deferred to PR #3

Incremental +126 / -21 ad32c48

The round-3 fixes hold. I would approve PR #1 at ad32c48. No new blocking findings emerged from reviewing the latest runtime changes and their affected callers. This verdict covers the safety pass; the later feature and recovery PRs still need their own reviews.

Reviewed on 22 September 2026 at exact head ad32c4806123f5cfe19d81e81eb7444ef7825750, in an isolated worktree. PR #1 passes all 202 repository tests and pnpm typecheck. All 30 independent reproductions from the previous rounds pass, together with four new completion and retry checks. The previous reproductions overlap scenarios retained in the repository; they are not 30 additional unique baseline scenarios.

Why the latest fixes are sufficient

F28 is fixed at both relevant boundaries: concurrent reconciliation calls share one request, and the writer rechecks the monotonic outcome rule after the adapter returns. The additional checks confirm that a transient adapter error leaves reconciliation retryable, and callers receive detached results.

F29 is fixed at each dispatch claim. A pause received while an invocation is awaiting prevents the next pending effect from starting. The added completion check confirms that an explicit resume then starts the retained effect once and finishes the transaction.

F30 is fixed before clarification consumes pending context. An ambiguous answer preserves the pending question and cannot create an executable proposal. A later unambiguous answer completes the same request successfully. This matters because simply disabling the path would have passed the original negative assertion without preserving useful behavior.

Architecture judgment

These changes enforce the intended safety rules where asynchronous work and interpretation can change execution. The fixes now cover both rejected actions and subsequent valid progress. That supports accepting this safety foundation and moving to the next bounded review.

This round does not establish that the whole product architecture is optimal. F17's durable reconstruction requirement remains an explicit follow-up for PR #3. Real semantic interpretation, persistence behavior and the inspector's end-to-end guarantees must be judged in their respective PRs. Passing the stack's tests alone does not approve those implementations.

Stack and rebase verification

All six PRs are open, declare the expected successive bases, and have the expected parent chain. PR #2 at f1634eb is patch-equivalent to its earlier 9fde66a version according to git range-diff; the recovered accepted-turn changes were retained.

The stack tip 3a7b60e independently passes typecheck and 261 tests: 229 evals, 14 journal-file, 12 perception-jev and six inspector tests. The 30 prior reviewer reproductions also pass against that tip. This checks regression and rebase integrity, with no full code review of PRs #2–#6 implied.

Recommendation

Merge PR #1 at the reviewed head, then review PR #2 as the next step. There are no remaining actionable findings within PR #1's agreed scope. The sidecar retains F17 as open and deferred, so the global historical count is one even though this PR's blocker count is zero. No GitHub approval, comment or merge was submitted during this review.

Reproduce

cd /tmp/circe-pr1-round4
pnpm test
pnpm typecheck
pnpm exec vitest run --config /tmp/circe-round4-evidence/vitest.config.mjs --reporter=verbose

The independent suite returns 34 passed: 18 first-round, nine second-round, three third-round and four completion checks. The downloaded configuration and fixture imports use this machine's isolated-worktree paths; adjust those paths elsewhere. Circe source files were not edited.

4 completion tests 18 first-round tests 9 second-round tests 3 third-round tests Test config 34-test output PR #1 baseline PR #1 typecheck Stack baseline Stack typecheck Stack reproductions Review sidecar

Files changed in this round
Round-3 findings resolved

F28. Concurrent reconciliation now shares one adapter call per effect and rechecks canAdvanceEffect after the await. The original regression passes, as do transient-error retry and detached-result checks.

F29. Each dispatch claim rechecks transaction pause and lifecycle authorization. The original regression passes; explicit resume subsequently invokes the retained pending effect exactly once.

F30. Ambiguous fragments and clarifications return a question before consuming pending context or references. The original regression passes; the pending question survives and a later unambiguous answer completes successfully.

Deferred follow-up prompt · Ctrl+`
Prompt to rerun review · Ctrl+Space
Earlier reviews and original finding bodies, preserved

This is the historical round-3 report at 5b165b5. Its requested changes have now been verified fixed. Open round 3 archive.

PR #1: three remaining safety gaps

Changes requested

199 repository tests pass · 27 previous reproductions pass · 3 new reproductions fail

3 current P1 findings · F17 recovery deferred separately

Incremental +274 / -46 5b165b5

The previous fixes hold. I would still request changes for three remaining paths. I verified the exact PR head, 5b165b5cb9d319c4503fe675b6bf811fd6851da1, in an isolated worktree. This review covers the changes since 5afb275 and the callers affected by those safety checks.

Reviewed on 22 September 2026. PR #1 now passes all 199 repository tests across 16 files and pnpm typecheck. The 18 first-round and nine second-round independent reproductions also pass. Those reruns cover the same scenarios retained in the repository; they are not 27 additional unique baseline scenarios.

What is fixed

Rejected revisions now revoke pending authorization. The finite-concurrency dispatch loop prevents the reproduced duplicate invocation. Ambiguous affirmations no longer commit. Adapted commands are snapshotted, lost-response batch receipts retain the dispatch key, and late invoke results preserve terminal outcomes. Frame identity and slot exclusion checks pass the earlier reproductions. Completed command IDs now reject conflicting payloads.

The PR also correctly separates the original F17 recovery work from command identity recording. Recovery remains outside this safety-pass verdict. I did not audit the rebased PRs #2-#6 or independently verify their 258-test claim here.

What remains

The statement that every writer uses the shared checks is still broader than the implementation. reconcile() never calls canAdvanceEffect(). Dispatch rechecks an effect's PENDING state after an await but does not recheck a pause received in the meantime. The ambiguity guard excludes the clarification branch even though that branch can emit an executable proposal.

These are concrete gaps in the same safety rules under review. The three new tests produce a completed effect becoming UNKNOWN, a new invocation after pause, and execution from an explicitly ambiguous clarification. They do not depend on restart recovery or a real semantic model.

Next step

Make reconciliation apply the shared outcome guard after its await, recheck dispatch eligibility before every effect claim, and guard clarification consumption before it creates or revises an intent. Then run the 199-test baseline, the 27 previous reproductions and the three new tests against the final PR head. Keep F17 deferred and review the rest of the stack separately.

The reduced set of remaining failures supports keeping the current direction. I would finish these checks before treating PR #1 as the approved foundation for the stack.

Reproduce

cd /tmp/circe-pr1-round3
pnpm exec vitest run --config /tmp/circe-round3-evidence/vitest.config.mjs --reporter=verbose

On the reviewed commit this yields 27 passed and three failed, all assertion failures. The configuration and test-frame imports use the local isolated-worktree paths; adapt those paths to reproduce elsewhere. No Circe source was edited, and the user's main workspace stayed on its existing branch.

3 new tests 18 first-round tests 9 second-round tests Test config Reproduction output Baseline Typecheck Review sidecar

Current findings
  1. Concurrent reconciliation bypasses the monotonic outcome rule P1
  2. An interrupted dispatch loop starts its next pending effect P1
  3. Ambiguous clarification can complete a request into execution P1
Prompt for current findings · Ctrl+`
Prompt to rerun review · Ctrl+Space
F29. An interrupted dispatch loop starts its next pending effectpackages/core/src/runtime.ts:695-700P1 logic correctness

The revised dispatch loop rechecks whether each effect is still PENDING, but checks tx.paused only once at function entry. An interrupt received while invokeBatch() is awaiting does not stop the loop from dispatching its next pending effect.

With concurrency set to one, the test starts A and holds its invoke response. It sends an interrupt and verifies that the transaction is paused. Once A returns SUCCEEDED, B invokes without any resume command. The fix prevents duplicate ownership, but the loop still fails to recheck whether dispatch is currently authorized.

Smallest fix. Before claiming every pending effect, recheck transaction pause and lifecycle eligibility as well as effect status and available capacity. Stop the active loop when paused and leave the remaining effects pending for an explicit resume.

Source at reviewed commit · Related to F20

Captured source
695:       for (const effect of pendingEffects) {
696:         // Another dispatch loop (resume, observation) may have claimed this
697:         // effect while this loop was awaiting; never start it twice.
698:         if (!stillPending(effect)) continue;
699:         if (runningCount() >= max) break;
700:         await invokeBatch(ctx, [effect], descriptor);
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 `packages/core/src/runtime.ts` around lines 695-700, The revised dispatch loop rechecks whether each effect is still PENDING, but checks `tx.paused` only once at function entry. An interrupt received while `invokeBatch()` is awaiting does not stop the loop from dispatching its next pending effect. With concurrency set to one, the test starts A and holds its invoke response. It sends an interrupt and verifies that the transaction is paused. Once A returns SUCCEEDED, B invokes without any resume command. The fix prevents duplicate ownership, but the loop still fails to recheck whether dispatch is currently authorized. Before claiming every pending effect, recheck transaction pause and lifecycle eligibility as well as effect status and available capacity. Stop the active loop when paused and leave the remaining effects pending for an explicit resume. Reproduce using `/tmp/circe-round3-evidence/round3.test.ts` against PR #1 at 5b165b5cb9d319c4503fe675b6bf811fd6851da1. Retain the required-behavior test in the repository and run the full suite and typecheck. Keep the earlier 27 reproductions passing; this is a remaining path related to F20, not a failure of its original reproduction.
F30. Ambiguous clarification can complete a request into executionpackages/core/src/runtime.ts:1606-1616P1 logic correctness

Ambiguity checks cover affirm, reject and request, while the fragment/clarify branch still accepts references without checking classification.ambiguous. That branch can consume pendingFrame and call applyProposal(), so it is also a consequence-bearing entry point.

The test first sends an ambiguous unresolved request for task.resume. The runtime asks for a reference. The next classification is an ambiguous clarification naming A at .51 confidence. With a current UI observation and a capability whose commit policy is none, the clarification produces an invocation. No accepted referent decision or affirmative confirmation intervenes.

Smallest fix. Apply semantic acceptance before consuming a clarification into an intent or command. Preserve the pending question on an ambiguous answer, and route accepted clarification-generated proposals and revisions through the same validated consequence boundary.

Source at reviewed commit · Related to F21

Captured source
1606:       case "fragment":
1607:       case "clarify": {
1608:         if (references.length > 0) {
1609:           setCurrentReferent(ctx, references, ctx.now);
1610:           if (state.pendingQuestion?.awaiting === "reference") {
1611:             const pendingTx = state.pendingQuestion.transactionId
1612:               ? state.transactions.get(state.pendingQuestion.transactionId)
1613:               : undefined;
1614:             const capability = state.pendingFrame?.capability;
1615:             state.pendingQuestion = undefined;
1616:             state.pendingFrame = undefined;
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 `packages/core/src/runtime.ts` around lines 1606-1616, Ambiguity checks cover affirm, reject and request, while the fragment/clarify branch still accepts references without checking `classification.ambiguous`. That branch can consume `pendingFrame` and call `applyProposal()`, so it is also a consequence-bearing entry point. The test first sends an ambiguous unresolved request for task.resume. The runtime asks for a reference. The next classification is an ambiguous clarification naming A at .51 confidence. With a current UI observation and a capability whose commit policy is none, the clarification produces an invocation. No accepted referent decision or affirmative confirmation intervenes. Apply semantic acceptance before consuming a clarification into an intent or command. Preserve the pending question on an ambiguous answer, and route accepted clarification-generated proposals and revisions through the same validated consequence boundary. Reproduce using `/tmp/circe-round3-evidence/round3.test.ts` against PR #1 at 5b165b5cb9d319c4503fe675b6bf811fd6851da1. Retain the required-behavior test in the repository and run the full suite and typecheck. Keep the earlier 27 reproductions passing; this is a remaining path related to F21, not a failure of its original reproduction.
F28. Concurrent reconciliation bypasses the monotonic outcome rulepackages/core/src/runtime.ts:1894-1903P1 logic data-integrity

canAdvanceEffect() is used by invocation responses, invocation exceptions and observations, but never by reconcile(). Reconciliation checks UNKNOWN before calling the executor, then writes the result after an await without reserving the operation or rechecking the effect state.

The reproduction starts two reconciliations of one UNKNOWN effect. The first resolves SUCCEEDED; a delayed result from the second reports UNKNOWN and overwrites that completed outcome. Both the effect and transaction regress. This contradicts the claimed shared rule for every writer, even though the original late-invocation acknowledgement test now passes.

Smallest fix. Deduplicate concurrent reconciliation per effect and apply the same terminal-state guard after the adapter await. Recheck the current effect or reservation before recording any state transition; a late result must not overwrite a terminal outcome.

Source at reviewed commit · Related to F23

Captured source
1894:     if (result.status === "SUCCEEDED") {
1895:       effect.status = "SUCCEEDED";
1896:       effect.result = result.result;
1897:       effect.endedAt = ctx.now;
1898:     } else if (result.status === "FAILED") {
1899:       effect.status = "FAILED";
1900:       effect.failure = result.failure ?? { kind: "EXECUTION_ERROR" };
1901:       effect.endedAt = ctx.now;
1902:     } else {
1903:       effect.status = "UNKNOWN";
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 `packages/core/src/runtime.ts` around lines 1894-1903, `canAdvanceEffect()` is used by invocation responses, invocation exceptions and observations, but never by `reconcile()`. Reconciliation checks UNKNOWN before calling the executor, then writes the result after an await without reserving the operation or rechecking the effect state. The reproduction starts two reconciliations of one UNKNOWN effect. The first resolves SUCCEEDED; a delayed result from the second reports UNKNOWN and overwrites that completed outcome. Both the effect and transaction regress. This contradicts the claimed shared rule for every writer, even though the original late-invocation acknowledgement test now passes. Deduplicate concurrent reconciliation per effect and apply the same terminal-state guard after the adapter await. Recheck the current effect or reservation before recording any state transition; a late result must not overwrite a terminal outcome. Reproduce using `/tmp/circe-round3-evidence/round3.test.ts` against PR #1 at 5b165b5cb9d319c4503fe675b6bf811fd6851da1. Retain the required-behavior test in the repository and run the full suite and typecheck. Keep the earlier 27 reproductions passing; this is a remaining path related to F23, not a failure of its original reproduction.
Round-2 resolutions

All nine original reproductions pass at 5b165b5. Their bodies remain preserved below; adjacent remaining paths have new IDs.

  • F19: Rejected revision does not revoke an in-flight confirmation. Original reproduction fixed.
  • F20: Overlapping dispatch loops invoke the same frozen effect twice. Original reproduction fixed.
  • F21: Ambiguous affirmations still authorize execution. Original reproduction fixed.
  • F22: Classifier-produced commands bypass the immutable input boundary. Original reproduction fixed.
  • F23: Late executor acknowledgements overwrite terminal outcomes. Original reproduction fixed.
  • F24: Batch reconciliation uses keys that were never dispatched. Original reproduction fixed.
  • F25: Semantic turns can borrow evidence from the wrong frame. Original reproduction fixed.
  • F26: Exclusions check summary refs but execute slot refs. Original reproduction fixed.
  • F27: Completed command IDs skip payload conflict detection. Original reproduction fixed.
Earlier reviews and original finding bodies, preserved

This is the historical round-2 report at 5afb275. Its verdict and counts describe that older commit. Open round 2 archive.

PR #1: the previous cases are fixed, but approval still needs changes

Changes requested

190 repository tests pass · 18 prior reproductions pass · 9 new reproductions fail

8 P1 findings · 1 P2 finding · restart recovery tracked separately

+4225 / -444

The direction is better and the original repairs are real. I would still request changes. The PR's shared contracts remove several earlier bypasses. Nine additional required-behavior tests expose gaps that can still execute an invalidated intent, duplicate an effect or accept ambiguous authorization.

Reviewed on 22 September 2026 at commit 5afb2757db227cd3c50e539d61e53f23b12e8ed7, the exact head of PR #1. Its base is 68927499f971d9a6302d1cbe2dd98c39a46184d0. The working project was already on feat/inspector-e2e, so I created a detached worktree at /tmp/circe-pr1-review. Later recovery, Jev and inspector work is outside this verdict.

What now passes

  • pnpm test: 190 passed across 15 files on the PR head.
  • pnpm typecheck: clean on the PR head.
  • The original independent 18 required-behavior tests: all pass, with only local import paths adapted to the isolated worktree.
  • The PR retains those scenarios and adds 11 adversarial regressions.

I am not treating the previous failures as still broken. Concurrent duplicate proposals, explicit missing transaction IDs, revision arity, ordinary ambiguous proposals, caller mutation through command(), pinned overflow and coverage hashing now pass their reproductions. The new failures combine operations or enter through other paths.

Why I still would not approve

The shared contracts are called at some boundaries but bypassed at others. command() snapshots input, while semantic ingestion directly applies its adapter result. commit() checks the transition before awaiting validation but does not fully recheck it afterward. Effect observations obey a terminal-state guard, while executor responses write status directly. A dispatch loop owns a captured list, but resume can start another loop over the same effects.

This is a finite implementation problem with a clear repair direction. Make every writer use the same transitions and immutable inputs, and revalidate ownership after awaits. The existing architecture does not need to be replaced, but adding more branches on top of these gaps will make later debugging harder.

The semantic proposal consumer also accepts independently supplied turn, frame and decision identities without proving they belong together. It checks exclusions against one representation of targets and executes another. Those checks belong in the accepted-turn boundary before any kernel proposal can be emitted.

What should happen next

  1. Close the in-flight invalid-revision race, overlapping dispatch race and late-acknowledgement regression. Test delayed validation, delayed invoke and interleaved resume/observation calls against the same effect.
  2. Route accepted semantic commands through the same snapshot and transition handling as typed commands. Apply acceptance to affirmations and other consequence-bearing acts, not only proposals.
  3. Bind turns, frames and supporting decisions together, enforce exclusions on executable slots, preserve real batch dispatch keys and check payload identity before returning cached results.
  4. Rerun the 190-test suite, the original 18 reproductions and these nine tests on the final PR head. Review the recovery and Jev branches separately after this foundation is corrected.

Restart recovery is explicitly deferred in the PR. That is acceptable for a scoped safety pass, provided the PR stops claiming the full journal finding is closed. The narrow command-ID test passes; reconstruction was part of the broader original finding and remains tracked as F17. It is not included among the nine new failures and is not the reason for requesting changes here.

Reproduce this review

All failures below are assertion failures in local deterministic tests. No production executor or model was called. No Circe source was edited. The main workspace was left on its existing branch.

cd /tmp/circe-pr1-review
pnpm exec vitest run --config /tmp/circe-pr1-evidence/vitest.config.mjs --reporter=verbose

Expected review output on this commit: 18 previous tests pass; nine additional tests fail. The downloaded config contains local paths. Update its root and aliases and the test-frame imports when reproducing elsewhere.

9 new tests 18 previous tests Test config Test output Baseline Typecheck Updated sidecar

Round 2 issues
  1. Rejected revision does not revoke an in-flight confirmation P1
  2. Overlapping dispatch loops invoke the same frozen effect twice P1
  3. Ambiguous affirmations still authorize execution P1
  4. Classifier-produced commands bypass the immutable input boundary P1
  5. Late executor acknowledgements overwrite terminal outcomes P1
  6. Batch reconciliation uses keys that were never dispatched P1
  7. Semantic turns can borrow evidence from the wrong frame P1
  8. Exclusions check summary refs but execute slot refs P1
  9. Completed command IDs skip payload conflict detection P2
One owner at each boundary

Semantic acceptance → immutable command → checked transaction transition → validation reservation → state recheck → reserved dispatch → monotonic effect outcome.

Every async callback and ingestion path must enter through the same checks.

Prompt for current review findings · Ctrl+`
Prompt to rerun review · Ctrl+Space
F23. Late executor acknowledgements overwrite terminal outcomespackages/core/src/runtime.ts:625-634P1 logic correctness

applyEffectObservation() protects terminal effects only from later observations. invokeBatch() assigns status directly after its await, without checking whether an observation already settled the effect.

The test holds an invocation response, ingests a SUCCEEDED effect observation, then releases the earlier response with status RUNNING. The effect regresses to RUNNING and the transaction loses its completed state. A late transport exception can similarly overwrite a known outcome with UNKNOWN.

Smallest fix. Apply one monotonic effect-transition rule to observations, invoke responses, exceptions and reconciliation results. Accept receipt metadata separately from status so a late acknowledgement cannot undo a terminal result.

Source at reviewed commit

Captured source
625:         log({ kind: "dispatch-acknowledged", receipt: effect.receipt, at: ctx.now });
626:         if (!result.ok) {
627:           effect.status = "FAILED";
628:           effect.failure = result.failure ?? { kind: "EXECUTION_ERROR" };
629:         } else if (result.status === "RUNNING") {
630:           effect.status = "RUNNING";
631:         } else {
632:           effect.status = "SUCCEEDED";
633:           effect.result = result.result;
634:           effect.endedAt = ctx.now;
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 `packages/core/src/runtime.ts` around lines 625-634, `applyEffectObservation()` protects terminal effects only from later observations. `invokeBatch()` assigns status directly after its await, without checking whether an observation already settled the effect. The test holds an invocation response, ingests a SUCCEEDED effect observation, then releases the earlier response with status RUNNING. The effect regresses to RUNNING and the transaction loses its completed state. A late transport exception can similarly overwrite a known outcome with UNKNOWN. Apply one monotonic effect-transition rule to observations, invoke responses, exceptions and reconciliation results. Accept receipt metadata separately from status so a late acknowledgement cannot undo a terminal result. Reproduce against PR #1 at 5afb2757db227cd3c50e539d61e53f23b12e8ed7 using `/tmp/circe-pr1-evidence/additional.test.ts`; then add the desired-behavior test to the repository and run the full test suite and typecheck. Preserve the typed semantic boundary and avoid English routing rules.
F24. Batch reconciliation uses keys that were never dispatchedpackages/core/src/runtime.ts:642-647P1 logic data-integrity

invokeBatch() sends one idempotency key, the first effect ID, for the entire invocation. If the response is lost, its synthesized receipts use each individual effect ID as the idempotency key. All but the first receipt therefore refer to an operation the executor never saw. The no-receipt acknowledgement branch has the same mismatch.

The fake executor records the actual batch key, applies the batch, then throws to simulate a lost response. Reconciliation succeeds for the first effect but returns UNKNOWN for the second. The batch was applied, yet the runtime cannot resolve all of it using its own receipts.

Smallest fix. Record a stable dispatch-operation identity shared by the batch and preserve its actual key in every member receipt. Keep per-effect identity separate. Define whether reconciliation returns a batch result or per-member results and test the lost-response path.

Source at reviewed commit

Captured source
642:       for (const effect of effects) {
643:         effect.receipt ??= {
644:           effectId: effect.id,
645:           executor: executor.id,
646:           idempotencyKey: effect.id,
647:           dispatchedAt: at,
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 `packages/core/src/runtime.ts` around lines 642-647, `invokeBatch()` sends one idempotency key, the first effect ID, for the entire invocation. If the response is lost, its synthesized receipts use each individual effect ID as the idempotency key. All but the first receipt therefore refer to an operation the executor never saw. The no-receipt acknowledgement branch has the same mismatch. The fake executor records the actual batch key, applies the batch, then throws to simulate a lost response. Reconciliation succeeds for the first effect but returns UNKNOWN for the second. The batch was applied, yet the runtime cannot resolve all of it using its own receipts. Record a stable dispatch-operation identity shared by the batch and preserve its actual key in every member receipt. Keep per-effect identity separate. Define whether reconciliation returns a batch result or per-member results and test the lost-response path. Reproduce against PR #1 at 5afb2757db227cd3c50e539d61e53f23b12e8ed7 using `/tmp/circe-pr1-evidence/additional.test.ts`; then add the desired-behavior test to the repository and run the full test suite and typecheck. Preserve the typed semantic boundary and avoid English routing rules.
F20. Overlapping dispatch loops invoke the same frozen effect twicepackages/core/src/runtime.ts:677-679P1 logic correctness

dispatch() captures a list of PENDING effects once, then awaits each invocation without rechecking an effect's state before starting it. Another resume call can dispatch an effect that remains in the first loop's captured list.

With concurrency set to two, the test holds invocation A, resumes the transaction so B starts, then releases A. The original loop starts B again. Two frozen effects produce three executor calls. The test uses a low-risk capability whose executor declares idempotency unsupported, so a duplicate can repeat the operation.

Smallest fix. Reserve each effect synchronously with a PENDING-to-RUNNING transition immediately before dispatch. Recheck eligibility after every await and share a transaction dispatch coordinator or otherwise prevent overlapping ownership.

Source at reviewed commit

Captured source
677:       for (const effect of pendingEffects) {
678:         if (runningCount() >= max) break;
679:         await invokeBatch(ctx, [effect], descriptor);
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 `packages/core/src/runtime.ts` around lines 677-679, `dispatch()` captures a list of PENDING effects once, then awaits each invocation without rechecking an effect's state before starting it. Another resume call can dispatch an effect that remains in the first loop's captured list. With concurrency set to two, the test holds invocation A, resumes the transaction so B starts, then releases A. The original loop starts B again. Two frozen effects produce three executor calls. The test uses a low-risk capability whose executor declares idempotency unsupported, so a duplicate can repeat the operation. Reserve each effect synchronously with a PENDING-to-RUNNING transition immediately before dispatch. Recheck eligibility after every await and share a transaction dispatch coordinator or otherwise prevent overlapping ownership. Reproduce against PR #1 at 5afb2757db227cd3c50e539d61e53f23b12e8ed7 using `/tmp/circe-pr1-evidence/additional.test.ts`; then add the desired-behavior test to the repository and run the full test suite and typecheck. Preserve the typed semantic boundary and avoid English routing rules.
F19. Rejected revision does not revoke an in-flight confirmationpackages/core/src/runtime.ts:1098-1103P1 logic correctness

applyRevision() marks an invalid revision FAILED but leaves pendingCommitRevision and the old intent revision intact. After authority validation completes, commit() checks only cancellation and revision identity at lines 802-806, so it proceeds from the FAILED state.

The test starts confirmation, submits an invalid two-target revision to an arity-one slot, verifies the transaction is FAILED, then releases validation. The old action still invokes once. The newer user instruction was rejected, but the old authorization survived it.

Smallest fix. Revoke the pending commit on every intent-invalidating transition. After every await, require both the original reservation identity and a still-committable transaction state before freezing effects.

Source at reviewed commit

Captured source
1098:     if (!validateProposedSlots(ctx, descriptor, command.slots)) {
1099:       // The user's latest expressed intent is unexecutable. Never silently
1100:       // fall back to the revision they were replacing.
1101:       tx.state = "FAILED";
1102:       touchedTx(ctx, tx);
1103:       return;
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 `packages/core/src/runtime.ts` around lines 1098-1103, `applyRevision()` marks an invalid revision FAILED but leaves `pendingCommitRevision` and the old intent revision intact. After authority validation completes, `commit()` checks only cancellation and revision identity at lines 802-806, so it proceeds from the FAILED state. The test starts confirmation, submits an invalid two-target revision to an arity-one slot, verifies the transaction is FAILED, then releases validation. The old action still invokes once. The newer user instruction was rejected, but the old authorization survived it. Revoke the pending commit on every intent-invalidating transition. After every await, require both the original reservation identity and a still-committable transaction state before freezing effects. Reproduce against PR #1 at 5afb2757db227cd3c50e539d61e53f23b12e8ed7 using `/tmp/circe-pr1-evidence/additional.test.ts`; then add the desired-behavior test to the repository and run the full test suite and typecheck. Preserve the typed semantic boundary and avoid English routing rules.
F21. Ambiguous affirmations still authorize executionpackages/core/src/runtime.ts:1482-1487P1 logic correctness

The new ambiguity guard lives in proposalFromFrame(). The affirmation branch ignores that guard and calls commit() directly. An ambiguous interpretation of a confirmation therefore still carries execution authority.

The reproduction first creates a transaction requiring affirmative confirmation. Its classifier then returns act=affirm, ambiguous=true and confidence=.51. One invocation occurs. The earlier ambiguous-request regression passes because it exercises a different branch.

Smallest fix. Gate every consequence-bearing semantic act on acceptance before entering act-specific handling. Route accepted confirm, revise, cancel and resume commands through the same validated command mechanism as accepted proposals.

Source at reviewed commit

Captured source
1482:       case "affirm": {
1483:         if (event.operations.includes("correction")) {
1484:           applyCorrection(ctx, references);
1485:         } else if (gateTransaction()) {
1486:           const tx = gateTransaction()!;
1487:           await commit(ctx, tx);
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 `packages/core/src/runtime.ts` around lines 1482-1487, The new ambiguity guard lives in `proposalFromFrame()`. The affirmation branch ignores that guard and calls `commit()` directly. An ambiguous interpretation of a confirmation therefore still carries execution authority. The reproduction first creates a transaction requiring affirmative confirmation. Its classifier then returns act=affirm, ambiguous=true and confidence=.51. One invocation occurs. The earlier ambiguous-request regression passes because it exercises a different branch. Gate every consequence-bearing semantic act on acceptance before entering act-specific handling. Route accepted confirm, revise, cancel and resume commands through the same validated command mechanism as accepted proposals. Reproduce against PR #1 at 5afb2757db227cd3c50e539d61e53f23b12e8ed7 using `/tmp/circe-pr1-evidence/additional.test.ts`; then add the desired-behavior test to the repository and run the full test suite and typecheck. Preserve the typed semantic boundary and avoid English routing rules.
F22. Classifier-produced commands bypass the immutable input boundarypackages/core/src/runtime.ts:1681-1685P1 logic data-integrity

The public command() method snapshots and freezes its input, but ingestReserved() calls applyProposal() or applyRevision() directly with the adapter result. The classification object remains owned by the classifier, and nested argument objects are shared through shallow copies.

The test returns a classification with a nested message argument, pauses authority validation, changes that argument through the classifier's retained object, then releases validation. The executor receives the changed message. The snapshot passed into the classifier protects kernel state in one direction; the returned result needs the same treatment.

Smallest fix. Snapshot and validate the adapted command before any application or await. Use a common internal command entry point for typed and semantic inputs so both receive identical isolation and transition checks.

Source at reviewed commit

Captured source
1681:         if (command?.kind === "propose" || command?.kind === "revise") {
1682:           if (command.kind === "revise") {
1683:             await applyRevision(ctx, command);
1684:           } else {
1685:             await applyProposal(ctx, command, references);
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 `packages/core/src/runtime.ts` around lines 1681-1685, The public `command()` method snapshots and freezes its input, but `ingestReserved()` calls `applyProposal()` or `applyRevision()` directly with the adapter result. The classification object remains owned by the classifier, and nested argument objects are shared through shallow copies. The test returns a classification with a nested message argument, pauses authority validation, changes that argument through the classifier's retained object, then releases validation. The executor receives the changed message. The snapshot passed into the classifier protects kernel state in one direction; the returned result needs the same treatment. Snapshot and validate the adapted command before any application or await. Use a common internal command entry point for typed and semantic inputs so both receive identical isolation and transition checks. Reproduce against PR #1 at 5afb2757db227cd3c50e539d61e53f23b12e8ed7 using `/tmp/circe-pr1-evidence/additional.test.ts`; then add the desired-behavior test to the repository and run the full test suite and typecheck. Preserve the typed semantic boundary and avoid English routing rules.
F26. Exclusions check summary refs but execute slot refspackages/perception/src/proposals.ts:82-91P1 logic correctness

Turn exclusion checks compare proposition.refs, while command construction prefers proposition.slots. These can name different entities, and the helper neither validates their consistency nor applies exclusions to the executable bindings.

The test prohibits B, then supplies an action whose summary refs contain A but whose targets slot contains B. The action explicitly relates to the prohibition through excludes. The check passes and the helper emits a proposal for B with valid version evidence. A contradictory model output is sufficient to bypass the prohibition.

Smallest fix. Normalize executable bindings before checking prohibitions. Either reject contradictions between summary refs and slots or define one canonical source of targets, then enforce all turn-level exclusions against every relevant executable binding.

Source at reviewed commit

Captured source
82:     if (relation.kind === "excludes") {
83:       const excluded = new Set(related.refs.map(entityRefKey));
84:       if (proposition.refs.some((ref) => excluded.has(entityRefKey(ref)))) return undefined;
85:     }
86:   }
87:
88:   const slots =
89:     proposition.slots && proposition.slots.length > 0
90:       ? proposition.slots.map((slot) => ({ slot: slot.slot, refs: [...slot.refs] }))
91:       : [{ slot: "targets", refs: [...proposition.refs] }];
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 `packages/perception/src/proposals.ts` around lines 82-91, Turn exclusion checks compare `proposition.refs`, while command construction prefers `proposition.slots`. These can name different entities, and the helper neither validates their consistency nor applies exclusions to the executable bindings. The test prohibits B, then supplies an action whose summary refs contain A but whose targets slot contains B. The action explicitly relates to the prohibition through excludes. The check passes and the helper emits a proposal for B with valid version evidence. A contradictory model output is sufficient to bypass the prohibition. Normalize executable bindings before checking prohibitions. Either reject contradictions between summary refs and slots or define one canonical source of targets, then enforce all turn-level exclusions against every relevant executable binding. Reproduce against PR #1 at 5afb2757db227cd3c50e539d61e53f23b12e8ed7 using `/tmp/circe-pr1-evidence/additional.test.ts`; then add the desired-behavior test to the repository and run the full test suite and typecheck. Preserve the typed semantic boundary and avoid English routing rules.
F25. Semantic turns can borrow evidence from the wrong framepackages/perception/src/proposals.ts:94-96P1 logic data-integrity

propositionCommand() now extracts versions from options.frame, but never checks that this is the frame identified by turn.frameId. An accepted decision set is still just a set of strings; the helper does not establish a connection between those decisions and that frame.

The test builds a turn against frame A@1, creates a later frame A@2, and passes the later frame to the helper. It returns an executable proposal carrying version 2. This recreates the evidence substitution problem even though the old missing-frame test now correctly refuses execution.

Smallest fix. Reject mismatched frame identity before constructing a command. Pass a validated accepted interpretation containing its immutable frame and decision provenance together, and verify the provenance belongs to that frame.

Source at reviewed commit

Captured source
94:   // Presence is meaningful: an absent frame means "no declared read set", so
95:   // the command carries an explicit empty one and cannot execute.
96:   const expectedVersions = expectedVersionsFromFrame(options.frame, slots) ?? [];
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 `packages/perception/src/proposals.ts` around lines 94-96, `propositionCommand()` now extracts versions from `options.frame`, but never checks that this is the frame identified by `turn.frameId`. An accepted decision set is still just a set of strings; the helper does not establish a connection between those decisions and that frame. The test builds a turn against frame A@1, creates a later frame A@2, and passes the later frame to the helper. It returns an executable proposal carrying version 2. This recreates the evidence substitution problem even though the old missing-frame test now correctly refuses execution. Reject mismatched frame identity before constructing a command. Pass a validated accepted interpretation containing its immutable frame and decision provenance together, and verify the provenance belongs to that frame. Reproduce against PR #1 at 5afb2757db227cd3c50e539d61e53f23b12e8ed7 using `/tmp/circe-pr1-evidence/additional.test.ts`; then add the desired-behavior test to the repository and run the full test suite and typecheck. Preserve the typed semantic boundary and avoid English routing rules.
F27. Completed command IDs skip payload conflict detectionpackages/core/src/runtime.ts:449-454P2 logic data-integrity

runReserved() returns a completed cached result before comparing content fingerprints. Payload conflict detection therefore works only while the original command is still in flight.

After command same completes for A, the test resubmits same for B. Instead of rejecting the conflicting payload, the runtime returns A's successful result. B does not execute, but the caller receives success for a different request and the documented identity contract is violated.

Smallest fix. Compute and compare the accepted fingerprint before either completed-result or in-flight lookup. Use the same conflict response for both states and retain tests for identical replay and conflicting replay.

Source at reviewed commit

Captured source
449:     const completed = state.commandResults.get(key);
450:     if (completed) return structuredClone(completed);
451:     const fingerprint = fingerprintOf(content);
452:     const previous = state.commandFingerprints.get(key);
453:     if (previous !== undefined && previous !== fingerprint) {
454:       throw new Error(`command identity ${key} was reused with different content`);
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 `packages/core/src/runtime.ts` around lines 449-454, `runReserved()` returns a completed cached result before comparing content fingerprints. Payload conflict detection therefore works only while the original command is still in flight. After command same completes for A, the test resubmits same for B. Instead of rejecting the conflicting payload, the runtime returns A's successful result. B does not execute, but the caller receives success for a different request and the documented identity contract is violated. Compute and compare the accepted fingerprint before either completed-result or in-flight lookup. Use the same conflict response for both states and retain tests for identical replay and conflicting replay. Reproduce against PR #1 at 5afb2757db227cd3c50e539d61e53f23b12e8ed7 using `/tmp/circe-pr1-evidence/additional.test.ts`; then add the desired-behavior test to the repository and run the full test suite and typecheck. Preserve the typed semantic boundary and avoid English routing rules.
Resolution of the original 18 findings

All 18 narrow reproductions pass. Broader failures discovered in this round have new IDs. F17 remains open for its explicitly deferred recovery work.

  • F1: Concurrent duplicate proposals still execute twice. Original reproduction fixed.
  • F2: Confirming a running transaction creates another effect. Original reproduction fixed.
  • F3: An unknown transaction ID targets unrelated live work. Original reproduction fixed.
  • F4: Revision can resurrect a cancelled transaction. Original reproduction fixed.
  • F5: Mutable command objects can change the target after validation starts. Original reproduction fixed.
  • F6: Effect identities still collide across runtime instances. Original reproduction fixed.
  • F7: Explicitly incomplete evidence silently falls back to newer snapshots. Original reproduction fixed.
  • F8: Semantic proposals lose the versions that supported the decision. Original reproduction fixed.
  • F9: The proposition consumer turns queries into execution proposals. Original reproduction fixed.
  • F10: The classifier-frame path bypasses the ambiguity guard. Original reproduction fixed.
  • F11: Oversized pinned frames are reported as within budget. Original reproduction fixed.
  • F12: The exact-input hash omits coverage metadata. Original reproduction fixed.
  • F13: Late neighborhood judgments capture the new generation at publication. Original reproduction fixed.
  • F14: New obligation events do not invalidate prior judgments. Original reproduction fixed.
  • F15: A rejected retry can leave the old answer in accepted state. Original reproduction fixed.
  • F16: Revision skips slot arity validation. Original reproduction fixed.
  • F17: The journal omits typed commands and their deduplication identities. Recovery deferred; command identity now recorded.
  • F18: Clear-selection without refs leaves the selection active. Original reproduction fixed.
Round 1 full report and original finding bodies, preserved

This is the historical report before PR #1. Its original test counts and verdict describe that earlier snapshot. Open archived round 1.

22 September 2026 · implementation follow-up

Circe corrective pass review

The corrective pass makes real progress, but execution and evidence boundaries still fail concrete counterexamples.

Changes requested

18 actionable findings. 161 repository tests pass; 18 additional required-behavior tests fail.

+3151 / -42068927499f9 + working tree

Changes requested. I would not sign off on the claim that the blockers are eliminated. The typed command path is a useful architectural improvement. Several remaining failures can duplicate effects or execute a target different from the one validated.

Reviewed on 22 September 2026. This review covers the uncommitted corrective pass on top of 68927499f971d9a6302d1cbe2dd98c39a46184d0, including all four new files. The source snapshot manifest identifies the reviewed contents. The original architecture review remains available unchanged.

What the tests establish

pnpm typecheck passes. pnpm test passes all 161 tests across 13 files, including the new 35-test safety suite. I added 18 independent tests asserting required behavior against the new APIs. All 18 fail with assertion failures. These are deterministic, local adapter reproductions; no Jev backend or live executor was involved.

The original 25 defect-assertion tests now fail, but that is insufficient evidence of repair. Several old fixtures no longer supply the required routing adapter, so they never create the transaction under test or fail while accessing an absent result. The new positive tests are stronger evidence. They establish fixes for their covered cases, while the additional tests below disprove the broader guarantees.

Architectural judgment

The project now has a credible route out of the parser. SemanticRuntime.command() accepts a capability directly, and the lexical adapter is outside the kernel. Keep that change. Identity, authoritative observations, bounded context and uncertain semantic decisions remain justified abstractions for this product.

The fragile part is invariant ownership. Proposal validates arity but revision does not. Confirmation protects a pending validation but permits an already running effect to be frozen again. One semantic helper checks acceptance while classifier ingestion can execute ambiguous input. These are signs that the same safety contract is being reimplemented along separate paths.

The next pass should consolidate those contracts. One immutable command boundary should own command identity and transaction selection. One transition function should decide whether the current state permits the action. One intent validator should cover proposal, revision and precommit. The accepted interpretation should carry the evidence that actually supported it all the way to authority validation. Additional flags at individual call sites will leave more bypasses.

What is genuinely better

  • Capability selection can bypass raw tokens through typed commands.
  • Null backend output produces structured failure, and score validation checks more of the distribution contract.
  • Accepted-value accessors protect ordinary dependent workflow stages.
  • Pins survive the seed cap, and provider truncation plus per-candidate omissions are exposed.
  • Frames carry more of the stored assertions, observations, relation evidence, event participants and scoped history.
  • Observation deduplication and several observation/relation invalidation cases now work.
  • Thrown invocation becomes UNKNOWN with a receipt, and in-process reconciliation avoids a blind retry.
  • Detached state snapshots and delimiter-safe entity identity address real boundary problems.

Readiness and work sequence

  1. Close the effect boundary first. Fix concurrent command deduplication, lifecycle transitions, explicit transaction targeting, immutable inputs, evidence coverage and revision validation. Give effect IDs a real interaction namespace. Require one effect set per intent revision and prove it under delayed validation and delayed invocation.
  2. Make semantic evidence survive consumption. Use one accepted-turn consumer that validates speech act, operation, prohibitions and dependencies. Reject unsupported structures. Carry exact frame versions and captured knowledge generations. Fix frame size reporting, exact request hashing, invalidation and reused decision IDs before treating model evaluations as reliable evidence.
  3. Then connect Jev in a read-only evaluation path. A small isolated SDK exploration can start now with effects disabled. Keep the semantic corpus RED until a real interpreter is wired in; do not expand English rules to make it green. Effect-driving integration should wait for the first two steps. Add held-out and metamorphic cases that cover the whole turn, including exclusions and changing evidence.

A disk-backed journal and calibrated risk thresholds can remain deferred for an isolated read-only experiment. Before durability claims, repair the journal's command/transition schema and recovery contract, then choose a persistence adapter and exercise crash boundaries. A persistent implementation cannot reconstruct information the journal never records.

Remaining end-to-end gap

For "Leave the sketchy auth one alone and continue the other one," the adapter now refuses the mixed clause instead of guessing a capability. That is appropriate containment for bootstrap code. Context assembly can expose relevant evidence, and typed commands can reach the kernel. But there is still no real semantic producer for this utterance, and propositionCommand() processes one proposition while ignoring its act, operation and cross-proposition relations. The full accepted-turn-to-safe-effect path is unfinished.

The bounded entity query API is useful, but it does not itself prove scalable retrieval. The in-memory implementation still gathers and sorts entities. Benchmark realistic world sizes before making retrieval performance claims. This review's new tests establish correctness failures, not scale or model calibration.

Evidence and reproduction

The tests and configuration retain the local paths used during review. Run from /home/abstergo/Programming/Circe:

pnpm exec vitest run --config /tmp/circe-safety-pass-review/vitest.config.mjs --reporter=verbose

If reproducing elsewhere, update the config's root and package aliases plus the test-frame import. Desired behavior is encoded as the assertion; failures identify remaining defects. Source snippets below refer to the captured uncommitted files. No Circe source was edited for this review.

18 tests Test config Failure output Baseline output Typecheck Source manifest Review sidecar Markdown report Old counterexample output

Walkthrough and changed files

The pass introduces typed kernel commands, moves lexical routing into an adapter, adds evidence coverage checks and a journal port, and strengthens frame assembly and perception validation. The 35 new safety tests exercise useful required behavior. The remaining failures cluster around rules implemented separately in multiple entry points.

  • Typed commands now provide a real path around lexicon-based capability routing.
  • Proposal, revision, confirmation and classifier ingestion still apply different safety rules.
  • A frame ID and accepted decision IDs do not yet carry the exact evidence needed to authorize an effect.
  • Recovery remains an in-process mechanism; the journal contract does not yet support reconstructing commands and their outcomes.
FileChange
packages/context/src/assembly.ts+205 / -87
packages/context/src/metrics.ts+4 / -0
packages/context/src/seeds.ts+55 / -32
packages/context/src/store.ts+187 / -10
packages/context/src/types.ts+81 / -1
packages/core/src/identity.ts+58 / -2
packages/core/src/index.ts+1 / -0
packages/core/src/runtime.ts+955 / -242
packages/core/src/types.ts+117 / -1
packages/evals/src/deps.ts+2 / -1
packages/evals/src/expectations.ts+2 / -3
packages/evals/src/perception-workflow.test.ts+8 / -4
packages/evals/src/policy.test.ts+5 / -0
packages/evals/src/replay.ts+3 / -3
packages/evals/src/test-frame.ts+5 / -0
packages/perception/src/index.ts+1 / -0
packages/perception/src/orchestrator.ts+148 / -17
packages/perception/src/policy.ts+29 / -0
packages/perception/src/trace.ts+3 / -0
packages/perception/src/types.ts+13 / -0
packages/perception/src/validation.ts+251 / -17
packages/resolvers-deterministic/src/index.ts+1 / -0
packages/core/src/journal.ts+19 / -0
packages/evals/src/safety-fixes.test.ts+848 / -0
packages/perception/src/proposals.ts+36 / -0
packages/resolvers-deterministic/src/adapter.ts+114 / -0
Boundary that needs to hold

Input snapshot → reserved command identity → exact transaction → valid state transition → complete intent and evidence → one frozen effect set → dispatch / reconcile.

Semantic interpretation must supply its original read set at the input boundary.

Issues index
  1. Concurrent duplicate proposals still execute twice P1
  2. Confirming a running transaction creates another effect P1
  3. An unknown transaction ID targets unrelated live work P1
  4. Revision can resurrect a cancelled transaction P1
  5. Mutable command objects can change the target after validation starts P1
  6. Effect identities still collide across runtime instances P1
  7. Explicitly incomplete evidence silently falls back to newer snapshots P1
  8. Semantic proposals lose the versions that supported the decision P1
  9. The proposition consumer turns queries into execution proposals P1
  10. The classifier-frame path bypasses the ambiguity guard P1
  11. Oversized pinned frames are reported as within budget P1
  12. The exact-input hash omits coverage metadata P1
  13. Late neighborhood judgments capture the new generation at publication P1
  14. New obligation events do not invalidate prior judgments P1
  15. A rejected retry can leave the old answer in accepted state P1
  16. Revision skips slot arity validation P1
  17. The journal omits typed commands and their deduplication identities P2
  18. Clear-selection without refs leaves the selection active P2
Prompt for all review findings · Ctrl+`
Prompt to rerun review · Ctrl+Space
F12. The exact-input hash omits coverage metadatapackages/context/src/assembly.ts:535-553P1 logic data-integrity

The hash payload is built before completeness and excludes it. It also differs from the full ContextFrame passed to the backend. The capability-order case is fixed, but exact-input provenance is not.

Two providers return identical seeds and different truncated flags. The resulting frames have different completeness and can trigger different acceptance policy, yet their hashes are equal. With the same store version, their frame IDs also coincide; recordFrame treats the second as the already stored first frame because only hashes are compared.

Smallest fix. Define and persist one canonical model request, including coverage and relevant policy metadata, then hash that representation. Keep semantic cache keys separate from exact request identity.

Gate: Before model evaluation

Captured source
535:   const buildPayload = (candidates: ContextCandidate[]) => ({
536:     interactionId: request.interactionId,
537:     observationId: request.observationId,
538:     utterance: request.utterance,
539:     interaction: request.interaction,
540:     policy: policy.version,
541:     expansionLevel: level,
542:     // Capability order is caller order: the hash must cover the exact bytes
543:     // the backend will receive, not a canonicalized subset.
544:     capabilities: [...(request.capabilities ?? [])].map((capability) => ({
545:       capabilityId: capability.capabilityId,
546:       description: capability.description,
547:       semanticEffects: capability.semanticEffects,
548:       slots: capability.slots,
549:     })),
550:     candidates: candidates.map((candidate) => ({
551:       ...candidate,
552:       reasons: [...candidate.reasons].sort(),
553:     })),
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 `packages/context/src/assembly.ts` around lines 535-553, The hash payload is built before completeness and excludes it. It also differs from the full ContextFrame passed to the backend. The capability-order case is fixed, but exact-input provenance is not. Two providers return identical seeds and different truncated flags. The resulting frames have different completeness and can trigger different acceptance policy, yet their hashes are equal. With the same store version, their frame IDs also coincide; recordFrame treats the second as the already stored first frame because only hashes are compared. Define and persist one canonical model request, including coverage and relevant policy metadata, then hash that representation. Keep semantic cache keys separate from exact request identity. Use the corresponding desired-behavior reproduction in `/tmp/circe-safety-pass-review/required.test.ts`, then retain it in the appropriate repository suite and run typecheck and the full baseline. Preserve the typed kernel boundary and do not add English routing rules.
F11. Oversized pinned frames are reported as within budgetpackages/context/src/assembly.ts:558-590P1 logic correctness

frameSizeExceeded becomes true only after a soft candidate is removed. If all remaining candidates are pinned, dropIndex < 0 breaks the loop while leaving the flag false. The size calculation also counts JavaScript code units rather than UTF-8 bytes.

One explicitly pinned candidate with a 300,000-character attribute exceeds the default 262,144 budget, remains fully included, and reports frameSizeExceeded=false. This can send an oversized request without the promised explicit failure signal.

Smallest fix. Calculate whether the final complete serialized request exceeds the budget independently of whether anything was dropped. Preserve pins but return an explicit unusable/needs-expansion outcome. Measure the documented byte unit.

Gate: Before model evaluation

Captured source
558:   let frameSizeExceeded = false;
559:   const maxFrameBytes = budget.maxFrameBytes;
560:   while (stableStringify(payload).length > maxFrameBytes) {
561:     // Drop the weakest soft candidate first; required (pinned) context is
562:     // never silently discarded, it is reported instead.
563:     let dropIndex = -1;
564:     let dropScore = Number.POSITIVE_INFINITY;
565:     candidates.forEach((candidate, index) => {
566:       const key = entityRefKey(candidate.ref);
567:       if (seedClasses.get(key) === "pinned") return;
568:       const score = scores.get(key) ?? 0;
569:       if (score < dropScore) {
570:         dropScore = score;
571:         dropIndex = index;
572:       }
573:     });
574:     if (dropIndex < 0) break;
575:     candidates.splice(dropIndex, 1);
576:     payload = buildPayload(candidates);
577:     frameSizeExceeded = true;
578:   }
579:
580:   const completeness: AssemblyCompleteness = {
581:     seedTruncated: seedCollection.truncated,
582:     traversalTruncated,
583:     candidateTruncated: selectedNonPinned.length < nonPinned.length || frameSizeExceeded,
584:     evidenceTruncated: candidates.some(
585:       (candidate) =>
586:         candidate.omitted.relations > 0 ||
587:         candidate.omitted.events > 0 ||
588:         candidate.omitted.annotations > 0,
589:     ),
590:     frameSizeExceeded,
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 `packages/context/src/assembly.ts` around lines 558-590, `frameSizeExceeded` becomes true only after a soft candidate is removed. If all remaining candidates are pinned, `dropIndex < 0` breaks the loop while leaving the flag false. The size calculation also counts JavaScript code units rather than UTF-8 bytes. One explicitly pinned candidate with a 300,000-character attribute exceeds the default 262,144 budget, remains fully included, and reports frameSizeExceeded=false. This can send an oversized request without the promised explicit failure signal. Calculate whether the final complete serialized request exceeds the budget independently of whether anything was dropped. Preserve pins but return an explicit unusable/needs-expansion outcome. Measure the documented byte unit. Use the corresponding desired-behavior reproduction in `/tmp/circe-safety-pass-review/required.test.ts`, then retain it in the appropriate repository suite and run typecheck and the full baseline. Preserve the typed kernel boundary and do not add English routing rules.
F14. New obligation events do not invalidate prior judgmentspackages/context/src/store.ts:255-263P1 logic data-integrity

Knowledge generations advance on observation and relation changes, but recordEvent() only appends and bumps the store version. Existing beliefs depending on a subject generation remain active.

The test creates a belief from current knowledge and then records a new obligation-opened event for the same entity. The belief remains valid. This directly affects questions such as 'which one needs me?' where obligations are material evidence.

Smallest fix. Declare and advance event/obligation read generations, or a broader subject generation where appropriate. Test event additions, resolutions, removals and temporal expiry against dependent beliefs.

Gate: Before reusable semantic beliefs

Captured source
255:   recordEvent(input: Omit<TemporalEventRecord, "id">): EventRecordId {
256:     const id = `event:${++this.counters.event}`;
257:     const record = snapshot({ ...input, id } as TemporalEventRecord);
258:     this.eventRecords.set(id, record);
259:     for (const ref of record.refs) this.push(this.eventsByRef, entityRefKey(ref), id);
260:     this.bump();
261:     return id;
262:   }
263: 
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 `packages/context/src/store.ts` around lines 255-263, Knowledge generations advance on observation and relation changes, but `recordEvent()` only appends and bumps the store version. Existing beliefs depending on a subject generation remain active. The test creates a belief from current knowledge and then records a new obligation-opened event for the same entity. The belief remains valid. This directly affects questions such as 'which one needs me?' where obligations are material evidence. Declare and advance event/obligation read generations, or a broader subject generation where appropriate. Test event additions, resolutions, removals and temporal expiry against dependent beliefs. Use the corresponding desired-behavior reproduction in `/tmp/circe-safety-pass-review/required.test.ts`, then retain it in the appropriate repository suite and run typecheck and the full baseline. Preserve the typed kernel boundary and do not add English routing rules.
F13. Late neighborhood judgments capture the new generation at publicationpackages/context/src/store.ts:278-290P1 logic data-integrity

Annotations implicitly depend on each subject's current generation at insertion time, not the generation read during inference. Only explicit dependsOn values are checked for staleness. The helper does not derive the read generations from the recorded context frame.

The test assembles a frame, adds a new blockedBy relation while its interpretation is outstanding, then inserts an annotation depending on the unchanged observation. It remains active because the new generation is attached after the change. Capturing the old generation explicitly can avoid this, but the default claimed guarantee does not.

Smallest fix. Capture generations in the interpretation's read set and require them when publishing a belief. Validate those captured values atomically; do not infer freshness from the publication-time generation.

Gate: Before reusable semantic beliefs

Captured source
278:   addAnnotation(input: Omit<SemanticAnnotation, "id">): AnnotationId {
279:     const id = `annotation:${++this.counters.annotation}`;
280:     const record = snapshot({ ...input, id } as SemanticAnnotation);
281:     this.annotationRecords.set(id, record);
282:     for (const ref of record.refs) this.push(this.annotationsByRef, entityRefKey(ref), id);
283:     const generations = record.refs.map((ref) => this.currentGeneration(ref));
284:     this.linkDependencies(id, [...record.dependsOn, ...generations]);
285:     // A result published after its evidence changed is stale on arrival; keep
286:     // it as history but never let it masquerade as current.
287:     if (!this.dependenciesFresh(record.dependsOn)) {
288:       this.annotationRecords.set(id, snapshot({ ...record, invalidatedAt: record.createdAt }));
289:     }
290:     this.bump();
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 `packages/context/src/store.ts` around lines 278-290, Annotations implicitly depend on each subject's current generation at insertion time, not the generation read during inference. Only explicit dependsOn values are checked for staleness. The helper does not derive the read generations from the recorded context frame. The test assembles a frame, adds a new blockedBy relation while its interpretation is outstanding, then inserts an annotation depending on the unchanged observation. It remains active because the new generation is attached after the change. Capturing the old generation explicitly can avoid this, but the default claimed guarantee does not. Capture generations in the interpretation's read set and require them when publishing a belief. Validate those captured values atomically; do not infer freshness from the publication-time generation. Use the corresponding desired-behavior reproduction in `/tmp/circe-safety-pass-review/required.test.ts`, then retain it in the appropriate repository suite and run typecheck and the full baseline. Preserve the typed kernel boundary and do not add English routing rules.
F6. Effect identities still collide across runtime instancespackages/core/src/runtime.ts:185-191P1 logic data-integrity

nextEffectId() includes state.interactionId, but every runtime initializes that value to the literal interaction:1. There is no RuntimeDeps option to assign a distinct interaction identity.

Two independently created runtimes both send effect:interaction:1:1 as their first idempotency key. A shared idempotent executor can suppress legitimate work or return another operation's result. Restart also resets the counters.

Smallest fix. Require an injected, stable interaction identity and persist operation identity allocation. Test two concurrent interactions against the same executor and recovery against an existing journal.

Gate: Before shared executors

Captured source
185:
186: export function createRuntime(deps: RuntimeDeps): SemanticRuntime {
187:   const clock = deps.clock ?? (() => new Date().toISOString());
188:   const state: MutableState = {
189:     interactionId: "interaction:1",
190:     scopes: new Map(),
191:     scopeStack: [],
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 `packages/core/src/runtime.ts` around lines 185-191, `nextEffectId()` includes `state.interactionId`, but every runtime initializes that value to the literal `interaction:1`. There is no RuntimeDeps option to assign a distinct interaction identity. Two independently created runtimes both send `effect:interaction:1:1` as their first idempotency key. A shared idempotent executor can suppress legitimate work or return another operation's result. Restart also resets the counters. Require an injected, stable interaction identity and persist operation identity allocation. Test two concurrent interactions against the same executor and recovery against an existing journal. Use the corresponding desired-behavior reproduction in `/tmp/circe-safety-pass-review/required.test.ts`, then retain it in the appropriate repository suite and run typecheck and the full baseline. Preserve the typed kernel boundary and do not add English routing rules.
F5. Mutable command objects can change the target after validation startspackages/core/src/runtime.ts:289-309P1 logic data-integrity

makeSlotBinding() copies arrays and precondition wrappers but shares nested refs and tokens with the caller. Arguments and other proposal fields also receive only shallow copies.

The test starts validation of A, changes command.slots[0].refs[0].id to B, then releases validation. The adapter recorded A as the checked target, but the executor receives B. Returning a detached state() snapshot does not protect this input boundary.

Smallest fix. Snapshot and validate the entire incoming command before storing it or awaiting anything. Freeze immutable intent/effect inputs and avoid sharing mutable nested objects with caller or adapter code.

Gate: Before effects

Captured source
289:   function makeSlotBinding(
290:     slot: string,
291:     refs: EntityRef[],
292:     references: ResolvedReference[],
293:     now: string,
294:     expectations?: FrozenPrecondition[],
295:     origins?: SlotBinding["origins"],
296:   ): SlotBinding {
297:     let frozen: FrozenPrecondition[];
298:     if (expectations && expectations.length > 0) {
299:       frozen = expectations.map((precondition) => ({ ...precondition }));
300:     } else {
301:       const observed = latestSnapshots(refs);
302:       frozen = [];
303:       for (const ref of refs) {
304:         const snapshot = observed.get(entityRefKey(ref));
305:         if (snapshot && snapshot.version.authority === ref.authority) {
306:           frozen.push({ ref, token: snapshot.version });
307:         }
308:       }
309:     }
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 `packages/core/src/runtime.ts` around lines 289-309, `makeSlotBinding()` copies arrays and precondition wrappers but shares nested refs and tokens with the caller. Arguments and other proposal fields also receive only shallow copies. The test starts validation of A, changes `command.slots[0].refs[0].id` to B, then releases validation. The adapter recorded A as the checked target, but the executor receives B. Returning a detached `state()` snapshot does not protect this input boundary. Snapshot and validate the entire incoming command before storing it or awaiting anything. Freeze immutable intent/effect inputs and avoid sharing mutable nested objects with caller or adapter code. Use the corresponding desired-behavior reproduction in `/tmp/circe-safety-pass-review/required.test.ts`, then retain it in the appropriate repository suite and run typecheck and the full baseline. Preserve the typed kernel boundary and do not add English routing rules.
F2. Confirming a running transaction creates another effectpackages/core/src/runtime.ts:480-486P1 logic correctness

commit() rejects terminal states and an active validation, but allows COMMITTED and DISPATCHED transactions. The pending revision flag is cleared before invocation completes, and existing frozen effects are not checked.

After an executor returns RUNNING, a typed confirmation with a new command ID for the same transaction and revision validates, freezes and invokes again. The reproduction records two invokes for one intent revision.

Smallest fix. Restrict confirmation to explicitly committable states and atomically associate one frozen effect set with an intent revision. Repeated confirmation of that revision must never create another set.

Gate: Before effects

Captured source
480:   async function commit(ctx: TurnCtx, tx: Transaction): Promise<void> {
481:     const intent = state.intents.get(tx.intentId);
482:     if (!intent) return;
483:     if (isTerminal(tx)) return;
484:     // A commit for this revision is already in flight: a duplicate
485:     // confirmation must not start a second authority validation.
486:     if (tx.pendingCommitRevision !== undefined) return;
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 `packages/core/src/runtime.ts` around lines 480-486, `commit()` rejects terminal states and an active validation, but allows COMMITTED and DISPATCHED transactions. The pending revision flag is cleared before invocation completes, and existing frozen effects are not checked. After an executor returns RUNNING, a typed confirmation with a new command ID for the same transaction and revision validates, freezes and invokes again. The reproduction records two invokes for one intent revision. Restrict confirmation to explicitly committable states and atomically associate one frozen effect set with an intent revision. Repeated confirmation of that revision must never create another set. Use the corresponding desired-behavior reproduction in `/tmp/circe-safety-pass-review/required.test.ts`, then retain it in the appropriate repository suite and run typecheck and the full baseline. Preserve the typed kernel boundary and do not add English routing rules.
F7. Explicitly incomplete evidence silently falls back to newer snapshotspackages/core/src/runtime.ts:801-820P1 logic correctness

applyProposal() filters supplied expectedVersions for each slot, then passes an empty array to makeSlotBinding(). That helper treats an empty array as absent and fills in versions from current kernel views. This contradicts the comment saying incomplete declared evidence will be rejected.

The test supplies evidence for an unrelated ref while A exists in a version-2 view. Instead of rejecting missing evidence for A, the kernel validates A at version 2 and invokes.

Smallest fix. Distinguish absent evidence from explicitly supplied empty or non-covering evidence. Never fill holes in a supplied read set with current snapshots; reject incomplete coverage.

Gate: Before effects

Captured source
801:       // Evidence the interpretation declared wins over a kernel snapshot: if
802:       // expected versions were supplied but do not cover a ref, that ref has
803:       // incomplete evidence and commit will refuse it.
804:       const declaredEvidence =
805:         proposed.expectations && proposed.expectations.length > 0
806:           ? proposed.expectations
807:           : command.expectedVersions && command.expectedVersions.length > 0
808:             ? command.expectedVersions.filter((precondition) =>
809:                 proposed.refs.some((ref) => entityRefKey(ref) === entityRefKey(precondition.ref)),
810:               )
811:             : undefined;
812:       return makeSlotBinding(
813:         proposed.slot,
814:         proposed.refs,
815:         references,
816:         ctx.now,
817:         declaredEvidence,
818:         proposed.origins,
819:       );
820:     });
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 `packages/core/src/runtime.ts` around lines 801-820, `applyProposal()` filters supplied expectedVersions for each slot, then passes an empty array to `makeSlotBinding()`. That helper treats an empty array as absent and fills in versions from current kernel views. This contradicts the comment saying incomplete declared evidence will be rejected. The test supplies evidence for an unrelated ref while A exists in a version-2 view. Instead of rejecting missing evidence for A, the kernel validates A at version 2 and invokes. Distinguish absent evidence from explicitly supplied empty or non-covering evidence. Never fill holes in a supplied read set with current snapshots; reject incomplete coverage. Use the corresponding desired-behavior reproduction in `/tmp/circe-safety-pass-review/required.test.ts`, then retain it in the appropriate repository suite and run typecheck and the full baseline. Preserve the typed kernel boundary and do not add English routing rules.
F4. Revision can resurrect a cancelled transactionpackages/core/src/runtime.ts:861-888P1 logic correctness

applyRevision() rejects transactions with effects but never rejects a terminal transaction addressed by ID. Cancellation before dispatch leaves no effects, so the revision is accepted and replaces CANCELLED with a new gate state.

The test cancels a transaction, revises its unchanged revision by explicit ID, then confirms revision 2. The cancelled action executes. This violates the transaction lifecycle even without concurrency.

Smallest fix. Validate the transition state before applying a revision. Terminal transactions stay terminal; further work must create an explicit new intent.

Gate: Before effects

Captured source
861:   async function applyRevision(ctx: TurnCtx, command: ReviseCommand): Promise<void> {
862:     const tx = transactionById(command.transactionId) ?? liveTransaction();
863:     if (!tx) {
864:       ctx.errors.push("STALE_INTENT");
865:       ctx.reply = "none";
866:       return;
867:     }
868:     if (tx.effects.length > 0) {
869:       // frozen effects are immutable; a revision after dispatch is a new intent
870:       ctx.errors.push("STALE_INTENT");
871:       ctx.reply = "none";
872:       return;
873:     }
874:     const intent = state.intents.get(tx.intentId);
875:     if (!intent) {
876:       ctx.errors.push("STALE_INTENT");
877:       ctx.reply = "none";
878:       return;
879:     }
880:     if (intent.revision !== command.expectedIntentRevision) {
881:       ctx.errors.push("STALE_INTENT");
882:       ctx.reply = "none";
883:       return;
884:     }
885:     const descriptor = descriptorFor(intent.capability);
886:     if (!descriptor) {
887:       ctx.errors.push("UNSUPPORTED_CAPABILITY");
888:       ctx.reply = "question";
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 `packages/core/src/runtime.ts` around lines 861-888, `applyRevision()` rejects transactions with effects but never rejects a terminal transaction addressed by ID. Cancellation before dispatch leaves no effects, so the revision is accepted and replaces CANCELLED with a new gate state. The test cancels a transaction, revises its unchanged revision by explicit ID, then confirms revision 2. The cancelled action executes. This violates the transaction lifecycle even without concurrency. Validate the transition state before applying a revision. Terminal transactions stay terminal; further work must create an explicit new intent. Use the corresponding desired-behavior reproduction in `/tmp/circe-safety-pass-review/required.test.ts`, then retain it in the appropriate repository suite and run typecheck and the full baseline. Preserve the typed kernel boundary and do not add English routing rules.
F16. Revision skips slot arity validationpackages/core/src/runtime.ts:890-908P1 logic correctness

applyProposal() checks arity-one slots, but applyRevision() checks only that slot names exist. missingRequired() and commit do not enforce arity later.

The test creates a one-target intent, revises the targets slot to contain A and B, then confirms. Both targets reach execution despite the descriptor allowing one.

Smallest fix. Use a shared intent/binding validator on proposal, revision and precommit. Validate arity, duplicate slot names, required values and supported argument schemas consistently.

Gate: Before effects

Captured source
890:     }
891:     const declared = new Map(descriptor.slots.map((slot) => [slot.name, slot]));
892:     for (const proposed of command.slots) {
893:       if (!declared.has(proposed.slot)) {
894:         ctx.errors.push("INVALID_SLOT");
895:         ctx.reply = "question";
896:         return;
897:       }
898:     }
899:     const replaced = new Set(command.slots.map((slot) => slot.slot));
900:     const bindings = [
901:       ...intent.bindings.filter((binding) => !replaced.has(binding.slot)),
902:       ...command.slots.map((proposed) =>
903:         makeSlotBinding(proposed.slot, proposed.refs, [], ctx.now, proposed.expectations, proposed.origins),
904:       ),
905:     ];
906:     const updated: Intent = {
907:       ...intent,
908:       revision: intent.revision + 1,
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 `packages/core/src/runtime.ts` around lines 890-908, `applyProposal()` checks arity-one slots, but `applyRevision()` checks only that slot names exist. `missingRequired()` and commit do not enforce arity later. The test creates a one-target intent, revises the targets slot to contain A and B, then confirms. Both targets reach execution despite the descriptor allowing one. Use a shared intent/binding validator on proposal, revision and precommit. Validate arity, duplicate slot names, required values and supported argument schemas consistently. Use the corresponding desired-behavior reproduction in `/tmp/circe-safety-pass-review/required.test.ts`, then retain it in the appropriate repository suite and run typecheck and the full baseline. Preserve the typed kernel boundary and do not add English routing rules.
F10. The classifier-frame path bypasses the ambiguity guardpackages/core/src/runtime.ts:1053-1089P1 logic correctness

proposalFromFrame() returns a proposal even when classification.ambiguous is true. It adds pendingCapability as extra metadata, but request handling executes the proposal first. This path bypasses the accepted-decision guard in the separate proposition helper.

The test supplies an ambiguous classification with capability, slots and complete version evidence. ingest() invokes the executor. This reproduces an actual effect path, not merely an ambiguous answer being exposed to a later stage.

Smallest fix. Reject ambiguous classifications before building or applying executable commands. Prefer one validated semantic-command path so frame-based ingestion cannot bypass the acceptance rules.

Gate: Before semantic ingest

Captured source
1053:   function proposalFromFrame(
1054:     classification: ClassificationResult,
1055:     observation: Observation,
1056:   ): AdaptedClassification | undefined {
1057:     const frame = classification.frame as SemanticFrame;
1058:     if (typeof frame.capability !== "string" || frame.capability.length === 0) return undefined;
1059:     const descriptor = descriptorFor(frame.capability);
1060:     const slot = descriptor ? targetSlotFor(descriptor) : "targets";
1061:     let slots = parseProposedSlots(frame.slots);
1062:     if (!slots) {
1063:       slots = [];
1064:     }
1065:     if (slots.length === 0) {
1066:       const refs = targetRefsFrom(classification.references);
1067:       if (refs.length > 0) slots = [{ slot, refs }];
1068:     }
1069:     const expectedVersions = parseExpectedVersions(frame.expectedVersions);
1070:     const command: ProposeCommand = {
1071:       kind: "propose",
1072:       commandId: observation.id,
1073:       observationId: observation.id,
1074:       capability: frame.capability,
1075:       slots,
1076:       ...(isRecord(frame.arguments) ? { arguments: { ...frame.arguments } } : {}),
1077:       ...(Array.isArray(frame.decisions)
1078:         ? { decisions: frame.decisions.filter((decision): decision is string => typeof decision === "string") }
1079:         : {}),
1080:       ...(expectedVersions ? { expectedVersions } : {}),
1081:       ...(typeof frame.expectedIntentRevision === "number"
1082:         ? { expectedIntentRevision: frame.expectedIntentRevision }
1083:         : {}),
1084:       provenance: classification.provenance,
1085:     };
1086:     return {
1087:       command,
1088:       ...(classification.ambiguous ? { pendingCapability: frame.capability } : {}),
1089:     };
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 `packages/core/src/runtime.ts` around lines 1053-1089, `proposalFromFrame()` returns a proposal even when `classification.ambiguous` is true. It adds pendingCapability as extra metadata, but request handling executes the proposal first. This path bypasses the accepted-decision guard in the separate proposition helper. The test supplies an ambiguous classification with capability, slots and complete version evidence. `ingest()` invokes the executor. This reproduces an actual effect path, not merely an ambiguous answer being exposed to a later stage. Reject ambiguous classifications before building or applying executable commands. Prefer one validated semantic-command path so frame-based ingestion cannot bypass the acceptance rules. Use the corresponding desired-behavior reproduction in `/tmp/circe-safety-pass-review/required.test.ts`, then retain it in the appropriate repository suite and run typecheck and the full baseline. Preserve the typed kernel boundary and do not add English routing rules.
F1. Concurrent duplicate proposals still execute twicepackages/core/src/runtime.ts:1478-1484P1 logic correctness

command() checks only completed commandResults; it does not reserve an in-flight command ID. Two deliveries of the same proposal each create a transaction before either finishes. The per-transaction commit flag cannot deduplicate them.

The required-behavior test submits the same command twice while validation is deferred. Both validations complete and two invokes occur. Sequential duplicate delivery is fixed, but concurrent delivery remains unsafe.

Smallest fix. Reserve a command identity before the first await and share its in-flight result. Check that repeats have identical content; clear or record failures under a defined retry policy.

Gate: Before effects

Captured source
1478:   async function command(command: KernelCommand): Promise<TurnResult> {
1479:     const cached = state.commandResults.get(command.commandId);
1480:     if (cached) return structuredClone(cached);
1481:     const ctx = newCtx(clock(), command.commandId, command.commandId);
1482:     pruneScopes(ctx.now);
1483:     switch (command.kind) {
1484:       case "propose": {
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 `packages/core/src/runtime.ts` around lines 1478-1484, `command()` checks only completed `commandResults`; it does not reserve an in-flight command ID. Two deliveries of the same proposal each create a transaction before either finishes. The per-transaction commit flag cannot deduplicate them. The required-behavior test submits the same command twice while validation is deferred. Both validations complete and two invokes occur. Sequential duplicate delivery is fixed, but concurrent delivery remains unsafe. Reserve a command identity before the first await and share its in-flight result. Check that repeats have identical content; clear or record failures under a defined retry policy. Use the corresponding desired-behavior reproduction in `/tmp/circe-safety-pass-review/required.test.ts`, then retain it in the appropriate repository suite and run typecheck and the full baseline. Preserve the typed kernel boundary and do not add English routing rules.
F3. An unknown transaction ID targets unrelated live workpackages/core/src/runtime.ts:1491-1498P1 logic correctness

transactionById(command.transactionId) ?? gateTransaction() ?? liveTransaction() treats an explicit but missing ID as if no ID was supplied. Revision, cancellation and resumption have similar fallback behavior.

The reproduction creates one pending transaction, then confirms tx:missing. The existing transaction executes. A stale or mistyped reference therefore authorizes different work, contrary to the no-substitution rule.

Smallest fix. Use fallback selection only when the command omits transactionId. Reject any explicit ID that is missing, and apply the same rule consistently to confirm, revise, cancel and resume.

Gate: Before effects

Captured source
1491:       }
1492:       case "confirm": {
1493:         const tx = transactionById(command.transactionId) ?? gateTransaction() ?? liveTransaction();
1494:         if (!tx) {
1495:           ctx.reply = "none";
1496:           break;
1497:         }
1498:         const intent = state.intents.get(tx.intentId);
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 `packages/core/src/runtime.ts` around lines 1491-1498, `transactionById(command.transactionId) ?? gateTransaction() ?? liveTransaction()` treats an explicit but missing ID as if no ID was supplied. Revision, cancellation and resumption have similar fallback behavior. The reproduction creates one pending transaction, then confirms `tx:missing`. The existing transaction executes. A stale or mistyped reference therefore authorizes different work, contrary to the no-substitution rule. Use fallback selection only when the command omits transactionId. Reject any explicit ID that is missing, and apply the same rule consistently to confirm, revise, cancel and resume. Use the corresponding desired-behavior reproduction in `/tmp/circe-safety-pass-review/required.test.ts`, then retain it in the appropriate repository suite and run typecheck and the full baseline. Preserve the typed kernel boundary and do not add English routing rules.
F15. A rejected retry can leave the old answer in accepted statepackages/perception/src/orchestrator.ts:331-335P1 logic correctness

Spec IDs are checked for duplicates only inside each stage. If a later stage reuses an ID, raw answers and outcomes are replaced, but accepted is updated only for auto-bind and never cleared when the new answer is rejected.

The test asks spec x twice. The first result is accepted at .99; the second requires clarification at .55. choiceOf(state, 'x') still returns the first accepted answer. The aggregate run stays AMBIGUOUS, but accepted-value access and further dependencies contradict the latest outcome.

Smallest fix. Either reject reused spec IDs across the workflow or define explicit attempts and clear/recompute accepted values and dependent decisions when an attempt changes the outcome.

Gate: Before model evaluation

Captured source
331:       state.answers.set(spec.id, answer);
332:       if (outcome.bindingAction === "auto-bind") state.accepted.set(spec.id, answer);
333:       state.outcomes.set(spec.id, outcome);
334:       state.traces.push(trace);
335:       answers.push(answer);
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 `packages/perception/src/orchestrator.ts` around lines 331-335, Spec IDs are checked for duplicates only inside each stage. If a later stage reuses an ID, raw answers and outcomes are replaced, but accepted is updated only for auto-bind and never cleared when the new answer is rejected. The test asks spec x twice. The first result is accepted at .99; the second requires clarification at .55. `choiceOf(state, 'x')` still returns the first accepted answer. The aggregate run stays AMBIGUOUS, but accepted-value access and further dependencies contradict the latest outcome. Either reject reused spec IDs across the workflow or define explicit attempts and clear/recompute accepted values and dependent decisions when an attempt changes the outcome. Use the corresponding desired-behavior reproduction in `/tmp/circe-safety-pass-review/required.test.ts`, then retain it in the appropriate repository suite and run typecheck and the full baseline. Preserve the typed kernel boundary and do not add English routing rules.
F9. The proposition consumer turns queries into execution proposalspackages/perception/src/proposals.ts:14-25P1 logic correctness

The new consumer only checks turn status, a capability, accepted decision IDs and include polarity. It ignores proposition.act, operation and relations, and always returns kind propose. Empty decision lists also pass its every() check.

A query/inspect proposition naming task.resume returns an executable proposal in the required-behavior test. The helper cannot distinguish asking about an operation from requesting it. Exclusion/dependency relationships between propositions are likewise not compiled.

Smallest fix. Validate allowed act/operation combinations and compile the turn as a whole, preserving prohibitions and dependencies. Emit no executable command for unsupported or incomplete semantic structures.

Gate: Before connecting perception to effects

Captured source
14:   if (turn.status !== "RESOLVED") return undefined;
15:   if (!proposition.capability) return undefined;
16:   if (!proposition.decisions.every((decision) => options.acceptedDecisions.has(decision))) {
17:     return undefined;
18:   }
19:   if (proposition.polarity === "exclude") return undefined;
20:   const slots =
21:     proposition.slots && proposition.slots.length > 0
22:       ? proposition.slots.map((slot) => ({ slot: slot.slot, refs: [...slot.refs] }))
23:       : [{ slot: "targets", refs: [...proposition.refs] }];
24:   return {
25:     kind: "propose",
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 `packages/perception/src/proposals.ts` around lines 14-25, The new consumer only checks turn status, a capability, accepted decision IDs and include polarity. It ignores proposition.act, operation and relations, and always returns kind `propose`. Empty decision lists also pass its `every()` check. A query/inspect proposition naming `task.resume` returns an executable proposal in the required-behavior test. The helper cannot distinguish asking about an operation from requesting it. Exclusion/dependency relationships between propositions are likewise not compiled. Validate allowed act/operation combinations and compile the turn as a whole, preserving prohibitions and dependencies. Emit no executable command for unsupported or incomplete semantic structures. Use the corresponding desired-behavior reproduction in `/tmp/circe-safety-pass-review/required.test.ts`, then retain it in the appropriate repository suite and run typecheck and the full baseline. Preserve the typed kernel boundary and do not add English routing rules.
F8. Semantic proposals lose the versions that supported the decisionpackages/perception/src/proposals.ts:19-34P1 logic data-integrity

propositionCommand() accepts a frame ID and decision ID set but receives no frame/read set and emits no expectedVersions. SemanticProposition has no observation-version field. Neither acceptance nor dependency freshness is checked against the store here.

The test interprets A from a version-1 ContextFrame, updates the kernel view to version 2, then builds a command with this helper. The kernel silently binds and validates version 2. It never tests the version that supported the model's judgment.

Smallest fix. Build proposals from an accepted interpretation plus its immutable frame/read set. Carry exact observation versions and captured knowledge generations; reject stale support rather than reinterpreting it as current.

Gate: Before connecting perception to effects

Captured source
19:   if (proposition.polarity === "exclude") return undefined;
20:   const slots =
21:     proposition.slots && proposition.slots.length > 0
22:       ? proposition.slots.map((slot) => ({ slot: slot.slot, refs: [...slot.refs] }))
23:       : [{ slot: "targets", refs: [...proposition.refs] }];
24:   return {
25:     kind: "propose",
26:     commandId: options.commandId,
27:     capability: proposition.capability,
28:     slots,
29:     ...(proposition.arguments ? { arguments: proposition.arguments } : {}),
30:     ...(proposition.constraints ? { constraints: proposition.constraints } : {}),
31:     ...(proposition.targetIntentRevision !== undefined
32:       ? { expectedIntentRevision: proposition.targetIntentRevision }
33:       : {}),
34:     decisions: [...proposition.decisions],
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 `packages/perception/src/proposals.ts` around lines 19-34, `propositionCommand()` accepts a frame ID and decision ID set but receives no frame/read set and emits no expectedVersions. SemanticProposition has no observation-version field. Neither acceptance nor dependency freshness is checked against the store here. The test interprets A from a version-1 ContextFrame, updates the kernel view to version 2, then builds a command with this helper. The kernel silently binds and validates version 2. It never tests the version that supported the model's judgment. Build proposals from an accepted interpretation plus its immutable frame/read set. Carry exact observation versions and captured knowledge generations; reject stale support rather than reinterpreting it as current. Use the corresponding desired-behavior reproduction in `/tmp/circe-safety-pass-review/required.test.ts`, then retain it in the appropriate repository suite and run typecheck and the full baseline. Preserve the typed kernel boundary and do not add English routing rules.
F18. Clear-selection without refs leaves the selection activepackages/core/src/runtime.ts:1004-1041P2 logic correctness

clear-selection handling is nested inside if (event.refs && event.refs.length > 0). A clear event naturally needs no refs, so it bypasses that branch.

The test selects A and then sends { action: 'clear-selection' }. currentReferentId still points at A. Subsequent deictic interpretation can continue using a selection the user cleared.

Smallest fix. Handle clear-selection independently of supplied refs, and explicitly define which focus, scope and referent records should be cleared.

Gate: Before UI use

Captured source
1004:     const event = observation.event;
1005:     if (event.view) state.views.set(event.view.id, event.view);
1006:     if (event.refs && event.refs.length > 0) {
1007:       if (event.action === "select" || event.action === "focus") {
1008:         const scope: Scope = {
1009:           id: `scope:${String(++state.counters.scope).padStart(2, "0")}`,
1010:           refs: [...event.refs],
1011:           origin: { kind: "focus", refs: [...event.refs] },
1012:           lifetime: { kind: "until-replaced" },
1013:           createdAt: ctx.now,
1014:         };
1015:         state.scopes.set(scope.id, scope);
1016:         state.scopeStack.push(scope.id);
1017:         state.retainedScopes.unshift(scope.id);
1018:         state.retainedScopes = state.retainedScopes.slice(0, 8);
1019:         const kind: Referent["kind"] = event.refs.length > 1 ? "plural" : "singular";
1020:         const referent: Referent = {
1021:           id: `referent:${++state.counters.referent}`,
1022:           role: kind === "singular" ? "current" : "set",
1023:           kind,
1024:           refs: [...event.refs],
1025:           scopeId: scope.id,
1026:           provenance: { rule: "ui.selection.v1", inputs: [], conclusion: event.refs.map(entityRefKey).join(","), confidence: 1, at: ctx.now },
1027:           createdAt: ctx.now,
1028:           updatedAt: ctx.now,
1029:         };
1030:         state.referents.set(referent.id, referent);
1031:         state.recentReferentIds.unshift(referent.id);
1032:         if (kind === "singular") state.currentReferentId = referent.id;
1033:         ctx.reply = "ack";
1034:         return;
1035:       }
1036:       if (event.action === "clear-selection") {
1037:         state.currentReferentId = undefined;
1038:         ctx.reply = "ack";
1039:         return;
1040:       }
1041:     }
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 `packages/core/src/runtime.ts` around lines 1004-1041, `clear-selection` handling is nested inside `if (event.refs && event.refs.length > 0)`. A clear event naturally needs no refs, so it bypasses that branch. The test selects A and then sends `{ action: 'clear-selection' }`. currentReferentId still points at A. Subsequent deictic interpretation can continue using a selection the user cleared. Handle clear-selection independently of supplied refs, and explicitly define which focus, scope and referent records should be cleared. Use the corresponding desired-behavior reproduction in `/tmp/circe-safety-pass-review/required.test.ts`, then retain it in the appropriate repository suite and run typecheck and the full baseline. Preserve the typed kernel boundary and do not add English routing rules.
F17. The journal omits typed commands and their deduplication identitiespackages/core/src/runtime.ts:1478-1488P2 logic data-integrity

command() does not append the command. InteractionLogEntry has no typed-command record, and intent-revised stores only an ID and revision. The runtime neither reads an existing journal nor reconstructs commandResults. A persistent append implementation alone cannot add those missing semantics.

After a typed proposal with commandId unique-command-123 completes, that command ID appears nowhere in the journal. The test fails even before attempting restart. Deferring a disk-backed implementation is reasonable; this is a separate missing recovery contract.

Smallest fix. Record the full accepted command and identity, reconstructible intent/transaction transitions, and command results or sufficient replay events. Add a recovery entry point and crash-boundary tests before claiming durability.

Gate: Before durability claims

Captured source
1478:   async function command(command: KernelCommand): Promise<TurnResult> {
1479:     const cached = state.commandResults.get(command.commandId);
1480:     if (cached) return structuredClone(cached);
1481:     const ctx = newCtx(clock(), command.commandId, command.commandId);
1482:     pruneScopes(ctx.now);
1483:     switch (command.kind) {
1484:       case "propose": {
1485:         await applyProposal(ctx, command, []);
1486:         break;
1487:       }
1488:       case "revise": {
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 `packages/core/src/runtime.ts` around lines 1478-1488, `command()` does not append the command. InteractionLogEntry has no typed-command record, and intent-revised stores only an ID and revision. The runtime neither reads an existing journal nor reconstructs commandResults. A persistent append implementation alone cannot add those missing semantics. After a typed proposal with commandId unique-command-123 completes, that command ID appears nowhere in the journal. The test fails even before attempting restart. Deferring a disk-backed implementation is reasonable; this is a separate missing recovery contract. Record the full accepted command and identity, reconstructible intent/transaction transitions, and command results or sufficient replay events. Add a recovery entry point and crash-boundary tests before claiming durability. Use the corresponding desired-behavior reproduction in `/tmp/circe-safety-pass-review/required.test.ts`, then retain it in the appropriate repository suite and run typecheck and the full baseline. Preserve the typed kernel boundary and do not add English routing rules.