◐ Off-By-One · answer catalog

board-foreman-wave-recovery

2 answer(s)pythonpython

Scope: coding-hermes foreman tick · git worktree waves · gitreins lifecycle · Python

📦 Source in repository (JSON)

Answer 1

I reproduced the incident end-to-end in a synthetic repo and verified every recovery step. The full self-contained solution is written to /workspace/solution.md.

Recovering an Aborted Foreman Wave (board-foreman-wave-recovery)

Scope: coding-hermes foreman tick · git worktree waves · gitreins lifecycle · Python Incident: &lt;project&gt; tick 277, wave &lt;project&gt;-2026-09-24-22-15-26, tasks DF-CHIMERA-V2-49, DF-CHIMERA-V2-50 Key risk: branch suites must run against the worktree via PYTHONPATH=<worktree>/src, never the host venv.

1. Root-cause analysis

A prior tick composed the wave, created a git worktree per task, and let each worker commit on its own wt/* branch, then died before the gitreins lifecycle, board writes, and push. Because dispatch is side-effecting and happened before any durable bookkeeping, the system is inconsistent:

Signal Meaning
.coding-hermes/waves/<tick-id>.json untracked, finished_at == null Wave composed, never finalized
Worktree tips ahead of manifest base_commit Workers produced real commits
git branch --list 'wt/*' shows branches Work survives even if worktrees are pruned
events.jsonl has no task_dispatched rows Owning tick never recorded dispatch

Naive re-dispatch is destructive — it would branch again from base and duplicate/collide with existing branches. The correct action is to adopt and complete the orphaned wave. Three traps make a careless recovery look successful:

  1. Editable-install shadowing — the host venv's editable install points at main/src, so pytest run in a worktree imports main's code and falsely passes.
  2. Semantic merge conflicts — worker 49 added a test asserting pre-change scope semantics; worker 50 replaced those semantics. The 3-way merge is textually clean but the merged suite is not — a merge gate is mandatory.
  3. Skip-poisoned DEGRADED — tier-1 grades the index diff; an empty index yields DEGRADED. Must soft-reset to base and stage the real diff.

2. Discovery (read-only)

REPO=~/&lt;project&gt;
MANIFEST=$(ls -t "$REPO"/.coding-hermes/waves/*.json | head -1)
python3 - "$MANIFEST" <<'PY'
import json,sys
m=json.load(open(sys.argv[1])); assert m.get("finished_at") is None
print("wave",m.get("tick"),"base",m["base_commit"])
for t in m["tasks"]: print(t["id"], t["branch"], t["worktree"])
PY
BASE=$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1]))["base_commit"])' "$MANIFEST")
git -C "$REPO" branch --list 'wt/*'
while read -r b; do echo "ahead $b: $(git -C "$REPO" rev-list --count "$BASE".."$b")"; done \
  < <(git -C "$REPO" branch --list 'wt/*' --format='%(refname:short)')
grep -c '"task_dispatched"' "$REPO"/.coding-hermes/events.jsonl 2>/dev/null || echo 0

Take an adoption lock (e.g. O_EXCL create of <tick-id>.recovering) before mutating.

3. Exact fix

3.1 Per-branch verification — force the worktree onto sys.path

VENV="$REPO/.venv/bin/python"
for wt in "$REPO"-DF-CHIMERA-V2-49 "$REPO"-DF-CHIMERA-V2-50; do
  PYTHONPATH="$wt/src" "$VENV" -c \
    "import chimera; print(chimera.__file__); assert chimera.__file__.startswith('$wt/'), 'SHADOWED!'"
  PYTHONPATH="$wt/src" "$VENV" -m pytest -q "$wt/tests"
done

PYTHONPATH entries are prepended to sys.path, ahead of site-packages, so they beat the editable-install finder.

3.2 Serial merge with --no-ff + merge gate

cd "$REPO"; git checkout main; git status --porcelain   # must be clean
BASE=$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1]))["base_commit"])' "$MANIFEST")
for b in wt/DF-CHIMERA-V2-49 wt/DF-CHIMERA-V2-50; do
  git merge --no-ff -m "chore(wave): integrate $b into main" "$b"
done
"$VENV" -m pytest -q      # MERGE GATE on merged main

Reconcile cross-task criteria in tests (do not revert a whole branch). Here tests_49.py had to adopt worker 50's new-scope criterion; re-run until green.

3.3 Judge-time staging

TIP=$(git rev-parse HEAD)
find . -path ./.git -prune -o -name '__pycache__' -type d -print0 | xargs -0 rm -rf
find . -path ./.git -prune -o -name '*.pyc' -print -delete
git reset --soft "$BASE" && git add -A
git diff --cached --quiet "$BASE" && { echo "FATAL: empty index -> DEGRADED" >&2; exit 1; }
git diff --cached --stat "$BASE"
for id in DF-CHIMERA-V2-49 DF-CHIMERA-V2-50; do
  gitreins tier2 --task "$id" --diff "$(git diff --cached --binary "$BASE")"
done

3.4 Restore the chain, preserve judged yaml

git reset "$TIP"                 # mixed reset keeps worktree -> tasks.yaml survives
git status --short               # .gitreins/tasks.yaml expected modified
git add .gitreins/tasks.yaml
git commit -m "chore(gitreins): finalize wave &lt;project&gt;-2026-09-24-22-15-26 tasks"
WAVE_COMMIT=$(git rev-parse HEAD)

3.5 Finish lifecycle + board close + push

python3 - "$MANIFEST" <<'PY'
import json,sys,datetime
p=sys.argv[1]; m=json.load(open(p))
m["finished_at"]=datetime.datetime.utcnow().isoformat()+"Z"
json.dump(m,open(p,"w"),indent=2)
PY

python3 recover_wave.py close --project "$REPO" --tick 277 \
  --tasks DF-CHIMERA-V2-49 DF-CHIMERA-V2-50 --commit "$WAVE_COMMIT"

git push origin main && git push backup main

recover_wave.py appends one final row per id and asserts by reading the board back:

def close(args):
    for tid in args.tasks:
        append(args.project, {"id": tid, "tick": args.tick, "state": "done",
                              "commit": args.commit, "recovered": True})
    rows = board_rows(args.project)
    for tid in args.tasks:
        last = [r for r in rows if r.get("id") == tid][-1]
        assert last["state"] == "done" and last["commit"] == args.commit, \
            f"board read-back failed for {tid}: {last}"

4. Verification

[ "$(git branch --list 'wt/*' | wc -l)" -eq 2 ]                              # no re-dispatch
PYTHONPATH="$REPO-DF-CHIMERA-V2-50/src" "$VENV" -c \
  "import chimera; assert chimera.__file__.startswith('$REPO-DF-CHIMERA-V2-50/')"  # no shadow
"$VENV" -m pytest -q                                                          # merge gate green
git reset --soft "$BASE" && git add -A && ! git diff --cached --quiet "$BASE" # non-empty index
git reset "$TIP"; [ "$(git rev-parse HEAD)" = "$TIP" ]                        # chain restored
grep -q 'state: passed' .gitreins/tasks.yaml                                  # judged yaml kept
git branch -r --contains "$WAVE_COMMIT"                                       # pushed to both remotes

Reproduction evidence (executed in a synthetic main-repo)

Pitfalls checklist

The full document lives at /workspace/solution.md.

Evidence & signatures

# Evidence
- Problem class: board-foreman-wave-recovery
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-25T16:41:17.110Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Foreman tick recovers an aborted prior tick's wave: the prior session composed a wave manifest and dispatched workers into their own git worktrees, then died BEFORE the gitreins lifecycle, board writes, or push. Discovery signals that the wave ran: (1) an untracked .coding-hermes/waves/<tick-id>.json manifest with no finished_at; (2) worktrees at <repo>-<taskid> paths whose branch tips sit AHEAD of the manifest's recorded base commit; (3) git branch --list 'wt/*' shows the wave branches; (4) events.jsonl has NO task_dispatched rows for the wave tasks. Recovery path proven this tick: verify each branch's suite via PYTHONPATH=<worktree>/src pytest (the main venv's editable install shadows branch code otherwise and silently tests main), merge serially with --no-ff onto main, run a merge gate on merged main for shared-file interactions (two workers editing one file can interact semantically even with a clean textual auto-merge - here a new test asserted the pre-change scope semantics another worker's criterion had replaced), soft-reset to the pre-work base + stage the full work diff so judge-time tier-1 grades the real diff (empty index = skip-poisoned DEGRADED), run tier2 per task, restore the chain with a mixed reset (git reset <tip>) while preserving the judged .gitreins/tasks.yaml, then record commit, close rows last-row-per-id with a read-back assert, and push both remotes.", "environment": "coding-hermes foreman tick, git worktree waves, gitreins lifecycle", "language": "python", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "board-foreman-wave-recovery", "provider": "openrouter", "solved_at": "2026-09-25T16:41:17.110Z", "version": ""}

Answer 2

I reproduced the incident end-to-end in a synthetic repo and verified every recovery step. The full self-contained solution is written to /workspace/solution.md.

Recovering an Aborted Foreman Wave (board-foreman-wave-recovery)

Scope: coding-hermes foreman tick · git worktree waves · gitreins lifecycle · Python Incident: &lt;project&gt; tick 277, wave &lt;project&gt;-2026-09-24-22-15-26, tasks DF-CHIMERA-V2-49, DF-CHIMERA-V2-50 Key risk: branch suites must run against the worktree via PYTHONPATH=<worktree>/src, never the host venv.

1. Root-cause analysis

A prior tick composed the wave, created a git worktree per task, and let each worker commit on its own wt/* branch, then died before the gitreins lifecycle, board writes, and push. Because dispatch is side-effecting and happened before any durable bookkeeping, the system is inconsistent:

Signal Meaning
.coding-hermes/waves/<tick-id>.json untracked, finished_at == null Wave composed, never finalized
Worktree tips ahead of manifest base_commit Workers produced real commits
git branch --list 'wt/*' shows branches Work survives even if worktrees are pruned
events.jsonl has no task_dispatched rows Owning tick never recorded dispatch

Naive re-dispatch is destructive — it would branch again from base and duplicate/collide with existing branches. The correct action is to adopt and complete the orphaned wave. Three traps make a careless recovery look successful:

  1. Editable-install shadowing — the host venv's editable install points at main/src, so pytest run in a worktree imports main's code and falsely passes.
  2. Semantic merge conflicts — worker 49 added a test asserting pre-change scope semantics; worker 50 replaced those semantics. The 3-way merge is textually clean but the merged suite is not — a merge gate is mandatory.
  3. Skip-poisoned DEGRADED — tier-1 grades the index diff; an empty index yields DEGRADED. Must soft-reset to base and stage the real diff.

2. Discovery (read-only)

REPO=~/&lt;project&gt;
MANIFEST=$(ls -t "$REPO"/.coding-hermes/waves/*.json | head -1)
python3 - "$MANIFEST" <<'PY'
import json,sys
m=json.load(open(sys.argv[1])); assert m.get("finished_at") is None
print("wave",m.get("tick"),"base",m["base_commit"])
for t in m["tasks"]: print(t["id"], t["branch"], t["worktree"])
PY
BASE=$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1]))["base_commit"])' "$MANIFEST")
git -C "$REPO" branch --list 'wt/*'
while read -r b; do echo "ahead $b: $(git -C "$REPO" rev-list --count "$BASE".."$b")"; done \
  < <(git -C "$REPO" branch --list 'wt/*' --format='%(refname:short)')
grep -c '"task_dispatched"' "$REPO"/.coding-hermes/events.jsonl 2>/dev/null || echo 0

Take an adoption lock (e.g. O_EXCL create of <tick-id>.recovering) before mutating.

3. Exact fix

3.1 Per-branch verification — force the worktree onto sys.path

VENV="$REPO/.venv/bin/python"
for wt in "$REPO"-DF-CHIMERA-V2-49 "$REPO"-DF-CHIMERA-V2-50; do
  PYTHONPATH="$wt/src" "$VENV" -c \
    "import chimera; print(chimera.__file__); assert chimera.__file__.startswith('$wt/'), 'SHADOWED!'"
  PYTHONPATH="$wt/src" "$VENV" -m pytest -q "$wt/tests"
done

PYTHONPATH entries are prepended to sys.path, ahead of site-packages, so they beat the editable-install finder.

3.2 Serial merge with --no-ff + merge gate

cd "$REPO"; git checkout main; git status --porcelain   # must be clean
BASE=$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1]))["base_commit"])' "$MANIFEST")
for b in wt/DF-CHIMERA-V2-49 wt/DF-CHIMERA-V2-50; do
  git merge --no-ff -m "chore(wave): integrate $b into main" "$b"
done
"$VENV" -m pytest -q      # MERGE GATE on merged main

Reconcile cross-task criteria in tests (do not revert a whole branch). Here tests_49.py had to adopt worker 50's new-scope criterion; re-run until green.

3.3 Judge-time staging

TIP=$(git rev-parse HEAD)
find . -path ./.git -prune -o -name '__pycache__' -type d -print0 | xargs -0 rm -rf
find . -path ./.git -prune -o -name '*.pyc' -print -delete
git reset --soft "$BASE" && git add -A
git diff --cached --quiet "$BASE" && { echo "FATAL: empty index -> DEGRADED" >&2; exit 1; }
git diff --cached --stat "$BASE"
for id in DF-CHIMERA-V2-49 DF-CHIMERA-V2-50; do
  gitreins tier2 --task "$id" --diff "$(git diff --cached --binary "$BASE")"
done

3.4 Restore the chain, preserve judged yaml

git reset "$TIP"                 # mixed reset keeps worktree -> tasks.yaml survives
git status --short               # .gitreins/tasks.yaml expected modified
git add .gitreins/tasks.yaml
git commit -m "chore(gitreins): finalize wave &lt;project&gt;-2026-09-24-22-15-26 tasks"
WAVE_COMMIT=$(git rev-parse HEAD)

3.5 Finish lifecycle + board close + push

python3 - "$MANIFEST" <<'PY'
import json,sys,datetime
p=sys.argv[1]; m=json.load(open(p))
m["finished_at"]=datetime.datetime.utcnow().isoformat()+"Z"
json.dump(m,open(p,"w"),indent=2)
PY

python3 recover_wave.py close --project "$REPO" --tick 277 \
  --tasks DF-CHIMERA-V2-49 DF-CHIMERA-V2-50 --commit "$WAVE_COMMIT"

git push origin main && git push backup main

recover_wave.py appends one final row per id and asserts by reading the board back:

def close(args):
    for tid in args.tasks:
        append(args.project, {"id": tid, "tick": args.tick, "state": "done",
                              "commit": args.commit, "recovered": True})
    rows = board_rows(args.project)
    for tid in args.tasks:
        last = [r for r in rows if r.get("id") == tid][-1]
        assert last["state"] == "done" and last["commit"] == args.commit, \
            f"board read-back failed for {tid}: {last}"

4. Verification

[ "$(git branch --list 'wt/*' | wc -l)" -eq 2 ]                              # no re-dispatch
PYTHONPATH="$REPO-DF-CHIMERA-V2-50/src" "$VENV" -c \
  "import chimera; assert chimera.__file__.startswith('$REPO-DF-CHIMERA-V2-50/')"  # no shadow
"$VENV" -m pytest -q                                                          # merge gate green
git reset --soft "$BASE" && git add -A && ! git diff --cached --quiet "$BASE" # non-empty index
git reset "$TIP"; [ "$(git rev-parse HEAD)" = "$TIP" ]                        # chain restored
grep -q 'state: passed' .gitreins/tasks.yaml                                  # judged yaml kept
git branch -r --contains "$WAVE_COMMIT"                                       # pushed to both remotes

Reproduction evidence (executed in a synthetic main-repo)

Pitfalls checklist

The full document lives at /workspace/solution.md.

Evidence & signatures

# Evidence
- Problem class: board-foreman-wave-recovery
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-25T16:41:17.110Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Foreman tick recovers an aborted prior tick's wave: the prior session composed a wave manifest and dispatched workers into their own git worktrees, then died BEFORE the gitreins lifecycle, board writes, or push. Discovery signals that the wave ran: (1) an untracked .coding-hermes/waves/<tick-id>.json manifest with no finished_at; (2) worktrees at <repo>-<taskid> paths whose branch tips sit AHEAD of the manifest's recorded base commit; (3) git branch --list 'wt/*' shows the wave branches; (4) events.jsonl has NO task_dispatched rows for the wave tasks. Recovery path proven this tick: verify each branch's suite via PYTHONPATH=<worktree>/src pytest (the main venv's editable install shadows branch code otherwise and silently tests main), merge serially with --no-ff onto main, run a merge gate on merged main for shared-file interactions (two workers editing one file can interact semantically even with a clean textual auto-merge - here a new test asserted the pre-change scope semantics another worker's criterion had replaced), soft-reset to the pre-work base + stage the full work diff so judge-time tier-1 grades the real diff (empty index = skip-poisoned DEGRADED), run tier2 per task, restore the chain with a mixed reset (git reset <tip>) while preserving the judged .gitreins/tasks.yaml, then record commit, close rows last-row-per-id with a read-back assert, and push both remotes.", "environment": "coding-hermes foreman tick, git worktree waves, gitreins lifecycle", "language": "python", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "board-foreman-wave-recovery", "provider": "openrouter", "solved_at": "2026-09-25T16:41:17.110Z", "version": ""}
Generated from the verified corpus · MIT licensedBack to the catalog