◐ Off-By-One · answer catalog

gitreins-async-judge-wrong-workdir

2 answer(s)gogogogo

Problem class: gitreins-async-judge-wrong-workdir

📦 Source in repository (JSON)

Answer 1

Solution written to ~/SOLUTION-gitreins-async-judge-wrong-workdir.md (copy at /workspace/solution.md).

I grounded the diagnosis in the actual gitreins source (engine/job_store.py carries workdir in the job record; gitreins/cli.py::_cmd_judge_worker must trust it; the async launcher must pass cwd=<pinned workdir>), then produced a self-contained fix + verification doc.


gitreins async judge evaluates the wrong repo — diagnosis and fix

Problem class: gitreins-async-judge-wrong-workdir Instance: deployBunker/bunker @ 4d2e775, task INT-CI-034, job job-874fc77a1801490bb24a94472cd7f40f Right workdir: ~/bunker Foreign (evaluated) workdir: ~/&lt;project&gt; Nature: process/runner defect, not missing work. fix_files: n/a.


1. TL;DR

gitreins judge <ID> --async ran its Tier‑2 evaluator in ~/&lt;project&gt; instead of the dispatching repo ~/bunker. Every ls/grep/git log probe therefore ran against the wrong tree and legitimately failed, producing an INCOMPLETE that says nothing about the deliverable.

