8.1 KiB
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()returnssubmodulewhen.git/modulesexists (lstat, any kind of entry), after the walk and before the commondir check. A clone never has one;git submodulemakes it.update()runsgit ls-files --stage -zright afterunsafe(). A failure keeps the clone asstatus-failed. Any entry with mode160000, a gitlink, keeps it assubmodule. This runs beforerootBranch,symbolic-refandstatus.ls-filesreads only the index, so no filter, hook or submodule runs there.statuspasses--ignore-submodules=allinstead of=none.SAFEgains-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.