PR #2: keep the architecture, fix two regressions

Two focused follow-up fixes

274 baseline tests pass · all 49 prior probes pass · 3 new assertions fail

2 P2 correctness findings · no demonstrated unsafe dispatch in these new cases

Incremental +2768 / -107 a5d74b5 · merged

The evidence repairs are sound enough to keep. The self-review also introduced two reproducible functional regressions by adding rules that lack a single authoritative contract. I would fix those before extending the affected paths. I do not recommend a rewrite or reverting the whole pass.

GitHub reports PR #2 already merged on 22 September 2026 as f823054. This review, dated 23 September, examines final PR head a5d74b5790867568948c579ec07e2756fd6292fc against the last reviewed 2cb82c8, plus affected callers. The same new failures reproduce on current stack tip 8cea673. Circe source was not edited; no GitHub comment, approval or merge was submitted.

What the pass gets right

All 49 previous independent review probes pass. In particular, F40 and F41 are fixed: a reference may have one version but multiple uses, a target also used as an argument remains protected in every consuming effect, explicit guards survive unrelated revisions, and versions transfer when a target becomes an argument. The earlier independent-target control still passes, so the repair did not restore the false dependency between unrelated effects.

The shared whole-turn evaluator remains the right boundary. Both compiler entry points apply structural checks, exclusions and dependency propagation through evaluateTurn. The normal accepted semantic path produces a typed command and reaches validation and dispatch without raw-token capability routing. Legacy correction/deictic work stays in the existing classifier/runtime path; these commits do not add a new English lexicon or a general agent scheduler.

Tracking the presented transaction is also a legitimate requirement. An unaddressed confirmation must refer to the gate the user saw. Cancelling or settling that gate must not silently redirect the next confirmation to older work.

Where self-review went wrong

F42 is a revision identity problem. The implementation tries to compensate for a continuation carrying only a revision number by checking every historical intent. Revisions are local counters, so an unrelated completed revision 2 blocks a fresh presented revision 1. The minimal durable answer is a named intent or transaction plus its revision. More global history checks cannot recover an identity absent from the command.

F43 is duplicated schema. CapabilityDescriptor already owns target-slot selection and cardinality, but the compiler now invents defaults from a semantic overlay. It picks the first overlay slot, while the kernel prefers targets; it assumes non-target slots are scalar, while mechanical descriptors may allow sets. A single normalized mechanical contract should feed both. Adding more compiler-specific fallback branches would continue the drift.

These are P2 functional bugs. The new tests demonstrate wrong compilation and false rejection of valid work. They do not demonstrate an unauthorized dispatch, lost durable state or a reason to roll back the whole merged PR.

Has it become overcomplicated?

Some parts have. The problem is concentrated in repeated state-selection and schema rules, rather than in the basic separation of context, perception and consequences.

The latest 16 commits add 2,768 lines and remove 107. Of the additions, 2,112 are tests; 656 are implementation and types. The runtime is now 2,501 lines, but line count alone is not the objection. Its target-selection helpers overlap: gateTransaction, presentedTransaction, unaddressedTransaction, pendingDraft, resumeTarget and selectTransaction each encode parts of which transaction an action means. The deterministic classifier also selects anchors. These policies deserve one explicit selection contract tested across the entry points.

There is smaller mechanical duplication too. canonicalEvidenceOf already builds the canonical evidence map, yet recomputeReadEvidence and commit repeat its construction. hasPresentedGate is set alongside presentedGateId and neither is cleared in this version, so the boolean duplicates information. Rejected corrections repeat authorization-clearing blocks. Consolidating these in a focused follow-up would reduce the number of places a future writer must remember to update. It does not justify introducing a new framework or rewriting the runtime now.

The 1,869-line evidence-ownership test file covers much more than evidence, including corrections, referents and gate selection. Its negative cases are useful, but the newly failing positive controls show why a rising green test count is insufficient. Organize future cases by invariant and pair each rejection test with nearby valid behavior. The missing controls here are an unrelated completed intent followed by a valid continuation, and compiler/kernel agreement when optional overlay metadata is absent.

Verification and limits

At exact PR head a5d74b5, pnpm test passes 274 tests and pnpm typecheck passes. The independent suite passes all 49 prior cases and two new positive controls; three new assertions fail. All five new cases pass at 2cb82c8. On stack tip 8cea673, those same three fail and the two positive controls pass. The new probe file also passes strict TypeScript checking.

I checked the incremental production changes, affected compiler/runtime consumers and self-review tests. This is not a full review of the later durability, Jev or inspector PRs, and I did not rerun their entire baseline in this round. The intentionally RED semantic corpus still reports its expected lexical misses inside the passing baseline harness; 274 passing tests do not claim real semantic interpretation is solved.

Next step

Make two narrow follow-up fixes: scope continuation checks to an explicit anchor, and derive slot mechanics from the authoritative descriptor. Preserve the evidence-use model and all 49 prior review cases. Put selector/helper consolidation in a separate cleanup with behavior-preservation controls. Continue the stack after the two functional regressions are covered; do not start another broad speculative hardening pass.

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

The reviewed head yields 51 passed and three failed. Downloaded configs use local paths and need alias updates on another machine. Test labels P1/P2/P3 identify probes, not finding severity; both findings are priority P2.

New probes and controls Test config Current output Previous head output Stack probe output PR baseline PR typecheck Sidecar

Changed files this round
Current findings
Prompt for current findings · Ctrl+`
Prompt to rerun review · Ctrl+Space
P2 F42. An unrelated old revision permanently blocks valid new continuationspackages/core/src/runtime.ts:1408-1417logic correctness

applyProposal now compares expectedIntentRevision against every intent ever stored in the interaction. Revision numbers belong to individual intents and restart at 1. A larger number on an unrelated intent is not evidence that the presented intent has been superseded.

The reproduction proposes A, revises it to revision 2, and cancels it. It then proposes B at revision 1 and submits a continuation of B with expectedIntentRevision 1. The runtime returns STALE_INTENT even though B is the presented live intent and its revision matches exactly. The old cancelled A remains in state.intents, so the same condition poisons subsequent fresh revision-1 continuations for the rest of this interaction.

This is a functional regression, not a reproduced unauthorized dispatch. It passes at 2cb82c8 and fails at a5d74b5 and stack tip 8cea673. A control without the unrelated old revision still succeeds. The current tests exercise stale continuations but omit this valid continuation after an unrelated transaction ends.

Fix direction. Bind continuation evidence to an intent or transaction identity plus its revision. Compare that named anchor, including its lifecycle, rather than the maximum revision anywhere in the interaction. Preserve rejection when the named intent was revised, replaced or settled; simply deleting the global check would reopen the stale-anchor problem.

Source at reviewed head · Probe P1

Captured source
1408:       const declared = command.expectedIntentRevision;
1409:       const superseded = [...state.intents.values()].some(
1410:         (intent) => intent.revision > declared,
1411:       );
1412:       const anchor = presentedTransaction();
1413:       const anchorIntent = anchor ? state.intents.get(anchor.intentId) : undefined;
1414:       const stale =
1415:         superseded ||
1416:         (anchorIntent !== undefined && anchorIntent.revision !== declared) ||
1417:         (anchorIntent === undefined && state.hasPresentedGate);
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 1408-1417, applyProposal now compares expectedIntentRevision against every intent ever stored in the interaction. Revision numbers belong to individual intents and restart at 1. A larger number on an unrelated intent is not evidence that the presented intent has been superseded. Bind continuation evidence to an intent or transaction identity plus its revision. Compare that named anchor, including its lifecycle, rather than the maximum revision anywhere in the interaction. Preserve rejection when the named intent was revised, replaced or settled; simply deleting the global check would reopen the stale-anchor problem. Reproduce using `/tmp/circe-pr2-round4-evidence/controls.test.ts`, then retain all 49 earlier review cases and both new positive controls. Run the baseline suite and typecheck. This review covers merged PR #2 at a5d74b5790867568948c579ec07e2756fd6292fc; current tip also reproduces the failure.
P2 F43. The compiler invents slot mechanics that disagree with the kernelpackages/perception/src/proposals.ts:532-550logic correctness

The compiler chooses the first semantic overlay slot as the default target and assumes every other slot is scalar unless the overlay repeats its arity. The kernel instead prefers the mechanical targetSlot, then a slot named targets, then a required slot. Mechanical arities already exist on CapabilityDescriptor. The semantic overlay fields are optional, and validateCapabilityOverlays accepts overlays without them.

With a valid overlay ordered as optional note followed by targets, a proposition whose summary refs contain A now compiles to note=A instead of targets=A. A second case names targets=A and watchers=[B,C], with watchers mechanically declared many and its optional semantic arity omitted. The kernel accepts this command, but the compiler rejects it as a scalar-slot violation. Both overlays pass the public overlay validator.

Both cases pass at 2cb82c8 and fail at the merged PR head and current stack tip. Supplying complete semantic arity makes the second control pass. These are valid-command regressions caused by duplicating the execution schema in the perception layer; neither probe demonstrates an unauthorized external effect.

Fix direction. Use one authoritative mechanical slot contract. Materialize normalized targetSlot, requiredness and arity from CapabilityDescriptor into the frame, or share the normalization rule without making core depend on perception. Do not interpret missing optional semantic metadata as a contradictory execution constraint. Verify default-target behavior under slot reordering and valid secondary many-valued slots.

Source at reviewed head · Probe P2, P3

Captured source
532:     const fallbackSlot =
533:       offeredCapability?.targetSlot ??
534:       offeredCapability?.slots[0]?.name ??
535:       "targets";
536:     const declaredSlots = new Map(
537:       (offeredCapability?.slots ?? []).map((slot) => [slot.name, slot]),
538:     );
539:     if (offeredCapability && offeredCapability.slots.length > 0) {
540:       const unknownSlot = (proposition.slots ?? []).find(
541:         (slot) => !declaredSlots.has(slot.slot),
542:       );
543:       if (unknownSlot) {
544:         return fail(
545:           `slot ${unknownSlot.slot} is not offered by capability ${proposition.capability}`,
546:         );
547:       }
548:     }
549:     const effectiveArity = (slotName: string): "one" | "many" =>
550:       declaredSlots.get(slotName)?.arity ?? (slotName === fallbackSlot ? "many" : "one");
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 532-550, The compiler chooses the first semantic overlay slot as the default target and assumes every other slot is scalar unless the overlay repeats its arity. The kernel instead prefers the mechanical targetSlot, then a slot named targets, then a required slot. Mechanical arities already exist on CapabilityDescriptor. The semantic overlay fields are optional, and validateCapabilityOverlays accepts overlays without them. Use one authoritative mechanical slot contract. Materialize normalized targetSlot, requiredness and arity from CapabilityDescriptor into the frame, or share the normalization rule without making core depend on perception. Do not interpret missing optional semantic metadata as a contradictory execution constraint. Verify default-target behavior under slot reordering and valid secondary many-valued slots. Reproduce using `/tmp/circe-pr2-round4-evidence/controls.test.ts`, then retain all 49 earlier review cases and both new positive controls. Run the baseline suite and typecheck. This review covers merged PR #2 at a5d74b5790867568948c579ec07e2756fd6292fc; current tip also reproduces the failure.
Earlier reviews and original finding bodies

This preserved report describes 2cb82c8. F40 and F41 now pass and are marked fixed in the sidecar. Its historical verdict is not the current verdict. Open round 3 archive.

PR #2: evidence coverage still has two gaps

Changes requested

239 baseline tests pass · 46 prior reproductions pass · 3 new cases fail

2 P1 findings · all 3 new cases pass before this commit

Incremental +544 / -216 2cb82c8

The unified evaluator resolves the previously reproduced entry-point drift, and all 46 prior cases pass at 2cb82c8. I would still request changes for two evidence-coverage regressions, reproduced by three new tests.

Reviewed on 22 September 2026 at exact head 2cb82c8582ff2edf68fc8f47a5ec90d2aa1fada8, against the previous review head 34685ba. This incremental review covers the latest changes and their affected callers. PR #2 remained at that head during verification. Circe source files were not edited.

What is fixed

Both compiler entry points now select from evaluateTurn. Structural checks, exclusions, dependency rejection, evidence coverage and command construction share the same path. The previously rejected prerequisite can no longer authorize execution through the per-proposition helper.

The original evidence cases also pass. Revised target versions agree at validation and dispatch; retargeting retires the old target-only evidence; independent sequential targets no longer invalidate each other; and a genuine argument dependency outside the target set reaches every effect.

What remains

The rule that each ref has one owner conflates version storage with where the ref is used. B can be a target and an argument dependency of A's effect. The new filter makes B slot-owned globally, so A's effect keeps the B argument but loses B's precondition.

Revision has a related lifetime problem. It reconstructs the shared set from argument and constraint values, which can erase an explicitly declared read guard absent from those values. It also fails to transfer evidence from a replaced binding when the same ref remains in arguments. Both revision probes actually invoke after stale required evidence disappears from validation.

The smallest principled correction is to keep a canonical version per ref while tracking each use separately: slot target, shared argument dependency or explicit read guard. A revision may retire a use; effect splitting may select uses. Neither operation should erase a required version merely because another use exists or changed location. Fail closed if required evidence is unavailable. No new language rules or general scheduling framework are needed.

Verification

PR #2 passes 239 eval tests and typecheck. The independent suite passes all 46 prior cases and fails the three new cases. Those same three cases pass at 34685ba, establishing that they are regressions in this change. The new probe file passes strict typecheck.

Stack tip 9d78c7e passes 286 tests: 254 evals, 14 journal-file, 12 perception-jev and six inspector tests. Typecheck is clean. All 46 prior probes pass there, and all three new failures reproduce. The open PRs and declared bases match the reported stack. This does not constitute a full review of PRs #3-#6.

Recommendation

Fix F40 and F41 before merging PR #2. F38 and F39 retain their original bodies and are marked fixed for their original scenarios. The new findings preserve the distinction between successful repairs and newly introduced failures. No GitHub approval, comment or merge was submitted.

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

At the reviewed head this yields 46 passed and three failed. Downloaded configs contain local checkout paths; adjust aliases elsewhere.

3 new evidence probes Test config 46 prior cases Current output Evidence cases before fix Stack probe output PR #2 baseline PR #2 typecheck Stack baseline Stack typecheck Review sidecar

Changed files this round
  • packages/core/src/runtime.ts +72 / -7
  • packages/evals/src/accepted-turn-review2-round2.test.ts +258 / -0
  • packages/perception/src/proposals.ts +214 / -209
Current findings
Prompt for current findings · Ctrl+`
Prompt to rerun review · Ctrl+Space
P1 F40. A ref used as both a target and an argument loses effect-level coveragepackages/core/src/runtime.ts:1121-1125logic data-integrity

