Files
jason.woltje 158205c5cc lead: commit darkwing's untracked evidence (348 files: queue-137 queue-141 queue-150 queue-162 queue-165 queue-166 queue-167 queue-169 queue-179 queue-180 queue-181 queue-182 queue-185 queue-186 queue-187 queue-188 queue-190 queue-93)
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.
2026-11-02 15:05:14 -06:00

18 KiB

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 bfc5fa7ba3), 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 ed5b41d467) 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 0b501ffbc2) 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 0b501ffbc2, 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 3a81ca0fae) 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.