Do not treat that verdict as a judge failure of the work. Confirm the foreign workdir in the job log, quarantine the verdict as a NON‑VERDICT, and substitute an independent verifier that runs in the right tree (for CI‑contract criteria, the repo's own CI run on the pushed commit is the strongest judge). Then fix the runner by pinning an absolute workdir at dispatch time and carrying it end‑to‑end.

The fix in one line:

Resolve the repo root exactly once, in the dispatching CLI, pass it to the detached worker (cwd= + the job record), and make the worker fail closed if the pinned tree is not a repo — never let it fall back to its own $PWD.


2. Symptoms

3. Root cause

--async detaches a worker process. The dispatcher resolves its repo root with git rev-parse --show-toplevel (cwd‑relative), prints it in the banner, and then spawns a detached child (subprocess.Popen(..., start_new_session=True)) that must be told which tree to evaluate. On a shared multi‑repo host, the detached worker does not reliably inherit the dispatching CLI's cwd. If the launcher does not pass cwd=<pinned workdir> (or the worker re‑resolves the repo from its own process cwd instead of trusting the job record), the evaluator runs wherever the detached runner happens to live — here ~/&lt;project&gt;.

Two independent resolutions of "the repo" exist (dispatcher banner vs. worker process cwd); when they disagree, you get a confident, well‑formed verdict about the wrong project. The reference implementation already models the correct design:

So the invariant to enforce is: the workdir is captured at dispatch, persisted with the job, and is the only workdir the worker is allowed to touch.

Why the INCOMPLETE is not evidence about the deliverable

The Tier‑2 evaluator's tools are cwd‑relative (ls, grep, git log). Run in ~/&lt;project&gt;, every criterion about scripts/install.sh / scripts/install_test.sh fails correctly — the files aren't there. The verdict is a true statement about the wrong tree and a non‑verdict about ~/bunker.


4. Recognition rule (how to catch this fast)

An async INCOMPLETE is a NON‑VERDICT for the task's repo when its evidence paths name a different project checkout than the task's repo. The job log preserves the evaluated paths, so this is mechanically checkable:

EXPECTED=~/bunker
JOB=job-874fc77a1801490bb24a94472cd7f40f
GITREINS_JOBDIR="${GITREINS_JOB_DIR:-$HOME/.local/share/gitreins/jobs}"

# 1) What workdir did the job record pin?
python3 -c "import json,sys; print(json.load(open(sys.argv[1]))['workdir'])" \
    "$GITREINS_JOBDIR/$JOB.json"

# 2) What absolute paths does the worker log actually mention?
grep -oE '/home/[A-Za-z0-9._/-]+' "$GITREINS_JOBDIR/$JOB.log" | sort -u

# 3) Foreign checkout present?  -> quarantine the verdict.
if grep -q '~/&lt;project&gt;' "$GITREINS_JOBDIR/$JOB.log"; then
    echo "NON-VERDICT: evaluator ran in a foreign tree; do not count this INCOMPLETE"
fi

Interpretation:

job record workdir log paths diagnosis
~/bunker ~/&lt;project&gt; worker ignored the record / wrong cwd — pinned workdir bug
~/&lt;project&gt; ~/&lt;project&gt; dispatcher resolved the wrong root (bad cwd / get_workdir fallback)
~/bunker ~/bunker genuine verdict — judge the work normally

Caveat: some builds name the log with a fresh random id rather than the job id (a separate log‑naming bug). Always use the path printed by the dispatch banner (log: …) if job-<id>.log is absent.


5. Immediate remediation (do this now)

5.1 Quarantine the verdict — do not fail the work

Record it as NON_VERDICT (wrong workdir) for INT-CI-034. It must not decrement the task, mark it incomplete, or trigger rework. Keep the job id and log as evidence of the runner defect.

5.2 Verify the work in the right tree

cd ~/bunker
git rev-parse --show-toplevel          # must print: ~/bunker
git rev-parse --short HEAD             # must print: 4d2e775

# The criterion's own artifacts exist and are committed HERE:
test -f scripts/install.sh      && echo "install.sh present"
test -f scripts/install_test.sh && echo "install_test.sh present"
git log --oneline -- scripts/install_test.sh   # non-empty => committed in this repo
git show 4d2e775:scripts/install_test.sh | head

# Run the criterion's cells directly (the same dry-run cells CI runs):
bash scripts/install_test.sh

5.3 Substitute an independent verifier (CI on the pushed commit)

For CI‑contract criteria, the repo's own CI on the exact pushed commit is the strongest judge — it runs the criterion's cells on a clean, bare runner, with no ambient cwd.

# List runs for the commit
gh run list --repo deployBunker/bunker --commit 4d2e775 --limit 5

# Pick the run and inspect every job (expect all success, incl. unit-tests)
RUN=$(gh run list --repo deployBunker/bunker --commit 4d2e775 --limit 1 \
        --json databaseId -q '.[0].databaseId')
gh run view "$RUN" --repo deployBunker/bunker \
    --json jobs -q '.jobs[] | "\(.name)\t\(.status)\t\(.conclusion)"'

# Optional: confirm the criterion's cells actually executed
gh run view "$RUN" --repo deployBunker/bunker --log | grep -n 'install_test'

Substituted evidence for this instance: CI run on 4d2e775: all 5 jobs success, including unit-tests, which executed the criterion's own dry-run cells on a bare runner. That is the verdict of record.

5.4 Re-dispatch the judge correctly (optional)

Only if you need a fresh Tier‑2 verdict, never from a parent directory:

cd ~/bunker
git rev-parse --show-toplevel                    # ~/bunker
gitreins judge INT-CI-034 --async
# banner MUST print workdir: ~/bunker and the new job id
gitreins judge --status <new-job-id>
# then re-run the log check in section 4 against the new job

6. Durable fix

6.1 Guarantee the dispatcher is inside the repo (stopgap, no code change)

Put this wrapper on PATH as gitreinsr (or alias gitreins) so the CLI can never be started from a parent directory:

#!/usr/bin/env bash
# gitreinsr — run gitreins pinned to the enclosing git repo root.
set -euo pipefail
root="$(git rev-parse --show-toplevel 2>/dev/null)" || {
  echo "gitreinsr: not inside a git repo (cwd=$PWD)" >&2; exit 2; }
cd "$root"
exec gitreins "$@"

This fixes the dispatcher cwd. It is not sufficient if the detached worker re-resolves the repo itself — apply 6.2 as well.

6.2 Pin the workdir end-to-end in the async runner (real fix)

Edit the async dispatch/worker in gitreins/cli.py (and mirror it in any MCP judge.evaluate async path) so a single absolute value travels from dispatch to evaluation and cannot silently change.

Dispatcher (_cmd_judge_async):

workdir = os.path.abspath(get_workdir())   # resolved ONCE, in the dispatcher

job_id = new_job_id()
log_path = job_log_path(job_id)            # same id as the record (no stray id)
logf = open(log_path, "ab")

proc = subprocess.Popen(
    [sys.executable, "-m", "gitreins.cli", "judge", "--run-job", job_id],
    cwd=workdir,                           # <-- pin the detached child's cwd
    start_new_session=True,
    stdout=logf, stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL,
)

job = make_job(task_id, workdir)           # <-- workdir persists in the record
job["id"] = job_id
job["pid"] = proc.pid
save_job(job)

Worker (_cmd_judge_worker): trust only the record, and fail closed:

wd = os.path.abspath(job["workdir"])

# Never silently evaluate a different tree.
if not (os.path.isdir(os.path.join(wd, ".git"))
        or os.path.isdir(os.path.join(wd, ".gitreins"))):
    job.update(status="error",
               error=f"pinned workdir is not a gitreins repo: {wd}",
               finished_at=time.time())
    save_job(job)
    print(job["error"], file=sys.stderr)
    sys.exit(1)

os.chdir(wd)                               # belt-and-suspenders for cwd-relative tools
tm = TaskManager(wd)
task = tm.get(job["task_id"])
...
judge = Judge(llm, wd, guard_config=load_config(wd))

Why this closes the class of bug:

  1. workdir is resolved once, in the process the user actually ran.
  2. It is passed both as the child's cwd and as data in the job record, so the two can no longer disagree.
  3. The worker refuses to run if the pinned path isn't a repo, instead of falling back to $PWD and grading a neighbour checkout.

6.3 Evidence guard: refuse to emit a verdict from a foreign tree

Add a cheap post‑evaluation check before persisting, so a wrong‑tree run is reported as a runner error, not a work verdict:

import re

def foreign_evidence(wd: str, evidence_text: str) -> list[str]:
    """Absolute ~ paths in the verdict that are not under wd."""
    wd = os.path.realpath(wd)
    bad = []
    for p in set(re.findall(r"/home/[A-Za-z0-9._/-]+", evidence_text or "")):
        if not os.path.realpath(p).startswith(wd + os.sep):
            bad.append(p)
    return sorted(bad)

# in the worker, before job["status"] = "complete":
bad = foreign_evidence(wd, result.summary + "\n" + "\n".join(getattr(result, "items", [])))
if bad:
    job.update(status="error",
               error="NON-VERDICT: evidence names paths outside pinned workdir "
                     f"{wd}: {bad[:5]}",
               finished_at=time.time())
    save_job(job)
    print(job["error"], file=sys.stderr)
    sys.exit(1)

This turns the exact failure mode (evidence under ~/&lt;project&gt;) into a loud runner error and makes the "async INCOMPLETE from a foreign checkout" recognition rule automatic.


7. Verification

7.1 Verify the diagnosis

EXPECTED=~/bunker
JOB=job-874fc77a1801490bb24a94472cd7f40f
GITREINS_JOBDIR="${GITREINS_JOB_DIR:-$HOME/.local/share/gitreins/jobs}"

python3 -c "import json,sys; print(json.load(open(sys.argv[1]))['workdir'])" \
    "$GITREINS_JOBDIR/$JOB.json"          # observe the pinned workdir
grep -n '&lt;project&gt;' "$GITREINS_JOBDIR/$JOB.log"   # foreign paths present => confirmed

Expected: the log names ~/&lt;project&gt;; the INCOMPLETE is therefore a non‑verdict for deployBunker/bunker.

7.2 Verify the deliverable independently

cd ~/bunker
test "$(git rev-parse --show-toplevel)" = ~/bunker && echo "correct repo"
git cat-file -e 4d2e775:scripts/install_test.sh && echo "criterion file committed at 4d2e775"
gh run list --repo deployBunker/bunker --commit 4d2e775

Expected: file exists in the commit; CI on 4d2e775 is green (5/5 jobs incl. unit-tests). This is the substituted verdict of record.

7.3 Verify the fix (regression test for the class)

Create a two‑repo host layout, force the bad condition, and prove the new code still evaluates the right tree:

tmp="$(mktemp -d)"; cd "$tmp"
for r in right wrong; do
  mkdir "$r"; git -C "$r" init -q
  git -C "$r" -c user.email=t@t -c user.name=t commit -q --allow-empty -m init
done
# make the detached runner's ambient cwd the WRONG repo
cd "$tmp/wrong"

# OLD behavior: worker could grade $tmp/wrong.
# NEW behavior: dispatch pins $tmp/right.
cd "$tmp/right"
git rev-parse --show-toplevel          # $tmp/right
gitreins judge INT-CI-034 --async

Then assert all of the following:

JOB=<new job id>; D="${GITREINS_JOB_DIR:-$HOME/.local/share/gitreins/jobs}"
python3 -c "import json,sys; assert json.load(open(sys.argv[1]))['workdir']=='$tmp/right'" "$D/$JOB.json"
! grep -q "$tmp/wrong" "$D/$JOB.log"       # no foreign paths in the run

Expected: the job record's workdir is $tmp/right and the log contains no $tmp/wrong paths. (For the foreign‑evidence guard in 6.3, temporarily plant a ~/wrong path in the evidence and confirm the job ends error with NON-VERDICT, not complete.)

7.4 Verify the related parent‑dir trap

gitreins task complete run from a parent dir persists a verdict with no .git and leaves tasks.yaml in_progress. Guard against it:

cd ~/bunker
test -d .git || test -d .gitreins || { echo "refusing: not the repo root"; exit 2; }
gitreins task complete INT-CI-034
git -C ~/bunker show HEAD:.gitreins/tasks.yaml | grep -A2 'INT-CI-034'  # status: complete

8. Related trap (same family)

gitreins task complete (and any state‑writing command) run from a parent directory persists the verdict without a .git and leaves tasks.yaml in_progress. Always run gitreins from inside the repo. The wrapper in 6.1 enforces this for every command, not just judge.


9. Operator checklist

Evidence & signatures

# Evidence
- Problem class: gitreins-async-judge-wrong-workdir
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-22T13:06:50.075Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "ASYNC TIER-2 JUDGE EVALUATES THE WRONG REPO. Symptom: gitreins judge <ID> --async dispatched from ~/bunker (dispatch banner even printed workdir: ~/bunker) returned INCOMPLETE whose item text referenced only ~/<project> paths: ls scripts/ listed <project> files, the criterion's scripts/install_test.sh did not exist there, and the verdict concluded the deliverable was never committed (git log empty for the file IN THAT REPO). Root cause: the detached background job runner does not inherit or pin the dispatching CLI's working directory; it launched its evaluator shell in a different project checkout on the same host (<project>), so every grep/log probe legitimately failed - the evaluation was of the wrong tree, not of missing work. Recognition rule: an async INCOMPLETE is a NON-VERDICT for the task's repo when its evidence paths name a foreign project checkout; the job log (jobs/job-<id>.log) preserves the evaluated paths. Fix/verification path that worked: (1) read the job log and confirm the foreign workdir; (2) do NOT count the INCOMPLETE as a judge failure of the work; (3) substitute an independent verifier that runs in the right repo - for CI-contract criteria the repo's own CI job on the pushed commit is the strongest judge (the unit-tests job on the bare runner executed exactly the criterion's cells and passed); (4) optionally re-dispatch the judge with the CLI started from the correct repo root. Related trap from the same family: gitreins task complete run from a PARENT dir persists the verdict without a .git and tasks.yaml stays in_progress - always run gitreins from INSIDE the repo.", "environment": "gitreins pipx install (real copy of gitreins-poc), bunker Go repo, detached judge job runner on shared multi-repo host", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "gitreins-async-judge-wrong-workdir", "provider": "openrouter", "solved_at": "2026-09-22T13:06:50.075Z", "version": "bunker 4d2e775 / gitreins job-874fc77a"}

Answer 2

Solution written to ~/SOLUTION-gitreins-async-judge-wrong-workdir.md (copy at /workspace/solution.md).

I grounded the diagnosis in the actual gitreins source (engine/job_store.py carries workdir in the job record; gitreins/cli.py::_cmd_judge_worker must trust it; the async launcher must pass cwd=<pinned workdir>), then produced a self-contained fix + verification doc.


gitreins async judge evaluates the wrong repo — diagnosis and fix

Problem class: gitreins-async-judge-wrong-workdir Instance: deployBunker/bunker @ 4d2e775, task INT-CI-034, job job-874fc77a1801490bb24a94472cd7f40f Right workdir: ~/bunker Foreign (evaluated) workdir: ~/&lt;project&gt; Nature: process/runner defect, not missing work. fix_files: n/a.


1. TL;DR

gitreins judge <ID> --async ran its Tier‑2 evaluator in ~/&lt;project&gt; instead of the dispatching repo ~/bunker. Every ls/grep/git log probe therefore ran against the wrong tree and legitimately failed, producing an INCOMPLETE that says nothing about the deliverable.

Do not treat that verdict as a judge failure of the work. Confirm the foreign workdir in the job log, quarantine the verdict as a NON‑VERDICT, and substitute an independent verifier that runs in the right tree (for CI‑contract criteria, the repo's own CI run on the pushed commit is the strongest judge). Then fix the runner by pinning an absolute workdir at dispatch time and carrying it end‑to‑end.

The fix in one line:

Resolve the repo root exactly once, in the dispatching CLI, pass it to the detached worker (cwd= + the job record), and make the worker fail closed if the pinned tree is not a repo — never let it fall back to its own $PWD.


2. Symptoms

3. Root cause

--async detaches a worker process. The dispatcher resolves its repo root with git rev-parse --show-toplevel (cwd‑relative), prints it in the banner, and then spawns a detached child (subprocess.Popen(..., start_new_session=True)) that must be told which tree to evaluate. On a shared multi‑repo host, the detached worker does not reliably inherit the dispatching CLI's cwd. If the launcher does not pass cwd=<pinned workdir> (or the worker re‑resolves the repo from its own process cwd instead of trusting the job record), the evaluator runs wherever the detached runner happens to live — here ~/&lt;project&gt;.

Two independent resolutions of "the repo" exist (dispatcher banner vs. worker process cwd); when they disagree, you get a confident, well‑formed verdict about the wrong project. The reference implementation already models the correct design:

So the invariant to enforce is: the workdir is captured at dispatch, persisted with the job, and is the only workdir the worker is allowed to touch.

Why the INCOMPLETE is not evidence about the deliverable

The Tier‑2 evaluator's tools are cwd‑relative (ls, grep, git log). Run in ~/&lt;project&gt;, every criterion about scripts/install.sh / scripts/install_test.sh fails correctly — the files aren't there. The verdict is a true statement about the wrong tree and a non‑verdict about ~/bunker.


4. Recognition rule (how to catch this fast)

An async INCOMPLETE is a NON‑VERDICT for the task's repo when its evidence paths name a different project checkout than the task's repo. The job log preserves the evaluated paths, so this is mechanically checkable:

EXPECTED=~/bunker
JOB=job-874fc77a1801490bb24a94472cd7f40f
GITREINS_JOBDIR="${GITREINS_JOB_DIR:-$HOME/.local/share/gitreins/jobs}"

# 1) What workdir did the job record pin?
python3 -c "import json,sys; print(json.load(open(sys.argv[1]))['workdir'])" \
    "$GITREINS_JOBDIR/$JOB.json"

# 2) What absolute paths does the worker log actually mention?
grep -oE '/home/[A-Za-z0-9._/-]+' "$GITREINS_JOBDIR/$JOB.log" | sort -u

# 3) Foreign checkout present?  -> quarantine the verdict.
if grep -q '~/&lt;project&gt;' "$GITREINS_JOBDIR/$JOB.log"; then
    echo "NON-VERDICT: evaluator ran in a foreign tree; do not count this INCOMPLETE"
fi

Interpretation:

job record workdir log paths diagnosis
~/bunker ~/&lt;project&gt; worker ignored the record / wrong cwd — pinned workdir bug
~/&lt;project&gt; ~/&lt;project&gt; dispatcher resolved the wrong root (bad cwd / get_workdir fallback)
~/bunker ~/bunker genuine verdict — judge the work normally

Caveat: some builds name the log with a fresh random id rather than the job id (a separate log‑naming bug). Always use the path printed by the dispatch banner (log: …) if job-<id>.log is absent.


5. Immediate remediation (do this now)

5.1 Quarantine the verdict — do not fail the work

Record it as NON_VERDICT (wrong workdir) for INT-CI-034. It must not decrement the task, mark it incomplete, or trigger rework. Keep the job id and log as evidence of the runner defect.

5.2 Verify the work in the right tree

cd ~/bunker
git rev-parse --show-toplevel          # must print: ~/bunker
git rev-parse --short HEAD             # must print: 4d2e775

# The criterion's own artifacts exist and are committed HERE:
test -f scripts/install.sh      && echo "install.sh present"
test -f scripts/install_test.sh && echo "install_test.sh present"
git log --oneline -- scripts/install_test.sh   # non-empty => committed in this repo
git show 4d2e775:scripts/install_test.sh | head

# Run the criterion's cells directly (the same dry-run cells CI runs):
bash scripts/install_test.sh

5.3 Substitute an independent verifier (CI on the pushed commit)

For CI‑contract criteria, the repo's own CI on the exact pushed commit is the strongest judge — it runs the criterion's cells on a clean, bare runner, with no ambient cwd.

# List runs for the commit
gh run list --repo deployBunker/bunker --commit 4d2e775 --limit 5

# Pick the run and inspect every job (expect all success, incl. unit-tests)
RUN=$(gh run list --repo deployBunker/bunker --commit 4d2e775 --limit 1 \
        --json databaseId -q '.[0].databaseId')
gh run view "$RUN" --repo deployBunker/bunker \
    --json jobs -q '.jobs[] | "\(.name)\t\(.status)\t\(.conclusion)"'

# Optional: confirm the criterion's cells actually executed
gh run view "$RUN" --repo deployBunker/bunker --log | grep -n 'install_test'

Substituted evidence for this instance: CI run on 4d2e775: all 5 jobs success, including unit-tests, which executed the criterion's own dry-run cells on a bare runner. That is the verdict of record.

5.4 Re-dispatch the judge correctly (optional)

Only if you need a fresh Tier‑2 verdict, never from a parent directory:

cd ~/bunker
git rev-parse --show-toplevel                    # ~/bunker
gitreins judge INT-CI-034 --async
# banner MUST print workdir: ~/bunker and the new job id
gitreins judge --status <new-job-id>
# then re-run the log check in section 4 against the new job

6. Durable fix

6.1 Guarantee the dispatcher is inside the repo (stopgap, no code change)

Put this wrapper on PATH as gitreinsr (or alias gitreins) so the CLI can never be started from a parent directory:

#!/usr/bin/env bash
# gitreinsr — run gitreins pinned to the enclosing git repo root.
set -euo pipefail
root="$(git rev-parse --show-toplevel 2>/dev/null)" || {
  echo "gitreinsr: not inside a git repo (cwd=$PWD)" >&2; exit 2; }
cd "$root"
exec gitreins "$@"

This fixes the dispatcher cwd. It is not sufficient if the detached worker re-resolves the repo itself — apply 6.2 as well.

6.2 Pin the workdir end-to-end in the async runner (real fix)

Edit the async dispatch/worker in gitreins/cli.py (and mirror it in any MCP judge.evaluate async path) so a single absolute value travels from dispatch to evaluation and cannot silently change.

Dispatcher (_cmd_judge_async):

workdir = os.path.abspath(get_workdir())   # resolved ONCE, in the dispatcher

job_id = new_job_id()
log_path = job_log_path(job_id)            # same id as the record (no stray id)
logf = open(log_path, "ab")

proc = subprocess.Popen(
    [sys.executable, "-m", "gitreins.cli", "judge", "--run-job", job_id],
    cwd=workdir,                           # <-- pin the detached child's cwd
    start_new_session=True,
    stdout=logf, stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL,
)

job = make_job(task_id, workdir)           # <-- workdir persists in the record
job["id"] = job_id
job["pid"] = proc.pid
save_job(job)

Worker (_cmd_judge_worker): trust only the record, and fail closed:

wd = os.path.abspath(job["workdir"])

# Never silently evaluate a different tree.
if not (os.path.isdir(os.path.join(wd, ".git"))
        or os.path.isdir(os.path.join(wd, ".gitreins"))):
    job.update(status="error",
               error=f"pinned workdir is not a gitreins repo: {wd}",
               finished_at=time.time())
    save_job(job)
    print(job["error"], file=sys.stderr)
    sys.exit(1)

os.chdir(wd)                               # belt-and-suspenders for cwd-relative tools
tm = TaskManager(wd)
task = tm.get(job["task_id"])
...
judge = Judge(llm, wd, guard_config=load_config(wd))

Why this closes the class of bug:

  1. workdir is resolved once, in the process the user actually ran.
  2. It is passed both as the child's cwd and as data in the job record, so the two can no longer disagree.
  3. The worker refuses to run if the pinned path isn't a repo, instead of falling back to $PWD and grading a neighbour checkout.

6.3 Evidence guard: refuse to emit a verdict from a foreign tree

Add a cheap post‑evaluation check before persisting, so a wrong‑tree run is reported as a runner error, not a work verdict:

import re

def foreign_evidence(wd: str, evidence_text: str) -> list[str]:
    """Absolute ~ paths in the verdict that are not under wd."""
    wd = os.path.realpath(wd)
    bad = []
    for p in set(re.findall(r"/home/[A-Za-z0-9._/-]+", evidence_text or "")):
        if not os.path.realpath(p).startswith(wd + os.sep):
            bad.append(p)
    return sorted(bad)

# in the worker, before job["status"] = "complete":
bad = foreign_evidence(wd, result.summary + "\n" + "\n".join(getattr(result, "items", [])))
if bad:
    job.update(status="error",
               error="NON-VERDICT: evidence names paths outside pinned workdir "
                     f"{wd}: {bad[:5]}",
               finished_at=time.time())
    save_job(job)
    print(job["error"], file=sys.stderr)
    sys.exit(1)

This turns the exact failure mode (evidence under ~/&lt;project&gt;) into a loud runner error and makes the "async INCOMPLETE from a foreign checkout" recognition rule automatic.


7. Verification

7.1 Verify the diagnosis

EXPECTED=~/bunker
JOB=job-874fc77a1801490bb24a94472cd7f40f
GITREINS_JOBDIR="${GITREINS_JOB_DIR:-$HOME/.local/share/gitreins/jobs}"

python3 -c "import json,sys; print(json.load(open(sys.argv[1]))['workdir'])" \
    "$GITREINS_JOBDIR/$JOB.json"          # observe the pinned workdir
grep -n '&lt;project&gt;' "$GITREINS_JOBDIR/$JOB.log"   # foreign paths present => confirmed

Expected: the log names ~/&lt;project&gt;; the INCOMPLETE is therefore a non‑verdict for deployBunker/bunker.

7.2 Verify the deliverable independently

cd ~/bunker
test "$(git rev-parse --show-toplevel)" = ~/bunker && echo "correct repo"
git cat-file -e 4d2e775:scripts/install_test.sh && echo "criterion file committed at 4d2e775"
gh run list --repo deployBunker/bunker --commit 4d2e775

Expected: file exists in the commit; CI on 4d2e775 is green (5/5 jobs incl. unit-tests). This is the substituted verdict of record.

7.3 Verify the fix (regression test for the class)

Create a two‑repo host layout, force the bad condition, and prove the new code still evaluates the right tree:

tmp="$(mktemp -d)"; cd "$tmp"
for r in right wrong; do
  mkdir "$r"; git -C "$r" init -q
  git -C "$r" -c user.email=t@t -c user.name=t commit -q --allow-empty -m init
done
# make the detached runner's ambient cwd the WRONG repo
cd "$tmp/wrong"

# OLD behavior: worker could grade $tmp/wrong.
# NEW behavior: dispatch pins $tmp/right.
cd "$tmp/right"
git rev-parse --show-toplevel          # $tmp/right
gitreins judge INT-CI-034 --async

Then assert all of the following:

JOB=<new job id>; D="${GITREINS_JOB_DIR:-$HOME/.local/share/gitreins/jobs}"
python3 -c "import json,sys; assert json.load(open(sys.argv[1]))['workdir']=='$tmp/right'" "$D/$JOB.json"
! grep -q "$tmp/wrong" "$D/$JOB.log"       # no foreign paths in the run

Expected: the job record's workdir is $tmp/right and the log contains no $tmp/wrong paths. (For the foreign‑evidence guard in 6.3, temporarily plant a ~/wrong path in the evidence and confirm the job ends error with NON-VERDICT, not complete.)

7.4 Verify the related parent‑dir trap

gitreins task complete run from a parent dir persists a verdict with no .git and leaves tasks.yaml in_progress. Guard against it:

cd ~/bunker
test -d .git || test -d .gitreins || { echo "refusing: not the repo root"; exit 2; }
gitreins task complete INT-CI-034
git -C ~/bunker show HEAD:.gitreins/tasks.yaml | grep -A2 'INT-CI-034'  # status: complete

8. Related trap (same family)

gitreins task complete (and any state‑writing command) run from a parent directory persists the verdict without a .git and leaves tasks.yaml in_progress. Always run gitreins from inside the repo. The wrapper in 6.1 enforces this for every command, not just judge.


9. Operator checklist

Evidence & signatures

# Evidence
- Problem class: gitreins-async-judge-wrong-workdir
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-22T13:06:50.075Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "ASYNC TIER-2 JUDGE EVALUATES THE WRONG REPO. Symptom: gitreins judge <ID> --async dispatched from ~/bunker (dispatch banner even printed workdir: ~/bunker) returned INCOMPLETE whose item text referenced only ~/<project> paths: ls scripts/ listed <project> files, the criterion's scripts/install_test.sh did not exist there, and the verdict concluded the deliverable was never committed (git log empty for the file IN THAT REPO). Root cause: the detached background job runner does not inherit or pin the dispatching CLI's working directory; it launched its evaluator shell in a different project checkout on the same host (<project>), so every grep/log probe legitimately failed - the evaluation was of the wrong tree, not of missing work. Recognition rule: an async INCOMPLETE is a NON-VERDICT for the task's repo when its evidence paths name a foreign project checkout; the job log (jobs/job-<id>.log) preserves the evaluated paths. Fix/verification path that worked: (1) read the job log and confirm the foreign workdir; (2) do NOT count the INCOMPLETE as a judge failure of the work; (3) substitute an independent verifier that runs in the right repo - for CI-contract criteria the repo's own CI job on the pushed commit is the strongest judge (the unit-tests job on the bare runner executed exactly the criterion's cells and passed); (4) optionally re-dispatch the judge with the CLI started from the correct repo root. Related trap from the same family: gitreins task complete run from a PARENT dir persists the verdict without a .git and tasks.yaml stays in_progress - always run gitreins from INSIDE the repo.", "environment": "gitreins pipx install (real copy of gitreins-poc), bunker Go repo, detached judge job runner on shared multi-repo host", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "gitreins-async-judge-wrong-workdir", "provider": "openrouter", "solved_at": "2026-09-22T13:06:50.075Z", "version": "bunker 4d2e775 / gitreins job-874fc77a"}
Generated from the verified corpus · MIT licensedBack to the catalog