The new ownership filter removes every bound ref from shared readDependencies. A ref can simultaneously be a target and a shared argument dependency. When the runtime splits a command into target effects, each effect keeps the same arguments but only its own binding expectations.

The reproduction compiles targets A and B with arguments.secondary = B. Both refs have frame evidence. After dispatch, the A invocation carries the B argument but has no precondition for B. Initial validation included B, but the executor cannot conditionally protect that read when it applies the A effect. This is a regression from 34685ba, where the same test passes.

Canonical storage of one version per ref is reasonable. It does not mean the ref is used in only one role or by only one effect. The preceding tests covered shared argument C outside the target set and therefore missed this overlap.

Fix direction. Track evidence usage separately from its canonical version. A shared argument dependency must be attached to every effect that consumes it even when the same ref is also bound as a target. Preserve the passing independent-target case where there is no actual shared dependency.

Source at reviewed head · Probe O1 · Related to F39

Captured source
1121:     const boundKeys = new Set(bindings.flatMap((binding) => binding.refs).map(entityRefKey));
1122:     const readDependencies = dedupePreconditions(
1123:       (command.expectedVersions ?? []).filter(
1124:         (precondition) => !boundKeys.has(entityRefKey(precondition.ref)),
1125:       ),
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 1121-1125, The new ownership filter removes every bound ref from shared readDependencies. A ref can simultaneously be a target and a shared argument dependency. When the runtime splits a command into target effects, each effect keeps the same arguments but only its own binding expectations. Track evidence usage separately from its canonical version. A shared argument dependency must be attached to every effect that consumes it even when the same ref is also bound as a target. Preserve the passing independent-target case where there is no actual shared dependency. Reproduce O1 using `/tmp/circe-pr2-round3-evidence/ownership.test.ts` at PR #2 head 2cb82c8582ff2edf68fc8f47a5ec90d2aa1fada8. Retain all 46 prior reviewer cases, including independent targets and genuine shared dependencies, and run the full suite and typecheck.
P1 F41. Revisions silently discard still-required read dependenciespackages/core/src/runtime.ts:1212-1231logic data-integrity

applyRevision now rebuilds readDependencies exclusively from refs found inside current arguments and constraints. It does not preserve an explicit command-wide read guard that has no corresponding argument. It also searches only the old shared list and latestSnapshots, ignoring evidence previously owned by a slot that the revision replaces. Missing versions are filtered out, after which commit checks coverage only for bound refs.

One reproduction proposes A with an explicit read dependency on C, changes C to a newer version, then revises only a scalar note. C disappears from validation and the request invokes despite the stale declared guard. A second starts with B as both target and argument, then retargets to A while retaining the B argument. The old B version exists on the replaced binding but is not transferred. With no kernel snapshot, B disappears from the read set and the request invokes even though B is stale.

Both cases pass on 34685ba and fail on 2cb82c8. The fix for obsolete slot evidence should retire evidence whose use ended, rather than discard explicit shared guards or evidence whose reference still appears in an executable argument.

Fix direction. Make revision update evidence from explicit dependency roles and the prior canonical evidence set, including replaced bindings. Preserve declared shared guards unless explicitly replaced or retired. Transfer versions for refs whose role changes, and reject missing required evidence instead of filtering it out. Validate coverage against the refs and guards the revised effect actually consumes.

Source at reviewed head · Probe O2, O3 · Related to F39

Captured source
1212:     const existingReadDependencies = new Map(
1213:       (intent.readDependencies ?? []).map((precondition) => [
1214:         entityRefKey(precondition.ref),
1215:         precondition,
1216:       ]),
1217:     );
1218:     const sharedRefs = refsInValues([argumentsNow, intent.constraints]).filter(
1219:       (ref) => !boundKeys.has(entityRefKey(ref)),
1220:     );
1221:     const observed = latestSnapshots(sharedRefs);
1222:     const readDependencies = dedupePreconditions(
1223:       sharedRefs.map((ref) => {
1224:         const existing = existingReadDependencies.get(entityRefKey(ref));
1225:         if (existing) return existing;
1226:         const snapshot = observed.get(entityRefKey(ref));
1227:         if (snapshot && snapshot.version.authority === ref.authority) {
1228:           return { ref: { ...ref }, token: { ...snapshot.version } };
1229:         }
1230:         return undefined;
1231:       }).filter((precondition): precondition is FrozenPrecondition => Boolean(precondition)),
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 1212-1231, applyRevision now rebuilds readDependencies exclusively from refs found inside current arguments and constraints. It does not preserve an explicit command-wide read guard that has no corresponding argument. It also searches only the old shared list and latestSnapshots, ignoring evidence previously owned by a slot that the revision replaces. Missing versions are filtered out, after which commit checks coverage only for bound refs. Make revision update evidence from explicit dependency roles and the prior canonical evidence set, including replaced bindings. Preserve declared shared guards unless explicitly replaced or retired. Transfer versions for refs whose role changes, and reject missing required evidence instead of filtering it out. Validate coverage against the refs and guards the revised effect actually consumes. Reproduce O2, O3 using `/tmp/circe-pr2-round3-evidence/ownership.test.ts` at PR #2 head 2cb82c8582ff2edf68fc8f47a5ec90d2aa1fada8. Retain all 46 prior reviewer cases, including independent targets and genuine shared dependencies, and run the full suite and typecheck.
Original review, baseline and finding bodies preserved

The following report describes 34685ba. Its original F38-F39 cases now pass. Open archive.

PR #2: two blockers remain

Changes requested

234 baseline tests pass · 41 prior reproductions pass · 4 new cases fail

2 P1 findings · genuine shared-dependency control passes

Incremental +411 / -36 34685ba

The original F31-F37 reproductions pass at 34685ba, but I would still request changes. One dependency check remains specific to the whole-turn entry point. The new read-dependency field also duplicates slot evidence, producing inconsistent versions after revisions and false dependencies between target effects.

Reviewed on 22 September 2026 at exact PR #2 head 34685ba2cbb50d7d029437a040d1d3a6dfe0eac6. This incremental review covers changes since 91c123b and their affected callers. PR #2 was unchanged at verification. Circe source files were not edited.

What now works

Both entry points use the new whole-turn shape validation and executable act/operation checks. The explicit index must match its proposition, dangling relations fail closed, and frame id and hash are both checked. The whole-turn compiler rejects a dependent when its earlier prerequisite was rejected. Argument-only reference evidence now reaches authority validation and frozen effects.

All 41 independent prior reproductions pass: 18 first-round safety cases, nine second-round cases, three third-round cases and the 11 PR #2 cases. Those scenarios overlap the tests now retained in the repository. PR #2 independently passes 234 eval tests and typecheck.

What still breaks

The per-proposition API never consults the whole-turn rejected-prerequisite analysis. A dependent rejected by compileSemanticTurn is still returned as a command by propositionCommand. The new shared helper covers act and shape checks, not the complete acceptance contract.

The evidence fix copies all expected versions into shared dependencies. Slot expectations already own many of those versions. After a revision, the two copies disagree: validation checks version 2 but dispatch sends version 1. Retargeting from A to B can retain an obsolete A precondition. Splitting an A/B command into sequential target effects makes B depend on A's pre-mutation version, so successful execution of A invalidates B.

These evidence cases pass at the preceding commit and fail at the reviewed commit. They are regressions, not added requirements for a future feature. A passing control checks that genuine argument dependencies still reach every effect, so dropping shared evidence altogether would not be a valid repair.

Recommendation

Keep this architecture, but finish the shared analysis and define evidence ownership. Both APIs should select commands from the same validated turn. Slot-owned expectations and genuine shared read dependencies need distinct lifetimes and effect scope. Freeze exactly the versions that were validated for the current intent revision.

There are two P1 findings, F38 and F39, with four failing required-behavior cases. The original seven findings retain their bodies and are marked fixed for their original scenarios. Related remaining behavior is recorded separately so the review history stays accurate.

Stack verification

The declared bases for PRs #2-#6 match the reported stack. Tip 623e2d7 independently passes typecheck and 281 tests: 249 evals, 14 journal-file, 12 perception-jev and six inspector tests. All 41 prior probes and the shared-dependency control pass there too. All four new failures also reproduce on that tip. This is a regression check, not a full code review of the later PRs.

Reproduce

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

Expected result at the reviewed head: 42 passed and four failed. The 42 comprise the prior 41 cases and one additional control. The external probe file also passes strict typecheck. Downloaded configs contain local checkout paths; adjust them on another machine. No formal GitHub approval, comment or merge was submitted.

New probes and control Test config 41 prior cases Current output Evidence cases before fix Stack probe output PR #2 baseline PR #2 typecheck Stack baseline Stack typecheck Review sidecar

Changed files this round
  • packages/core/src/runtime.ts +27 / -3
  • packages/core/src/types.ts +6 / -0
  • packages/evals/src/accepted-turn-pipeline.test.ts +1 / -1
  • packages/evals/src/accepted-turn-review2.test.ts +263 / -0
  • packages/perception/src/proposals.ts +114 / -32
Current findings
Prompt for current findings · Ctrl+`
Prompt to rerun review · Ctrl+Space
P1 F39. Slot evidence is incorrectly retained as a shared read dependencypackages/core/src/runtime.ts:1088-1096logic data-integrity

applyProposal copies every command.expectedVersions entry into intent.readDependencies, including refs already owned by slot bindings. Revisions update the bindings but preserve this second copy through the intent spread. commit deduplicates with binding versions winning, while effect construction at lines 896-899 reverses that precedence and lets readDependencies win.

The first reproduction proposes A at version 1, revises its binding to version 2 and confirms. Authority validation receives version 2; the executor receives version 1. A second reproduction retargets the intent from A to B. A is no longer a target, but its retained version still reaches validation and blocks B when A has changed.

The same copy also breaks independent target effects. Every effect receives the entire readDependencies list, so an A/B request with maxConcurrentEffects:1 gives the B invocation a precondition on A. An executor that atomically checks preconditions updates A first, then refuses B because A is now newer. Only A is applied. All three evidence tests pass at 91c123b and fail at 34685ba; these are regressions introduced by the evidence repair.

The shared argument-evidence fix itself works: a separate passing control verifies that a genuine argument ref C survives into both effects. The repair must retain that behavior while distinguishing slot-owned evidence from true shared dependencies.

Fix direction. Represent slot-owned expectations and shared read dependencies separately at the command boundary. Derive a canonical evidence set for each intent revision, validate and freeze the same versions, retire evidence owned only by replaced bindings, and attach only genuine shared dependencies to every target effect. Preserve argument dependencies; do not remove all shared evidence as a workaround.

Source at reviewed head · Probe N2, N3, N4 · Related to F32

Captured source
1088:     // The declared read set is command-wide: refs carried in arguments (or
1089:     // otherwise not bound to a slot) must still be validated and frozen.
1090:     const readDependencies = dedupePreconditions(command.expectedVersions ?? []);
1091:     const intent: Intent = {
1092:       id: `intent:${++state.counters.intent}`,
1093:       revision: 1,
1094:       capability: command.capability,
1095:       bindings,
1096:       ...(readDependencies.length > 0 ? { readDependencies } : {}),
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 1088-1096, applyProposal copies every command.expectedVersions entry into intent.readDependencies, including refs already owned by slot bindings. Revisions update the bindings but preserve this second copy through the intent spread. commit deduplicates with binding versions winning, while effect construction at lines 896-899 reverses that precedence and lets readDependencies win. Represent slot-owned expectations and shared read dependencies separately at the command boundary. Derive a canonical evidence set for each intent revision, validate and freeze the same versions, retire evidence owned only by replaced bindings, and attach only genuine shared dependencies to every target effect. Preserve argument dependencies; do not remove all shared evidence as a workaround. Reproduce N2, N3, N4 using `/tmp/circe-pr2-round2-evidence/round2.test.ts` at PR #2 head 34685ba2cbb50d7d029437a040d1d3a6dfe0eac6. Retain the 41 prior reviewer cases and the genuine argument-dependency control, and run the full suite and typecheck.
P1 F38. The per-proposition API still bypasses prerequisite rejectionpackages/perception/src/proposals.ts:521-527logic correctness

The new whole-turn compiler tracks rejectedIndexes and prevents a dependent proposition from compiling when its prerequisite is rejected. propositionCommand still checks only whether the prerequisite decision ids are accepted. Shared shape and act checks do not make these dependency rules shared.

The reproduction uses an accepted request for B without a frame observation, followed by an accepted request for A with dependsOn:0. compileSemanticTurn correctly emits no commands. propositionCommand on the dependent request, with the correct explicit index, returns an executable command for A. This is the remaining entry-point path of F33; its original whole-turn reproduction now passes.

Fix direction. Share the complete dependency-validity analysis between both entry points, including evidence and prohibition rejection, or require the per-proposition helper to consume an already validated turn. Sharing only executableIssue is insufficient.

Source at reviewed head · Probe N1 · Related to F33

Captured source
521:   const isAccepted = (candidate: SemanticProposition): boolean =>
522:     decisionsAccepted(candidate, options.acceptedDecisions);
523:   const analysis = analyzeTurn(turn, isAccepted, issues);
524:   if (analysis.prohibited.has(index) || analysis.unvalidatedProhibited.has(index)) {
525:     return undefined;
526:   }
527:   if (relationIssue(turn, proposition, index, isAccepted)) return 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/perception/src/proposals.ts` around lines 521-527, The new whole-turn compiler tracks rejectedIndexes and prevents a dependent proposition from compiling when its prerequisite is rejected. propositionCommand still checks only whether the prerequisite decision ids are accepted. Shared shape and act checks do not make these dependency rules shared. Share the complete dependency-validity analysis between both entry points, including evidence and prohibition rejection, or require the per-proposition helper to consume an already validated turn. Sharing only executableIssue is insufficient. Reproduce N1 using `/tmp/circe-pr2-round2-evidence/round2.test.ts` at PR #2 head 34685ba2cbb50d7d029437a040d1d3a6dfe0eac6. Retain the 41 prior reviewer cases and the genuine argument-dependency control, and run the full suite and typecheck.
Original review, baseline and finding bodies preserved

The following report describes 91c123b. All seven original finding scenarios now pass. Open archive.

PR #2 needs changes before merge

Changes requested

6 P1 findings · 1 P2 finding · 223 baseline tests pass

10 required-behavior cases fail · valid-request control passes

+1531 / -84 91c123b

The accepted-turn bridge is the right architectural direction, but I would request changes at 91c123b. Six blocking findings and one smaller identity inconsistency remain. They are reproduced below, with ten failing required-behavior cases and one passing valid-request control.

Reviewed on 22 September 2026 against merged base 3d3bf9b, through exact head 91c123b06b177c6b943fe2ac2f0ffd1e8c2f9893. This is a review of PR #2, including its latest corrective commit. The main workspace remains unchanged. Tests ran in an isolated worktree.

Assessment

I would keep the frame-to-turn-to-command architecture. Recording accepted judgments, checking capability citations and applying exclusions before execution are useful responsibilities for this layer. There is no need to add English rules or replace the transaction kernel to resolve these findings.

The implementation has two partially independent validators. One rejects queries but ignores malformed sibling exclusions. The other rejects malformed siblings but now executes queries. This is the immediate architectural weakness: a caller's choice of API changes the safety contract. The latest refactor removed a call to the older helper and accidentally removed its act/operation checks from the new path.

The second boundary problem crosses packages. The compiler claims to cover refs in arguments, but the kernel drops their evidence unless those refs also occupy slots. Adding more compiler checks cannot compensate for a consumer that discards the certified read set.

Use one validated whole-turn representation and a narrow command constructor. Either preserve command-wide read dependencies through the kernel or refuse unsupported argument refs. Rejected prerequisites must not leave independently executable dependents behind. These are bounded repairs; a larger framework would not address the demonstrated failures.

Verification and test quality

pnpm test passes all 223 tests across 19 eval files, and pnpm typecheck passes. The PR description's 202-test figure is stale. The independent probe suite has ten assertion failures covering seven findings; its valid-request control passes. The probe file also passes the repository's strict TypeScript settings.

The query test already in the repository cites a referent decision, so capability-citation rejection makes it pass without testing the act guard. The independent query cases supply valid capability evidence, and each reaches an actual executor invocation. Evidence coverage is tested across compiler and kernel, including an authority that would reject a stale argument ref if that ref reached validation.

The difficult-utterance E2E test exercises a useful contract: exclusion of B, action on A and preservation of frame versions. Its backend answers and propositions are scripted. It is not evidence that the implementation can interpret arbitrary wording, and that remains a legitimate boundary for this PR.

Next step

Fix F31-F36 before merging. F37 is a smaller provenance inconsistency worth fixing in the same shared validation pass. Keep all 223 existing tests passing, retain these required-behavior probes, and rerun them against the final PR head. No GitHub comment, formal approval or source change was submitted by this review.

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

The downloaded test and config use local worktree paths. Adjust aliases for another checkout. Finding numbers continue from the earlier safety review; PR #1's scoped approval is unchanged.

Implementation walkthrough
  • semanticTurnFromRun maps accepted spec ids to trace ids and carries run/frame identity into the turn.
  • compileSemanticTurn checks citation provenance and capability selection, computes exclusions, derives observed versions and suppresses identical commands.
  • The latest refactor constructs commands directly, leaving act/operation checks only in propositionCommand.
  • The compiler now recognizes argument refs, but the kernel still stores read evidence only on slot bindings.
  • The new evals demonstrate contract behavior with scripted decisions and manually constructed propositions; real interpretation remains a later integration concern.
Workflow run → accepted decision ids + traces → whole-turn validation → evidence-bound commands → kernel validation/freeze → executor
Changed files
  • packages/context/src/types.ts +2 / -0
  • packages/evals/src/accepted-turn-adversarial.test.ts +350 / -0
  • packages/evals/src/accepted-turn-pipeline.test.ts +542 / -0
  • packages/perception/src/proposals.ts +631 / -84
  • packages/perception/src/trace.ts +1 / -0
  • packages/perception/src/types.ts +5 / -0

Findings

Required-behavior tests Vitest config Probe output Baseline output Typecheck output Review sidecar

Prompt for all findings · Ctrl+`
Prompt to rerun review · Ctrl+Space
P1 F34. A dangling prohibition is silently ignoredpackages/perception/src/proposals.ts:123-135logic correctness

shapeIssue checks that a relation targetIndex is an integer, but does not check its bounds. Bounds are checked later in relationIssue, after exclusions have already returned from the compilation loop. The whole-turn malformed-input rule consequently misses dangling relations, including prohibition-only propositions.

The reproduction submits an exclusion with no direct refs and an excludes relation targeting index 99 in a two-proposition turn. The exclusion is labeled an exclusion constraint, while the other proposition compiles into a command. The malformed prohibition was never validated or rejected as malformed.

The PR expressly promises that malformed relations make the whole turn uncompilable because ignoring them can discard intended prohibitions. The current test covers a string index, but an out-of-range integer passes that shape gate.

Fix direction. Validate relation bounds and target existence for the entire turn before analyzing exclusions or constructing any commands. Apply the same validation to exclusion-only propositions and both public entry points.

Source at reviewed head · Probe R4

Captured source
123:   if (proposition.relations !== undefined) {
124:     if (!Array.isArray(proposition.relations)) {
125:       return "malformed proposition: relations is not an array";
126:     }
127:     for (const relation of proposition.relations) {
128:       if (
129:         !isRecord(relation) ||
130:         typeof relation.kind !== "string" ||
131:         !RELATION_KINDS.has(relation.kind) ||
132:         !Number.isInteger(relation.targetIndex)
133:       ) {
134:         return "malformed proposition: relation is not a typed, integer-indexed reference";
135:       }
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 123-135, shapeIssue checks that a relation targetIndex is an integer, but does not check its bounds. Bounds are checked later in relationIssue, after exclusions have already returned from the compilation loop. The whole-turn malformed-input rule consequently misses dangling relations, including prohibition-only propositions. Validate relation bounds and target existence for the entire turn before analyzing exclusions or constructing any commands. Apply the same validation to exclusion-only propositions and both public entry points. Reproduce R4 using `/tmp/circe-pr2-evidence/probes.test.ts` at PR #2 head 91c123b06b177c6b943fe2ac2f0ffd1e8c2f9893. Keep valid requests working and run the full suite and typecheck after the change.
P1 F33. Rejected prerequisites do not block dependent commandspackages/perception/src/proposals.ts:355-359logic correctness

relationIssue treats a dependsOn target as satisfied when its cited decisions are accepted. It never checks whether that proposition survives evidence validation, prohibitions or other compilation checks. compileSemanticTurn then returns the dependent command with no representation of the relation.

The reproduction has proposition 0 request B without a frame observation, and proposition 1 request A with dependsOn:0. The compiler explicitly rejects proposition 0 for missing evidence yet emits proposition 1. A caller executing the returned commands cannot discover the missing prerequisite from that command.

Acceptance of a judgment is weaker than acceptance of the proposition it supports. A whole-turn compiler needs to preserve that distinction. This finding concerns a rejected prerequisite at compilation; it does not require adding a general workflow scheduler to this PR.

Fix direction. Compute proposition validity before emitting commands and propagate rejection through dependency edges. Reject executable dependency forms that the command contract cannot preserve; define semantic-only dependencies explicitly rather than silently treating rejected requests as satisfied.

Source at reviewed head · Probe R3

Captured source
355:     const related = turn.propositions[relation.targetIndex];
356:     if (!related) return "malformed proposition: relation target is missing";
357:     if (relation.kind === "dependsOn") {
358:       if (!isAccepted(related)) return "a dependency proposition was not accepted";
359:     }
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 355-359, relationIssue treats a dependsOn target as satisfied when its cited decisions are accepted. It never checks whether that proposition survives evidence validation, prohibitions or other compilation checks. compileSemanticTurn then returns the dependent command with no representation of the relation. Compute proposition validity before emitting commands and propagate rejection through dependency edges. Reject executable dependency forms that the command contract cannot preserve; define semantic-only dependencies explicitly rather than silently treating rejected requests as satisfied. Reproduce R3 using `/tmp/circe-pr2-evidence/probes.test.ts` at PR #2 head 91c123b06b177c6b943fe2ac2f0ffd1e8c2f9893. Keep valid requests working and run the full suite and typecheck after the change.
P1 F35. The explicit proposition index can bypass prohibitionspackages/perception/src/proposals.ts:453-469logic correctness

The new index option is trusted without checking its range or confirming that turn.propositions[index] matches the supplied proposition. The code uses that index to look up turn-wide prohibitions, while it constructs the command from the separate proposition argument.

The reproduction places a relation-only exclusion at index 0 prohibiting the request at index 1. Calling propositionCommand with that request and index:0 returns an executable command. Checking the correct position would reject it. Rehydration or filtering code can therefore make the guard inspect one proposition while executing another.

This is a defect in the new exported low-level API even though compileSemanticTurn no longer calls it. The API explicitly exists for callers holding accepted decisions and advertises the same turn-wide prohibitions.

Fix direction. Require an integer index within bounds and verify identity or canonical structural equality with the selected turn entry. Reject mismatches before reading prohibitions, or accept only the turn and index and resolve the proposition internally.

Source at reviewed head · Probe R5

Captured source
453:   // Prefer an explicit position; fall back to identity, then to a unique
454:   // structural match so rehydrated turns are not silently dropped.
455:   let index = options.index ?? turn.propositions.indexOf(proposition);
456:   if (index < 0) {
457:     const serialized = stableStringify(proposition);
458:     const matches = turn.propositions
459:       .map((candidate, candidateIndex) => ({ candidate, candidateIndex }))
460:       .filter(({ candidate }) => stableStringify(candidate) === serialized);
461:     if (matches.length === 1) index = matches[0]!.candidateIndex;
462:   }
463:   if (index < 0) return undefined;
464:
465:   const issues = turn.propositions.map((candidate) => shapeIssue(candidate));
466:   const isAccepted = (candidate: SemanticProposition): boolean =>
467:     decisionsAccepted(candidate, options.acceptedDecisions);
468:   const analysis = analyzeTurn(turn, isAccepted, issues);
469:   if (analysis.prohibited.has(index) || analysis.unvalidatedProhibited.has(index)) {
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 453-469, The new index option is trusted without checking its range or confirming that turn.propositions[index] matches the supplied proposition. The code uses that index to look up turn-wide prohibitions, while it constructs the command from the separate proposition argument. Require an integer index within bounds and verify identity or canonical structural equality with the selected turn entry. Reject mismatches before reading prohibitions, or accept only the turn and index and resolve the proposition internally. Reproduce R5 using `/tmp/circe-pr2-evidence/probes.test.ts` at PR #2 head 91c123b06b177c6b943fe2ac2f0ffd1e8c2f9893. Keep valid requests working and run the full suite and typecheck after the change.
P1 F36. The per-proposition API drops malformed exclusionspackages/perception/src/proposals.ts:465-468logic correctness

propositionCommand computes shape issues for every proposition but never rejects a malformed turn. analyzeTurn skips any proposition with a shape issue. In contrast, compileSemanticTurn rejects the whole turn before this step.

The reproduction supplies an exclusion of A whose relations field is malformed, alongside a valid request for A. The whole-turn API correctly rejects it. The per-proposition API skips the malformed exclusion and emits the request for A, complete with current version evidence.

The unvalidated-exclusion sets do not help here because the malformed proposition is skipped before any refs enter those sets. An API promising fail-closed exclusions must not turn a malformed constraint into permission.

Fix direction. Apply one whole-turn structural validation pass to both entry points. A malformed sibling must prevent command construction, or make the low-level helper internal to an already validated turn.

Source at reviewed head · Probe R6

Captured source
465:   const issues = turn.propositions.map((candidate) => shapeIssue(candidate));
466:   const isAccepted = (candidate: SemanticProposition): boolean =>
467:     decisionsAccepted(candidate, options.acceptedDecisions);
468:   const analysis = analyzeTurn(turn, isAccepted, issues);
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 465-468, propositionCommand computes shape issues for every proposition but never rejects a malformed turn. analyzeTurn skips any proposition with a shape issue. In contrast, compileSemanticTurn rejects the whole turn before this step. Apply one whole-turn structural validation pass to both entry points. A malformed sibling must prevent command construction, or make the low-level helper internal to an already validated turn. Reproduce R6 using `/tmp/circe-pr2-evidence/probes.test.ts` at PR #2 head 91c123b06b177c6b943fe2ac2f0ffd1e8c2f9893. Keep valid requests working and run the full suite and typecheck after the change.
P1 F31. The whole-turn compiler turns queries into mutationspackages/perception/src/proposals.ts:605-616logic correctness

compileSemanticTurn no longer calls propositionCommand. It calls buildCommand directly after citation and relation checks, but never checks that the act is request or that the operation is reference or slot-fill. Those checks exist only in the other API at lines 445-449. The latest 91c123b refactor removed their effective use by the whole-turn compiler.

A resolved query/inspect proposition with an accepted task.resume capability citation and an observed target compiles to a propose command and reaches the executor. The same happens for query/reference and request/compose. All three required-behavior tests fail on an actual invocation, not merely on command construction.

The existing query regression supplies a referent citation rather than the capability citation needed to get this far, so it passes for a different rejection reason. The kernel receives no dialogue act and cannot repair the lost distinction.

Fix direction. Use one executable-proposition validator for both entry points, including capability presence and act/operation eligibility. Test non-executable acts with otherwise valid citations and evidence.

Source at reviewed head · Probe R1

Captured source
605:     const issue = citationIssue(proposition, turn, acceptedDecisions, tracesById, tracesAvailable);
606:     if (issue) {
607:       rejected.push({ index, reason: issue });
608:       return;
609:     }
610:     const relation = relationIssue(turn, proposition, index, accepted);
611:     if (relation) {
612:       rejected.push({ index, reason: relation });
613:       return;
614:     }
615:
616:     const declared = slotsOf(proposition);
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 605-616, compileSemanticTurn no longer calls propositionCommand. It calls buildCommand directly after citation and relation checks, but never checks that the act is request or that the operation is reference or slot-fill. Those checks exist only in the other API at lines 445-449. The latest 91c123b refactor removed their effective use by the whole-turn compiler. Use one executable-proposition validator for both entry points, including capability presence and act/operation eligibility. Test non-executable acts with otherwise valid citations and evidence. Reproduce R1 using `/tmp/circe-pr2-evidence/probes.test.ts` at PR #2 head 91c123b06b177c6b943fe2ac2f0ffd1e8c2f9893. Keep valid requests working and run the full suite and typecheck after the change.
P1 F32. Argument reference evidence disappears at the kernel boundarypackages/perception/src/proposals.ts:665-676logic data-integrity

The new argument-reference path adds versions for argument-only refs to command.expectedVersions. However, the kernel in runtime.ts:1051-1057 filters that list to refs present in named slot bindings. Its commit path validates and freezes only binding expectations. The new compiler therefore emits evidence that its actual consumer silently drops.

The reproduction compiles slot target A plus arguments.secondary = B. The command correctly includes versions for A and B, but authority validation receives only A and the executor receives the B argument without a B precondition. A second test makes authority validation return stale for B if asked. Execution still happens because B is never checked.

This breaks the newly claimed coverage of every executable argument reference. Seeing B in the frame at compile time does not establish that B is current at dispatch. No real backend or persistence adapter is required to reproduce the gap.

Fix direction. Until command-wide read preconditions survive validation and freezing, reject argument refs outside executable bindings. Alternatively add explicit command-level read dependencies and preserve them through every effect unit; do not silently invent target slots that change execution semantics.

Source at reviewed head · Probe R2 / R2b

Captured source
665:     // Every executable ref must carry the version the interpretation read.
666:     const evidence = expectedVersionsFromFrame(options.frame, [
667:       ...constrained.flatMap((slot) => slot.refs),
668:       ...argumentRefs,
669:     ]);
670:     for (const ref of [...constrained.flatMap((slot) => slot.refs), ...argumentRefs]) {
671:       if (!evidence.some((precondition) => entityRefKey(precondition.ref) === entityRefKey(ref))) {
672:         rejected.push({ index, reason: `no frame observation for ${entityRefKey(ref)}` });
673:         return;
674:       }
675:     }
676:     const command = buildCommand({ proposition, commandId, slots: constrained, argumentRefs, evidence });
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 665-676, The new argument-reference path adds versions for argument-only refs to command.expectedVersions. However, the kernel in runtime.ts:1051-1057 filters that list to refs present in named slot bindings. Its commit path validates and freezes only binding expectations. The new compiler therefore emits evidence that its actual consumer silently drops. Until command-wide read preconditions survive validation and freezing, reject argument refs outside executable bindings. Alternatively add explicit command-level read dependencies and preserve them through every effect unit; do not silently invent target slots that change execution semantics. Reproduce R2 / R2b using `/tmp/circe-pr2-evidence/probes.test.ts` at PR #2 head 91c123b06b177c6b943fe2ac2f0ffd1e8c2f9893. Keep valid requests working and run the full suite and typecheck after the change.
P2 F37. Frame binding checks content but omits identitypackages/perception/src/proposals.ts:533-550logic data-integrity

The whole-turn compiler checks frame.hash against turn.frameHash but does not compare frame.id with turn.frameId. propositionCommand checks both. Before the latest refactor the whole-turn compiler reached that identity check through the helper; it no longer does.

The reproduction supplies the same content and hash under a different frame id. The turn still compiles. This is a provenance and contract inconsistency; the probe does not demonstrate different world content or an unsafe mutation, so it is lower severity than the execution defects.

The accepted-turn contract and PR description record and promise both frame id and hash. Enforcing one of them in only one API makes the evidence-binding behavior depend on which compiler the caller chooses.

Fix direction. Require both frame id and hash to match in the shared validation path. If content equivalence is intentionally sufficient, make that explicit across the contract and both APIs instead of silently changing it in one.

Source at reviewed head · Probe R7

Captured source
533:   // Evidence must come from the frame the turn was decided against.
534:   if (options.frame && turn.frameHash === undefined) {
535:     return {
536:       commands: [],
537:       rejected: turn.propositions.map((_, index) => ({
538:         index,
539:         reason: "turn has no frame identity to bind the supplied frame to",
540:       })),
541:     };
542:   }
543:   if (options.frame && turn.frameHash !== undefined && options.frame.hash !== turn.frameHash) {
544:     return {
545:       commands: [],
546:       rejected: turn.propositions.map((_, index) => ({
547:         index,
548:         reason: "frame does not match the frame the turn was decided against",
549:       })),
550:     };
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 533-550, The whole-turn compiler checks frame.hash against turn.frameHash but does not compare frame.id with turn.frameId. propositionCommand checks both. Before the latest refactor the whole-turn compiler reached that identity check through the helper; it no longer does. Require both frame id and hash to match in the shared validation path. If content equivalence is intentionally sufficient, make that explicit across the contract and both APIs instead of silently changing it in one. Reproduce R7 using `/tmp/circe-pr2-evidence/probes.test.ts` at PR #2 head 91c123b06b177c6b943fe2ac2f0ffd1e8c2f9893. Keep valid requests working and run the full suite and typecheck after the change.