fix(trekexecute): make parallel-mode plan delivery portable and gitignore-tolerant

Two independent defects took down a real voyage on macOS today
(llm-ingestion-okf, wave 1, zero steps executed). Both paths led to the same
end state: the worktree had no plan.

1. Phase 2.6 Step 2a' derived the project relpath with
   `realpath --relative-to`, which is GNU coreutils only. On BSD realpath
   (macOS default) the substitution fails silently: PROJECT_REL ends up
   empty, `mkdir -p "$wt/"` and `cp ... "$wt//"` both succeed, and
   brief.md/plan.md land at the worktree root where no child session looks.
   The portable form existed only as a prose note saying an operator "may
   substitute" it — the note was prose, the code was what ran. The block now
   uses `python3 -c os.path.relpath` (stdlib), resolves both operands with
   bare realpath (identical on BSD and GNU, and correct across the macOS
   /var -> /private/var symlink), and aborts loudly when the relpath is
   empty or escapes the repo instead of dropping files at the root. The
   coreutils note is deleted rather than extended: two forms means the next
   reader picks, and that pick is what failed.

   Measured on this machine 2026-08-31 (Intel Mac, no coreutils):
   `realpath --relative-to=...` -> `realpath: illegal option -- -`;
   bare `realpath /Users/ktg/.claude` -> correct path, exit 0.

2. Phase 2.55 Check 2 ran `git add {plan-path}` unconditionally. When the
   project directory is gitignored (`.claude/projects/` is tool-managed and
   local-only — normal, not exotic) the add refuses and the plan never
   reaches HEAD. Check 2 now probes with `git check-ignore` and branches:
   tracked -> nothing; untracked -> add + commit as before; ignored -> step
   aside and let Step 2a' be the delivery path. `git add -f` was rejected as
   the fix: forcing operator-local artifacts into history publishes them to
   whatever remote the repo pushes to. A `check-ignore` exit code other than
   0 or 1 is fatal and stops execution — a probe that failed is not a probe
   that answered "not ignored".

Also fixed in the same block: `[ -d research ] && cp -r ...` as the last
statement of the loop body made a project without research/ exit the wave
non-zero.

TDD, Iron Law: tests/commands/trekexecute-parallel-portability.test.mjs
extracts both shell blocks from commands/trekexecute.md by grep-able anchor
and executes them, so the tests bind to what an agent actually copies. The
assertions check FILE PLACEMENT, not exit status — the whole defect is that
the broken form exits 0. Controls, all present before the fix: a
known-positive BSD-realpath stub (bare path resolves, GNU flag rejected with
`illegal option`), a negative control running the old GNU form under that
stub and asserting plan.md lands at the worktree root, a known-positive
Check 2 arm where a non-ignored plan is still added and committed, and a
stubbed fatal `git check-ignore` (128). Red first: 7 failed, 2 passed (the
two controls). Green after: 9/9.

Suite 1013 (1011/0/2) -> 1022 (1020/0/2). No version bump, no release.

Order: 20260831T214411Z-941965142-from-.claude (from .claude).

Co-Authored-By: Claude <claude-opus-5>
This commit is contained in:
Kjell Tore Guttormsen 2026-08-31 23:53:55 +02:00
commit 63e78c5ec0
2 changed files with 389 additions and 17 deletions

View file

