Unclaimed files left in the canonical checkout. Inventory: canonical-cleanup-inventory.md (commit-as-evidence groups). detect-secrets scan clean on 2026-11-02: hits are sha256 pins, git object ids, test sentinels already in tracked tests and unit-file lines.
316 lines
18 KiB
Markdown
316 lines
18 KiB
Markdown
# Row 169 (P19 382, #1666): activate past c216f47db with the PM after the harness kind
|
|
|
|
Owner: Darkwing. Reviewer: Filbert. Gate owner: Sage.
|
|
Brief: `docs/plans/2026-10-19_pm-after-harness.md` (blob bfc5fa7ba36e), from mos-0848 (a) and (b).
|
|
Launch: sage-1014. Claimed at rev 1961.
|
|
Base: 4b738e2da, row 166's landing (sage-1070, mos-0852 point 5). Built first on row 166's
|
|
approved r1 candidate (manifest 4fc0613f...aced) applied on 56534526b; see "Rebase onto 4b738e2da".
|
|
Worktree: `~/darkwing-scratch/r169/wt` at 4b738e2da. The 56534526b worktree moved to
|
|
`~/darkwing-scratch/r169/wt-56534526b`.
|
|
|
|
## Files
|
|
|
|
| File | Change |
|
|
|---|---|
|
|
| `scripts/stack-release.sh` | items 1 to 4 and the `--plan` lines |
|
|
| `packages/cli/tests/stack-release.test.mjs` | T32 to T36; copies carry `kinds.mjs`, the business loader and `roles/`; every root gets a business file |
|
|
| `docs/TOOLS.md` | the "Pinned stack update" section only |
|
|
| this packet | |
|
|
|
|
## Items
|
|
|
|
**1. Phase 1, `activate ID BUSINESS --pm-after-harness`.** `parse_opts` takes the flag only for
|
|
`activate`, and `--pm` with it is a usage error (exit 4) in either order. `plugin_checks` runs
|
|
after `precheck` and before anything stops. In `--plan` it runs after `check_target`. It refuses:
|
|
|
|
- `pm-after-harness-target` when the target's `KINDS` has no `harness`;
|
|
- `pm-after-harness-set` when the target's loader reads a `plugins.harness`, `[]` included. The
|
|
message names `--pm`.
|
|
|
|
`flags_drift` needs a pointer with `pm` true for `--pm-after-harness`, as it compares `--pm`
|
|
with the pointer. `run_pm` sets the run's pm to 0 and pending to 1, so `install_stack` installs
|
|
the bus without `--pm` and the gate skips `pm_launched`. The pointer gets `pm: false` and
|
|
`pmPending: true`. The log's last line is `result ok` with `pm pending`, and the success line
|
|
names the next step.
|
|
|
|
**2. The pending state elsewhere.** `read_pointer` takes `pmPending` missing, or `true` beside
|
|
`pm: false`. Any other value, and `true` beside `pm: true`, is an unreadable pointer (exit 1), for
|
|
every verb. A plain `activate`, with or without `--pm` and with `--pm-after-harness`, refuses
|
|
`pm-pending` before anything stops. `adopt` keeps `already-adopted`. `status` prints
|
|
`pm: pending (activate --pm-after-harness); next: Jason's plugins set of harness, then
|
|
stack-release.sh pm-on BUSINESS`.
|
|
|
|
**3. `pm-on BUSINESS [--timeout S]`.** In order, before anything stops:
|
|
|
|
- `not-pending` unless the pointer is pending;
|
|
- `harness-unset` when the pointer copy's loader refuses the file, when it reads no `harness`,
|
|
or when `harness` isn't a non-empty list;
|
|
- `instance_busy`, `drop_in_override`, `read_units`, `no-claude-pin` and `flags_drift`.
|
|
|
|
Then it takes a new activation id, logs `started`, captures the rollback set, stops the bus
|
|
alone, installs it on the pointer's copy with `--pm` and the pointer's claude, starts it and runs
|
|
the full gate. A pass writes `pm: true` with no `pmPending` and logs `result ok pm on`. The
|
|
Console unit, the copy and the store are never touched.
|
|
|
|
A failure goes to `pm_on_fail`: stop the bus, `put_back` the captured phase-1 unit, start it and
|
|
rerun the gate without `pm_launched`. A pass there logs `pm-on-failed` with the code and why, keeps
|
|
the pointer pending and exits 1. A failure there stops both units, logs `pm-on-down` with both
|
|
codes and exits 2.
|
|
|
|
**4. Rollback and plug-in kinds.** `run_pm` gives a rollback from a pending pointer pm 1, so OLD
|
|
goes back with `--pm` and the gate checks `pm_launched`. `plugin_checks` runs for `activate` and
|
|
`rollback` and refuses `unknown-kind-for-target` when the business file's `plugins` has a key
|
|
the target's `KINDS` lacks. It names every such kind. On a rollback the message adds the fix:
|
|
set each to `null` with `plugins set` through the live bus.
|
|
|
|
**5. `--plan`.** In plan mode `check_refuse` collects refusals instead of exiting. `plan` prints
|
|
the pending pointer line, the pending-start line for `--pm-after-harness`, then
|
|
`refuses CODE: message` for each refusal, and exits 3 when there is any.
|
|
|
|
## Design notes
|
|
|
|
- `pmPending` is a separate key because `read_pointer` refuses a non-boolean `pm`. True is only
|
|
valid beside `pm: false`, so a pointer can't claim both a live and a pending PM.
|
|
- One node helper (`PLUGINS_JS`) imports the copy's own `kinds.mjs`, and for `harness` its
|
|
`business.mjs` loader with the copy's `roles/`. The unknown-kind check is a plain JSON read of
|
|
`plugins`, because OLD's loader refuses the first unknown kind and the brief wants every kind
|
|
named. A copy without `kinds.mjs` fails the import and refuses `business-unreadable`. No
|
|
fallback.
|
|
- `pm-after-harness-set` treats `[]` as set: the file already has the kind, so the operator
|
|
should see that rather than a phase 1 that `pm-on` then refuses. `pm-on` needs a non-empty
|
|
list, since `[]` switches no harness on.
|
|
- `pm-on` has its own activation id. `from_log` reads only `adopt`, `activate` and `rollback`
|
|
lines, so a `pm-on` line is never a rollback target or a backup source. It reuses `capture` and `put_back`, so the phase-1 unit is restored
|
|
byte for byte.
|
|
- `flags_drift` needs no pending clause. A pending pointer has `pm` false and its unit has no
|
|
`--pm`, so they match. `activate` refuses `pm-pending` before `flags_drift` and `adopt` refuses
|
|
`already-adopted`, so only `pm-on` and `rollback` reach it with a pending pointer.
|
|
- A plain `activate --plan` from pending prints the `--pm` it would install (the run pm is 1) and
|
|
then `refuses pm-pending`.
|
|
- Tests use the real loader and `kinds.mjs`. Copies older than c216f47db get `kinds.mjs` with
|
|
`harness` renamed, so `A` and `B` lack the kind and `C1` and `C` have it.
|
|
|
|
## Tests
|
|
|
|
T32 to T36 in `packages/cli/tests/stack-release.test.mjs`. Each refusal test checks exit 3, the
|
|
message, an unchanged fingerprint, no stand-in stop or start, and one log line.
|
|
|
|
| Test | Brief item | Covers |
|
|
|---|---|---|
|
|
| T32 | 1 | usage errors, `pm-after-harness-target`, `pm-after-harness-set` (`["claude-code"]` and `[]`), pm-less pointer drift, phase 1 pass |
|
|
| T33 | 2 | `read_pointer` bad `pmPending` values, `pm-pending` for three `activate` forms, `already-adopted`, the `status` line |
|
|
| T34 | 3 | `not-pending`, `harness-unset` (none, `[]`), pass, `pm-on-failed` (exit 1), `pm-on-down` (exit 2) |
|
|
| T35 | 4 | rollback from pending with `--pm`, `unknown-kind-for-target` on rollback then pass after the reset, on activate naming each kind |
|
|
| T36 | 5 | `--plan` pending lines and refusals |
|
|
|
|
## Mutants
|
|
|
|
`mutants.py` (copy in this packet, run log `mutants-run.log`), run with `python3 -I`. Each
|
|
mutant edits one check and must fail the named test.
|
|
|
|
| Id | Edit | Fails (node's fail count) |
|
|
|---|---|---|
|
|
| 1a | `pm-after-harness-target` removed | T32 (2 failed) |
|
|
| 1b | `pm-after-harness-set` removed | T32 (2 failed) |
|
|
| 1c | `--pm` with `--pm-after-harness` allowed | T32 (2 failed) |
|
|
| 1d | `--pm-after-harness` accepts a pointer without pm | T32 (2 failed) |
|
|
| 1e | phase 1 installs and gates `--pm` | T32 (2 failed) |
|
|
| 1f | phase 1 writes no `pmPending` | T32 (2 failed) |
|
|
| 2a | `read_pointer` takes any `pmPending` | T33 (2 failed) |
|
|
| 2b | `read_pointer` takes `pmPending` beside `pm` true | T33 (2 failed) |
|
|
| 2c | `pm-pending` removed | T33 (2 failed) |
|
|
| 2d | `status` prints no pending line | T33 (2 failed) |
|
|
| 3a | `not-pending` removed | T34 (2 failed) |
|
|
| 3b | `harness-unset` for a missing harness removed | T34 (2 failed) |
|
|
| 3c | `harness-unset` for an empty list removed | T34 (2 failed) |
|
|
| 3d | `pm-on` installs without `--pm` | T34 (2 failed) |
|
|
| 3e | `pm-on` gates without `pm_launched` | T34 (3 failed) |
|
|
| 3f | a failed `pm-on` keeps the `--pm` unit | T34 (3 failed) |
|
|
| 3g | a failed `pm-on` skips the phase-1 gate | T34 (2 failed) |
|
|
| 4a | rollback from pending installs without `--pm` | T35 (2 failed) |
|
|
| 4b | `unknown-kind-for-target` removed | T35 (3 failed) |
|
|
| 4c | `unknown-kind-for-target` names only the first kind | T35 (2 failed) |
|
|
| 5a | `--plan` prints no pending pointer line | T36 (1 failed) |
|
|
| 5b | `--plan` lists no refusal | T36 (1 failed) |
|
|
| 5c | `--plan` skips the plug-in checks | T36 (1 failed) |
|
|
|
|
## Rebase onto 4b738e2da (sage-1070)
|
|
|
|
Row 166 landed as 4b738e2da. At 4b738e2da, `scripts/stack-release.sh` (blob ed5b41d4677e) and
|
|
`packages/cli/tests/stack-release.test.mjs` are byte for byte the files in row 166's approved
|
|
manifest, so the base for those two did not move. `docs/TOOLS.md` changed once between 56534526b
|
|
and 4b738e2da: row 134 (3a600cf5b) rewrote two lines near line 685, outside the "Pinned stack
|
|
update" section.
|
|
|
|
`git apply` of the old `build.patch` on a clean 4b738e2da worktree: every hunk applied, no fuzz,
|
|
no offset. The one thing that didn't apply as written is the `scripts/stack-release.sh` header
|
|
`old mode 100644` / `new mode 100755`. The old patch was a `git diff --no-index` against a file
|
|
copy of 166's tree, where the file was 0644 on disk. At 4b738e2da the file is already 100755, so
|
|
git warned "has type 100755, expected 100644" and changed no mode. The new patch has no mode lines.
|
|
|
|
After the apply, `scripts/stack-release.sh` (blob 0b501ffbc247) and the test file are identical to
|
|
the 56534526b candidate. `docs/TOOLS.md` carries the same added and removed lines at the same hunk
|
|
line numbers, on a base that includes row 134's change, so its sha256 is new.
|
|
|
|
The patch-ids (`git patch-id --stable`): old 5dca3351b279, new 17a90c4ad795. With the two mode
|
|
lines removed, the old patch also gives 17a90c4ad795, so the content of the change didn't move.
|
|
|
|
## Results on 4b738e2da
|
|
|
|
On Sage's go (sage-1070), behind `busy.sh`, in the 4b738e2da worktree,
|
|
`TMPDIR=~/darkwing-scratch/r169/tmp`, spec reporter, sums from awk over the logs:
|
|
|
|
| File | Tests | Pass | Fail |
|
|
|---|---|---|---|
|
|
| `packages/cli/tests/stack-release.test.mjs` | 93 | 93 | 0 |
|
|
| `packages/cli/tests/stack-copy.test.mjs` | 12 | 12 | 0 |
|
|
| total | 105 | 105 | 0 |
|
|
|
|
T32 to T36 are among the 93 and all passed on the first run.
|
|
|
|
Mutants, `python3 -I mutants.py` right after (unchanged file; DRY=1 found every anchor first):
|
|
CONTROL passed (T32 6, T33 3, T34 6, T35 4, T36 1 subtests) and all 23 mutants were killed, each
|
|
failing the test in the table, the same tests and fail counts as on 56534526b. The run restored
|
|
`scripts/stack-release.sh` to blob 0b501ffbc247, and the sweep after found 0 processes. Log:
|
|
`mutants-run.log`.
|
|
|
|
## Results on 56534526b (superseded)
|
|
|
|
On Sage's slot go (sage-1028), in the worktree, `TMPDIR=~/darkwing-scratch/r169/tmp`, spec
|
|
reporter, sums from awk over the logs:
|
|
|
|
| File | Tests | Pass | Fail |
|
|
|---|---|---|---|
|
|
| `packages/cli/tests/stack-release.test.mjs` | 93 | 93 | 0 |
|
|
| `packages/cli/tests/stack-copy.test.mjs` | 12 | 12 | 0 |
|
|
| total | 105 | 105 | 0 |
|
|
|
|
T32 to T36 are among the 93 and all passed on the first run.
|
|
|
|
Mutants, run with `python3 -I mutants.py` right after: CONTROL passed (T32 6, T33 3, T34 6, T35 4,
|
|
T36 1 subtests) and all 23 mutants were killed, each failing the test in the table. The run
|
|
restored `scripts/stack-release.sh` to its pristine blob, `0b501ffbc247`, and the sweep after
|
|
found 0 processes.
|
|
|
|
Only `stack-release.test.mjs` reads `scripts/stack-release.sh` or `docs/TOOLS.md` among
|
|
`packages/*/tests` and `scripts/test-*.sh`, and it pins no hash.
|
|
|
|
## Candidate
|
|
|
|
- `build.patch`: `git diff 4b738e2da` of the three files in the worktree. Applied to a clean
|
|
4b738e2da tree it gives the manifest's three sha256 (checked with `sha256sum -c`).
|
|
patch-id 17a90c4ad79550af4f2910199338dcdd9bb20dc5.
|
|
- `candidate-manifest.sha256`: the three files' sha256, digest
|
|
c6aaac1ffa80a5c82158700113ab92f103630df5043ddd5c5164ab936e8f54af.
|
|
- Superseded, kept: `build-56534526b.patch` (sha256 89c2bd5863de),
|
|
`candidate-manifest-56534526b.sha256` (digest 0b304705d58a), `mutants-run-56534526b.log`.
|
|
|
|
While renaming the old files I overwrote them with the new ones (`git mv -k` skips untracked
|
|
files and exits 0). I rebuilt the old patch and manifest from the 56534526b worktree with the
|
|
same commands, and both match the sha256 recorded when they were first written. The old mutants
|
|
log is the run's own copy from `~/darkwing-scratch/r169/out-56534526b/`.
|
|
|
|
## Rehearsal
|
|
|
|
Run 2026-10-19T22:52:43Z to 22:53:27Z (attempt 5), scratch only, on Sage's go (sage-1075) with
|
|
Mos's conditions (mos-0878, sage-1077). Files are in `rehearsal/`. Every file's sha256 is in
|
|
`rehearsal/SHA256SUMS`, whose own sha256 is
|
|
a36c89eb1ec3cfe43ceabba13e19739611d670a31e3d0019bb8ec6261ee9b031.
|
|
|
|
- Root `~/darkwing-scratch/r169/reh`, outside `~/.mosaic-dev`. It has a `git clone --shared`
|
|
of the canonical checkout at 4b738e2da with this candidate applied, so
|
|
`sha256sum -c candidate-manifest.sha256` passes in the clone. `refactor` in the clone is
|
|
d195d171c. OLD is ca15d73e9 and NEW is d195d171c, both made fresh with `stack-copy.sh copy`.
|
|
- The business file is the committed example (blob 3a81ca0faea6) with
|
|
`{"usage-source":["transcript"],"channel":["cli","webui"]}` and no `harness`
|
|
(`scratch-business-B.json`, sha256 629931c5...). `scratch-business-H.json` adds
|
|
`"harness":["claude-code"]` for step 1.
|
|
- Every call runs under `env -i` with HOME, PATH, TMPDIR, LANG, MOSAIC_CONFIG, XDG_* and SD_*
|
|
only. There is no credential and no pi variable, and the scratch HOME is empty.
|
|
- The units are stand-ins (`harness/systemctl`). No live bus or Console unit was touched.
|
|
|
|
### Live store
|
|
|
|
`stat -c '%n %Y'` before step 1 and after step 6, from `rehearsal-169-live-store-stat.txt`:
|
|
|
|
```
|
|
# before prep, 2026-10-19T22:52:43Z
|
|
/home/jwoltje/.mosaic-dev/bus/bus.sqlite 1792446324
|
|
/home/jwoltje/.mosaic-dev/bus 1792446325
|
|
# after teardown, 2026-10-19T22:53:27Z
|
|
/home/jwoltje/.mosaic-dev/bus/bus.sqlite 1792446324
|
|
/home/jwoltje/.mosaic-dev/bus 1792446325
|
|
```
|
|
|
|
They match. They also match before and after each of attempts 1 to 4.
|
|
|
|
### Result
|
|
|
|
The transcript (`rehearsal-169.txt`) has 29 parts: 23 exit 0, 2 exit 1 and 4 exit 3. Each
|
|
nonzero exit is the refusal its part expects:
|
|
|
|
| Brief step | Parts | Outcome |
|
|
|---|---|---|
|
|
| 1 | 1a, 1b, 1c | OLD on H refuses `unknown kind "harness"`, exit 1. NEW on H and OLD on B pass |
|
|
| 2 | 2a, 2b, 2c | `activate --pm-after-harness --plan`, then phase 1. The pointer has `pmPending: true` and the bus unit has no `--pm` |
|
|
| 6, from pending | 6a-1 to 6a-3 | rollback from pending installs OLD with `--pm` and passes the gate |
|
|
| 4, before 3 | 2d, 4-pre | a fresh phase 1, then `pm-on` refuses `harness-unset`, exit 3 |
|
|
| 3 | 3 | `harness` `["claude-code"]` set (see the stand-in below) |
|
|
| 4 | 4, 4a, 4b | `pm-on` passes. The pointer has `pm: true` and the unit has `--pm`. The PM launch is emulated |
|
|
| 6, after the set | 6b-1 to 6b-4 | `rollback --plan` and `rollback` refuse `unknown-kind-for-target` by name before any stop (exit 3). After `harness` is set to null, rollback passes |
|
|
| 5 | 5a, 5b, 5-route, 5c to 5e | a fresh phase 1 with `harness` `["pi"]`, then a real `pm-on`. The real launchPm refuses `no-route` and the gate fails `units-inactive`. pm_on_fail puts the phase-1 unit back and the phase-1 gate passes. The run exits 1 with the pointer still pending, and `launches list` is `[]` |
|
|
| 6, from step 5 | 6c-1 to 6c-4 | rollback refuses `unknown-kind-for-target`. After `harness` is set to null, OLD is back with `--pm` |
|
|
|
|
Step 5 started no pi process. Part 5c lists the processes under the root: the bus and the
|
|
Console stand-in, and 0 named pi. The 5-route guard ran before the real `pm-on`. It passes only a
|
|
chief route that `check()` refuses before the loader: `no-route`, or a claude-code primary with
|
|
no fallback, which `harness-off` refuses with `["pi"]`. It found `no-route`.
|
|
|
|
Teardown stopped both units, port 47393 closed, and 0 processes were left. The Docker network
|
|
list (`docker-networks-before.txt`, `-after.txt`) has the same 15 names before and after. My
|
|
sweep afterwards found 0 processes.
|
|
|
|
### Stand-ins and findings
|
|
|
|
1. **Step 3 is a stand-in.** The bus refuses the real `mosaic plugins` read and set
|
|
`human-required`. `verifyHuman` (`packages/bus/src/human.mjs`) walks the caller's parents
|
|
and refuses at this agent's claude process. That is the agent check working, and I didn't
|
|
try to get past it. `harness/hcli.sh` runs the real CLI first and records the refusal
|
|
(exit 3). Then `harness/hset.mjs` calls the writer the NEW host builds for `plugins.set`
|
|
(`packages/bus/src/runtime.mjs:93`: `pluginSettings` with the same dir, rolesDir and
|
|
`hasModule`) with actor `standin.darkwing`. So the file write, the revision check and the
|
|
module check are the host's own. Only the bus's human proof is skipped. Steps 6b-3, 5b and
|
|
6c-2 use the same stand-in. In the window, the set is Jason's, in his own shell.
|
|
2. **`scripts/mosaic` at d195d171c doesn't dispatch `plugins`.** `scripts/mosaic plugins`
|
|
prints the seat usage text, so the stand-in calls `packages/cli/src/cli.mjs plugins`.
|
|
This matters for any window step that says `mosaic plugins set`. TOOLS.md and the window
|
|
plan should name the command that works on the pinned copy.
|
|
3. **The example's chief has no `harness` var.** Since row 65, `check()` takes the route's
|
|
harness from the instance vars only (`routeOf`, `packages/cli/src/launcher.mjs`). With the
|
|
briefed file, every real PM launch at NEW refuses `no-route` at `routeOf("primary")`. That
|
|
happens before readSkills, the harness-off check and the loader, whatever `plugins.harness`
|
|
holds. So step 5's refusal is `no-route`, not `harness-off`. pm_on_fail's restore path
|
|
doesn't depend on the code, and the transcript shows it. Step 4 is emulated (a real
|
|
claude-code launch needs a session credential in preflight), and a real one on this file
|
|
would also refuse `no-route`. Whether Jason's live file sets the chief's `harness` var is
|
|
for the window plan's checks. I didn't read that file.
|
|
4. **The scratch human id is the example's `"jason"`.** It appears in the refused launch
|
|
event in part 5c (`"by":"jason"`). It is a name in a scratch business, and nothing ran as
|
|
Jason or in a human shell.
|
|
|
|
### Attempts
|
|
|
|
Attempts 1 to 4 stopped on harness bugs, not on the candidate. Each transcript is kept as
|
|
`attemptN-rehearsal-169.txt`, and each attempt's teardown left 0 processes, the live stat
|
|
matched, and the networks were unchanged.
|
|
|
|
1. Step 3: `hcli.sh` read `.host.path` from `bus status --json`, which has no such field.
|
|
2. Step 3: `scripts/mosaic plugins` isn't dispatched (finding 2).
|
|
3. Step 3: the real `mosaic plugins` is refused `human-required` (finding 1).
|
|
4. 5-route: my probe called `resolveInstance` with `system: {}` instead of the
|
|
`systemVars(config)` that `check()` uses. It threw, so the guard stopped the run before 5c.
|
|
That probe found finding 3.
|
|
|
|
The candidate files didn't change, so the digest stays
|
|
c6aaac1ffa80a5c82158700113ab92f103630df5043ddd5c5164ab936e8f54af.
|