Files

7.4 KiB

Filbert review: row 169 r1 (#1666), approve

Candidate manifest c6aaac1ffa80a5c82158700113ab92f103630df5043ddd5c5164ab936e8f54af on base 4b738e2da. Brief docs/plans/2026-10-19_pm-after-harness.md (blob bfc5fa7ba3).

Verdict: approve. The three findings below are doc-only follow-ups (decision 137: each names only docs/TOOLS.md, and no test reads it).

What I ran

  • Built ~/filbert-scratch/r169-r1/wt: 4b738e2da, then git apply --index build.patch. sha256sum -c on the manifest: 3/3 OK. The manifest file hashes to c6aaac1f...54af. The cached diff is byte-identical to build.patch, patch-id 17a90c4ad795. Diffstat: 3 files, +430/-32.
  • Base drift: 4b738e2da..d63cfca22 touches none of the three manifest paths.
  • Path readers and pins: only packages/cli/tests/stack-release.test.mjs reads scripts/stack-release.sh. No test or scripts/test-*.sh reads docs/TOOLS.md; the only mention is a refusal string at stack-release.test.mjs:428. I found no 64-hex pin on any manifest path.
  • The TOOLS.md hunks all sit inside "Pinned stack update" (TOOLS.md:365-469).
  • Rehearsal: sha256sum rehearsal/SHA256SUMS = a36c89eb...b031, and sha256sum -c inside rehearsal/ passes.
  • Tests, one file at a time, scratch TMPDIR: stack-release.test.mjs 93/93 pass, 0 fail, 0 skip (23:11:46Z-23:14:47Z); stack-copy.test.mjs 12/12 pass, 0 fail, 0 skip (23:14:47Z-23:14:54Z). Counts are awk sums over the spec logs, and the slot was Sage's grant sage-1087 after my /proc CLEAR.
  • I did not run the 23 mutants. "All killed after the control" is Darkwing's receipt.

Brief items against the code

  1. --pm-after-harness:
    • parse_opts refuses it together with --pm (stack-release.sh:77), and with any verb other than activate.
    • plugin_checks (stack-release.sh:265-277) runs after precheck and before new_activation, so before any stop. It refuses unknown kinds first, then pm-after-harness-target and pm-after-harness-set. [] counts as set.
    • flags_drift requires a pointer with pm 1 (:602-603).
    • run_pm (:791-797) sets RUN_PM=0 and RUN_PENDING=1. run_steps then installs and gates with RUN_PM and writes pmPending:true. The log's last line is result ok pm pending.
  2. The pending state:
    • read_pointer (:95) accepts pmPending only when it is missing, or true beside pm false.
    • do_activate refuses pm-pending right after the pointer read, before precheck.
    • adopt still refuses already-adopted.
    • status prints the pending line.
    • from_log (:141) still reads only adopt/activate/rollback ok lines, so a pm-on line never becomes a rollback target or a backup source.
  3. pm-on (:890-917):
    • It refuses not-pending, then harness-unset (loader refusal, none, or anything but a non-empty list), then instance_busy, drop_in_override, read_units and flags_drift, all before capture and the stop.
    • It stops and starts the bus alone and gates with pm 1.
    • On failure, pm_on_fail (:865-887) runs stop_bus, put_back, start_bus and gate "$P_COPY" 0. A pass exits 1 with the pointer untouched. A fail stops both units and exits 2.
    • Neither failure path writes the pointer or the store.
  4. Rollback and kinds:
    • From pending, run_pm sets RUN_PM=1, so OLD is installed with --pm and gated with pm_launched.
    • An in-run failure gates P_COPY with P_PM (0 when pending), which matches the restored pending unit.
    • Both activate and rollback run plugin_checks before any stop. The refusal names every unknown key, and rollback adds the null hint.
    • plugins.set with null deletes the key (plugin-settings.mjs, delete next.plugins[kind]), so the brief's "passes after harness is set to null" holds.
  5. --plan: check_refuse (:257-262) collects the refusals. plan prints the pending lines and the refuses lines, and exits 3 when there are any.

The tests T32-T36 cover each line of the brief's Tests section, and refused() asserts no start or stop for every refusal.

Sage's questions

Q1, the stand-in writer. It is a fair stand-in for the file write. hset.mjs builds pluginSettings({dir: configDir(env), rolesDir: rolesDir(env), exists: hasModule}) from the NEW copy's own modules. The host builds the same thing: organization at cli/src/config.mjs:97 is {dir: configDir(env), rolesDir: rolesDir(env)}, and runtime.mjs:93 passes organization.dir, organization.rolesDir and hasModule. So the revision check, the kind and module checks, the write and the changes.jsonl receipt are the host's own code.

The stand-in skips three things:

  • the bus's human proof, which is the refusal hcli.sh recorded;
  • broker.mjs:1236's keys(args, ["revision","plugins"]) shape check, which hset satisfies by construction;
  • the background limits.refresh, which doesn't touch the file.

The actor is standin.darkwing instead of jason. What the stand-in does not show is that Jason's real CLI path works. At d195d171c, scripts/mosaic does not dispatch plugins; row 175's brief carries that fix (3dfaea286). See F2.

Q2, step 5. The claim holds. launchPm (cli/src/launcher.mjs:659-669) turns every Refusal from check() into a CliError with exit 3. That covers no-route and harness-off alike. stack-release.sh never sees the refusal code: it sees the bus go inactive (units-inactive) or no launched line (pm-launch-unconfirmed). pm_on_fail does not branch on the code. It takes the same stop_bus, put_back, start_bus and phase-1 gate path for any failure. The rehearsal's 5c shows that path: pm-on-failed with units-inactive, the phase-1 gate passed, the pointer pending, exit 1.

The harness-off code itself was never produced, in either the rehearsal or the tests. T34 drives the failure with a stand-in pm-launch-unconfirmed. From the script's side the two are equivalent.

Q3. Run singly, plus the greps above: stack-release.test.mjs 93/93 and stack-copy.test.mjs 12/12, matching Darkwing's counts.

Doc-only follow-ups

  • F1 docs/TOOLS.md:453-455: "Neither touches the copy, the Console or the store." The pm-on-down branch it follows stops the Console (stack-release.sh:881). Suggested wording: "Neither touches the copy or the store, and only pm-on-down stops the Console."
  • F2 docs/TOOLS.md:426, :446: "plugins set through the live bus" names no command that works on a copy at d195d171c, because scripts/mosaic there doesn't dispatch plugins. Until row 175 lands, the window plan should name the working form (packages/cli/src/cli.mjs plugins set --file PATCH) and the PATCH shape ({"revision": ..., "plugins": {"harness": [...]}}).
  • F3 docs/TOOLS.md (step 5 and 7): the new business-unreadable refusal (stack-release.sh:267, :275) isn't listed. It means every activate and rollback now reads the business file with the target's code. That makes two cases refuse before any stop: a copy without packages/plugins/src/kinds.mjs (anything before eaeb5ebf2, 2026-10-15), and a host where the script's environment can't find the business file. Today's candidates (5fa9cc8f2, ca15d73e9, 8e7ea89a1) all have kinds.mjs, so nothing live is affected. It is fail-closed, but it should be documented. No test drives this refusal. The brief doesn't require one, so this isn't blocking.

Notes, not findings

  • TOOLS.md:436 begins with #1666,. CommonMark won't render it as a heading (there's no space after #), and :332 already does the same.
  • With a pending pointer, plain activate --plan prints a bus line with --pm above its refuses pm-pending line. That's harmless, since the plan exits 3.