@ -419,19 +419,47 @@ to stderr but do NOT block the stop; `progress.json` is still authoritative.
`status: stopped`) so the next-session producer-mismatch check has both
candidates available. Use the same combined ESM block pattern as Phase 8.
### Check 2 — Plan file is tracked by git
### Check 2 — Plan file reaches every worktree
Run `git ls-files --error-unmatch {plan-path} 2>/dev/null`. If the plan file is
untracked (exit code != 0):
Worktrees are created from HEAD, so tracking the plan file is the cheapest way
to make it visible in each one. But the project directory may be **gitignored**
`.claude/projects/` is tool-managed and local-only, and a repo that ignores it
is normal, not exotic. `git add -f` is **not** the answer there: it would push
operator-local artifacts into history, and into whatever remote the repo
publishes to. When the plan file is ignored, Phase 2.6 Step 2a' (which copies
brief/plan/research into each worktree) is the delivery path, and this check
must step aside instead of failing.
Set `PLAN_PATH` to the plan path, then run:
```bash
git add {plan-path}
git commit -m "chore: track plan file for parallel execution"
if git ls-files --error-unmatch "$PLAN_PATH" >/dev/null 2>&1; then
PLAN_TRACKING="tracked"
else
git check-ignore -q "$PLAN_PATH"
case "$?" in
0) PLAN_TRACKING="ignored" ;;
1) PLAN_TRACKING="untracked" ;;
*) echo "Error: git check-ignore failed on $PLAN_PATH — a fatal probe is not an answer about ignore status." >&2
exit 1 ;;
esac
fi
if [ "$PLAN_TRACKING" = "untracked" ]; then
git add "$PLAN_PATH"
git commit -m "chore: track plan file for parallel execution"
fi
```
Report: `Plan file committed for worktree visibility.`
Report by outcome:
This ensures every worktree created from HEAD will have the plan file.
| `PLAN_TRACKING` | Report |
|---|---|
| `tracked` | `Plan file already tracked.` |
| `untracked` | `Plan file committed for worktree visibility.` |
| `ignored` | `Plan file is gitignored — not forced into history. Step 2a' copies it into each worktree.` |
Any other `git check-ignore` exit code is fatal and stops execution: a probe
that failed is not a probe that answered "not ignored".
### Check 3 — Scope fence overlap validation
@ -478,7 +506,7 @@ If cleanup fails, report the manual commands and stop.
After all 4 checks pass:
```
Pre-flight: PASS (clean tree, plan tracked, no overlaps, no stale worktrees)
Pre-flight: PASS (clean tree, plan reaches worktrees, no overlaps, no stale worktrees)
```
## Phase 2.6 — Multi-session orchestration (worktree-isolated)
@ -604,27 +632,45 @@ Insert this block AFTER the worktree-creation loop and BEFORE wave dispatch
```bash
PROJECT_SOURCE="$(realpath "${PROJECT_DIR}")"
REPO_ROOT_REAL="$(realpath "${REPO_ROOT}")"
# Compute destination relpath: PROJECT_DIR relative to REPO_ROOT.
# This makes $wt/$PROJECT_REL valid regardless of whether the operator
# passed --project as relative (.claude/projects/...) or absolute.
PROJECT_REL="$(realpath --relative-to="$REPO_ROOT" "$PROJECT_SOURCE")"
# python3 + os.path.relpath is stdlib and portable — see the note below the
# block for why no realpath flag may be used here.
PROJECT_REL="$(python3 -c 'import os.path,sys; print(os.path.relpath(sys.argv[1], sys.argv[2]))' "$PROJECT_SOURCE" "$REPO_ROOT_REAL")"
case "$PROJECT_REL" in
""|..*)
echo "Error: cannot derive a project relpath inside the repo ($PROJECT_SOURCE vs $REPO_ROOT_REAL)." >&2
exit 1 ;;
esac
for wt in "$WORKTREE_DIR"/session-*; do
[ -d "$wt" ] || continue
mkdir -p "$wt/$PROJECT_REL"
cp "$PROJECT_SOURCE"/brief.md "$wt/$PROJECT_REL/"
cp "$PROJECT_SOURCE"/plan.md "$wt/$PROJECT_REL/"
[ -d "$PROJECT_SOURCE/research" ] && \
if [ -d "$PROJECT_SOURCE/research" ]; then
cp -r "$PROJECT_SOURCE/research" "$wt/$PROJECT_REL/"
fi
done
```
Note: `realpath --relative-to` is GNU coreutils. macOS users without
`coreutils` (Homebrew `brew install coreutils` provides `grealpath`) may
substitute a Python fallback:
`python3 -c "import os.path,sys; print(os.path.relpath(sys.argv[1], sys.argv[2]))" "$PROJECT_SOURCE" "$REPO_ROOT"`.
Do not "improve" the relpath line into `realpath --relative-to=...`. That flag
is GNU coreutils only; BSD `realpath` (the macOS default) rejects it, and the
failure is **silent** — the command substitution leaves `PROJECT_REL` empty, so
`mkdir -p "$wt/"` and `cp ... "$wt//"` both succeed and drop `brief.md`/`plan.md`
at the worktree root, where no child session looks for them. Measured on an
Intel Mac 2026-08-31: `realpath --relative-to=... ` → `realpath: illegal option
-- -`, while bare `realpath <path>` works; a whole wave ran with zero steps
executed. Both `realpath` calls above are bare path resolution, which BSD and
GNU handle identically; resolving both operands before the relpath is what keeps
it correct when one side goes through a symlink (macOS `/var``/private/var`).
Failure mode: any `cp` failure exits the wave non-zero; reported via Step 4
cleanup. Source: brief Constraint 2.
Failure modes: an underivable relpath (empty, or outside the repo) aborts
before anything is copied — better a loud stop than files delivered where no
session reads them; any `cp` failure exits the wave non-zero, reported via
Step 4 cleanup. A project without `research/` is not a failure.
Source: brief Constraint 2.
**2b. Launch sessions in this wave (each in its own worktree):**