Skip to content

fix(opencode): review-result-artifacts.ts honors stale GENTLE_AI_REVIEW_CWD across projects, silently hijacking capture #2446

Description

@miguelpuente

Pre-flight Checklist

  • I have searched existing issues and this is not a duplicate
  • I understand that PRs will be rejected if the linked issue does not have status:approved

Related closed issue: #1484 ("fix(review): propagate explicit review cwd into bound lens capture") covered the fallback path resolving to / when no GENTLE_AI_REVIEW_CWD is set. This report covers the override path silently hijacking capture to the wrong repository when a stale env var points elsewhere.

Related closed issue: #1610 / open issue #1789 / PR #2099 cover prose and fence stripping. Different failure mode. Not a duplicate.

📝 Bug Description

plugins/review-result-artifacts.ts reads process.env["GENTLE_AI_REVIEW_CWD"] with priority over the opencode worktree/directory in captureCwd() (line 109 in the upstream main checkout). When a previous opencode session set this env var to a different project root (e.g., a sibling checkout under /opt/sion_factory/projects/), the next session's review sub-agents silently capture to the wrong .git/gentle-ai/review-transactions/ directory:

  • Every gentle-ai review capture-result --preflight fails with Error: resolve reviewing authority for lineage "review-XXXX" under repository "/other/project": load compact facade review lineage: open /other/project/.git/gentle-ai/review-transactions/v2/review-XXXX/review-state.json: no such file or directory.
  • The native admission rejects every reviewer task as repository_context_capture_failed or incomplete.
  • All lens results are stranded as .git/gentle-ai/review-transactions/incidents/<lineage>/ preserved incidents.
  • The reviewer task is marked as "exactly once" preserved, so retries hit the same wall.

The env var is undocumented but persists across sessions in many shells. There is no warning, no log, and no recovery — the user sees a flood of admission failures with no obvious cause.

🔄 Steps to Reproduce

  1. Open an opencode session in project A; set GENTLE_AI_REVIEW_CWD to project A's .git ancestor via shell or any subprocess. End the session.
  2. Open a new opencode session in project B (a different git repo). The env var persists.
  3. Run a bounded review: gentle-ai review start --base-ref main --committed-only.
  4. Launch any lens sub-agent (e.g., task --subagent_type review-risk ...).
  5. Observe the preflight failure.

Concrete reproduction (one-liner):

# Set up two real git repos in different parents
TMPDIR=$(mktemp -d); git -C "$TMPDIR" init --quiet
TMPDIR2=$(mktemp -d); git -C "$TMPDIR2" init --quiet

# Pretend previous session left the env var pointing to TMPDIR
export GENTLE_AI_REVIEW_CWD="$TMPDIR"

# Now operate in TMPDIR2 — should fail to find any review authority there
cd "$TMPDIR2"
gentle-ai review start --base-ref HEAD --committed-only --contract gentle-ai.review-integration/v2 \
  --target sha256:0000000000000000000000000000000000000000000000000000000000000000 \
  --projection workspace
# → preflight fails: "under /tmp/...TMPDIR: no such file or directory"

✅ Expected Behavior

The hook should refuse to honor an env var that points outside the opencode worktree/directory, fall back to the opencode anchor, and emit a console.warn so the user notices. Nested worktrees (which legitimately share a .git directory with the main checkout) should still be accepted.

❌ Actual Behavior

The hook returns override.trim() unconditionally. Captures silently land in the wrong .git/gentle-ai/review-transactions/. Every preflight fails with the confusing "no such file" error and no hint that the env var is the cause.

🖥️ Environment

  • Gentle AI Version: 2.2.4
  • OS: Linux x86_64 (Ubuntu 24.04)
  • Agent / Client: OpenCode with managed plugins/review-result-artifacts.ts
  • Reproducible across: any sibling-project layout under a common parent (e.g., /opt/sion_factory/projects/{projectA,projectB}).

Suggested Fix

Validate that the override and the opencode anchor resolve to the same Git common directory before honoring it. If not, log a warning and fall back. Use git rev-parse --absolute-git-dir (returns the absolute path to the .git directory of the main checkout, even for nested worktrees). Do NOT use --git-common-dir: for invalid paths, git walks up the directory tree and returns the nearest ancestor's .git, falsely matching unrelated repositories. Empirically confirmed: git -C /opt/sion_factory/projects/MFAI rev-parse --git-common-dir returns .git even though MFAI is a separate repo.

Proposed patch (minimal, against upstream main):

--- a/internal/assets/opencode/plugins/review-result-artifacts.ts
+++ b/internal/assets/opencode/plugins/review-result-artifacts.ts
@@ function captureCwd(worktree: string | undefined, directory: string): string {
-  const override = process.env["GENTLE_AI_REVIEW_CWD"]
-  if (typeof override === "string" && override.trim() !== "") return override.trim()
-  return worktree || directory
+  const override = process.env["GENTLE_AI_REVIEW_CWD"]
+  const fallback = worktree || directory
+  if (typeof override === "string" && override.trim() !== "") {
+    if (isInsideSameGitRepo(override, fallback)) return override.trim()
+    console.warn(
+      `[review-result-artifacts] GENTLE_AI_REVIEW_CWD=${override} is outside the ` +
+      `current Git repository rooted at ${fallback}; ignoring override and using ${fallback}.`,
+    )
+  }
+  return fallback
+}
+
+function isInsideSameGitRepo(candidate: string, anchor: string): boolean {
+  try {
+    const { execFileSync } = require("node:child_process") as typeof import("node:child_process")
+    const abs = (cwd: string) => execFileSync("git", ["-C", cwd, "rev-parse", "--absolute-git-dir"], { encoding: "utf8" }).trim()
+    const anchorGit = abs(anchor)
+    const candidateGit = abs(candidate)
+    return anchorGit !== "" && anchorGit === candidateGit
+  } catch {
+    return false
+  }
 }

Test Scenarios (Go side, internal/assets/review_plugin_recovery_test.go style)

Following the pattern established by PR #2099:

  • before-cwd-same-repo: GENTLE_AI_REVIEW_CWD set to worktree path → preflight proceeds, capture succeeds.
  • before-cwd-cross-project: GENTLE_AI_REVIEW_CWD set to a sibling project's .git parent → preflight fails with unsupported-capability (existing behavior for opaque failures) and console.warn is emitted with the rejection message.
  • before-cwd-nonexistent: GENTLE_AI_REVIEW_CWD set to a non-git path → preflight fails, fallback to anchor used, warning logged.

The Go harness needs a small extension to capture stderr from the plugin (currently it only captures stdout for native calls). Will propose separately once this issue is approved.

Metadata

Metadata

Assignees

No one assigned

    Labels

    status:approvedApproved for implementation — PRs can now be opened

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions