diff --git a/.orchestrator/BACKLOG.md b/.orchestrator/BACKLOG.md index 0502a75..dffd890 100644 --- a/.orchestrator/BACKLOG.md +++ b/.orchestrator/BACKLOG.md @@ -64,5 +64,5 @@ knows them, accepts them, and does not want them built (2026-08-06). Do not re-a `R102_BENCHMARK=1`. PLAN-014, PLAN-015 and SPEC-054 are retired unmerged. - Test the rendered reviewer EVIDENCE section (incl. spec-declared commands) directly instead of relying partly on a source-marker grep (harness-spec-command-evidence round 1, PASS backlog note). -- Relax scripts/review's model-level self-review refusal to instance/context level to match - CLAUDE.md rule 7 (owner, 2026-08-06) — stricter-than-rule today, safe to keep until needed. +- Reviewer-pairing comparison (Sol-reviews-Sol vs cross-model) as its own experiment — owner interest + 2026-08-09; not representable in R102's worker-row matrix; first datapoint free in plan-001 rounds. diff --git a/.orchestrator/REVIEW-FRAMING.md b/.orchestrator/REVIEW-FRAMING.md index 0facea0..240702d 100644 --- a/.orchestrator/REVIEW-FRAMING.md +++ b/.orchestrator/REVIEW-FRAMING.md @@ -41,3 +41,4 @@ the simpler version; a vague "could be simpler" goes to the backlog. Every block must name the affected code, a credible consequence, and the needed change. Backlog: stylistic preferences, optional refactors, speculative peer behavior, defense against implausible conditions, low-impact edge cases. +For plan reviews: the Minimal existing path must name a command and paste its argv, installed commit, input digests, exit status and complete output — any one of those missing is a FAIL. diff --git a/AGENTS.md b/AGENTS.md index 246f4f1..53afa4f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,7 +40,7 @@ An orchestrator that dispatches worker jobs from schema-validated specs, checks ## Codex on this box -- Model split (`scripts/models.json`): worker BUILD `gpt-5.6-luna`; plans `gpt-5.6-sol`; artifact reviews `gpt-5.6-luna` — never the plan author's model, or every plan is refused. +- Model split (`scripts/models.json`): worker BUILD `gpt-5.6-luna`; plans `gpt-5.6-sol`; artifact reviews `gpt-5.6-sol` — same model as the plan author is allowed: a review is always a fresh instance (rule 7; owner 2026-08-09). - Invocation: `codex exec -m -c model_reasoning_effort=high --sandbox read-only --skip-git-repo-check - // is classified from THAT attempt's @@ -37,9 +38,9 @@ # has no frontmatter of its own, so a raw stream RENAMED off its reserved PLAN-NNN.* name loses the # only handle on its sibling and falls back to --author — the plan .md it came from does not. # -# The reviewer's own MODEL is always refused as the derived/checked author (nothing grades its own -# work), and the author's whole VENDOR is refused wherever no author model is on record. So one -# vendor can hold both roles, on different models — set in scripts/models.json, never here. +# The self-review rule is INSTANCE-level (rule 7): a fresh reviewer instance may share the author's +# model. The author's whole VENDOR is refused wherever no author model is on record. So one vendor +# can hold both roles, same model included — the pairing is set in scripts/models.json, never here. # # Usage: scripts/review --topic --author --context FILE [--context FILE]... # [--] [PROMPT...] @@ -429,7 +430,7 @@ done # id comes from the artifact's own frontmatter (derived above), not its filename, so a rename cannot # change it. For a PLAN artifact the topic is DERIVED, not chosen: it must be exactly plan-nnn (id # lowercased, digits verbatim), which already satisfies the slug regex above. These checks run -# BEFORE the author-mismatch and codex self-review refusals so the exit code names the REAL reason: +# BEFORE the author-mismatch and vendor-provenance refusals so the exit code names the REAL reason: # multiple distinct plans is exit 2 (not merely self-review 4), and a single plan under a renamed # topic is a cap-reset attempt (exit 6) even for a Sol plan that would otherwise exit 4. # Non-PLAN contexts keep caller-chosen topics: they have no durable artifact id to bind to. @@ -457,19 +458,16 @@ if (( ${#plan_ids[@]} == 1 )); then fi # -------------------------------------------------------------------------------------------------- -# B18 self-review guard: the reviewer must not be the artifact's own author. Recorded provenance -# decides it at MODEL level, matching the dispatcher's worker-diff rule — a different model of the -# same vendor is a different reviewer. Branches 3–5 record no author model, so provenance that -# reaches only the vendor keeps refusing the whole vendor: the guard weakens only where the author -# model is actually on record. -if [[ "$derived" == "$REVIEWER_VENDOR" && ( "$derived_model" == "-" || "$derived_model" == "$MODEL" ) ]]; then - if [[ "$derived_model" == "-" ]]; then - echo "review: REFUSED — recorded provenance shows this is a $REVIEWER_VENDOR-authored artifact with no author model on record, and this review uses $REVIEWER_VENDOR model $MODEL." >&2 - else - echo "review: REFUSED — recorded provenance shows this artifact was authored by $derived_model, which is the model this review uses." >&2 - fi +# B18 self-review guard, relaxed to INSTANCE level (CLAUDE.md rule 7; owner 2026-08-06, reaffirmed +# 2026-08-09: Sol may review Sol). Every invocation of this script spawns a fresh reviewer instance, +# so an artifact whose author MODEL is on record is never the reviewer's own context — even when the +# model matches. What stays refused is vendor-only provenance (branches 3–5 record no author model): +# there the asserted --author is the only word we have, a fallback derivation carries no model, and +# fail-closed beats guessing (SECURITY.md gap 8). +if [[ "$derived" == "$REVIEWER_VENDOR" && "$derived_model" == "-" ]]; then + echo "review: REFUSED — recorded provenance shows this is a $REVIEWER_VENDOR-authored artifact with no author model on record, and this review uses $REVIEWER_VENDOR model $MODEL." >&2 for line in "${evidence_lines[@]}"; do echo " evidence: $line" >&2; done - echo " Self-review is not allowed (CLAUDE.md). Use a reviewer model the artifact's author is not." >&2 + echo " Without a recorded author model, instance separation cannot be shown (CLAUDE.md rule 7); use a reviewer of another vendor." >&2 exit 4 fi diff --git a/tests/codex_plan.sh b/tests/codex_plan.sh index 40db260..d1403b8 100755 --- a/tests/codex_plan.sh +++ b/tests/codex_plan.sh @@ -258,10 +258,10 @@ assert_file "$run_dir/PLAN-012.stdout" # the refused oversized default retaine # under it. Every fixture below is a way the first (substring-scan) version of this check was # fooled. Each valid brief fixture is padded to an exact line count so the 400/401 boundary is # tested for real — the earlier version tested 399 and called it 400. -sections=(Outcome "Scope and non-goals" "Frozen decisions" Assumptions "Earliest falsifiable proof" \ - Slices Gates Verification Rollback Deferred "Definition of done") -valid_brief() { # $1 = total line count (>= 22: eleven headings + one content line each) - local n=$1 filler=$(( $1 - 22 )) out="" +sections=(Outcome "Scope and non-goals" "Frozen decisions" Assumptions "Minimal existing path" \ + "Earliest falsifiable proof" Slices Gates Verification Rollback Deferred "Definition of done") +valid_brief() { # $1 = total line count (>= 24: twelve headings + one content line each) + local n=$1 filler=$(( $1 - 24 )) out="" for s in "${sections[@]}"; do out+="## $s"$'\n'"content for $s"$'\n'; done ((filler > 0)) && out+="$(body "$filler")"$'\n' printf '%s' "${out%$'\n'}" @@ -277,9 +277,9 @@ for suffix in '' $'\n'; do [ "$(try_brief "$(valid_brief 401)$suffix")" = refused ] || fail "a 401-line brief was accepted (cap is 400)" done -# Structure, not substrings: each of these contains all eleven heading strings and must still fail. +# Structure, not substrings: each of these contains all twelve heading strings and must still fail. all_on_one_line=""; for s in "${sections[@]}"; do all_on_one_line+="## $s "; done -[ "$(try_brief "$all_on_one_line")" = refused ] || fail "eleven headings crammed on ONE line were accepted as a brief" +[ "$(try_brief "$all_on_one_line")" = refused ] || fail "twelve headings crammed on ONE line were accepted as a brief" fenced=$'```\n'"$(valid_brief 30)"$'\n```\nprose' [ "$(try_brief "$fenced")" = refused ] || fail "headings inside a code fence were accepted as real sections" @@ -290,7 +290,8 @@ done [ "$(try_brief "$empty_section")" = refused ] || fail "a brief with an EMPTY '## Gates' section was accepted" out_of_order=""; for s in "Scope and non-goals" Outcome "Frozen decisions" Assumptions \ - "Earliest falsifiable proof" Slices Gates Verification Rollback Deferred "Definition of done"; do + "Minimal existing path" "Earliest falsifiable proof" Slices Gates Verification Rollback Deferred \ + "Definition of done"; do out_of_order+="## $s"$'\n'"content"$'\n' done [ "$(try_brief "$out_of_order")" = refused ] || fail "out-of-order sections were accepted" @@ -301,12 +302,26 @@ near_miss="$(valid_brief 30)"; near_miss="${near_miss/'## Outcome'/'## Outcomes' missing="$(valid_brief 30)"; missing="${missing/'## Earliest falsifiable proof'/'## Notes'}" [ "$(try_brief "$missing")" = refused ] || fail "a brief with no 'Earliest falsifiable proof' section was accepted" +# The gate that exists to stop a program being built over installed code must itself be refused when +# absent — a heading the parser does not require is advice, not a gate (slice1-diff review round 1). +no_mep="$(valid_brief 30)"; no_mep="${no_mep/'## Minimal existing path'/'## Notes'}" +[ "$(try_brief "$no_mep")" = refused ] || fail "a brief with no 'Minimal existing path' section was accepted" +empty_mep="${no_mep/'## Notes'$'\n'/'## Minimal existing path'$'\n'}" +empty_mep="${empty_mep/'content for Minimal existing path'/}" +[ "$(try_brief "$empty_mep")" = refused ] || fail "an EMPTY '## Minimal existing path' section was accepted" + # A required heading twice = two answers to the same question, and nothing says which one binds. duplicated="$(valid_brief 30)"$'\n## Outcome\na second, contradictory outcome' [ "$(try_brief "$duplicated")" = refused ] || fail "a brief with TWO '## Outcome' sections was accepted" # The brief prompt must actually ask for the anatomy — the cap alone does not make a brief a brief. grep -qi 'earliest falsifiable' "$prompt_file" || fail "brief prompt missing the falsifiable-proof section" +# The minimal-existing-path gate is worthless as a bare heading: the prompt must demand the run's +# own evidence, or an author can assert insufficiency with nothing a reviewer can re-run. +grep -qi 'minimal existing path' "$prompt_file" || fail "brief prompt missing the minimal-existing-path section" +for demanded in argv 'installed commit' digest 'exit status' 'COMPLETE output'; do + grep -qi "$demanded" "$prompt_file" || fail "brief prompt does not demand $demanded for the minimal existing path" +done grep -qi 'independently shippable' "$prompt_file" || fail "brief prompt missing the slices section" # --- the regression the stdin fix exists for ---------------------------------------------------- diff --git a/tests/prose_cap.sh b/tests/prose_cap.sh index 3709535..1da93ec 100755 --- a/tests/prose_cap.sh +++ b/tests/prose_cap.sh @@ -95,12 +95,16 @@ for f in "${md_files[@]}"; do fi done -# 2. Totals: 540 lines AND 60,000 bytes — the byte cap stops one-line walls of text that a line +# 2. Totals: 580 lines AND 60,000 bytes — the byte cap stops one-line walls of text that a line # count cannot see. A zero total means the counting broke (this exact check once passed # vacuously with "0/600" because awk was invoked wrong) — zero is a failure, not a clean repo. +# Raised 540 -> 580 on 2026-08-11, deliberately and under review, to buy the README the room to +# describe the machinery instead of gesturing at it: the owner read the 540-era README and said +# it "sounds stupid". The cap exists to stop PROCESS prose accreting, and the README is the only +# external explanation this repo has. Per-file caps are untouched, so the room is not general. [ "$total_lines" -gt 0 ] || bad "total tracked markdown counted as 0 lines — the count is broken, not the repo empty" -[ "$total_lines" -le 540 ] && ok "total tracked markdown: $total_lines/540 lines" \ - || bad "total tracked markdown is $total_lines lines — cap is 540. Delete before you add." +[ "$total_lines" -le 580 ] && ok "total tracked markdown: $total_lines/580 lines" \ + || bad "total tracked markdown is $total_lines lines — cap is 580. Delete before you add." [ "$total_bytes" -le 60000 ] && ok "total tracked markdown: $total_bytes/60000 bytes" \ || bad "total tracked markdown is $total_bytes bytes — cap is 60000. Delete before you add." diff --git a/tests/r102_benchmark.sh b/tests/r102_benchmark.sh index e8dd2da..214979e 100755 --- a/tests/r102_benchmark.sh +++ b/tests/r102_benchmark.sh @@ -641,7 +641,7 @@ if trial_dir.is_dir(): orchestrator_evidence = context.metadata["r102"]["orchestrator_evidence"] check("agent orchestrator evidence", isinstance(orchestrator_evidence, dict) and orchestrator_evidence.get("vendor") == "claude" and - orchestrator_evidence.get("model") == "claude-opus-4-8" and + orchestrator_evidence.get("model") == "claude-opus-5" and orchestrator_evidence.get("log") == "orchestrator-round-0.log") codex_locator = "if [ -s ~/.nvm/nvm.sh ]; then . ~/.nvm/nvm.sh; fi; " check("in-container Codex worker uses the nvm locator", diff --git a/tests/review_authorship.sh b/tests/review_authorship.sh index b2fc01c..f8c651f 100755 --- a/tests/review_authorship.sh +++ b/tests/review_authorship.sh @@ -7,9 +7,9 @@ # stub codex binary (no network, no real Codex/Claude invoked): # (a) a forged --author that disagrees with recorded provenance is REFUSED (exit 6), never routed; # (b) a call with no provenance evidence at all is REFUSED (fail closed), never trusted; -# (c) a correctly-derived, matching --author still runs and routes through the normal cap/reviewer -# machinery (a claude-authored topic proceeds; a codex-authored topic is refused as self-review, -# exit 4 — not because the flag said so, but because the derivation agreed). +# (c) a correctly-derived, matching --author routes through the normal cap/reviewer machinery; a +# RECORDED author model runs even when it equals the reviewer's (instance-level rule 7, owner +# 2026-08-09), while vendor-only provenance stays refused whole (exit 4, fail closed). set -uo pipefail cd "$(dirname "$0")/.." ROOT="$PWD" @@ -29,9 +29,9 @@ cp -p scripts/models_check.py "$tmp/repo/scripts/models_check.py" # dispatch integrate grades from a write-stripped tree; cp -p carries that read-only mode into # this test's own scratch copy, which case 5 must rewrite — make the copy writable regardless. chmod u+w "$tmp/repo/scripts/models.json" -# The self-review gate compares the artifact's recorded author MODEL with the reviewer's, so these -# assertions pin the reviewer role instead of inheriting the owner's live config — otherwise -# flipping models.json turns an exit-4 assertion into a silent pass. +# The self-review gate keys on whether an author MODEL is on record at all, so these assertions pin +# the reviewer role instead of inheriting the owner's live config — otherwise flipping models.json +# changes which arm each fixture exercises and turns assertions into silent passes. pin_reviewer() { # $1 model python3 - "$tmp/repo/scripts/models.json" "$1" <<'PIN' import json, sys @@ -158,19 +158,20 @@ grep -q 'stub review verdict' .orchestrator/reviews/real-claude-topic/round-1.md && ok "round 1 output came from the (stub) Codex reviewer, i.e. cross-vendor routing held" \ || bad "round 1 output did not come from the stub reviewer" -# - codex-attempt evidence + matching --author codex is refused as self-review (exit 4, not 6): -# the derivation and the flag AGREE this time, so this exercises the vendor gate, not the -# mismatch gate — proving the fix didn't just relabel every refusal as a mismatch. +# - codex-attempt evidence + matching --author codex + a RECORDED worker_model equal to the +# reviewer's model RUNS: the self-review rule is instance-level (rule 7; owner 2026-08-09) and +# a fresh reviewer instance may share the author's model. The recorded model is what proves +# instance separation is decidable — contrast 3c, where no model is on record. scripts/review --topic real-codex-topic --author codex --context .orchestrator/attempts/SPEC-901/1/diff.patch "please review" >/dev/null 2>&1 rc=$? -[ "$rc" = 4 ] && ok "correctly-derived codex authorship is refused as self-review (exit 4)" \ - || bad "correctly-derived codex authorship gave exit $rc, expected 4 (self-review vendor gate)" -[ -e .orchestrator/reviews/real-codex-topic ] && bad "self-review refusal still created review state" \ - || ok "self-review refusal writes nothing" - -# 3b. SAME VENDOR, DIFFERENT MODEL: the gate is model-level, matching the dispatcher's worker-diff -# rule, so a sol-authored attempt reviewed by luna runs. This is the case that lets one vendor -# supply both the orchestrator and its artifact reviewer. +[ "$rc" = 0 ] && ok "recorded same-model authorship runs under a fresh instance (exit 0)" \ + || bad "recorded same-model authorship gave exit $rc, expected 0 (instance-level rule 7)" +[ -s .orchestrator/reviews/real-codex-topic/round-1.md ] \ + && ok "same-model review wrote its round-1 verdict" || bad "no round-1.md for the same-model review" + +# 3b. SAME VENDOR, DIFFERENT MODEL: also runs — a recorded author model plus any fresh reviewer +# instance satisfies rule 7. This is the case that lets one vendor supply both the orchestrator +# and its artifact reviewer. pin_reviewer gpt-5.6-luna scripts/review --topic cross-model-topic --author codex --context .orchestrator/attempts/SPEC-901/1/diff.patch "please review" >/dev/null 2>&1 rc=$? diff --git a/tests/review_cap.sh b/tests/review_cap.sh index 15fbede..3d63683 100755 --- a/tests/review_cap.sh +++ b/tests/review_cap.sh @@ -2,7 +2,7 @@ # The review-round cap must live in code: a prose cap already lost once to a ten-round review loop # (~10,000 lines of revisions later replaced by a ~50-line hand fix). scripts/review allows five # rounds per topic, refuses the sixth, counts ONLY round-N.md files as rounds (a sibling artifact -# once consumed a phantom round), refuses Codex-authored artifacts (its reviewer is Codex), and +# once consumed a phantom round), refuses Codex-vendor artifacts with no author model on record, and # must hold the cap under concurrent invocations. Codex is always a local stub here. set -uo pipefail cd "$(dirname "$0")/.." @@ -55,12 +55,15 @@ if scripts/review --topic 'Bad Slug!' --author claude --context claude-note.md x if scripts/review --topic demo-topic --context claude-note.md x 2>/dev/null; then bad "accepted a review with no --author"; else ok "refuses a missing --author"; fi if scripts/review --topic demo-topic --author gemini --context claude-note.md x 2>/dev/null; then bad "accepted an unknown author"; else ok "refuses an unknown author"; fi -# 2. Artifacts authored by the reviewer's own model are refused — nothing grades its own work. -# (--author here MATCHES the derived provenance, so this exercises the vendor refusal, not B18's -# mismatch refusal — see tests/review_authorship.sh for the mismatch/no-provenance cases.) -scripts/review --topic demo-topic --author codex --context .orchestrator/attempts/SPEC-900/1/diff.patch "review this codex plan" >/dev/null 2>&1 +# 2. Vendor-only provenance under a same-vendor reviewer is refused — with no author model on +# record, instance separation is undecidable (rule 7), so the whole vendor fails closed. A worker +# worktree derives 'codex' by path with no model. (--author MATCHES the derived provenance, so this +# exercises the vendor refusal, not B18's mismatch — see tests/review_authorship.sh for the rest.) +mkdir -p .worktrees/SPEC-900-1 +printf 'worker worktree note, no recorded model\n' > .worktrees/SPEC-900-1/notes.txt +scripts/review --topic demo-topic --author codex --context .worktrees/SPEC-900-1/notes.txt "review this codex artifact" >/dev/null 2>&1 rc=$? -[ "$rc" = 4 ] && ok "Codex-authored artifact refused (exit 4)" || bad "Codex-on-Codex not refused (exit $rc)" +[ "$rc" = 4 ] && ok "vendor-only codex provenance refused under codex reviewer (exit 4)" || bad "vendor-only Codex-on-Codex not refused (exit $rc)" [ -e .orchestrator/reviews/demo-topic ] && bad "refused author still created state" || ok "author refusal writes nothing" # 3. Rounds 1-5 run and are recorded; sibling artifacts in the topic dir do NOT consume rounds. diff --git a/tests/review_kimi.sh b/tests/review_kimi.sh index 38885d2..4d71bbb 100755 --- a/tests/review_kimi.sh +++ b/tests/review_kimi.sh @@ -5,7 +5,8 @@ # (b) a kimi-authored artifact (attempt worker_model=kimi-k3) derives 'kimi' and routes # normally when the reviewer is codex (cross-vendor, not self-review); # (c) when the reviewer IS kimi (orchestrator_artifact_reviewer=kimi-k3 in models.json), -# a kimi-authored artifact is REFUSED as self-review (exit 4, B18 security gate); +# a kimi-vendor artifact with NO author model on record is REFUSED (exit 4): instance +# separation cannot be shown, so it fails closed (R128 relaxed this to instance level); # (d) when the reviewer is kimi, a claude-authored artifact runs through the kimi dispatch # path and the adapter recovers the stub kimi's stream-json output as the round output; # (e) a prompt exceeding 120000 bytes is refused by the adapter before kimi is invoked. @@ -91,15 +92,17 @@ cfg["roles"]["orchestrator_artifact_reviewer"] = {"model": "kimi-k3", "effort": json.dump(cfg, open(sys.argv[1], "w")) GUT -# ---- (c) self-review guard: kimi reviewer refuses kimi-authored artifact (exit 4, B18) ------- +# ---- (c) same-model kimi review RUNS: recorded worker_model=kimi-k3 equals the reviewer, and the +# self-review rule is instance-level (rule 7; owner 2026-08-09) — a fresh instance may share +# the author's model. Vendor-only provenance staying refused is proven in review_authorship.sh. scripts/review --topic kimi-self-review --author kimi \ --context .orchestrator/attempts/SPEC-501/1/diff.patch "please review" >/dev/null 2>&1 rc=$? -[ "$rc" = 4 ] && ok "kimi-authored artifact refused as self-review under kimi reviewer (exit 4, B18)" \ - || bad "kimi self-review not refused: got exit $rc, expected 4" -[ -e .orchestrator/reviews/kimi-self-review ] \ - && bad "self-review refusal still created review state" \ - || ok "self-review refusal writes nothing" +[ "$rc" = 0 ] && ok "kimi-authored artifact runs under a fresh kimi reviewer instance (exit 0)" \ + || bad "same-model kimi review gave exit $rc, expected 0 (instance-level rule 7)" +grep -q 'stub kimi verdict' .orchestrator/reviews/kimi-self-review/round-1.md 2>/dev/null \ + && ok "same-model kimi round came from the stub kimi reviewer" \ + || bad "no stub kimi verdict recorded for the same-model kimi round" # ---- (d) kimi dispatch: claude-authored artifact invokes stub kimi and recovers content ------ scripts/review --topic kimi-dispatch-claude --author claude \ diff --git a/tests/review_plan_authorship.sh b/tests/review_plan_authorship.sh index 032e287..ba8b0c9 100755 --- a/tests/review_plan_authorship.sh +++ b/tests/review_plan_authorship.sh @@ -5,9 +5,9 @@ # (1) Plan authorship derives from the .md frontmatter's author_model via the models.json # vendor_map — spec_author is a ROLE, not a vendor, so the old unconditional-codex namespace # rule would misclassify the moment the owner flips roles.spec_author in models.json. A -# Claude-authored plan proceeds to Codex review; a Sol-authored plan is still refused as -# self-review (exit 4); broken provenance (missing sibling .md, missing frontmatter, an -# unmapped model) is refused outright, never guessed. +# Claude-authored plan proceeds to Codex review; a Sol-authored plan runs under a Sol reviewer +# (instance-level rule 7, owner 2026-08-09); broken provenance (missing sibling .md, missing +# frontmatter, an unmapped model) is refused outright, never guessed. # (2) Review round dirs BIND to the artifact identity: a PLAN-NNN context forces --topic plan-nnn, # so a renamed topic can no longer mint a fresh directory and reset the 5-round cap. set -uo pipefail @@ -25,8 +25,8 @@ cp -p scripts/vendor_adapters.py "$tmp/repo/scripts/vendor_adapters.py" cp -p scripts/models.json "$tmp/repo/scripts/models.json" cp -p scripts/models_check.py "$tmp/repo/scripts/models_check.py" chmod u+w "$tmp/repo/scripts/models.json" -# The self-review gate compares author MODEL with reviewer model, so the sol fixtures below only -# exercise it while the reviewer IS the sol model. Pin it rather than inherit the owner's config. +# The sol fixtures below prove the same-model PROCEED arm, which only means anything while the +# reviewer IS the sol model. Pin it rather than inherit the owner's config. python3 - "$tmp/repo/scripts/models.json" <<'PIN' import json, sys cfg = json.load(open(sys.argv[1])) @@ -138,11 +138,13 @@ for wrong in plan-102 fresh-slug-reset; do || ok "renamed topic '$wrong' writes nothing" done -# 3. Legacy Sol-authored plan: derivation says codex. Matching --author codex hits the self-review -# vendor gate (exit 4); forged --author claude hits the mismatch gate (exit 6). +# 3. Legacy Sol-authored plan under a Sol reviewer: the recorded author_model proves instance +# separation is decidable, so the same-model review RUNS (rule 7; owner 2026-08-09 — Sol may +# review Sol); forged --author claude still hits the mismatch gate (exit 6). scripts/review --topic plan-001 --author codex --context .orchestrator/plans/PLAN-001.md "review" >/dev/null 2>&1 rc=$? -[ "$rc" = 4 ] && ok "sol-authored plan refused as self-review (exit 4)" || bad "sol-authored plan gave exit $rc, expected 4" +[ "$rc" = 0 ] && ok "sol-authored plan runs under a sol reviewer (exit 0, rule 7)" || bad "sol plan under sol reviewer gave exit $rc, expected 0" +[ -s .orchestrator/reviews/plan-001/round-1.md ] && ok "same-model plan review wrote round-1.md" || bad "no round-1.md for the same-model plan review" scripts/review --topic plan-001 --author claude --context .orchestrator/plans/PLAN-001.md "review" >/dev/null 2>&1 rc=$? [ "$rc" = 6 ] && ok "forged --author claude on a sol plan refused (exit 6)" || bad "forged claude on sol plan gave exit $rc, expected 6" @@ -295,11 +297,12 @@ scripts/review --topic codex-attempt-reslug --author codex --context .orchestrat rc=$? [ "$rc" = 6 ] && ok "in-attempt codex plan under a renamed topic refuses via binding (exit 6, not 4)" \ || bad "in-attempt codex plan renamed topic gave exit $rc, expected 6" -# ...and under its CORRECT bound topic it is the self-review gate (exit 4): derivation held. +# ...and under its CORRECT bound topic the recorded author_model runs under a fresh same-model +# instance (rule 7; owner 2026-08-09) — derivation held, and the binding gate ran first. scripts/review --topic plan-402 --author codex --context .orchestrator/attempts/SPEC-402/1/PLAN-402.md "review" >/dev/null 2>&1 rc=$? -[ "$rc" = 4 ] && ok "in-attempt codex plan under its bound topic is self-review (exit 4)" \ - || bad "in-attempt codex plan bound topic gave exit $rc, expected 4" +[ "$rc" = 0 ] && ok "in-attempt codex plan under its bound topic runs (exit 0, rule 7)" \ + || bad "in-attempt codex plan bound topic gave exit $rc, expected 0" # (c) CONFLICT: a Claude attempt hosting a Sol-authored plan — the attempt's worker_model vendor and # the plan's frontmatter author_model vendor disagree. Ambiguous provenance refuses (exit 2), # so an attempt cannot host a foreign-vendor plan to launder its authorship.