From e30a904c72538238b8b1e1d25fab8c1d8a1f02b5 Mon Sep 17 00:00:00 2001 From: David Sifry Date: Sat, 26 Sep 2026 19:05:21 -0700 Subject: [PATCH 1/7] fix(judge): run judge CLIs in an isolated directory, not the reviewed repo The claude-cli/ and codex/ judges started the model CLI in metareview's own working directory, which is normally the repository under review. A headless CLI still loads that directory's project configuration without the interactive trust prompt, and disabling tools does not switch it off, so reviewing an untrusted checkout (a fork's PR branch) was not isolated from it. Verified offline: under 0.13.2, `fsm judge --model claude-cli/...` run inside a repo carrying project hooks fired them; with this change it does not. - Every attempt runs in a fresh, empty, 0700 temp dir (judge.isolatedDir), removed after the call; if one cannot be made the attempt fails as a transport error and nothing is spawned. - claude: --setting-sources user --strict-mcp-config (not --bare, which skips the keychain and so the OAuth session). - codex: -c project_doc_max_bytes=0, covering the escalation path's materialized tree, which is still honoured as an explicit work dir. Refs: mr-84y, #167 (W0), found reviewing #165. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 16 +++ internal/fsm/cli/deps.go | 6 +- internal/fsm/judge/claude.go | 14 +- internal/fsm/judge/codex.go | 17 ++- internal/fsm/judge/codex_test.go | 14 +- internal/fsm/judge/isolate.go | 18 +++ internal/fsm/judge/isolation_test.go | 195 +++++++++++++++++++++++++++ 7 files changed, 268 insertions(+), 12 deletions(-) create mode 100644 internal/fsm/judge/isolate.go create mode 100644 internal/fsm/judge/isolation_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 004e8763..63104d49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## Unreleased + +### Security + +- **The `claude-cli/` and `codex/` judges no longer run inside the repository under review.** Each + judge attempt now starts the model CLI in a fresh, empty, private temp directory, removed after + the call. Before, the CLI inherited metareview's working directory, which is normally the + repository being reviewed, and in headless mode a CLI can still load that repository's + project-level configuration without the interactive trust prompt. Disabling tools does not + switch that configuration off. So reviewing an untrusted checkout, such as a fork's PR branch, + was not isolated from it. The Claude judge also passes `--setting-sources user + --strict-mcp-config`. The Codex judge passes `-c project_doc_max_bytes=0`, which also covers the + escalation path's materialized evidence tree (still honoured as an explicit work dir). If no + private directory can be made, the attempt fails as a transport error and nothing is spawned in + the caller's directory. Reported while reviewing #165; plan in #167 (W0). + ## 0.13.2 - 2026-09-26 ### Fixed diff --git a/internal/fsm/cli/deps.go b/internal/fsm/cli/deps.go index 5bdc1ec1..41ad14b6 100644 --- a/internal/fsm/cli/deps.go +++ b/internal/fsm/cli/deps.go @@ -134,9 +134,9 @@ func realClaudeExec(ctx context.Context, dir string, args []string, stdin string // metareview never handles the token itself. func realCodexExec(ctx context.Context, dir string, args []string, stdin string) ([]byte, int, error) { cmd := exec.CommandContext(ctx, codexBin, args...) - // Empty means inherit, which is metareview's own repository. A caller that has materialized - // an evidence tree passes it here so that tree is what relative paths resolve against. This - // does not stop the CLI reading elsewhere - cmd.Dir is a starting point, not a jail. + // The judge always passes a directory: a fresh isolated one (judge.isolatedDir), or a + // materialized evidence tree a caller confined it to. This does not stop the CLI reading + // elsewhere - cmd.Dir is a starting point, not a jail. cmd.Dir = dir cmd.Stdin = strings.NewReader(stdin) var out bytes.Buffer diff --git a/internal/fsm/judge/claude.go b/internal/fsm/judge/claude.go index e20beaf2..b2509488 100644 --- a/internal/fsm/judge/claude.go +++ b/internal/fsm/judge/claude.go @@ -79,6 +79,12 @@ func (j *claudeJudge) Call(ctx context.Context, r Request) (v Verdict, err error // remains a denial rather than a hang in a headless run. "--disallowed-tools", "*", "--permission-mode", "dontAsk", + // Behind the isolated working directory (isolatedDir): load the user's settings only, + // never a project's or a local override, and no MCP server that is not passed explicitly + // (none is). --bare would also skip hooks but skips the keychain, and with it the OAuth + // session this transport exists to use. + "--setting-sources", "user", + "--strict-mcp-config", // The system prompt must always be passed. Without it `claude -p` can // silently fall back to Haiku for the work turn even with --model set — // the judge would then be a different model than the one recorded in the @@ -105,9 +111,15 @@ func (j *claudeJudge) Call(ctx context.Context, r Request) (v Verdict, err error case <-j.clock.After(backoff(classBackoff, attempt-1)): } } + dir, cleanup, dirErr := isolatedDir() + if dirErr != nil { + lastErr = errs.E(CodeJudgeTransport, "claude could not be given an isolated working directory: "+dirErr.Error(), "provider", "claude-cli") + continue + } actx, cancel := context.WithTimeout(ctx, j.timeout()) - stdout, code, execErr := j.exec(actx, "", args, user) + stdout, code, execErr := j.exec(actx, dir, args, user) cancel() + cleanup() text, tokens, found, transient := parseClaudeResult(stdout) v.Tokens = v.Tokens.Add(tokens) diff --git a/internal/fsm/judge/codex.go b/internal/fsm/judge/codex.go index a398d92c..9a9ecc71 100644 --- a/internal/fsm/judge/codex.go +++ b/internal/fsm/judge/codex.go @@ -38,7 +38,7 @@ type codexJudge struct { exec CodexExec nonce func() string clock Clock - workDir string // empty: inherit the caller's directory + workDir string // empty: a fresh isolated directory per attempt (isolatedDir) attemptTimeout time.Duration // zero: the AttemptTimeout default } @@ -71,6 +71,10 @@ func (j *codexJudge) Call(ctx context.Context, r Request) (v Verdict, err error) "--color", "never", "-m", wireModel(r.Model), "-c", "model_reasoning_effort=" + r.Effort, + // AGENTS.md is read from the working directory up and would be spliced into the judge's + // instructions. The default directory is isolated and empty, but an explicit work dir is a + // materialized copy of the change under review, which can carry one: project docs off. + "-c", "project_doc_max_bytes=0", "-", // the prompt arrives on stdin, never as an argv the process table would show } // The same attempt ceiling, per-attempt deadline and backoff as the HTTP arm. @@ -98,9 +102,18 @@ func (j *codexJudge) Call(ctx context.Context, r Request) (v Verdict, err error) case <-j.clock.After(backoff(classBackoff, attempt-1)): } } + dir, cleanup := j.workDir, func() {} + if dir == "" { + var dirErr error + if dir, cleanup, dirErr = isolatedDir(); dirErr != nil { + lastErr = errs.E(CodeJudgeTransport, "codex could not be given an isolated working directory: "+dirErr.Error(), "provider", "codex") + continue + } + } actx, cancel := context.WithTimeout(ctx, j.timeout()) - stdout, code, execErr := j.exec(actx, j.workDir, args, prompt) + stdout, code, execErr := j.exec(actx, dir, args, prompt) cancel() + cleanup() text, tokens, found := parseCodexEvents(stdout) v.Tokens = v.Tokens.Add(tokens) diff --git a/internal/fsm/judge/codex_test.go b/internal/fsm/judge/codex_test.go index c99dd41c..13966010 100644 --- a/internal/fsm/judge/codex_test.go +++ b/internal/fsm/judge/codex_test.go @@ -440,8 +440,10 @@ func TestCodexJudgeRunsInItsConfiguredWorkDir(t *testing.T) { } } -// Unset means "inherit", which is today's behaviour: the seam must not invent a directory. -func TestCodexJudgeWithNoWorkDirPassesEmpty(t *testing.T) { +// Unset no longer means "inherit": the caller's directory is the repository under review, whose +// AGENTS.md a headless codex would splice into the judge's instructions. Unset means a fresh, +// empty, isolated directory (see isolation_test.go for the full contract). +func TestCodexJudgeWithNoWorkDirIsIsolated(t *testing.T) { var gotDir = "unset" f := func(_ context.Context, dir string, _ []string, _ string) ([]byte, int, error) { gotDir = dir @@ -451,8 +453,8 @@ func TestCodexJudgeWithNoWorkDirPassesEmpty(t *testing.T) { if _, err := j.Call(context.Background(), codexRequest()); err != nil { t.Fatalf("call: %v", err) } - if gotDir != "" { - t.Errorf("exec dir = %q, want empty when unconfigured", gotDir) + if gotDir == "" || gotDir == "unset" || !strings.Contains(gotDir, "metareview-judge-") { + t.Errorf("exec dir = %q, want a fresh isolated directory when unconfigured", gotDir) } } @@ -481,8 +483,8 @@ func TestWithCodexWorkDirConfinesOnlyCodexJudges(t *testing.T) { if _, err := base.Call(context.Background(), codexRequest()); err != nil { t.Fatalf("base call: %v", err) } - if gotDir != "" { - t.Errorf("confining one judge changed the original, which ran in %q", gotDir) + if gotDir == "/evidence" || !strings.Contains(gotDir, "metareview-judge-") { + t.Errorf("confining one judge changed the original, which ran in %q (want its own isolated dir)", gotDir) } // a judge that is not the HTTP/codex router is returned as-is diff --git a/internal/fsm/judge/isolate.go b/internal/fsm/judge/isolate.go new file mode 100644 index 00000000..f46fd4f5 --- /dev/null +++ b/internal/fsm/judge/isolate.go @@ -0,0 +1,18 @@ +package judge + +import "os" + +// isolatedDir makes the private, empty working directory a judge CLI runs in, and returns its +// cleanup. A headless CLI still loads the project configuration of the directory it starts in +// (Claude Code's .claude/settings.json hooks and CLAUDE.md; Codex's AGENTS.md), and neither +// disabling tools nor a read-only sandbox switches that off. Starting inside the repository under +// review would let that repository run code, or rewrite the judge's instructions, before the +// judge reads a byte of the prompt. The prompt arrives on stdin, so the CLI needs no files of the +// repository at all. A var so the failure path is testable. +var isolatedDir = func() (string, func(), error) { + dir, err := os.MkdirTemp("", "metareview-judge-") + if err != nil { + return "", func() {}, err + } + return dir, func() { _ = os.RemoveAll(dir) }, nil +} diff --git a/internal/fsm/judge/isolation_test.go b/internal/fsm/judge/isolation_test.go new file mode 100644 index 00000000..e7a52e14 --- /dev/null +++ b/internal/fsm/judge/isolation_test.go @@ -0,0 +1,195 @@ +package judge + +import ( + "context" + "errors" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/dsifry/metareview/internal/fsm/errs" +) + +// dirProbe is a CLI exec seam that records the working directory each attempt was given and what +// that directory held at the moment the CLI would have started. +type dirProbe struct { + dirs []string + contents [][]string + stdout string + code int +} + +func (p *dirProbe) exec(_ context.Context, dir string, _ []string, _ string) ([]byte, int, error) { + p.dirs = append(p.dirs, dir) + entries, err := os.ReadDir(dir) + names := []string{} + if err != nil { + names = append(names, "ERR: "+err.Error()) + } + for _, e := range entries { + names = append(names, e.Name()) + } + p.contents = append(p.contents, names) + return []byte(p.stdout), p.code, nil +} + +// The judge CLIs must never start inside the repository under review. A headless CLI still loads +// that directory's project configuration (Claude Code's .claude/settings.json hooks and +// CLAUDE.md, Codex's AGENTS.md), without the interactive trust prompt, and disabling tools does not +// switch that off. So every attempt runs in a fresh, empty, private directory that is removed after. +func assertIsolated(t *testing.T, p *dirProbe, wantAttempts int) { + t.Helper() + if len(p.dirs) != wantAttempts { + t.Fatalf("attempts: got %d want %d", len(p.dirs), wantAttempts) + } + cwd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + for i, dir := range p.dirs { + if dir == "" || !filepath.IsAbs(dir) { + t.Fatalf("attempt %d ran in %q: an empty dir inherits the caller's cwd (the reviewed repo)", i+1, dir) + } + if rel, err := filepath.Rel(cwd, dir); err == nil && !strings.HasPrefix(rel, "..") { + t.Fatalf("attempt %d ran inside the caller's tree: %s", i+1, dir) + } + if len(p.contents[i]) != 0 { + t.Fatalf("attempt %d's directory was not empty: %v", i+1, p.contents[i]) + } + if _, err := os.Stat(dir); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("attempt %d's directory %s was not removed (stat: %v)", i+1, dir, err) + } + } +} + +func TestClaudeJudgeRunsIsolatedFromTheReviewedRepo(t *testing.T) { + p := &dirProbe{stdout: claudeJSON(`{"reasoning":"r","is_real":true,"confidence":0.9}`)} + f := &fakeClaude{stdout: p.stdout} + j := &claudeJudge{exec: func(ctx context.Context, dir string, args []string, stdin string) ([]byte, int, error) { + f.args = args + return p.exec(ctx, dir, args, stdin) + }, nonce: func() string { return "n0" }, clock: codexClock()} + if _, err := j.Call(context.Background(), claudeRequest()); err != nil { + t.Fatal(err) + } + assertIsolated(t, p, 1) + joined := strings.Join(f.args, " ") + // Defence in depth behind the empty directory: user settings only (never a project's or a + // local override), and no MCP server that is not passed explicitly (none is). + for _, want := range []string{"--setting-sources user", "--strict-mcp-config"} { + if !strings.Contains(joined, want) { + t.Fatalf("args %q missing %q", joined, want) + } + } +} + +// Every retry gets its own fresh directory, and none is left behind. +func TestClaudeJudgeIsolatesEveryAttempt(t *testing.T) { + p := &dirProbe{code: 1} + j := &claudeJudge{exec: p.exec, nonce: func() string { return "n0" }, clock: codexClock()} + if _, err := j.Call(context.Background(), claudeRequest()); err == nil { + t.Fatal("a CLI that always exits 1 must fail the call") + } + assertIsolated(t, p, MaxAttempts) + seen := map[string]bool{} + for _, d := range p.dirs { + if seen[d] { + t.Fatalf("attempts shared a directory: %s", d) + } + seen[d] = true + } +} + +func TestCodexJudgeRunsIsolatedByDefault(t *testing.T) { + var args []string + p := &dirProbe{stdout: codexStream(`{"reasoning":"r","is_real":true,"confidence":0.9}`)} + j := &codexJudge{exec: func(ctx context.Context, dir string, a []string, stdin string) ([]byte, int, error) { + args = a + return p.exec(ctx, dir, a, stdin) + }, nonce: func() string { return "n0" }, clock: codexClock()} + if _, err := j.Call(context.Background(), codexRequest()); err != nil { + t.Fatal(err) + } + assertIsolated(t, p, 1) + // AGENTS.md is read from the working directory up, including from a materialized evidence tree, + // which can carry one from the change under review: project docs are off entirely. + if !strings.Contains(strings.Join(args, " "), "-c project_doc_max_bytes=0") { + t.Fatalf("args %q must disable project docs", args) + } +} + +// An explicit work dir (the escalation path's materialized evidence tree) is honoured as-is and +// never deleted by the judge. +func TestCodexJudgeHonoursAnExplicitWorkDir(t *testing.T) { + tree := t.TempDir() + if err := os.WriteFile(filepath.Join(tree, "a.go"), []byte("package a\n"), 0o644); err != nil { + t.Fatal(err) + } + p := &dirProbe{stdout: codexStream(`{"reasoning":"r","is_real":true,"confidence":0.9}`)} + j := &codexJudge{exec: p.exec, nonce: func() string { return "n0" }, clock: codexClock(), workDir: tree} + if _, err := j.Call(context.Background(), codexRequest()); err != nil { + t.Fatal(err) + } + if len(p.dirs) != 1 || p.dirs[0] != tree { + t.Fatalf("explicit work dir not used: %v", p.dirs) + } + if _, err := os.Stat(filepath.Join(tree, "a.go")); err != nil { + t.Fatalf("the judge must not remove a caller's tree: %v", err) + } +} + +// If no private directory can be made, nothing is spawned in the caller's directory as a fallback: +// the call fails as a transport error, which the ladder retries and the run records. +func TestJudgesRefuseToRunWithoutAnIsolatedDir(t *testing.T) { + saved := isolatedDir + t.Cleanup(func() { isolatedDir = saved }) + isolatedDir = func() (string, func(), error) { return "", func() {}, errors.New("disk full") } + + calls := 0 + spy := func(context.Context, string, []string, string) ([]byte, int, error) { + calls++ + return nil, 0, nil + } + cj := &claudeJudge{exec: spy, nonce: func() string { return "n0" }, clock: codexClock()} + _, cerr := cj.Call(context.Background(), claudeRequest()) + xj := &codexJudge{exec: spy, nonce: func() string { return "n0" }, clock: codexClock()} + _, xerr := xj.Call(context.Background(), codexRequest()) + for name, err := range map[string]error{"claude": cerr, "codex": xerr} { + var e *errs.Error + if !errors.As(err, &e) || e.Code != CodeJudgeTransport || !strings.Contains(e.Detail, "isolated") { + t.Fatalf("%s: want an isolated-dir transport error, got %v", name, err) + } + } + if calls != 0 { + t.Fatalf("a CLI was spawned without an isolated directory (%d calls)", calls) + } +} + +// The real directory maker: private (0700), empty, outside the caller's tree, and removed by its +// cleanup. +func TestIsolatedDir(t *testing.T) { + dir, cleanup, err := isolatedDir() + if err != nil { + t.Fatal(err) + } + info, err := os.Stat(dir) + if err != nil || !info.IsDir() || info.Mode().Perm() != 0o700 { + t.Fatalf("stat %s: %v %v", dir, info, err) + } + cleanup() + if _, err := os.Stat(dir); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("cleanup left %s", dir) + } +} + +// With no usable temp directory the real maker reports the error rather than returning a +// directory the CLI would then be run in. +func TestIsolatedDirReportsAnUnusableTempDir(t *testing.T) { + t.Setenv("TMPDIR", filepath.Join(t.TempDir(), "does-not-exist")) + dir, cleanup, err := isolatedDir() + if err == nil || dir != "" { + t.Fatalf("want an error and no dir, got %q %v", dir, err) + } + cleanup() // a no-op, but always safe to call +} From 1fbc77ca97b93bd71dffa53bd8962b5b1c4cbd6a Mon Sep 17 00:00:00 2001 From: David Sifry Date: Sat, 26 Sep 2026 19:15:42 -0700 Subject: [PATCH 2/7] fix(judge): user-private judge dirs, deferred cleanup, no session persistence From the self-review of e30a904: - Create judge dirs under os.UserCacheDir (0700 base) instead of a shared temp dir: codex walks up to a .git root and loads repo-scoped skills, which on a multi-user host with TMPDIR unset another user could plant in /tmp. The temp dir remains the fallback when there is no cache dir. - Defer the directory cleanup so a panic in the exec seam cannot leave it. - claude --no-session-persistence and codex --ephemeral: a judge call is not a session, and a fresh dir per attempt would otherwise leave one transcript (carrying the full prompt and diff) per call. Verified offline: no .jsonl transcript is written. Refs: mr-84y. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 8 ++- internal/fsm/judge/claude.go | 13 ++-- internal/fsm/judge/codex.go | 11 ++-- internal/fsm/judge/isolate.go | 21 +++++- internal/fsm/judge/isolation_test.go | 98 ++++++++++++++++++++++++++-- 5 files changed, 136 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63104d49..5881e878 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,13 @@ --strict-mcp-config`. The Codex judge passes `-c project_doc_max_bytes=0`, which also covers the escalation path's materialized evidence tree (still honoured as an explicit work dir). If no private directory can be made, the attempt fails as a transport error and nothing is spawned in - the caller's directory. Reported while reviewing #165; plan in #167 (W0). + the caller's directory. The directory is created under the user's own cache directory + (`os.UserCacheDir`), not a shared temp dir, because Codex walks up from its working directory to + a `.git` root and loads repo-scoped skills it finds there; the temp dir is only a fallback when + there is no cache directory. Judge calls also no longer persist sessions: Claude gets + `--no-session-persistence` and Codex `--ephemeral`, so a judge's full prompt and diff are no + longer written into the user's session transcripts. Reported while reviewing #165; plan in #167 + (W0). ## 0.13.2 - 2026-09-26 diff --git a/internal/fsm/judge/claude.go b/internal/fsm/judge/claude.go index b2509488..5627542a 100644 --- a/internal/fsm/judge/claude.go +++ b/internal/fsm/judge/claude.go @@ -85,6 +85,9 @@ func (j *claudeJudge) Call(ctx context.Context, r Request) (v Verdict, err error // session this transport exists to use. "--setting-sources", "user", "--strict-mcp-config", + // A judge call is not a session: nothing to resume, and with a fresh directory per + // attempt a saved transcript would leave one project directory behind per call. + "--no-session-persistence", // The system prompt must always be passed. Without it `claude -p` can // silently fall back to Haiku for the work turn even with --model set — // the judge would then be a different model than the one recorded in the @@ -116,10 +119,12 @@ func (j *claudeJudge) Call(ctx context.Context, r Request) (v Verdict, err error lastErr = errs.E(CodeJudgeTransport, "claude could not be given an isolated working directory: "+dirErr.Error(), "provider", "claude-cli") continue } - actx, cancel := context.WithTimeout(ctx, j.timeout()) - stdout, code, execErr := j.exec(actx, dir, args, user) - cancel() - cleanup() + stdout, code, execErr := func() ([]byte, int, error) { + defer cleanup() // deferred: a panic in the seam must not leave the directory behind + actx, cancel := context.WithTimeout(ctx, j.timeout()) + defer cancel() + return j.exec(actx, dir, args, user) + }() text, tokens, found, transient := parseClaudeResult(stdout) v.Tokens = v.Tokens.Add(tokens) diff --git a/internal/fsm/judge/codex.go b/internal/fsm/judge/codex.go index 9a9ecc71..dd88fdab 100644 --- a/internal/fsm/judge/codex.go +++ b/internal/fsm/judge/codex.go @@ -66,6 +66,7 @@ func (j *codexJudge) Call(ctx context.Context, r Request) (v Verdict, err error) args := []string{ "exec", "--json", + "--ephemeral", // a judge call is not a session: persist nothing (one fresh dir per attempt) "--sandbox", "read-only", // a judge reads; it must never edit the tree it is judging "--skip-git-repo-check", // judging is not tied to a repository "--color", "never", @@ -110,10 +111,12 @@ func (j *codexJudge) Call(ctx context.Context, r Request) (v Verdict, err error) continue } } - actx, cancel := context.WithTimeout(ctx, j.timeout()) - stdout, code, execErr := j.exec(actx, dir, args, prompt) - cancel() - cleanup() + stdout, code, execErr := func() ([]byte, int, error) { + defer cleanup() // deferred: a panic in the seam must not leave the directory behind + actx, cancel := context.WithTimeout(ctx, j.timeout()) + defer cancel() + return j.exec(actx, dir, args, prompt) + }() text, tokens, found := parseCodexEvents(stdout) v.Tokens = v.Tokens.Add(tokens) diff --git a/internal/fsm/judge/isolate.go b/internal/fsm/judge/isolate.go index f46fd4f5..1d39ebfd 100644 --- a/internal/fsm/judge/isolate.go +++ b/internal/fsm/judge/isolate.go @@ -1,6 +1,12 @@ package judge -import "os" +import ( + "os" + "path/filepath" +) + +// userCacheDir is os.UserCacheDir, a seam so the fallback paths are testable. +var userCacheDir = os.UserCacheDir // isolatedDir makes the private, empty working directory a judge CLI runs in, and returns its // cleanup. A headless CLI still loads the project configuration of the directory it starts in @@ -9,8 +15,19 @@ import "os" // review would let that repository run code, or rewrite the judge's instructions, before the // judge reads a byte of the prompt. The prompt arrives on stdin, so the CLI needs no files of the // repository at all. A var so the failure path is testable. +// +// The directory lives under the user's own cache directory, not a shared temp dir: Codex walks up +// from its working directory to a .git root and loads the repo-scoped skills it finds there, so +// under a world-writable /tmp another local user could plant both. The temp dir is only the +// fallback when there is no usable cache directory (no $HOME). var isolatedDir = func() (string, func(), error) { - dir, err := os.MkdirTemp("", "metareview-judge-") + base := os.TempDir() + if cache, err := userCacheDir(); err == nil { + if b := filepath.Join(cache, "metareview", "judge"); os.MkdirAll(b, 0o700) == nil { + base = b + } + } + dir, err := os.MkdirTemp(base, "metareview-judge-") if err != nil { return "", func() {}, err } diff --git a/internal/fsm/judge/isolation_test.go b/internal/fsm/judge/isolation_test.go index e7a52e14..59fa04a6 100644 --- a/internal/fsm/judge/isolation_test.go +++ b/internal/fsm/judge/isolation_test.go @@ -166,9 +166,15 @@ func TestJudgesRefuseToRunWithoutAnIsolatedDir(t *testing.T) { } } -// The real directory maker: private (0700), empty, outside the caller's tree, and removed by its -// cleanup. +// The real directory maker: private (0700), empty, under the user's own cache directory (not a +// shared temp dir, where another local user could plant a .git root and repo-scoped skills a CLI +// walks up to), and removed by its cleanup. func TestIsolatedDir(t *testing.T) { + cache := t.TempDir() + saved := userCacheDir + t.Cleanup(func() { userCacheDir = saved }) + userCacheDir = func() (string, error) { return cache, nil } + dir, cleanup, err := isolatedDir() if err != nil { t.Fatal(err) @@ -177,15 +183,49 @@ func TestIsolatedDir(t *testing.T) { if err != nil || !info.IsDir() || info.Mode().Perm() != 0o700 { t.Fatalf("stat %s: %v %v", dir, info, err) } + if want := filepath.Join(cache, "metareview", "judge") + string(filepath.Separator); !strings.HasPrefix(dir, want) { + t.Fatalf("dir %s is not under the user cache base %s", dir, want) + } + base, err := os.Stat(filepath.Join(cache, "metareview", "judge")) + if err != nil || base.Mode().Perm() != 0o700 { + t.Fatalf("the base must be private: %v %v", base, err) + } cleanup() if _, err := os.Stat(dir); !errors.Is(err, os.ErrNotExist) { t.Fatalf("cleanup left %s", dir) } } -// With no usable temp directory the real maker reports the error rather than returning a -// directory the CLI would then be run in. +// No user cache directory (HOME unset), or one that cannot hold the base: fall back to the temp +// directory rather than refuse to judge. +func TestIsolatedDirFallsBackToTheTempDir(t *testing.T) { + saved := userCacheDir + t.Cleanup(func() { userCacheDir = saved }) + tmp := t.TempDir() + t.Setenv("TMPDIR", tmp) + blocked := filepath.Join(t.TempDir(), "file") + if err := os.WriteFile(blocked, nil, 0o600); err != nil { + t.Fatal(err) + } + for name, cacheDir := range map[string]func() (string, error){ + "no cache dir": func() (string, error) { return "", errors.New("$HOME is not defined") }, + "unusable cache": func() (string, error) { return blocked, nil }, // a file: MkdirAll fails + } { + userCacheDir = cacheDir + dir, cleanup, err := isolatedDir() + if err != nil || !strings.HasPrefix(dir, tmp) { + t.Fatalf("%s: want a dir under %s, got %q %v", name, tmp, dir, err) + } + cleanup() + } +} + +// With no usable directory anywhere the maker reports the error rather than returning a directory +// the CLI would then be run in. func TestIsolatedDirReportsAnUnusableTempDir(t *testing.T) { + saved := userCacheDir + t.Cleanup(func() { userCacheDir = saved }) + userCacheDir = func() (string, error) { return "", errors.New("no home") } t.Setenv("TMPDIR", filepath.Join(t.TempDir(), "does-not-exist")) dir, cleanup, err := isolatedDir() if err == nil || dir != "" { @@ -193,3 +233,53 @@ func TestIsolatedDirReportsAnUnusableTempDir(t *testing.T) { } cleanup() // a no-op, but always safe to call } + +// A panic inside the CLI seam still removes the attempt's directory. +func TestJudgesRemoveTheDirEvenOnPanic(t *testing.T) { + for name, call := range map[string]func(ClaudeExec){ + "claude": func(e ClaudeExec) { + _, _ = (&claudeJudge{exec: e, nonce: func() string { return "n0" }, clock: codexClock()}).Call(context.Background(), claudeRequest()) + }, + "codex": func(e ClaudeExec) { + _, _ = (&codexJudge{exec: CodexExec(e), nonce: func() string { return "n0" }, clock: codexClock()}).Call(context.Background(), codexRequest()) + }, + } { + var dir string + func() { + defer func() { _ = recover() }() + call(func(_ context.Context, d string, _ []string, _ string) ([]byte, int, error) { + dir = d + panic("boom") + }) + }() + if dir == "" { + t.Fatalf("%s: the seam was never called", name) + } + if _, err := os.Stat(dir); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("%s: a panic left %s behind", name, dir) + } + } +} + +// Judge calls are not sessions: nothing is persisted (and with a fresh directory per attempt, +// persisting would leave one transcript directory behind per call). +func TestJudgesPersistNoSession(t *testing.T) { + f := &fakeClaude{stdout: claudeJSON(`{"reasoning":"r","is_real":true,"confidence":0.9}`)} + if _, err := (&claudeJudge{exec: f.exec, nonce: func() string { return "n0" }, clock: codexClock()}).Call(context.Background(), claudeRequest()); err != nil { + t.Fatal(err) + } + if !strings.Contains(strings.Join(f.args, " "), "--no-session-persistence") { + t.Fatalf("claude args %q", f.args) + } + var args []string + x := &codexJudge{exec: func(_ context.Context, _ string, a []string, _ string) ([]byte, int, error) { + args = a + return []byte(codexStream(`{"reasoning":"r","is_real":true,"confidence":0.9}`)), 0, nil + }, nonce: func() string { return "n0" }, clock: codexClock()} + if _, err := x.Call(context.Background(), codexRequest()); err != nil { + t.Fatal(err) + } + if !strings.Contains(strings.Join(args, " "), "--ephemeral") { + t.Fatalf("codex args %q", args) + } +} From ecb254cb6be02c35d923b5b881ef3c2c33ed387e Mon Sep 17 00:00:00 2001 From: David Sifry Date: Sat, 26 Sep 2026 19:16:26 -0700 Subject: [PATCH 3/7] docs(metareview): pr-ready gate for judge CLI isolation Co-Authored-By: Claude Opus 5.5 --- ...506000-pr-ready-branch-10d735e5-context.md | 739 ++++++++++++++++++ ...21621749506000-pr-ready-branch-10d735e5.md | 149 ++++ 2 files changed, 888 insertions(+) create mode 100644 docs/metareview/context/mrv-20260927-021621749506000-pr-ready-branch-10d735e5-context.md create mode 100644 docs/metareview/reviews/mrv-20260927-021621749506000-pr-ready-branch-10d735e5.md diff --git a/docs/metareview/context/mrv-20260927-021621749506000-pr-ready-branch-10d735e5-context.md b/docs/metareview/context/mrv-20260927-021621749506000-pr-ready-branch-10d735e5-context.md new file mode 100644 index 00000000..89b00d8d --- /dev/null +++ b/docs/metareview/context/mrv-20260927-021621749506000-pr-ready-branch-10d735e5-context.md @@ -0,0 +1,739 @@ +# metareview pr-ready context + +Run ID: `mrv-20260927-021621749506000-pr-ready-branch-10d735e5` + +## Git + +- Base: `53ee3962f57c1f972aa532252b0a24cf808f4ffa` +- Head: `1fbc77ca97b93bd71dffa53bd8962b5b1c4cbd6a` +- Branch: `fix/judge-cli-isolation` +- Reviewer input digest: `sha256:f354738402a1c7d90e5a95fa14fcf41da86fa435ff7b08d39bac3fcff248dc45` +- Gate effect: `gate` + +## Context Profile + +- Raw diff bytes: `22585` +- Filtered diff bytes: `22585` +- Risk level: `none` + +## Context Shard Plan + +Not sharded. + +## Review Manifest + +- Manifest verdict: `PASS` +- Source manifest hash: not sharded +- Runtime assessment: static-only; runtime not assessed + +### Source Paths +- CHANGELOG.md +- internal/fsm/cli/deps.go +- internal/fsm/judge/claude.go +- internal/fsm/judge/codex.go +- internal/fsm/judge/codex_test.go +- internal/fsm/judge/isolate.go +- internal/fsm/judge/isolation_test.go + +### Manifest Blockers +No manifest blockers. + +## Changed Files + +- CHANGELOG.md +- internal/fsm/cli/deps.go +- internal/fsm/judge/claude.go +- internal/fsm/judge/codex.go +- internal/fsm/judge/codex_test.go +- internal/fsm/judge/isolate.go +- internal/fsm/judge/isolation_test.go + +## Diff + +```diff +diff --git a/CHANGELOG.md b/CHANGELOG.md +index 004e876..5881e87 100644 +--- a/CHANGELOG.md ++++ b/CHANGELOG.md +@@ -1,5 +1,27 @@ + # Changelog + ++## Unreleased ++ ++### Security ++ ++- **The `claude-cli/` and `codex/` judges no longer run inside the repository under review.** Each ++ judge attempt now starts the model CLI in a fresh, empty, private temp directory, removed after ++ the call. Before, the CLI inherited metareview's working directory, which is normally the ++ repository being reviewed, and in headless mode a CLI can still load that repository's ++ project-level configuration without the interactive trust prompt. Disabling tools does not ++ switch that configuration off. So reviewing an untrusted checkout, such as a fork's PR branch, ++ was not isolated from it. The Claude judge also passes `--setting-sources user ++ --strict-mcp-config`. The Codex judge passes `-c project_doc_max_bytes=0`, which also covers the ++ escalation path's materialized evidence tree (still honoured as an explicit work dir). If no ++ private directory can be made, the attempt fails as a transport error and nothing is spawned in ++ the caller's directory. The directory is created under the user's own cache directory ++ (`os.UserCacheDir`), not a shared temp dir, because Codex walks up from its working directory to ++ a `.git` root and loads repo-scoped skills it finds there; the temp dir is only a fallback when ++ there is no cache directory. Judge calls also no longer persist sessions: Claude gets ++ `--no-session-persistence` and Codex `--ephemeral`, so a judge's full prompt and diff are no ++ longer written into the user's session transcripts. Reported while reviewing #165; plan in #167 ++ (W0). ++ + ## 0.13.2 - 2026-09-26 + + ### Fixed +diff --git a/internal/fsm/cli/deps.go b/internal/fsm/cli/deps.go +index 5bdc1ec..41ad14b 100644 +--- a/internal/fsm/cli/deps.go ++++ b/internal/fsm/cli/deps.go +@@ -134,9 +134,9 @@ func realClaudeExec(ctx context.Context, dir string, args []string, stdin string + // metareview never handles the token itself. + func realCodexExec(ctx context.Context, dir string, args []string, stdin string) ([]byte, int, error) { + cmd := exec.CommandContext(ctx, codexBin, args...) +- // Empty means inherit, which is metareview's own repository. A caller that has materialized +- // an evidence tree passes it here so that tree is what relative paths resolve against. This +- // does not stop the CLI reading elsewhere - cmd.Dir is a starting point, not a jail. ++ // The judge always passes a directory: a fresh isolated one (judge.isolatedDir), or a ++ // materialized evidence tree a caller confined it to. This does not stop the CLI reading ++ // elsewhere - cmd.Dir is a starting point, not a jail. + cmd.Dir = dir + cmd.Stdin = strings.NewReader(stdin) + var out bytes.Buffer +diff --git a/internal/fsm/judge/claude.go b/internal/fsm/judge/claude.go +index e20beaf..5627542 100644 +--- a/internal/fsm/judge/claude.go ++++ b/internal/fsm/judge/claude.go +@@ -79,6 +79,15 @@ func (j *claudeJudge) Call(ctx context.Context, r Request) (v Verdict, err error + // remains a denial rather than a hang in a headless run. + "--disallowed-tools", "*", + "--permission-mode", "dontAsk", ++ // Behind the isolated working directory (isolatedDir): load the user's settings only, ++ // never a project's or a local override, and no MCP server that is not passed explicitly ++ // (none is). --bare would also skip hooks but skips the keychain, and with it the OAuth ++ // session this transport exists to use. ++ "--setting-sources", "user", ++ "--strict-mcp-config", ++ // A judge call is not a session: nothing to resume, and with a fresh directory per ++ // attempt a saved transcript would leave one project directory behind per call. ++ "--no-session-persistence", + // The system prompt must always be passed. Without it `claude -p` can + // silently fall back to Haiku for the work turn even with --model set — + // the judge would then be a different model than the one recorded in the +@@ -105,9 +114,17 @@ func (j *claudeJudge) Call(ctx context.Context, r Request) (v Verdict, err error + case <-j.clock.After(backoff(classBackoff, attempt-1)): + } + } +- actx, cancel := context.WithTimeout(ctx, j.timeout()) +- stdout, code, execErr := j.exec(actx, "", args, user) +- cancel() ++ dir, cleanup, dirErr := isolatedDir() ++ if dirErr != nil { ++ lastErr = errs.E(CodeJudgeTransport, "claude could not be given an isolated working directory: "+dirErr.Error(), "provider", "claude-cli") ++ continue ++ } ++ stdout, code, execErr := func() ([]byte, int, error) { ++ defer cleanup() // deferred: a panic in the seam must not leave the directory behind ++ actx, cancel := context.WithTimeout(ctx, j.timeout()) ++ defer cancel() ++ return j.exec(actx, dir, args, user) ++ }() + + text, tokens, found, transient := parseClaudeResult(stdout) + v.Tokens = v.Tokens.Add(tokens) +diff --git a/internal/fsm/judge/codex.go b/internal/fsm/judge/codex.go +index a398d92..dd88fda 100644 +--- a/internal/fsm/judge/codex.go ++++ b/internal/fsm/judge/codex.go +@@ -38,7 +38,7 @@ type codexJudge struct { + exec CodexExec + nonce func() string + clock Clock +- workDir string // empty: inherit the caller's directory ++ workDir string // empty: a fresh isolated directory per attempt (isolatedDir) + attemptTimeout time.Duration // zero: the AttemptTimeout default + } + +@@ -66,11 +66,16 @@ func (j *codexJudge) Call(ctx context.Context, r Request) (v Verdict, err error) + + args := []string{ + "exec", "--json", ++ "--ephemeral", // a judge call is not a session: persist nothing (one fresh dir per attempt) + "--sandbox", "read-only", // a judge reads; it must never edit the tree it is judging + "--skip-git-repo-check", // judging is not tied to a repository + "--color", "never", + "-m", wireModel(r.Model), + "-c", "model_reasoning_effort=" + r.Effort, ++ // AGENTS.md is read from the working directory up and would be spliced into the judge's ++ // instructions. The default directory is isolated and empty, but an explicit work dir is a ++ // materialized copy of the change under review, which can carry one: project docs off. ++ "-c", "project_doc_max_bytes=0", + "-", // the prompt arrives on stdin, never as an argv the process table would show + } + // The same attempt ceiling, per-attempt deadline and backoff as the HTTP arm. +@@ -98,9 +103,20 @@ func (j *codexJudge) Call(ctx context.Context, r Request) (v Verdict, err error) + case <-j.clock.After(backoff(classBackoff, attempt-1)): + } + } +- actx, cancel := context.WithTimeout(ctx, j.timeout()) +- stdout, code, execErr := j.exec(actx, j.workDir, args, prompt) +- cancel() ++ dir, cleanup := j.workDir, func() {} ++ if dir == "" { ++ var dirErr error ++ if dir, cleanup, dirErr = isolatedDir(); dirErr != nil { ++ lastErr = errs.E(CodeJudgeTransport, "codex could not be given an isolated working directory: "+dirErr.Error(), "provider", "codex") ++ continue ++ } ++ } ++ stdout, code, execErr := func() ([]byte, int, error) { ++ defer cleanup() // deferred: a panic in the seam must not leave the directory behind ++ actx, cancel := context.WithTimeout(ctx, j.timeout()) ++ defer cancel() ++ return j.exec(actx, dir, args, prompt) ++ }() + + text, tokens, found := parseCodexEvents(stdout) + v.Tokens = v.Tokens.Add(tokens) +diff --git a/internal/fsm/judge/codex_test.go b/internal/fsm/judge/codex_test.go +index c99dd41..1396601 100644 +--- a/internal/fsm/judge/codex_test.go ++++ b/internal/fsm/judge/codex_test.go +@@ -440,8 +440,10 @@ func TestCodexJudgeRunsInItsConfiguredWorkDir(t *testing.T) { + } + } + +-// Unset means "inherit", which is today's behaviour: the seam must not invent a directory. +-func TestCodexJudgeWithNoWorkDirPassesEmpty(t *testing.T) { ++// Unset no longer means "inherit": the caller's directory is the repository under review, whose ++// AGENTS.md a headless codex would splice into the judge's instructions. Unset means a fresh, ++// empty, isolated directory (see isolation_test.go for the full contract). ++func TestCodexJudgeWithNoWorkDirIsIsolated(t *testing.T) { + var gotDir = "unset" + f := func(_ context.Context, dir string, _ []string, _ string) ([]byte, int, error) { + gotDir = dir +@@ -451,8 +453,8 @@ func TestCodexJudgeWithNoWorkDirPassesEmpty(t *testing.T) { + if _, err := j.Call(context.Background(), codexRequest()); err != nil { + t.Fatalf("call: %v", err) + } +- if gotDir != "" { +- t.Errorf("exec dir = %q, want empty when unconfigured", gotDir) ++ if gotDir == "" || gotDir == "unset" || !strings.Contains(gotDir, "metareview-judge-") { ++ t.Errorf("exec dir = %q, want a fresh isolated directory when unconfigured", gotDir) + } + } + +@@ -481,8 +483,8 @@ func TestWithCodexWorkDirConfinesOnlyCodexJudges(t *testing.T) { + if _, err := base.Call(context.Background(), codexRequest()); err != nil { + t.Fatalf("base call: %v", err) + } +- if gotDir != "" { +- t.Errorf("confining one judge changed the original, which ran in %q", gotDir) ++ if gotDir == "/evidence" || !strings.Contains(gotDir, "metareview-judge-") { ++ t.Errorf("confining one judge changed the original, which ran in %q (want its own isolated dir)", gotDir) + } + + // a judge that is not the HTTP/codex router is returned as-is +diff --git a/internal/fsm/judge/isolate.go b/internal/fsm/judge/isolate.go +new file mode 100644 +index 0000000..1d39ebf +--- /dev/null ++++ b/internal/fsm/judge/isolate.go +@@ -0,0 +1,35 @@ ++package judge ++ ++import ( ++ "os" ++ "path/filepath" ++) ++ ++// userCacheDir is os.UserCacheDir, a seam so the fallback paths are testable. ++var userCacheDir = os.UserCacheDir ++ ++// isolatedDir makes the private, empty working directory a judge CLI runs in, and returns its ++// cleanup. A headless CLI still loads the project configuration of the directory it starts in ++// (Claude Code's .claude/settings.json hooks and CLAUDE.md; Codex's AGENTS.md), and neither ++// disabling tools nor a read-only sandbox switches that off. Starting inside the repository under ++// review would let that repository run code, or rewrite the judge's instructions, before the ++// judge reads a byte of the prompt. The prompt arrives on stdin, so the CLI needs no files of the ++// repository at all. A var so the failure path is testable. ++// ++// The directory lives under the user's own cache directory, not a shared temp dir: Codex walks up ++// from its working directory to a .git root and loads the repo-scoped skills it finds there, so ++// under a world-writable /tmp another local user could plant both. The temp dir is only the ++// fallback when there is no usable cache directory (no $HOME). ++var isolatedDir = func() (string, func(), error) { ++ base := os.TempDir() ++ if cache, err := userCacheDir(); err == nil { ++ if b := filepath.Join(cache, "metareview", "judge"); os.MkdirAll(b, 0o700) == nil { ++ base = b ++ } ++ } ++ dir, err := os.MkdirTemp(base, "metareview-judge-") ++ if err != nil { ++ return "", func() {}, err ++ } ++ return dir, func() { _ = os.RemoveAll(dir) }, nil ++} +diff --git a/internal/fsm/judge/isolation_test.go b/internal/fsm/judge/isolation_test.go +new file mode 100644 +index 0000000..59fa04a +--- /dev/null ++++ b/internal/fsm/judge/isolation_test.go +@@ -0,0 +1,285 @@ ++package judge ++ ++import ( ++ "context" ++ "errors" ++ "os" ++ "path/filepath" ++ "strings" ++ "testing" ++ ++ "github.com/dsifry/metareview/internal/fsm/errs" ++) ++ ++// dirProbe is a CLI exec seam that records the working directory each attempt was given and what ++// that directory held at the moment the CLI would have started. ++type dirProbe struct { ++ dirs []string ++ contents [][]string ++ stdout string ++ code int ++} ++ ++func (p *dirProbe) exec(_ context.Context, dir string, _ []string, _ string) ([]byte, int, error) { ++ p.dirs = append(p.dirs, dir) ++ entries, err := os.ReadDir(dir) ++ names := []string{} ++ if err != nil { ++ names = append(names, "ERR: "+err.Error()) ++ } ++ for _, e := range entries { ++ names = append(names, e.Name()) ++ } ++ p.contents = append(p.contents, names) ++ return []byte(p.stdout), p.code, nil ++} ++ ++// The judge CLIs must never start inside the repository under review. A headless CLI still loads ++// that directory's project configuration (Claude Code's .claude/settings.json hooks and ++// CLAUDE.md, Codex's AGENTS.md), without the interactive trust prompt, and disabling tools does not ++// switch that off. So every attempt runs in a fresh, empty, private directory that is removed after. ++func assertIsolated(t *testing.T, p *dirProbe, wantAttempts int) { ++ t.Helper() ++ if len(p.dirs) != wantAttempts { ++ t.Fatalf("attempts: got %d want %d", len(p.dirs), wantAttempts) ++ } ++ cwd, err := os.Getwd() ++ if err != nil { ++ t.Fatal(err) ++ } ++ for i, dir := range p.dirs { ++ if dir == "" || !filepath.IsAbs(dir) { ++ t.Fatalf("attempt %d ran in %q: an empty dir inherits the caller's cwd (the reviewed repo)", i+1, dir) ++ } ++ if rel, err := filepath.Rel(cwd, dir); err == nil && !strings.HasPrefix(rel, "..") { ++ t.Fatalf("attempt %d ran inside the caller's tree: %s", i+1, dir) ++ } ++ if len(p.contents[i]) != 0 { ++ t.Fatalf("attempt %d's directory was not empty: %v", i+1, p.contents[i]) ++ } ++ if _, err := os.Stat(dir); !errors.Is(err, os.ErrNotExist) { ++ t.Fatalf("attempt %d's directory %s was not removed (stat: %v)", i+1, dir, err) ++ } ++ } ++} ++ ++func TestClaudeJudgeRunsIsolatedFromTheReviewedRepo(t *testing.T) { ++ p := &dirProbe{stdout: claudeJSON(`{"reasoning":"r","is_real":true,"confidence":0.9}`)} ++ f := &fakeClaude{stdout: p.stdout} ++ j := &claudeJudge{exec: func(ctx context.Context, dir string, args []string, stdin string) ([]byte, int, error) { ++ f.args = args ++ return p.exec(ctx, dir, args, stdin) ++ }, nonce: func() string { return "n0" }, clock: codexClock()} ++ if _, err := j.Call(context.Background(), claudeRequest()); err != nil { ++ t.Fatal(err) ++ } ++ assertIsolated(t, p, 1) ++ joined := strings.Join(f.args, " ") ++ // Defence in depth behind the empty directory: user settings only (never a project's or a ++ // local override), and no MCP server that is not passed explicitly (none is). ++ for _, want := range []string{"--setting-sources user", "--strict-mcp-config"} { ++ if !strings.Contains(joined, want) { ++ t.Fatalf("args %q missing %q", joined, want) ++ } ++ } ++} ++ ++// Every retry gets its own fresh directory, and none is left behind. ++func TestClaudeJudgeIsolatesEveryAttempt(t *testing.T) { ++ p := &dirProbe{code: 1} ++ j := &claudeJudge{exec: p.exec, nonce: func() string { return "n0" }, clock: codexClock()} ++ if _, err := j.Call(context.Background(), claudeRequest()); err == nil { ++ t.Fatal("a CLI that always exits 1 must fail the call") ++ } ++ assertIsolated(t, p, MaxAttempts) ++ seen := map[string]bool{} ++ for _, d := range p.dirs { ++ if seen[d] { ++ t.Fatalf("attempts shared a directory: %s", d) ++ } ++ seen[d] = true ++ } ++} ++ ++func TestCodexJudgeRunsIsolatedByDefault(t *testing.T) { ++ var args []string ++ p := &dirProbe{stdout: codexStream(`{"reasoning":"r","is_real":true,"confidence":0.9}`)} ++ j := &codexJudge{exec: func(ctx context.Context, dir string, a []string, stdin string) ([]byte, int, error) { ++ args = a ++ return p.exec(ctx, dir, a, stdin) ++ }, nonce: func() string { return "n0" }, clock: codexClock()} ++ if _, err := j.Call(context.Background(), codexRequest()); err != nil { ++ t.Fatal(err) ++ } ++ assertIsolated(t, p, 1) ++ // AGENTS.md is read from the working directory up, including from a materialized evidence tree, ++ // which can carry one from the change under review: project docs are off entirely. ++ if !strings.Contains(strings.Join(args, " "), "-c project_doc_max_bytes=0") { ++ t.Fatalf("args %q must disable project docs", args) ++ } ++} ++ ++// An explicit work dir (the escalation path's materialized evidence tree) is honoured as-is and ++// never deleted by the judge. ++func TestCodexJudgeHonoursAnExplicitWorkDir(t *testing.T) { ++ tree := t.TempDir() ++ if err := os.WriteFile(filepath.Join(tree, "a.go"), []byte("package a\n"), 0o644); err != nil { ++ t.Fatal(err) ++ } ++ p := &dirProbe{stdout: codexStream(`{"reasoning":"r","is_real":true,"confidence":0.9}`)} ++ j := &codexJudge{exec: p.exec, nonce: func() string { return "n0" }, clock: codexClock(), workDir: tree} ++ if _, err := j.Call(context.Background(), codexRequest()); err != nil { ++ t.Fatal(err) ++ } ++ if len(p.dirs) != 1 || p.dirs[0] != tree { ++ t.Fatalf("explicit work dir not used: %v", p.dirs) ++ } ++ if _, err := os.Stat(filepath.Join(tree, "a.go")); err != nil { ++ t.Fatalf("the judge must not remove a caller's tree: %v", err) ++ } ++} ++ ++// If no private directory can be made, nothing is spawned in the caller's directory as a fallback: ++// the call fails as a transport error, which the ladder retries and the run records. ++func TestJudgesRefuseToRunWithoutAnIsolatedDir(t *testing.T) { ++ saved := isolatedDir ++ t.Cleanup(func() { isolatedDir = saved }) ++ isolatedDir = func() (string, func(), error) { return "", func() {}, errors.New("disk full") } ++ ++ calls := 0 ++ spy := func(context.Context, string, []string, string) ([]byte, int, error) { ++ calls++ ++ return nil, 0, nil ++ } ++ cj := &claudeJudge{exec: spy, nonce: func() string { return "n0" }, clock: codexClock()} ++ _, cerr := cj.Call(context.Background(), claudeRequest()) ++ xj := &codexJudge{exec: spy, nonce: func() string { return "n0" }, clock: codexClock()} ++ _, xerr := xj.Call(context.Background(), codexRequest()) ++ for name, err := range map[string]error{"claude": cerr, "codex": xerr} { ++ var e *errs.Error ++ if !errors.As(err, &e) || e.Code != CodeJudgeTransport || !strings.Contains(e.Detail, "isolated") { ++ t.Fatalf("%s: want an isolated-dir transport error, got %v", name, err) ++ } ++ } ++ if calls != 0 { ++ t.Fatalf("a CLI was spawned without an isolated directory (%d calls)", calls) ++ } ++} ++ ++// The real directory maker: private (0700), empty, under the user's own cache directory (not a ++// shared temp dir, where another local user could plant a .git root and repo-scoped skills a CLI ++// walks up to), and removed by its cleanup. ++func TestIsolatedDir(t *testing.T) { ++ cache := t.TempDir() ++ saved := userCacheDir ++ t.Cleanup(func() { userCacheDir = saved }) ++ userCacheDir = func() (string, error) { return cache, nil } ++ ++ dir, cleanup, err := isolatedDir() ++ if err != nil { ++ t.Fatal(err) ++ } ++ info, err := os.Stat(dir) ++ if err != nil || !info.IsDir() || info.Mode().Perm() != 0o700 { ++ t.Fatalf("stat %s: %v %v", dir, info, err) ++ } ++ if want := filepath.Join(cache, "metareview", "judge") + string(filepath.Separator); !strings.HasPrefix(dir, want) { ++ t.Fatalf("dir %s is not under the user cache base %s", dir, want) ++ } ++ base, err := os.Stat(filepath.Join(cache, "metareview", "judge")) ++ if err != nil || base.Mode().Perm() != 0o700 { ++ t.Fatalf("the base must be private: %v %v", base, err) ++ } ++ cleanup() ++ if _, err := os.Stat(dir); !errors.Is(err, os.ErrNotExist) { ++ t.Fatalf("cleanup left %s", dir) ++ } ++} ++ ++// No user cache directory (HOME unset), or one that cannot hold the base: fall back to the temp ++// directory rather than refuse to judge. ++func TestIsolatedDirFallsBackToTheTempDir(t *testing.T) { ++ saved := userCacheDir ++ t.Cleanup(func() { userCacheDir = saved }) ++ tmp := t.TempDir() ++ t.Setenv("TMPDIR", tmp) ++ blocked := filepath.Join(t.TempDir(), "file") ++ if err := os.WriteFile(blocked, nil, 0o600); err != nil { ++ t.Fatal(err) ++ } ++ for name, cacheDir := range map[string]func() (string, error){ ++ "no cache dir": func() (string, error) { return "", errors.New("$HOME is not defined") }, ++ "unusable cache": func() (string, error) { return blocked, nil }, // a file: MkdirAll fails ++ } { ++ userCacheDir = cacheDir ++ dir, cleanup, err := isolatedDir() ++ if err != nil || !strings.HasPrefix(dir, tmp) { ++ t.Fatalf("%s: want a dir under %s, got %q %v", name, tmp, dir, err) ++ } ++ cleanup() ++ } ++} ++ ++// With no usable directory anywhere the maker reports the error rather than returning a directory ++// the CLI would then be run in. ++func TestIsolatedDirReportsAnUnusableTempDir(t *testing.T) { ++ saved := userCacheDir ++ t.Cleanup(func() { userCacheDir = saved }) ++ userCacheDir = func() (string, error) { return "", errors.New("no home") } ++ t.Setenv("TMPDIR", filepath.Join(t.TempDir(), "does-not-exist")) ++ dir, cleanup, err := isolatedDir() ++ if err == nil || dir != "" { ++ t.Fatalf("want an error and no dir, got %q %v", dir, err) ++ } ++ cleanup() // a no-op, but always safe to call ++} ++ ++// A panic inside the CLI seam still removes the attempt's directory. ++func TestJudgesRemoveTheDirEvenOnPanic(t *testing.T) { ++ for name, call := range map[string]func(ClaudeExec){ ++ "claude": func(e ClaudeExec) { ++ _, _ = (&claudeJudge{exec: e, nonce: func() string { return "n0" }, clock: codexClock()}).Call(context.Background(), claudeRequest()) ++ }, ++ "codex": func(e ClaudeExec) { ++ _, _ = (&codexJudge{exec: CodexExec(e), nonce: func() string { return "n0" }, clock: codexClock()}).Call(context.Background(), codexRequest()) ++ }, ++ } { ++ var dir string ++ func() { ++ defer func() { _ = recover() }() ++ call(func(_ context.Context, d string, _ []string, _ string) ([]byte, int, error) { ++ dir = d ++ panic("boom") ++ }) ++ }() ++ if dir == "" { ++ t.Fatalf("%s: the seam was never called", name) ++ } ++ if _, err := os.Stat(dir); !errors.Is(err, os.ErrNotExist) { ++ t.Fatalf("%s: a panic left %s behind", name, dir) ++ } ++ } ++} ++ ++// Judge calls are not sessions: nothing is persisted (and with a fresh directory per attempt, ++// persisting would leave one transcript directory behind per call). ++func TestJudgesPersistNoSession(t *testing.T) { ++ f := &fakeClaude{stdout: claudeJSON(`{"reasoning":"r","is_real":true,"confidence":0.9}`)} ++ if _, err := (&claudeJudge{exec: f.exec, nonce: func() string { return "n0" }, clock: codexClock()}).Call(context.Background(), claudeRequest()); err != nil { ++ t.Fatal(err) ++ } ++ if !strings.Contains(strings.Join(f.args, " "), "--no-session-persistence") { ++ t.Fatalf("claude args %q", f.args) ++ } ++ var args []string ++ x := &codexJudge{exec: func(_ context.Context, _ string, a []string, _ string) ([]byte, int, error) { ++ args = a ++ return []byte(codexStream(`{"reasoning":"r","is_real":true,"confidence":0.9}`)), 0, nil ++ }, nonce: func() string { return "n0" }, clock: codexClock()} ++ if _, err := x.Call(context.Background(), codexRequest()); err != nil { ++ t.Fatal(err) ++ } ++ if !strings.Contains(strings.Join(args, " "), "--ephemeral") { ++ t.Fatalf("codex args %q", args) ++ } ++} + + + +``` + +## Review Logs + +- --help: PASS (docs/metareview/reviews/mrv-20260822-220108922308000-task-done-help-9a8265a5.md) +- CHANGELOG.md: PASS (docs/metareview/reviews/mrv-20260908-073527271951000-artifact-changelog-ab09011f.md) +- cmd/metareview/reviewhelp_test.go: PASS_ADVISORY (docs/metareview/reviews/mrv-20260924-062613603963000-task-done-reviewhelp-test-b88768c5.md) +- docs-0.6.0-documentation: PASS (docs/metareview/reviews/mrv-20260705-161047045358000-task-done-docs-0-6-a2ac175a.md) +- docs-fsm-result-contract: PASS (docs/metareview/reviews/mrv-20260705-215610270004000-task-done-docs-fsm-result-contract-cbf40d94.md) +- docs/metareview/evidence/t0.1-finding-identity-rewire.md: PASS (docs/metareview/reviews/mrv-20260901-042205337128000-task-done-t0-1-finding-identity-rewire-01ca0bf9.md) +- docs/metareview/evidence/t1.3-pins-proven-gate-pr-a.md: PASS (docs/metareview/reviews/mrv-20260901-051912009394000-task-done-t1-3-pins-proven-gate-pr-a-51440dbf.md) +- docs/quickstart.md: PASS (docs/metareview/reviews/mrv-20260528-222809220757000-task-done-quickstart-f3ea78a9.md) +- docs/specs/2026-05-27-artifact-review-parallel-subagents.md: PASS (docs/metareview/reviews/mrv-20260527-193044946912000-task-done-2026-05-27-artifact-review-parallel-subagents-cf4f2168.md) +- docs/specs/2026-05-27-fail-closed-artifact-review-gates.md: PASS (docs/metareview/reviews/mrv-20260527-182255753628000-task-done-2026-05-27-fail-closed-artifact-review-gates-5243a194.md) +- docs/superpowers/plans/2026-07-05-issue-2-wu4-review-manifest-shard-aggregation.md: PASS (docs/metareview/reviews/mrv-20260705-055835877369000-task-done-2026-07-05-issue-2-wu4-review-manifest-shard-agg-b924a5ff.md) +- docs/superpowers/plans/2026-09-23-mutation-incremental-1c-proof.md: PASS_ADVISORY (docs/metareview/reviews/mrv-20260924-034929320260000-task-done-2026-09-23-mutation-incremental-1c-proof-380f704d.md) +- docs/superpowers/plans/2026-09-23-mutation-incremental-2a-gate.md: PASS_ADVISORY (docs/metareview/reviews/mrv-20260924-045211619641000-task-done-2026-09-23-mutation-incremental-2a-gate-ed03c179.md) +- docs/superpowers/plans/2026-09-23-mutation-incremental-2b-views.md: PASS_ADVISORY (docs/metareview/reviews/mrv-20260924-051035853973000-task-done-2026-09-23-mutation-incremental-2b-views-6f8a9d6c.md) +- docs/tasks/m0-fsm-run-persistence.md: PASS (docs/metareview/reviews/mrv-20260827-130422417348000-task-done-m0-fsm-run-persistence-8a9eddc9.md) +- docs/tasks/m1-m6-fsm-packages.md: PASS (docs/metareview/reviews/mrv-20260827-090548364954000-task-done-m1-m6-fsm-packages-a99c72f1.md) +- docs/tasks/m7-fork-record-export.md: PASS (docs/metareview/reviews/mrv-20260827-104833919009000-task-done-m7-fork-record-export-9dc62119.md) +- docs/tasks/m8-cli-suite-docs.md: PASS (docs/metareview/reviews/mrv-20260827-114104823011000-task-done-m8-cli-suite-docs-46a961bc.md) +- docs/tasks/pr-a-sharded-measurement.md: PASS (docs/metareview/reviews/mrv-20260827-164005027099000-task-done-pr-a-sharded-measurement-8215c1f2.md) +- docs/tasks/pr-b-shard-ingestion.md: PASS (docs/metareview/reviews/mrv-20260827-181704826892000-task-done-pr-b-shard-ingestion-f6a50afe.md) +- docs/tasks/pr-b-slice-3.md: PASS (docs/metareview/reviews/mrv-20260827-181553019412000-task-done-pr-b-slice-3-e224e5b6.md) +- docs/tasks/pr-b-slices-1-2.md: PASS (docs/metareview/reviews/mrv-20260827-181426808610000-task-done-pr-b-slices-1-2-ce580d87.md) +- docs/tasks/process-overrides.md: PASS (docs/metareview/reviews/mrv-20260827-180059866454000-task-done-process-overrides-93d185e3.md) +- fix-node-elicits-proofs: PASS (docs/metareview/reviews/mrv-20260901-183938772835000-task-done-fix-node-elicits-proofs-e988c555.md) +- fix-pin-id-finding-consistency: PASS (docs/metareview/reviews/mrv-20260901-191438733281000-task-done-fix-pin-id-finding-consistency-b6a37c98.md) +- fix-proof-binds-fix-diff: PASS (docs/metareview/reviews/mrv-20260901-204852362514000-task-done-fix-proof-binds-fix-diff-3ad76b46.md) +- github-reviewdecision-prready-fix: PASS (docs/metareview/reviews/mrv-20260705-221742306514000-task-done-github-reviewdecision-prready-fix-3de6a2de.md) +- internal/fsm/gate/gates.go: PASS (docs/metareview/reviews/mrv-20260902-040527894922000-task-done-gates-bca330f8.md) +- internal/fsm/judge/judge.go: PASS (docs/metareview/reviews/mrv-20260902-045920838914000-task-done-judge-b7060768.md) +- internal/fsm/kind/kind.go: PASS_ADVISORY (docs/metareview/reviews/mrv-20260910-153756405005000-task-done-kind-b7bb121c.md) +- internal/gitcontext/gitcontext.go: PASS (docs/metareview/reviews/mrv-20260902-060726814084000-task-done-gitcontext-9e14bb40.md) +- internal/githubcontext/githubcontext.go: PASS (docs/metareview/reviews/mrv-20260902-034259118558000-task-done-githubcontext-37fa1398.md) +- internal/prready/review.go: PASS (docs/metareview/reviews/mrv-20260902-040416607878000-task-done-review-c809e786.md) +- internal/reviewers/epicready.go: PASS (docs/metareview/reviews/mrv-20260902-035232185408000-task-done-epicready-27fabbd2.md) +- internal/testconv/typescript.go: PASS (docs/metareview/reviews/mrv-20260902-051859934731000-task-done-typescript-4dea666f.md) +- issue-2-wu1: PASS (docs/metareview/reviews/mrv-20260705-012852131814000-task-done-issue-2-wu1-a2da3dbe.md) +- issue-2-wu1-version-0.6.0: PASS (docs/metareview/reviews/mrv-20260705-030845991374000-task-done-issue-2-wu1-version-0-6-69ccbe27.md) +- issue-2-wu2: PASS (docs/metareview/reviews/mrv-20260705-034317422615000-task-done-issue-2-wu2-eb5dad78.md) +- issue-2-wu2-cursor-followup-fix: PASS (docs/metareview/reviews/mrv-20260705-040500203996000-task-done-issue-2-wu2-cursor-followup-fix-e87ecaa9.md) +- issue-2-wu2-prready-filter-fix: PASS (docs/metareview/reviews/mrv-20260705-035954884586000-task-done-issue-2-wu2-prready-filter-fix-d4005ab8.md) +- issue-2-wu2-review-fix: PASS (docs/metareview/reviews/mrv-20260705-035540088635000-task-done-issue-2-wu2-review-fix-05b5e486.md) +- issue-2-wu3: PASS (docs/metareview/reviews/mrv-20260705-050759694908000-task-done-issue-2-wu3-2688b027.md) +- mechanical-precision-lens: PASS (docs/metareview/reviews/mrv-20260831-183207933768000-task-done-mechanical-precision-lens-c79c1389.md) +- multilang-pytest-conv: PASS (docs/metareview/reviews/mrv-20260901-190331877901000-task-done-multilang-pytest-conv-1f2195e7.md) +- multilang-testconv-seam: PASS (docs/metareview/reviews/mrv-20260901-165623149954000-task-done-multilang-testconv-seam-01cc5b3d.md) +- multilang-typescript-conv: PASS (docs/metareview/reviews/mrv-20260901-172216437441000-task-done-multilang-typescript-conv-6cdb50c2.md) +- multilang-vitest-conv: PASS (docs/metareview/reviews/mrv-20260901-174949441189000-task-done-multilang-vitest-conv-56254690.md) +- runchain-headsha-escalation-reset: PASS (docs/metareview/reviews/mrv-20260705-222055675782000-task-done-runchain-headsha-escalation-reset-37b22cb1.md) +- runchain-reset-findings-reconciliation: PASS (docs/metareview/reviews/mrv-20260705-223346154492000-task-done-runchain-reset-findings-reconciliation-6cafdbd1.md) +- t1.1-differentialproof-model: PASS (docs/metareview/reviews/mrv-20260901-001859561454000-task-done-t1-aa37529c.md) +- t1.2-agentedit-reduce-seam: PASS (docs/metareview/reviews/mrv-20260901-002517703556000-task-done-t1-b5a41665.md) +- t1.3-reproduction-engine-pr-b: PASS (docs/metareview/reviews/mrv-20260901-080559565942000-task-done-t1-8af2654f.md) +- t1.4-right-reason-reviewer: PASS (docs/metareview/reviews/mrv-20260901-100944138141000-task-done-t1-ab64ea55.md) +- t1.5-deletion-provable-fix: PASS (docs/metareview/reviews/mrv-20260901-105248210036000-task-done-t1-4ad043b2.md) +- t1.6-trivial-pin-prescreen: PASS (docs/metareview/reviews/mrv-20260901-115747009329000-task-done-t1-b353673e.md) +- t2.1-test-deletion-gate: PASS (docs/metareview/reviews/mrv-20260901-145243731066000-task-done-t2-0fe76ae4.md) +- t2.2-trajectory-monitor: PASS (docs/metareview/reviews/mrv-20260901-154004114739000-task-done-t2-d4fcf27a.md) +- workflows/sdlc-loop-clean.yaml: PASS (docs/metareview/reviews/mrv-20260902-082859947245000-task-done-sdlc-loop-clean-510ba162.md) + +## Knowledge And Registries + +Service inventory: none + +No service inventory found. + +Knowledge facts: + +No Beads knowledge facts found. + +## Validation Evidence + +{"schemaVersion":1,"kind":"validation","command":["go","test","./..."],"cwd":".","exitCode":0,"startedAt":"2026-09-27T02:15:52.722131Z","finishedAt":"2026-09-27T02:16:21.494406Z","stdoutSha256":"a7d02b15bcf32c8718d19818059ae5211cec37586e181a74379066c5115ae83b","stderrSha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","summary":"go test ./... exited 0"} + +## GitHub Context + +GitHub context unavailable: pr-number-unavailable + + +## Suggested PR Evidence + +## metareview PR Evidence + +### Summary + +fix/judge-cli-isolation changes CHANGELOG.md, internal/fsm/cli/deps.go, internal/fsm/judge/claude.go, internal/fsm/judge/codex.go, internal/fsm/judge/codex_test.go, internal/fsm/judge/isolate.go, internal/fsm/judge/isolation_test.go + +### Validation + +- structured validation: go test ./... exited 0 (exit 0) + +### Task Review Evidence + +- --help: PASS (docs/metareview/reviews/mrv-20260822-220108922308000-task-done-help-9a8265a5.md) +- CHANGELOG.md: PASS (docs/metareview/reviews/mrv-20260908-073527271951000-artifact-changelog-ab09011f.md) +- cmd/metareview/reviewhelp_test.go: PASS_ADVISORY (docs/metareview/reviews/mrv-20260924-062613603963000-ta[REDACTED].md) findings: mrvf-20260924-062613603963000-ta[REDACTED] +- docs-0.6.0-documentation: PASS (docs/metareview/reviews/mrv-20260705-161047045358000-ta[REDACTED].md) +- docs-fsm-result-contract: PASS (docs/metareview/reviews/mrv-20260705-215610270004000-ta[REDACTED].md) +- docs/metareview/evidence/t0.1-finding-identity-rewire.md: PASS (docs/metareview/reviews/mrv-20260901-042205337128000-ta[REDACTED].md) +- docs/metareview/evidence/t1.3-pins-proven-gate-pr-a.md: PASS (docs/metareview/reviews/mrv-20260901-051912009394000-ta[REDACTED].md) +- docs/quickstart.md: PASS (docs/metareview/reviews/mrv-20260528-222809220757000-ta[REDACTED].md) +- docs/specs/2026-05-27-artifact-review-parallel-subagents.md: PASS (docs/metareview/reviews/mrv-20260527-193044946912000-ta[REDACTED].md) +- docs/specs/2026-05-27-fail-closed-artifact-review-gates.md: PASS (docs/metareview/reviews/mrv-20260527-182255753628000-ta[REDACTED].md) +- docs/superpowers/plans/2026-07-05-issue-2-wu4-review-manifest-shard-aggregation.md: PASS (docs/metareview/reviews/mrv-20260705-055835877369000-ta[REDACTED].md) +- docs/superpowers/plans/2026-09-23-mutation-incremental-1c-proof.md: PASS_ADVISORY (docs/metareview/reviews/mrv-20260924-034929320260000-ta[REDACTED].md) findings: mrvf-20260924-034929320260000-ta[REDACTED], mrvf-20260924-034929320260000-ta[REDACTED], mrvf-20260924-034929320260000-ta[REDACTED] +- docs/superpowers/plans/2026-09-23-mutation-incremental-2a-gate.md: PASS_ADVISORY (docs/metareview/reviews/mrv-20260924-045211619641000-ta[REDACTED].md) findings: mrvf-20260924-045211619641000-ta[REDACTED], mrvf-20260924-045211619641000-ta[REDACTED], mrvf-20260924-045211619641000-ta[REDACTED] +- docs/superpowers/plans/2026-09-23-mutation-incremental-2b-views.md: PASS_ADVISORY (docs/metareview/reviews/mrv-20260924-051035853973000-ta[REDACTED].md) findings: mrvf-20260924-051035853973000-ta[REDACTED] +- docs/tasks/m0-fsm-run-persistence.md: PASS (docs/metareview/reviews/mrv-20260827-130422417348000-ta[REDACTED].md) +- docs/tasks/m1-m6-fsm-packages.md: PASS (docs/metareview/reviews/mrv-20260827-090548364954000-ta[REDACTED].md) +- docs/tasks/m7-fork-record-export.md: PASS (docs/metareview/reviews/mrv-20260827-104833919009000-ta[REDACTED].md) +- docs/tasks/m8-cli-suite-docs.md: PASS (docs/metareview/reviews/mrv-20260827-114104823011000-ta[REDACTED].md) +- docs/tasks/pr-a-sharded-measurement.md: PASS (docs/metareview/reviews/mrv-20260827-164005027099000-ta[REDACTED].md) +- docs/tasks/pr-b-shard-ingestion.md: PASS (docs/metareview/reviews/mrv-20260827-181704826892000-ta[REDACTED].md) +- docs/tasks/pr-b-slice-3.md: PASS (docs/metareview/reviews/mrv-20260827-181553019412000-ta[REDACTED].md) +- docs/tasks/pr-b-slices-1-2.md: PASS (docs/metareview/reviews/mrv-20260827-181426808610000-ta[REDACTED].md) +- docs/tasks/process-overrides.md: PASS (docs/metareview/reviews/mrv-20260827-180059866454000-ta[REDACTED].md) +- fix-node-elicits-proofs: PASS (docs/metareview/reviews/mrv-20260901-183938772835000-ta[REDACTED].md) +- fix-pin-id-finding-consistency: PASS (docs/metareview/reviews/mrv-20260901-191438733281000-ta[REDACTED].md) +- fix-proof-binds-fix-diff: PASS (docs/metareview/reviews/mrv-20260901-204852362514000-ta[REDACTED].md) +- github-reviewdecision-prready-fix: PASS (docs/metareview/reviews/mrv-20260705-221742306514000-ta[REDACTED].md) +- internal/fsm/gate/gates.go: PASS (docs/metareview/reviews/mrv-20260902-040527894922000-task-done-gates-bca330f8.md) +- internal/fsm/judge/judge.go: PASS (docs/metareview/reviews/mrv-20260902-045920838914000-task-done-judge-b7060768.md) +- internal/fsm/kind/kind.go: PASS_ADVISORY (docs/metareview/reviews/mrv-20260910-153756405005000-task-done-kind-b7bb121c.md) findings: mrvf-20260910-151423657039000-ta[REDACTED], mrvf-20260910-151423657039000-ta[REDACTED], mrvf-20260910-151538496642000-ta[REDACTED] +- internal/gitcontext/gitcontext.go: PASS (docs/metareview/reviews/mrv-20260902-060726814084000-ta[REDACTED].md) +- internal/githubcontext/githubcontext.go: PASS (docs/metareview/reviews/mrv-20260902-034259118558000-ta[REDACTED].md) +- internal/prready/review.go: PASS (docs/metareview/reviews/mrv-20260902-040416607878000-ta[REDACTED].md) +- internal/testconv/typescript.go: PASS (docs/metareview/reviews/mrv-20260902-051859934731000-ta[REDACTED].md) +- issue-2-wu1: PASS (docs/metareview/reviews/mrv-20260705-012852131814000-ta[REDACTED].md) +- issue-2-wu1-version-0.6.0: PASS (docs/metareview/reviews/mrv-20260705-030845991374000-ta[REDACTED].md) +- issue-2-wu2: PASS (docs/metareview/reviews/mrv-20260705-034317422615000-ta[REDACTED].md) +- issue-2-wu2-cursor-followup-fix: PASS (docs/metareview/reviews/mrv-20260705-040500203996000-ta[REDACTED].md) +- issue-2-wu2-prready-filter-fix: PASS (docs/metareview/reviews/mrv-20260705-035954884586000-ta[REDACTED].md) +- issue-2-wu2-review-fix: PASS (docs/metareview/reviews/mrv-20260705-035540088635000-ta[REDACTED].md) +- issue-2-wu3: PASS (docs/metareview/reviews/mrv-20260705-050759694908000-ta[REDACTED].md) +- mechanical-precision-lens: PASS (docs/metareview/reviews/mrv-20260831-183207933768000-ta[REDACTED].md) +- multilang-pytest-conv: PASS (docs/metareview/reviews/mrv-20260901-190331877901000-ta[REDACTED].md) +- multilang-testconv-seam: PASS (docs/metareview/reviews/mrv-20260901-165623149954000-ta[REDACTED].md) +- multilang-typescript-conv: PASS (docs/metareview/reviews/mrv-20260901-172216437441000-ta[REDACTED].md) +- multilang-vitest-conv: PASS (docs/metareview/reviews/mrv-20260901-174949441189000-ta[REDACTED].md) +- runchain-headsha-escalation-reset: PASS (docs/metareview/reviews/mrv-20260705-222055675782000-ta[REDACTED].md) +- runchain-reset-findings-reconciliation: PASS (docs/metareview/reviews/mrv-20260705-223346154492000-ta[REDACTED].md) +- t1.1-differentialproof-model: PASS (docs/metareview/reviews/mrv-20260901-001859561454000-task-done-t1-aa37529c.md) +- t1.2-agentedit-reduce-seam: PASS (docs/metareview/reviews/mrv-20260901-002517703556000-task-done-t1-b5a41665.md) +- t1.3-reproduction-engine-pr-b: PASS (docs/metareview/reviews/mrv-20260901-080559565942000-task-done-t1-8af2654f.md) +- t1.4-right-reason-reviewer: PASS (docs/metareview/reviews/mrv-20260901-100944138141000-task-done-t1-ab64ea55.md) +- t1.5-deletion-provable-fix: PASS (docs/metareview/reviews/mrv-20260901-105248210036000-task-done-t1-4ad043b2.md) +- t1.6-trivial-pin-prescreen: PASS (docs/metareview/reviews/mrv-20260901-115747009329000-task-done-t1-b353673e.md) +- t2.1-test-deletion-gate: PASS (docs/metareview/reviews/mrv-20260901-145243731066000-task-done-t2-0fe76ae4.md) +- t2.2-trajectory-monitor: PASS (docs/metareview/reviews/mrv-20260901-154004114739000-task-done-t2-d4fcf27a.md) +- workflows/sdlc-loop-clean.yaml: PASS (docs/metareview/reviews/mrv-20260902-082859947245000-ta[REDACTED].md) + +### Epic Review Evidence + +- internal/reviewers/epicready.go: PASS (docs/metareview/reviews/mrv-20260902-035232185408000-ta[REDACTED].md) + +### Recorded-Evidence Blocker Status + +_Blockers carried forward from recorded review evidence; findings this gate run raised are under `## Blocking Findings` above._ + +No unresolved blockers in the recorded review evidence. + +### External GitHub Review Context + +GitHub context unavailable: pr-number-unavailable + diff --git a/docs/metareview/reviews/mrv-20260927-021621749506000-pr-ready-branch-10d735e5.md b/docs/metareview/reviews/mrv-20260927-021621749506000-pr-ready-branch-10d735e5.md new file mode 100644 index 00000000..482c145d --- /dev/null +++ b/docs/metareview/reviews/mrv-20260927-021621749506000-pr-ready-branch-10d735e5.md @@ -0,0 +1,149 @@ +# metareview: pr-ready review + +Run ID: `mrv-20260927-021621749506000-pr-ready-branch-10d735e5` + +Target: `current branch` + +Context pack: `docs/metareview/context/mrv-20260927-021621749506000-pr-ready-branch-10d735e5-context.md` + +Execution mode: `deterministic-local` + +Gate effect: `gate` + +Previous run: `none` + +Attempt: `1/3` + +Reviewer input digest: `sha256:f354738402a1c7d90e5a95fa14fcf41da86fa435ff7b08d39bac3fcff248dc45` + +Covered paths: `["CHANGELOG.md","internal/fsm/cli/deps.go","internal/fsm/judge/claude.go","internal/fsm/judge/codex.go","internal/fsm/judge/codex_test.go","internal/fsm/judge/isolate.go","internal/fsm/judge/isolation_test.go"]` + +## Verdict + +PASS_ADVISORY + +## Reviewer Results + +| Reviewer | Verdict | Blocking | Notes | +| --- | --- | ---: | --- | +| pr-readiness-reviewer | PASS | 0 | No blocking findings. | +| validation-reviewer | PASS | 0 | No blocking findings. | +| security-reviewer | PASS | 0 | No blocking findings. | +| code-quality-reviewer | PASS | 0 | No blocking findings. | +| architecture-reviewer | PASS | 0 | No blocking findings. | +| external-reviewer | PASS | 0 | No blocking findings. | + +## Blocking Findings + +No findings in this class. + + +## Advisory Findings + +### mrvf-20260927-021621749506000-pr-ready-branch-10d735e5-001: Adversarial review was in-session-emulated + +- Reviewer: adversarial-review-reviewer +- Severity: low +- Classification: advisory +- Finding: The adjudicated review for this head was recorded as in-session-emulated (no independent subagents), which is weaker, non-independent evidence. +- Expected: An independent subagent-adjudicated review where delegation is available. +- Found: executionMode = in-session-emulated. +- Recommendation: When subagents are available, prefer `metareview fsm --workflow review-loop` for an independent review. + + +## Follow-up Findings + +No findings in this class. + + +## Warnings + +No findings in this class. + + +## Suggested PR Evidence + +## metareview PR Evidence + +### Summary + +fix/judge-cli-isolation changes CHANGELOG.md, internal/fsm/cli/deps.go, internal/fsm/judge/claude.go, internal/fsm/judge/codex.go, internal/fsm/judge/codex_test.go, internal/fsm/judge/isolate.go, internal/fsm/judge/isolation_test.go + +### Validation + +- structured validation: go test ./... exited 0 (exit 0) + +### Task Review Evidence + +- --help: PASS (docs/metareview/reviews/mrv-20260822-220108922308000-task-done-help-9a8265a5.md) +- CHANGELOG.md: PASS (docs/metareview/reviews/mrv-20260908-073527271951000-artifact-changelog-ab09011f.md) +- cmd/metareview/reviewhelp_test.go: PASS_ADVISORY (docs/metareview/reviews/mrv-20260924-062613603963000-ta[REDACTED].md) findings: mrvf-20260924-062613603963000-ta[REDACTED] +- docs-0.6.0-documentation: PASS (docs/metareview/reviews/mrv-20260705-161047045358000-ta[REDACTED].md) +- docs-fsm-result-contract: PASS (docs/metareview/reviews/mrv-20260705-215610270004000-ta[REDACTED].md) +- docs/metareview/evidence/t0.1-finding-identity-rewire.md: PASS (docs/metareview/reviews/mrv-20260901-042205337128000-ta[REDACTED].md) +- docs/metareview/evidence/t1.3-pins-proven-gate-pr-a.md: PASS (docs/metareview/reviews/mrv-20260901-051912009394000-ta[REDACTED].md) +- docs/quickstart.md: PASS (docs/metareview/reviews/mrv-20260528-222809220757000-ta[REDACTED].md) +- docs/specs/2026-05-27-artifact-review-parallel-subagents.md: PASS (docs/metareview/reviews/mrv-20260527-193044946912000-ta[REDACTED].md) +- docs/specs/2026-05-27-fail-closed-artifact-review-gates.md: PASS (docs/metareview/reviews/mrv-20260527-182255753628000-ta[REDACTED].md) +- docs/superpowers/plans/2026-07-05-issue-2-wu4-review-manifest-shard-aggregation.md: PASS (docs/metareview/reviews/mrv-20260705-055835877369000-ta[REDACTED].md) +- docs/superpowers/plans/2026-09-23-mutation-incremental-1c-proof.md: PASS_ADVISORY (docs/metareview/reviews/mrv-20260924-034929320260000-ta[REDACTED].md) findings: mrvf-20260924-034929320260000-ta[REDACTED], mrvf-20260924-034929320260000-ta[REDACTED], mrvf-20260924-034929320260000-ta[REDACTED] +- docs/superpowers/plans/2026-09-23-mutation-incremental-2a-gate.md: PASS_ADVISORY (docs/metareview/reviews/mrv-20260924-045211619641000-ta[REDACTED].md) findings: mrvf-20260924-045211619641000-ta[REDACTED], mrvf-20260924-045211619641000-ta[REDACTED], mrvf-20260924-045211619641000-ta[REDACTED] +- docs/superpowers/plans/2026-09-23-mutation-incremental-2b-views.md: PASS_ADVISORY (docs/metareview/reviews/mrv-20260924-051035853973000-ta[REDACTED].md) findings: mrvf-20260924-051035853973000-ta[REDACTED] +- docs/tasks/m0-fsm-run-persistence.md: PASS (docs/metareview/reviews/mrv-20260827-130422417348000-ta[REDACTED].md) +- docs/tasks/m1-m6-fsm-packages.md: PASS (docs/metareview/reviews/mrv-20260827-090548364954000-ta[REDACTED].md) +- docs/tasks/m7-fork-record-export.md: PASS (docs/metareview/reviews/mrv-20260827-104833919009000-ta[REDACTED].md) +- docs/tasks/m8-cli-suite-docs.md: PASS (docs/metareview/reviews/mrv-20260827-114104823011000-ta[REDACTED].md) +- docs/tasks/pr-a-sharded-measurement.md: PASS (docs/metareview/reviews/mrv-20260827-164005027099000-ta[REDACTED].md) +- docs/tasks/pr-b-shard-ingestion.md: PASS (docs/metareview/reviews/mrv-20260827-181704826892000-ta[REDACTED].md) +- docs/tasks/pr-b-slice-3.md: PASS (docs/metareview/reviews/mrv-20260827-181553019412000-ta[REDACTED].md) +- docs/tasks/pr-b-slices-1-2.md: PASS (docs/metareview/reviews/mrv-20260827-181426808610000-ta[REDACTED].md) +- docs/tasks/process-overrides.md: PASS (docs/metareview/reviews/mrv-20260827-180059866454000-ta[REDACTED].md) +- fix-node-elicits-proofs: PASS (docs/metareview/reviews/mrv-20260901-183938772835000-ta[REDACTED].md) +- fix-pin-id-finding-consistency: PASS (docs/metareview/reviews/mrv-20260901-191438733281000-ta[REDACTED].md) +- fix-proof-binds-fix-diff: PASS (docs/metareview/reviews/mrv-20260901-204852362514000-ta[REDACTED].md) +- github-reviewdecision-prready-fix: PASS (docs/metareview/reviews/mrv-20260705-221742306514000-ta[REDACTED].md) +- internal/fsm/gate/gates.go: PASS (docs/metareview/reviews/mrv-20260902-040527894922000-task-done-gates-bca330f8.md) +- internal/fsm/judge/judge.go: PASS (docs/metareview/reviews/mrv-20260902-045920838914000-task-done-judge-b7060768.md) +- internal/fsm/kind/kind.go: PASS_ADVISORY (docs/metareview/reviews/mrv-20260910-153756405005000-task-done-kind-b7bb121c.md) findings: mrvf-20260910-151423657039000-ta[REDACTED], mrvf-20260910-151423657039000-ta[REDACTED], mrvf-20260910-151538496642000-ta[REDACTED] +- internal/gitcontext/gitcontext.go: PASS (docs/metareview/reviews/mrv-20260902-060726814084000-ta[REDACTED].md) +- internal/githubcontext/githubcontext.go: PASS (docs/metareview/reviews/mrv-20260902-034259118558000-ta[REDACTED].md) +- internal/prready/review.go: PASS (docs/metareview/reviews/mrv-20260902-040416607878000-ta[REDACTED].md) +- internal/testconv/typescript.go: PASS (docs/metareview/reviews/mrv-20260902-051859934731000-ta[REDACTED].md) +- issue-2-wu1: PASS (docs/metareview/reviews/mrv-20260705-012852131814000-ta[REDACTED].md) +- issue-2-wu1-version-0.6.0: PASS (docs/metareview/reviews/mrv-20260705-030845991374000-ta[REDACTED].md) +- issue-2-wu2: PASS (docs/metareview/reviews/mrv-20260705-034317422615000-ta[REDACTED].md) +- issue-2-wu2-cursor-followup-fix: PASS (docs/metareview/reviews/mrv-20260705-040500203996000-ta[REDACTED].md) +- issue-2-wu2-prready-filter-fix: PASS (docs/metareview/reviews/mrv-20260705-035954884586000-ta[REDACTED].md) +- issue-2-wu2-review-fix: PASS (docs/metareview/reviews/mrv-20260705-035540088635000-ta[REDACTED].md) +- issue-2-wu3: PASS (docs/metareview/reviews/mrv-20260705-050759694908000-ta[REDACTED].md) +- mechanical-precision-lens: PASS (docs/metareview/reviews/mrv-20260831-183207933768000-ta[REDACTED].md) +- multilang-pytest-conv: PASS (docs/metareview/reviews/mrv-20260901-190331877901000-ta[REDACTED].md) +- multilang-testconv-seam: PASS (docs/metareview/reviews/mrv-20260901-165623149954000-ta[REDACTED].md) +- multilang-typescript-conv: PASS (docs/metareview/reviews/mrv-20260901-172216437441000-ta[REDACTED].md) +- multilang-vitest-conv: PASS (docs/metareview/reviews/mrv-20260901-174949441189000-ta[REDACTED].md) +- runchain-headsha-escalation-reset: PASS (docs/metareview/reviews/mrv-20260705-222055675782000-ta[REDACTED].md) +- runchain-reset-findings-reconciliation: PASS (docs/metareview/reviews/mrv-20260705-223346154492000-ta[REDACTED].md) +- t1.1-differentialproof-model: PASS (docs/metareview/reviews/mrv-20260901-001859561454000-task-done-t1-aa37529c.md) +- t1.2-agentedit-reduce-seam: PASS (docs/metareview/reviews/mrv-20260901-002517703556000-task-done-t1-b5a41665.md) +- t1.3-reproduction-engine-pr-b: PASS (docs/metareview/reviews/mrv-20260901-080559565942000-task-done-t1-8af2654f.md) +- t1.4-right-reason-reviewer: PASS (docs/metareview/reviews/mrv-20260901-100944138141000-task-done-t1-ab64ea55.md) +- t1.5-deletion-provable-fix: PASS (docs/metareview/reviews/mrv-20260901-105248210036000-task-done-t1-4ad043b2.md) +- t1.6-trivial-pin-prescreen: PASS (docs/metareview/reviews/mrv-20260901-115747009329000-task-done-t1-b353673e.md) +- t2.1-test-deletion-gate: PASS (docs/metareview/reviews/mrv-20260901-145243731066000-task-done-t2-0fe76ae4.md) +- t2.2-trajectory-monitor: PASS (docs/metareview/reviews/mrv-20260901-154004114739000-task-done-t2-d4fcf27a.md) +- workflows/sdlc-loop-clean.yaml: PASS (docs/metareview/reviews/mrv-20260902-082859947245000-ta[REDACTED].md) + +### Epic Review Evidence + +- internal/reviewers/epicready.go: PASS (docs/metareview/reviews/mrv-20260902-035232185408000-ta[REDACTED].md) + +### Recorded-Evidence Blocker Status + +_Blockers carried forward from recorded review evidence; findings this gate run raised are under `## Blocking Findings` above._ + +No unresolved blockers in the recorded review evidence. + +### External GitHub Review Context + +GitHub context unavailable: pr-number-unavailable + From a53c0a2297c37862520414a4227a5dd6829cddc5 Mon Sep 17 00:00:00 2001 From: David Sifry Date: Sat, 26 Sep 2026 19:38:18 -0700 Subject: [PATCH 4/7] release: 0.13.3 Co-Authored-By: Claude Opus 5.5 --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- CHANGELOG.md | 2 +- internal/version/version.go | 2 +- package.json | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 01c2af7d..a1125198 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -8,7 +8,7 @@ { "name": "metareview", "description": "Internal review harness, adversarial gates, and post-merge learning for coding agents", - "version": "0.13.2", + "version": "0.13.3", "source": "./", "author": { "name": "David Sifry" diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 413a42aa..7c5707d4 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "metareview", - "version": "0.13.2", + "version": "0.13.3", "description": "Go-based metaswarm-compatible internal review harness for plans, specs, decompositions, task-done code review, acceptance evidence, PR readiness, and post-merge learning. Packaged releases use bin/metareview; source checkout mode requires Go 1.26+, plus FSM workflow runs (metareview fsm).", "author": { "name": "David Sifry" diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index eac09f0d..369105b9 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "metareview", - "version": "0.13.2", + "version": "0.13.3", "description": "Go-based metaswarm-compatible internal review harness for plans, specs, decompositions, task-done code review, acceptance evidence, PR readiness, and post-merge learning, plus FSM workflow runs (metareview fsm).", "author": { "name": "David Sifry" diff --git a/CHANGELOG.md b/CHANGELOG.md index 5881e878..dd981d06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## 0.13.3 - 2026-09-26 ### Security diff --git a/internal/version/version.go b/internal/version/version.go index 79c28adb..e768901f 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -1,3 +1,3 @@ package version -const Version = "0.13.2" +const Version = "0.13.3" diff --git a/package.json b/package.json index 9b34346c..7ba81ecb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "metareview", - "version": "0.13.2", + "version": "0.13.3", "description": "Go-based metaswarm-compatible internal review harness for plans, specs, decompositions, code, acceptance evidence, PR readiness, and post-merge learning", "bin": { "metareview": "cli/metareview.js" From f832ee097f1c4d16f3a532cdc1e3590c025c04f8 Mon Sep 17 00:00:00 2001 From: David Sifry Date: Sat, 26 Sep 2026 19:38:57 -0700 Subject: [PATCH 5/7] docs(metareview): 0.13.3 pr-ready gate Co-Authored-By: Claude Opus 5.5 --- ...886000-pr-ready-branch-10d735e5-context.md | 812 ++++++++++++++++++ ...23857249886000-pr-ready-branch-10d735e5.md | 149 ++++ 2 files changed, 961 insertions(+) create mode 100644 docs/metareview/context/mrv-20260927-023857249886000-pr-ready-branch-10d735e5-context.md create mode 100644 docs/metareview/reviews/mrv-20260927-023857249886000-pr-ready-branch-10d735e5.md diff --git a/docs/metareview/context/mrv-20260927-023857249886000-pr-ready-branch-10d735e5-context.md b/docs/metareview/context/mrv-20260927-023857249886000-pr-ready-branch-10d735e5-context.md new file mode 100644 index 00000000..83e32dbb --- /dev/null +++ b/docs/metareview/context/mrv-20260927-023857249886000-pr-ready-branch-10d735e5-context.md @@ -0,0 +1,812 @@ +# metareview pr-ready context + +Run ID: `mrv-20260927-023857249886000-pr-ready-branch-10d735e5` + +## Git + +- Base: `53ee3962f57c1f972aa532252b0a24cf808f4ffa` +- Head: `a53c0a2297c37862520414a4227a5dd6829cddc5` +- Branch: `fix/judge-cli-isolation` +- Reviewer input digest: `sha256:22598681355a4d6c01b56981c362491a673caa4c617d51213d59f8c012d405a0` +- Gate effect: `gate` + +## Context Profile + +- Raw diff bytes: `24891` +- Filtered diff bytes: `24891` +- Risk level: `none` +- Generated files excluded: docs/metareview/context/mrv-20260927-021621749506000-pr-ready-branch-10d735e5-context.md, docs/metareview/reviews/mrv-20260927-021621749506000-pr-ready-branch-10d735e5.md + +## Context Shard Plan + +Not sharded. + +## Review Manifest + +- Manifest verdict: `PASS` +- Source manifest hash: not sharded +- Runtime assessment: static-only; runtime not assessed + +### Source Paths +- .claude-plugin/marketplace.json +- .claude-plugin/plugin.json +- .codex-plugin/plugin.json +- CHANGELOG.md +- internal/fsm/cli/deps.go +- internal/fsm/judge/claude.go +- internal/fsm/judge/codex.go +- internal/fsm/judge/codex_test.go +- internal/fsm/judge/isolate.go +- internal/fsm/judge/isolation_test.go +- internal/version/version.go +- package.json + +### Path Dispositions +- docs/metareview/context/mrv-20260927-021621749506000-pr-ready-branch-10d735e5-context.md: generated (metareview generated review artifact excluded from source manifest) +- docs/metareview/reviews/mrv-20260927-021621749506000-pr-ready-branch-10d735e5.md: generated (metareview generated review artifact excluded from source manifest) + +### Manifest Blockers +No manifest blockers. + +## Changed Files + +- .claude-plugin/marketplace.json +- .claude-plugin/plugin.json +- .codex-plugin/plugin.json +- CHANGELOG.md +- internal/fsm/cli/deps.go +- internal/fsm/judge/claude.go +- internal/fsm/judge/codex.go +- internal/fsm/judge/codex_test.go +- internal/fsm/judge/isolate.go +- internal/fsm/judge/isolation_test.go +- internal/version/version.go +- package.json + +## Diff + +```diff +diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json +index 01c2af7..a112519 100644 +--- a/.claude-plugin/marketplace.json ++++ b/.claude-plugin/marketplace.json +@@ -8,7 +8,7 @@ + { + "name": "metareview", + "description": "Internal review harness, adversarial gates, and post-merge learning for coding agents", +- "version": "0.13.2", ++ "version": "0.13.3", + "source": "./", + "author": { + "name": "David Sifry" +diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json +index 413a42a..7c5707d 100644 +--- a/.claude-plugin/plugin.json ++++ b/.claude-plugin/plugin.json +@@ -1,6 +1,6 @@ + { + "name": "metareview", +- "version": "0.13.2", ++ "version": "0.13.3", + "description": "Go-based metaswarm-compatible internal review harness for plans, specs, decompositions, task-done code review, acceptance evidence, PR readiness, and post-merge learning. Packaged releases use bin/metareview; source checkout mode requires Go 1.26+, plus FSM workflow runs (metareview fsm).", + "author": { + "name": "David Sifry" +diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json +index eac09f0..369105b 100644 +--- a/.codex-plugin/plugin.json ++++ b/.codex-plugin/plugin.json +@@ -1,6 +1,6 @@ + { + "name": "metareview", +- "version": "0.13.2", ++ "version": "0.13.3", + "description": "Go-based metaswarm-compatible internal review harness for plans, specs, decompositions, task-done code review, acceptance evidence, PR readiness, and post-merge learning, plus FSM workflow runs (metareview fsm).", + "author": { + "name": "David Sifry" +diff --git a/CHANGELOG.md b/CHANGELOG.md +index 004e876..dd981d0 100644 +--- a/CHANGELOG.md ++++ b/CHANGELOG.md +@@ -1,5 +1,27 @@ + # Changelog + ++## 0.13.3 - 2026-09-26 ++ ++### Security ++ ++- **The `claude-cli/` and `codex/` judges no longer run inside the repository under review.** Each ++ judge attempt now starts the model CLI in a fresh, empty, private temp directory, removed after ++ the call. Before, the CLI inherited metareview's working directory, which is normally the ++ repository being reviewed, and in headless mode a CLI can still load that repository's ++ project-level configuration without the interactive trust prompt. Disabling tools does not ++ switch that configuration off. So reviewing an untrusted checkout, such as a fork's PR branch, ++ was not isolated from it. The Claude judge also passes `--setting-sources user ++ --strict-mcp-config`. The Codex judge passes `-c project_doc_max_bytes=0`, which also covers the ++ escalation path's materialized evidence tree (still honoured as an explicit work dir). If no ++ private directory can be made, the attempt fails as a transport error and nothing is spawned in ++ the caller's directory. The directory is created under the user's own cache directory ++ (`os.UserCacheDir`), not a shared temp dir, because Codex walks up from its working directory to ++ a `.git` root and loads repo-scoped skills it finds there; the temp dir is only a fallback when ++ there is no cache directory. Judge calls also no longer persist sessions: Claude gets ++ `--no-session-persistence` and Codex `--ephemeral`, so a judge's full prompt and diff are no ++ longer written into the user's session transcripts. Reported while reviewing #165; plan in #167 ++ (W0). ++ + ## 0.13.2 - 2026-09-26 + + ### Fixed +diff --git a/internal/fsm/cli/deps.go b/internal/fsm/cli/deps.go +index 5bdc1ec..41ad14b 100644 +--- a/internal/fsm/cli/deps.go ++++ b/internal/fsm/cli/deps.go +@@ -134,9 +134,9 @@ func realClaudeExec(ctx context.Context, dir string, args []string, stdin string + // metareview never handles the token itself. + func realCodexExec(ctx context.Context, dir string, args []string, stdin string) ([]byte, int, error) { + cmd := exec.CommandContext(ctx, codexBin, args...) +- // Empty means inherit, which is metareview's own repository. A caller that has materialized +- // an evidence tree passes it here so that tree is what relative paths resolve against. This +- // does not stop the CLI reading elsewhere - cmd.Dir is a starting point, not a jail. ++ // The judge always passes a directory: a fresh isolated one (judge.isolatedDir), or a ++ // materialized evidence tree a caller confined it to. This does not stop the CLI reading ++ // elsewhere - cmd.Dir is a starting point, not a jail. + cmd.Dir = dir + cmd.Stdin = strings.NewReader(stdin) + var out bytes.Buffer +diff --git a/internal/fsm/judge/claude.go b/internal/fsm/judge/claude.go +index e20beaf..5627542 100644 +--- a/internal/fsm/judge/claude.go ++++ b/internal/fsm/judge/claude.go +@@ -79,6 +79,15 @@ func (j *claudeJudge) Call(ctx context.Context, r Request) (v Verdict, err error + // remains a denial rather than a hang in a headless run. + "--disallowed-tools", "*", + "--permission-mode", "dontAsk", ++ // Behind the isolated working directory (isolatedDir): load the user's settings only, ++ // never a project's or a local override, and no MCP server that is not passed explicitly ++ // (none is). --bare would also skip hooks but skips the keychain, and with it the OAuth ++ // session this transport exists to use. ++ "--setting-sources", "user", ++ "--strict-mcp-config", ++ // A judge call is not a session: nothing to resume, and with a fresh directory per ++ // attempt a saved transcript would leave one project directory behind per call. ++ "--no-session-persistence", + // The system prompt must always be passed. Without it `claude -p` can + // silently fall back to Haiku for the work turn even with --model set — + // the judge would then be a different model than the one recorded in the +@@ -105,9 +114,17 @@ func (j *claudeJudge) Call(ctx context.Context, r Request) (v Verdict, err error + case <-j.clock.After(backoff(classBackoff, attempt-1)): + } + } +- actx, cancel := context.WithTimeout(ctx, j.timeout()) +- stdout, code, execErr := j.exec(actx, "", args, user) +- cancel() ++ dir, cleanup, dirErr := isolatedDir() ++ if dirErr != nil { ++ lastErr = errs.E(CodeJudgeTransport, "claude could not be given an isolated working directory: "+dirErr.Error(), "provider", "claude-cli") ++ continue ++ } ++ stdout, code, execErr := func() ([]byte, int, error) { ++ defer cleanup() // deferred: a panic in the seam must not leave the directory behind ++ actx, cancel := context.WithTimeout(ctx, j.timeout()) ++ defer cancel() ++ return j.exec(actx, dir, args, user) ++ }() + + text, tokens, found, transient := parseClaudeResult(stdout) + v.Tokens = v.Tokens.Add(tokens) +diff --git a/internal/fsm/judge/codex.go b/internal/fsm/judge/codex.go +index a398d92..dd88fda 100644 +--- a/internal/fsm/judge/codex.go ++++ b/internal/fsm/judge/codex.go +@@ -38,7 +38,7 @@ type codexJudge struct { + exec CodexExec + nonce func() string + clock Clock +- workDir string // empty: inherit the caller's directory ++ workDir string // empty: a fresh isolated directory per attempt (isolatedDir) + attemptTimeout time.Duration // zero: the AttemptTimeout default + } + +@@ -66,11 +66,16 @@ func (j *codexJudge) Call(ctx context.Context, r Request) (v Verdict, err error) + + args := []string{ + "exec", "--json", ++ "--ephemeral", // a judge call is not a session: persist nothing (one fresh dir per attempt) + "--sandbox", "read-only", // a judge reads; it must never edit the tree it is judging + "--skip-git-repo-check", // judging is not tied to a repository + "--color", "never", + "-m", wireModel(r.Model), + "-c", "model_reasoning_effort=" + r.Effort, ++ // AGENTS.md is read from the working directory up and would be spliced into the judge's ++ // instructions. The default directory is isolated and empty, but an explicit work dir is a ++ // materialized copy of the change under review, which can carry one: project docs off. ++ "-c", "project_doc_max_bytes=0", + "-", // the prompt arrives on stdin, never as an argv the process table would show + } + // The same attempt ceiling, per-attempt deadline and backoff as the HTTP arm. +@@ -98,9 +103,20 @@ func (j *codexJudge) Call(ctx context.Context, r Request) (v Verdict, err error) + case <-j.clock.After(backoff(classBackoff, attempt-1)): + } + } +- actx, cancel := context.WithTimeout(ctx, j.timeout()) +- stdout, code, execErr := j.exec(actx, j.workDir, args, prompt) +- cancel() ++ dir, cleanup := j.workDir, func() {} ++ if dir == "" { ++ var dirErr error ++ if dir, cleanup, dirErr = isolatedDir(); dirErr != nil { ++ lastErr = errs.E(CodeJudgeTransport, "codex could not be given an isolated working directory: "+dirErr.Error(), "provider", "codex") ++ continue ++ } ++ } ++ stdout, code, execErr := func() ([]byte, int, error) { ++ defer cleanup() // deferred: a panic in the seam must not leave the directory behind ++ actx, cancel := context.WithTimeout(ctx, j.timeout()) ++ defer cancel() ++ return j.exec(actx, dir, args, prompt) ++ }() + + text, tokens, found := parseCodexEvents(stdout) + v.Tokens = v.Tokens.Add(tokens) +diff --git a/internal/fsm/judge/codex_test.go b/internal/fsm/judge/codex_test.go +index c99dd41..1396601 100644 +--- a/internal/fsm/judge/codex_test.go ++++ b/internal/fsm/judge/codex_test.go +@@ -440,8 +440,10 @@ func TestCodexJudgeRunsInItsConfiguredWorkDir(t *testing.T) { + } + } + +-// Unset means "inherit", which is today's behaviour: the seam must not invent a directory. +-func TestCodexJudgeWithNoWorkDirPassesEmpty(t *testing.T) { ++// Unset no longer means "inherit": the caller's directory is the repository under review, whose ++// AGENTS.md a headless codex would splice into the judge's instructions. Unset means a fresh, ++// empty, isolated directory (see isolation_test.go for the full contract). ++func TestCodexJudgeWithNoWorkDirIsIsolated(t *testing.T) { + var gotDir = "unset" + f := func(_ context.Context, dir string, _ []string, _ string) ([]byte, int, error) { + gotDir = dir +@@ -451,8 +453,8 @@ func TestCodexJudgeWithNoWorkDirPassesEmpty(t *testing.T) { + if _, err := j.Call(context.Background(), codexRequest()); err != nil { + t.Fatalf("call: %v", err) + } +- if gotDir != "" { +- t.Errorf("exec dir = %q, want empty when unconfigured", gotDir) ++ if gotDir == "" || gotDir == "unset" || !strings.Contains(gotDir, "metareview-judge-") { ++ t.Errorf("exec dir = %q, want a fresh isolated directory when unconfigured", gotDir) + } + } + +@@ -481,8 +483,8 @@ func TestWithCodexWorkDirConfinesOnlyCodexJudges(t *testing.T) { + if _, err := base.Call(context.Background(), codexRequest()); err != nil { + t.Fatalf("base call: %v", err) + } +- if gotDir != "" { +- t.Errorf("confining one judge changed the original, which ran in %q", gotDir) ++ if gotDir == "/evidence" || !strings.Contains(gotDir, "metareview-judge-") { ++ t.Errorf("confining one judge changed the original, which ran in %q (want its own isolated dir)", gotDir) + } + + // a judge that is not the HTTP/codex router is returned as-is +diff --git a/internal/fsm/judge/isolate.go b/internal/fsm/judge/isolate.go +new file mode 100644 +index 0000000..1d39ebf +--- /dev/null ++++ b/internal/fsm/judge/isolate.go +@@ -0,0 +1,35 @@ ++package judge ++ ++import ( ++ "os" ++ "path/filepath" ++) ++ ++// userCacheDir is os.UserCacheDir, a seam so the fallback paths are testable. ++var userCacheDir = os.UserCacheDir ++ ++// isolatedDir makes the private, empty working directory a judge CLI runs in, and returns its ++// cleanup. A headless CLI still loads the project configuration of the directory it starts in ++// (Claude Code's .claude/settings.json hooks and CLAUDE.md; Codex's AGENTS.md), and neither ++// disabling tools nor a read-only sandbox switches that off. Starting inside the repository under ++// review would let that repository run code, or rewrite the judge's instructions, before the ++// judge reads a byte of the prompt. The prompt arrives on stdin, so the CLI needs no files of the ++// repository at all. A var so the failure path is testable. ++// ++// The directory lives under the user's own cache directory, not a shared temp dir: Codex walks up ++// from its working directory to a .git root and loads the repo-scoped skills it finds there, so ++// under a world-writable /tmp another local user could plant both. The temp dir is only the ++// fallback when there is no usable cache directory (no $HOME). ++var isolatedDir = func() (string, func(), error) { ++ base := os.TempDir() ++ if cache, err := userCacheDir(); err == nil { ++ if b := filepath.Join(cache, "metareview", "judge"); os.MkdirAll(b, 0o700) == nil { ++ base = b ++ } ++ } ++ dir, err := os.MkdirTemp(base, "metareview-judge-") ++ if err != nil { ++ return "", func() {}, err ++ } ++ return dir, func() { _ = os.RemoveAll(dir) }, nil ++} +diff --git a/internal/fsm/judge/isolation_test.go b/internal/fsm/judge/isolation_test.go +new file mode 100644 +index 0000000..59fa04a +--- /dev/null ++++ b/internal/fsm/judge/isolation_test.go +@@ -0,0 +1,285 @@ ++package judge ++ ++import ( ++ "context" ++ "errors" ++ "os" ++ "path/filepath" ++ "strings" ++ "testing" ++ ++ "github.com/dsifry/metareview/internal/fsm/errs" ++) ++ ++// dirProbe is a CLI exec seam that records the working directory each attempt was given and what ++// that directory held at the moment the CLI would have started. ++type dirProbe struct { ++ dirs []string ++ contents [][]string ++ stdout string ++ code int ++} ++ ++func (p *dirProbe) exec(_ context.Context, dir string, _ []string, _ string) ([]byte, int, error) { ++ p.dirs = append(p.dirs, dir) ++ entries, err := os.ReadDir(dir) ++ names := []string{} ++ if err != nil { ++ names = append(names, "ERR: "+err.Error()) ++ } ++ for _, e := range entries { ++ names = append(names, e.Name()) ++ } ++ p.contents = append(p.contents, names) ++ return []byte(p.stdout), p.code, nil ++} ++ ++// The judge CLIs must never start inside the repository under review. A headless CLI still loads ++// that directory's project configuration (Claude Code's .claude/settings.json hooks and ++// CLAUDE.md, Codex's AGENTS.md), without the interactive trust prompt, and disabling tools does not ++// switch that off. So every attempt runs in a fresh, empty, private directory that is removed after. ++func assertIsolated(t *testing.T, p *dirProbe, wantAttempts int) { ++ t.Helper() ++ if len(p.dirs) != wantAttempts { ++ t.Fatalf("attempts: got %d want %d", len(p.dirs), wantAttempts) ++ } ++ cwd, err := os.Getwd() ++ if err != nil { ++ t.Fatal(err) ++ } ++ for i, dir := range p.dirs { ++ if dir == "" || !filepath.IsAbs(dir) { ++ t.Fatalf("attempt %d ran in %q: an empty dir inherits the caller's cwd (the reviewed repo)", i+1, dir) ++ } ++ if rel, err := filepath.Rel(cwd, dir); err == nil && !strings.HasPrefix(rel, "..") { ++ t.Fatalf("attempt %d ran inside the caller's tree: %s", i+1, dir) ++ } ++ if len(p.contents[i]) != 0 { ++ t.Fatalf("attempt %d's directory was not empty: %v", i+1, p.contents[i]) ++ } ++ if _, err := os.Stat(dir); !errors.Is(err, os.ErrNotExist) { ++ t.Fatalf("attempt %d's directory %s was not removed (stat: %v)", i+1, dir, err) ++ } ++ } ++} ++ ++func TestClaudeJudgeRunsIsolatedFromTheReviewedRepo(t *testing.T) { ++ p := &dirProbe{stdout: claudeJSON(`{"reasoning":"r","is_real":true,"confidence":0.9}`)} ++ f := &fakeClaude{stdout: p.stdout} ++ j := &claudeJudge{exec: func(ctx context.Context, dir string, args []string, stdin string) ([]byte, int, error) { ++ f.args = args ++ return p.exec(ctx, dir, args, stdin) ++ }, nonce: func() string { return "n0" }, clock: codexClock()} ++ if _, err := j.Call(context.Background(), claudeRequest()); err != nil { ++ t.Fatal(err) ++ } ++ assertIsolated(t, p, 1) ++ joined := strings.Join(f.args, " ") ++ // Defence in depth behind the empty directory: user settings only (never a project's or a ++ // local override), and no MCP server that is not passed explicitly (none is). ++ for _, want := range []string{"--setting-sources user", "--strict-mcp-config"} { ++ if !strings.Contains(joined, want) { ++ t.Fatalf("args %q missing %q", joined, want) ++ } ++ } ++} ++ ++// Every retry gets its own fresh directory, and none is left behind. ++func TestClaudeJudgeIsolatesEveryAttempt(t *testing.T) { ++ p := &dirProbe{code: 1} ++ j := &claudeJudge{exec: p.exec, nonce: func() string { return "n0" }, clock: codexClock()} ++ if _, err := j.Call(context.Background(), claudeRequest()); err == nil { ++ t.Fatal("a CLI that always exits 1 must fail the call") ++ } ++ assertIsolated(t, p, MaxAttempts) ++ seen := map[string]bool{} ++ for _, d := range p.dirs { ++ if seen[d] { ++ t.Fatalf("attempts shared a directory: %s", d) ++ } ++ seen[d] = true ++ } ++} ++ ++func TestCodexJudgeRunsIsolatedByDefault(t *testing.T) { ++ var args []string ++ p := &dirProbe{stdout: codexStream(`{"reasoning":"r","is_real":true,"confidence":0.9}`)} ++ j := &codexJudge{exec: func(ctx context.Context, dir string, a []string, stdin string) ([]byte, int, error) { ++ args = a ++ return p.exec(ctx, dir, a, stdin) ++ }, nonce: func() string { return "n0" }, clock: codexClock()} ++ if _, err := j.Call(context.Background(), codexRequest()); err != nil { ++ t.Fatal(err) ++ } ++ assertIsolated(t, p, 1) ++ // AGENTS.md is read from the working directory up, including from a materialized evidence tree, ++ // which can carry one from the change under review: project docs are off entirely. ++ if !strings.Contains(strings.Join(args, " "), "-c project_doc_max_bytes=0") { ++ t.Fatalf("args %q must disable project docs", args) ++ } ++} ++ ++// An explicit work dir (the escalation path's materialized evidence tree) is honoured as-is and ++// never deleted by the judge. ++func TestCodexJudgeHonoursAnExplicitWorkDir(t *testing.T) { ++ tree := t.TempDir() ++ if err := os.WriteFile(filepath.Join(tree, "a.go"), []byte("package a\n"), 0o644); err != nil { ++ t.Fatal(err) ++ } ++ p := &dirProbe{stdout: codexStream(`{"reasoning":"r","is_real":true,"confidence":0.9}`)} ++ j := &codexJudge{exec: p.exec, nonce: func() string { return "n0" }, clock: codexClock(), workDir: tree} ++ if _, err := j.Call(context.Background(), codexRequest()); err != nil { ++ t.Fatal(err) ++ } ++ if len(p.dirs) != 1 || p.dirs[0] != tree { ++ t.Fatalf("explicit work dir not used: %v", p.dirs) ++ } ++ if _, err := os.Stat(filepath.Join(tree, "a.go")); err != nil { ++ t.Fatalf("the judge must not remove a caller's tree: %v", err) ++ } ++} ++ ++// If no private directory can be made, nothing is spawned in the caller's directory as a fallback: ++// the call fails as a transport error, which the ladder retries and the run records. ++func TestJudgesRefuseToRunWithoutAnIsolatedDir(t *testing.T) { ++ saved := isolatedDir ++ t.Cleanup(func() { isolatedDir = saved }) ++ isolatedDir = func() (string, func(), error) { return "", func() {}, errors.New("disk full") } ++ ++ calls := 0 ++ spy := func(context.Context, string, []string, string) ([]byte, int, error) { ++ calls++ ++ return nil, 0, nil ++ } ++ cj := &claudeJudge{exec: spy, nonce: func() string { return "n0" }, clock: codexClock()} ++ _, cerr := cj.Call(context.Background(), claudeRequest()) ++ xj := &codexJudge{exec: spy, nonce: func() string { return "n0" }, clock: codexClock()} ++ _, xerr := xj.Call(context.Background(), codexRequest()) ++ for name, err := range map[string]error{"claude": cerr, "codex": xerr} { ++ var e *errs.Error ++ if !errors.As(err, &e) || e.Code != CodeJudgeTransport || !strings.Contains(e.Detail, "isolated") { ++ t.Fatalf("%s: want an isolated-dir transport error, got %v", name, err) ++ } ++ } ++ if calls != 0 { ++ t.Fatalf("a CLI was spawned without an isolated directory (%d calls)", calls) ++ } ++} ++ ++// The real directory maker: private (0700), empty, under the user's own cache directory (not a ++// shared temp dir, where another local user could plant a .git root and repo-scoped skills a CLI ++// walks up to), and removed by its cleanup. ++func TestIsolatedDir(t *testing.T) { ++ cache := t.TempDir() ++ saved := userCacheDir ++ t.Cleanup(func() { userCacheDir = saved }) ++ userCacheDir = func() (string, error) { return cache, nil } ++ ++ dir, cleanup, err := isolatedDir() ++ if err != nil { ++ t.Fatal(err) ++ } ++ info, err := os.Stat(dir) ++ if err != nil || !info.IsDir() || info.Mode().Perm() != 0o700 { ++ t.Fatalf("stat %s: %v %v", dir, info, err) ++ } ++ if want := filepath.Join(cache, "metareview", "judge") + string(filepath.Separator); !strings.HasPrefix(dir, want) { ++ t.Fatalf("dir %s is not under the user cache base %s", dir, want) ++ } ++ base, err := os.Stat(filepath.Join(cache, "metareview", "judge")) ++ if err != nil || base.Mode().Perm() != 0o700 { ++ t.Fatalf("the base must be private: %v %v", base, err) ++ } ++ cleanup() ++ if _, err := os.Stat(dir); !errors.Is(err, os.ErrNotExist) { ++ t.Fatalf("cleanup left %s", dir) ++ } ++} ++ ++// No user cache directory (HOME unset), or one that cannot hold the base: fall back to the temp ++// directory rather than refuse to judge. ++func TestIsolatedDirFallsBackToTheTempDir(t *testing.T) { ++ saved := userCacheDir ++ t.Cleanup(func() { userCacheDir = saved }) ++ tmp := t.TempDir() ++ t.Setenv("TMPDIR", tmp) ++ blocked := filepath.Join(t.TempDir(), "file") ++ if err := os.WriteFile(blocked, nil, 0o600); err != nil { ++ t.Fatal(err) ++ } ++ for name, cacheDir := range map[string]func() (string, error){ ++ "no cache dir": func() (string, error) { return "", errors.New("$HOME is not defined") }, ++ "unusable cache": func() (string, error) { return blocked, nil }, // a file: MkdirAll fails ++ } { ++ userCacheDir = cacheDir ++ dir, cleanup, err := isolatedDir() ++ if err != nil || !strings.HasPrefix(dir, tmp) { ++ t.Fatalf("%s: want a dir under %s, got %q %v", name, tmp, dir, err) ++ } ++ cleanup() ++ } ++} ++ ++// With no usable directory anywhere the maker reports the error rather than returning a directory ++// the CLI would then be run in. ++func TestIsolatedDirReportsAnUnusableTempDir(t *testing.T) { ++ saved := userCacheDir ++ t.Cleanup(func() { userCacheDir = saved }) ++ userCacheDir = func() (string, error) { return "", errors.New("no home") } ++ t.Setenv("TMPDIR", filepath.Join(t.TempDir(), "does-not-exist")) ++ dir, cleanup, err := isolatedDir() ++ if err == nil || dir != "" { ++ t.Fatalf("want an error and no dir, got %q %v", dir, err) ++ } ++ cleanup() // a no-op, but always safe to call ++} ++ ++// A panic inside the CLI seam still removes the attempt's directory. ++func TestJudgesRemoveTheDirEvenOnPanic(t *testing.T) { ++ for name, call := range map[string]func(ClaudeExec){ ++ "claude": func(e ClaudeExec) { ++ _, _ = (&claudeJudge{exec: e, nonce: func() string { return "n0" }, clock: codexClock()}).Call(context.Background(), claudeRequest()) ++ }, ++ "codex": func(e ClaudeExec) { ++ _, _ = (&codexJudge{exec: CodexExec(e), nonce: func() string { return "n0" }, clock: codexClock()}).Call(context.Background(), codexRequest()) ++ }, ++ } { ++ var dir string ++ func() { ++ defer func() { _ = recover() }() ++ call(func(_ context.Context, d string, _ []string, _ string) ([]byte, int, error) { ++ dir = d ++ panic("boom") ++ }) ++ }() ++ if dir == "" { ++ t.Fatalf("%s: the seam was never called", name) ++ } ++ if _, err := os.Stat(dir); !errors.Is(err, os.ErrNotExist) { ++ t.Fatalf("%s: a panic left %s behind", name, dir) ++ } ++ } ++} ++ ++// Judge calls are not sessions: nothing is persisted (and with a fresh directory per attempt, ++// persisting would leave one transcript directory behind per call). ++func TestJudgesPersistNoSession(t *testing.T) { ++ f := &fakeClaude{stdout: claudeJSON(`{"reasoning":"r","is_real":true,"confidence":0.9}`)} ++ if _, err := (&claudeJudge{exec: f.exec, nonce: func() string { return "n0" }, clock: codexClock()}).Call(context.Background(), claudeRequest()); err != nil { ++ t.Fatal(err) ++ } ++ if !strings.Contains(strings.Join(f.args, " "), "--no-session-persistence") { ++ t.Fatalf("claude args %q", f.args) ++ } ++ var args []string ++ x := &codexJudge{exec: func(_ context.Context, _ string, a []string, _ string) ([]byte, int, error) { ++ args = a ++ return []byte(codexStream(`{"reasoning":"r","is_real":true,"confidence":0.9}`)), 0, nil ++ }, nonce: func() string { return "n0" }, clock: codexClock()} ++ if _, err := x.Call(context.Background(), codexRequest()); err != nil { ++ t.Fatal(err) ++ } ++ if !strings.Contains(strings.Join(args, " "), "--ephemeral") { ++ t.Fatalf("codex args %q", args) ++ } ++} +diff --git a/internal/version/version.go b/internal/version/version.go +index 79c28ad..e768901 100644 +--- a/internal/version/version.go ++++ b/internal/version/version.go +@@ -1,3 +1,3 @@ + package version + +-const Version = "0.13.2" ++const Version = "0.13.3" +diff --git a/package.json b/package.json +index 9b34346..7ba81ec 100644 +--- a/package.json ++++ b/package.json +@@ -1,6 +1,6 @@ + { + "name": "metareview", +- "version": "0.13.2", ++ "version": "0.13.3", + "description": "Go-based metaswarm-compatible internal review harness for plans, specs, decompositions, code, acceptance evidence, PR readiness, and post-merge learning", + "bin": { + "metareview": "cli/metareview.js" + + + +``` + +## Review Logs + +- --help: PASS (docs/metareview/reviews/mrv-20260822-220108922308000-task-done-help-9a8265a5.md) +- CHANGELOG.md: PASS (docs/metareview/reviews/mrv-20260908-073527271951000-artifact-changelog-ab09011f.md) +- cmd/metareview/reviewhelp_test.go: PASS_ADVISORY (docs/metareview/reviews/mrv-20260924-062613603963000-task-done-reviewhelp-test-b88768c5.md) +- docs-0.6.0-documentation: PASS (docs/metareview/reviews/mrv-20260705-161047045358000-task-done-docs-0-6-a2ac175a.md) +- docs-fsm-result-contract: PASS (docs/metareview/reviews/mrv-20260705-215610270004000-task-done-docs-fsm-result-contract-cbf40d94.md) +- docs/metareview/evidence/t0.1-finding-identity-rewire.md: PASS (docs/metareview/reviews/mrv-20260901-042205337128000-task-done-t0-1-finding-identity-rewire-01ca0bf9.md) +- docs/metareview/evidence/t1.3-pins-proven-gate-pr-a.md: PASS (docs/metareview/reviews/mrv-20260901-051912009394000-task-done-t1-3-pins-proven-gate-pr-a-51440dbf.md) +- docs/quickstart.md: PASS (docs/metareview/reviews/mrv-20260528-222809220757000-task-done-quickstart-f3ea78a9.md) +- docs/specs/2026-05-27-artifact-review-parallel-subagents.md: PASS (docs/metareview/reviews/mrv-20260527-193044946912000-task-done-2026-05-27-artifact-review-parallel-subagents-cf4f2168.md) +- docs/specs/2026-05-27-fail-closed-artifact-review-gates.md: PASS (docs/metareview/reviews/mrv-20260527-182255753628000-task-done-2026-05-27-fail-closed-artifact-review-gates-5243a194.md) +- docs/superpowers/plans/2026-07-05-issue-2-wu4-review-manifest-shard-aggregation.md: PASS (docs/metareview/reviews/mrv-20260705-055835877369000-task-done-2026-07-05-issue-2-wu4-review-manifest-shard-agg-b924a5ff.md) +- docs/superpowers/plans/2026-09-23-mutation-incremental-1c-proof.md: PASS_ADVISORY (docs/metareview/reviews/mrv-20260924-034929320260000-task-done-2026-09-23-mutation-incremental-1c-proof-380f704d.md) +- docs/superpowers/plans/2026-09-23-mutation-incremental-2a-gate.md: PASS_ADVISORY (docs/metareview/reviews/mrv-20260924-045211619641000-task-done-2026-09-23-mutation-incremental-2a-gate-ed03c179.md) +- docs/superpowers/plans/2026-09-23-mutation-incremental-2b-views.md: PASS_ADVISORY (docs/metareview/reviews/mrv-20260924-051035853973000-task-done-2026-09-23-mutation-incremental-2b-views-6f8a9d6c.md) +- docs/tasks/m0-fsm-run-persistence.md: PASS (docs/metareview/reviews/mrv-20260827-130422417348000-task-done-m0-fsm-run-persistence-8a9eddc9.md) +- docs/tasks/m1-m6-fsm-packages.md: PASS (docs/metareview/reviews/mrv-20260827-090548364954000-task-done-m1-m6-fsm-packages-a99c72f1.md) +- docs/tasks/m7-fork-record-export.md: PASS (docs/metareview/reviews/mrv-20260827-104833919009000-task-done-m7-fork-record-export-9dc62119.md) +- docs/tasks/m8-cli-suite-docs.md: PASS (docs/metareview/reviews/mrv-20260827-114104823011000-task-done-m8-cli-suite-docs-46a961bc.md) +- docs/tasks/pr-a-sharded-measurement.md: PASS (docs/metareview/reviews/mrv-20260827-164005027099000-task-done-pr-a-sharded-measurement-8215c1f2.md) +- docs/tasks/pr-b-shard-ingestion.md: PASS (docs/metareview/reviews/mrv-20260827-181704826892000-task-done-pr-b-shard-ingestion-f6a50afe.md) +- docs/tasks/pr-b-slice-3.md: PASS (docs/metareview/reviews/mrv-20260827-181553019412000-task-done-pr-b-slice-3-e224e5b6.md) +- docs/tasks/pr-b-slices-1-2.md: PASS (docs/metareview/reviews/mrv-20260827-181426808610000-task-done-pr-b-slices-1-2-ce580d87.md) +- docs/tasks/process-overrides.md: PASS (docs/metareview/reviews/mrv-20260827-180059866454000-task-done-process-overrides-93d185e3.md) +- fix-node-elicits-proofs: PASS (docs/metareview/reviews/mrv-20260901-183938772835000-task-done-fix-node-elicits-proofs-e988c555.md) +- fix-pin-id-finding-consistency: PASS (docs/metareview/reviews/mrv-20260901-191438733281000-task-done-fix-pin-id-finding-consistency-b6a37c98.md) +- fix-proof-binds-fix-diff: PASS (docs/metareview/reviews/mrv-20260901-204852362514000-task-done-fix-proof-binds-fix-diff-3ad76b46.md) +- github-reviewdecision-prready-fix: PASS (docs/metareview/reviews/mrv-20260705-221742306514000-task-done-github-reviewdecision-prready-fix-3de6a2de.md) +- internal/fsm/gate/gates.go: PASS (docs/metareview/reviews/mrv-20260902-040527894922000-task-done-gates-bca330f8.md) +- internal/fsm/judge/judge.go: PASS (docs/metareview/reviews/mrv-20260902-045920838914000-task-done-judge-b7060768.md) +- internal/fsm/kind/kind.go: PASS_ADVISORY (docs/metareview/reviews/mrv-20260910-153756405005000-task-done-kind-b7bb121c.md) +- internal/gitcontext/gitcontext.go: PASS (docs/metareview/reviews/mrv-20260902-060726814084000-task-done-gitcontext-9e14bb40.md) +- internal/githubcontext/githubcontext.go: PASS (docs/metareview/reviews/mrv-20260902-034259118558000-task-done-githubcontext-37fa1398.md) +- internal/prready/review.go: PASS (docs/metareview/reviews/mrv-20260902-040416607878000-task-done-review-c809e786.md) +- internal/reviewers/epicready.go: PASS (docs/metareview/reviews/mrv-20260902-035232185408000-task-done-epicready-27fabbd2.md) +- internal/testconv/typescript.go: PASS (docs/metareview/reviews/mrv-20260902-051859934731000-task-done-typescript-4dea666f.md) +- issue-2-wu1: PASS (docs/metareview/reviews/mrv-20260705-012852131814000-task-done-issue-2-wu1-a2da3dbe.md) +- issue-2-wu1-version-0.6.0: PASS (docs/metareview/reviews/mrv-20260705-030845991374000-task-done-issue-2-wu1-version-0-6-69ccbe27.md) +- issue-2-wu2: PASS (docs/metareview/reviews/mrv-20260705-034317422615000-task-done-issue-2-wu2-eb5dad78.md) +- issue-2-wu2-cursor-followup-fix: PASS (docs/metareview/reviews/mrv-20260705-040500203996000-task-done-issue-2-wu2-cursor-followup-fix-e87ecaa9.md) +- issue-2-wu2-prready-filter-fix: PASS (docs/metareview/reviews/mrv-20260705-035954884586000-task-done-issue-2-wu2-prready-filter-fix-d4005ab8.md) +- issue-2-wu2-review-fix: PASS (docs/metareview/reviews/mrv-20260705-035540088635000-task-done-issue-2-wu2-review-fix-05b5e486.md) +- issue-2-wu3: PASS (docs/metareview/reviews/mrv-20260705-050759694908000-task-done-issue-2-wu3-2688b027.md) +- mechanical-precision-lens: PASS (docs/metareview/reviews/mrv-20260831-183207933768000-task-done-mechanical-precision-lens-c79c1389.md) +- multilang-pytest-conv: PASS (docs/metareview/reviews/mrv-20260901-190331877901000-task-done-multilang-pytest-conv-1f2195e7.md) +- multilang-testconv-seam: PASS (docs/metareview/reviews/mrv-20260901-165623149954000-task-done-multilang-testconv-seam-01cc5b3d.md) +- multilang-typescript-conv: PASS (docs/metareview/reviews/mrv-20260901-172216437441000-task-done-multilang-typescript-conv-6cdb50c2.md) +- multilang-vitest-conv: PASS (docs/metareview/reviews/mrv-20260901-174949441189000-task-done-multilang-vitest-conv-56254690.md) +- runchain-headsha-escalation-reset: PASS (docs/metareview/reviews/mrv-20260705-222055675782000-task-done-runchain-headsha-escalation-reset-37b22cb1.md) +- runchain-reset-findings-reconciliation: PASS (docs/metareview/reviews/mrv-20260705-223346154492000-task-done-runchain-reset-findings-reconciliation-6cafdbd1.md) +- t1.1-differentialproof-model: PASS (docs/metareview/reviews/mrv-20260901-001859561454000-task-done-t1-aa37529c.md) +- t1.2-agentedit-reduce-seam: PASS (docs/metareview/reviews/mrv-20260901-002517703556000-task-done-t1-b5a41665.md) +- t1.3-reproduction-engine-pr-b: PASS (docs/metareview/reviews/mrv-20260901-080559565942000-task-done-t1-8af2654f.md) +- t1.4-right-reason-reviewer: PASS (docs/metareview/reviews/mrv-20260901-100944138141000-task-done-t1-ab64ea55.md) +- t1.5-deletion-provable-fix: PASS (docs/metareview/reviews/mrv-20260901-105248210036000-task-done-t1-4ad043b2.md) +- t1.6-trivial-pin-prescreen: PASS (docs/metareview/reviews/mrv-20260901-115747009329000-task-done-t1-b353673e.md) +- t2.1-test-deletion-gate: PASS (docs/metareview/reviews/mrv-20260901-145243731066000-task-done-t2-0fe76ae4.md) +- t2.2-trajectory-monitor: PASS (docs/metareview/reviews/mrv-20260901-154004114739000-task-done-t2-d4fcf27a.md) +- workflows/sdlc-loop-clean.yaml: PASS (docs/metareview/reviews/mrv-20260902-082859947245000-task-done-sdlc-loop-clean-510ba162.md) + +## Knowledge And Registries + +Service inventory: none + +No service inventory found. + +Knowledge facts: + +No Beads knowledge facts found. + +## Validation Evidence + +{"schemaVersion":1,"kind":"validation","command":["go","test","./..."],"cwd":".","exitCode":0,"startedAt":"2026-09-27T02:38:20.290556Z","finishedAt":"2026-09-27T02:38:57.044289Z","stdoutSha256":"fd3b831078600f0ba7802628b9b0aaa15e2286d0d4eb48dd5fc20e85e8923082","stderrSha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","summary":"go test ./... exited 0"} + +## GitHub Context + +GitHub context unavailable: pr-number-unavailable + + +## Suggested PR Evidence + +## metareview PR Evidence + +### Summary + +fix/judge-cli-isolation changes .claude-plugin/marketplace.json, .claude-plugin/plugin.json, .codex-plugin/plugin.json, CHANGELOG.md, internal/fsm/cli/deps.go, internal/fsm/judge/claude.go, internal/fsm/judge/codex.go, internal/fsm/judge/codex_test.go, internal/fsm/judge/isolate.go, internal/fsm/judge/isolation_test.go, internal/version/version.go, package.json + +### Validation + +- structured validation: go test ./... exited 0 (exit 0) + +### Task Review Evidence + +- --help: PASS (docs/metareview/reviews/mrv-20260822-220108922308000-task-done-help-9a8265a5.md) +- CHANGELOG.md: PASS (docs/metareview/reviews/mrv-20260908-073527271951000-artifact-changelog-ab09011f.md) +- cmd/metareview/reviewhelp_test.go: PASS_ADVISORY (docs/metareview/reviews/mrv-20260924-062613603963000-ta[REDACTED].md) findings: mrvf-20260924-062613603963000-ta[REDACTED] +- docs-0.6.0-documentation: PASS (docs/metareview/reviews/mrv-20260705-161047045358000-ta[REDACTED].md) +- docs-fsm-result-contract: PASS (docs/metareview/reviews/mrv-20260705-215610270004000-ta[REDACTED].md) +- docs/metareview/evidence/t0.1-finding-identity-rewire.md: PASS (docs/metareview/reviews/mrv-20260901-042205337128000-ta[REDACTED].md) +- docs/metareview/evidence/t1.3-pins-proven-gate-pr-a.md: PASS (docs/metareview/reviews/mrv-20260901-051912009394000-ta[REDACTED].md) +- docs/quickstart.md: PASS (docs/metareview/reviews/mrv-20260528-222809220757000-ta[REDACTED].md) +- docs/specs/2026-05-27-artifact-review-parallel-subagents.md: PASS (docs/metareview/reviews/mrv-20260527-193044946912000-ta[REDACTED].md) +- docs/specs/2026-05-27-fail-closed-artifact-review-gates.md: PASS (docs/metareview/reviews/mrv-20260527-182255753628000-ta[REDACTED].md) +- docs/superpowers/plans/2026-07-05-issue-2-wu4-review-manifest-shard-aggregation.md: PASS (docs/metareview/reviews/mrv-20260705-055835877369000-ta[REDACTED].md) +- docs/superpowers/plans/2026-09-23-mutation-incremental-1c-proof.md: PASS_ADVISORY (docs/metareview/reviews/mrv-20260924-034929320260000-ta[REDACTED].md) findings: mrvf-20260924-034929320260000-ta[REDACTED], mrvf-20260924-034929320260000-ta[REDACTED], mrvf-20260924-034929320260000-ta[REDACTED] +- docs/superpowers/plans/2026-09-23-mutation-incremental-2a-gate.md: PASS_ADVISORY (docs/metareview/reviews/mrv-20260924-045211619641000-ta[REDACTED].md) findings: mrvf-20260924-045211619641000-ta[REDACTED], mrvf-20260924-045211619641000-ta[REDACTED], mrvf-20260924-045211619641000-ta[REDACTED] +- docs/superpowers/plans/2026-09-23-mutation-incremental-2b-views.md: PASS_ADVISORY (docs/metareview/reviews/mrv-20260924-051035853973000-ta[REDACTED].md) findings: mrvf-20260924-051035853973000-ta[REDACTED] +- docs/tasks/m0-fsm-run-persistence.md: PASS (docs/metareview/reviews/mrv-20260827-130422417348000-ta[REDACTED].md) +- docs/tasks/m1-m6-fsm-packages.md: PASS (docs/metareview/reviews/mrv-20260827-090548364954000-ta[REDACTED].md) +- docs/tasks/m7-fork-record-export.md: PASS (docs/metareview/reviews/mrv-20260827-104833919009000-ta[REDACTED].md) +- docs/tasks/m8-cli-suite-docs.md: PASS (docs/metareview/reviews/mrv-20260827-114104823011000-ta[REDACTED].md) +- docs/tasks/pr-a-sharded-measurement.md: PASS (docs/metareview/reviews/mrv-20260827-164005027099000-ta[REDACTED].md) +- docs/tasks/pr-b-shard-ingestion.md: PASS (docs/metareview/reviews/mrv-20260827-181704826892000-ta[REDACTED].md) +- docs/tasks/pr-b-slice-3.md: PASS (docs/metareview/reviews/mrv-20260827-181553019412000-ta[REDACTED].md) +- docs/tasks/pr-b-slices-1-2.md: PASS (docs/metareview/reviews/mrv-20260827-181426808610000-ta[REDACTED].md) +- docs/tasks/process-overrides.md: PASS (docs/metareview/reviews/mrv-20260827-180059866454000-ta[REDACTED].md) +- fix-node-elicits-proofs: PASS (docs/metareview/reviews/mrv-20260901-183938772835000-ta[REDACTED].md) +- fix-pin-id-finding-consistency: PASS (docs/metareview/reviews/mrv-20260901-191438733281000-ta[REDACTED].md) +- fix-proof-binds-fix-diff: PASS (docs/metareview/reviews/mrv-20260901-204852362514000-ta[REDACTED].md) +- github-reviewdecision-prready-fix: PASS (docs/metareview/reviews/mrv-20260705-221742306514000-ta[REDACTED].md) +- internal/fsm/gate/gates.go: PASS (docs/metareview/reviews/mrv-20260902-040527894922000-task-done-gates-bca330f8.md) +- internal/fsm/judge/judge.go: PASS (docs/metareview/reviews/mrv-20260902-045920838914000-task-done-judge-b7060768.md) +- internal/fsm/kind/kind.go: PASS_ADVISORY (docs/metareview/reviews/mrv-20260910-153756405005000-task-done-kind-b7bb121c.md) findings: mrvf-20260910-151423657039000-ta[REDACTED], mrvf-20260910-151423657039000-ta[REDACTED], mrvf-20260910-151538496642000-ta[REDACTED] +- internal/gitcontext/gitcontext.go: PASS (docs/metareview/reviews/mrv-20260902-060726814084000-ta[REDACTED].md) +- internal/githubcontext/githubcontext.go: PASS (docs/metareview/reviews/mrv-20260902-034259118558000-ta[REDACTED].md) +- internal/prready/review.go: PASS (docs/metareview/reviews/mrv-20260902-040416607878000-ta[REDACTED].md) +- internal/testconv/typescript.go: PASS (docs/metareview/reviews/mrv-20260902-051859934731000-ta[REDACTED].md) +- issue-2-wu1: PASS (docs/metareview/reviews/mrv-20260705-012852131814000-ta[REDACTED].md) +- issue-2-wu1-version-0.6.0: PASS (docs/metareview/reviews/mrv-20260705-030845991374000-ta[REDACTED].md) +- issue-2-wu2: PASS (docs/metareview/reviews/mrv-20260705-034317422615000-ta[REDACTED].md) +- issue-2-wu2-cursor-followup-fix: PASS (docs/metareview/reviews/mrv-20260705-040500203996000-ta[REDACTED].md) +- issue-2-wu2-prready-filter-fix: PASS (docs/metareview/reviews/mrv-20260705-035954884586000-ta[REDACTED].md) +- issue-2-wu2-review-fix: PASS (docs/metareview/reviews/mrv-20260705-035540088635000-ta[REDACTED].md) +- issue-2-wu3: PASS (docs/metareview/reviews/mrv-20260705-050759694908000-ta[REDACTED].md) +- mechanical-precision-lens: PASS (docs/metareview/reviews/mrv-20260831-183207933768000-ta[REDACTED].md) +- multilang-pytest-conv: PASS (docs/metareview/reviews/mrv-20260901-190331877901000-ta[REDACTED].md) +- multilang-testconv-seam: PASS (docs/metareview/reviews/mrv-20260901-165623149954000-ta[REDACTED].md) +- multilang-typescript-conv: PASS (docs/metareview/reviews/mrv-20260901-172216437441000-ta[REDACTED].md) +- multilang-vitest-conv: PASS (docs/metareview/reviews/mrv-20260901-174949441189000-ta[REDACTED].md) +- runchain-headsha-escalation-reset: PASS (docs/metareview/reviews/mrv-20260705-222055675782000-ta[REDACTED].md) +- runchain-reset-findings-reconciliation: PASS (docs/metareview/reviews/mrv-20260705-223346154492000-ta[REDACTED].md) +- t1.1-differentialproof-model: PASS (docs/metareview/reviews/mrv-20260901-001859561454000-task-done-t1-aa37529c.md) +- t1.2-agentedit-reduce-seam: PASS (docs/metareview/reviews/mrv-20260901-002517703556000-task-done-t1-b5a41665.md) +- t1.3-reproduction-engine-pr-b: PASS (docs/metareview/reviews/mrv-20260901-080559565942000-task-done-t1-8af2654f.md) +- t1.4-right-reason-reviewer: PASS (docs/metareview/reviews/mrv-20260901-100944138141000-task-done-t1-ab64ea55.md) +- t1.5-deletion-provable-fix: PASS (docs/metareview/reviews/mrv-20260901-105248210036000-task-done-t1-4ad043b2.md) +- t1.6-trivial-pin-prescreen: PASS (docs/metareview/reviews/mrv-20260901-115747009329000-task-done-t1-b353673e.md) +- t2.1-test-deletion-gate: PASS (docs/metareview/reviews/mrv-20260901-145243731066000-task-done-t2-0fe76ae4.md) +- t2.2-trajectory-monitor: PASS (docs/metareview/reviews/mrv-20260901-154004114739000-task-done-t2-d4fcf27a.md) +- workflows/sdlc-loop-clean.yaml: PASS (docs/metareview/reviews/mrv-20260902-082859947245000-ta[REDACTED].md) + +### Epic Review Evidence + +- internal/reviewers/epicready.go: PASS (docs/metareview/reviews/mrv-20260902-035232185408000-ta[REDACTED].md) + +### Recorded-Evidence Blocker Status + +_Blockers carried forward from recorded review evidence; findings this gate run raised are under `## Blocking Findings` above._ + +No unresolved blockers in the recorded review evidence. + +### External GitHub Review Context + +GitHub context unavailable: pr-number-unavailable + diff --git a/docs/metareview/reviews/mrv-20260927-023857249886000-pr-ready-branch-10d735e5.md b/docs/metareview/reviews/mrv-20260927-023857249886000-pr-ready-branch-10d735e5.md new file mode 100644 index 00000000..15b0f929 --- /dev/null +++ b/docs/metareview/reviews/mrv-20260927-023857249886000-pr-ready-branch-10d735e5.md @@ -0,0 +1,149 @@ +# metareview: pr-ready review + +Run ID: `mrv-20260927-023857249886000-pr-ready-branch-10d735e5` + +Target: `current branch` + +Context pack: `docs/metareview/context/mrv-20260927-023857249886000-pr-ready-branch-10d735e5-context.md` + +Execution mode: `deterministic-local` + +Gate effect: `gate` + +Previous run: `mrv-20260927-021621749506000-pr-ready-branch-10d735e5` + +Attempt: `2/3` + +Reviewer input digest: `sha256:22598681355a4d6c01b56981c362491a673caa4c617d51213d59f8c012d405a0` + +Covered paths: `[".claude-plugin/marketplace.json",".claude-plugin/plugin.json",".codex-plugin/plugin.json","CHANGELOG.md","internal/fsm/cli/deps.go","internal/fsm/judge/claude.go","internal/fsm/judge/codex.go","internal/fsm/judge/codex_test.go","internal/fsm/judge/isolate.go","internal/fsm/judge/isolation_test.go","internal/version/version.go","package.json"]` + +## Verdict + +PASS_ADVISORY + +## Reviewer Results + +| Reviewer | Verdict | Blocking | Notes | +| --- | --- | ---: | --- | +| pr-readiness-reviewer | PASS | 0 | No blocking findings. | +| validation-reviewer | PASS | 0 | No blocking findings. | +| security-reviewer | PASS | 0 | No blocking findings. | +| code-quality-reviewer | PASS | 0 | No blocking findings. | +| architecture-reviewer | PASS | 0 | No blocking findings. | +| external-reviewer | PASS | 0 | No blocking findings. | + +## Blocking Findings + +No findings in this class. + + +## Advisory Findings + +### mrvf-20260927-021621749506000-pr-ready-branch-10d735e5-001: Adversarial review was in-session-emulated + +- Reviewer: adversarial-review-reviewer +- Severity: low +- Classification: advisory +- Finding: The adjudicated review for this head was recorded as in-session-emulated (no independent subagents), which is weaker, non-independent evidence. +- Expected: An independent subagent-adjudicated review where delegation is available. +- Found: executionMode = in-session-emulated. +- Recommendation: When subagents are available, prefer `metareview fsm --workflow review-loop` for an independent review. + + +## Follow-up Findings + +No findings in this class. + + +## Warnings + +No findings in this class. + + +## Suggested PR Evidence + +## metareview PR Evidence + +### Summary + +fix/judge-cli-isolation changes .claude-plugin/marketplace.json, .claude-plugin/plugin.json, .codex-plugin/plugin.json, CHANGELOG.md, internal/fsm/cli/deps.go, internal/fsm/judge/claude.go, internal/fsm/judge/codex.go, internal/fsm/judge/codex_test.go, internal/fsm/judge/isolate.go, internal/fsm/judge/isolation_test.go, internal/version/version.go, package.json + +### Validation + +- structured validation: go test ./... exited 0 (exit 0) + +### Task Review Evidence + +- --help: PASS (docs/metareview/reviews/mrv-20260822-220108922308000-task-done-help-9a8265a5.md) +- CHANGELOG.md: PASS (docs/metareview/reviews/mrv-20260908-073527271951000-artifact-changelog-ab09011f.md) +- cmd/metareview/reviewhelp_test.go: PASS_ADVISORY (docs/metareview/reviews/mrv-20260924-062613603963000-ta[REDACTED].md) findings: mrvf-20260924-062613603963000-ta[REDACTED] +- docs-0.6.0-documentation: PASS (docs/metareview/reviews/mrv-20260705-161047045358000-ta[REDACTED].md) +- docs-fsm-result-contract: PASS (docs/metareview/reviews/mrv-20260705-215610270004000-ta[REDACTED].md) +- docs/metareview/evidence/t0.1-finding-identity-rewire.md: PASS (docs/metareview/reviews/mrv-20260901-042205337128000-ta[REDACTED].md) +- docs/metareview/evidence/t1.3-pins-proven-gate-pr-a.md: PASS (docs/metareview/reviews/mrv-20260901-051912009394000-ta[REDACTED].md) +- docs/quickstart.md: PASS (docs/metareview/reviews/mrv-20260528-222809220757000-ta[REDACTED].md) +- docs/specs/2026-05-27-artifact-review-parallel-subagents.md: PASS (docs/metareview/reviews/mrv-20260527-193044946912000-ta[REDACTED].md) +- docs/specs/2026-05-27-fail-closed-artifact-review-gates.md: PASS (docs/metareview/reviews/mrv-20260527-182255753628000-ta[REDACTED].md) +- docs/superpowers/plans/2026-07-05-issue-2-wu4-review-manifest-shard-aggregation.md: PASS (docs/metareview/reviews/mrv-20260705-055835877369000-ta[REDACTED].md) +- docs/superpowers/plans/2026-09-23-mutation-incremental-1c-proof.md: PASS_ADVISORY (docs/metareview/reviews/mrv-20260924-034929320260000-ta[REDACTED].md) findings: mrvf-20260924-034929320260000-ta[REDACTED], mrvf-20260924-034929320260000-ta[REDACTED], mrvf-20260924-034929320260000-ta[REDACTED] +- docs/superpowers/plans/2026-09-23-mutation-incremental-2a-gate.md: PASS_ADVISORY (docs/metareview/reviews/mrv-20260924-045211619641000-ta[REDACTED].md) findings: mrvf-20260924-045211619641000-ta[REDACTED], mrvf-20260924-045211619641000-ta[REDACTED], mrvf-20260924-045211619641000-ta[REDACTED] +- docs/superpowers/plans/2026-09-23-mutation-incremental-2b-views.md: PASS_ADVISORY (docs/metareview/reviews/mrv-20260924-051035853973000-ta[REDACTED].md) findings: mrvf-20260924-051035853973000-ta[REDACTED] +- docs/tasks/m0-fsm-run-persistence.md: PASS (docs/metareview/reviews/mrv-20260827-130422417348000-ta[REDACTED].md) +- docs/tasks/m1-m6-fsm-packages.md: PASS (docs/metareview/reviews/mrv-20260827-090548364954000-ta[REDACTED].md) +- docs/tasks/m7-fork-record-export.md: PASS (docs/metareview/reviews/mrv-20260827-104833919009000-ta[REDACTED].md) +- docs/tasks/m8-cli-suite-docs.md: PASS (docs/metareview/reviews/mrv-20260827-114104823011000-ta[REDACTED].md) +- docs/tasks/pr-a-sharded-measurement.md: PASS (docs/metareview/reviews/mrv-20260827-164005027099000-ta[REDACTED].md) +- docs/tasks/pr-b-shard-ingestion.md: PASS (docs/metareview/reviews/mrv-20260827-181704826892000-ta[REDACTED].md) +- docs/tasks/pr-b-slice-3.md: PASS (docs/metareview/reviews/mrv-20260827-181553019412000-ta[REDACTED].md) +- docs/tasks/pr-b-slices-1-2.md: PASS (docs/metareview/reviews/mrv-20260827-181426808610000-ta[REDACTED].md) +- docs/tasks/process-overrides.md: PASS (docs/metareview/reviews/mrv-20260827-180059866454000-ta[REDACTED].md) +- fix-node-elicits-proofs: PASS (docs/metareview/reviews/mrv-20260901-183938772835000-ta[REDACTED].md) +- fix-pin-id-finding-consistency: PASS (docs/metareview/reviews/mrv-20260901-191438733281000-ta[REDACTED].md) +- fix-proof-binds-fix-diff: PASS (docs/metareview/reviews/mrv-20260901-204852362514000-ta[REDACTED].md) +- github-reviewdecision-prready-fix: PASS (docs/metareview/reviews/mrv-20260705-221742306514000-ta[REDACTED].md) +- internal/fsm/gate/gates.go: PASS (docs/metareview/reviews/mrv-20260902-040527894922000-task-done-gates-bca330f8.md) +- internal/fsm/judge/judge.go: PASS (docs/metareview/reviews/mrv-20260902-045920838914000-task-done-judge-b7060768.md) +- internal/fsm/kind/kind.go: PASS_ADVISORY (docs/metareview/reviews/mrv-20260910-153756405005000-task-done-kind-b7bb121c.md) findings: mrvf-20260910-151423657039000-ta[REDACTED], mrvf-20260910-151423657039000-ta[REDACTED], mrvf-20260910-151538496642000-ta[REDACTED] +- internal/gitcontext/gitcontext.go: PASS (docs/metareview/reviews/mrv-20260902-060726814084000-ta[REDACTED].md) +- internal/githubcontext/githubcontext.go: PASS (docs/metareview/reviews/mrv-20260902-034259118558000-ta[REDACTED].md) +- internal/prready/review.go: PASS (docs/metareview/reviews/mrv-20260902-040416607878000-ta[REDACTED].md) +- internal/testconv/typescript.go: PASS (docs/metareview/reviews/mrv-20260902-051859934731000-ta[REDACTED].md) +- issue-2-wu1: PASS (docs/metareview/reviews/mrv-20260705-012852131814000-ta[REDACTED].md) +- issue-2-wu1-version-0.6.0: PASS (docs/metareview/reviews/mrv-20260705-030845991374000-ta[REDACTED].md) +- issue-2-wu2: PASS (docs/metareview/reviews/mrv-20260705-034317422615000-ta[REDACTED].md) +- issue-2-wu2-cursor-followup-fix: PASS (docs/metareview/reviews/mrv-20260705-040500203996000-ta[REDACTED].md) +- issue-2-wu2-prready-filter-fix: PASS (docs/metareview/reviews/mrv-20260705-035954884586000-ta[REDACTED].md) +- issue-2-wu2-review-fix: PASS (docs/metareview/reviews/mrv-20260705-035540088635000-ta[REDACTED].md) +- issue-2-wu3: PASS (docs/metareview/reviews/mrv-20260705-050759694908000-ta[REDACTED].md) +- mechanical-precision-lens: PASS (docs/metareview/reviews/mrv-20260831-183207933768000-ta[REDACTED].md) +- multilang-pytest-conv: PASS (docs/metareview/reviews/mrv-20260901-190331877901000-ta[REDACTED].md) +- multilang-testconv-seam: PASS (docs/metareview/reviews/mrv-20260901-165623149954000-ta[REDACTED].md) +- multilang-typescript-conv: PASS (docs/metareview/reviews/mrv-20260901-172216437441000-ta[REDACTED].md) +- multilang-vitest-conv: PASS (docs/metareview/reviews/mrv-20260901-174949441189000-ta[REDACTED].md) +- runchain-headsha-escalation-reset: PASS (docs/metareview/reviews/mrv-20260705-222055675782000-ta[REDACTED].md) +- runchain-reset-findings-reconciliation: PASS (docs/metareview/reviews/mrv-20260705-223346154492000-ta[REDACTED].md) +- t1.1-differentialproof-model: PASS (docs/metareview/reviews/mrv-20260901-001859561454000-task-done-t1-aa37529c.md) +- t1.2-agentedit-reduce-seam: PASS (docs/metareview/reviews/mrv-20260901-002517703556000-task-done-t1-b5a41665.md) +- t1.3-reproduction-engine-pr-b: PASS (docs/metareview/reviews/mrv-20260901-080559565942000-task-done-t1-8af2654f.md) +- t1.4-right-reason-reviewer: PASS (docs/metareview/reviews/mrv-20260901-100944138141000-task-done-t1-ab64ea55.md) +- t1.5-deletion-provable-fix: PASS (docs/metareview/reviews/mrv-20260901-105248210036000-task-done-t1-4ad043b2.md) +- t1.6-trivial-pin-prescreen: PASS (docs/metareview/reviews/mrv-20260901-115747009329000-task-done-t1-b353673e.md) +- t2.1-test-deletion-gate: PASS (docs/metareview/reviews/mrv-20260901-145243731066000-task-done-t2-0fe76ae4.md) +- t2.2-trajectory-monitor: PASS (docs/metareview/reviews/mrv-20260901-154004114739000-task-done-t2-d4fcf27a.md) +- workflows/sdlc-loop-clean.yaml: PASS (docs/metareview/reviews/mrv-20260902-082859947245000-ta[REDACTED].md) + +### Epic Review Evidence + +- internal/reviewers/epicready.go: PASS (docs/metareview/reviews/mrv-20260902-035232185408000-ta[REDACTED].md) + +### Recorded-Evidence Blocker Status + +_Blockers carried forward from recorded review evidence; findings this gate run raised are under `## Blocking Findings` above._ + +No unresolved blockers in the recorded review evidence. + +### External GitHub Review Context + +GitHub context unavailable: pr-number-unavailable + From 868cd84808cdb1fa7645c0d8d6645a15a402f4d7 Mon Sep 17 00:00:00 2001 From: David Sifry Date: Sat, 26 Sep 2026 19:54:57 -0700 Subject: [PATCH 6/7] fix(judge): fail closed without a user cache dir (no shared temp-dir fallback) CodeRabbit on #168: the os.TempDir fallback reopened the planted .git root + repo-scoped skills path under /tmp that the user-cache base exists to close. Without a usable cache dir the judge attempt now fails as a transport error; the CLI judges need $HOME for their OAuth session anyway. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 5 +-- internal/fsm/judge/isolate.go | 22 +++++++------ internal/fsm/judge/isolation_test.go | 47 ++++++++++++++-------------- 3 files changed, 39 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd981d06..47655ce5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,8 +16,9 @@ private directory can be made, the attempt fails as a transport error and nothing is spawned in the caller's directory. The directory is created under the user's own cache directory (`os.UserCacheDir`), not a shared temp dir, because Codex walks up from its working directory to - a `.git` root and loads repo-scoped skills it finds there; the temp dir is only a fallback when - there is no cache directory. Judge calls also no longer persist sessions: Claude gets + a `.git` root and loads repo-scoped skills it finds there. There is no temp-dir fallback: without + a usable cache directory the judge attempt fails (the CLI judges need `$HOME` for their OAuth + session anyway). Judge calls also no longer persist sessions: Claude gets `--no-session-persistence` and Codex `--ephemeral`, so a judge's full prompt and diff are no longer written into the user's session transcripts. Reported while reviewing #165; plan in #167 (W0). diff --git a/internal/fsm/judge/isolate.go b/internal/fsm/judge/isolate.go index 1d39ebfd..4910a9db 100644 --- a/internal/fsm/judge/isolate.go +++ b/internal/fsm/judge/isolate.go @@ -16,16 +16,20 @@ var userCacheDir = os.UserCacheDir // judge reads a byte of the prompt. The prompt arrives on stdin, so the CLI needs no files of the // repository at all. A var so the failure path is testable. // -// The directory lives under the user's own cache directory, not a shared temp dir: Codex walks up -// from its working directory to a .git root and loads the repo-scoped skills it finds there, so -// under a world-writable /tmp another local user could plant both. The temp dir is only the -// fallback when there is no usable cache directory (no $HOME). +// The directory lives under the user's own cache directory and nowhere else: Codex walks up from +// its working directory to a project root and loads the repo-scoped skills it finds on the way +// (project_doc_max_bytes=0 does not switch those off), so under a shared temp dir another local +// user could plant a .git root and skills in /tmp. There is deliberately no temp-dir fallback: +// without a usable cache directory the attempt fails, and the CLI judges need $HOME for their +// OAuth session anyway. var isolatedDir = func() (string, func(), error) { - base := os.TempDir() - if cache, err := userCacheDir(); err == nil { - if b := filepath.Join(cache, "metareview", "judge"); os.MkdirAll(b, 0o700) == nil { - base = b - } + cache, err := userCacheDir() + if err != nil { + return "", func() {}, err + } + base := filepath.Join(cache, "metareview", "judge") + if err := os.MkdirAll(base, 0o700); err != nil { + return "", func() {}, err } dir, err := os.MkdirTemp(base, "metareview-judge-") if err != nil { diff --git a/internal/fsm/judge/isolation_test.go b/internal/fsm/judge/isolation_test.go index 59fa04a6..8427a40d 100644 --- a/internal/fsm/judge/isolation_test.go +++ b/internal/fsm/judge/isolation_test.go @@ -196,42 +196,41 @@ func TestIsolatedDir(t *testing.T) { } } -// No user cache directory (HOME unset), or one that cannot hold the base: fall back to the temp -// directory rather than refuse to judge. -func TestIsolatedDirFallsBackToTheTempDir(t *testing.T) { +// No usable user cache directory: refuse, never fall back to a shared temp dir. Under /tmp another +// local user could plant a .git root and repo-scoped skills that Codex walks up to and loads +// (project_doc_max_bytes=0 does not switch skills off). The CLI judges need $HOME for their OAuth +// session anyway, so refusing costs nothing a working setup has. +func TestIsolatedDirFailsClosedWithoutAUserCacheDir(t *testing.T) { saved := userCacheDir t.Cleanup(func() { userCacheDir = saved }) - tmp := t.TempDir() - t.Setenv("TMPDIR", tmp) blocked := filepath.Join(t.TempDir(), "file") if err := os.WriteFile(blocked, nil, 0o600); err != nil { t.Fatal(err) } + readOnly := t.TempDir() + base := filepath.Join(readOnly, "metareview", "judge") + if err := os.MkdirAll(base, 0o700); err != nil { + t.Fatal(err) + } + if err := os.Chmod(base, 0o500); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = os.Chmod(base, 0o700) }) for name, cacheDir := range map[string]func() (string, error){ - "no cache dir": func() (string, error) { return "", errors.New("$HOME is not defined") }, - "unusable cache": func() (string, error) { return blocked, nil }, // a file: MkdirAll fails + "no cache dir": func() (string, error) { return "", errors.New("$HOME is not defined") }, + "unusable cache": func() (string, error) { return blocked, nil }, // a file: MkdirAll fails + "unwritable base": func() (string, error) { return readOnly, nil }, // MkdirTemp fails } { + if name == "unwritable base" && os.Geteuid() == 0 { + continue // root ignores the mode + } userCacheDir = cacheDir dir, cleanup, err := isolatedDir() - if err != nil || !strings.HasPrefix(dir, tmp) { - t.Fatalf("%s: want a dir under %s, got %q %v", name, tmp, dir, err) + if err == nil || dir != "" { + t.Fatalf("%s: want an error and no dir, got %q %v", name, dir, err) } - cleanup() - } -} - -// With no usable directory anywhere the maker reports the error rather than returning a directory -// the CLI would then be run in. -func TestIsolatedDirReportsAnUnusableTempDir(t *testing.T) { - saved := userCacheDir - t.Cleanup(func() { userCacheDir = saved }) - userCacheDir = func() (string, error) { return "", errors.New("no home") } - t.Setenv("TMPDIR", filepath.Join(t.TempDir(), "does-not-exist")) - dir, cleanup, err := isolatedDir() - if err == nil || dir != "" { - t.Fatalf("want an error and no dir, got %q %v", dir, err) + cleanup() // a no-op, but always safe to call } - cleanup() // a no-op, but always safe to call } // A panic inside the CLI seam still removes the attempt's directory. From de3edee927dd04e3cf6c1d645b8fae65c8a76334 Mon Sep 17 00:00:00 2001 From: David Sifry Date: Sat, 26 Sep 2026 19:55:18 -0700 Subject: [PATCH 7/7] docs(metareview): 0.13.3 pr-ready gate after CodeRabbit fix Co-Authored-By: Claude Opus 5.5 --- ...767000-pr-ready-branch-10d735e5-context.md | 818 ++++++++++++++++++ ...25518024767000-pr-ready-branch-10d735e5.md | 149 ++++ 2 files changed, 967 insertions(+) create mode 100644 docs/metareview/context/mrv-20260927-025518024767000-pr-ready-branch-10d735e5-context.md create mode 100644 docs/metareview/reviews/mrv-20260927-025518024767000-pr-ready-branch-10d735e5.md diff --git a/docs/metareview/context/mrv-20260927-025518024767000-pr-ready-branch-10d735e5-context.md b/docs/metareview/context/mrv-20260927-025518024767000-pr-ready-branch-10d735e5-context.md new file mode 100644 index 00000000..e37f1f11 --- /dev/null +++ b/docs/metareview/context/mrv-20260927-025518024767000-pr-ready-branch-10d735e5-context.md @@ -0,0 +1,818 @@ +# metareview pr-ready context + +Run ID: `mrv-20260927-025518024767000-pr-ready-branch-10d735e5` + +## Git + +- Base: `53ee3962f57c1f972aa532252b0a24cf808f4ffa` +- Head: `868cd84808cdb1fa7645c0d8d6645a15a402f4d7` +- Branch: `fix/judge-cli-isolation` +- Reviewer input digest: `sha256:3b9fd3f762e4de0ad49adec1d044fd8c10364a0bd2a773e60a69ca536725b119` +- Gate effect: `gate` + +## Context Profile + +- Raw diff bytes: `25244` +- Filtered diff bytes: `25244` +- Risk level: `none` +- Generated files excluded: docs/metareview/context/mrv-20260927-021621749506000-pr-ready-branch-10d735e5-context.md, docs/metareview/context/mrv-20260927-023857249886000-pr-ready-branch-10d735e5-context.md, docs/metareview/reviews/mrv-20260927-021621749506000-pr-ready-branch-10d735e5.md, docs/metareview/reviews/mrv-20260927-023857249886000-pr-ready-branch-10d735e5.md + +## Context Shard Plan + +Not sharded. + +## Review Manifest + +- Manifest verdict: `PASS` +- Source manifest hash: not sharded +- Runtime assessment: static-only; runtime not assessed + +### Source Paths +- .claude-plugin/marketplace.json +- .claude-plugin/plugin.json +- .codex-plugin/plugin.json +- CHANGELOG.md +- internal/fsm/cli/deps.go +- internal/fsm/judge/claude.go +- internal/fsm/judge/codex.go +- internal/fsm/judge/codex_test.go +- internal/fsm/judge/isolate.go +- internal/fsm/judge/isolation_test.go +- internal/version/version.go +- package.json + +### Path Dispositions +- docs/metareview/context/mrv-20260927-021621749506000-pr-ready-branch-10d735e5-context.md: generated (metareview generated review artifact excluded from source manifest) +- docs/metareview/context/mrv-20260927-023857249886000-pr-ready-branch-10d735e5-context.md: generated (metareview generated review artifact excluded from source manifest) +- docs/metareview/reviews/mrv-20260927-021621749506000-pr-ready-branch-10d735e5.md: generated (metareview generated review artifact excluded from source manifest) +- docs/metareview/reviews/mrv-20260927-023857249886000-pr-ready-branch-10d735e5.md: generated (metareview generated review artifact excluded from source manifest) + +### Manifest Blockers +No manifest blockers. + +## Changed Files + +- .claude-plugin/marketplace.json +- .claude-plugin/plugin.json +- .codex-plugin/plugin.json +- CHANGELOG.md +- internal/fsm/cli/deps.go +- internal/fsm/judge/claude.go +- internal/fsm/judge/codex.go +- internal/fsm/judge/codex_test.go +- internal/fsm/judge/isolate.go +- internal/fsm/judge/isolation_test.go +- internal/version/version.go +- package.json + +## Diff + +```diff +diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json +index 01c2af7..a112519 100644 +--- a/.claude-plugin/marketplace.json ++++ b/.claude-plugin/marketplace.json +@@ -8,7 +8,7 @@ + { + "name": "metareview", + "description": "Internal review harness, adversarial gates, and post-merge learning for coding agents", +- "version": "0.13.2", ++ "version": "0.13.3", + "source": "./", + "author": { + "name": "David Sifry" +diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json +index 413a42a..7c5707d 100644 +--- a/.claude-plugin/plugin.json ++++ b/.claude-plugin/plugin.json +@@ -1,6 +1,6 @@ + { + "name": "metareview", +- "version": "0.13.2", ++ "version": "0.13.3", + "description": "Go-based metaswarm-compatible internal review harness for plans, specs, decompositions, task-done code review, acceptance evidence, PR readiness, and post-merge learning. Packaged releases use bin/metareview; source checkout mode requires Go 1.26+, plus FSM workflow runs (metareview fsm).", + "author": { + "name": "David Sifry" +diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json +index eac09f0..369105b 100644 +--- a/.codex-plugin/plugin.json ++++ b/.codex-plugin/plugin.json +@@ -1,6 +1,6 @@ + { + "name": "metareview", +- "version": "0.13.2", ++ "version": "0.13.3", + "description": "Go-based metaswarm-compatible internal review harness for plans, specs, decompositions, task-done code review, acceptance evidence, PR readiness, and post-merge learning, plus FSM workflow runs (metareview fsm).", + "author": { + "name": "David Sifry" +diff --git a/CHANGELOG.md b/CHANGELOG.md +index 004e876..47655ce 100644 +--- a/CHANGELOG.md ++++ b/CHANGELOG.md +@@ -1,5 +1,28 @@ + # Changelog + ++## 0.13.3 - 2026-09-26 ++ ++### Security ++ ++- **The `claude-cli/` and `codex/` judges no longer run inside the repository under review.** Each ++ judge attempt now starts the model CLI in a fresh, empty, private temp directory, removed after ++ the call. Before, the CLI inherited metareview's working directory, which is normally the ++ repository being reviewed, and in headless mode a CLI can still load that repository's ++ project-level configuration without the interactive trust prompt. Disabling tools does not ++ switch that configuration off. So reviewing an untrusted checkout, such as a fork's PR branch, ++ was not isolated from it. The Claude judge also passes `--setting-sources user ++ --strict-mcp-config`. The Codex judge passes `-c project_doc_max_bytes=0`, which also covers the ++ escalation path's materialized evidence tree (still honoured as an explicit work dir). If no ++ private directory can be made, the attempt fails as a transport error and nothing is spawned in ++ the caller's directory. The directory is created under the user's own cache directory ++ (`os.UserCacheDir`), not a shared temp dir, because Codex walks up from its working directory to ++ a `.git` root and loads repo-scoped skills it finds there. There is no temp-dir fallback: without ++ a usable cache directory the judge attempt fails (the CLI judges need `$HOME` for their OAuth ++ session anyway). Judge calls also no longer persist sessions: Claude gets ++ `--no-session-persistence` and Codex `--ephemeral`, so a judge's full prompt and diff are no ++ longer written into the user's session transcripts. Reported while reviewing #165; plan in #167 ++ (W0). ++ + ## 0.13.2 - 2026-09-26 + + ### Fixed +diff --git a/internal/fsm/cli/deps.go b/internal/fsm/cli/deps.go +index 5bdc1ec..41ad14b 100644 +--- a/internal/fsm/cli/deps.go ++++ b/internal/fsm/cli/deps.go +@@ -134,9 +134,9 @@ func realClaudeExec(ctx context.Context, dir string, args []string, stdin string + // metareview never handles the token itself. + func realCodexExec(ctx context.Context, dir string, args []string, stdin string) ([]byte, int, error) { + cmd := exec.CommandContext(ctx, codexBin, args...) +- // Empty means inherit, which is metareview's own repository. A caller that has materialized +- // an evidence tree passes it here so that tree is what relative paths resolve against. This +- // does not stop the CLI reading elsewhere - cmd.Dir is a starting point, not a jail. ++ // The judge always passes a directory: a fresh isolated one (judge.isolatedDir), or a ++ // materialized evidence tree a caller confined it to. This does not stop the CLI reading ++ // elsewhere - cmd.Dir is a starting point, not a jail. + cmd.Dir = dir + cmd.Stdin = strings.NewReader(stdin) + var out bytes.Buffer +diff --git a/internal/fsm/judge/claude.go b/internal/fsm/judge/claude.go +index e20beaf..5627542 100644 +--- a/internal/fsm/judge/claude.go ++++ b/internal/fsm/judge/claude.go +@@ -79,6 +79,15 @@ func (j *claudeJudge) Call(ctx context.Context, r Request) (v Verdict, err error + // remains a denial rather than a hang in a headless run. + "--disallowed-tools", "*", + "--permission-mode", "dontAsk", ++ // Behind the isolated working directory (isolatedDir): load the user's settings only, ++ // never a project's or a local override, and no MCP server that is not passed explicitly ++ // (none is). --bare would also skip hooks but skips the keychain, and with it the OAuth ++ // session this transport exists to use. ++ "--setting-sources", "user", ++ "--strict-mcp-config", ++ // A judge call is not a session: nothing to resume, and with a fresh directory per ++ // attempt a saved transcript would leave one project directory behind per call. ++ "--no-session-persistence", + // The system prompt must always be passed. Without it `claude -p` can + // silently fall back to Haiku for the work turn even with --model set — + // the judge would then be a different model than the one recorded in the +@@ -105,9 +114,17 @@ func (j *claudeJudge) Call(ctx context.Context, r Request) (v Verdict, err error + case <-j.clock.After(backoff(classBackoff, attempt-1)): + } + } +- actx, cancel := context.WithTimeout(ctx, j.timeout()) +- stdout, code, execErr := j.exec(actx, "", args, user) +- cancel() ++ dir, cleanup, dirErr := isolatedDir() ++ if dirErr != nil { ++ lastErr = errs.E(CodeJudgeTransport, "claude could not be given an isolated working directory: "+dirErr.Error(), "provider", "claude-cli") ++ continue ++ } ++ stdout, code, execErr := func() ([]byte, int, error) { ++ defer cleanup() // deferred: a panic in the seam must not leave the directory behind ++ actx, cancel := context.WithTimeout(ctx, j.timeout()) ++ defer cancel() ++ return j.exec(actx, dir, args, user) ++ }() + + text, tokens, found, transient := parseClaudeResult(stdout) + v.Tokens = v.Tokens.Add(tokens) +diff --git a/internal/fsm/judge/codex.go b/internal/fsm/judge/codex.go +index a398d92..dd88fda 100644 +--- a/internal/fsm/judge/codex.go ++++ b/internal/fsm/judge/codex.go +@@ -38,7 +38,7 @@ type codexJudge struct { + exec CodexExec + nonce func() string + clock Clock +- workDir string // empty: inherit the caller's directory ++ workDir string // empty: a fresh isolated directory per attempt (isolatedDir) + attemptTimeout time.Duration // zero: the AttemptTimeout default + } + +@@ -66,11 +66,16 @@ func (j *codexJudge) Call(ctx context.Context, r Request) (v Verdict, err error) + + args := []string{ + "exec", "--json", ++ "--ephemeral", // a judge call is not a session: persist nothing (one fresh dir per attempt) + "--sandbox", "read-only", // a judge reads; it must never edit the tree it is judging + "--skip-git-repo-check", // judging is not tied to a repository + "--color", "never", + "-m", wireModel(r.Model), + "-c", "model_reasoning_effort=" + r.Effort, ++ // AGENTS.md is read from the working directory up and would be spliced into the judge's ++ // instructions. The default directory is isolated and empty, but an explicit work dir is a ++ // materialized copy of the change under review, which can carry one: project docs off. ++ "-c", "project_doc_max_bytes=0", + "-", // the prompt arrives on stdin, never as an argv the process table would show + } + // The same attempt ceiling, per-attempt deadline and backoff as the HTTP arm. +@@ -98,9 +103,20 @@ func (j *codexJudge) Call(ctx context.Context, r Request) (v Verdict, err error) + case <-j.clock.After(backoff(classBackoff, attempt-1)): + } + } +- actx, cancel := context.WithTimeout(ctx, j.timeout()) +- stdout, code, execErr := j.exec(actx, j.workDir, args, prompt) +- cancel() ++ dir, cleanup := j.workDir, func() {} ++ if dir == "" { ++ var dirErr error ++ if dir, cleanup, dirErr = isolatedDir(); dirErr != nil { ++ lastErr = errs.E(CodeJudgeTransport, "codex could not be given an isolated working directory: "+dirErr.Error(), "provider", "codex") ++ continue ++ } ++ } ++ stdout, code, execErr := func() ([]byte, int, error) { ++ defer cleanup() // deferred: a panic in the seam must not leave the directory behind ++ actx, cancel := context.WithTimeout(ctx, j.timeout()) ++ defer cancel() ++ return j.exec(actx, dir, args, prompt) ++ }() + + text, tokens, found := parseCodexEvents(stdout) + v.Tokens = v.Tokens.Add(tokens) +diff --git a/internal/fsm/judge/codex_test.go b/internal/fsm/judge/codex_test.go +index c99dd41..1396601 100644 +--- a/internal/fsm/judge/codex_test.go ++++ b/internal/fsm/judge/codex_test.go +@@ -440,8 +440,10 @@ func TestCodexJudgeRunsInItsConfiguredWorkDir(t *testing.T) { + } + } + +-// Unset means "inherit", which is today's behaviour: the seam must not invent a directory. +-func TestCodexJudgeWithNoWorkDirPassesEmpty(t *testing.T) { ++// Unset no longer means "inherit": the caller's directory is the repository under review, whose ++// AGENTS.md a headless codex would splice into the judge's instructions. Unset means a fresh, ++// empty, isolated directory (see isolation_test.go for the full contract). ++func TestCodexJudgeWithNoWorkDirIsIsolated(t *testing.T) { + var gotDir = "unset" + f := func(_ context.Context, dir string, _ []string, _ string) ([]byte, int, error) { + gotDir = dir +@@ -451,8 +453,8 @@ func TestCodexJudgeWithNoWorkDirPassesEmpty(t *testing.T) { + if _, err := j.Call(context.Background(), codexRequest()); err != nil { + t.Fatalf("call: %v", err) + } +- if gotDir != "" { +- t.Errorf("exec dir = %q, want empty when unconfigured", gotDir) ++ if gotDir == "" || gotDir == "unset" || !strings.Contains(gotDir, "metareview-judge-") { ++ t.Errorf("exec dir = %q, want a fresh isolated directory when unconfigured", gotDir) + } + } + +@@ -481,8 +483,8 @@ func TestWithCodexWorkDirConfinesOnlyCodexJudges(t *testing.T) { + if _, err := base.Call(context.Background(), codexRequest()); err != nil { + t.Fatalf("base call: %v", err) + } +- if gotDir != "" { +- t.Errorf("confining one judge changed the original, which ran in %q", gotDir) ++ if gotDir == "/evidence" || !strings.Contains(gotDir, "metareview-judge-") { ++ t.Errorf("confining one judge changed the original, which ran in %q (want its own isolated dir)", gotDir) + } + + // a judge that is not the HTTP/codex router is returned as-is +diff --git a/internal/fsm/judge/isolate.go b/internal/fsm/judge/isolate.go +new file mode 100644 +index 0000000..4910a9d +--- /dev/null ++++ b/internal/fsm/judge/isolate.go +@@ -0,0 +1,39 @@ ++package judge ++ ++import ( ++ "os" ++ "path/filepath" ++) ++ ++// userCacheDir is os.UserCacheDir, a seam so the fallback paths are testable. ++var userCacheDir = os.UserCacheDir ++ ++// isolatedDir makes the private, empty working directory a judge CLI runs in, and returns its ++// cleanup. A headless CLI still loads the project configuration of the directory it starts in ++// (Claude Code's .claude/settings.json hooks and CLAUDE.md; Codex's AGENTS.md), and neither ++// disabling tools nor a read-only sandbox switches that off. Starting inside the repository under ++// review would let that repository run code, or rewrite the judge's instructions, before the ++// judge reads a byte of the prompt. The prompt arrives on stdin, so the CLI needs no files of the ++// repository at all. A var so the failure path is testable. ++// ++// The directory lives under the user's own cache directory and nowhere else: Codex walks up from ++// its working directory to a project root and loads the repo-scoped skills it finds on the way ++// (project_doc_max_bytes=0 does not switch those off), so under a shared temp dir another local ++// user could plant a .git root and skills in /tmp. There is deliberately no temp-dir fallback: ++// without a usable cache directory the attempt fails, and the CLI judges need $HOME for their ++// OAuth session anyway. ++var isolatedDir = func() (string, func(), error) { ++ cache, err := userCacheDir() ++ if err != nil { ++ return "", func() {}, err ++ } ++ base := filepath.Join(cache, "metareview", "judge") ++ if err := os.MkdirAll(base, 0o700); err != nil { ++ return "", func() {}, err ++ } ++ dir, err := os.MkdirTemp(base, "metareview-judge-") ++ if err != nil { ++ return "", func() {}, err ++ } ++ return dir, func() { _ = os.RemoveAll(dir) }, nil ++} +diff --git a/internal/fsm/judge/isolation_test.go b/internal/fsm/judge/isolation_test.go +new file mode 100644 +index 0000000..8427a40 +--- /dev/null ++++ b/internal/fsm/judge/isolation_test.go +@@ -0,0 +1,284 @@ ++package judge ++ ++import ( ++ "context" ++ "errors" ++ "os" ++ "path/filepath" ++ "strings" ++ "testing" ++ ++ "github.com/dsifry/metareview/internal/fsm/errs" ++) ++ ++// dirProbe is a CLI exec seam that records the working directory each attempt was given and what ++// that directory held at the moment the CLI would have started. ++type dirProbe struct { ++ dirs []string ++ contents [][]string ++ stdout string ++ code int ++} ++ ++func (p *dirProbe) exec(_ context.Context, dir string, _ []string, _ string) ([]byte, int, error) { ++ p.dirs = append(p.dirs, dir) ++ entries, err := os.ReadDir(dir) ++ names := []string{} ++ if err != nil { ++ names = append(names, "ERR: "+err.Error()) ++ } ++ for _, e := range entries { ++ names = append(names, e.Name()) ++ } ++ p.contents = append(p.contents, names) ++ return []byte(p.stdout), p.code, nil ++} ++ ++// The judge CLIs must never start inside the repository under review. A headless CLI still loads ++// that directory's project configuration (Claude Code's .claude/settings.json hooks and ++// CLAUDE.md, Codex's AGENTS.md), without the interactive trust prompt, and disabling tools does not ++// switch that off. So every attempt runs in a fresh, empty, private directory that is removed after. ++func assertIsolated(t *testing.T, p *dirProbe, wantAttempts int) { ++ t.Helper() ++ if len(p.dirs) != wantAttempts { ++ t.Fatalf("attempts: got %d want %d", len(p.dirs), wantAttempts) ++ } ++ cwd, err := os.Getwd() ++ if err != nil { ++ t.Fatal(err) ++ } ++ for i, dir := range p.dirs { ++ if dir == "" || !filepath.IsAbs(dir) { ++ t.Fatalf("attempt %d ran in %q: an empty dir inherits the caller's cwd (the reviewed repo)", i+1, dir) ++ } ++ if rel, err := filepath.Rel(cwd, dir); err == nil && !strings.HasPrefix(rel, "..") { ++ t.Fatalf("attempt %d ran inside the caller's tree: %s", i+1, dir) ++ } ++ if len(p.contents[i]) != 0 { ++ t.Fatalf("attempt %d's directory was not empty: %v", i+1, p.contents[i]) ++ } ++ if _, err := os.Stat(dir); !errors.Is(err, os.ErrNotExist) { ++ t.Fatalf("attempt %d's directory %s was not removed (stat: %v)", i+1, dir, err) ++ } ++ } ++} ++ ++func TestClaudeJudgeRunsIsolatedFromTheReviewedRepo(t *testing.T) { ++ p := &dirProbe{stdout: claudeJSON(`{"reasoning":"r","is_real":true,"confidence":0.9}`)} ++ f := &fakeClaude{stdout: p.stdout} ++ j := &claudeJudge{exec: func(ctx context.Context, dir string, args []string, stdin string) ([]byte, int, error) { ++ f.args = args ++ return p.exec(ctx, dir, args, stdin) ++ }, nonce: func() string { return "n0" }, clock: codexClock()} ++ if _, err := j.Call(context.Background(), claudeRequest()); err != nil { ++ t.Fatal(err) ++ } ++ assertIsolated(t, p, 1) ++ joined := strings.Join(f.args, " ") ++ // Defence in depth behind the empty directory: user settings only (never a project's or a ++ // local override), and no MCP server that is not passed explicitly (none is). ++ for _, want := range []string{"--setting-sources user", "--strict-mcp-config"} { ++ if !strings.Contains(joined, want) { ++ t.Fatalf("args %q missing %q", joined, want) ++ } ++ } ++} ++ ++// Every retry gets its own fresh directory, and none is left behind. ++func TestClaudeJudgeIsolatesEveryAttempt(t *testing.T) { ++ p := &dirProbe{code: 1} ++ j := &claudeJudge{exec: p.exec, nonce: func() string { return "n0" }, clock: codexClock()} ++ if _, err := j.Call(context.Background(), claudeRequest()); err == nil { ++ t.Fatal("a CLI that always exits 1 must fail the call") ++ } ++ assertIsolated(t, p, MaxAttempts) ++ seen := map[string]bool{} ++ for _, d := range p.dirs { ++ if seen[d] { ++ t.Fatalf("attempts shared a directory: %s", d) ++ } ++ seen[d] = true ++ } ++} ++ ++func TestCodexJudgeRunsIsolatedByDefault(t *testing.T) { ++ var args []string ++ p := &dirProbe{stdout: codexStream(`{"reasoning":"r","is_real":true,"confidence":0.9}`)} ++ j := &codexJudge{exec: func(ctx context.Context, dir string, a []string, stdin string) ([]byte, int, error) { ++ args = a ++ return p.exec(ctx, dir, a, stdin) ++ }, nonce: func() string { return "n0" }, clock: codexClock()} ++ if _, err := j.Call(context.Background(), codexRequest()); err != nil { ++ t.Fatal(err) ++ } ++ assertIsolated(t, p, 1) ++ // AGENTS.md is read from the working directory up, including from a materialized evidence tree, ++ // which can carry one from the change under review: project docs are off entirely. ++ if !strings.Contains(strings.Join(args, " "), "-c project_doc_max_bytes=0") { ++ t.Fatalf("args %q must disable project docs", args) ++ } ++} ++ ++// An explicit work dir (the escalation path's materialized evidence tree) is honoured as-is and ++// never deleted by the judge. ++func TestCodexJudgeHonoursAnExplicitWorkDir(t *testing.T) { ++ tree := t.TempDir() ++ if err := os.WriteFile(filepath.Join(tree, "a.go"), []byte("package a\n"), 0o644); err != nil { ++ t.Fatal(err) ++ } ++ p := &dirProbe{stdout: codexStream(`{"reasoning":"r","is_real":true,"confidence":0.9}`)} ++ j := &codexJudge{exec: p.exec, nonce: func() string { return "n0" }, clock: codexClock(), workDir: tree} ++ if _, err := j.Call(context.Background(), codexRequest()); err != nil { ++ t.Fatal(err) ++ } ++ if len(p.dirs) != 1 || p.dirs[0] != tree { ++ t.Fatalf("explicit work dir not used: %v", p.dirs) ++ } ++ if _, err := os.Stat(filepath.Join(tree, "a.go")); err != nil { ++ t.Fatalf("the judge must not remove a caller's tree: %v", err) ++ } ++} ++ ++// If no private directory can be made, nothing is spawned in the caller's directory as a fallback: ++// the call fails as a transport error, which the ladder retries and the run records. ++func TestJudgesRefuseToRunWithoutAnIsolatedDir(t *testing.T) { ++ saved := isolatedDir ++ t.Cleanup(func() { isolatedDir = saved }) ++ isolatedDir = func() (string, func(), error) { return "", func() {}, errors.New("disk full") } ++ ++ calls := 0 ++ spy := func(context.Context, string, []string, string) ([]byte, int, error) { ++ calls++ ++ return nil, 0, nil ++ } ++ cj := &claudeJudge{exec: spy, nonce: func() string { return "n0" }, clock: codexClock()} ++ _, cerr := cj.Call(context.Background(), claudeRequest()) ++ xj := &codexJudge{exec: spy, nonce: func() string { return "n0" }, clock: codexClock()} ++ _, xerr := xj.Call(context.Background(), codexRequest()) ++ for name, err := range map[string]error{"claude": cerr, "codex": xerr} { ++ var e *errs.Error ++ if !errors.As(err, &e) || e.Code != CodeJudgeTransport || !strings.Contains(e.Detail, "isolated") { ++ t.Fatalf("%s: want an isolated-dir transport error, got %v", name, err) ++ } ++ } ++ if calls != 0 { ++ t.Fatalf("a CLI was spawned without an isolated directory (%d calls)", calls) ++ } ++} ++ ++// The real directory maker: private (0700), empty, under the user's own cache directory (not a ++// shared temp dir, where another local user could plant a .git root and repo-scoped skills a CLI ++// walks up to), and removed by its cleanup. ++func TestIsolatedDir(t *testing.T) { ++ cache := t.TempDir() ++ saved := userCacheDir ++ t.Cleanup(func() { userCacheDir = saved }) ++ userCacheDir = func() (string, error) { return cache, nil } ++ ++ dir, cleanup, err := isolatedDir() ++ if err != nil { ++ t.Fatal(err) ++ } ++ info, err := os.Stat(dir) ++ if err != nil || !info.IsDir() || info.Mode().Perm() != 0o700 { ++ t.Fatalf("stat %s: %v %v", dir, info, err) ++ } ++ if want := filepath.Join(cache, "metareview", "judge") + string(filepath.Separator); !strings.HasPrefix(dir, want) { ++ t.Fatalf("dir %s is not under the user cache base %s", dir, want) ++ } ++ base, err := os.Stat(filepath.Join(cache, "metareview", "judge")) ++ if err != nil || base.Mode().Perm() != 0o700 { ++ t.Fatalf("the base must be private: %v %v", base, err) ++ } ++ cleanup() ++ if _, err := os.Stat(dir); !errors.Is(err, os.ErrNotExist) { ++ t.Fatalf("cleanup left %s", dir) ++ } ++} ++ ++// No usable user cache directory: refuse, never fall back to a shared temp dir. Under /tmp another ++// local user could plant a .git root and repo-scoped skills that Codex walks up to and loads ++// (project_doc_max_bytes=0 does not switch skills off). The CLI judges need $HOME for their OAuth ++// session anyway, so refusing costs nothing a working setup has. ++func TestIsolatedDirFailsClosedWithoutAUserCacheDir(t *testing.T) { ++ saved := userCacheDir ++ t.Cleanup(func() { userCacheDir = saved }) ++ blocked := filepath.Join(t.TempDir(), "file") ++ if err := os.WriteFile(blocked, nil, 0o600); err != nil { ++ t.Fatal(err) ++ } ++ readOnly := t.TempDir() ++ base := filepath.Join(readOnly, "metareview", "judge") ++ if err := os.MkdirAll(base, 0o700); err != nil { ++ t.Fatal(err) ++ } ++ if err := os.Chmod(base, 0o500); err != nil { ++ t.Fatal(err) ++ } ++ t.Cleanup(func() { _ = os.Chmod(base, 0o700) }) ++ for name, cacheDir := range map[string]func() (string, error){ ++ "no cache dir": func() (string, error) { return "", errors.New("$HOME is not defined") }, ++ "unusable cache": func() (string, error) { return blocked, nil }, // a file: MkdirAll fails ++ "unwritable base": func() (string, error) { return readOnly, nil }, // MkdirTemp fails ++ } { ++ if name == "unwritable base" && os.Geteuid() == 0 { ++ continue // root ignores the mode ++ } ++ userCacheDir = cacheDir ++ dir, cleanup, err := isolatedDir() ++ if err == nil || dir != "" { ++ t.Fatalf("%s: want an error and no dir, got %q %v", name, dir, err) ++ } ++ cleanup() // a no-op, but always safe to call ++ } ++} ++ ++// A panic inside the CLI seam still removes the attempt's directory. ++func TestJudgesRemoveTheDirEvenOnPanic(t *testing.T) { ++ for name, call := range map[string]func(ClaudeExec){ ++ "claude": func(e ClaudeExec) { ++ _, _ = (&claudeJudge{exec: e, nonce: func() string { return "n0" }, clock: codexClock()}).Call(context.Background(), claudeRequest()) ++ }, ++ "codex": func(e ClaudeExec) { ++ _, _ = (&codexJudge{exec: CodexExec(e), nonce: func() string { return "n0" }, clock: codexClock()}).Call(context.Background(), codexRequest()) ++ }, ++ } { ++ var dir string ++ func() { ++ defer func() { _ = recover() }() ++ call(func(_ context.Context, d string, _ []string, _ string) ([]byte, int, error) { ++ dir = d ++ panic("boom") ++ }) ++ }() ++ if dir == "" { ++ t.Fatalf("%s: the seam was never called", name) ++ } ++ if _, err := os.Stat(dir); !errors.Is(err, os.ErrNotExist) { ++ t.Fatalf("%s: a panic left %s behind", name, dir) ++ } ++ } ++} ++ ++// Judge calls are not sessions: nothing is persisted (and with a fresh directory per attempt, ++// persisting would leave one transcript directory behind per call). ++func TestJudgesPersistNoSession(t *testing.T) { ++ f := &fakeClaude{stdout: claudeJSON(`{"reasoning":"r","is_real":true,"confidence":0.9}`)} ++ if _, err := (&claudeJudge{exec: f.exec, nonce: func() string { return "n0" }, clock: codexClock()}).Call(context.Background(), claudeRequest()); err != nil { ++ t.Fatal(err) ++ } ++ if !strings.Contains(strings.Join(f.args, " "), "--no-session-persistence") { ++ t.Fatalf("claude args %q", f.args) ++ } ++ var args []string ++ x := &codexJudge{exec: func(_ context.Context, _ string, a []string, _ string) ([]byte, int, error) { ++ args = a ++ return []byte(codexStream(`{"reasoning":"r","is_real":true,"confidence":0.9}`)), 0, nil ++ }, nonce: func() string { return "n0" }, clock: codexClock()} ++ if _, err := x.Call(context.Background(), codexRequest()); err != nil { ++ t.Fatal(err) ++ } ++ if !strings.Contains(strings.Join(args, " "), "--ephemeral") { ++ t.Fatalf("codex args %q", args) ++ } ++} +diff --git a/internal/version/version.go b/internal/version/version.go +index 79c28ad..e768901 100644 +--- a/internal/version/version.go ++++ b/internal/version/version.go +@@ -1,3 +1,3 @@ + package version + +-const Version = "0.13.2" ++const Version = "0.13.3" +diff --git a/package.json b/package.json +index 9b34346..7ba81ec 100644 +--- a/package.json ++++ b/package.json +@@ -1,6 +1,6 @@ + { + "name": "metareview", +- "version": "0.13.2", ++ "version": "0.13.3", + "description": "Go-based metaswarm-compatible internal review harness for plans, specs, decompositions, code, acceptance evidence, PR readiness, and post-merge learning", + "bin": { + "metareview": "cli/metareview.js" + + + +``` + +## Review Logs + +- --help: PASS (docs/metareview/reviews/mrv-20260822-220108922308000-task-done-help-9a8265a5.md) +- CHANGELOG.md: PASS (docs/metareview/reviews/mrv-20260908-073527271951000-artifact-changelog-ab09011f.md) +- cmd/metareview/reviewhelp_test.go: PASS_ADVISORY (docs/metareview/reviews/mrv-20260924-062613603963000-task-done-reviewhelp-test-b88768c5.md) +- docs-0.6.0-documentation: PASS (docs/metareview/reviews/mrv-20260705-161047045358000-task-done-docs-0-6-a2ac175a.md) +- docs-fsm-result-contract: PASS (docs/metareview/reviews/mrv-20260705-215610270004000-task-done-docs-fsm-result-contract-cbf40d94.md) +- docs/metareview/evidence/t0.1-finding-identity-rewire.md: PASS (docs/metareview/reviews/mrv-20260901-042205337128000-task-done-t0-1-finding-identity-rewire-01ca0bf9.md) +- docs/metareview/evidence/t1.3-pins-proven-gate-pr-a.md: PASS (docs/metareview/reviews/mrv-20260901-051912009394000-task-done-t1-3-pins-proven-gate-pr-a-51440dbf.md) +- docs/quickstart.md: PASS (docs/metareview/reviews/mrv-20260528-222809220757000-task-done-quickstart-f3ea78a9.md) +- docs/specs/2026-05-27-artifact-review-parallel-subagents.md: PASS (docs/metareview/reviews/mrv-20260527-193044946912000-task-done-2026-05-27-artifact-review-parallel-subagents-cf4f2168.md) +- docs/specs/2026-05-27-fail-closed-artifact-review-gates.md: PASS (docs/metareview/reviews/mrv-20260527-182255753628000-task-done-2026-05-27-fail-closed-artifact-review-gates-5243a194.md) +- docs/superpowers/plans/2026-07-05-issue-2-wu4-review-manifest-shard-aggregation.md: PASS (docs/metareview/reviews/mrv-20260705-055835877369000-task-done-2026-07-05-issue-2-wu4-review-manifest-shard-agg-b924a5ff.md) +- docs/superpowers/plans/2026-09-23-mutation-incremental-1c-proof.md: PASS_ADVISORY (docs/metareview/reviews/mrv-20260924-034929320260000-task-done-2026-09-23-mutation-incremental-1c-proof-380f704d.md) +- docs/superpowers/plans/2026-09-23-mutation-incremental-2a-gate.md: PASS_ADVISORY (docs/metareview/reviews/mrv-20260924-045211619641000-task-done-2026-09-23-mutation-incremental-2a-gate-ed03c179.md) +- docs/superpowers/plans/2026-09-23-mutation-incremental-2b-views.md: PASS_ADVISORY (docs/metareview/reviews/mrv-20260924-051035853973000-task-done-2026-09-23-mutation-incremental-2b-views-6f8a9d6c.md) +- docs/tasks/m0-fsm-run-persistence.md: PASS (docs/metareview/reviews/mrv-20260827-130422417348000-task-done-m0-fsm-run-persistence-8a9eddc9.md) +- docs/tasks/m1-m6-fsm-packages.md: PASS (docs/metareview/reviews/mrv-20260827-090548364954000-task-done-m1-m6-fsm-packages-a99c72f1.md) +- docs/tasks/m7-fork-record-export.md: PASS (docs/metareview/reviews/mrv-20260827-104833919009000-task-done-m7-fork-record-export-9dc62119.md) +- docs/tasks/m8-cli-suite-docs.md: PASS (docs/metareview/reviews/mrv-20260827-114104823011000-task-done-m8-cli-suite-docs-46a961bc.md) +- docs/tasks/pr-a-sharded-measurement.md: PASS (docs/metareview/reviews/mrv-20260827-164005027099000-task-done-pr-a-sharded-measurement-8215c1f2.md) +- docs/tasks/pr-b-shard-ingestion.md: PASS (docs/metareview/reviews/mrv-20260827-181704826892000-task-done-pr-b-shard-ingestion-f6a50afe.md) +- docs/tasks/pr-b-slice-3.md: PASS (docs/metareview/reviews/mrv-20260827-181553019412000-task-done-pr-b-slice-3-e224e5b6.md) +- docs/tasks/pr-b-slices-1-2.md: PASS (docs/metareview/reviews/mrv-20260827-181426808610000-task-done-pr-b-slices-1-2-ce580d87.md) +- docs/tasks/process-overrides.md: PASS (docs/metareview/reviews/mrv-20260827-180059866454000-task-done-process-overrides-93d185e3.md) +- fix-node-elicits-proofs: PASS (docs/metareview/reviews/mrv-20260901-183938772835000-task-done-fix-node-elicits-proofs-e988c555.md) +- fix-pin-id-finding-consistency: PASS (docs/metareview/reviews/mrv-20260901-191438733281000-task-done-fix-pin-id-finding-consistency-b6a37c98.md) +- fix-proof-binds-fix-diff: PASS (docs/metareview/reviews/mrv-20260901-204852362514000-task-done-fix-proof-binds-fix-diff-3ad76b46.md) +- github-reviewdecision-prready-fix: PASS (docs/metareview/reviews/mrv-20260705-221742306514000-task-done-github-reviewdecision-prready-fix-3de6a2de.md) +- internal/fsm/gate/gates.go: PASS (docs/metareview/reviews/mrv-20260902-040527894922000-task-done-gates-bca330f8.md) +- internal/fsm/judge/judge.go: PASS (docs/metareview/reviews/mrv-20260902-045920838914000-task-done-judge-b7060768.md) +- internal/fsm/kind/kind.go: PASS_ADVISORY (docs/metareview/reviews/mrv-20260910-153756405005000-task-done-kind-b7bb121c.md) +- internal/gitcontext/gitcontext.go: PASS (docs/metareview/reviews/mrv-20260902-060726814084000-task-done-gitcontext-9e14bb40.md) +- internal/githubcontext/githubcontext.go: PASS (docs/metareview/reviews/mrv-20260902-034259118558000-task-done-githubcontext-37fa1398.md) +- internal/prready/review.go: PASS (docs/metareview/reviews/mrv-20260902-040416607878000-task-done-review-c809e786.md) +- internal/reviewers/epicready.go: PASS (docs/metareview/reviews/mrv-20260902-035232185408000-task-done-epicready-27fabbd2.md) +- internal/testconv/typescript.go: PASS (docs/metareview/reviews/mrv-20260902-051859934731000-task-done-typescript-4dea666f.md) +- issue-2-wu1: PASS (docs/metareview/reviews/mrv-20260705-012852131814000-task-done-issue-2-wu1-a2da3dbe.md) +- issue-2-wu1-version-0.6.0: PASS (docs/metareview/reviews/mrv-20260705-030845991374000-task-done-issue-2-wu1-version-0-6-69ccbe27.md) +- issue-2-wu2: PASS (docs/metareview/reviews/mrv-20260705-034317422615000-task-done-issue-2-wu2-eb5dad78.md) +- issue-2-wu2-cursor-followup-fix: PASS (docs/metareview/reviews/mrv-20260705-040500203996000-task-done-issue-2-wu2-cursor-followup-fix-e87ecaa9.md) +- issue-2-wu2-prready-filter-fix: PASS (docs/metareview/reviews/mrv-20260705-035954884586000-task-done-issue-2-wu2-prready-filter-fix-d4005ab8.md) +- issue-2-wu2-review-fix: PASS (docs/metareview/reviews/mrv-20260705-035540088635000-task-done-issue-2-wu2-review-fix-05b5e486.md) +- issue-2-wu3: PASS (docs/metareview/reviews/mrv-20260705-050759694908000-task-done-issue-2-wu3-2688b027.md) +- mechanical-precision-lens: PASS (docs/metareview/reviews/mrv-20260831-183207933768000-task-done-mechanical-precision-lens-c79c1389.md) +- multilang-pytest-conv: PASS (docs/metareview/reviews/mrv-20260901-190331877901000-task-done-multilang-pytest-conv-1f2195e7.md) +- multilang-testconv-seam: PASS (docs/metareview/reviews/mrv-20260901-165623149954000-task-done-multilang-testconv-seam-01cc5b3d.md) +- multilang-typescript-conv: PASS (docs/metareview/reviews/mrv-20260901-172216437441000-task-done-multilang-typescript-conv-6cdb50c2.md) +- multilang-vitest-conv: PASS (docs/metareview/reviews/mrv-20260901-174949441189000-task-done-multilang-vitest-conv-56254690.md) +- runchain-headsha-escalation-reset: PASS (docs/metareview/reviews/mrv-20260705-222055675782000-task-done-runchain-headsha-escalation-reset-37b22cb1.md) +- runchain-reset-findings-reconciliation: PASS (docs/metareview/reviews/mrv-20260705-223346154492000-task-done-runchain-reset-findings-reconciliation-6cafdbd1.md) +- t1.1-differentialproof-model: PASS (docs/metareview/reviews/mrv-20260901-001859561454000-task-done-t1-aa37529c.md) +- t1.2-agentedit-reduce-seam: PASS (docs/metareview/reviews/mrv-20260901-002517703556000-task-done-t1-b5a41665.md) +- t1.3-reproduction-engine-pr-b: PASS (docs/metareview/reviews/mrv-20260901-080559565942000-task-done-t1-8af2654f.md) +- t1.4-right-reason-reviewer: PASS (docs/metareview/reviews/mrv-20260901-100944138141000-task-done-t1-ab64ea55.md) +- t1.5-deletion-provable-fix: PASS (docs/metareview/reviews/mrv-20260901-105248210036000-task-done-t1-4ad043b2.md) +- t1.6-trivial-pin-prescreen: PASS (docs/metareview/reviews/mrv-20260901-115747009329000-task-done-t1-b353673e.md) +- t2.1-test-deletion-gate: PASS (docs/metareview/reviews/mrv-20260901-145243731066000-task-done-t2-0fe76ae4.md) +- t2.2-trajectory-monitor: PASS (docs/metareview/reviews/mrv-20260901-154004114739000-task-done-t2-d4fcf27a.md) +- workflows/sdlc-loop-clean.yaml: PASS (docs/metareview/reviews/mrv-20260902-082859947245000-task-done-sdlc-loop-clean-510ba162.md) + +## Knowledge And Registries + +Service inventory: none + +No service inventory found. + +Knowledge facts: + +No Beads knowledge facts found. + +## Validation Evidence + +{"schemaVersion":1,"kind":"validation","command":["go","test","./..."],"cwd":".","exitCode":0,"startedAt":"2026-09-27T02:54:58.107085Z","finishedAt":"2026-09-27T02:55:17.598128Z","stdoutSha256":"cadfe12c3b58350e99202c5c66d06d8b27f9fb298edf0ecb05c9a816d5c4bbb6","stderrSha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","summary":"go test ./... exited 0"} + +## GitHub Context + +GitHub context unavailable: pr-number-unavailable + + +## Suggested PR Evidence + +## metareview PR Evidence + +### Summary + +fix/judge-cli-isolation changes .claude-plugin/marketplace.json, .claude-plugin/plugin.json, .codex-plugin/plugin.json, CHANGELOG.md, internal/fsm/cli/deps.go, internal/fsm/judge/claude.go, internal/fsm/judge/codex.go, internal/fsm/judge/codex_test.go, internal/fsm/judge/isolate.go, internal/fsm/judge/isolation_test.go, internal/version/version.go, package.json + +### Validation + +- structured validation: go test ./... exited 0 (exit 0) + +### Task Review Evidence + +- --help: PASS (docs/metareview/reviews/mrv-20260822-220108922308000-task-done-help-9a8265a5.md) +- CHANGELOG.md: PASS (docs/metareview/reviews/mrv-20260908-073527271951000-artifact-changelog-ab09011f.md) +- cmd/metareview/reviewhelp_test.go: PASS_ADVISORY (docs/metareview/reviews/mrv-20260924-062613603963000-ta[REDACTED].md) findings: mrvf-20260924-062613603963000-ta[REDACTED] +- docs-0.6.0-documentation: PASS (docs/metareview/reviews/mrv-20260705-161047045358000-ta[REDACTED].md) +- docs-fsm-result-contract: PASS (docs/metareview/reviews/mrv-20260705-215610270004000-ta[REDACTED].md) +- docs/metareview/evidence/t0.1-finding-identity-rewire.md: PASS (docs/metareview/reviews/mrv-20260901-042205337128000-ta[REDACTED].md) +- docs/metareview/evidence/t1.3-pins-proven-gate-pr-a.md: PASS (docs/metareview/reviews/mrv-20260901-051912009394000-ta[REDACTED].md) +- docs/quickstart.md: PASS (docs/metareview/reviews/mrv-20260528-222809220757000-ta[REDACTED].md) +- docs/specs/2026-05-27-artifact-review-parallel-subagents.md: PASS (docs/metareview/reviews/mrv-20260527-193044946912000-ta[REDACTED].md) +- docs/specs/2026-05-27-fail-closed-artifact-review-gates.md: PASS (docs/metareview/reviews/mrv-20260527-182255753628000-ta[REDACTED].md) +- docs/superpowers/plans/2026-07-05-issue-2-wu4-review-manifest-shard-aggregation.md: PASS (docs/metareview/reviews/mrv-20260705-055835877369000-ta[REDACTED].md) +- docs/superpowers/plans/2026-09-23-mutation-incremental-1c-proof.md: PASS_ADVISORY (docs/metareview/reviews/mrv-20260924-034929320260000-ta[REDACTED].md) findings: mrvf-20260924-034929320260000-ta[REDACTED], mrvf-20260924-034929320260000-ta[REDACTED], mrvf-20260924-034929320260000-ta[REDACTED] +- docs/superpowers/plans/2026-09-23-mutation-incremental-2a-gate.md: PASS_ADVISORY (docs/metareview/reviews/mrv-20260924-045211619641000-ta[REDACTED].md) findings: mrvf-20260924-045211619641000-ta[REDACTED], mrvf-20260924-045211619641000-ta[REDACTED], mrvf-20260924-045211619641000-ta[REDACTED] +- docs/superpowers/plans/2026-09-23-mutation-incremental-2b-views.md: PASS_ADVISORY (docs/metareview/reviews/mrv-20260924-051035853973000-ta[REDACTED].md) findings: mrvf-20260924-051035853973000-ta[REDACTED] +- docs/tasks/m0-fsm-run-persistence.md: PASS (docs/metareview/reviews/mrv-20260827-130422417348000-ta[REDACTED].md) +- docs/tasks/m1-m6-fsm-packages.md: PASS (docs/metareview/reviews/mrv-20260827-090548364954000-ta[REDACTED].md) +- docs/tasks/m7-fork-record-export.md: PASS (docs/metareview/reviews/mrv-20260827-104833919009000-ta[REDACTED].md) +- docs/tasks/m8-cli-suite-docs.md: PASS (docs/metareview/reviews/mrv-20260827-114104823011000-ta[REDACTED].md) +- docs/tasks/pr-a-sharded-measurement.md: PASS (docs/metareview/reviews/mrv-20260827-164005027099000-ta[REDACTED].md) +- docs/tasks/pr-b-shard-ingestion.md: PASS (docs/metareview/reviews/mrv-20260827-181704826892000-ta[REDACTED].md) +- docs/tasks/pr-b-slice-3.md: PASS (docs/metareview/reviews/mrv-20260827-181553019412000-ta[REDACTED].md) +- docs/tasks/pr-b-slices-1-2.md: PASS (docs/metareview/reviews/mrv-20260827-181426808610000-ta[REDACTED].md) +- docs/tasks/process-overrides.md: PASS (docs/metareview/reviews/mrv-20260827-180059866454000-ta[REDACTED].md) +- fix-node-elicits-proofs: PASS (docs/metareview/reviews/mrv-20260901-183938772835000-ta[REDACTED].md) +- fix-pin-id-finding-consistency: PASS (docs/metareview/reviews/mrv-20260901-191438733281000-ta[REDACTED].md) +- fix-proof-binds-fix-diff: PASS (docs/metareview/reviews/mrv-20260901-204852362514000-ta[REDACTED].md) +- github-reviewdecision-prready-fix: PASS (docs/metareview/reviews/mrv-20260705-221742306514000-ta[REDACTED].md) +- internal/fsm/gate/gates.go: PASS (docs/metareview/reviews/mrv-20260902-040527894922000-task-done-gates-bca330f8.md) +- internal/fsm/judge/judge.go: PASS (docs/metareview/reviews/mrv-20260902-045920838914000-task-done-judge-b7060768.md) +- internal/fsm/kind/kind.go: PASS_ADVISORY (docs/metareview/reviews/mrv-20260910-153756405005000-task-done-kind-b7bb121c.md) findings: mrvf-20260910-151423657039000-ta[REDACTED], mrvf-20260910-151423657039000-ta[REDACTED], mrvf-20260910-151538496642000-ta[REDACTED] +- internal/gitcontext/gitcontext.go: PASS (docs/metareview/reviews/mrv-20260902-060726814084000-ta[REDACTED].md) +- internal/githubcontext/githubcontext.go: PASS (docs/metareview/reviews/mrv-20260902-034259118558000-ta[REDACTED].md) +- internal/prready/review.go: PASS (docs/metareview/reviews/mrv-20260902-040416607878000-ta[REDACTED].md) +- internal/testconv/typescript.go: PASS (docs/metareview/reviews/mrv-20260902-051859934731000-ta[REDACTED].md) +- issue-2-wu1: PASS (docs/metareview/reviews/mrv-20260705-012852131814000-ta[REDACTED].md) +- issue-2-wu1-version-0.6.0: PASS (docs/metareview/reviews/mrv-20260705-030845991374000-ta[REDACTED].md) +- issue-2-wu2: PASS (docs/metareview/reviews/mrv-20260705-034317422615000-ta[REDACTED].md) +- issue-2-wu2-cursor-followup-fix: PASS (docs/metareview/reviews/mrv-20260705-040500203996000-ta[REDACTED].md) +- issue-2-wu2-prready-filter-fix: PASS (docs/metareview/reviews/mrv-20260705-035954884586000-ta[REDACTED].md) +- issue-2-wu2-review-fix: PASS (docs/metareview/reviews/mrv-20260705-035540088635000-ta[REDACTED].md) +- issue-2-wu3: PASS (docs/metareview/reviews/mrv-20260705-050759694908000-ta[REDACTED].md) +- mechanical-precision-lens: PASS (docs/metareview/reviews/mrv-20260831-183207933768000-ta[REDACTED].md) +- multilang-pytest-conv: PASS (docs/metareview/reviews/mrv-20260901-190331877901000-ta[REDACTED].md) +- multilang-testconv-seam: PASS (docs/metareview/reviews/mrv-20260901-165623149954000-ta[REDACTED].md) +- multilang-typescript-conv: PASS (docs/metareview/reviews/mrv-20260901-172216437441000-ta[REDACTED].md) +- multilang-vitest-conv: PASS (docs/metareview/reviews/mrv-20260901-174949441189000-ta[REDACTED].md) +- runchain-headsha-escalation-reset: PASS (docs/metareview/reviews/mrv-20260705-222055675782000-ta[REDACTED].md) +- runchain-reset-findings-reconciliation: PASS (docs/metareview/reviews/mrv-20260705-223346154492000-ta[REDACTED].md) +- t1.1-differentialproof-model: PASS (docs/metareview/reviews/mrv-20260901-001859561454000-task-done-t1-aa37529c.md) +- t1.2-agentedit-reduce-seam: PASS (docs/metareview/reviews/mrv-20260901-002517703556000-task-done-t1-b5a41665.md) +- t1.3-reproduction-engine-pr-b: PASS (docs/metareview/reviews/mrv-20260901-080559565942000-task-done-t1-8af2654f.md) +- t1.4-right-reason-reviewer: PASS (docs/metareview/reviews/mrv-20260901-100944138141000-task-done-t1-ab64ea55.md) +- t1.5-deletion-provable-fix: PASS (docs/metareview/reviews/mrv-20260901-105248210036000-task-done-t1-4ad043b2.md) +- t1.6-trivial-pin-prescreen: PASS (docs/metareview/reviews/mrv-20260901-115747009329000-task-done-t1-b353673e.md) +- t2.1-test-deletion-gate: PASS (docs/metareview/reviews/mrv-20260901-145243731066000-task-done-t2-0fe76ae4.md) +- t2.2-trajectory-monitor: PASS (docs/metareview/reviews/mrv-20260901-154004114739000-task-done-t2-d4fcf27a.md) +- workflows/sdlc-loop-clean.yaml: PASS (docs/metareview/reviews/mrv-20260902-082859947245000-ta[REDACTED].md) + +### Epic Review Evidence + +- internal/reviewers/epicready.go: PASS (docs/metareview/reviews/mrv-20260902-035232185408000-ta[REDACTED].md) + +### Recorded-Evidence Blocker Status + +_Blockers carried forward from recorded review evidence; findings this gate run raised are under `## Blocking Findings` above._ + +No unresolved blockers in the recorded review evidence. + +### External GitHub Review Context + +GitHub context unavailable: pr-number-unavailable + diff --git a/docs/metareview/reviews/mrv-20260927-025518024767000-pr-ready-branch-10d735e5.md b/docs/metareview/reviews/mrv-20260927-025518024767000-pr-ready-branch-10d735e5.md new file mode 100644 index 00000000..c43ac4fd --- /dev/null +++ b/docs/metareview/reviews/mrv-20260927-025518024767000-pr-ready-branch-10d735e5.md @@ -0,0 +1,149 @@ +# metareview: pr-ready review + +Run ID: `mrv-20260927-025518024767000-pr-ready-branch-10d735e5` + +Target: `current branch` + +Context pack: `docs/metareview/context/mrv-20260927-025518024767000-pr-ready-branch-10d735e5-context.md` + +Execution mode: `deterministic-local` + +Gate effect: `gate` + +Previous run: `mrv-20260927-023857249886000-pr-ready-branch-10d735e5` + +Attempt: `3/3` + +Reviewer input digest: `sha256:3b9fd3f762e4de0ad49adec1d044fd8c10364a0bd2a773e60a69ca536725b119` + +Covered paths: `[".claude-plugin/marketplace.json",".claude-plugin/plugin.json",".codex-plugin/plugin.json","CHANGELOG.md","internal/fsm/cli/deps.go","internal/fsm/judge/claude.go","internal/fsm/judge/codex.go","internal/fsm/judge/codex_test.go","internal/fsm/judge/isolate.go","internal/fsm/judge/isolation_test.go","internal/version/version.go","package.json"]` + +## Verdict + +PASS_ADVISORY + +## Reviewer Results + +| Reviewer | Verdict | Blocking | Notes | +| --- | --- | ---: | --- | +| pr-readiness-reviewer | PASS | 0 | No blocking findings. | +| validation-reviewer | PASS | 0 | No blocking findings. | +| security-reviewer | PASS | 0 | No blocking findings. | +| code-quality-reviewer | PASS | 0 | No blocking findings. | +| architecture-reviewer | PASS | 0 | No blocking findings. | +| external-reviewer | PASS | 0 | No blocking findings. | + +## Blocking Findings + +No findings in this class. + + +## Advisory Findings + +### mrvf-20260927-021621749506000-pr-ready-branch-10d735e5-001: Adversarial review was in-session-emulated + +- Reviewer: adversarial-review-reviewer +- Severity: low +- Classification: advisory +- Finding: The adjudicated review for this head was recorded as in-session-emulated (no independent subagents), which is weaker, non-independent evidence. +- Expected: An independent subagent-adjudicated review where delegation is available. +- Found: executionMode = in-session-emulated. +- Recommendation: When subagents are available, prefer `metareview fsm --workflow review-loop` for an independent review. + + +## Follow-up Findings + +No findings in this class. + + +## Warnings + +No findings in this class. + + +## Suggested PR Evidence + +## metareview PR Evidence + +### Summary + +fix/judge-cli-isolation changes .claude-plugin/marketplace.json, .claude-plugin/plugin.json, .codex-plugin/plugin.json, CHANGELOG.md, internal/fsm/cli/deps.go, internal/fsm/judge/claude.go, internal/fsm/judge/codex.go, internal/fsm/judge/codex_test.go, internal/fsm/judge/isolate.go, internal/fsm/judge/isolation_test.go, internal/version/version.go, package.json + +### Validation + +- structured validation: go test ./... exited 0 (exit 0) + +### Task Review Evidence + +- --help: PASS (docs/metareview/reviews/mrv-20260822-220108922308000-task-done-help-9a8265a5.md) +- CHANGELOG.md: PASS (docs/metareview/reviews/mrv-20260908-073527271951000-artifact-changelog-ab09011f.md) +- cmd/metareview/reviewhelp_test.go: PASS_ADVISORY (docs/metareview/reviews/mrv-20260924-062613603963000-ta[REDACTED].md) findings: mrvf-20260924-062613603963000-ta[REDACTED] +- docs-0.6.0-documentation: PASS (docs/metareview/reviews/mrv-20260705-161047045358000-ta[REDACTED].md) +- docs-fsm-result-contract: PASS (docs/metareview/reviews/mrv-20260705-215610270004000-ta[REDACTED].md) +- docs/metareview/evidence/t0.1-finding-identity-rewire.md: PASS (docs/metareview/reviews/mrv-20260901-042205337128000-ta[REDACTED].md) +- docs/metareview/evidence/t1.3-pins-proven-gate-pr-a.md: PASS (docs/metareview/reviews/mrv-20260901-051912009394000-ta[REDACTED].md) +- docs/quickstart.md: PASS (docs/metareview/reviews/mrv-20260528-222809220757000-ta[REDACTED].md) +- docs/specs/2026-05-27-artifact-review-parallel-subagents.md: PASS (docs/metareview/reviews/mrv-20260527-193044946912000-ta[REDACTED].md) +- docs/specs/2026-05-27-fail-closed-artifact-review-gates.md: PASS (docs/metareview/reviews/mrv-20260527-182255753628000-ta[REDACTED].md) +- docs/superpowers/plans/2026-07-05-issue-2-wu4-review-manifest-shard-aggregation.md: PASS (docs/metareview/reviews/mrv-20260705-055835877369000-ta[REDACTED].md) +- docs/superpowers/plans/2026-09-23-mutation-incremental-1c-proof.md: PASS_ADVISORY (docs/metareview/reviews/mrv-20260924-034929320260000-ta[REDACTED].md) findings: mrvf-20260924-034929320260000-ta[REDACTED], mrvf-20260924-034929320260000-ta[REDACTED], mrvf-20260924-034929320260000-ta[REDACTED] +- docs/superpowers/plans/2026-09-23-mutation-incremental-2a-gate.md: PASS_ADVISORY (docs/metareview/reviews/mrv-20260924-045211619641000-ta[REDACTED].md) findings: mrvf-20260924-045211619641000-ta[REDACTED], mrvf-20260924-045211619641000-ta[REDACTED], mrvf-20260924-045211619641000-ta[REDACTED] +- docs/superpowers/plans/2026-09-23-mutation-incremental-2b-views.md: PASS_ADVISORY (docs/metareview/reviews/mrv-20260924-051035853973000-ta[REDACTED].md) findings: mrvf-20260924-051035853973000-ta[REDACTED] +- docs/tasks/m0-fsm-run-persistence.md: PASS (docs/metareview/reviews/mrv-20260827-130422417348000-ta[REDACTED].md) +- docs/tasks/m1-m6-fsm-packages.md: PASS (docs/metareview/reviews/mrv-20260827-090548364954000-ta[REDACTED].md) +- docs/tasks/m7-fork-record-export.md: PASS (docs/metareview/reviews/mrv-20260827-104833919009000-ta[REDACTED].md) +- docs/tasks/m8-cli-suite-docs.md: PASS (docs/metareview/reviews/mrv-20260827-114104823011000-ta[REDACTED].md) +- docs/tasks/pr-a-sharded-measurement.md: PASS (docs/metareview/reviews/mrv-20260827-164005027099000-ta[REDACTED].md) +- docs/tasks/pr-b-shard-ingestion.md: PASS (docs/metareview/reviews/mrv-20260827-181704826892000-ta[REDACTED].md) +- docs/tasks/pr-b-slice-3.md: PASS (docs/metareview/reviews/mrv-20260827-181553019412000-ta[REDACTED].md) +- docs/tasks/pr-b-slices-1-2.md: PASS (docs/metareview/reviews/mrv-20260827-181426808610000-ta[REDACTED].md) +- docs/tasks/process-overrides.md: PASS (docs/metareview/reviews/mrv-20260827-180059866454000-ta[REDACTED].md) +- fix-node-elicits-proofs: PASS (docs/metareview/reviews/mrv-20260901-183938772835000-ta[REDACTED].md) +- fix-pin-id-finding-consistency: PASS (docs/metareview/reviews/mrv-20260901-191438733281000-ta[REDACTED].md) +- fix-proof-binds-fix-diff: PASS (docs/metareview/reviews/mrv-20260901-204852362514000-ta[REDACTED].md) +- github-reviewdecision-prready-fix: PASS (docs/metareview/reviews/mrv-20260705-221742306514000-ta[REDACTED].md) +- internal/fsm/gate/gates.go: PASS (docs/metareview/reviews/mrv-20260902-040527894922000-task-done-gates-bca330f8.md) +- internal/fsm/judge/judge.go: PASS (docs/metareview/reviews/mrv-20260902-045920838914000-task-done-judge-b7060768.md) +- internal/fsm/kind/kind.go: PASS_ADVISORY (docs/metareview/reviews/mrv-20260910-153756405005000-task-done-kind-b7bb121c.md) findings: mrvf-20260910-151423657039000-ta[REDACTED], mrvf-20260910-151423657039000-ta[REDACTED], mrvf-20260910-151538496642000-ta[REDACTED] +- internal/gitcontext/gitcontext.go: PASS (docs/metareview/reviews/mrv-20260902-060726814084000-ta[REDACTED].md) +- internal/githubcontext/githubcontext.go: PASS (docs/metareview/reviews/mrv-20260902-034259118558000-ta[REDACTED].md) +- internal/prready/review.go: PASS (docs/metareview/reviews/mrv-20260902-040416607878000-ta[REDACTED].md) +- internal/testconv/typescript.go: PASS (docs/metareview/reviews/mrv-20260902-051859934731000-ta[REDACTED].md) +- issue-2-wu1: PASS (docs/metareview/reviews/mrv-20260705-012852131814000-ta[REDACTED].md) +- issue-2-wu1-version-0.6.0: PASS (docs/metareview/reviews/mrv-20260705-030845991374000-ta[REDACTED].md) +- issue-2-wu2: PASS (docs/metareview/reviews/mrv-20260705-034317422615000-ta[REDACTED].md) +- issue-2-wu2-cursor-followup-fix: PASS (docs/metareview/reviews/mrv-20260705-040500203996000-ta[REDACTED].md) +- issue-2-wu2-prready-filter-fix: PASS (docs/metareview/reviews/mrv-20260705-035954884586000-ta[REDACTED].md) +- issue-2-wu2-review-fix: PASS (docs/metareview/reviews/mrv-20260705-035540088635000-ta[REDACTED].md) +- issue-2-wu3: PASS (docs/metareview/reviews/mrv-20260705-050759694908000-ta[REDACTED].md) +- mechanical-precision-lens: PASS (docs/metareview/reviews/mrv-20260831-183207933768000-ta[REDACTED].md) +- multilang-pytest-conv: PASS (docs/metareview/reviews/mrv-20260901-190331877901000-ta[REDACTED].md) +- multilang-testconv-seam: PASS (docs/metareview/reviews/mrv-20260901-165623149954000-ta[REDACTED].md) +- multilang-typescript-conv: PASS (docs/metareview/reviews/mrv-20260901-172216437441000-ta[REDACTED].md) +- multilang-vitest-conv: PASS (docs/metareview/reviews/mrv-20260901-174949441189000-ta[REDACTED].md) +- runchain-headsha-escalation-reset: PASS (docs/metareview/reviews/mrv-20260705-222055675782000-ta[REDACTED].md) +- runchain-reset-findings-reconciliation: PASS (docs/metareview/reviews/mrv-20260705-223346154492000-ta[REDACTED].md) +- t1.1-differentialproof-model: PASS (docs/metareview/reviews/mrv-20260901-001859561454000-task-done-t1-aa37529c.md) +- t1.2-agentedit-reduce-seam: PASS (docs/metareview/reviews/mrv-20260901-002517703556000-task-done-t1-b5a41665.md) +- t1.3-reproduction-engine-pr-b: PASS (docs/metareview/reviews/mrv-20260901-080559565942000-task-done-t1-8af2654f.md) +- t1.4-right-reason-reviewer: PASS (docs/metareview/reviews/mrv-20260901-100944138141000-task-done-t1-ab64ea55.md) +- t1.5-deletion-provable-fix: PASS (docs/metareview/reviews/mrv-20260901-105248210036000-task-done-t1-4ad043b2.md) +- t1.6-trivial-pin-prescreen: PASS (docs/metareview/reviews/mrv-20260901-115747009329000-task-done-t1-b353673e.md) +- t2.1-test-deletion-gate: PASS (docs/metareview/reviews/mrv-20260901-145243731066000-task-done-t2-0fe76ae4.md) +- t2.2-trajectory-monitor: PASS (docs/metareview/reviews/mrv-20260901-154004114739000-task-done-t2-d4fcf27a.md) +- workflows/sdlc-loop-clean.yaml: PASS (docs/metareview/reviews/mrv-20260902-082859947245000-ta[REDACTED].md) + +### Epic Review Evidence + +- internal/reviewers/epicready.go: PASS (docs/metareview/reviews/mrv-20260902-035232185408000-ta[REDACTED].md) + +### Recorded-Evidence Blocker Status + +_Blockers carried forward from recorded review evidence; findings this gate run raised are under `## Blocking Findings` above._ + +No unresolved blockers in the recorded review evidence. + +### External GitHub Review Context + +GitHub context unavailable: pr-number-unavailable +