Files
stack/agents/darkwing/work/queue-91/r3/build.md
T

182 lines
8.1 KiB
Markdown

# Row 91, a project clone in each role workspace, build round 3 (Darkwing)
Issue #1585. Brief: `docs/plans/2026-10-12_pm-task-flow.md`, "A project
checkout in each role workspace", with Sage's decision 126 (brief blob
`844832c3`). Reviewer: Dewey (decision 130). Gate as revs 881 to 883 set
it.
Rounds 1 and 2 are in `agents/darkwing/work/queue-91/` and `r2/`, with
request comments 28067 and 28087. Round 2's `build.md` describes the row
as a whole: the outcomes, decision 126's three additions, the launcher
and prompt changes. That all still holds. This file covers what round 3
changes.
## Round 3
Round 3 closes the escape Filbert found in round 2. `finding.md` here
says what ran, where, and which guard it got past. In short, `git status
--ignore-submodules=none` started a second git inside a submodule the
session had made in its clone, and that git ran a clean filter from the
submodule's own config, on the host. None of round 2's checks read a
submodule's config.
Sage approved the approach (decision 130 thread): a clone with a
submodule is kept, and no host git recurses into one.
## Candidate and base
Base `2b2a9e14`. I built round 3 on `fbbcf0b7`. Before opening the round
I checked `fbbcf0b7..HEAD` on the manifest paths: row 88's landing
(`2b2a9e14`) changed `packages/cli/README.md`, one of them, in the
command list near the top, not in this row's lines. So I moved round 3 to
`2b2a9e14`. The `fbbcf0b7` patch applies there with `git apply --index`
and no fuzz, and the tests, mutants and gate below all ran on
`2b2a9e14`.
`build.patch` is `git diff 2b2a9e14` of the same eight paths as round 2,
the two new files included. Apply it with `git apply --index` to a
detached worktree of `2b2a9e14`; `sha256sum -c candidate-manifest.sha256`
then gives 8/8 OK.
Against round 2's manifest (`6b58085b`), three lines change:
`packages/seat/src/checkout.mjs`, `packages/seat/tests/checkout.test.mjs`
and `packages/seat/README.md` for this round's edits, and
`packages/cli/README.md` for row 88's lines only. The launcher, its test
and the two bundle files are byte for byte round 2's.
## What changed
`packages/seat/src/checkout.mjs`:
- `unsafe()` returns `submodule` when `.git/modules` exists (lstat, any
kind of entry), after the walk and before the commondir check. A clone
never has one; `git submodule` makes it.
- `update()` runs `git ls-files --stage -z` right after `unsafe()`. A
failure keeps the clone as `status-failed`. Any entry with mode
`160000`, a gitlink, keeps it as `submodule`. This runs before
`rootBranch`, `symbolic-ref` and `status`. `ls-files` reads only the
index, so no filter, hook or submodule runs there.
- `status` passes `--ignore-submodules=all` instead of `=none`.
- `SAFE` gains `-c submodule.recurse=false`, so no git call recurses.
- The header comment says what the checks cover and why a submodule is
kept.
`packages/seat/README.md`: the reasons table gains `submodule`;
`status-failed` names `git ls-files`; one paragraph after the guards says
why the host never recurses into a submodule.
`packages/seat/tests/checkout.test.mjs`: six new cases in "a clone the
session rigged is kept, and git runs nothing and writes nothing outside
it". Each builds a repository at `<clone>/sub` whose own config names a
clean filter that runs the bait script, with a same-size edit to the
filtered file so any status inside it must run the filter.
| case | reason |
|---|---|
| a gitlink staged, no .gitmodules | `submodule` |
| a gitlink and .gitmodules staged | `submodule` |
| a gitlink to a repository with its own gitlink (nested, filter in the inner one only) | `submodule` |
| a gitlink and .gitmodules committed | `submodule` |
| a gitlink and submodule.active in config | `config-changed` |
| a .git/modules with no gitlink | `submodule` |
As for every case in that test, each one checks the outcome, that the
bait's marker file is absent, and that the bait repository's fingerprint
is unchanged. Sage asked for "a bare gitlink, a gitlink with config, and a
populated .git/modules with no gitlink": those are the first, the fifth
and the sixth.
## Mutants
`mutants.sh` is round 1's 31 mutants plus seven for round 3, with the
same `mut.mjs`. All 38 ran on the round 3 candidate on `2b2a9e14`,
from about 13:09Z to 13:10:48Z, `mutants.txt`: 32 killed, 6 survived. Round 1's
results are unchanged (the same three survivors, explained in round 2's
`build.md`).
| mutant | what it does | result |
|---|---|---|
| gitlink-check-off | drops the `160000` check | killed, the four gitlink cases |
| modules-check-off | drops the `.git/modules` return | killed, "a .git/modules with no gitlink" |
| status-recurses | `--ignore-submodules=none` | survived |
| safe-no-submodule-recurse | drops `-c submodule.recurse=false` | survived |
| lsfiles-failure-passes | drops the `ls-files` failure return | survived |
| recursion-unguarded | gitlink check off and `=none` | killed, the four gitlink cases, reason `dirty` |
| gitlink-check-after-status | gitlink check moved after a `=none` status | killed, the four gitlink cases, on "nothing the session wrote ran" |
The last one is the order test Sage asked for: with the check after the
status, the outcome is still `submodule` but the marker is there, so only
the order of the two calls stops the escape, and the test sees it.
The three survivors are second-line guards, expected to survive.
`status-recurses` and `safe-no-submodule-recurse` can't matter while the
gitlink check runs first, since `status` then never sees a gitlink.
`lsfiles-failure-passes` needs an index `ls-files` can't read; a later
`status` on that index fails too, so the clone is still kept as
`status-failed`.
One thing the logs don't show: under `gitlink-check-off` and
`recursion-unguarded` the outcome assertion fails first, so the marker
assertion never runs. Whether `--ignore-submodules=all` alone stops the
filter is therefore not tested here. I didn't swap the two assertions,
because that needs another test run and the slot is released. It would
make each escape fail on the marker first, if Dewey wants it.
## Gate
`gate.sh`, `gate-summary.txt`. 13:04:52Z to 13:07:13Z on 2026-10-13, in
the slot Sage cleared for me at 12:56Z, after the busy check. Base
`2b2a9e14` with this `build.patch` staged; the script checks the
manifest 8/8 OK first. Sequential in a clean worktree with
`node_modules` linked and `DOCKER_HOST=unix:///nonexistent`. All 14
rc=0:
| suite | result |
|---|---|
| seat `node --test tests/*.test.mjs` | 89 pass, 0 fail, 0 skipped |
| cli | 131 pass, 0 fail, 0 skipped |
| bus | 115 pass, 0 fail, 0 skipped |
| harness | 67 pass, 0 fail, 0 skipped |
| tasks | 89 pass, 0 fail, 0 skipped |
| `test-auth.sh` | 15/0 |
| `test-conductor.sh` | 17/0 |
| `test-config.sh` | 24/0 |
| `test-discord.sh` | 66/0 |
| `test-extension-package.sh` | 18/0 |
| `test-foundation.sh` | 44/0 |
| `test-queue.sh` | 27/0 |
| `test-release.sh` | 4/0 |
| `test-task.sh` | 26/0 |
Seat is round 2's 83 plus the six new cases. Cli (+6) and bus (+4) are
row 88's tests on the new base. The Docker-off skips are the same as in
round 2's `build.md`. No Docker network was made, and no process was left
with a cwd in my worktrees.
## Limit
A project whose root has submodules gets a clone with gitlinks in its
index from the first `git clone` (no `--recurse-submodules`). From then
on every launch keeps it as `submodule` and never fast-forwards it. That
fails closed. The one project in the `mosaic-stack` business file today, `stack` (this repository), has no `.gitmodules`. If
one does later, it needs its own rule, not a looser check.
## Found, not fixed
Round 2's note on `recover()` in the launcher still stands.
## Secrets scan
I scanned the packet for token, secret, password, api key and private key
markers. The matches are round 2's test stand-ins in `build.patch`
(`CLAUDE_CODE_OAUTH_TOKEN = "stand-in"`, the fake
`bot:[email protected]` origin and `http.extraHeader` in the "no
credential or forge remote" test). `finding.md` holds no command strings
from the probe and no probe output.
## Correction (2026-10-13T13:14:59Z)
Every "row 88" above should read row 94 (#1588, messages). `2b2a9e14` is
row 94's landing; row 88 has not landed. Sage caught it. The rebase, the
manifest and the gate are unchanged.