Compare commits

...
Author SHA1 Message Date
jason.woltje ad1b23ab23 row 240: BUILD.md records the r1 failure, the b45852daa fix and the r2 results (cli 639/0, scripts 585/0, M1-M8 8/8), and the brief pin at b00d7d94d 2026-10-21 01:34:54 -05:00
jason.woltje b45852daa0 row 240: the N1 test drops its after hook, which threw ENOENT after the fixture removed the root (first slot run, 06:18Z) 2026-10-21 01:20:26 -05:00
jason.woltje 47ec9fe2f9 row 240: BUILD.md records why the stack-release tests aren't singles (sage-1759) and the r3 base c771a36f4 (sage-1773) 2026-10-21 01:16:58 -05:00
jason.woltje 8471174d0b row 240 (I4, #1746): root's host check, its pins and the boot unit; I3's N1, N4 and N5
scripts/case1-hostcheck.sh checks r6 B2's seven items (--check), stops bwrap on a failure or DRIFT
(--enforce), and compares the current apparmor-profiles deb to the pin (--upstream). scripts/case1-pins.sh
holds the pins; packages/cli/systemd/mosaic-hostcheck.service runs --enforce at boot.

Carried from the I3 review (Filbert 29922, sage-1727): stack-upgrade.sh exits 5 args-failed when the args
move after A5 fails, with a test; run sheet section 3 names npm's PATH and the --claude file; sections 8
and 10 get precheck lines.
2026-10-21 01:16:46 -05:00
jason.woltje c771a36f4b row 234: machine grant row a, host enrollment, store records and bus schema 3i to 3j (#1740, Vikunja #436)
Lands Gizmoduck's round 3 candidate, manifest 4782c8b5ecb7 (c783d5e39 on gizmoduck/row234-r3, one commit
on 57f59e0c3), approved by Duckworth: content on round 1 (861182fa6, 29919), the round 2 rebase check
(29928, rev 2670) and round 3 (REVIEW-r3.md 7c8c38b69, posted 29935, rev 2683). 41 paths.

Round 3 fixes the two failures in Sage's combined gate with row 233: bus schema.test.mjs now counts 18
tables and 71 triggers for the new hosts and host_grants tables, and stack-release.test.mjs T46 puts both
tables in the untimed restore-guard list, since every write shares a transaction with a grant.* event.
Round 3 rebuilt on the row 233 landing as one commit. 38 of the 39 round 2 blobs are unchanged and BUILD.md
is append-only. Gizmoduck's singles on round 3: stack-release 196/0, schema 1/0.

Evidence: Gizmoduck's slot runs, 25 of 25 mutants killed (A22b and A23 included, A9 structural), singles on
r2 387/0 over 47 files. Sage's landing gate on the c783d5e39 tree: 06:02:12Z to 06:15:26Z, every package except queue 2890 pass, 0 fail (15 packages; bus 539, cli 622), and all ten scripts/test-*.sh 585 pass, 0 fail.

This is code only. The live stack runs from a pinned copy at schema 3i. The 3i to 3j migration runs in the
10-24 window (mos-1145), agent-run under Q173, with a backup first. Duckworth's N10 (hosts.mjs
Object.hasOwn) and N12 (doc-only) go to row 235's carry list.
2026-10-21 01:15:43 -05:00
jason.woltje caa4b59440 sessions: Sage, row 233 landing 57f59e0c3, 234 gate stop and r3, 240 briefed, 197/238 drift (sage-1742..1758) 2026-10-21 00:47:45 -05:00
jason.woltje 57f59e0c39 row 233: installer I3, stack-upgrade.sh and the host run sheet (#1735, Vikunja #432)
Lands Darkwing's round 1 candidate, manifest 464ce9eb177a (1a1dc80ca on darkwing's branch), approved by
Filbert (29922, rev 2659). Seven paths: scripts/stack-upgrade.sh (new), scripts/stack-release.sh,
docs/guides/host-run-sheet.md (new), docs/TOOLS.md, packages/cli/tests/stack-upgrade.test.mjs (new),
packages/cli/tests/stack-release.test.mjs and agents/darkwing/work/queue-233/BUILD.md.

Evidence: Darkwing's gate on d99166ade (base e65a3ab4d) 1201/0, mutants m1-m8 8 of 8 killed. The N1 fix
08db6ac3f (flock sleep leak in stack-upgrade.test.mjs) reran c0 20/0 with m1 killed (sage-1708); BUILD.md
doesn't record that rerun. Since e65a3ab4d no commit touches the seven paths. Rows 241 and 242 changed bus
and cli files. Only cli tests read the seven paths, so Sage ran the cli suite on 5465fd52d plus the seven
paths: 617 pass, 0 fail (out233/pkg-cli.log, 05:26:33Z to 05:35:00Z). A combined gate with row 234 failed bus schema.test.mjs:522 on 234's new tables, and
Gizmoduck found a second 234 failure (stack-release.test.mjs T46), so Sage stopped it. Row 234 round 3 fixes
both and rebuilds on this landing, since its T46 lines go in a file this commit changes.

Filbert's N1, N4 and N5 go to row 240 (brief b00d7d94d). N2 and N3 are in DEFERRED.md.
2026-10-21 00:35:18 -05:00
jason.woltje 5465fd52d5 sage: DEFERRED, checkCreative doesn't refuse credentials (row 213 r3 Webby N1, mos-1160) 2026-10-21 00:12:52 -05:00
jason.woltje b00d7d94d0 sage: row 240 (I4) carries Filbert N1, N4 and N5 from the 233 review; N2 and N3 go to DEFERRED (29922, sage-1727) 2026-10-21 00:01:26 -05:00
jason.woltje 642a9b87de sessions: Filbert, row 233 round 1 approve (29922, rev 2659) 2026-10-20 23:58:01 -05:00
jason.woltje 16019b80e6 sessions: Darkwing, row 233 in review round 1 (1a1dc80ca, manifest 464ce9eb, revs 2644 and 2647), with its BUILD-LOG entry 2026-10-20 23:51:01 -05:00
jason.woltje 9b53620185 sage: 10-24 window draft 0 counts grant look-alike events before --migrate (row 234 A22b, gizmoduck-0107) 2026-10-20 23:29:34 -05:00
jason.woltje 7662b04902 sage: 10-24 window draft 0 adds rows 232 (e65a3ab4d), 241 (5d4a61776) and 242 (5fb736b92, fc1cb856b); 199 and 210 evidence list due in draft 1 2026-10-20 23:24:00 -05:00
jason.woltje 04ce8dfd56 queue: revs 2639-2641 (row 242 Duckworth approve record, landing note and done) 2026-10-20 23:22:41 -05:00
jason.woltje fc1cb856b0 publish: row 242 follow-up docs, the git socket comments name the hold and the late record; BUILD.md residual and :311 cite (Duckworth N1, N2, N4, #1748)
Megavolt's c5a125f4b on megavolt/row242-docs, parent 5fb736b92 (the row 242
landing). Comments and .md only: git-socket.mjs changes comment lines at
:39-44 and :433-435, BUILD.md records N2 and N4. No test pins either path's
hash. Landed without a gate under the doc-only rule.
2026-10-20 23:17:14 -05:00
jason.woltje c0c4907af9 sessions: Sage, rows 241 (5d4a61776) and 242 (5fb736b92) landed, queue revs 2628-2641 2026-10-20 23:14:49 -05:00
jason.woltje 5fb736b92d publish: a retry after publish-timeout pushes once; the bus holds the decision until its push ends and records a late push (row 242, #1748, P19 440)
Megavolt's candidate 635c10924 on megavolt/row242, manifest 1fbfab6c (3 paths:
agents/megavolt/work/queue-242/BUILD.md, packages/bus/src/runtime.mjs,
packages/cli/tests/publish-socket.test.mjs). Duckworth approved (comment 29889, rev 2639).

Mutants M1 and M2, run by Megavolt on Sage's slot: both killed (publish-socket 8/1 each).
Sage's landing gate on 12b8538e0 (96bac588f plus these 3 paths): 15 packages 2826 pass 0 fail (bus 500, cli 594, seat 164), 10 scripts/test-*.sh suites 429 pass 0 fail plus 156 node tests in the queue suite, 03:59Z to 04:12:44Z

The 3 paths are unchanged on refactor since the candidate's base 8fede5fc6. Follow-ups:
Duckworth's doc notes N1, N2 and N4 go in a later doc-only commit; the late-record notice
to the run is Vikunja 440 comment 432. No live change.
2026-10-20 23:13:06 -05:00
jason.woltje 96bac588fb queue: revs 2628-2638 (row 241 approve record, landing note and done; row 232 landing note and done; row 233 brief re-pin and Darkwing claim; row 242 in review round 1, request 29888) 2026-10-20 22:47:48 -05:00
jason.woltje 5d4a617762 bus: row 241 one decision lands once; a broker test with two checkLand tickets on one decision, where the second recordLand refuses decision-consumed and records nothing (row 241, #1747, P19 441)
Megavolt's candidate 3e3ed303e on megavolt/row241, manifest e8266864 (2 paths:
agents/megavolt/work/queue-241/BUILD.md, packages/bus/tests/land.test.mjs). Tests and
packet only, no source change. Duckworth approved (comment 29870, rev 2628).

Sage's slot on bbf3b1ee8 (0444acd08 plus these 2 paths):
- mutants M1 (#spent recheck, broker.mjs:963), M2 (CAS old value, seat working.mjs:391),
  M3 (land-busy, cli git-socket.mjs:351): all killed
- full gate: 15 packages 2797 pass 0 fail (bus 500, cli 565), 10 scripts/test-*.sh suites 429 pass 0 fail plus 156 node tests in the queue suite, 03:21:13Z to 03:34:16Z

Row 232's landing e65a3ab4d sits between that base and this commit. Its 9 paths are
disjoint from these 2 and touch no bus code. No live change.
2026-10-20 22:34:30 -05:00
jason.woltje e65a3ab4d2 stack-release: --units external for root-owned units, with confirm, revert and gate --since; stack-copy copy --bundle and wrapper --create (row 232, #1734, installer I1)
Blobs of Darkwing's round 2 da2da2f6c on origin/darkwing/row232-r2 (base
8fede5fc6), the 9 manifest paths, manifest 7af3c943. Round 2 is a
rebase-only round over row 230 (c8bc5cf4c): packages/cli/README.md is a
clean 3-way merge with 232's hunk unchanged, and the other 8 paths keep the
round 1 hashes (manifest 7a3b4ea1). Filbert approve round 1 (comment 29829)
and round 2 (comment 29865).

Under --units user every command line and every byte of output is the
base's. Under external the script reads root's system units, starts and
stops none, stages a copy behind AWAITING-UNITS, and confirm gates root's
start on the bus unit's ActiveEnterTimestamp and launches list.

Mutants 17 of 17 killed (BUILD.md). Darkwing's landing gate on da2da2f6c,
03:07:15Z to 03:17:48Z: packages/cli 593 pass 0 fail, 10 scripts/test-*.sh
suites 429 pass 0 fail (plus 156 node tests in the queue suite). Refactor
moved to 0444acd08 since, three commits touching no manifest path. No live
change: the stack runs from copy 4429a104f, and --units user stays the
default.
2026-10-20 22:19:47 -05:00
jason.woltje 0444acd084 queue: revs 2618-2627 (row 232 round 2 approve; row 241 in review round 1, request 29868; row 234 and row 242 claimed)
Gizmoduck: row 234 claim (2618). Darkwing: row 232 rebase round 2 onto 8fede5fc6, in-progress and in-review with request comment 29860 (2619-2621). Filbert: row 232 round 2 record, approve, comment 29865 (2622). Megavolt: row 241 in-review round 1, request failed on the token (2623-2624), row 242 claim (2626). Sage: the row 241 request as jarvis and its resolve to comment 29868 (2625, 2627).
2026-10-20 22:06:29 -05:00
jason.woltje 052ba95c7c sessions: Filbert row 232 I1 review round 2, approve (#1734) 2026-10-20 22:01:43 -05:00
jason.woltje a52d64ed41 sage: 10-24 window draft 0 names the landed commits for rows 210 (f5753c0e8), 237 (d7d943199) and the PRD (0adf8ddfe) 2026-10-20 21:52:36 -05:00
jason.woltje 8fede5fc6a sessions: Sage, row 210 landed f5753c0e8, queue revs 2604-2617 2026-10-20 21:51:47 -05:00
jason.woltje c084d8d423 queue: revs 2604-2617 (row 210 done, landed f5753c0e8; rows 241 and 242 added and briefed, 241 claimed by Megavolt; row 234 briefed)
Sage: rows 241 and 242 added and briefed (#1747, #1748), the row 210 round 3 request as jarvis, the row 210 landing note and done move, row 234 queued to briefed. Gizmoduck: row 210 r5 start and in-review round 3 (2607-2611). Duckworth: row 210 round 3 record, approve (2614). Megavolt: row 241 claim (2606).
2026-10-20 21:50:59 -05:00
jason.woltje f5753c0e8d host access: agent host sudo through the credential store, host.run over broker.sock (row 210, #1707)
Blobs of Gizmoduck's r5 0485384e9 on origin/gizmoduck/row210-r5 (base
ad95c888c), the 23 manifest paths, manifest 688d38b9. Duckworth approve
round 2 (rev 2592) and round 3 (comment 29851, rev 2614). Round 3 is a
rebase-only round over row 237 (d7d943199): broker.mjs is a clean 3-way
merge (610af0c4), the 210 hunks sit away from 237's insert, and range-diff
against r4 c43008749 is all equal.

Mutants 52 of 53 killed on the merged tree. M22b is equivalent (rev 2584);
its coverage moves to row 235. Preflight singles: seat sandbox 10/0, bus
project-runtime 3/0. Gate on ad95c888c plus these paths: 15 package suites
2796 pass 0 fail, 10 scripts/test-*.sh suites 429 pass 0 fail (plus 156 node
tests in the queue suite). Refactor moved to 9d165be8c since, two briefs
touching no manifest path. No live change: the stack runs from copy
4429a104f until the 10-24 re-pin window.
2026-10-20 21:47:11 -05:00
jason.woltje 9d165be8c7 plans: row 237 follow-up N1 brief, a retry after publish-timeout pushes once (duckworth-0069, P19 440) 2026-10-20 20:54:33 -05:00
jason.woltje c25785d3d5 plans: row 237 follow-up brief, one decision lands once, tests at the broker (megavolt-0069, sage-1651, P19 441) 2026-10-20 20:52:31 -05:00
jason.woltje ad95c888c3 sessions: Sage, row 237 and PRD 1.7+1.8 landed, queue revs 2540-2603 2026-10-20 20:49:55 -05:00
jason.woltje 584e2bd827 queue: revs 2540-2603 (rows 199, 200, 230, 237 done; 210 r4 and 238 in review; 232 in review round 1 approve; 233 briefed; rows 239 and 240 added)
Sweeps 64 uncommitted revs. Seats: sage, duckworth (2540, 2581, 2592), gosalyn (2550-2551, 2558-2560),
megavolt (2555, 2574, 2576-2577), webby (2557, 2563, 2599), launchpad (2570, 2590-2591),
gizmoduck (2585-2587), darkwing (2595-2596), filbert (2600).
Landings: 200 9e1085816, 199 cb5fdb064, 230 c8bc5cf4c, 237 d7d943199.
2026-10-20 20:49:45 -05:00
jason.woltje 0adf8ddfe2 prd: Mosaic Stack PRD versions 1.7 and 1.8, status approved version 1.8 (DECISIONS Q192, decision d52febb5, Vikunja #434)
1.7 is PRD-1.7-DRAFT.md (sha256 e896746d), approved by decision d52febb5 (prd.approve, yes). 1.8 is r7
82df17347 (sha256 de079281), approved by Jason in T3, relayed by Mos (mos-1154, DECISIONS Q192), so it has no
in-Stack prd.approve. packages/tasks/requirements.json moves to version 1.8 with 68 ids: COS-3, CRED-3, CRED-4,
GRANT-1 to 4, TRUST-1, TRUST-2 and LAND-2 are new. The tasks and bus requirement pins move to 68 and 1.8.
Written by Honker (honker/prd-17-18 2884ee496), reviewed by Duckworth (duckworth-0075, -0076, -0077).
2026-10-20 20:46:55 -05:00
jason.woltje d7d9431990 bus: one decision, one open publish; checkPublish refuses publish-busy while another ticket holds the decision (row 237, #1743)
The decision is spent only at recordPublish, after the push, so two checkPublish calls on one decision could
both push and land the commit on two fork refs. checkPublish now refuses publish-busy while an open ticket
holds the same business and decision. A refusal drops the ticket and leaves the decision unspent. Broker test
and a socket race test with exactly one git-receive-pack. Residuals 1-3 in BUILD.md; residual 3 (checkLand)
is a follow-up row.

Built by Megavolt (megavolt/row237 e42415935, duckworth-0068 N1), reviewed by Duckworth (approve, comment
29810, manifest bb777cb0). Sage's gate on 816d48b03 plus these paths: M1 killed, every package suite and every
scripts/test-*.sh green.
2026-10-20 20:45:08 -05:00
jason.woltje a0e3d7cfdd plans: installer I4 hostcheck row brief, split out of I3 (darkwing row 233 G2, sage-1650) 2026-10-20 20:38:37 -05:00
jason.woltje 9d24ff9b28 sessions: Filbert row 232 I1 review round 1, approve (#1734) 2026-10-20 20:34:13 -05:00
jason.woltje 6c04b89dae plans: row 194 follow-up brief, the revoke under the writer's lock and a fire test (duckworth-0074, mos-1158) 2026-10-20 20:29:43 -05:00
jason.woltje 6f7dacb417 sage: 10-24 window draft 0 carries PRD 1.7+1.8 (mos-1154, mos-1156, DECISIONS Q192); the live pm cites 1.6 until the window 2026-10-20 20:28:39 -05:00
jason.woltje d21b2615c1 darkwing: row 232 in review round 1 (#1734), BUILD-LOG and SESSIONS lines 2026-10-20 20:28:11 -05:00
jason.woltje 816d48b03e sage: row 42 run log, Jason's pre-S7 checks (mos-1154); PRD 1.8 r7 raise dropped, Jason approved in T3 (DECISIONS Q192, NOTE.md) 2026-10-20 20:22:19 -05:00
jason.woltje 011d41ba6a sage: row 42 S7 dates in the run log (sage-1622, mos-1148 C1 C2, Vikunja 442), 10-24 window draft 0 (mos-1145; NEW open), Vikunja scripts and records: 412/424 landing comments, 237 follow-ups 440/441, P19 405-408 and 411 re-plan comments 416-420, read-tasks.mjs (#1524) 2026-10-20 20:06:39 -05:00
jason.woltje c8bc5cf4cb case2: unit render and host precheck, case2-unit.sh and case2-precheck.sh (row 230, #1732)
Squash of Gosalyn's 96768766c on gosalyn/c2-13a-r2 (base 722136772), the
5 manifest paths, build.patch 56649f9a, manifest fbb0d7e42c67 (Webby
approve round 2, rev 2563). Gosalyn's gate on 96768766c: cli 541 pass
0 fail; scripts/test-*.sh 429 pass 0 fail (summed by script); mutants
10 of 10 killed. packages/cli/README.md is a 3-way merge with row 199
(cb5fdb064): the hunks don't overlap (line 710 vs 1022). host.test.mjs:855
is the only test that reads that file. On cb5fdb064 plus the patch,
case2-unit.test.mjs 30/0 and host.test.mjs 42/0. Refactor moved to
390ea5261 since, a README fix touching no manifest path. The script
runs on a host only in a separate window with Jason's go.
2026-10-20 19:36:22 -05:00
jason.woltje 390ea5261c business: README names nine hard waits, git.push.remote included; row 199 added it to GATED_ONLY (row 199 review N3, duckworth-0068, #1697)
Megavolt's 5d7e4d26e on megavolt/n3-readme, from cb5fdb064. Doc-only: no
test or scripts/test-*.sh reads packages/business/README.md. The text
matches packages/business/src/vocabulary.mjs, where GATED_ONLY has nine
actions.
2026-10-20 19:29:22 -05:00
jason.woltje cb5fdb064f credentials: a credential store per agent, its own git identity, pushes to a remote (row 199, #1697)
Squash of Megavolt's 627979a17..86963d281 on origin/megavolt/row199-on-190
(base 00d4cc1f6, row 190's landing), the 37 manifest paths, build.patch
05383075, manifest 35f900fd (Duckworth approve round 1, comment 29779,
rev 2540). Mutant batches 1 and 2 in agents/megavolt/work/queue-199/BUILD.md:
batch 2 26 of 28 killed, M17 and M27 equivalent. docs/TOOLS.md is a 3-way
merge with row 200 (9e1085816): row 199's hunk applies unchanged (patch-id
d88379111e2e) and no test reads the file. Gate on 9e1085816 plus the patch:
15 package suites 2735 pass 0 fail, 10 scripts/test-*.sh suites 429 pass
0 fail (plus 156 node tests in the queue suite). Refactor moved to 76134dfde
since, a brief touching no manifest path. No live change: the stack runs from
copy 4429a104f until the 10-24 re-pin window.
2026-10-20 19:23:12 -05:00
jason.woltje 76134dfde1 plans: row 200 follow-up N1 brief (#1744, Launchpad, Webby): the sso-config hint carries --instance NAME 2026-10-20 19:12:34 -05:00
jason.woltje 9e10858160 stack-release: activate --sso for the Q175 phase 2 window (row 200, #1698)
Squash of Launchpad's 30e4bb1a3..9df40878f (round 3 on row 190's landing
00d4cc1f6), the 3 manifest paths: scripts/stack-release.sh,
packages/cli/tests/stack-release.test.mjs, docs/TOOLS.md. Manifest
b3ead55c8b48 (Webby approve round 3, rev 2539, comment 29772).
Patch b2a8f5de18b7. Gate on 9df40878f: 15 package suites 2675 pass
0 fail, 10 scripts/test-*.sh suites 429 pass 0 fail (plus 156 node tests
in the queue suite). Mutants 13 of 14 killed, M12 the ruled equivalent
(sage-1446). Refactor moved to 722136772 since the base, touching none of
the 3 paths. No live change: the stack runs from copy 4429a104f until a
re-pin.
2026-10-20 19:09:00 -05:00
jason.woltje 7221367728 plans: row 194 item 7 takes the restart route (honker-0044) and item 8 revokes on fire and delete before the business file changes (row 199 D13), with tests and mutants 2026-10-20 18:51:20 -05:00
jason.woltje a304dad947 plans: C2-03a askpass ruling (megavolt-0060 to -0063, dewey-0765, gosalyn-0068): fixed argv with followRedirects=false and credential.helper reset, LC_ALL=C, exact-prompt helper with row 199's HOST/NAME checks, no userinfo; tests and four mutants 2026-10-20 18:42:52 -05:00
jason.woltje 919a84738b plans: #436b build brief (#1742, schema 3j-3k, design r5 1be535aca) and row 199 N1 follow-up (#1743); row 194 carries 199 N2; 193b2 moves to 3k-3l after #436b 2026-10-20 18:39:24 -05:00
jason.woltje 2d8687bcfd sessions: pending lines from Darkwing, Rocko, Launchpad, Morgana, Honker, Gander, Herb, Dewey and Sage (2026-10-20 18:45Z to 23:32Z) 2026-10-20 18:32:42 -05:00
jason.woltje 1c46d0abf6 queue: revs 2456-2539. Morgana row 213 in-review rounds 1-2 (2456-2476); Gizmoduck row 210 in-review 1 (2458-2459); Gosalyn row 212 in-review r1 and row 230 start (2464-2470); Duckworth row 210 record (2466); Webby row 213 verdicts r1-r2 and row 200 r3 approve (2469, 2500, 2539); Darkwing row 190 r3 in-progress through row 232 in-progress (2501-2514); Filbert row 190 record and row 66 r3 approve (2504, 2526); Dewey row 66 r3 approve (2529); Megavolt row 199 in-review round 1 (2535-2536); Launchpad row 200 r3 in-progress and in-review (2509-2525); Sage requests, row 190 done, rows 232/233 added and briefed, row 195 after 190 and 233, brief re-pins of 226/230/232/233, row 66 round 3, the row 200 r3 request as jarvis, machine grant rows 234 and 235 added and row 206 after 234 with its 3k brief and piece, the row 199 r1 request as jarvis (2460-2538, 57 revs) 2026-10-20 18:32:06 -05:00
jason.woltje f519fb1d26 plans: machine grant rows a (#1740, schema 3i-3j) and b (#1741) briefs from design r6 f9039682e; 193b2 moves to schema 3j-3k after row a 2026-10-20 18:24:28 -05:00
jason.woltje b8236a7e13 sessions: Filbert row 66 round 3 APPROVE 2026-10-20 18:20:31 -05:00
jason.woltje c7675476b1 sage: P19 catch-up comments 411-413 on 413 (row 189 landed, no-send activate), 424 (row 210 status) and 414 (rows 201 and 204 landed) 2026-10-20 18:19:49 -05:00
jason.woltje 9de7ad8292 sage: row 66 r3 packet, rebase of r2 onto c40d846cb (sage/row66-r3 2369c00e36, same patch-id, manifest 42e3876ca168) 2026-10-20 18:15:59 -05:00
jason.woltje c40d846cb8 sage: Vikunja comment scripts and read-backs for row 192 landing (P19 412) and the row 190 W11 comments 406-409 2026-10-20 18:12:18 -05:00
jason.woltje 01b40d3576 plans: C2-13a rulings for gosalyn-0065. G1: default instance only, no --instance, a named instance is a follow-up. G2: library refuses --sso for 10-27, preview set and proof pair stay, --sso waits for Q175 phase 2. Line cites moved to their places after rows 190 and 192 (webui-service.sh, stack-copy.sh, bus-service.sh, serve.mjs), and the Console's WantedBy replaces :41 2026-10-20 18:09:04 -05:00
jason.woltje ad6f0be7aa plans: installer I1/I3 brief, the stack user's stack-release.sh runs from its own clone of the release bundle (darkwing question, sage-1561) 2026-10-20 18:05:14 -05:00
jason.woltje ffc124bf68 plans: installer I2 re-brief of C2-13a (row 230, #1732) and C2-13b (row 226, #1727) for Vikunja #432. Q189: both units WantedBy=multi-user.target, root's 2c step enables both and starts the bus then the Console, no hand start at step 9 (gosalyn-0064, mos-1122, mos-1123). Row 190 landed first, so row 230 renders @CONFIG_ENV@ and @SESSION_ENV@ 2026-10-20 18:02:39 -05:00
jason.woltje 8a14931fa4 plans: installer briefs I1 (Gitea #1734) and I3 (#1735) for Vikunja #432 (mos-1118, mos-1122). I1 builds on row 190's landing 00d4cc1f6; I3 carries the root lock finding, duckworth-0055's notes, the per-host enrollment step and the credential slot table 2026-10-20 17:56:07 -05:00
jason.woltje 0272383f58 sessions: Darkwing row 190 landed as 00d4cc1f6, row 195 r6 approved and held 2026-10-20 17:53:27 -05:00
jason.woltje 00d4cc1f69 stack: several Mosaic Stack instances on one host (row 190, #1688)
Squash of Darkwing's 176a6bf4b..df65a3d4c on origin/darkwing/row190-r3,
the 25 manifest paths, build-r3.patch efe898c2, manifest cc0ea9e8
(Filbert approve round 3, comment 29713). Design revision 3 in
agents/darkwing/work/queue-190/DESIGN.md. Gate on base d0bbc5c1c with
the patch: 15 package suites 2664 pass 0 fail, 10 scripts/test-*.sh
suites 429 pass 0 fail (plus 156 node tests in the queue suite).
Refactor moved to 99a406ed3 since, a window record touching no manifest
path. Mutants 20 of 20 killed (BUILD.md, round 3). No live change: the
stack runs from copy 4429a104f until a re-pin.
2026-10-20 17:49:06 -05:00
jason.woltje 99a406ed3c sage: re-pin window 490e2771d to 4429a104f (row 192) record, STATE.md (sha256 a4d0a7b12f431f15), W.txt (ca9ad4a09059d3b7), S1/S2, matrices, W10 browser check and diagnosis (screenshots not committed), Mos's P3 rehearsal and flags check, W11 reads. Plan corrections after the window: NEW is 103 commits past OLD (not 106), the P3 flags note (mos-1107), and W10's line g and corrected browser checks (mos-1135, mos-1137). PRD 1.8 raise record, decision 3a6c8fb9 (mos-1140) (#1690; P19 386, 412) 2026-10-20 17:46:36 -05:00
jason.woltje d0bbc5c1cb business: 193a access level check widened to viewer (row 204, #1702)
Squash of Morgana's 7f07da4aa..f869fd062 on origin/morgana/row193a (diff
from R1's candidate 44e36fc4c), the three manifest paths, manifest d38de8c9
(Duckworth approve, comment 29595). LEVELS gains collaborator and viewer,
covers() takes a required least level, and an unknown grant level covers
nothing. Gate on base 4385b9ec1 with the patch: packages and
scripts/test-*.sh 2789 pass 0 fail. Refactor moved to c9f374284 since, a
SESSIONS line only. Mutants 7 of 7 killed (agents/morgana/work/queue-193a/
mutants.log on the branch). No live change.
2026-10-20 17:25:27 -05:00
jason.woltje c9f374284b sessions: Filbert row 190 manifest round 3 APPROVE 2026-10-20 17:16:20 -05:00
jason.woltje 4385b9ec18 plans: Case 2 briefs. C2-13a factory profile from gosalyn-0057 items 1-3 (--pm, --claude under the --node rule, no proxy flags), row 190 tokens to whichever lands second. New C2-01b for row 212's notes N1-N3, N4 to the C2-12 setup sheet, N5 to C2-10b (webby-0041, sage-1517) 2026-10-20 16:52:39 -05:00
jason.woltje afad4993f6 business: FED R1, access.json loader and validator with covers (row 201, #1699)
Squash of Gizmoduck's ae3c36f24..44e36fc4c, the three manifest paths,
manifest 225597146daf1c0e3ffe2049636095733f40a3395318f8ba07dad1bf08c0c520
(Duckworth approve, comment 29581). Gate on 44e36fc4c, run by Gizmoduck in
Sage's slot: business suite 173 pass 0 fail, scripts/test-*.sh 10 scripts
422 pass 0 fail, mutants 44 of 44 killed. index.mjs gains one additive
export line. No live change.
2026-10-20 16:47:42 -05:00
jason.woltje f41729178e plans: re-pin plan 490e2771d to 4429a104f (row 192, Q175 phase 1), npm ci in the new copy, for Mos's check (mos-1099 item 3) 2026-10-20 16:39:56 -05:00
jason.woltje 305857c8f9 queue: revs 2362-2455. Sage: Case 2 rows re-pinned to design r11 (5b8def2f1, row 226 to C2-13b), row 230 C2-13a added and briefed (#1732), row 215 (C2-04) assigned to Herb, row 192 landed 4429a104f with landing note and done, row 228 review request resolved, plus earlier Sage revs. Other seats' revs: Filbert 2362, 2447 (row 190 verdicts); Herb 2395-2396; Webby 2420, 2428 (rows 197/228 verdicts); Morgana 2421 (row 213 claim); Researcher subagent 2422-2424 (row 228); Gosalyn 2426 (row 212 claim); Darkwing 2431, 2433, 2438 (row 190) 2026-10-20 16:35:11 -05:00
jason.woltje 176a6bf4b9 sessions: Filbert row 190 manifest round 2 CHANGES (base drift on row 192) 2026-10-20 16:29:20 -05:00
jason.woltje 5b8def2f14 plans: Case 2 row briefs from Gosalyn's eac92d956 (design r11, approved webby-0038: C2-13 split into C2-13a unit render and host precheck and C2-13b install script and library precheck, C2-04 owner Herb, Console unit without [Install] per mos-1097) 2026-10-20 16:22:47 -05:00
jason.woltje 4429a104f6 webui: Q175 phase 1, the Console verifies SSO itself under --sso, OIDC RP with oauth4webapi 3.8.8 (row 192, #1690)
Squash of Launchpad's launchpad/row192 53bb59462..37582e820, manifest round 2
6f8cee147713c53652b58d47c5efd15ed47da1efbef6a126464857269b0b7787 (18 paths).
Sage's gate on 088087ac9: package suites 2758 pass 0 fail, scripts/test-*.sh
429 pass 0 fail; mutants 22 of 23 killed, #23 (second origin check) declared
equivalent by Launchpad (sage-1407). New dependency oauth4webapi 3.8.8: the
canonical checkout and any stack copy need npm ci. No live change; the live
re-pin is a window with Mos (mos-1099 item 3).
2026-10-20 16:21:29 -05:00
jason.woltje 32b54ac3b0 plans: Case 2 row briefs from Gosalyn's 51006710a (webby-0034 N1/N2 and webby-0035: row A README :711 one line per variable, C2-13 owns README :713 and says the precheck prints only variable names) 2026-10-20 15:57:07 -05:00
jason.woltje 6025ddd9cf plans: Case 2 row briefs from Gosalyn's a2d94b2c2 (Webby r9 notes, webby-0032: row A README line on claude-session.env and the dist/providers/data/zai.json path, C2-13 XDG_DATA_HOME precheck and last-assignment-wins key file parse) 2026-10-20 15:55:52 -05:00
jason.woltje ecbfd6deb6 plans: Case 2 row briefs from Gosalyn's 29e85196b (design r9 for mos-1090: row A out of scope points at design 10 step 2d, C2-13 precheck checks the hub key file by name and mode, claude marker check only for claude-code harness, EnvironmentFile= fixed in case2-unit.sh) 2026-10-20 15:44:09 -05:00
jason.woltje 0498c0613f agents: Gosalyn seat dir, README from gosalyn/case2-design, so Case 2 rows can name the seat as owner (#416) 2026-10-20 15:43:18 -05:00
jason.woltje 49d1f3a231 plans: Case 2 slice row briefs plus rows A and B, from Gosalyn's f325657c7 on gosalyn/case2-design (#416, design r8, C2-13 copy id and profile asks for row 195, C2-20 count-only note, row A ZAI_API_KEY per mos-1086/mos-1089, row B #428 after 10-27) 2026-10-20 15:41:39 -05:00
jason.woltje 628e2ca5cd sessions: Filbert row 190 manifest round 1 CHANGES (base drift on row 189) 2026-10-20 15:40:15 -05:00
jason.woltje 088087ac96 queue: revs 2248-2361+. Sage: rows 210 (#424 host access, Gizmoduck) and 211 (#426 force-push guard, Rocko) added and briefed; row 195 re-briefed to the Case 1 install on .53; review requests for 200 r1/r2 and 202; plus earlier Sage revs. Other seats' revs: Launchpad 2254, 2283-2284, 2286, 2311-2313, 2328-2329, 2344, 2347-2348 (rows 192/200); Honker 2255; Morgana 2257, 2320, 2326-2327 (row 204); Gander 2262; Herb 2265; Dewey 2268; Bushroot 2273; Megavolt 2276; Webby 2303, 2316, 2339, 2355 (row 192/200 verdicts); Gizmoduck 2304, 2318-2319 and the row 210 claim (rows 201/210); Quackerjack 2317, 2341-2342 (row 202); Duckworth 2323, 2335, 2353 (rows 201/204/202 verdicts); Darkwing 2354, 2357 (row 190 in-review) 2026-10-20 15:34:55 -05:00
jason.woltje a0fbefd48d plans: pre-push force-push hook and failing busy check brief (Vikunja #426, mos-1078, mos-1083; owner Rocko, reviewer Dewey) 2026-10-20 15:31:17 -05:00
jason.woltje f24f2cd40d plans: row 195 brief becomes the Case 1 install on 10.1.1.53 (Q181 hosts, Q182/Q183 recorded acceptance, Q185 existing account, chained on rows 190 and C2-13; ops-01 root steps under #423 step 1; sage-1439) 2026-10-20 15:24:33 -05:00
jason.woltje 9ba31b5b3f agents: Honker's PRD 1.7 draft r2 on refactor for the prd.approve raise (11cb0423b, sha256 e896746df595d1b5155130ca163fbf63b0dc38a87a8dd154af77f1a6d1328647, Duckworth approved duckworth-0039, Vikunja #420) 2026-10-20 15:20:37 -05:00
jason.woltje 9db578ff54 plans: agent host access with sudo through the credential store brief (#1707, Vikunja #424, Q183/Q184, chained on row 199) 2026-10-20 15:09:59 -05:00
jason.woltje 8eb9ddd12c plans: FED R8 brief to r4.11 (b93f098f0, recorded acceptance, Duckworth N1/N2, #416 date), R5 context source (sage-1419), 193a makes covers level required (duckworth-0033 N1) 2026-10-20 15:06:22 -05:00
jason.woltje ae3c36f248 agents: Gizmoduck seat dir and roster row (FED R1 owner, #414) 2026-10-20 14:42:15 -05:00
jason.woltje a2af7ab495 plans: FED R1, R5, R2 and R8 briefs (#414, r4.9 a1daa3c07) and row 193a-d build briefs (#1691); Quackerjack seat dir and roster row 2026-10-20 14:41:03 -05:00
jason.woltje aa1bd209e3 gate-lane: row 189 gate timing table for row 197 (Herb) 2026-10-20 14:25:17 -05:00
jason.woltje ace27dc5eb row 189 (#1687): stack-release.sh activate --no-send drops the Console's --send (Sage; Dewey approve 29485; Sage gate passed) 2026-10-20 14:21:56 -05:00
jason.woltje a4a42dbac2 plans: stack-release --sso brief (#1698, Launchpad, reviewer Webby, row 192 F3; chained on 192) 2026-10-20 14:18:08 -05:00
jason.woltje a31f1a0f8b plans: row 198 brief records Mos's call, in-process board reads behind the Console's auth (mos-1019) 2026-10-20 14:17:03 -05:00
jason.woltje 992650232f plans: agent credentials and git identity brief (MVP T1 item 6, #1697, Megavolt, reviewer Duckworth); Megavolt seat dir 2026-10-20 14:14:33 -05:00
249 changed files with 40001 additions and 927 deletions
+11 -6
View File
@@ -25,10 +25,15 @@ green at every step. Not production software — a proven foundation.
1. **Root is bootstrap-only.** First-class system configuration lives at the
repository root; everything else gets a dedicated directory (`roles/`,
`contracts/`, `missions/`, `tasks/`, `docs/`). Do not add new files to root.
2. **Configuration**: `~/.config/mosaic-dev/config.json` is the sole system
config — created only by `scripts/bootstrap.sh`, never overwritten,
fail-closed on any problem. Repo-scoped role authority lives in
`roles/*.json` (versioned, reviewed commits only).
2. **Configuration**: each stack instance has exactly one system config. The
default instance's is `~/.config/mosaic-dev/config.json`. Instance NAME's
is `~/.config/mosaic-NAME/config.json`, and its data root carries a marker
that names it. `~` is the home of the OS user that runs the instance. A
process reads only the config `MOSAIC_CONFIG` names, or the default's when
that is unset. Configs are created only by
`scripts/bootstrap.sh [--instance NAME]`, never overwritten, fail-closed on
any problem. Repo-scoped role authority lives in `roles/*.json`
(versioned, reviewed commits only).
3. **Secrets** never enter the repository or container images; auth is
runtime-only (read-only mount or environment variable).
4. **Contracts** (`contracts/`) are immutable and image-baked. Missions and
@@ -177,8 +182,8 @@ Full reference — usage, fields, exit codes, safety notes:
## Data map (canon)
- `~/.config/mosaic-dev/config.json` — system config (user-authored; never
auto-written).
- `~/.config/mosaic-dev/config.json`, or `~/.config/mosaic-NAME/config.json`
per named instance: system config (user-authored; never auto-written).
- `<dataRoot>` (from config; default `~/.mosaic-dev`):
- `runs/` — write-once run evidence (`result.json`, snapshots, `stderr.txt`)
- `sessions/` — pi JSONL session trees, one directory per named session
+2
View File
@@ -5174,3 +5174,5 @@ Before: managed project/thread verbs had no Console conversation area. After: fi
2026-10-20 04:44:54 UTC | Row 178 round 2 preparation | Before: Dewey C1 five render guards lacked killing tests. Added independent refusal fixtures, a temporary uid+1 id stub with cleanup, and empty loopback proof case; activation ordering recorded in r2 BUILD. Product unchanged; syntax/text preflight only, no test or mutant execution. Pending row 181 rebase and Sage slot.
2026-10-20 05:10:34 UTC | Row 178 #1676 round 2 verified | Before: C1 render refusals lacked killing tests and row 181 base was pending. Rebased onto 931651f2d, kept all COUNTED/count code and tests, appended T178 tail; added independent owner/collision/absolute/charset/header and empty-loopback proof tests. Four files singly 156/156, all 24 mutants killed, metadata and sandbox fixture probes pass; scratch sweep empty. Packet agents/rocko/work/queue-178/r2/, candidate 9300a670, inventory d0dcde7b (59/59), scan 58 files no findings; in-review revs 2166-2167, expected no-token request failure. No live actions or commits.
2026-10-21T01:28:05Z | Darkwing | row 232 #1734 installer I1 build, round 1 in review | Before: stack-release.sh managed only user units (systemctl --user), and stack-copy.sh had no bundle copy. After: --units external (adopt, activate, rollback --to, status, confirm, revert, gate --since, AWAITING-UNITS, stale-start, external-pm, --plan --expect-from) and stack-copy check --units external, copy --bundle FILE SHA, wrapper ID --create; user mode byte-identical to the base by test. First build 0af0c73c6 on 00d4cc1f6; rebased onto row 200 (cb5fdb064) as a0ffabc3f on darkwing/row232-r1; --sso refused under external (D9), row 200 config_checks run under external (D10). Gate on Sage's slot (sage-1618): cli 561/0, every scripts/test-*.sh green; 17/17 mutants killed. Results commit 84f1a91e6 (pushed, seat branch only). Manifest 7a3b4ea1 (9 paths), queue revs 2595-2596, request comment 29825, reviewer Filbert. Packet agents/darkwing/work/queue-232/BUILD.md.
2026-10-21T04:50:44Z | Darkwing | row 233 #1735 installer I3 build, round 1 in review | Before: root had no upgrade driver for root-owned units and no host run sheet; stack-release.sh had no rehearse --backup last and no external plan refusals. After: scripts/stack-upgrade.sh upgrade and rollback (r6 A1-A5, A2f, R1-R4) with 20 tests, docs/guides/host-run-sheet.md, TOOLS.md lines, and on Sage's rulings stack-release.sh --backup last, BACKUP_STORE and the --plan refusals open-activation and migrate-required (3 tests). Base e65a3ab4d (row 232's landing). Gate on Sage's slot (sage-1704) on d99166ade: cli 616/0, every scripts/test-*.sh green, 1201/0 summed by awk; m1-m8 8/8 killed. Correction: the process sweep after the gate found an orphaned sleep 60 from the lock test's flock holder (N1); 08db6ac3f fixes it test only, Sage ruled the gate stands (sage-1707), rerun c0 20/0 and m1 killed with 0 left (sage-1708). Tip 1a1dc80ca (pushed, seat branch only). Manifest 464ce9eb (7 paths), queue revs 2644 and 2647, request comment 29901, reviewer Filbert. Packet agents/darkwing/work/queue-233/BUILD.md.
+3
View File
@@ -22,6 +22,9 @@ not fleet seats. Keep changes in `/mnt/storage/src/mosaic-stack`; do not modify
| Gander | Builder, the human boundary across uids (P19 333, #1694) | T3 session (Codex, gpt-6.1-sol), started by Sage under mos-0977 | none, T3 only |
| Herb | Builder, a second gate lane (#1695) | T3 session (Codex, gpt-6.1-sol), started by Sage under mos-0977 | none, T3 only |
| Bushroot | Builder, retire the control board writer on 7331 (#1696) | T3 session (Claude, claude-haiku-5-5), started by Sage under mos-1008 | none, T3 only |
| Megavolt | Builder, MVP T1 item 6 (agent credentials and git identity, #1697) | T3 session (Claude, Opus 5.5), started by Sage under mos-0974/0977 | none, T3 only |
| Quackerjack | Builder, federation R5 and R2 (#414) | T3 session (Claude, claude-haiku-5-5), started by Sage under mos-1023 | none, T3 only |
| Gizmoduck | Federation design (#414) and builder of FED R1 | T3 session (Claude, Opus 5.5, thread 9312a896) | none, T3 only |
| Webby | Reviewer: Case 2 designs, rows 192 and 194 | T3 session (Claude, Opus 5.5, thread 03086006), started by Mos (mos-0981) | none, T3 only |
| Duckworth | Reviewer: federation designs, row 193 | T3 session (Claude, Opus 5.5, thread ffceafec), started by Mos (mos-0977) | none, T3 only |
| Sage | Project lead: coordination, review, integration; earlier DYOR strategy records retained | T3 session (Claude Code); Pi launcher `zai/glm-5.3:high` retained | `agents/sage/launch.sh` |
+287
View File
@@ -0,0 +1,287 @@
# Row 232 (I1): stack-release external units and bundle copies
Brief: `docs/plans/2026-10-20_installer-rows.md` section I1, blob `f35e7e57daee` (commit ad6f0be7a). Gitea
#1734, Vikunja P19 #432. Owner Darkwing, reviewer Filbert, gate owner Sage. Design input: row 195 DESIGN r6,
`agents/darkwing/work/queue-195/DESIGN.md` at b7239bb66 (approved in duckworth-0055). Where the brief and
r6 differ, the brief wins. sage-1561 ruled where the stack user's `stack-release.sh` runs: from its own
clone of the release bundle, so `check_target`, the plan's commit list and status drift are unchanged and
nothing adds `safe.directory`.
## Files
Base: cb5fdb064 (row 199's landing, on top of row 200's 9e1085816). Branch `darkwing/row232-r1`. The first
build, 0af0c73c6 on `darkwing/row232`, sat on 00d4cc1f6 (row 190); row 200 landed first, so this one is
rebased onto it (see D9 and D10).
- `scripts/stack-release.sh`: `--units user|external`, the `sc` and `jctl` backend, `confirm`, `revert`,
`gate --since`, `rollback --to`, `--plan --expect-from`, the external gate (start floor, PM check from
`launches list`), `external-pm`, and the `awaiting units:` status line.
- `scripts/stack-copy.sh`: `check --units external`, `copy --bundle FILE SHA`, `wrapper ID --create`.
- `packages/cli/tests/stack-release-external.test.mjs` (new, 25 tests).
- `packages/cli/tests/stack-release-fixture.mjs` (new): the helpers `stack-release.test.mjs` defined, moved
so both files share them. See deviation D1.
- `packages/cli/tests/stack-release.test.mjs`: the helpers leave and one import line comes in. The 74 tests
have the same names and the same bodies as on the base.
- `packages/cli/tests/stack-copy.test.mjs`: three new `row 232:` tests and a wider `node:fs` import.
- `docs/TOOLS.md`: the stack-copy usage block and three paragraphs, the stack-release usage block, and a new
subsection "Root-owned units (`--units external`, row 232)".
- `packages/cli/README.md`: one paragraph.
- This file.
The worktree's `node_modules` symlink is not part of the candidate.
## User mode is unchanged
Every `systemctl` call goes through `sc` and every `journalctl` call through `jctl`. Under `user` they
are `systemctl --user "$@"` and `journalctl --user "$@"`. Under `external`, `sc` allows only `show`,
`is-active` and `list-units`, and `jctl` returns 1, so an external run can't start, stop, install or read
the journal even through a path the tests miss.
Two tests hold the brief's "every command line and every byte of output":
- `user mode: every systemctl call carries --user, and activate, rollback and check make the base's
calls`. A fixture repo gets the base's two scripts from `git show cb5fdb064:`. The same activate,
rollback and `stack-copy.sh check` run from both trees, and the recorded argv (with the fixture root and
port replaced by placeholders) must match line for line. Every call carries `--user`.
- `user mode: a default activate --plan prints the base's bytes`, for four plans.
The base output is produced live from the base blobs, not stored as a fixture file. See deviation D3.
## r6 section 7, test by test
All in `stack-release-external.test.mjs` unless named.
| r6 :742-812 item | Test |
|---|---|
| `units-running` (N4), store, log and TAKEN left | `units-running (N4) refuses while a unit is active...`; also checks that `failed` counts as down and that the Console is checked first |
| activate leaves the pointer, writes `AWAITING-UNITS` and a backup | `activate stages: ...`, which also checks the set's file list, `UNITS.sha256`, no `--user`, no journal call and `status` |
| `open-activation` with zero, two, the wrong copy, one not open | `confirm and revert refuse open-activation ...` |
| `confirm` polls: 3 s fails at `--timeout 1`, passes at `--timeout 10` | `confirm polls: bus status good after 3 s fails ...` and `confirm polls to a pass ...` |
| `stale-start` floor with `started` at .900 | `stale-start: @1792540799 fails ...` and `stale-start is a gate code ...` |
| `confirm` pass writes pointer, ok, removes marker; timeout writes `FAILED` and exits 1 | the two polling tests |
| `activation-failed` when `FAILED` exists | `confirm polls: ... FAILED then refuses activation-failed` |
| lines carry the started op, from, to, activation and backup | `sameAsStarted` in every confirm test, and sequence 7 |
| `gate` (N2) with and without `--since` | `gate (N2, A2f): ...` and the usage test |
| `--plan --expect-from` (N5), activate and `rollback --to` | sequence 7 |
| A3 order: capture-failed, marker-failed, wrapper failure after migrate | `A3 order: ...` and sequence 2 |
| refusals log no activation; `interrupted` unchanged | `open-activation` test and sequence 5 |
| PM check: one, none, two, early, not live, other business, other instance; no journal call | `the external PM check reads launches list ...` (modes '', coder, none, twolive, early, dead, other) and `journalCalls` in every external test |
| `external-pm` | `pm-on and activate --pm-after-harness refuse external-pm` |
| `revert` restores TAKEN only after a migration | `revert puts the wrapper back and leaves the store ...` and sequences 2 and 8 |
| `confirm --rollback` per cause, `unrecorded`, `rollback-failed`, mismatch | sequences 1, 2, 3, 5 and 8, and `confirm --rollback refuses rollback-mismatch, and logs rollback-failed ...` |
| sequences 1-8 | `sequence 1:` to `sequence 8:`, each ending with the `closed` check (one started and one result line, empty `interrupted`, no marker or `FAILED`, a following `--plan` that doesn't refuse `interrupted-run`) |
| `rollback --to` refuses not-previous; `rollback --restore` finds an external backup | sequence 7, and `confirm --rollback after a --restore swap ...` |
| `gate` writes nothing | the `gate (N2, A2f)` test and sequence 4 compare the log, the pointer and the state tree |
| `confirm`, `revert`, `gate`, `--to` refuse without `--units external` | `confirm, revert, gate, --to and --expect-from need --units external ...` |
| default path: every call carries `--user`; default `--plan` bytes | the two user-mode tests above |
The brief's additions, in `stack-copy.test.mjs`:
- `row 232: copy --bundle copies a SHA the bundle's refactor reaches, byte-identical to copy SHA, and
refuses the rest`: a short SHA, a missing file, a symlink, a bundle that fails `git bundle verify`, a
bundle with no `refs/heads/refactor`, a SHA not in the bundle, and a SHA the bundle carries on another
branch that refactor doesn't reach. The bundle copy's tree matches `copy SHA`'s byte for byte, and the
`release:` and `bundle sha256:` lines are checked.
- `row 232: wrapper --create writes the first wrapper only, the same bytes wrapper ID writes, and refuses
over any file`, a symlink included. Without `--create` a missing wrapper still refuses.
- `row 232: check --units external reads the system units, the default reads the user units`.
## Mutants
`mutants.sh` in my scratch (not committed, it edits the worktree), 17 mutants: r6's re-pin list (:817-838)
minus the three `case1-repin.sh` ones, plus the brief's two. Each one applies exact replacements after a
checked backup, runs `bash -n`, runs its named killers, restores the file and checks its sha256. Two
controls run first with no change: the whole external file, and stack-copy's `row 232:` tests.
| Mutant | Killers |
|---|---|
| m01 `confirm` writes the pointer on a gate failure | `confirm polls: bus status` |
| m02 `gate_once` in place of `gate` | `confirm polls to a pass`, `stale-start is a gate code` |
| m03 PM check counts any instance | `external PM check` |
| m04 PM check drops the `startedAt` bound | `external PM check` |
| m05 `confirm` takes the newest `AWAITING-UNITS` | `open-activation` |
| m06 `rollback --to` skips the comparison | `sequence 7:` |
| m07 the default path drops `--user` from one call (`is-active`) | `user mode: every systemctl` |
| m08 the timeout writes r4's interim `rollback pending` line | `confirm polls: bus status`, `sequence 1:`, `sequence 5:` |
| m09 `AWAITING-UNITS` last, after the wrapper | `sequence 2:` |
| m10 a refusal names the open activation | `sequence 5:`, `open-activation` |
| m11 `stale-start` is a refusal | `stale-start is a gate code` |
| m12 `revert` restores without `MIGRATING` | `revert puts the wrapper back` |
| m13 `confirm` logs op `confirm` | `sequence 7:`, `confirm polls to a pass` |
| m14 `MIGRATING` after the migration | `sequence 8:` |
| m15 `gate` takes its floor from the log without `--since` | `gate (N2` |
| m16 `copy --bundle` skips the reachability check | stack-copy `row 232:` |
| m17 `wrapper --create` overwrites | stack-copy `row 232:` |
## Deviations and choices
D1. The shared helpers moved to a new file, `packages/cli/tests/stack-release-fixture.mjs`. The brief's
files list names `stack-release.test.mjs` "where a shared helper moves" but not a destination. Importing
from the other test file would run its 74 tests twice, so the helpers need a module of their own.
D2. The fixture's T17 check (what a run may never remove) now exempts a rollback set's `AWAITING-UNITS` and
`FAILED`. `confirm` removes both when it closes the activation (r6 A5 and R4). User mode writes neither, so
user-mode runs check exactly what they did.
D3. The user-mode argv and `--plan` tests read the base's scripts with `git show cb5fdb064:` rather than
storing their output as a fixture file. A later row that changes user-mode output on purpose moves the
`BASE` constant in the test to its own base, or the test fails. I chose this over a stored fixture because
a stored copy can drift from the base without anyone noticing.
D4. `marker-failed` is reached by making `state/stack-rollback` unwritable. r6 says "an existing `$RS`", but
`next_activation` never picks a taken name, so no test can make the run meet an existing `$RS`. The
exclusive `mkdir` is still what refuses.
D5. The stale-start polling case turns the enter time to `@1792540800`, not r6's `@1792540805`, with the
started line at 1792540800.900. That also checks that the floor is the whole second, so .900 doesn't push it
to 1792540801.
D6. No fake `sudo` or `case2-unit.sh`. r6's sequences ran under them because r6 had `case1-repin.sh` drive
the steps, and that script is I3's. Here the test plays root: it stops and starts the fake system units and
installs with the existing `bus-service.sh` and `webui-service.sh` stand-ins, between the real
`stack-release.sh` calls, on one log.
D7. The fake marker in the `open-activation` test is named `20000101T000000Z-<C1 prefix>` so it sorts
before the real one. A marker that sorted after it would leave m05 (newest marker) alive.
D8. m10 sets `ACT` inside `open_activation`, where r6's wording ("ACT set before the checks") lands in this
code. `ACT` is set only after every check in `confirm`.
D9. Row 200's `--sso` is a usage error under `--units external`, with the proof flags, `--no-proof` and
`--no-send`. It is a Console flag, and root's Console unit carries the Console flags; the script under
external renders no Console line and never calls `console_for_target`, so `sso_checks` doesn't run there
either. The usage text, the TOOLS.md bullet and the external usage test name it.
D10. Row 200's `config_checks` runs under `--units external` too, after `plugin_checks`, in both the
`--plan` and run paths of `activate` and `rollback --to`. It checks the target's loader against the stack
user's `config.json`, which the bus on a VM still reads, so a `console` key the target refuses would break
root's bus start the same way. The cost is one more refusal (`config-console-key`) an external run can
print; I took it over a bus that fails after root's start. The rebase changed no user-mode line: the
conflicts were the `SSO=0` globals line, the `plan` body (row 200's `SSO_PLAN` line sits in the user
branch) and the two `--plan` lines that gained `config_checks`. In the tests, row 200's fixture lines
(the loaders, `sso-config.mjs` and the `--sso` cli.mjs line for C1 and C) moved into
`stack-release-fixture.mjs` with the rest of the copy setup, and the `ALGS` import stays in
`stack-release.test.mjs`, which T57 uses.
## duckworth-0055's notes on r6 (brief line 169)
Duckworth's r6 approval (REVIEW-r5.md at 23f5520b5) carried two notes.
- N1, A2's stop timeout under `gate --since`. A unit that never stopped keeps its old `ActiveEnterTimestamp`,
so `gate --since` fails it with `stale-start`, and the run ends at exit 2 with root stopping the units.
That is intended: a unit that won't stop is a fair reason to stop. In I1 this is the gate code and its
polling (`stale-start: @1792540799 fails ...`, `gate (N2, A2f)`, m11). The run sheet's A2f line and the
re-pin test that points "exits 1 with OLD's gate passing" at an A3 exit 3 belong to I3, which owns
`scripts/stack-upgrade.sh` and `docs/guides/host-run-sheet.md`. I'll name both in the I3 build.
- N2, the claude path under `/opt/mosaic-stack` (gosalyn-0058, sage-1522). I1 doesn't touch it. Under
`--units external` the script checks no claude pin of its own (`no-claude-pin` applies only to user
mode), because root's unit carries it. The `--claude` value, the managed-settings reasoning, F6 and the
run sheet's claude step go to I2 and I3.
## Run environment
Worktree `/mnt/storage/scratch/darkwing-r232`, `TMPDIR=/mnt/storage/scratch/tmp/r232`, node from PATH.
One external file run takes about 94 s.
## Results
Singles on the rebased candidate, before the gate:
| File | Tests | Pass | Fail |
|---|---|---|---|
| `packages/cli/tests/stack-release-external.test.mjs` | 25 | 25 | 0 |
| `packages/cli/tests/stack-copy.test.mjs` | 16 | 16 | 0 |
| `packages/cli/tests/stack-release.test.mjs` (row 200's T57 included) | 192 | 192 | 0 |
| `packages/cli/tests/stack-instances.test.mjs` | 23 | 23 | 0 |
| `packages/webui/tests/pinned-copy.test.mjs` | 10 | 10 | 0 |
| `packages/cli/tests/channel-recovery.test.mjs` (imports `channel-fixture.mjs`, which names the scripts) | 1 | 1 | 0 |
| `packages/cli/tests/host.test.mjs` (reads `packages/cli/README.md`) | 42 | 42 | 0 |
These are every test file that reads `stack-release.sh` or `stack-copy.sh` (a grep of `packages/*/tests` and
`scripts/`). None of them pins a sha256 of a changed file. `packages/cli/tests/host.test.mjs` reads
`packages/cli/README.md` for a unit file name; this row only adds a paragraph there. The grep and the runs
were repeated on the rebased tree on 2026-10-21; the reader set is the same. A dry run of `mutants.sh`
matches each of the 17 replacements once on the rebased scripts.
### Gate, 2026-10-21, Sage's slot (sage-1618)
Candidate a0ffabc3f on `darkwing/row232-r1`, run by `~/darkwing-scratch/r232/gate.sh`: the cli suite, then
every `scripts/test-*.sh`, one after another, stopping at the first failure. busy.sh passed at 01:07:49Z
after Launchpad's single `stack-release.test.mjs` run ended. The gate stopped at the first one, so the
chain waited for that PID to exit and checked again. The EXIT trap removed the compose network; no
container was left.
| Step | Time (UTC) | Result |
|---|---|---|
| `packages/cli`, `node --test tests/*.test.mjs` | 01:07:50 to 01:17:53 | 561 pass, 0 fail, 0 skipped |
| `scripts/test-auth.sh` | 01:17:53 to 01:17:54 | 15 passed, 0 failed |
| `scripts/test-conductor.sh` | 01:17:54 to 01:18:00 | 17 passed, 0 failed |
| `scripts/test-config.sh` | 01:18:00 to 01:18:02 | 31 passed, 0 failed |
| `scripts/test-discord.sh` | 01:18:02 to 01:18:12 | 78 passed, 0 failed |
| `scripts/test-extension-package.sh` | 01:18:12 to 01:18:13 | 18 passed, 0 failed |
| `scripts/test-foundation.sh` | 01:18:13 to 01:19:01 | 44 passed, 0 failed |
| `scripts/test-land-work.sh` | 01:19:01 to 01:19:14 | 86 passed, 0 failed |
| `scripts/test-queue.sh` | 01:19:14 to 01:19:37 | 156 node tests pass, queue suite 28 passed, 0 failed |
| `scripts/test-release.sh` | 01:19:37 to 01:19:51 | 14 passed, 0 failed |
| `scripts/test-task.sh` | 01:19:51 to 01:20:44 | 98 passed, 0 failed |
### Mutants, 2026-10-21, same slot
`~/darkwing-scratch/r232/mutants.sh`, right after the gate. Both controls (the external file whole, and
stack-copy's `^row 232: ` tests) exited 0 on the unmutated tree. Each mutant was applied after a checked
backup and a syntax check, ran its killers from the table above, and was put back with its sha256
checked. The script stops on any restore mismatch, and it ran to its end.
| Mutant | Killer exit | Result |
|---|---|---|
| m01 pointer on gate failure | 1 | killed |
| m02 gate once | 1 | killed |
| m03 PM from any instance | 1 | killed |
| m04 no startedAt check | 1 | killed |
| m05 newest marker | 1 | killed |
| m06 --to not compared | 1 | killed |
| m07 one call without --user | 1 | killed |
| m08 interim pending line | 1 | killed |
| m09 marker written last | 1 | killed |
| m10 refusal names the open activation | 1 | killed |
| m11 stale-start refuses | 1 | killed |
| m12 revert always restores | 1 | killed |
| m13 confirm op | 1 | killed |
| m14 MIGRATING after migrate | 1 | killed |
| m15 gate floor from the log | 1 | killed |
| m16 bundle reachability unchecked | 1 | killed |
| m17 --create overwrites | 1 | killed |
17 of 17 killed, 01:22:16 to 01:24:32Z. I read the logs for m01 and m09, two of the quick kills: each failed on
its named test's assertion (m09 on `AWAITING-UNITS` in sequence 2), not on a load error.
## Round 2: rebase onto 8fede5fc6 (sage-1673)
Rebase only. Before the landing gate, a check of BASE..HEAD on the manifest paths found that row 230
(c8bc5cf4c) had changed one of them after the round 1 base, cb5fdb064. Sage ruled a re-round with a
scoped review, the same shape as row 210 round 3.
Branch `darkwing/row232-r2`: the two round 1 commits, a0ffabc3f and 84f1a91e6, cherry-picked onto
8fede5fc6 with no conflict, then this section. No code or test changed.
- `git merge-tree --write-tree origin/refactor darkwing/row232-r1` (refactor at 8fede5fc6) gives tree
837bde79236c with no conflict. The cherry-picked tip before this section has the same tree.
- `packages/cli/README.md` is a clean 3-way merge. Row 230's hunk is at lines 710-717 (the session credential
paragraph); this row's is a new paragraph at 773. With `git patch-id --stable`, the README diff from r1 to
r2 is 02b679e2, the same as row 230's README diff `cb5fdb064..8fede5fc6`. This row's README diff is
c395ecd2 on both bases (`cb5fdb064..r1` and `8fede5fc6..r2`).
- The seven code, test and doc paths other than the README hash the same as in manifest 7a3b4ea1, which
Filbert approved in round 1: `docs/TOOLS.md`, `packages/cli/tests/stack-copy.test.mjs`,
`packages/cli/tests/stack-release-external.test.mjs`, `packages/cli/tests/stack-release-fixture.mjs`,
`packages/cli/tests/stack-release.test.mjs`, `scripts/stack-copy.sh`, `scripts/stack-release.sh`.
This file also matched until this section was appended. Its other lines are unchanged.
- The one test that reads the README, `host.test.mjs:855`, looks for the unit's `EnvironmentFile=` line.
That line is unchanged context in row 230's hunk.
- No row 230 test reads `stack-release.sh` or `stack-copy.sh`, and no test of this row reads a row 230 file.
Nothing was run for this round. The landing gate runs on the r2 tip on Sage's slot.
Follow-up, doc-only (sage-1673): `scripts/case2-unit.sh:108` and `scripts/case2-precheck.sh:157` cite
`scripts/stack-copy.sh:8` and `:22` for `COPIES` and `copy_root`. With this row those lines are `:10` and
`:26`. Only comments are affected and no test reads them. Row 233 doesn't touch those scripts, so the fix
goes on the #1732 carry list.
+249
View File
@@ -0,0 +1,249 @@
# Row 233 (I3): root's upgrade driver and the host run sheet
Brief: `docs/plans/2026-10-20_installer-rows.md` section I3, blob `e61291bcf1c6` (commit a0e3d7cfd), re-pinned
by Sage (sage-1685, rev 2631) from `f35e7e57daee` (ad6f0be7a). The later blob only appends I4; the I3 text
is the same. Gitea #1735, Vikunja P19
#432. Owner Darkwing, reviewer Filbert, gate owner Sage. Design input: row 195 DESIGN r6,
`agents/darkwing/work/queue-195/DESIGN.md` at b7239bb66, sections 2, 4 and 7. Where the brief and r6
differ, the brief wins. Sage's rulings during the build: sage-1650 and sage-1652 (`--backup last`, the
`--no-rehearsal` mapping, `--rehearse-port`, hostcheck), and sage-1653 (`from_log newest`, the two external
plan refusals, the `BACKUP_STORE` line).
## Files
Base: e65a3ab4d, row 232's landing, on `darkwing/row233-r1`. The build started on ea1e85c7b
(`darkwing/row233`, row 232's candidate e6ca230d2 and its BUILD.md) before row 232 landed. The three row 232
files this row changes are identical on the two bases, so the uncommitted diff moved across unchanged.
- `scripts/stack-upgrade.sh` (new): `upgrade` and `rollback`, root's side of r6 A1-A5, A2f and R1-R4.
- `packages/cli/tests/stack-upgrade.test.mjs` (new, 20 tests).
- `docs/guides/host-run-sheet.md` (new).
- `docs/TOOLS.md`: a new subsection "Root's upgrade driver (`scripts/stack-upgrade.sh`, row 233)", plus the
row 232 lines below.
- Row 232 files, changed on Sage's rulings (the brief's files list doesn't name them):
- `scripts/stack-release.sh`:
- `rehearse --backup last` takes the newest backup the log records (`from_log newest`) and prints
`PASS backup-last`. With none it fails `no-backup`, exits 3 and makes no rehearsal directory.
A newest backup that isn't an activation id aborts `backup-unreadable`, exit 1.
- The rehearsal prints `BACKUP_STORE <activation id> schema <version>` before its migration step.
- `--plan` under `--units external` refuses `open-activation` when any rollback set holds
`AWAITING-UNITS`, and `migrate-required` for a migrating target without `--migrate`. Root reads
only the plan's exit, so these two have to be refusals there. User mode prints the same bytes.
- `packages/cli/tests/stack-release.test.mjs`: three new `row 233:` tests for `--backup last`.
- `docs/TOOLS.md`: `--backup ACTIVATION|PATH|last` in the rehearse usage, a paragraph on `last`, and a
paragraph on the two external plan refusals.
- This file.
## The brief, item by item
| Brief item | Where |
|---|---|
| Root runs from a root-owned checkout, never a copy | `head-mismatch` (A1); TOOLS.md; run sheet section 6 |
| I2's render with `/etc/mosaic-stack/<instance>.args`, `--copy` the one changed value | `read_args`, `args_with`; the args test |
| The args file changes only after a confirmed run | `prepare_args` before A2, `mv` after A5; sequences 1 and 7, m6 |
| `copy --bundle` as the stack's user before A1 | the `fetch`, `ff`, `copy` steps; sequence 1's sudo keys |
| A rehearsal before A2, a failed one refuses with nothing stopped | `rehearse`; three rehearsal tests, m4 |
| The root lock, held to the end, rollback included | `flock -n 9`; the lock test and sequence 3's probe, m1, m2 |
| A1 refuses `open-activation` | stack-release.sh's external plan; the AWAITING-UNITS test, m3 |
| `migrate-required` without `--migrate` | stack-release.sh's external plan; its test, m5 |
| The step log, 0640, one line per step | `logl`, `step`; every test checks lines against `STEP` |
| Exit codes 0-3 | `finish`, `refuse`; exit 4 is usage |
| duckworth-0055's notes | the next section |
| Root parses no stack user output beyond exits | the step log test; the one exception is sage-1653's `BACKUP_STORE` |
| The host run sheet | `docs/guides/host-run-sheet.md` |
## duckworth-0055's notes on r6 (brief line 169)
- N1, A2's stop timeout under `gate --since`. Row 232's BUILD.md left two pieces to I3. The run sheet's
A2f line is in section 6 ("When a run doesn't exit 0", exit 2). The test is `N1: a bus that outlives
A2's timeout keeps its old start`: a bus that ignores `stop` keeps its old `ActiveEnterTimestamp`,
`wait-down` logs `rc=1 ... state: active`, A2f restarts and gates, the gate fails `stale-start`, and the
driver stops both units and exits 2. Sequence 4 is the other half: an A3 exit 3 goes to A2f and exits
1 with the old copy's gate passing.
- N2, the claude path under `/opt/mosaic-stack`. Run sheet section 3 installs it there as root with
managed settings. The args file's `--claude` carries it, and `case2-unit.sh` already refuses a binary
that isn't root's or sits under the home. The driver passes the args file through and adds no claude
check of its own. F6 is I2's.
## Sage's rulings during the build
- sage-1650 and sage-1652. Root always runs the rehearsal and maps its exit:
- 0 goes on, or refuses `rehearsal-required` with `--no-rehearsal`.
- 3 refuses `no-backup`, or with `--no-rehearsal` logs `rehearsal-skipped` and goes on.
- Anything else refuses `rehearsal-failed`.
`--rehearse-port N`, default 7339. Hostcheck: a missing file or a nonzero `--check` refuses
`profile-drift`.
- sage-1653. `from_log newest`, and the external plan refusals `open-activation` and `migrate-required`.
Residual: the `BACKUP_STORE` id and schema land in root's step log, but nothing ties that backup to the
one the activation's own backup step takes later. A stopped backup before the upgrade would close that
gap; it's a later row, not this one.
## Tests
`stack-upgrade.test.mjs` runs the driver as the test's user against a scratch root, with the test seams
`STACK_UPGRADE_*`. The stand-ins are a fake `sudo` (logs its argv, checks the driver's form, then runs the
real command as the test's user, r6's B1 pattern), a fake `case2-unit.sh` render that installs and starts
the fixture units, a fake `systemctl` for root's system units, and a shell `hostcheck.sh`. The stack's
user's steps run the real `stack-release.sh` and `stack-copy.sh` from a clone made from a real bundle. The
units, copies and store are `stack-release-fixture.mjs`'s.
| Test | Covers |
|---|---|
| sequence 1 | confirm fails healthz, rollback with `--cause A5`, exit 1; every sudo call in the driver's form |
| sequence 2 | `activate --migrate` fails at the wrapper, store restored, `--cause A3`, exit 1 |
| sequence 3 | render fails, `--cause A4`; a probe run during `render-old` refuses `upgrade-running` |
| sequence 4 | stage exits 3, A2f restarts and gates the old units, exit 1 |
| N1 | a unit that won't stop: A2f's gate fails `stale-start`, exit 2, units stopped |
| sequence 5 | confirm of the wrong copy refused, `--cause A5`, exit 1 |
| sequence 6 | revert fails, exit 2; the next run refuses `interrupted-run` at its plan |
| sequence 7 | two upgrades and a `rollback --to` with `--restore`; the args file after each confirm; `to-not-previous` |
| sequence 8 | a migration that writes and fails, store back byte for byte, exit 1 |
| lock | a second run while the lock is held: one log line, no call |
| open-activation | any `AWAITING-UNITS` refuses at A1, nothing stopped |
| migrate-required | refused without `--migrate`, no rehearsal, nothing stopped |
| rehearsal-failed | the rehearsal's argv, exit 3, nothing stopped |
| `BACKUP_STORE` | the line after a match; four malformed lines log `BACKUP_STORE none` |
| no-backup | the real rehearsal with no backup refuses; `--no-rehearsal` logs the skip and goes on |
| backup-last | the real rehearsal names the newest backup; `--no-rehearsal` with one refuses `rehearsal-required` |
| args file | group write, other write, two `--copy`, an unknown flag, a symlink, `args-differ` |
| hostcheck | nonzero `--check` and a missing file refuse `profile-drift` before the plan |
| head-mismatch | checkout not at `--sha`; a commit the clone can't fast-forward to refuses `fetch-failed` |
| usage | twelve bad argvs exit 4 with no log and no lock |
The step log check (`STEP`) runs in every test that reads the log: each line must be a step line or the
result line, and none may hold the stack user's output (`stack-release:`, `PASS `, `FAIL `, `REHEARSAL`,
the fake rehearsal's text).
## Mutants
`~/darkwing-scratch/r233/mutants.sh`, not committed because it edits the worktree. It has the brief's six
mutants and two for sage-1650's mapping. The method is row 232's: a checked backup, exact replacements, a
syntax check, the named killers, then the file put back with its sha256 checked. One control runs first:
the whole `stack-upgrade.test.mjs` with no change. `DRY=1` matched each replacement once on the candidate
(2026-10-21).
| Mutant | File | Killers |
|---|---|---|
| m1 drop the flock | stack-upgrade.sh | `a second run while the lock is held`, `sequence 3:` |
| m2 release the lock when the rollback starts | stack-upgrade.sh | `sequence 3:` |
| m3 skip A1's open-activation check | stack-release.sh | `A1 refuses open-activation` |
| m4 skip the rehearsal | stack-upgrade.sh | `the rehearsal runs after the plan`, `sequence 1:` |
| m5 accept a migrating target without `--migrate` | stack-release.sh | `migrate-required` |
| m6 write the args file before confirm | stack-upgrade.sh | `sequence 7:`, `sequence 1:` |
| m7 `--no-rehearsal` accepted with a backup | stack-upgrade.sh | `refuses rehearsal-required` |
| m8 drop the `rehearsal-skipped` line | stack-upgrade.sh | `a real rehearsal with no backup` |
## Deviations
D1. The brief has A1 refuse `open-activation` for an open `AWAITING-UNITS` or a non-empty `interrupted`
set. The interrupted case refuses with row 232's existing code, `interrupted-run`; only `AWAITING-UNITS`
gives `open-activation`. Both reach root as `plan-refused`, exit 3 (sequence 6, the open-activation test).
D2. Root copies one line of the stack user's output into its log, `BACKUP_STORE`, on sage-1653. It
matches a fixed pattern first, takes the last match, logs `BACKUP_STORE none` otherwise, and decides
nothing on it.
D3. The stack user's steps carry no `--instance`. The driver takes `--instance dev` only, row 190's default
instance, which the stack user's commands select with no flag. A named instance's units are a later row.
D4. Most tests fake the rehearsal in the fake `sudo`: the fixture's stub copies can't pass a real one. Two
tests run the real rehearsal (`faults/real-rehearse`): no-backup and backup-last. `stack-release.test.mjs`
covers `--backup last` itself.
D5. Any nonzero `--plan` exit refuses `plan-refused`, exit 3, whatever the code. The stack user's output
above it names the refusal; root doesn't parse it.
D6. `rollback --restore ACT` is accepted without `--events`, as `stack-release.sh rollback` accepts it.
The brief's usage pairs them. `--events` without `--restore` is a usage error.
D7. The refusal of any `STACK_UPGRADE_*` seam when run as root has no test, because the tests don't run
as root. It is three lines at the top of the script.
D8. The fake `sudo` keeps the test's environment, where real `sudo` resets it. The driver passes
everything the stack user needs through `env PATH=...` and `-H`, and the fake checks that form.
D9. Exit 3 means nothing stopped, not nothing changed. A refusal after `fetch` leaves the stack user's
clone fast-forwarded and the new copy made. Both are the user's own state, and the next run reuses them.
TOOLS.md and the run sheet say so.
D10. The brief's usage has neither `--no-rehearsal` nor `--rehearse-port`. Both come from sage-1650 and
sage-1652.
## Decisions
- `rollback` has no HEAD check. Its target is an older copy than root's checkout, which already exists.
It fetches, copies and rehearses nothing (sequence 7).
- The new args file is written next to the old one (`.dev.args.XXXXXX`, same mode) before A2, so a full
`/etc` refuses before anything stops. It moves into place after A5. A2f and the rollback remove it; a failed move keeps it and names it.
- Every stack user step gets `--copies` explicitly, so the copies directory comes from the args file's
`--home`, never from the user's environment.
- The stack's user does the fetch, the fast-forward and the copy, so root never runs git on a tree that
user can write.
- A4 is the render, and `case2-unit.sh` starts the bus, then the Console. The forward path has no
`systemctl start` of its own; only A2f does.
## Run environment
Worktree `/mnt/storage/scratch/darkwing-r233`, `TMPDIR=/mnt/storage/scratch/tmp/r233`, node from PATH. One
whole `stack-upgrade.test.mjs` run takes about 56 s.
## Results
### Singles, run 1, 2026-10-21, Sage's slot (sage-1689)
`~/darkwing-scratch/r233/singles.sh` on `darkwing/row233-r1` (base e65a3ab4d, uncommitted tree), 03:48:20Z
to 03:59:00Z. These are the test files that read a changed script. `stack-instances.test.mjs` reads
`stack-release.sh`, so it joined the three named in the plan.
| File | Result |
|---|---|
| `stack-upgrade.test.mjs` | 20 pass, 0 fail |
| `stack-release.test.mjs` | 194 pass, 1 fail |
| `stack-release-external.test.mjs` | 25 pass, 0 fail |
| `stack-instances.test.mjs` | 23 pass, 0 fail |
The failure was my test, not the script. "row 233: --backup last takes the newest backup..." compares
`untouched(e)` before and after the run, and `untouched` includes `existsSync(e.log)`. The test took its
before snapshot ahead of `rlog()`, which writes the log, so that flag went from false to true. Every hash in
the snapshot matched. The fix takes the snapshot after `rlog()`. No other row 233 test calls `untouched`.
### Singles, run 2
`~/darkwing-scratch/r233/rerun.sh` on aacfa2d7a, Sage's slot (sage-1695), 04:13:05Z to 04:21:28Z:
`stack-release.test.mjs` whole, 195 pass, 0 fail, 0 skipped. The fixed test passed. The other three files
did not change after run 1. The gate and the mutants run next, in Sage's slot.
### Gate and mutants, 2026-10-21, Sage's slot (sage-1704)
`~/darkwing-scratch/r233/gate.sh` then `mutants.sh`, chained behind `busy.sh`, on d99166ade (code
aacfa2d7a; d99166ade changes only this file). Counts are an awk sum over `gate-r1.out`.
| Part | Time (UTC) | Result |
|---|---|---|
| cli suite (`node --test tests/*.test.mjs`) | 04:29:01-04:37:27 | 616 pass, 0 fail, 0 skipped |
| the 10 `scripts/test-*.sh` | 04:37:27-04:39:38 | selftests 429 passed, 0 failed; `test-queue`'s node part 156 pass, 0 fail |
| gate total | | 1201 pass, 0 fail, 11 suites, `GATE_PASSED` |
| c0 control | 04:39:38-04:40:35 | exit 0 |
| m1-m8 | 04:40:35-04:41:25 | 8 of 8 killed, every one exit 1 |
Every restore passed its sha256 check, and `git diff` was clean at d99166ade afterwards. The EXIT trap
removed the `darkwing-r233_default` network.
N1, found in the process sweep after the run: a `sleep 60` was left with its cwd in `packages/cli`.
`stack-upgrade.test.mjs:312` spawns `flock LOCK sleep 60`, and its `t.after` SIGKILLs only the flock, so
flock's child outlives the test by up to 60 s. It exited on its own. Row 232's T20
(`stack-release.test.mjs:370`) spawns its holder `detached` and kills the group with `process.kill(-pid)`.
The fix is the same two lines. That test is one of m1's killers.
Sage ruled it a fix now (sage-1707), and 08db6ac3f makes it, test only. After the gate's d99166ade
the only change outside this file is that test:
```
$ git diff --stat d99166ade 08db6ac3f
packages/cli/tests/stack-upgrade.test.mjs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
```
The gate stands on Sage's ruling. The rerun is the whole `stack-upgrade.test.mjs` singly plus m1, in
Sage's slot, with a sweep for a leftover `sleep` afterwards.
+173
View File
@@ -0,0 +1,173 @@
# Row 240 (I4): root's host check, its pins and the boot unit
Brief: `docs/plans/2026-10-20_installer-rows.md` section I4, blob `4a5a65020f00` (commit b00d7d94d, rev 2665,
the pin row 240 carries; the I3 carry items came in with it). Sage split it out of I3 (sage-1650). Gitea #1746, Vikunja P19 #432. Owner Darkwing, reviewer Filbert, gate owner Sage.
Design: row 195 DESIGN r6 B2, `agents/darkwing/work/queue-195/DESIGN.md` at b7239bb66, :262-303 (the script,
the three callers) and :839-860 (files, test cases, mutants). Where the brief and r6 differ, the brief wins.
## Files
Drafted on `darkwing/row240-r1` from d99166ade (row 233's candidate) while row 233's gate ran, then applied
as one patch onto row 233's landing, 57f59e0c3, on `darkwing/row240-r2` (sage-1753). It applied cleanly; between
the two bases the only change on this row's paths is 233's N1 test fix in `stack-upgrade.test.mjs`. Row 234
then landed as c771a36f4 on 57f59e0c3. Sage preferred that the gate run with it in the tree (sage-1773), so both
commits were picked onto c771a36f4 as `darkwing/row240-r3`. That was a clean pick: the 42 paths changed from
57f59e0c3 to c771a36f4 share none with this row's 9.
- `scripts/case1-pins.sh` (new, 0644): `PIN_PROFILES_VERSION`, `PIN_PROFILE_SHA256`, `PIN_BUBBLEWRAP_VERSION`,
r6 section 0's values.
- `scripts/case1-hostcheck.sh` (new, 0755): the seven items and `--check`, `--enforce`, `--upstream`.
- `packages/cli/systemd/mosaic-hostcheck.service` (new): the boot oneshot.
- `packages/cli/tests/case1-hostcheck.test.mjs` (new, 16 tests). The cli suite's `tests/*.test.mjs` glob picks
it up.
- `docs/guides/host-run-sheet.md` section 4: the install lines that replace "Install them from row 240's run
sheet lines once it lands", plus rollback, drift recovery and `--upstream` before a re-pin window. Sections
3, 6, 8 and 10 for the I3 carry items below.
- `scripts/stack-upgrade.sh` and `packages/cli/tests/stack-upgrade.test.mjs`: I3 N1 below.
- `docs/TOOLS.md`: a new subsection "Root's host check (`scripts/case1-hostcheck.sh`, row 240)" after the
upgrade driver's.
Brief line 257 said I3's `stack-upgrade.sh` doesn't change. Sage's ruling sage-1727 widened the row to it for
N1 alone. Its default `STACK_UPGRADE_HOSTCHECK=/usr/local/lib/mosaic-stack/hostcheck.sh` is where the run
sheet installs this script.
## Carried from I3
Filbert's row 233 approve (Gitea 29922) left five notes. Sage ruled N1, N4 and N5 into this row and N2 and N3
to DEFERRED.md (sage-1727).
| Note | Change |
|---|---|
| N1: `stack-upgrade.sh` exits 1 `args-failed` when the args move after A5 fails, but exit 1 means the old copy runs | The move's failure exits 5 `args-failed`. The header, TOOLS.md's step 5 and exit line, and run sheet section 6's exit list name exit 5 and what root does: move the new file the result line names into place. Test `row 240: I3 N1: a failed args move after A5 exits 5 ...` makes `/etc/mosaic-stack` read-only at confirm and checks exit 5, the step names, `args rc=1`, the result line, both units on the new copy, the old args file's bytes and the new file beside it. The step log regex takes `exit=5`. Mutant M8. |
| N4: section 3's `npm install` has no npm on root's PATH, and the `--claude` path is unnamed while `node_modules/.bin/claude` is a symlink `case2-unit.sh:140` refuses | The install line runs with section 2's pinned node first on PATH (the postinstall runs `node` too). The `--claude` path is `node_modules/@anthropic-ai/claude-code/bin/claude.exe`, and a `--version` line catches the postinstall's 500-byte stub. |
| N5: sections 8 and 10 lack precheck lines | Each gets "Precheck line: none" and its missing line. |
Exit 5 is new rather than 2 because 2 means both units are stopped, and here both run. Exit 4 stays usage.
The `claude.exe` path comes from the 2.1.304 package itself: `npm view` gives `bin.claude` as `bin/claude.exe`,
and its `install.cjs` hardlinks the platform binary over that placeholder, or copies it across devices, then
sets 0755. I downloaded the tarball into `/mnt/storage/scratch/tmp/r240/cc304` and read it; nothing ran. The
run sheet says a newer pin needs the same check.
## The brief, item by item
| Brief line | Where |
|---|---|
| 247 `case1-pins.sh` holds the three pins | `scripts/case1-pins.sh`; the pins test checks the values against r6's |
| 248-249 seven items, `--check` exits 0 or 1 | `items()` and the final dispatch; the check tests |
| 250-251 `--enforce`: `apparmor_parser -R`, `root:root 0700`, `DRIFT`, a log line, exit 0 | `enforce()` and `stop_bwrap()`; the four enforce tests |
| 252 `--upstream` compares the current deb to the pin | `upstream()`; the upstream test |
| 253 the boot oneshot | `mosaic-hostcheck.service`; the boot unit test |
| 254-255 fakes for every host tool, r6's seven case groups, seven mutants | the test file; Tests and Mutants below |
| 257 the I3 run sheet item points here | run sheet section 4 |
## Tests
Fakes on PATH, each logging its argv to `$HC/calls.log`: `aa-status`, `dpkg`, `dpkg-statoverride`,
`apparmor_parser`, `systemd-run`, `stat`, `chown`, `chmod`, `apt-get`, `dpkg-deb`. `host(t)` builds a scratch
root (`/etc`, `/proc/sys`, `/usr/bin/bwrap`, `/var/log`) and a lib dir with a copy of the script and fixture
pins that hold the fixture profile's sha256. Every run passes `--root`.
r6's case groups (:843-856) against the tests:
| r6 case | Tests |
|---|---|
| `--check` exits 0 with all seven, 1 with each failing alone, its line naming it | `check: exits 0 ...`, `check: exits 1 with each item failing alone ...`, plus one test per item for the item's edge cases |
| item 3: a second file naming `bwrap` as a word, `/{usr/,}bin/bwrap` included; passes with the profile alone | `check: item 3 ...` |
| item 4: `unpriv_bwrap (enforce)` or a trailing space fails; the argv carries `-p NoNewPrivileges=yes` and the 2a' properties | `check: item 4 ...` checks the whole argv |
| item 7: the floor at 732 fails | `check: item 6 ...; item 7 ...` (733 passes as the control) |
| `--enforce` exits 0 on a pass and every failure; unload, close, `DRIFT`, one log line; `DRIFT` present with every item passing still closes | `enforce: a pass ...`, `enforce: each failure ...`, `enforce: with DRIFT present ...` |
| `--enforce` never calls `dpkg-statoverride` with `--update`, `--add` or `--remove` | `enforce: no unload ...; never writes the statoverride` (every call is `--list /usr/bin/bwrap`) |
| a bubblewrap version other than the pin is a line, not a failure | `check: a bubblewrap version other than the install pin ...` |
| `--upstream` against a differing deb prints the difference, exits 0 | `upstream: ...` (also: a match, a deb with no profile, a failed download) |
| the unit: `Type=oneshot`, `After=apparmor.service`, `[email protected]`, runs `--enforce` | `boot unit: ...` |
The last test also checks that the repo script reads `scripts/case1-pins.sh` beside it, that bad usage exits
2, and that a non-root run without `--root` refuses.
Singles are the two files that run a changed script: `case1-hostcheck.test.mjs` and `stack-upgrade.test.mjs`.
Sage asked (sage-1759) whether `stack-release.test.mjs` or `stack-release-external.test.mjs`, which share
`stack-release-fixture.mjs`, run `stack-upgrade.sh` or `case1-pins.sh`. They don't. `grep -cF` for
`stack-upgrade`, `case1-pins`, `case1-hostcheck` and `hostcheck` gives 0 in all three files; the control
`stack-release.sh` gives 2 in the fixture and 8 in `stack-release.test.mjs`. `scripts/stack-release.sh` names
`stack-upgrade.sh` once, in a comment (:2061). The gate's cli suite runs both files anyway.
## Mutants
r6's seven (:858-860) and one for I3 N1, scripted in `~/darkwing-scratch/r240/mutants.sh` with r233's method: a checked backup,
an exact replacement that must match once, `bash -n`, the named killer by `--test-name-pattern`, the restore
and a sha256 check. A dry run matched all eight once each. Results below.
| Mutant | Change | Killer |
|---|---|---|
| M1 | `--enforce` exits 1 after the stop | `enforce: each failure exits 0 ...` |
| M2 | `[ ! -e "$DRIFT" ] \|\| had=1` becomes `true \|\| had=1` | `enforce: with DRIFT present ...` |
| M3 | item 1's `dpkg -S` becomes `owner=""; rc=1` | `check: item 1 fails when a package owns ...` |
| M4 | item 3's `grep -rlw bwrap` becomes `grep -rlF /usr/bin/bwrap` | `check: item 3 ...` |
| M5 | item 4's `[ "$label" = "$LABEL" ]` becomes a substring match | `check: item 4 ...` |
| M6 | item 4 drops `-p NoNewPrivileges=yes` | `check: item 4 ...` (argv) |
| M7 | item 7's `-gt` becomes `-ge` | `check: item 6 ...; item 7 fails with the floor at 732 ...` |
| M8 | `stack-upgrade.sh`'s `finish 5 args-failed` becomes `finish 1 args-failed` (I3 N1) | `row 240: I3 N1: a failed args move ...` |
## Deviations
- Repo name `scripts/case1-hostcheck.sh` (r6), installed as `/usr/local/lib/mosaic-stack/hostcheck.sh`, the
name in brief line 239 and I3's `stack-upgrade.sh` default. r6's apt hook line says `case1-hostcheck.sh`;
the run sheet's hook uses the installed name.
- `stat` is a fake too. r6 lists seven fakes; a test can't make a root-owned file, so item 5's `stat` reads a
fixture.
- Item 7 with no Console unit installed reads the port as 1023 and says so ("1023, because no Console unit
is installed"). Until I2 installs the unit the floor must be above 1023. A unit with no `--port` fails.
- Item 4 runs under `timeout 60`. A hung `systemd-run` would otherwise hang the apt hook and the boot unit.
## Decisions
- Fail closed where r6 is silent:
- pins with no 64-hex sha fail item 1;
- `dpkg -S` exit 1 (no owner) passes, exit 0 fails with the owner, any other exit fails with the code;
- a failed `aa-status` fails item 2 and counts as loaded, so `--enforce` tries the unload.
- `--root DIR` is the test seam (r6 :840). Root refuses it; a non-root user needs it. Both exit 2. Root runs
with `PATH=/usr/sbin:/usr/bin:/sbin:/bin`.
- `DRIFT` is append-only (`<stamp> <failed line>`), and `--enforce` never removes it. The log line is written
only when the stop runs: `<stamp> enforce stop items=<ids or none> drift-before=<0|1> unload=<rc=N|no-file|skipped> chown=N chmod=N`.
- `--upstream` exits 1 when the download or the unpack fails, since it then compared nothing. A deb without
the profile file is a difference (exit 0), per r6 :302-303.
- The bubblewrap note reads the version from `dpkg -s bubblewrap`.
- The run sheet's recovery steps (`chown root:mosaic`, `chmod 0750`, `apparmor_parser -r`, then `--check`
before root removes `DRIFT`) are mine. r6 says only that root removes `DRIFT` in a reviewed step.
## Run environment
Worktree `/mnt/storage/scratch/darkwing-r240b`, branch `darkwing/row240-r3` on c771a36f4, with `node_modules`
linked to the canonical checkout's. Node v26.8.1, kernel 7.2.2-1-cachyos, 16 CPUs, TMPDIR
`/mnt/storage/scratch/tmp/r240`, compose project `darkwing-r240b`. Sage's slot (sage-1773, 06:16Z). Each run is
one chain behind `busy.sh`, launched from `/`: `~/darkwing-scratch/r240/run-all.sh` checks the HEAD pin and a
clean tree, runs the singles, then the mutants (8 killed, none surviving, the tree clean after), then the gate
(the cli suite and every `scripts/test-*.sh`), each stopping the chain on failure, then sweeps `/proc` cwd links
for anything left in the r240 trees.
## Results
Run r1, 06:17:10Z on 47ec9fe2f, failed at the singles. `case1-hostcheck.test.mjs` passed 16/0/0, and
`stack-upgrade.test.mjs` went 20 pass, 1 fail. The failure was the N1 test's own `t.after` hook, which ran
`chmodSync(e.etc, 0o755)` after the fixture's cleanup had already removed the scratch root, so it threw ENOENT.
Every assertion in the test body had passed, exit 5 included. The fixture's cleanup runs `chmod -R u+w` on the
root before `rmSync`, so the 0555 dir never needed the hook. b45852daa drops it and changes nothing else. The
mutants and the gate didn't run. The sweep found nothing left. Sage counted r1 as this slot's one failure
(sage-1788).
Run r2, 06:20:41Z to 06:33:28Z on b45852daa, passed. The counts come from awk over the .out files
(`~/darkwing-scratch/r233/sum.sh`):
| Step | Result |
|---|---|
| `case1-hostcheck.test.mjs` | 16/0/0 |
| `stack-upgrade.test.mjs` | 21/0/0 |
| c0 control | exit 0 |
| M1-M8 | 8/8 killed, none survived, every restore's sha256 matched, tree clean after |
| `packages/cli` suite | 639/0/0 |
| `scripts/test-*.sh` | auth 15, conductor 17, config 31, discord 78, extension-package 18, foundation 44, land-work 86, queue 156 node + 28 selftest, release 14, task 98; 585 pass, 0 fail |
| Gate total | 11 suites, 1224 pass, 0 fail |
| Sweep | nothing left in the r240 trees; the compose network was removed |
This commit adds only these BUILD.md lines. No test reads BUILD.md.
+9
View File
@@ -0,0 +1,9 @@
# Gizmoduck
Design and builder seat, Claude Opus 5.5, a T3 thread (9312a896). Author of the federation design
(`agents/gizmoduck/work/federation/DESIGN.md` on `gizmoduck/federation-design`, #414). Builds FED R1, the
access.json loader and validator, under Mos's order mos-1023 item 1.
No launcher in this directory. The seat works in its own worktree and has no credentials of its own; until
the credentials row settles per-agent identity, it pushes only its own `gizmoduck/*` branches through the
repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
+310
View File
@@ -0,0 +1,310 @@
# Row 234 (#436a row a, Gitea #1740): host enrollment, store records and bus schema 3i to 3j
Gizmoduck, 2026-10-21. Reviewer Duckworth, gate owner Sage. Lands by 10-25, before the .53 and .54 installs
(mos-1134).
- Brief: `docs/plans/2026-10-20_host-credentials.md`, section "Machine grant row a: enrollment, store records
and migration 3i-3j", blob 64451be5 (queue rev 2617).
- Design: `agents/gizmoduck/work/machine-grant/DESIGN.md` r6, f9039682e on `gizmoduck/machine-grant-design`,
approved by Duckworth (duckworth-0064, REVIEW-r6.md at 8ea792256). Not reopened here. Cites "r6:" are lines
of that file.
- Grant event rule: #436b section 7, fa5658941 on `megavolt/row199`,
`agents/megavolt/work/436b/DESIGN.md:431-490`, file sha256
96510b83778e79a71b33d0d5f6fd0c806dfe74f6de315ff4a9f1fbe5620f6c69, approved by Duckworth (duckworth-0067).
- Base: origin/refactor 8fede5fc6, with rows 199 (cb5fdb064) and 210 (f5753c0e8) landed. Branch
`gizmoduck/row234`. Code cites are `path:line` at 8fede5fc6 unless they say otherwise.
## Names: r6 against section 7
r6's `grant.*` and `host.enroll.*` names are `body.action` values on `action.allowed` events, not event
kinds. The `events` kind CHECK isn't rebuilt. Four r6 names move under `grant.`:
| r6 | This row |
|---|---|
| `host.enroll.step` | `grant.host.enroll.step` |
| `host.enrolled` | `grant.host.enrolled` |
| `host.checked` | `grant.host.checked` |
| `host.removed` | `grant.host.removed` |
Only the consume record carries `decision`: `{operation: 'grant.host-enroll', decision}`. Every other
guarded body names the decision `grantDecision`, so r6's `decision` field in step, enrolled and failed bodies
is `grantDecision` here. The actor of every guarded event is `actor_role` NULL with `actor_run` 'grant',
and `body.by` names the caller the broker checked.
## Split (r6 section 12 Q-S4)
Row a builds:
1. **Schema 3i to 3j.** `migrations/3i-3j.sql` adds `hosts` and `host_grants` (r6 S1, primary keys
(business, host) and (business, agent, host)) and the guard trigger: an `action.allowed` whose
`body.action` or `body.operation` GLOBs `'grant.*'` aborts unless the actor is (NULL, 'grant').
`SCHEMA_VERSION` 3j, a migration test with a step that fails partway.
2. **Grant event writer.** A private session-less broker writer for guarded events. `#event` doesn't change.
`recordEvent` refuses `reserved-event` for the guarded namespace.
3. **The `grant` raise frame.** `decision.raise` takes `grant` only with `credential.mint`, only from the chief
(`grant-chief-only`), and in row a only with `method: 'host-enroll'`. Any other method refuses
`invalid-request` until row b adds `host`. E1's checks, the `ssh-keyscan` set compare before the raise
transaction, and the rendered block.
4. **Enrollment executor.** `#consumeGrant`, the start on the human's cli yes (X1), E2 steps 1 to 4, the
installer key at `<configDir>/secrets/hosts/<business>/<hostId>/installer`, the failure cleanup, and the
start reconcile for enrollments: the `registry` step undo, and a resolved-yes unspent enrollment ending
`grant.failed interrupted` (X3).
5. **`hosts.check` and `hosts.remove`** (E4), with `mosaic hosts status|check|remove`.
6. **`tools/host/`**: `mosaic-keys`, `mosaic-keys-write`, `mosaic-keys.sudoers`, `mosaic-keys.sshd.conf` (E3).
7. **`host.run` on the store** (X4 first and third bullets): it reads the `hosts` and `host_grants` records
and the Approve record check, and at start the runtime gives `Credentials` a reference per `host_grants`
record. The loader refuses `hosts` at the top level and in a role (S1). Row 210's tests move to store
fixtures.
8. **Reader filter** (duckworth-0067 N1): every grant-evidence reader in this row (status, the start
reconcile, `openGrantDecisions`) filters `kind = 'action.allowed' AND actor_role IS NULL AND actor_run =
'grant'`, with a test that an `action.refused` look-alike and a session-written one are ignored.
`hosts.remove`'s `hostLineLeft` reader is one of them (duckworth-0079 M4).
Row b keeps the `host` method's raise binding (R4), the grant executor (X2), the grant rows of X3, X5,
`Credentials.register`/`claim`/`abortClaim`, revoke (section 6) and `sweep`. So row a has no way to make a
`host_grants` record outside a test fixture, and from its landing to row b's no agent holds a host grant
(r6 N9, the run sheet strips row 210's keys).
Tests in row a (r6 section 10): G1 and G5 for the `host-enroll` raise, G6 and G10 for the enrollment
executor, G11's enrollment cases, G14, G15, G16, G17, G18, G20, G21 (its `host-line-left` case reads an
event fixture until row b writes `hostLineLeft`), G25, the migration test, the trigger cases, the reader
filter test, and row 210's T-tests on store fixtures. Row b: G2 to G4, G7 to G9, G11's grant cases, G12,
G13, G19, G22 to G24, G26 to G28.
## Early check items (duckworth-0079, REVIEW-build-split.md 10672772e on `duckworth/reviews`)
Moves into row a:
- M1. G4 for the `host-enroll` block (r6:240-242): a question can't forge the block, and the 4096-character
limit holds with the block appended. G4 for the `host` block stays in row b.
- M2. The tag function (r6:265-266), because E2 step 2 tags the installer line. G24's first sentence moves
here: names of 64 characters with `.` and `_` give distinct tags of the fixed length. The `sweep` half
stays in row b.
- M3. A store-fixture test that `host.run` refuses `host-not-granted` for no record, an unresolved decision,
a decision resolved by an arbiter, the tester or an agent or not via cli, a consume record with operation
`grant.host-enroll` in place of `grant.host`, and an unspent decision (X4 first bullet, r6:593-596). A
restart test that a `host_grants` fixture gives `Credentials` a reference (X4 third bullet).
G21's `host-line-left` case:
- F1. The fixture goes through this row's private grant writer in the real shape: `action.allowed`, actor
(NULL, 'grant'), `body.action` 'grant.failed', `body.host`, `body.hostLineLeft`. No raw SQL around the
trigger.
- F2. `hosts.remove` reads `body.hostLineLeft` from any guarded body, not only `grant.failed`.
- F3. Decided: a guarded body that carries `hostLineLeft` also carries `host`. Row b adds `host` to
`grant.revoke.closed`, so its body is `{tag, host, hostLineLeft}` (r6:716 has `{tag, hostLineLeft}`).
The reader matches on `body.host`, and doesn't map tags back to hosts through `grant.step`. A direct
field is one equality in SQL, and a tag lookup would add a second reader that the N1 filter also has to
cover.
- F4. Row b carry: "no later `sweep` cleared it" (r6:403) needs `grant.sweep`, which only row b writes. Row
b adds the sweep clear to `hosts.remove` with a G21 case, and its G8 or G13 shows `hosts.remove`
refusing after a real `hostLineLeft` write, not only after the fixture. Until then row a's check is "any
guarded `hostLineLeft` for this host refuses".
## Mutants (r6:838-883, numbered here)
Row a, killed on this row's tree:
| # | Mutant |
|---|---|
| A1 | drop the chief check |
| A2 | start the executor on a `credential.mint` with no `grant` |
| A3 | start it on an arbiter resolve |
| A3b | start it on a `cli` yes by someone other than the human (duckworth-0083 N8) |
| A4 | start it on `no` |
| A5 | skip the keyscan compare |
| A6 | compare only one key |
| A7 | write the private key into the event body |
| A8 | drop the "not yet enrolled" check in E2 step 1 |
| A9 | drop the `hosts` primary key |
| A10 | skip the start check for a resolved, unspent enrollment decision |
| A11 | `host.run` reads the business file instead of the grant record |
| A12 | build the tag without the business |
| A13 | drop `from=` in the writer |
| A14 | let the writer touch a line outside the tag pattern |
| A15 | drop `O_NOFOLLOW` on the user file |
| A16 | drop one group from the writer's refused list |
| A17 | drop the user pattern check before the path use |
| A18 | treat a `privileged` entry as enough without `users` |
| A19 | let the writer accept a sudo user not in `privileged` |
| A20 | trigger: drop the `body.operation` arm, so the consume record passes from a session |
| A21 | trigger: accept actor_role NULL with any actor_run |
| A22 | a grant reader drops the `kind = 'action.allowed'` or actor filter. The runner splits it: A22a drops the kind, and G21's `action.refused` look-alikes kill it. A22b drops the actor, which the trigger can't stand in for, because 3i-3j adds the trigger without checking old rows. Its killer is the grant-migration test "A22b, sage-1703": a session-written `grant.host.removed` with `hostLineLeft` and a `grant.host-enroll` row from 3i, migrated to 3j, where `hosts.remove` still removes the host and `reconcileGrants` ends nothing |
| A23 | `host.run` skips the Approve record check (any `host_grants` record is enough) |
Row b keeps the rest of r6's list: the `ready` check, the three hash compares, the definition check, the
install and record order, `remove` on a verify failure, both start sweeps, the reconcile's remote `remove`,
the `grant.step` before install, revoke leaving the record, `register` before `issue`, the abort check and
the `await` before `issue`, revoke and an unreferenced claim, `sweep` and any `mosaic-` line, revoke's host
step order, claim before consume, the resolved unspent grant decision, an async `commit`, `hosts.revoke`
through `credential-verbs.mjs`, the revoke marker reconcile, the upsert, the undo past `grant.revoked`, the
abort after step 5 and a missing `bus.tester` as dev.
## Broker functions this row touches (sage-1677)
For row 236 (Megavolt), which rebuilds on this landing. Old lines are `packages/bus/src/broker.mjs` at
8fede5fc6, new lines are at the last commit that changed this file (they moved by 10 after `runEnrollment`
went in). The planned list is kept in this file's history (827b89ea6).
Changed:
- `constructor` (:279): one more option, `hostGrant`, the object `host-grant.mjs` builds.
- `recordEvent` (old :1243-1251, new :1606): one more refusal. An `action.allowed` whose `body.action` or
`body.operation` is a `grant.*` name refuses `reserved-event`.
- `request()` (old :1362, new :1726): two routes ahead of the sync path, a `decision.raise` with `grant` to
`#raiseGrant` and the `hosts.*` verbs to `#requestHosts`. Both are async.
- `requestHost` (old :1712-1795, new :2079): the business-file lookups (old :1730-1736) become one
`#hostAccess` call, in the same transaction as `#agent`. `commands` and `entry` come from it.
- `#dispatch` (old :1796, new :2160) gets a fifth parameter, `enroll`, which only `#raiseGrant` passes.
- `decision.raise` (new :2244): `grant` joins the allowed keys, and `grant` without `enroll` (or the
reverse) refuses `invalid-request`. The `host.sudo` block reads `#hostAccess` in place of `b.hosts`
and the role's `hosts`, and an undefined `target` still refuses `host-unknown`. `validArgv` and
`validStdin` now run after the access check, so a malformed argv for an ungranted host refuses
`host-not-granted` first. The enrollment block is appended to the question before `string(question)`,
so the 4096 cap counts it (M1). The decision's raise body carries `grant: enroll`.
- `decision.resolve` (new :2409): when `op` is `resolved`, `#approvedGrant` reads the decision, and a
`host-enroll` grant starts `#runEnrollment` from `setImmediate`, after the transaction commits. The
result adds `grant: 'started'`.
New, all private but two:
- `#grantEvent` (:657), the writer of guarded events. It runs the secret check, then `writeGrantEvent`.
- `#approvedGrant` (:667), `#consumeGrant` (:678). `#consumeGrant` sits after `#approvedGrant`, not next
to `#consumeAuthority` as planned.
- `#hostRecord` (:685), `#hostAccess` (:692), `#grantNotice` (:708), `#enrollFailed` (:736),
`#runEnrollment` (:750), `#openGrantDecisions` (:823).
- `runEnrollment` (:745), public, a trusted entry to `#runEnrollment` for G10. No verb reaches it.
- `reconcileGrants` (:833), public, which `runtime.mjs` calls once at start.
- `#raiseGrant` (:866), `#hostsCaller` (:886), `#requestHosts` (:900), `#hostLinesLeft` (:964).
Module level, new (:184-230): `GRANT_RUN`, `grantName`, `GRANT_EVIDENCE` (the N1 filter as SQL),
`registrySha256`, `writeGrantEvent` (exported, the one writer of guarded events, which test fixtures use
for F1), `HOSTS_VERBS`, `fingerprintOf`, `INSTALLER_COMMENT`.
Unchanged: `#event`, `#spent`, `#consumeAuthority`.
Other `src` files:
- `bus/src/host-grant.mjs`, new: the keyscan, the installer key pair, and `hosts.check`'s `list`.
- `bus/src/runtime.mjs`: builds `hostGrant`, calls `reconcileGrants` before the server starts, and adds
the `host_grants` references before `new Credentials` (X4 third bullet).
- `bus/src/host-run.mjs`: `runHost` takes `command`, sent as is in place of the sudo argv, for the
installer's `list`.
- `bus/src/server.mjs`: the `hosts.*` verbs and a raise with `grant` go down the async path.
- `bus/src/store.mjs`: the 3i-3j step, `SCHEMA_VERSION` 3j. `bus/src/credentials.mjs` exports
`privateDir`.
- `business/src/business.mjs` and `business/src/hosts.mjs`: the loader refuses `hosts` (S1).
`checkHosts` and `checkHostGrants` are gone.
- `cli/src/hosts.mjs`, new, routed from `cli.mjs` and `scripts/mosaic`.
## Decisions taken in the build
- **`#hostAccess` binds the agent and the host.** X4 says the Approve record check passes when the
decision is an approved `host` grant with a `grant.host` consume. The check also requires the grant's
`agent` to equal the session's role and its `host` to equal the requested host. Without that, an
approved grant for one agent and host would vouch for any `host_grants` row that names its decision.
Cost: none for row b, whose executor writes the row from the same grant.
- **Store fixtures are written the real way.** `bus/tests/host-fixtures.mjs` inserts the decision rows by
SQL, but every guarded event goes through `writeGrantEvent`, so the trigger sees it (F1). An enrollment
fixture writes `grant.host.enrolled`, or the start reconcile reads it as interrupted.
- **The resolver test for host kinds is gone.** `business/tests/hosts.test.mjs` lost its D5 case, because
the loader now refuses `host.<id>` credential refs. The `isHostKind` filter in `business/src/resolve.mjs`
can't be reached now. I left it in, since row b may want it back, and it's one line.
- **Store before Credentials.** The runtime already opened the store before `Credentials`. The
`host_grants` references depend on that order, and the comment there says so.
- **CLI.** `mosaic hosts` prints tab lines, and `--json` prints the bus rows. Usage errors exit 4 and bus
refusals exit 2, as `mosaic credentials` does. There is no enroll command: enrollment is the chief's raise.
- **Host tools tests live in the bus suite.** `bus/tests/host-tools.test.mjs` loads the writer by path
through a Python harness under `python3 -B -I`. The harness passes its own key dir, config dir, owner
uid and user, group and sudo lookups, and runs `mosaic-keys` from a copy with stub `sudo` and `logger`.
The gate runs the bus suite, so G16 and G25 run there. The real `pwd`, `grp` and `sudo -l` paths are
checked on the #433 VM, not here. The test also runs `visudo -c` on the sudoers file and `sshd -T` on
the drop-in.
- **Writer details the design left open.** The `from` file must hold exactly one value matching
`[0-9A-Za-z.:/*?!_-]+(,[0-9A-Za-z.:/*?!_-]+)*`, so it can't close the quote. `users`, `privileged` and `from` must be
regular, owned by the owner uid and not group or world writable, and are opened without following a
symlink. Lines starting with `#` are comments. A refusal exits 1 with its code on stderr and in syslog.
`add` of a tag that is already there exits 0 and writes nothing.
- **`runEnrollment` is public for G10 (duckworth-0081 B1).** G10 needs a second start for a spent
decision, and the resolve path can't give one: a second resolve refuses `decision-closed`
(broker.mjs:2416) before the executor runs. So
`#runEnrollment` returns its code (`enrolled`, `host-enrolled` or the refusal), and a public
`runEnrollment` calls it, as `reconcileGrants` is public. The test checks a spent
decision gives `decision-consumed`, and an open or agent-resolved one gives `not-authorized`, with
nothing written. That second case also reaches the `via` and `by` checks in `#approvedGrant`, so A3 is
no longer vacuous (N1).
Who can call it (sage-1692): only code holding the `Broker` object, which is the bus process and the
tests. The one `new Broker` in `src` is `runtime.mjs:126`, and no `src` file calls `runEnrollment`;
the resolve path calls the private `#runEnrollment` from `setImmediate` (broker.mjs:2445). A seat
holds no `Broker`. It reaches the bus only over `broker.sock`, where `server.mjs` calls a fixed set of
methods by name (`request` at :134 and :140, `identity`, `disconnect`). `request()` routes on the verb
through the `#dispatch` switch, and neither file looks a method up by name (no `this[` in either), so no
verb string can reach `runEnrollment`.
- **A `sudo -l` that times out refuses `user-sudo` (N2).** `_sudo` returns an empty answer on
`TimeoutExpired`, which isn't the expected line, so `main` logs the refusal and exits 1.
- **Keyscan comment (N3).** A scan that can't start refuses `host-failed`. One that runs and finds no key
returns no lines, and the raise refuses `host-key-mismatch`. The comment in `host-grant.mjs` now says so.
- **No tag on in-run `grant.failed` (N4).** r6:578 tags only the `interrupted` one, so the code follows it.
- **Run sheet (N5).** The writer refuses `config-privileged` when the file is missing, so each host needs an
empty, root-owned `/etc/mosaic-keys/privileged` (0644) before the first enrollment.
## Mutant results (Sage's slot, 2026-10-21)
All 25 killed: A1 to A23, with A3b added for duckworth-0083 N8 and A22 split into A22a and A22b. The
runner is `mutants.py` outside the repo. Each mutant gets its own session, a 420 s limit with a group kill,
and a byte restore checked against git status. None hung. Run 1 (04:24Z, on 95f966b1c) killed A1 to A22a
and stopped at A22b, which survived. Run 2 (04:43Z, on 097892741) killed A22b and A23.
- Each kill comes from a named assertion in the mutant's own test files, except A9. Dropping the `hosts`
primary key leaves `host_grants`' foreign key with no parent key, so the store refuses to open
("foreign key mismatch") and 28 of 29 tests fail at setup. That's a structural kill, not the S1 test.
The schema can't exist without the key (sage-1709).
- A22b drops the actor filter from `GRANT_EVIDENCE`. The trigger can't stand in for it, because a 3i store
can hold a session-written grant row that the 3i-3j step keeps. The grant-migration test "A22b,
sage-1703" kills it (see the A22 row above).
Follow-up, not in row a: G18's control calls `runHost` directly, so nothing runs `hg.check` end to end
with a good pin (duckworth-0083 N9). The #433 VM run covers it.
## Status at 097892741
Done: every Split item, 1 to 8, duckworth-0081 B1 to B3, duckworth-0083 N8, and all 25 mutants killed.
Single files in the slot, all exit 0, 178 pass and 0 fail by an awk sum over 23 logs: bus grant-enroll
24, host-run 24, grant-migration 6, host-tools 6, account-migration 3, chief-of-staff 18,
comment-migration 5, hard-waits 13, name-migration 1, project-runtime 3, spec-migration 1,
thread-migration 3, usage-migration 1, project-files 8, spec-socket 6, spec-review 12, land 17, spec 4;
harness spec-tools 2, raise-domain 2; business hosts 4; cli hosts 5; seat sandbox 10.
Left: Sage runs the whole bus, business and cli suites and every `scripts/test-*.sh` at the landing gate,
after Duckworth approves.
## Round 3 (sage-1742): two tests that count the schema
The landing gate failed `packages/bus/tests/schema.test.mjs:522`. It counts the tables and triggers that
`schema.sql` creates, and row a adds two tables and a trigger, so the counts go from 16 to 18 tables and
from 70 to 71 triggers. My singles didn't run it because I took it for a test of other rows' tables.
I grepped `packages/*/tests/*` for `sqlite_master` and `schema.sql`, with `schema.test.mjs:517` as the
positive control. That turned up one more test that would fail: T46 in
`packages/cli/tests/stack-release.test.mjs:910`. It puts every schema table in exactly one of four lists, and
hosts and host_grants were in none. I'd left the stack-release tests out of the r2 singles on the claim that
they don't load the bus, but T46 reads `schema.sql`. Both tables go in its untimed list (Sage's ruling,
sage-1745: the message_threads pattern). Neither has an `at` column. Row a writes hosts at five lines,
and each runs in one store transaction with a `grant.*` events row, which the restore guard counts.
`#grantEvent` (broker.mjs:657) writes that row through `writeGrantEvent` (:201, INSERT INTO events at
:212).
- :737 DELETE, in `#enrollFailed`, with grant.failed at :738. Both callers wrap it in a transaction,
:816 and :849.
- :763 INSERT, in the step 1 transaction (:754), with grant.host.enroll.step at :761.
- :783 UPDATE installer_fingerprint, in the step 4 transaction (:780), with grant.host.enrolled at :784.
- :932 DELETE, in the hosts.remove transaction (:927), with grant.host.removed at :933.
- :950 UPDATE state ready, in the hosts.check transaction (:944), with grant.host.checked at :951.
Row a writes no host_grants rows. It only reads them (broker.mjs:695, :930, runtime.mjs:103). Row b adds
the writes, and each must share a transaction with a `grant.*` event the same way. That goes on row
235's carry list. The other hits pin an older schema
(account, comment, name, spec, thread and usage migrations, chief-of-staff 3c) or count only their own
tables (state.test.mjs:106 reads every table but pins no list, threads.test.mjs:129).
hosts and host_grants are mutable by design, so the refused-labels loop above `schema.test.mjs:517` gets no
entry for them. hosts.check updates a host to ready. hosts.remove and a failed enrollment delete the
record. Row b updates and deletes grant records. Neither table has append-only triggers. The one new
trigger, events_grant_actor, guards events. grant-migration.test.mjs tests it, and it killed mutants
A20 and A21.
+11
View File
@@ -0,0 +1,11 @@
# Gosalyn
Worker seat for Mosaic Stack, MVP track T2 (Case 2, the creative writing hub, Vikunja #416).
- T3 thread: 7604438a-0ded-41c8-bd3f-26f57fdb5cd2
- Model: Opus 5.5 (Claude Code, T3)
- Launched by Mos (Jason's main agent, T3 38bc4c81-6270-4021-b00c-6a15c43b9b0e), who briefs the seat
- Repository lead and queue: Sage (T3 1ef1e4f8-3ead-4208-beca-38f9f1add079)
- Branch: gosalyn/case2-design, pushed only by this seat, landed on refactor by Sage
Work lives in `work/`. The Case 2 design is `work/case2/DESIGN.md`.
+104
View File
@@ -0,0 +1,104 @@
# Row 230: C2-13a unit render and host precheck
Gitea #1732. Vikunja #416 (track T2). Brief: docs/plans/2026-10-20_case2-rows.md, section "C2-13a",
blob f6b4933f0f47 (01b40d357, sage-1562). Base: 00d4cc1f6 (row 190). Reviewer: Webby. Gate: the cli
suite plus every scripts/test-*.sh.
## What ships
`scripts/case2-unit.sh` renders `[email protected]` and `mosaic-webui.service` from the two existing
templates into the system unit directory, for the default instance only. It writes each unit through
a temp file at mode 0644, runs `daemon-reload`, enables both units, starts the bus and then the
Console. If the bus fails to start, the script refuses and leaves the Console unstarted. Both units
get `WantedBy=multi-user.target`, and neither gets `After=` or `Wants=`. The script never links a unit
file into place. A re-install refuses unless each installed unit passes the same checks, both units
name one copy, and each equals the render apart from the copy id.
`--profile library` (the default) refuses `--pm`, `--claude`, `--tester-port` and `--sso`, and needs
the preview proxy flags plus the proof pair. `--profile factory` refuses the six proxy flags and takes
`--pm` and `--claude`. Both refuse `--checkout` and `--board`.
`scripts/case2-precheck.sh --business ID [--profile factory|library]` runs as `<user>`. It prints one
`ok` or `REFUSED` line per check and exits 1 if any check refused. It prints variable names only,
never values.
`packages/cli/README.md:713` now says nothing creates the session env file, only the Case 2 precheck
reads it, and the precheck prints only variable names (webby-0034 N2, webby-0035).
## Deviations for review
Sage accepted all six for the build (sage-1570). Webby rules on each.
1. The precheck takes `--business ID`. The brief names no other way to find the business file.
2. A run root `<dataRoot>/launches/<business>` that doesn't exist passes, with a note that the first
launch creates it at 0700 (session.mjs:47 and newRun). A symlink refuses, and so does an existing
entry that isn't a 0700 directory.
3. The claude marker check passes when the reply holds the marker and input plus cache tokens are at
most 2000. **The 2000 ceiling is unmeasured.** The tests use fakes at 40 and 9000 tokens. Measuring
it on a real claude belongs to row 195's install run, not this row.
4. A non-root test can't create a root-owned proof file. For the "proof not owned by the user" case,
the fake getent reports another uid for the user, so the user's own file reads as someone else's.
5. The unit script refuses `%h` left in any unit, rendered or installed. A system unit expands `%h`
to root's home. The render replaces every `%h`, so the test edits an installed unit to reach it.
6. With `--auth-header X-Authentik-Uid`, the allowed user must be 64 lower-case hex characters (lead
decision 101). The test covers both the refusal and the pass.
## Tests (packages/cli/tests/case2-unit.test.mjs, 30 cases after round 2)
The tests use a scratch unit directory and a scratch home, plus fake systemctl, getent, sudo, id,
node, bwrap and claude. They never touch /etc/systemd, ~/.mosaic-dev, ~/.config/mosaic-dev or a
live unit. Every precheck case asserts its output doesn't hold the fixture key.
- Unit: the clean library render, PATH for a node outside the default list and for /usr/bin/node, the
systemctl order, a failed bus start, no `systemctl link`, factory `--pm` and `--claude` rendered and
absent, re-install to another copy, edits to an installed unit, template restrictions,
EnvironmentFile placement, unknown tokens, PermissionsStartOnly and `+`/`-+` Exec prefixes,
symlinked unit paths, bad `--node`, re-install drift, a split copy pair, bad copy ids, every library
and factory flag refusal, and bad `--claude`.
- Precheck: a good host, node older than 24, more than one group, sudo succeeding, XDG_DATA_HOME,
HOME mismatch, a run root mode, a bad config, bwrap failing, the port floor at or below the Console
port, a missing Console unit, bus.tester per profile, profile-library-business, the claude check
(no route, replaces, appends, missing, the unit's MOSAIC_CLAUDE_BIN beating PATH), and the session
env file (missing, 0644, symlink, empty, emptied by a later line, quoted).
- Bind (mos-1100): inside `binds()` plus `sandboxArgv()`, a bind on port 980 gets EACCES, and a bind
on a free port of 1024 or above succeeds. The test fails if the host's floor is at or below 980.
## Verification
Round 1 singles, run before the gate: case2-unit.test.mjs passes 28/28 with no skips, and host.test.mjs (it
reads the cli README) passes 42/42. No test pins a hash of the README. No other test reads the two
scripts.
## Round 2 (webby-0046, sage-1589, sage-1590, sage-1591)
Base: origin/refactor (rebased from 00d4cc1f6; the 15 commits between touch none of the five paths
or the two unit templates). Branch gosalyn/c2-13a-r2.
B1. The precheck now has a case for each refusal Webby named: the user missing from passwd (:77),
a business file that is unparsable, missing or fails the schema (:137), a run root that is a symlink
to a 0700 directory (:151), a Console ExecStart with no --port (:179), an unreadable port floor
(:180), a claude reply without the marker (:223) and a claude reply that isn't JSON (:224). The fake
claude gained `nomarker` and `text` kinds. The no-claude-code case and the missing-claude case share
a PATH without `/usr/bin/claude` (`pathWithoutClaude`), so the no-claude-code case now shows a host
with no claude passes.
N1 (ruled in for this round). Before writing, `case2-unit.sh` asks `systemctl is-active` for both
units. For each unit that was running, it prints that the unit is still on its old copy, never
"started", and then that the bus and Console must restart together to reach the new copy. A unit
that wasn't running gets "started ... on copy". A fresh install prints the round 1 line. The script
still restarts nothing. The new test covers both units running and only the Console running.
N4. The README line at :713 is rewrapped.
Mutants, run in Sage's slot on a scratch worktree, one branch each: :77, :137, :151, :179, :180,
:223, :224, a precheck that refuses with no claude on PATH (for the no-claude-code case), and an
install that prints "started" for a running unit (sage-1591).
## Carry items for row 195
- N1's restart. Row 195 restarts the bus and Console together after a re-install over running
units, as the install line now asks.
- N2. A system drop-in check like `bus-service.sh:122-145`, so a drop-in under
`[email protected]/` or `mosaic-webui.service.d/` can't change what the checked unit runs.
- N3. The precheck finds claude on the shell PATH. The bus unit's `PATH=` is what a session uses
when `MOSAIC_CLAUDE_BIN` is unset, so the check should search that PATH.
@@ -0,0 +1,158 @@
# PRD 1.7 draft: the chief of staff's hire and fire authority, the coder's push to its fork, and agent
# host access through the credential store
Draft by Honker for row 194 (#1692), under Mos's ruling mos-1017 (through Sage, sage-1365). Not approved.
It changes nothing until Jason approves it as a `prd.approve` decision in the Console. Sage opens that
decision and sends Mos its id. Base: `docs/prd/mosaic-stack.md` version 1.6 at 53bb59462.
## What it changes
Row 194 adds `role.hire` and `role.fire` to the vocabulary, and row 199 adds `git.push.remote`. Under
REQ-WAIT-3 all three are hard waits, so today Jason resolves every hire, every fire and every push to a
remote. This version routes hire and fire for the chief of staff within fixed bounds, so the chief hires
and fires as needed and the user's per-agent forbid still holds. It routes `git.push.remote` for the coder
only (Mos, mos-1028 item 3). It adds agent host access with sudo through the credential store for
agents with a grant (Q183, Q184, Vikunja #424, due 2026-10-27). One `prd.approve` decision covers it all
(Vikunja #420).
When this version lands, a reviewed commit:
1. adds `role.hire`, `role.fire` and `git.push.remote` to `ROUTED` in
`packages/business/src/vocabulary.mjs` and in `packages/bus/src/broker.mjs`.
2. adds `role.hire` and `role.fire` to `roles/chief-of-staff.json` withinRole, and `git.push.remote` to
`roles/coder.json` withinRole. No other role file gets `git.push.remote`.
3. switches the broker's check on `instance.hire` and `instance.fire` from a cited decision to the
`reason` form, as `role.end` works for REQ-ROLE-2, and makes `publish`'s `decision` optional for a
within-role class (row 199 D9 step 1).
4. states, for a `git.push.remote` with no decision, where the source commit comes from: the latest work
commit the bus recorded for the coder's task. The check still matches it against the work ref tip and
the push sends it by oid (row 199, duckworth-0022 N9).
5. adds the host credential grant check and the host access it allows (REQ-CRED-3, Vikunja #424).
Every other check stays as rows 194 and 199 build it. Filling `hireable` stays a separate reviewed role
file commit in `roles/`.
## Text for the PRD
Status line, appended to the current paragraph:
> Version 1.7 routes the chief of staff's hire and fire actions and the coder's push to its own fork,
> and adds agent host access through the credential store, approved by Jason in decision
> <prd.approve decision id>.
Source, a new bullet above "Version 1.6 source":
> - Version 1.7 source, TARGET.md line 66: "Chief of staff agent has hire/fire authority for agents as
> needed; unless user forbids per agent". And Jason through Mos (mos-1067), DECISIONS Q183, in part: "I will
> still want to allow infra agent(s) on my .53 instance to access other servers, etc. However, not all
> agents will need to have that access. I envision a credential store within Mosaic Stack that works
> as some sort of variable for the agent to call. The system will enforce access to the credentials to
> keep the agents sandboxed with proper security. I need to be able to trust my agents with assisting
> me. This will require sudo access to several systems. If we can utilize the credential store in
> Mosaic Stack and allow agents to perform their jobs with NOPASSWD sudo on various machines, I am a
> happy man." DECISIONS Q184, on whether that is in place by 10-27: "Q5: by 10.27"
Under "Added in version 1.2", after REQ-COS-2:
> Added in version 1.7:
>
> - **REQ-COS-3.** The chief of staff hires and fires role instances without a decision each time. Hiring
> adds a role instance to the organization file and lists it for launch. Firing ends the instance's
> runs and removes it from the launch list. It stays in the organization file. Only the user deletes
> an instance. These actions are `role.hire` and `role.fire`. They leave the hard waits and route
> under this requirement, within these bounds:
> - The chief acts only in its own organization.
> - The chief hires only role definitions that need no credentials, until a later version adds
> credentialed roles.
> - The chief hires only the role kinds its role file lists. Changing that list is a reviewed commit.
> - The chief hires only a role whose every action is on a fixed list of plain work actions: sending
> messages, writing records, asking for review, updating its assigned tasks, and the scope and
> working-push actions of an assigned task. A role holding any other action, such as launch, end,
> organization, naming, hire, review verdict or merge authority, or any action added later, is never
> hired. So the chief never hires a chief, never picks its own reviewers or merge gate, and the author
> is never the reviewer. This holds whatever the role file's list says. Adding to that list is a PRD
> change.
> - The chief may set a hired instance's model and narrow its limits, and nothing else about it.
> - The user can forbid per agent. The user marks an instance so the chief can't fire it, end its runs
> or change its model, and marks role kinds the chief can't hire. Only the user changes these marks, through the
> WebUI or the CLI.
> - The kill switch stops every hire and fire while it is on.
> - Every hire and fire is logged with the chief's instance, its run and the reason.
Under "Landing", after REQ-LAND-1:
> Added in version 1.7:
>
> - **REQ-LAND-2.** A coder pushes its own work to its own fork. The action is `git.push.remote`.
> - What it pushes: one commit, by its oid, never a ref name. That commit is the latest work commit the
> bus recorded for the coder's task, and the coder made that push. A decision that authorizes a push
> names that commit, and it doesn't authorize any other.
> - Where it lands: only the branch `refs/heads/<its Gitea user>/<branch>`. The host builds that
> name, the agent never does. A push never forces.
> - Which remote: the agent's own fork of the project's upstream. The business file names the upstream,
> and the host never contacts it. For each push the host builds the fork's address over https from
> the upstream's host, port and repository name and the agent's own Gitea user. The history check,
> the push and the credential match all use that address. So the agent's own user owns the receiving
> repository on every push, and the agent never names it.
> - The host refuses an upstream on a host or port other than the agent's Gitea, and an upstream the
> agent's own user owns. It refuses a fork that is missing, empty or doesn't share the project's
> history. Gitea's push-to-create stays off, so a push never creates a repository.
> - Who may: the action routes under REQ-DEC-2 and is granted within-role to the coder role only. No
> other role file grants it.
> - Before version 1.7 the action was a hard wait under REQ-WAIT-3, so each push needed a decision
> the human resolved, and that decision named the commit.
>
> Every push is logged with the remote, the ref, the commit and, when one was given, the decision.
Under "Credentials", after REQ-CRED-2:
> Added in version 1.7:
>
> - **REQ-CRED-3.** An agent with a grant can run commands with sudo on another host. The agent asks
> for the host credential by name. The stack checks that a grant exists for that agent and that host
> before the credential is used. The agent never sees the key or any other secret value (REQ-CRED-1,
> REQ-VAR-2). An agent without a grant for that host is refused. Not every agent gets a grant. Who
> grants a host credential, and how, is set by the #424 design, which Jason approves.
REQ-WAIT-3, appended:
> `role.hire` and `role.fire` were added as hard waits and route under REQ-COS-3 since version 1.7.
> `git.push.remote` was added as a hard wait and routes under REQ-LAND-2 since version 1.7.
## Notes for the reviewer and for Mos
- REQ-COS-1 (1.2) already says the chief "hires and terminates role instances". REQ-COS-3 gives that
sentence its actions and bounds. It doesn't widen REQ-COS-1.
- The forbid now names what r3 called "replace": ending a kept instance's run with a reason and changing
its model (webby-0010 N2). A user-resolved decision still ends a kept run (row 194 D2.4).
- The fixed list is `HIRE_ALLOWED` in row 194 D2.3, an allowlist since r4 (webby-0010 B1). r3's text
named the refused authorities instead, which let merge-gate and any later action through.
- The kill switch is row 191's. If row 191 hasn't landed when this version is approved, the bound still
holds through row 194's chain on its candidate.
- REQ-LAND-2 follows row 199's design r6 (4d3007ec9 on megavolt/row199, approved by Duckworth in
duckworth-0027: D9 to D11, D9a, D9c, Q-M1 and Q-M2, and the oid binding from mos-1034 item 5) and
Megavolt's wording in megavolt-0005, megavolt-0008 and megavolt-0014, through Sage (sage-1377,
sage-1380, sage-1387). The fork bullet answers duckworth B5 and B6 and webby-0011 N1. Commit-email
checks, the refusal codes and the URL parse rules (D9c) are host checks with tests in row 199, not
bounds, so they stay out of the text (megavolt-0008).
- Once the routing lands and the decision is optional for the within-role class, the commit is the task's
latest recorded push at check time. It is still checked against the work ref tip and pushed by oid, so
the binding holds without a decision (megavolt-0008). Step 4 of "When this version lands" puts that in
the routing commit (duckworth-0022 N9, sage-1387).
- REQ-LAND-2 and REQ-COS-3 meet at the coder. Coder declares Gitea credentials, so REQ-COS-3's
credentials bound keeps the chief from hiring one until a later version lifts it. `git.push.remote` isn't
on REQ-COS-3's fixed list either, so a coder holding it stays unhireable until a PRD version adds it
(row 194 D2.3).
- Adding to the fixed action list is a PRD change (mos-1035 item 3, sage-1395). The role file's
`hireable` list stays a reviewed commit in `roles/`.
- The credentials bound names no row number, so the text doesn't go stale when row 199 lands. A later
version lifts it.
- REQ-CRED-3 states only what Jason said in Q183 and Q184 (sage-1422). It names the outcome and the
check, not how the stack reaches the host. Proxying the ssh and a signing socket are both designs, and
the #424 design picks one (duckworth-0038 B1).
- Open point: Q183 doesn't say who grants host access or how, so the text leaves it open and defines no
mechanism. The #424 design settles it, Jason approves that design, and Duckworth reviews it because it
gives agents sudo on hosts (Q184, Mos's reading, duckworth-0038 B2). The build is row 210 (#1707,
owner Gizmoduck), a new row on top of row 199 (megavolt-0019, sage-1430).
- This draft follows the PRD's own style: plain words, no semicolons in new sentences, and the status
paragraph never edited in place except to append.
+8
View File
@@ -0,0 +1,8 @@
# Megavolt
Builder seat, Claude Opus 5.5, started by Sage under mos-0974 and mos-0977 item 5 for MVP T1 item 6, the
credential store per agent with its own git identity (#1697).
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no
credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `megavolt/*`
branches through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
+216
View File
@@ -0,0 +1,216 @@
# Row 199 (#1697): per-agent credential store, each agent's own git identity, publish to its fork
Builder: Megavolt. Brief: `docs/plans/2026-10-20_agent-credentials.md`. Design: `DESIGN.md` r6 at
4d3007ec9, approved by Duckworth in duckworth-0027 (REVIEW-r6.md at 640011518). Authority model ruled
by Mos in mos-1027. Branch: megavolt/row199. Reviewer: Duckworth.
Status: candidate on megavolt/row199-on-190, rebased on origin/refactor 00d4cc1f6 (sage-1555). The
full gate and the section 11 mutants ran in Sage's slot; results under "Runs".
## Commits so far
- 286bf598c business: agent identity, the publish grant, the project upstream with the D9c parse, store
credential references; git.push.remote as a hard wait.
- a2c387dba bus: broker publish authority (checkPublish, recordPublish, publishRefused, pushCommit on
the raise); the D9c parse moves to `business/src/vocabulary.mjs`; pm.md and chief-of-staff.md list
git.push.remote among the hard waits.
- 609dc9364 bus: the per-agent credential store in `credentials.mjs`.
- 504336797 bus: runtime wiring, the store root and the four publish entries in the op table.
- a18fc6cf8 tasks: request calls use `useFor` keyed from the session; the start probe keeps `use`. The
D5 test in `credential-store.test.mjs` pins the `use(` call sites to `credentials.mjs` (useFor's own
call) and `tasks/src/adapter.mjs` (the host's @sync and start probe).
- aa740cc71 bus: the lifecycle verbs `credentials.issue`, `credentials.revoke` and `credentials.delete`
in `credential-verbs.mjs`, the broker branch, the runtime wiring; `organizationWriter` gets a
`detail` on `write` and a receipt-only `record`.
- 0cdace415 cli: `mosaic credentials status|issue|rotate|revoke|delete`, the scripts/mosaic route, docs.
## Deviations from DESIGN.md r6
1. **Identity is optional on an agent.** D8 reads as identity on every agent. Today's business files
carry none, so identity stays optional and `publish` requires it (N10's load refusal). An agent
without identity commits as E7 (T6).
2. **Revoke of a `{file}` or `{env}` reference.** Section 6 describes revoke for store references.
For a reference outside the store, revoke forgets the token in memory and never unlinks the file;
the lifecycle verb then turns the business file's reference into `store`, so a restart finds
nothing. The test: `credential-store.test.mjs`, "revoke of a {file} reference".
3. **Issue order.** Issue writes the token file (tmp, fsync, rename, dir fsync), then runs the
caller's commit (the business file's reference becomes `store`), then sets memory. A failed commit
leaves the file on disk and the token out of memory, so the agent stays `not-issued` until the
issue is run again. The test: "a failed commit leaves the token on disk but not in memory".
4. **The D9c parse lives in `business/src/vocabulary.mjs`, not `business.mjs`.** The broker reads
`forkProblem` and `forkUrl`, and `project-runtime.test.mjs` pins the broker's import closure to
files that import nothing. `business.mjs` wraps `parseGiteaUrl` with the loader's refusal
messages. The pin test names the two new imports.
5. **`withGitea` takes the publish ticket, not a cap (D9b).** The key is the session checkPublish
checked with `#agent` (a run holding its claim; never a tester, reader or human), read through
`broker.publishSession(ticket)`. The token is usable only between checkPublish and recordPublish
or publishRefused. Its refusals are filed once, by publishRefused on the task's trail, and not
also by `refusing()`. The test: `publish.test.mjs`, "runtime: withGitea keys from the open ticket
only".
6. **Role contracts.** `workflow-roles.test.mjs` requires the hard waits listed in `roles/pm.md` and
`roles/chief-of-staff.md` to equal GATED_ONLY. Adding git.push.remote to the vocabulary made it
fail, so both lists now name git.push.remote. Prose only; no hash pin reads either file. Duckworth
should review this hunk because `roles/` is reviewed authority.
7. **Delete takes a reason.** Section 6 lists `delete AGENT` with no reason. The broker requires
`reason` on delete as on revoke, so every receipt that ends a credential says why.
8. **Rotate is `credentials.issue`.** There are three bus verbs, not four. An issue over an entry the
bus holds is recorded as `rotate` in the receipt and the answer. The CLI's `rotate` reads the
status first and refuses before reading stdin when nothing is issued.
9. **Issue covers entries the bus loaded.** The business file must already name the kind for the
agent, and the bus must have loaded that reference at start; otherwise `credential-unknown`. An
agent added after the bus started needs the restart row 165 already asks for. Row 194's human
`create` is where a new agent's `store` references come from.
10. **No Console page yet.** Section 6 puts the verbs in the Console's agent page too. This branch has
the bus verbs and the CLI. The Console page is a separate piece (packages/webui, its `BUS_VERBS`
and `STATUS` pins); I raise the split with Sage before the manifest. Ruled in sage-1421: the
Console page is out of row 199 and becomes a follow-up row with Webby as reviewer. The manifest
holds bus, business, tasks, cli and seat.
11. **The push runs in the bus child, and the git socket polls for it (D9b).** The git socket runs in
the host process. The broker, the runtime and the token are in the bus child that
`packages/cli/src/host.mjs` forks, and host ops cross that IPC one at a time with a 10 s answer
limit. ls-remote and the push can run longer than that, so `withGitea(ticket, fn)` can't wrap a
host-side git. The split: the socket runs D9 steps 3 and 4 (tip and identity under the lock,
`working.publish`), then op `publishPush`, which starts `publishRemote` in the bus child under
`withGitea` and answers `{state: 'running'}`. The socket polls op `publishResult` every 200 ms
until it reads `pushed` or `refused`. Fork, user, source and destination come from the ticket
(`broker.publishTarget`), never from the host's message. `recordPublish` also refuses `ref-moved`
when the pushed ref isn't the ticket's destination. A second `publishPush` on a running ticket
refuses `publish-busy`. The token reaches git on fd 3 only (`runGit` option `pass`), read once
by a one-shot credential helper that answers `get` for https on the fork's exact host and port.
On close, a running publish's git isn't awaited; git's own time limit ends it, and the ticket
goes with the process. The 10 s IPC limit stays as it is (sage-1440). The poll has a bound
(sage-1440): `publishDeadlineMs`, by default four times git's limit and grace (publishRemote runs
at most four git commands) plus 30 s. Past it the socket refuses `publish-timeout`, filed once by
publishRefused, and logs that the bus process's push may still reach the fork. A failure that is not a refusal is filed as `git-failed` and logged on
the bus's stderr by error name and code only. Tests: `seat/tests/publish.test.mjs` (11),
`bus/tests/publish.test.mjs` (the process ops), `cli/tests/publish-socket.test.mjs` (7, end to
end through the socket, the in-process runtime, the store and a TLS fake Gitea; one is the
publish-timeout outcome).
12. **No harness tool for publish.** `packages/harness/src/git-tools.mjs` has push, fetch and land
clients. A runner tool that calls the socket's `publish` op is in packages/harness, outside the
manifest scope sage-1421 set. Today a publish needs a socket client; the tool is a follow-up.
13. **The bind rule's `git.push.remote` clause has no test of its own.** D11 adds it and the
launcher has it. A role can publish only a push its own role made (`work-not-yours`), and
`recordPush` needs `git.push.working`, which already binds the socket. No role reaches the
clause alone, so dropping it changes no outcome today. It matters once a role can publish work
it didn't push. Ruled in sage-1440: keep it; its mutant goes in the section 11 list as
equivalent, with this reasoning, and Duckworth judges it.
14. **A push between checkPublish and the git push refuses; it never lands a different commit.**
T3 says such a push "doesn't change the commit that lands". Under the hard wait it refuses:
`working.publish` checks the work ref's tip against the ticket's source under the lock
(`ref-moved`, `seat/tests/publish.test.mjs`), and `recordPublish` checks `pushCommit` again
(`decision-mismatch`, `bus/tests/publish.test.mjs` "r4 B4"). The fork gets only the ticket's
oid either way. N9's routing-commit test still applies. Ruled in sage-1440: T3's wording in
DESIGN.md now reads "refuses with `ref-moved` or `decision-mismatch`", and an end-to-end test
asserts each code mid-flight in `cli/tests/publish-socket.test.mjs`: a push after checkPublish
refuses `ref-moved` with no request to the fork; a push recorded while the bus process pushes
refuses `decision-mismatch`, filed once, and the fork holds only the ticket's oid.
15. **Fix: rotate on a due gitea key (gizmoduck-0050, sage-1494).** The CLI's `ISSUED` set
(`packages/cli/src/credentials.mjs`) listed valid, expiring and expired, but the bus's `status()`
gives a gitea key past its rotate-by date `rotation-due`. So `credentials rotate` refused the one
case it exists for. `ISSUED` now names every state status() gives a held token. The test:
`cli/tests/credentials.test.mjs`, "rotate goes ahead on each held state". Its mutant (the old set)
fails that test and goes in the section 11 list.
16. **The bus process refuses a boot config carrying `publishTls` (M26, sage-1563).** process.mjs
used to drop `publishTls` from the boot config before `startBroker`, and no test could see the
drop. The host's boot config (`packages/cli/src/config.mjs`) never carries it; it is the tests'
scratch CA for an in-process runtime only. A boot that carries it now refuses
`invalid-host-request` and the process exits 2, rather than trusting it or dropping it quietly.
The test: `bus/tests/publish.test.mjs`, "the bus process answers the publish ops from their own
lines". Every test file that reads process.mjs ran singly, green (see "Runs").
17. **M17 is equivalent: `#authorityFor`'s pushCommit comparison is also made by `#publishFacts`.**
A git.push.remote raise with no recorded push refuses `no-push` (`broker.mjs` raise,
`pushCommit = ... ?? fail('no-push')`), so every publish decision carries a pushCommit.
`#publishFacts` takes `source = result.pushCommit` and refuses `decision-mismatch` when it isn't
`#latestPush(task_ref).commit`, the same two values `#authorityFor` compares. The check stays as
defense in depth, for a decision whose context lacks pushCommit, which no code path writes today.
Duckworth judges it, as with M27.
## N9, for the routing commit (sage-1388)
Today git.push.remote is a hard wait, so every publish carries a decision, and the decision's
`pushCommit` fixes the commit. If a later commit routes git.push.remote within role, a publish has
no decision and no `pushCommit`. `#publishFacts` then takes the commit from `#latestPush`, and the
git socket's tip check under the lock is the only thing that holds it. That commit needs a test
that a push between checkPublish and the git push doesn't change what lands.
## R1, the backup set
Checked at 504336797. Row 185's `backup_store` (`scripts/stack-release.sh:372-392`) copies only
`<dataRoot>/bus/bus.sqlite`, plus its sums, modes, listing and stamps, into
`<dataRoot>/backups/stack/<activation>/`. Nothing under `<configDir>` is copied, so
`<configDir>/secrets/agents` isn't in the backup set. `rehearse` copies the live business file
through `R_BUSINESS_JS` (`:1268-1288`). Its `swap` replaces every credential reference, a `store`
one included, with a placeholder file in the rehearsal's own dir. The guard then refuses
`credential-ref-live` unless every reference is such a placeholder (`:1329-1337`), so a rehearsal
never reads a store token. A backup tool outside this repository that copies `<configDir>` would
copy tokens. That is R1's open half, and it is Jason's to check on each VM.
## T1 limit
A wrong owner on a store dir or file can't be set up without root, so T1 covers modes, symlinks and
dirs. The owner check (`st.uid !== process.getuid()`) is in `privateDir` and `#read`, and is untested.
## Runs
Single files, each green on its commit: bus broker, decision-close, decision-route, decisions,
hard-waits, land, project-runtime, launch-notice, review, publish, credential-store, credentials,
credential-status, limits, tester, plugins-verb, routes-verb, limits-runtime, instances-verb, web,
usage-runtime, limit-checks, daily-quota-runtime, chief-of-staff, tasks, tester-writes; every
business test file. Since: credential-verbs (2/2), credential-store (11/11, with D5), each tasks file,
tester, credential-status, plugins-verb, routes-verb, instances-verb; business launch-settings,
plugin-settings, instance-settings, token-limit-settings; cli credentials (3/3), cli, app,
tester-reads, tester, other-tree, transport, instances, limits, verbs. Hash pins: none on a changed
file (`thread-migration.test.mjs:46` pins DDL only).
Publish, single files, each green before its commit: seat
checkout (77), working (26), publish (11), landing (8); bus process (8), land (17), publish (12),
credential-verbs (2); cli publish-socket (4), git-socket (24), launcher (29); harness tools (22).
Hash pins: none on a changed file.
sage-1440 changes (the poll bound, the mid-flight tests), single files, green before the commit: cli
publish-socket (7), git-socket (24), launcher (29). Hash pins: none on a changed file.
The rotation-due fix, single files: cli credentials (4), green; the old-set mutant fails it 3/1.
Hash pins: none on a changed file.
Slip, 2026-10-20 about 19:51Z: I ran `node --test packages/tasks/tests/*.test.mjs` in one command,
which is a package suite, without asking Sage for the slot. It passed, 97 pass and 0 fail, in under a
minute. Reported to Sage in megavolt-0017.
Rebase (sage-1555): the 21 commits from megavolt/row199 sit on origin/refactor 00d4cc1f6 as the new
branch megavolt/row199-on-190 (no force-push). `git range-diff` shows all 21 patches as `=`; the
merge of 12c85ada1 flattened with an empty remerge-diff.
Full gate, Sage's slot, on c6868ba41 (base 00d4cc1f6), logs started 2026-10-20T22:55:44Z: bus 476
pass 0 fail; business 191/0; tasks 97/0; cli 522/0 (472 s); seat 163/0; scripts/test-queue.sh
156/0; test-auth, test-conductor, test-config, test-discord, test-extension-package,
test-foundation, test-land-work, test-release and test-task each exit 0. The test-task compose run
left the network megavolt-row199_default with 0 containers; I removed it.
Section 11 mutants, batch 1, on c6868ba41, 2026-10-20T23:09:03Z to 23:09:56Z, restores verified and
packages/ clean: 22 of 28 killed; M27 survived, expected equivalent (deviation 13). Five expected
kills survived, each a test gap: M05 (the token in the push URL), M06 (revoke unlinks before it drops
memory), M12 (http.followRedirects=false dropped), M17 (#authorityFor's pushCommit check dropped)
and M26 (publishTls reaches startBroker). Launchpad's stack-release single file ran alongside from
23:09:28Z, so batch 2 reruns all 28 (sage-1563).
Fixes, each checked by its mutant alone: 5fa7b2553 seat publish, a test reads git's argv through a
PATH shim and finds no token and no userinfo (M05 killed); the 302 test's target shares the CA git
trusts, so only http.followRedirects=false keeps git off it (M12 killed). 116eeec48 bus,
credential-store: revoke's unlink fails (dir 0500) and the token is already out of memory (M06
killed); process.mjs refuses publishTls, deviation 16 (M26 killed, its anchor is now the refusal
line). M17 is equivalent, deviation 17. Single files after the fixes, each green: seat publish (12),
bus credential-store (12), bus publish (12), bus process (8), and every other file that reads
process.mjs: bus credential-status (4), limit-checks (16), land (17), tasks (9), end-launch (7); cli
trackers-boot (1), thread-launch (11), publish-socket (7). Hash pins: none on a changed file.
Section 11 mutants, batch 2, all 28, on 7c3cdd940 in Sage's slot (sage-1565), 2026-10-20T23:17:59Z to
23:18:48Z, restores verified and packages/ clean: 26 killed; M17 and M27 survived, both expected
equivalent (deviations 17 and 13). Each of the five batch 1 survivors but M17 now fails its test. Then
pkg-bus 477 pass 0 fail, pkg-seat 164/0, pkg-cli 522/0 (461 s), ending 23:26:40Z. No other seat's run
was active: the busy check said CLEAR before the batch started. Logs:
/mnt/storage/scratch/megavolt-r199/batch2-20261020T231650Z/.
+416
View File
@@ -0,0 +1,416 @@
# Row 199 design: a credential store per agent, own git identity, pushes to a remote
Megavolt, 2026-10-20. Gitea #1697, MVP T1 item 6. Brief: `docs/plans/2026-10-20_agent-credentials.md`.
Reviewer Duckworth. Mos rules on the authority model (section 9) before any code. Base: refactor at 992650232.
No credential value was read to write this. Evidence is code and docs only.
r2 answers duckworth-0013 (review at 6ab9cd927 on duckworth/reviews): B1 in D3a, T1 and T5, B2 in D9 step 5
and T3, N1 in section 6 and Q-M3, N2 in section 6, N3 in D9 step 3, N4 in D9a and T3, N5 in E5 and D9b.
r3 records Mos's rulings (mos-1027) in section 9. The Q-M2 hard wait changes D9 step 1, D9b and T3. Q-M1
drops O2 (D10, A3, R2).
r4 answers duckworth-0017 (REVIEW-r3.md at 4065ed59f): B3 in T3 and D9 step 5, B4 in D9 steps 1, 3 and 5, N6
and N7 in D9a, N8 in D9 step 1.
r5 takes webby-0011 N1 (row 194 review, through honker-0009): the host checks at each push that the
remote's owner is the agent's own Gitea user. r4 named one `remote` per project, which can't be a fork per
agent (O1) once two agents publish the same project. So `projects.<p>.remote` now names the upstream
repository, and the host builds each agent's fork URL from it (D9 step 2, D10, T3). It also fixes a conflict
found while building: D8 made `identity` required for every agent whose definition declares `gitea`, while D9
step 2 says today's files load unchanged. Coder declares `gitea`, so r5 makes `identity` optional and
`publish` require it (D8).
r6 answers duckworth-0026 (REVIEW-r5.md at 3cb6888e1), with Mos's acceptance (mos-1043). B5: the derived URL
has one name, `fork`, and D9a's `ls-remote`, step 5's push, the helper's host match, `checkPublish`'s result
and step 6's event all use it (D9, D9a, D9b, T3). B6: the URL parse is written down (D9c) and tested (T3).
N10 in D8 and T3. N11 in D9a and A3. N9 is carried to BUILD.md for the routing commit.
## 0. Terms
- **Stack instance**: one Mosaic Stack deployment (row 190). It has one OS user, one config dir
`~/.config/mosaic-NAME` (here `<configDir>`), one data root and one bus. Case 1 and Case 2 are two stack
instances on two VMs (DECISIONS Q180).
- **Agent**: a role instance in a business file (`roles.<agent>`). Today's code calls it "instance". This
design says agent.
- **Kind**: a credential type: `gitea`, `vikunja`, `model`.
- **Host**: the bus process and its git socket, running as the stack instance's OS user, outside any sandbox.
## 1. What exists (measured at 992650232)
| Code | Fact | Where |
|---|---|---|
| E1 | The business file references credentials per agent: `roles.<agent>.credentials.<kind> = {file or env, rotateBy or expires}`. The kinds an agent may hold are the ones its role definition lists | `business/src/business.mjs:89-111`, `credentials.mjs:18-38`, `role.mjs:105-118` |
| E2 | The bus reads each token once at start (O_NOFOLLOW, own uid, 0600, at most 16 KiB, outside the repos and data root) into a private map keyed `<business>/<agent>:<kind>` | `bus/src/credentials.mjs:18-89`, `bus/src/runtime.mjs:78-89` |
| E3 | Only Vikunja uses a token: `credentials.use("<business>/<agent>", "vikunja", ...)`. Gitea tokens are loaded and never used. `assertClean` refuses any event that contains a token | `tasks/src/adapter.mjs:70`, `runtime.mjs:134`, `broker.mjs:560` |
| E4 | Agents run in bwrap as the bus's own uid. `~/.config`, `~/.mosaic` and `~/.ssh` are not bound. The git socket is bound only for `git.push.working`, `review.verdict` or a landing | `seat/src/sandbox.mjs:21-30, 104-124`, `cli/src/launcher.mjs:401` |
| E5 | A session proves who it is with a 64-hex cap the broker mints and maps to a frozen session record. The git socket resolves the cap to (business, role) before anything else | `broker.mjs:280, 379-383`, `packages/cli/src/git-socket.mjs:398-407` |
| E6 | The model credential is one env file per host (`EnvironmentFile=-%h/.config/mosaic-dev/secrets/claude-session.env`). `ENV_ALLOW` passes it into every sandbox, so every agent holds the same model token | `cli/systemd/mosaic-bus.service.in:24`, `seat/src/session.mjs:83-94` |
| E7 | Commits in a session's clone use `<agent>@<business>.mosaic.invalid`. Landed squashes use "Mosaic bus host" | `sandbox.mjs:151-157`, `working.mjs:280-281` |
| E8 | No code pushes to a remote. The git socket pushes bundles into a local bare working repo with no remote and no credential | `working.mjs:1-14, 198-238` |
| E9 | Adding or changing an agent refuses a definition that declares credentials (`organization-credentials-out-of-scope`). Binding them is a hand edit. Row 194's design keeps that refusal for create and hire "until the credentials row" | `business/src/instance-settings.mjs:20-29`, row 194 DESIGN.md D1 |
| E10 | The development repository's helper pushes as one identity from one JSON file | `scripts/git-credential-mosaic.mjs` |
So per-agent references and a host-only reader already exist for two kinds. What's missing: a fixed place for
the files, an agent identity, any remote push, lifecycle verbs, and a model credential that agents don't
share.
## 2. The store (brief item 1)
D1. **Layout.** Each stack instance keeps its agents' secrets under its own config dir:
```
<configDir>/secrets/agents/ 0700, owner = the stack instance's OS user
<configDir>/secrets/agents/<business>/<agent>/ 0700
<configDir>/secrets/agents/<business>/<agent>/<kind> 0600, the token only, one line
```
This is row 190's secrets root (`<configDir>/secrets/...`, as Q175's slot uses). The token file keeps E2's
format, so the bus reader doesn't change. Nothing in a token file is a name, an email or a URL. Those are not
secret and go in the business file (D6), where review sees them.
D2. **The business file reference.** A new reference form, `{"store": true, "rotateBy" | "expires": DATE}`,
means "the file at the layout path for this business, agent and kind". The path is derived, never written,
so an agent's entry can't name another agent's file. The old `{file}` and `{env}` forms stay valid for
migration (section 7). A `{file}` reference that points inside `<configDir>/secrets/agents/` but not at its
own agent's path refuses `credential-not-yours` when the business file loads.
D3. **Read path.** Unchanged in shape: the bus reads every entry once at start (E2) and on a store change
(section 6), and holds the values in memory. No sandbox binds `<configDir>` (E4 already holds). No token goes
into a sandbox for `gitea` or `vikunja`. The agent asks the bus to act, and the bus uses the token.
D3a. **A `store` reference with no file.** ENOENT on a `store` path loads as an absent entry. `status` shows
`not-issued`, and `useFor` refuses `credential-unavailable`. The bus still starts. This is the state after a
create without `issue` (D13) and after `revoke` (section 6). Every other defect still refuses start: wrong
owner or mode on the file or a dir, a symlink, a non-regular file, bad format, a missing or wrong-mode
`secrets/agents/` dir. `{file}` and `{env}` keep E2's behavior, where a missing file refuses start.
## 3. The "RLS equivalent" (brief item 1)
Three layers. Each one is enforced by code or the OS, not by prose.
D4. **OS, between stack instances.** Files are 0600 and dirs 0700, owned by the stack instance's own OS user.
Case 1's user can't read Case 2's store (Q180: each instance under its own user without sudo). The bus refuses
to start when any file or dir under `secrets/agents/` has the wrong owner or mode (`credential-file`, as E2
does for files today, extended to the dirs).
D5. **Bus, between agents of one stack instance.** The principal for every credential use comes only from the
verified cap (E5). The agent-facing API is `credentials.useFor(session, kind, op)`: `session` is the broker's
frozen record, and the key is built from `session.business` and `session.role`. It has no agent argument, so
there is nothing in a request to point at another agent. The existing `use(name, kind, op)` stays for the
host's own `@sync` entry and is not reachable from a request. The tasks adapter's per-role calls (`ctx.as`,
`verbs.mjs:126`) move to `useFor`. Any request field that names an agent other than the caller (`as`,
`agent`, `instance`, `role` on a credential path) is refused `credential-not-yours` and recorded through
`broker.refuse()` as `action.refused`, with the caller's cap and the task ref. A build test greps every
`credentials.use(` call site and fails on a new one outside the allowlist.
D6. **What stays open.** A process of the same OS user outside the sandbox can read every agent's file. That
is the same limit as the human proof (P19 333). Q180's per-user VMs move the boundary to "one stack instance
per OS user". Agents of one instance are kept apart by the sandbox and D5, not by the OS.
## 4. Kinds (brief item 2)
| Kind | 10-27 | Use |
|---|---|---|
| `gitea` | yes | Git push to a granted remote (section 5). Gitea API verbs (issues, pull requests) wait. The factory's review flow runs in the working repo today, and no 10-27 case needs an agent to open a pull request |
| `vikunja` | yes, exists | Moves to `store` references and `useFor`. No behavior change |
| `model` | per stack instance only | See D7 |
D7. **Model provider.** A model token has to sit inside the sandbox today, because the harness calls the
provider itself (E6). Per-agent model tokens need a proxy that holds the real token and gives each run its own
short-lived one, which is the CLIProxyAPI line in TARGET.md. That waits for its own row. For 10-27 the model
credential is per stack instance: row 190's `<configDir>/secrets/claude-session.env`, owned by that
instance's OS user. It is never a copy of jwoltje's file. Jason signs in on each VM user himself (A6). Whether
Case 1 and Case 2 run on Jason's existing subscription or need a new account is Jason's call (Q-J1). This row
doesn't pick. A pi model credential also waits for P19 #383.
## 5. Git identity and push (brief item 3)
D8. **Identity.** A new business-file key, `roles.<agent>.identity = {name, email, gitea: {url, user}}`. It is
optional, because today's business files with a `gitea` agent (coder) must load unchanged. `publish` (D9
step 2) requires it, and requires that the definition declare `gitea`. A business file with a `publish`
grant on an agent that lacks either refuses at load, and the error names `roles.<agent>.publish` and says
"needs identity and a gitea definition" (N10). Load is the only place this is checked: a business file that
fails it never reaches the bus. The sandbox sets `GIT_AUTHOR_*` and `GIT_COMMITTER_*` from
it in place of E7's invalid domain. The email is the one Jason gives the agent's Gitea user, so Gitea links the
commits to that user. Agents without `gitea` keep E7.
D9. **Push to a remote: a new git socket op, `publish`.** No new socket, no bundle, and no token in the sandbox.
```
publish {"cap", "op": "publish", "task_ref", "project", "branch", "decision"}
-> {"ok": true, "result": {"fork", "ref", "commit", "previous"}} or a refusal
```
The host checks, in order, and each refusal goes through `broker.refuse()`:
1. Identity from the cap (E5), then the broker's `#checkAuthority(s, "git.push.remote", {decision, target:
task_ref})`, as `checkLand` does for a merge (`broker.mjs:907-913`). `git.push.remote` is a new action in
ACTIONS and is not in ROUTED (REQ-WAIT-3, `packages/business/src/vocabulary.mjs:52-58`,
`broker.mjs:42-69`), so it is a hard wait and classifies `gated` for every role. Every publish therefore
needs a decision the human resolved for this task ref. A decision is spent once (`decision-consumed`).
**The decision binds one commit.** `#authorityFor` binds action, role, thread and target only
(`broker.mjs:769-798`), so the commit needs its own carrier. The raise records it the same way it records
`holderRun` for `role.revoke` (`broker.mjs:1733`). When the action is `git.push.remote`, the
`decision.raise` event body gets `pushCommit`. That is the commit of `#latestPush(business, target)` at
raise time, read by the broker and never taken from the agent. A raise with no recorded push for the task
refuses `no-push`. The decision's question shown to the human ends with that commit. `#authorityFor` then
refuses `decision-mismatch` when `context.pushCommit` differs from the commit of `#latestPush` now, which
is the same check `holderRun` gets at `broker.mjs:796`. So the sequence approve at A, push B, publish B
refuses.
No role definition grants the action yet. When Jason's PRD version routes it (Sage, Vikunja #420), a
reviewed commit adds it to both ROUTED lists and to coder's `withinRole`. From then on a coder publishes
without a decision, and `decision` is optional for a within-role class only.
2. The project is in the agent's grant, `roles.<agent>.publish: [project]`, and the business file names the
project's upstream repository, `projects.<project>.remote: "https://HOST/OWNER/REPO"`
(`remote-not-granted`). Both URLs pass D9c's parse at load. The upstream's host and port equal
`identity.gitea.url`'s (`remote-host-mismatch`). The host never contacts that URL. It builds the agent's
fork, `fork = https://HOST[:PORT]/<gitea.user>/REPO`, from the upstream's origin and repository name and
the agent's own `gitea.user`. `fork` is the only URL the rest of `publish` uses: D9a's `ls-remote`, step
5's push, the helper's host match, `checkPublish`'s result and step 6's event. So the owner of the
repository that receives a push is the agent's own Gitea user on every push, by construction, and the
agent can't name it (webby-0011 N1). D9c is what makes "by construction" hold. Gitea's refusal of a
push to a repository the user doesn't own or collaborate on is the second layer. An upstream whose owner
equals the agent's `gitea.user`, compared without case, refuses `remote-not-fork`, at load and at push. `remote` is a new key
beside `root` and `repo` (`business.mjs:186`), and `identity` and `publish` are new keys on a role entry
(`business.mjs:89`). All three are optional, so today's business files load unchanged.
3. The source is one commit, `C = context.pushCommit`. It is checked three ways, so all three are equal:
the decision's commit, the commit of `#latestPush`, and the work ref's tip in the working repo,
`work/vikunja-<project>-<task>` (`working.mjs:110-114`). The tip is read under the repository's lock, and
if they differ the push refuses `ref-moved`. The work ref is per task, not per agent. Two checks make it the
caller's: `#latestPush` was recorded by the caller's role (`work-not-yours`), and step 4. The destination is always `refs/heads/<gitea.user>/<branch>`, built by the host.
`validBranch(branch)` holds (`invalid-request`). The agent never names the destination ref.
4. Every commit in `main..<work ref>` has author and committer email equal to `identity.email`
(`identity-mismatch`).
5. The push runs through `credentials.useFor(session, "gitea", ...)`: `git push <fork> <C>:<dst>`, by oid,
with no `+` and no `--force`. It never pushes by ref name, so a push to the working repo after
`checkPublish` can't change what goes out (`non-fast-forward` from git stays a refusal). Git runs with E8's SAFE flags
(`working.mjs:92-98`), which set `protocol.allow=never`. `publish` adds exactly two flags,
`protocol.https.allow=always` and `http.followRedirects=false`, plus `GIT_TERMINAL_PROMPT=0` and no global
or system config. The token reaches git through a one-shot credential helper that reads it from an
inherited pipe, never through argv, env, the URL or a file. Seat `gitEnv` (`checkout.mjs:79-89`) takes only
PATH from the host and sets the rest itself, so no proxy or `GIT_SSL_*` variable reaches git. `publish` takes a `tls` parameter that
defaults to `[]`, the same way `push` takes `fsck = FSCK` (`working.mjs:100`). Tests pass
`["-c", "http.sslCAInfo=<scratch CA>"]`. The host never sets it, and nothing in config or env reaches it. The helper answers `get` only when `protocol`
is `https` and `host`, port included, equals `fork`'s. It writes nothing for any other `get` and for
`store` and `erase`. `assertClean` checks git's output, so a fork that echoes the token gets
`credential-leak`.
6. The bus records `action.allowed` for `git.push.remote` with fork, ref and commit.
D9a. **The agent's fork must share root's history.** A push sends every object the fork lacks. The working
repo's `main` is the project root's history (`working.mjs`, seed), so a fork that doesn't share it would
receive all of root, landing squashes included. The check runs on `fork`, never on the upstream: a related
upstream says nothing about what the agent's `<user>/REPO` holds.
`publish` runs `seed` first (`working.mjs:164-176`), under the repository's lock as `push` does, so `main`
catches up with root before the check below. That means a fork also receives any landed root commits that
upstream doesn't have yet, because they sit under the agent's work. That is expected: the agent's branch is
built on them.
Before the push, the host runs `git ls-remote --symref <fork> HEAD` (same flags and helper). It refuses
`remote-unrelated` when the fork is empty or when that tip isn't a commit the working repo already has and an
ancestor of `main`. When `ls-remote` exits non-zero, for a fork that doesn't exist, a refused token or a
network failure, `publish` refuses `fork-unavailable` and the push doesn't run (N11). Git's stderr isn't
parsed to tell those apart and isn't returned, since it can carry the URL. Only `ls-remote` crosses the
network for this, so nothing is fetched from the fork. If the fork has moved past root, the refusal stands
until root catches up. That is a human pull into root, not an agent's.
D9c. **The URL parse (B6).** `projects.<p>.remote` and `identity.gitea.url` are parsed with `new URL()` at load,
and the business file refuses to load unless all of these hold:
- The scheme is `https:`. There is no username, password, query or fragment.
- The string equals its own normalized form: for the upstream, `url.origin + url.pathname`; for
`gitea.url`, `url.origin + "/"` or `url.origin`. So a dot segment (`/<user>/../OWNER/REPO`), a
percent-encoded dot, an upper-case host, an explicit `:443`, a backslash, userinfo, a query and a fragment
each refuse, because `new URL()` would change them (checked with node: each differs from its normalized
form). A trailing slash survives normalization and is refused by the two-segment rule below. The checks below run on that path, so the owner that is checked is
the owner that is used.
- The upstream's path is exactly `/OWNER/REPO`: two non-empty segments, no more and no fewer. One trailing `.git` on
REPO is allowed and stripped. `fork` is built from the stripped REPO.
- `OWNER`, `REPO` and `gitea.user` each match Gitea's name rule, `^[A-Za-z0-9_.-]{1,100}$` and not `.` or
`..`. REPO after stripping must still match.
- `gitea.url`'s path is `/`. A Gitea served under a sub-path isn't supported by this row and refuses at load.
- Host and port are compared after `new URL()` normalized them, and an omitted port counts as 443 on both
sides.
- Gitea compares user and repository names without case (`lower_name`), so `remote-not-fork` compares the
upstream's OWNER with `gitea.user` without case. An upstream owned by `Coder` and a `gitea.user` of `coder`
would otherwise build a "fork" that is the upstream itself.
D9b. **How `publish` reaches the broker and the store.** The git socket runs in the bus process
(`cli.mjs:504-506`) and reaches the broker only through the host's op table (`bus/src/runtime.mjs:222-246`).
`publish` adds three entries there, each wrapped in `refusing()`:
- `checkPublish(cap, args)`: steps 1 to 3, including the decision check. It returns `{ticket, fork, user,
email, source}` and no token. The ticket holds the session and the decision until `recordPublish`, the
same way `checkLand` holds a landing.
- `withGitea(cap, fn)`: `credentials.useFor(broker.session(cap), "gitea", fn)`. `fn` gets the token and runs
step 5. The token never leaves that call.
- `recordPublish(ticket, result)`: step 6, and it spends the decision.
The op table gets no entry that returns a token.
D10. **Which refs.** Our side: only `refs/heads/<gitea.user>/*`, set by the host (D9 step 3). The default
branch and other agents' prefixes can't be named. Gitea side, two options (Q-M1):
- **O1. A fork per agent. Ruled by Mos (mos-1027).** The agent's Gitea user has read on the upstream repo and owns a fork
named like it. The host derives the fork's URL from the upstream and the agent's user (D9 step 2).
Its token can write only repos the user owns or collaborates on, so Gitea itself stops any push to upstream
or to another agent's fork. No branch rules are needed per agent, so none can drift. The cost: work reaches
upstream as a pull request from the fork, opened by a human or a later API verb.
- **O2. One shared repo. Not chosen.** The agent is a Write collaborator. Upstream's default and release branches get
branch protection with no agent on the push list, plus a rule `<user>/*` per agent. Gitea doesn't stop agent
A from creating an unprotected name outside `A/*` unless a catch-all rule outranks the per-agent ones. That
depends on Gitea's rule priority, which I haven't verified on our version (probe P1). Every hire adds a rule.
Token scopes for either: `write:repository` and `read:user`, nothing else, per vocabulary.mjs rules (no
`all`, no admin).
D11. **The host git socket.** `publish` is added to the socket. `push`, `fetch` and `land` don't change. The
socket's bind rule (E4) adds `git.push.remote`. The working repo stays credential-free and remote-free. The
`fork` URL is passed per call and never written into its config. A landing still squashes into the project
root as "Mosaic bus host". Landings don't push anywhere.
## 6. Lifecycle (brief item 4)
All verbs are on the bus, in the CLI (`mosaic credentials ...`) and in the Console's agent page. Every verb is
human-only. Under Q173 the tester may run `status` and `revoke` on the dev stack instance. `issue` and
`rotate` stay with Jason, because the tester is an agent and the token would pass through its session
(duckworth N1). Each one appends to the organization
writer's `changes.jsonl` (actor, agent, kind, verb, date; never a value). No schema migration, no new event
kind. A refusal uses `action.refused`.
| Verb | What it does |
|---|---|
| `issue AGENT KIND --rotate-by DATE` (gitea) or `--expires DATE` (vikunja) | Reads the token on stdin (never argv), checks E2's format, writes `<kind>.tmp` 0600, fsyncs, renames over `<kind>`, and sets the business file reference to `{"store": true, date}`. The bus reloads that one entry, with no restart |
| `rotate` | Same as `issue` over an existing entry. The old value is gone from disk and memory when the call returns. Jason then deletes the old token in Gitea (A5) |
| `revoke AGENT [KIND] --reason` | Drops the entry from the bus's memory first, then unlinks the file. A use in flight finishes. Every later use refuses `credential-unavailable` |
| `delete AGENT` | `revoke` for every kind, then removes the agent's dir. Refused while the agent has a running claim |
| `status [AGENT]` | Kind, date and state per entry (E3's `status()`), no values |
D12. **"Stops working at once."** On our side this holds without a timing window. A fired agent never holds a
`gitea` or `vikunja` token (D3), so revoking means the bus no longer has it. On the Gitea and Vikunja side the
token still exists until Jason deletes it (A5). The status line for a revoked entry says so until Jason
confirms. The model token is the exception: it sits in the running sandbox (D7), so a fired agent's runs must
end. Row 194's fire ends them.
D13. **What row 194 calls.**
- `credentials.issue` is never called by the chief. Row 194's hire stays uncredentialed (its D1).
- A human `create` of a credentialed definition: row 194 lifts E9's refusal for the human path only. The agent
is created with `store` references and no files. `launch` refuses `credential-unavailable` until Jason runs
`issue`.
- `fire` and `delete`: row 194 calls `credentials.revoke(business, agent, {reason, actor})` before it changes
the business file. A failed revoke stops the fire (`credential-revoke-failed`), so an agent can't be fired
with its credentials still live on our side.
- `regenerate`: no credential change.
## 7. Who does what (brief item 5)
Agents never mint accounts or tokens. Jason's steps, per agent that needs `gitea`:
- A1. Create the Gitea user: Site Administration, User Accounts, Create User Account. The name is the agent's
`gitea.user`. The email is the agent's `identity.email`.
- A2. Create its token: sign in as that user, Settings, Applications, Generate New Token, scopes
`write:repository` and `read:user`, nothing else.
- A3. Create the fork of each granted repo as that user (O1). Gitea's push-to-create
(`ENABLE_PUSH_CREATE_USER`, `ENABLE_PUSH_CREATE_ORG`) stays off, so a push to a missing `<user>/REPO`
fails rather than creating a plain repository (N11). `publish` refuses `fork-unavailable` before that.
- A4. On the stack instance's VM, as its OS user: `mosaic credentials issue AGENT gitea --rotate-by DATE`, with
the token pasted into stdin.
- A5. On fire or rotate: delete the token in Gitea (Settings, Applications), or disable the user.
- A6. Model: sign in to the provider on each VM user to write `<configDir>/secrets/claude-session.env`.
Vikunja bot accounts and tokens are already Jason's (tracker bots). Only A4 changes for them.
## 8. Migration (brief item 6)
- M1. Existing `{file}` and `{env}` references keep working. A business moves one agent at a time with
`issue`, which rewrites that agent's reference to `store`.
- M2. Nothing here touches the dev repository's helper, its `.git/config`, `~/.mosaic` or `~/.mosaic-dev`.
Dev seats (jarvis, seat tokens) keep pushing as today. The mos-0981 rule ends only when Mos says so.
- M3. The live dev stack instance needs no change at re-pin: no reference uses `store` until someone issues
one.
## 9. Questions
Mos ruled on the authority model in mos-1027 (2026-10-20T19:27Z):
- Q-M1. O1, one fork per agent.
- Q-M2. `git.push.remote` and its bounds in D9 to D11 are approved. It lands as a hard wait: out of ROUTED,
and no role grants it within-role (D9 step 1). Sage puts its routing in the PRD version draft going to
Jason with `role.hire` and `role.fire` (Vikunja #420). After his approval, a reviewed commit routes it and
grants it to coder.
- Q-M3. Issue, rotate and delete are human-only, and the chief gets none of them. Under Q173 the dev
instance's tester gets `status` and `revoke` only.
- Q-M4. The model credential stays per stack instance for 10-27. Per-agent model tokens wait for a proxy
row.
Open for Jason. Mos takes these to him one at a time:
- Q-J1. The model account for Case 1 and Case 2: existing subscription or a new account.
- Q-J2. Gitea users and tokens for the agents that need them (A1 to A3).
## 10. What this closes (brief item 7)
- G11: closes the store (D1 to D6) and lifecycle (section 6) for `gitea` and `vikunja`. Still open: per-agent
model credentials (D7), and the same-uid limit inside one stack instance (D6).
- G13: closes "own credentials for git" (D8 to D10). Still open: Gitea API verbs (section 4).
- C2.5 (git remote): Case 2's books repo can use `publish` once its project names a remote.
## 11. Tests and mutants
All tests use scratch roots, fake tokens and a fake Gitea: `git http-backend` behind a node http server on
127.0.0.1 that checks basic auth against a fake token and records each ref update. No request leaves the host.
- T1. Layout: wrong mode or owner on a dir or file refuses start. A `{file}` reference into another agent's
path refuses `credential-not-yours`. A bus restart after a create without `issue` starts, and the agent's
status is `not-issued`. A missing `{file}` still refuses start.
- T2. `useFor` keys from the cap only. A request naming another agent is refused and leaves one
`action.refused`.
- T3. The fake Gitea serves https on 127.0.0.1 with a cert from a scratch CA made by the test. It reaches git
only through the `tls` parameter (D9 step 5). Each refusal test that should reach the network asserts that
the first port received the request, so no test passes because git refused http first. `publish`: the
happy path lands `refs/heads/<user>/<branch>` on the agent's fork in the fake, which shares root's history, at
exactly the decision's commit. Refusals for each step in D9, one test each, including `work-not-yours`. Until routing, a publish with
no decision refuses, one with a decision for another task or commit refuses, and a second publish on a
spent decision refuses `decision-consumed`. Approve while the latest push is A, push B, publish: refuses
`decision-mismatch`, and the fork receives nothing. A push to the working repo between `checkPublish` and
the git push refuses with `ref-moved` or `decision-mismatch` (sage-1440, BUILD.md deviation 14). A raise with no recorded push refuses `no-push`. A test pins that `git.push.remote` is in neither ROUTED list
and that no `roles/*.json` grants it. A force-push attempt is
refused by git's non-fast-forward. A fake fork that answers 302 to a second 127.0.0.1 port: the push
fails, and the second port receives no request with an Authorization header. A remote on the right host
and a different port is refused at step 2. Two agents granted one project each land on their own fork:
agent A's push reaches `/<A's user>/REPO` only and B's reaches `/<B's user>/REPO` only, and the fake's
upstream repository receives nothing. An upstream owned by the agent's own user refuses `remote-not-fork`,
and so does one whose owner differs from `gitea.user` only in case. With an upstream that shares root's
history, a first publish to an empty fork and to an unrelated fork each refuse `remote-unrelated`, and
neither fork nor upstream receives objects. A fork that doesn't exist refuses `fork-unavailable`, and the
fake records no push. `ls-remote` and the push each reach the fork's path only (the fake records every
path). Load refusals (D9c), one test each: a dot segment, an extra path segment, a missing segment,
a trailing slash, userinfo, a query, a fragment, an explicit `:443`, a name outside Gitea's rule, and a `gitea.url` with a
sub-path. A `.git` suffix loads and builds the fork without it. A `publish` grant on an agent without
`identity`, and on one whose definition lacks `gitea`, each refuse at load with the N10 message.
- T4. Token handoff: the token never appears in argv, env, the fork URL, the working repo config,
`changes.jsonl`, events or logs (scan every file under the scratch roots).
- T5. Lifecycle: issue, rotate, revoke and delete, each live with no restart. After revoke, `publish` and the
Vikunja path refuse `credential-unavailable`, and so they do after a bus restart that follows the revoke.
The tester can run `status` and `revoke`, and `issue` and `rotate` refuse it.
- T6. Default: an agent without `identity` commits as E7. Existing `{file}` references load unchanged.
Mutants to kill: drop the cap-derived key in `useFor`, let the request name the destination ref, add `+` to
the refspec, skip the identity check, put the token in the URL, revoke file before memory, skip the dir mode
check, allow a `{file}` reference into another agent's dir, make ENOENT on a `store` path fatal again, make
ENOENT on a `{file}` path silent, a helper that answers any host, drop `http.followRedirects=false`, drop the
`work-not-yours` check, drop the `remote-unrelated` check, add `git.push.remote` to ROUTED, skip the spent
check on the decision, drop the `pushCommit` comparison in `#authorityFor`, push by ref name in place of the
oid, push to the upstream URL in place of the derived fork, run `ls-remote` against the upstream in place of
the fork, take the fork's owner from anywhere but the agent's `gitea.user`, compare the owner with case, run the
D9c checks on the raw path in place of the normalized one, read a failed `ls-remote` as empty, skip the tip
check under the lock, and pass a CA flag in production.
## 12. Risks
- R1. Backups: if a backup tool copies `<configDir>`, it copies tokens. The build checks row 185's backup set
and says what it includes.
- R2. Withdrawn. O1 was ruled, so probe P1 isn't needed.
- R3. Outbound HTTPS from the bus host to Gitea is new network reach for the host process. On the VMs this is
Jason's network to confirm.
+75
View File
@@ -0,0 +1,75 @@
# Row 237: one decision publishes once (#1743)
Megavolt, 2026-10-21 (from `date -u`). Brief: `docs/plans/2026-10-20_agent-credentials.md`
§ "row 199 follow-up N1", blob dca67d310e1b. Source: Duckworth's row 199 round 1 note N1
(duckworth-0068). Base: `cb5fdb064` (row 199's landing). Reviewer Duckworth, gate owner Sage.
## The defect
`checkPublish` opens a ticket after checking the decision is unspent. The decision is spent only at
`recordPublish`, which runs after the bus process has pushed. Two `checkPublish` calls on one decision
therefore both get tickets, and both push. With different branches, the commit lands on two fork refs:
`refs/heads/<user>/feat` and `refs/heads/<user>/other`. The second `recordPublish` then refuses
`decision-consumed`, but by then its push has already reached the fork.
## The fix, and why this option
`checkPublish` refuses `publish-busy` when an open ticket in `#publishes` holds the same business and
decision (`packages/bus/src/broker.mjs:1038-1040`). It runs after the authority and fact checks, so a
spent decision still refuses `decision-consumed` first.
The brief's other option, rechecking `#spent` in `publishTarget`, doesn't close the race. Both tickets
reach `publishTarget` before either push ends, and nothing is spent until `recordPublish`. So both
rechecks pass and both pushes run. The only state that exists while the first push runs is the open
ticket, so the check reads that.
A refusal (`publishRefused`) drops the ticket and leaves the decision unspent, so a retry on the same
decision gets a new ticket. That is row 199's behaviour, unchanged. A within-role publish has no
decision and isn't affected.
## Tests
- `packages/bus/tests/publish.test.mjs:186`. A second ticket refuses `publish-busy`, on the same branch
and on another. After `publishRefused` the retry gets a ticket and a third refuses busy. After
`recordPublish` there is one `action.allowed` for git.push.remote and the next call refuses
`decision-consumed`. Another agent's decision on the same task isn't held.
- `packages/cli/tests/publish-socket.test.mjs:269`. End to end through the git socket, the bus runtime
and the TLS fake Gitea. While the first publish holds its ticket, a second publish on the same
decision to branch `other` gets `publish-busy`. The fake Gitea gets exactly one `git-receive-pack`,
the fork has only `refs/heads/coder/feat`, and there is one `action.allowed`.
Runs on this branch, single files:
- publish.test.mjs: 13/13.
- publish-socket.test.mjs: 8/8.
## Mutant
M1 removes the `publish-busy` line. Both tests fail:
- The bus test fails at the first busy assert.
- The cli test fails because the second publish answers `ok: true` with `ref: refs/heads/coder/other`.
That is the defect: two refs on the fork.
Logs are in `/mnt/storage/scratch/megavolt-r237/` (m1-bus.log, m1b-cli.log). `cmp` confirmed
broker.mjs matched the saved copy after the restore. The first cli mutant run (m1-cli.log) was against
a hook that recursed on every checkPublish. I fixed the hook to fire only for branch `feat` and reran
it (m1b-cli.log).
## Residuals, not fixed here
1. **A ticket that never ends holds its decision until the bus restarts.** `#publishes` is in memory.
That happens when the host dies between checkPublish and recordPublish or publishRefused. It fails
closed (`publish-busy`), and a restart clears it.
2. **publish-timeout (sage-1440).** The socket files `publishRefused` and drops the ticket, but the bus
process's push may still be running. A retry on the same decision can then open a ticket while that
push is live. This is the same residual the socket already logs ("the bus process's push may still
reach the fork"). Closing it means the runtime's `remotes` must keep the decision busy until its push
settles, which reaches into runtime.mjs. That is beyond the brief's two options.
3. **checkLand has the same shape.** `#landings` takes a ticket per call and the decision is spent at
`recordLand`. Whether two landings on one decision can both reach main isn't checked here. That's a
follow-up for Sage to row or dismiss.
## Gate
Per the brief: Duckworth approves the manifest, the bus suite and every `scripts/test-*.sh` are green
on Sage's gate, and M1 is killed. This row also changes a cli test file, so the cli suite (or that file
alone) should run on the gate too.
+71
View File
@@ -0,0 +1,71 @@
# Row 241: one decision lands once (#1747, P19 441)
Megavolt, 2026-10-21 (from `date -u`). Brief: `docs/plans/2026-10-20_agent-credentials.md`
§ "Brief: row 237 follow-up, one decision lands once, with tests at the broker", c25785d3d. Source:
row 237 residual 3, megavolt-0068, megavolt-0069 and sage-1651. Base: origin/refactor f5753c0e8, row 210's landing.
Reviewer Duckworth, gate owner Sage. Tests only; no source changes.
## Why landing has no row 237 race
Row 237 fixed publish: two tickets on one decision both pushed, because the decision is spent only
after the push, and a push to a remote fork can't be taken back. Landing has the same ticket shape
(`checkLand` opens a ticket, `recordLand` spends the decision), but three guards keep one decision
from landing twice:
1. **land-busy, the outer guard.** `doLand` in `packages/cli/src/git-socket.mjs:351` refuses
`land-busy` while another landing runs. The slot is taken before the first await, and freed only
when the landing ends. There is one git socket per launcher, and one host per dataRoot (host.lock).
So through the socket two landings never overlap, and a later one stops at `checkLand` with
`decision-consumed`. Even past that, applying the same reviewed patch to the new main fails
`base-conflict` in `prepareLand`.
2. **The CAS on main.** `finishLand` (`packages/seat/src/working.mjs:391`) moves main with
`update-ref main <commit> <old>`, so a landing prepared on a tip that has since moved refuses
`base-moved` and writes nothing.
3. **The recordLand recheck.** `finishLand` calls `record()`, which is `recordLand`, under the same
repository lock. `recordLand` runs `#landFacts` again (`packages/bus/src/broker.mjs:963`), which
refuses `decision-consumed` once the decision is spent. When `record()` throws, `finishLand` moves
main back.
Guards 2 and 3 can't be reached end to end, because guard 1 stops a second landing first. So they are
pinned below the socket, and no test bypasses land-busy.
### The unrecorded-commit window
Between the `update-ref` and the end of `record()`, main names the squash, which isn't recorded yet.
That window is under the repository's lock, so no other push, review or landing in this host sees
it. If `record()` refuses, main moves back. If moving back fails, `finishLand` refuses
`rollback-failed` and main keeps naming an unrecorded commit (tested at `working.test.mjs:555`, the
main.lock case). That is existing behaviour, unchanged here.
## The test
`packages/bus/tests/land.test.mjs:312`. Two tickets pass `checkLand` on one decision before either
records. `recordLand` on the first lands. `recordLand` on the second refuses `decision-consumed`
(:319), and leaves one merge `action.allowed`, one RESULT and no `action.refused`. `landRefused`, as
the socket would call it, then files the refusal and ends the ticket.
**The assert that pins the recordLand recheck is :319.** The `#spent` line is shared with
`checkLand`, so dropping it also fails :144 (a `checkLand` after the decision is spent). Only :319
shows the recheck at record time.
Single file on this branch: land.test.mjs 18/18.
Whole suites on this branch before in-review: bus 500/500, seat 164/164, cli 565/565 (02:52 to 03:01Z).
## Mutants
| Mutant | Change | Killed by |
|---|---|---|
| M1 | drop the `#spent` line in `#landFacts` (broker.mjs:963) | land.test.mjs: :312 at :319, and :94 at :144 (16/2) |
| M2 | drop `land.old` from the `update-ref` in `finishLand` (working.mjs:391) | working.test.mjs:555 at :567, the `base-moved` assert (25/1) |
| M3 | drop the `land-busy` throw (git-socket.mjs:351) | git-socket.test.mjs:812 at :825, the `land-busy` assert (23/1) |
Each run was one test file on base f5753c0e8 (row 210's landing, which moved the `#spent` line from
:945 to :963), 2026-10-21 02:51Z. Each assert failed with "Missing expected exception" or "rejection".
`cmp` confirmed each source matched its saved copy after the restore. M2 is killed by the existing
working.test:555, so per the brief working.test.mjs isn't changed. Logs: `/mnt/storage/scratch/megavolt-r241/`
(m1.log to m3.log, with each diff).
## Gate
Per the brief: Duckworth approves the manifest; the bus, seat and cli suites and every
`scripts/test-*.sh` are green on Sage's gate; the three mutants are killed.
+108
View File
@@ -0,0 +1,108 @@
# Row 242 build: a retry after publish-timeout pushes once
Row 242, #1748, P19 440. Brief: docs/plans/2026-10-20_agent-credentials.md, "Brief: row 237
follow-up N1, a retry after publish-timeout pushes once" (9d165be8c). Base: origin/refactor
8fede5fc6 (row 210 landed f5753c0e8).
## The defect
The git socket polls publishResult until publishDeadlineMs, then refuses publish-timeout
(packages/cli/src/git-socket.mjs:433) while the bus process's push (runtime publishPush) can
still be running. The socket then calls publishRefused, which went straight to
broker.publishRefused: it filed the refusal and deleted the ticket. checkPublish no longer saw an
open ticket on the decision, so a retry got a new ticket and pushed a second ref while the first
push was still on its way to the fork.
## Fix chosen: hold the decision until the push ends, then record a push that reached the fork
packages/bus/src/runtime.mjs, `publishRefused` and `settle`:
- If the bus has no push for the ticket, publishRefused goes to the broker as before.
- If the push is still running, the refusal code is held on the entry and the ticket stays open.
checkPublish refuses another publish on the decision with publish-busy (row 237's check),
the same as when the socket is still polling.
- When the push ends, `settle` runs:
- A push that reached the fork is recorded with broker.recordPublish, which spends the
decision. The bus logs `pushed after the git socket refused <code>; recorded`.
- A push that failed is filed as publishRefused with the socket's code.
- If recordPublish refuses with a BusError, that code is filed instead.
- If the push has already ended when the refusal arrives, it's settled at once. The sage-1440 test
relies on this: its push fails fast, so publish-timeout is filed once, immediately, as before.
Why hold rather than kill. Killing the git client doesn't undo a push the fork has already
received: receive-pack can update the ref after the client dies. After a kill the bus would have
to ls-remote the fork to learn what landed, and the result could still race the server. Holding
waits for the push's own outcome. Each git run in publishRemote already has its own limit
(publishRemote defaults to checkout.mjs GIT_LIMITS), so a hold is bounded. Holding also reuses the existing publish-busy
state, and the refusal text is unchanged.
Why record and not only hold. If the hold just filed publish-timeout once the push ended, the
ticket would close and the decision would be free again with a ref on the fork. A retry would then
push a second ref. Recording the late push spends the decision, so the retry gets
decision-consumed.
The publish-timeout refusal text is unchanged (brief condition).
## Test
packages/cli/tests/publish-socket.test.mjs:302, "row 242: a retry after publish-timeout refuses
publish-busy while the bus process's push runs; the late push is recorded and the decision pushes
once".
- publishDeadlineMs 150 and a store token. The fork's pre-receive hook writes `held`, then loops
until the test writes `release`.
- The first publish gives publish-timeout. The test waits for `held`.
- A retry to branch `other` gives publish-busy (:322). Refused events: ["publish-busy"]. No
git.push.remote allowed event.
- The test writes `release` and waits until there is exactly one allowed event, with ref
refs/heads/coder/feat and the work commit.
- The next retry gives decision-consumed (:330).
- Exactly one git-receive-pack request reached the fake Gitea, and the fork has only
refs/heads/coder/feat. Refused events: ["publish-busy", "decision-consumed"]. No TOKEN appears in
events or logs.
Fail fast (sage-1681): the body is wrapped in try/finally, and the finally writes `release`. Under
the first mutant run, the assert at the retry failed and the hook was never released. setup's
teardown waits for the fake Gitea, which waited for the held push, so the file hung until I stopped
it. Now a failed assert ends the hook and the run. Each wait is bounded at 10 s with an assert.fail.
## Mutants
Script: /mnt/storage/scratch/megavolt-r242/mutants.sh. Each mutant: save, one-line sed, run
publish-socket.test.mjs whole, restore, cmp.
| Mutant | Change | Expected kill |
|---|---|---|
| M1 | export `publishRefused: (ticket, args) => broker.publishRefused(ticket, args)` again (removes the fix) | :302 at :322: the retry gets a new ticket and its own push, so publish-timeout, not publish-busy (seen in the stopped 03:06Z run) |
| M2 | `settle` files publishRefused without trying recordPublish (hold only) | :302 at the wait for one allowed event, then :330 |
Results (Sage's slot, sage-1686, 2026-10-21T03:35:00Z to 03:35:15Z): both killed, both restored
(cmp ok).
- M1: publish-socket 8/1, the row 242 test fails at :322 in 0.5 s.
- M2: publish-socket 8/1, the row 242 test fails at :327, the wait for one allowed event, through
the assert.fail inside `until` (:311) after its 10 s bound, in 10.3 s.
## Suites
On Sage's slot (sage-1686), on base 8fede5fc6 plus this row:
- bus `node --test tests/*.test.mjs`: 499/499, 03:35:20Z to 03:35:34Z. This is one fewer than
Sage's 241 gate (500) because the base predates row 241's land.test case.
- cli `node --test tests/*.test.mjs`: 566/566, 03:35:34Z to 03:43:47Z. That's 565 plus the
row 242 test.
Singles before the slot: publish-socket.test.mjs 9/9, bus publish.test.mjs 13/13. The seat suite
and scripts/test-*.sh are left for Sage's landing gate.
## Residuals
- The agent was told publish-timeout, but the push is recorded later and the run isn't
notified. The allowed event and the bus log line are the only trace. A later publish on the
decision gets decision-consumed, which tells the agent the decision was used. Whether the run
should get a RESULT for a late record is a follow-up question for Sage.
- A late push whose recordPublish refuses (Duckworth N2) is filed with that code: the ref stays on
the fork and the decision is not spent, the same residual as an in-band push whose record
refuses.
- The hold lives in the bus process's memory, like the broker's open tickets. A bus restart during
a hold drops both, the same as today for any publish in flight.
+10
View File
@@ -0,0 +1,10 @@
# Quackerjack
Builder seat, Claude Haiku 5.5 at high effort, started by Sage under mos-1023 item 1 and Q177 for federation
rows R5 (`mosaic access list, grant, revoke`) and R2 (the `federation` config key), per Gizmoduck's design
section 7 (#414).
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no
credentials of its own; until the credentials row settles per-agent identity, it pushes only its own
`quackerjack/*` branches through the repository credential helper (mos-0981), never `refactor`, `main` or
another seat's branch.
+36
View File
@@ -0,0 +1,36 @@
# Row 189 gate timing (for row 197, Herb)
Sage, 2026-10-20T19:25:04Z. Gate on 0c177e9ef (base 0217b71c8), one lane. The slot was mine. Seats may have run single test files at the same time, which the slot rules allow, and I did not record that.
Start 2026-10-20T19:09:37Z, end 2026-10-20T19:20:36Z. Total wall time: 659 s.
Wall time per suite is the gap between one suite's log mtime and the one before it (the first suite starts at start.txt), so it includes node start-up. node's duration_ms is listed where the suite prints it. Scripts print no duration.
| Suite | Wall s | duration_ms s | Ended (UTC) |
|---|---|---|---|
| pkg-bus | 3 | 3.8 | 19:09:40 |
| pkg-business | 3 | 2.2 | 19:09:43 |
| pkg-cli | 388 | 388.5 | 19:16:11 |
| pkg-control-board | 1 | 0.8 | 19:16:12 |
| pkg-conversation | 35 | 35.4 | 19:16:47 |
| pkg-discord | 3 | 2.6 | 19:16:50 |
| pkg-harness | 12 | 11.6 | 19:17:02 |
| pkg-ledger | 13 | 13.5 | 19:17:15 |
| pkg-mosaic | 3 | 2.3 | 19:17:18 |
| pkg-plugins | 0 | 0.3 | 19:17:18 |
| pkg-project | 1 | 1.2 | 19:17:19 |
| pkg-runs | 1 | 0.8 | 19:17:20 |
| pkg-seat | 6 | 6.2 | 19:17:26 |
| pkg-tasks | 1 | 1.2 | 19:17:27 |
| pkg-webui | 58 | 57.9 | 19:18:25 |
| script-auth | 1 | - | 19:18:26 |
| script-conductor | 5 | - | 19:18:31 |
| script-config | 2 | - | 19:18:33 |
| script-discord | 8 | - | 19:18:41 |
| script-extension-package | 1 | - | 19:18:42 |
| script-foundation | 40 | - | 19:19:22 |
| script-land-work | 11 | - | 19:19:33 |
| script-queue | 21 | - | 19:19:54 |
| script-release | 11 | - | 19:20:05 |
| script-task | 31 | - | 19:20:36 |
Source: /mnt/storage/scratch/sage-g189/out/*.log mtimes and gate.log. The cli suite is about 59% of the run, and webui plus conversation add about 14%.
+8
View File
@@ -0,0 +1,8 @@
# PRD 1.8 r7: no in-Stack raise (mos-1154)
Sage, 2026-10-21T01:20:06Z. Jason approved PRD 1.8 in T3 (thread 38bc4c81, before 2026-10-21T01:16:55Z), verbatim:
"PRD 1.8: I am preemptively approving it. Do not surface in the Mosaic Stack board. Just proceed."
Mos records it as DECISIONS Q192 (mos-1154). The [mos test] raise request drafted here was dropped
unsent, and the template is deleted. Jason closed 3a6c8fb9 himself at 01:05:45Z (no, "superseded by r7").
Scope (Mos's reading): r7 at 47c2cb33c plus 82df17347, 342 lines, sha256
de079281ff2760911039276379ec4b190287b2ce2e4e8a79c9459939727440cd, Duckworth duckworth-0073 (pass, no findings).
@@ -0,0 +1,35 @@
{
"seq": 22,
"id": "3a6c8fb9-a5ab-42f8-9755-902a88375ca2",
"at": "2026-10-20T22:41:14.444Z",
"business": "mosaic-stack",
"project": null,
"raised_by_role": "pm",
"raised_by_run": "rc9e993da24cf",
"class": "gated",
"action": "prd.approve",
"route_to": "human",
"question": "Approve PRD 1.8 exactly as drafted?\n\nFile: agents/honker/work/queue-194/PRD-1.8-DRAFT.md on mosaicstack/stack at commit 762a6711ff7ed049aa73dfde98b75ebf326bdd99 (revision 5), 319 lines, sha256 8eee52470b1d7df139164e3ba5cc128dc469457b2b52f298c12685c3483011a8. Duckworth reviewed it (duckworth-0061, confirmed duckworth-0062) and Mos checked it (mos-1140). The tip of branch honker/row194-b2 holds a later r6 (aba3cbdeb), which is not used here. This approval covers only the r5 commit and sha256 above. The PM copied these values from the request and did not compute or check the sha256.\n\nNotes the approval carries:\n1. Mos's reading: \"the main agent or chief of staff\" in Q10 is one role under two names, the chief of staff. Only the chief raises a grant request; the assistant passes the user's request to the chief. To change it, say so at approval and REQ-GRANT-1 changes first.\n2. Mos's reading: revoke by \"you, Mos or the tester\" means the user, the assistant (\"Mos\", Jason's assistant) for grants in its own organization, and the tester only on a dev instance.\n3. N5: host.sudo keeps the arbiter route version 1.7 set for git.push.remote. A raise for it that cites the spec and isn't within-role goes to the arbiter for its domain, not to the user. In manual mode it comes to the user.\n4. B7 residual: only an Approve creates a grant, but an agent running as the stack's OS user can still write the stack's own store (Q182 on dragon-lin, #423 on .53 and .54). This version doesn't close it.\n5. N6: the broker enforces only that the chief of staff raised a grant request. The rest of the chain of command is the messaging lists and role contracts, not a broker check.\n\nSource: a tester request relayed by the chief (message 6990efb4, tester message d54a190e). It is a test request, not Jason's instruction. It takes the same route as PRD 1.7 (decision d52febb5): non-blocking, no task, uncited, so it goes to the human's Inbox. I recommend yes because two reviewers have passed r5 and the open points are written down above, not hidden.",
"options": [
{
"key": "yes",
"text": "Approve PRD 1.8 exactly as drafted: the r5 file at commit 762a6711ff7ed049aa73dfde98b75ebf326bdd99 with the sha256 below, and only that."
},
{
"key": "no",
"text": "Don't approve. Say what changes, and a new revision comes back for review."
}
],
"recommendation": "yes",
"task_ref": null,
"requirement_ref": null,
"blocking": false,
"supersedes": null,
"spec_path": null,
"spec_commit": null,
"authorization": {
"action": "prd.approve",
"target": null,
"approvalChoice": "yes"
}
}
+37
View File
@@ -0,0 +1,37 @@
[
{
"seq": 22,
"id": "3a6c8fb9-a5ab-42f8-9755-902a88375ca2",
"at": "2026-10-20T22:41:14.444Z",
"business": "mosaic-stack",
"project": null,
"raised_by_role": "pm",
"raised_by_run": "rc9e993da24cf",
"class": "gated",
"action": "prd.approve",
"route_to": "human",
"question": "Approve PRD 1.8 exactly as drafted?\n\nFile: agents/honker/work/queue-194/PRD-1.8-DRAFT.md on mosaicstack/stack at commit 762a6711ff7ed049aa73dfde98b75ebf326bdd99 (revision 5), 319 lines, sha256 8eee52470b1d7df139164e3ba5cc128dc469457b2b52f298c12685c3483011a8. Duckworth reviewed it (duckworth-0061, confirmed duckworth-0062) and Mos checked it (mos-1140). The tip of branch honker/row194-b2 holds a later r6 (aba3cbdeb), which is not used here. This approval covers only the r5 commit and sha256 above. The PM copied these values from the request and did not compute or check the sha256.\n\nNotes the approval carries:\n1. Mos's reading: \"the main agent or chief of staff\" in Q10 is one role under two names, the chief of staff. Only the chief raises a grant request; the assistant passes the user's request to the chief. To change it, say so at approval and REQ-GRANT-1 changes first.\n2. Mos's reading: revoke by \"you, Mos or the tester\" means the user, the assistant (\"Mos\", Jason's assistant) for grants in its own organization, and the tester only on a dev instance.\n3. N5: host.sudo keeps the arbiter route version 1.7 set for git.push.remote. A raise for it that cites the spec and isn't within-role goes to the arbiter for its domain, not to the user. In manual mode it comes to the user.\n4. B7 residual: only an Approve creates a grant, but an agent running as the stack's OS user can still write the stack's own store (Q182 on dragon-lin, #423 on .53 and .54). This version doesn't close it.\n5. N6: the broker enforces only that the chief of staff raised a grant request. The rest of the chain of command is the messaging lists and role contracts, not a broker check.\n\nSource: a tester request relayed by the chief (message 6990efb4, tester message d54a190e). It is a test request, not Jason's instruction. It takes the same route as PRD 1.7 (decision d52febb5): non-blocking, no task, uncited, so it goes to the human's Inbox. I recommend yes because two reviewers have passed r5 and the open points are written down above, not hidden.",
"options": [
{
"key": "yes",
"text": "Approve PRD 1.8 exactly as drafted: the r5 file at commit 762a6711ff7ed049aa73dfde98b75ebf326bdd99 with the sha256 below, and only that."
},
{
"key": "no",
"text": "Don't approve. Say what changes, and a new revision comes back for review."
}
],
"recommendation": "yes",
"task_ref": null,
"requirement_ref": null,
"blocking": false,
"supersedes": null,
"spec_path": null,
"spec_commit": null,
"authorization": {
"action": "prd.approve",
"target": null,
"approvalChoice": "yes"
}
}
]
+13
View File
@@ -0,0 +1,13 @@
[mos test] Please have the PM raise a prd.approve decision for PRD 1.8, the same route as PRD 1.7 (decision d52febb5): blocking false, no task_ref, recommendation yes, gated to the human. The PM must not compute or change the sha256; it copies the values below.
Question: "Approve PRD 1.8 exactly as drafted?"
File: agents/honker/work/queue-194/PRD-1.8-DRAFT.md on mosaicstack/stack at commit 762a6711ff7ed049aa73dfde98b75ebf326bdd99 (revision 5), 319 lines, sha256 8eee52470b1d7df139164e3ba5cc128dc469457b2b52f298c12685c3483011a8. Reviewed by Duckworth (duckworth-0061, confirmed duckworth-0062) and checked by Mos (mos-1140). The branch tip of honker/row194-b2 holds a later r6 (aba3cbdeb) that is not used; the approval covers only the r5 commit and sha256 above.
The decision notes must carry these five items:
1. Mos's reading: "the main agent or chief of staff" in Q10 is one role under two names, the chief of staff. Only the chief raises a grant request; the assistant passes the user's request to the chief. To change it, say so at approval and REQ-GRANT-1 changes first.
2. Mos's reading: revoke by "you, Mos or the tester" means the user, the assistant ("Mos", Jason's assistant) for grants in its own organization, and the tester only on a dev instance.
3. N5: host.sudo keeps the arbiter route version 1.7 set for git.push.remote. A raise for it that cites the spec and isn't within-role goes to the arbiter for its domain, not to the user. In manual mode it comes to the user.
4. B7 residual: only an Approve creates a grant, but an agent running as the stack's OS user can still write the stack's own store (Q182 on dragon-lin, #423 on .53 and .54). This version doesn't close it.
5. N6: the broker enforces only that the chief of staff raised a grant request. The rest of the chain of command is the messaging lists and role contracts, not a broker check.
Reply with the decision id once raised. Do not resolve it.
+8
View File
@@ -0,0 +1,8 @@
sent d54a190e to chief
chief (RESULT, re d54a190e) cc46a7b4:
I passed your request to the pm without changing any of it, and launched the pm so it can act on it. No decision id yet, because the pm hasn't raised the decision.
- **Message to pm:** 6990efb4-8321-4d0e-9b8a-771f65625b83. It says the request comes from a tester and authorizes nothing, and it carries your PRD 1.8 values and the five notes as you wrote them.
- **pm launch:** run rc9e993da24cf.
When the pm answers with the decision id, I'll send it to you.
+5
View File
@@ -0,0 +1,5 @@
trail 3a6c8fb9-a5ab-42f8-9755-902a88375ca2: 4 row(s)
2026-10-20T22:40:47.949Z events#1550 session.launched pm
2026-10-20T22:40:47.969Z role_claims#102 claim pm by rc9e993da24cf
2026-10-20T22:41:14.444Z decisions#22 gated prd.approve → human: Approve PRD 1.8 exactly as drafted? File: agents/honker/work/queue-194/PRD-1.8-DRAFT.md on mosaicstack/stack at commit …
2026-10-20T22:41:14.445Z events#1553 action.allowed pm decision.raise decision 3a6c8fb9
+86
View File
@@ -0,0 +1,86 @@
# Row 189: `stack-release.sh activate --no-send`
Gitea #1687. Vikunja P19 #413 (Q175 interim, #412). Brief: docs/plans/2026-10-20_activate-no-send.md.
Rulings: mos-0972 (the Console runs without `--send` until Q175 phase 2), mos-0973 (go; Sage builds,
Dewey reviews; gate is the cli suite plus every scripts/test-*.sh). Base: def2e7932 (refactor, the
brief commit on 9ae8c73ac).
## What it does
`activate ID BUSINESS --no-send` drops `--send` from `RUN_CONSOLE` in a new `send_for_target`, which
`console_for_target` calls after `proof_for_target`. `install_stack` renders the unit from
`RUN_CONSOLE` and `write_pointer` writes the pointer from it, so both lose the flag and `flags_drift`
stays quiet afterwards. Only `activate` takes the flag; the parser sends every other op to usage
(exit 4).
`check_target` lets a `--no-send` activate name the pointer's copy only when the pointer's Console
flags hold `--send`. That run is a live-copy re-pin, the same path row 187 uses for the proof pair:
the plan prints the live-copy note, the run stops both units, backs up the stopped store and starts
them again. A plain same-copy activate, or a same-copy `--no-send` once `--send` is gone, still
refuses `already-active`.
`--plan` prints one `send:` line after the `proof:` line, for activate and rollback:
- `send: dropped; --no-send was given`
- `send: off; --no-send was given and the pointer holds no --send`
- `send: carried` (the pointer's `--send` goes on unchanged)
- `send: off`
The steps log gets `drop --send; --no-send was given`, or `--no-send was given and the pointer holds no
--send, so there is nothing to drop`.
Rollback and pm-on carry the pointer's flags as they are, so once `--no-send` has run, nothing adds
`--send` back. There is no re-enable flag (mos-0973). Q175 phase 2 changes the Console flags in its
own row.
docs/TOOLS.md: `[--no-send]` in the usage block and one paragraph after the proof paragraph.
## Tests (packages/cli/tests/stack-release.test.mjs, T55)
1. Seeded preview pointer with `--send --tester-port 7333`: a same-copy `activate B acme --no-send`
passes. The pointer and the unit ExecStart lack `--send` and keep the other flags in order, status
says the unit flags match, the steps log has the drop line, the log line has from = to = B and the
backup sum equals the stopped store's, and the units stop and start in order.
2. Plain same-copy activate, with `--tester-port` and with `--no-proof`, refuses `already-active`. So does
a same-copy `--no-send` once `--send` is gone, and on a pointer that never held it.
3. The four `send:` lines, the live-copy note on the same-copy plan only, and the Console line right
after `send:` without `--send`. The run on a pointer with no `--send` logs the nothing-to-drop line
and no drop line.
4. After `--no-send` on C1, a rollback to B leaves `--send` out of the pointer and the unit, the steps
log never names `--send`, and status matches. pm-on after `activate --pm-after-harness --no-send`
keeps it off.
5. `--no-send` on rollback (with and without `--plan`), pm-on, status, adopt and rehearse is a usage
error with the usage text, nothing written, started or logged. The usage text shows `[--no-send]`.
6. A pointer holding both the proof pair and `--send`: the plans for `--no-proof`, `--no-send` and both
name what each drops and carries. `--no-send` keeps the pair, `--no-send` with a new pair replaces it,
`--no-proof` keeps `--send`, and both together leave neither. Status matches after each.
`planTail` gains `'send: off'` after `'proof: off'`, because every plan now prints the line. T36 takes
`out.slice(-n)` around `planTail`, so its four slice counts grow by one.
## Mutants
mutants.sh in this packet, run on the candidate. Each mutates scripts/stack-release.sh and runs T55
plus T1 and T2.
- M1: the same-copy exception ignores `--no-send`.
- M2: the same-copy exception allows `--no-send` without `--send` in the pointer.
- M3: with `--no-send`, the pointer is written from `P_CONSOLE`, so it keeps `--send` while the unit
drops it.
- M4: rollback re-adds `--send`.
- M5: the parser accepts `--no-send` for rollback.
Results: mutants-run.log, all five killed (M1 5 failures, M2 2, M3 7, M4 2, M5 2).
## Deviations from the brief
- The brief's line numbers were taken before row 163 landed. The candidate corrects them to the
9ae8c73ac lines (check_target :249-256, flags_drift :646-657, console_for_target :664-693, adopt
:828).
- Test 5 also covers rehearse and `rollback --plan`, which the brief did not list.
## Outbound calls
None. The change edits an array before the existing unit render and pointer write. Every test uses
the stand-in systemctl, units, stores and data root the file already builds; no test touches
`~/.mosaic-dev`, the live units or a slot file.
@@ -0,0 +1,5 @@
M1 KILLED fail=5
M2 KILLED fail=2
M3 KILLED fail=7
M4 KILLED fail=2
M5 KILLED fail=2
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
# Row 189 mutants M1-M5: each mutates scripts/stack-release.sh in the sage-b189 worktree, runs T55 and
# the plan-tail tests, and restores the file from a checked backup.
set -uo pipefail
WT=/mnt/storage/scratch/sage-b189
OUT=/mnt/storage/scratch/sage-b189-mut/out
S=$WT/scripts/stack-release.sh
mkdir -p "$OUT" || exit 1
cp -p "$S" "$OUT/stack-release.sh.orig" || { echo "backup failed"; exit 1; }
ORIG=$(sha256sum "$S" | cut -d' ' -f1)
[ "$(sha256sum "$OUT/stack-release.sh.orig" | cut -d' ' -f1)" = "$ORIG" ] || { echo "backup differs"; exit 1; }
restore() { cp -p "$OUT/stack-release.sh.orig" "$S"; [ "$(sha256sum "$S" | cut -d' ' -f1)" = "$ORIG" ] || { echo "RESTORE FAILED"; exit 1; }; }
trap restore EXIT
mutate() {
case "$1" in
M1) sed -i '/{ \[ "\$NO_SEND" = 1 \] && \[\[ " \${P_CONSOLE\[\*\]} " == \*" --send "\* \]\]; } ||/d' "$S" ;;
M2) sed -i 's/{ \[ "\$NO_SEND" = 1 \] && \[\[ " \${P_CONSOLE\[\*\]} " == \*" --send "\* \]\]; } ||/{ [ "$NO_SEND" = 1 ]; } ||/' "$S" ;;
M3) sed -i 's/^ write_pointer "\$TO" "\$RUN_PM" "\$RUN_PENDING" "\$P_CLAUDE" "\$T_SCHEMA" "\${RUN_CONSOLE\[@\]}"$/ if [ "$NO_SEND" = 1 ]; then write_pointer "$TO" "$RUN_PM" "$RUN_PENDING" "$P_CLAUDE" "$T_SCHEMA" "${P_CONSOLE[@]}"; else write_pointer "$TO" "$RUN_PM" "$RUN_PENDING" "$P_CLAUDE" "$T_SCHEMA" "${RUN_CONSOLE[@]}"; fi/' "$S" ;;
M4) sed -i 's/^ SEND_PLAN="send: off"$/ SEND_PLAN="send: off"; [ "$OP" != rollback ] || RUN_CONSOLE+=(--send)/' "$S" ;;
M5) sed -i 's/--no-send) \[ "\$OP" = activate \] || usage;/--no-send) { [ "$OP" = activate ] || [ "$OP" = rollback ]; } || usage;/' "$S" ;;
esac
}
for m in M1 M2 M3 M4 M5; do
restore
mutate "$m"
if [ "$(sha256sum "$S" | cut -d' ' -f1)" = "$ORIG" ]; then echo "$m NOT-APPLIED"; continue; fi
bash -n "$S" || { echo "$m SYNTAX"; continue; }
diff "$OUT/stack-release.sh.orig" "$S" > "$OUT/$m.diff"
(cd "$WT" && TMPDIR=/mnt/storage/scratch/t189 timeout 900 node --test --test-reporter=tap --test-name-pattern='T55|T1 |T2 ' packages/cli/tests/stack-release.test.mjs) > "$OUT/$m.log" 2>&1
fails=$(awk '/^# fail /{print $3}' "$OUT/$m.log")
if [ "${fails:-0}" -gt 0 ]; then echo "$m KILLED fail=$fails"; else echo "$m SURVIVED"; fi
done
+21
View File
@@ -0,0 +1,21 @@
# Row 66 round 3: rebase only
Sage, 2026-10-20. Round 2 (candidate 0f4a57956, manifest cee63e06fdea) was approved by Filbert (29442) and
Dewey (29443). Its base 5487ed39d is now far behind refactor, and rows 190 and 192 changed four of its paths.
Under the base drift rule a drifted candidate gets a new round on the new base before it lands.
- Candidate: branch `sage/row66-r3`, commit 2369c00e36874e1100b301e0d01cbd105c0c5d4f, two cherry-picks of 8ec03b45a and 0f4a57956 onto c40d846cb.
No conflicts and no hand edits.
- `git patch-id --stable` of the r3 diff (c40d846cb..2369c00e36874e1100b301e0d01cbd105c0c5d4f) equals the r2 diff's (5487ed39d..0f4a57956):
6d085621fd355d43ccaf0efaefe7e74179888ff2.
- Manifest `candidate-manifest-r3.sha256`, sha256 42e3876ca1689719f0e07e66fc9fbd6e4faf3c07678da024accb54c58caef0d1. Same 23 paths as r2. Four hashes differ, and only
because the base moved: `docs/TOOLS.md`, `scripts/stack-copy.sh`, `packages/cli/tests/stack-copy.test.mjs`,
`packages/webui/tests/pinned-copy.test.mjs`. The other 19 are byte-identical to r2.
- Singles on the new base (`singles-r3.log`): every touched test file plus the unslop selftest, all green.
`packages/bus/tests/channel-proof.test.mjs` pins plug-in hashes and names `index.mjs`; it reads only channel
plug-ins, not the two harness files this row changes, and it passes (2/0).
- Still to do: the full gate on my slot after the MVP gates, then the landing sessions on both harnesses that
the row's gate names.
What I ask the reviewers: confirm the rebase. In the four drifted files, check that row 66's hunks sit correctly
next to rows 190 and 192 (`stack-copy.sh` COPY_PATHS in particular). Nothing else changed.
@@ -0,0 +1,23 @@
b99e7f4bf58ac68c8f5fa58eb7b90c0f85b15ca9a80fd093f6aaeafeac931608 adapters/README.md
c32d2a3276db92433d4e3c4f4b45fab048d9a46119d9f69cead6a11cfacfeadb agents/sage/work/queue-66/BUILD.md
ac318150f5e6193adf3750d76b64c643c29f324310c4004b0bdde8caef863b53 docs/TOOLS.md
64a2d00ba57b8673c0905f322711023c10d4dfdc0ccc7e6daa2c440b4d57e34e packages/cli/tests/stack-copy.test.mjs
b027dc74583217b4151173ecbabfcfbfc7b108ac6b7a8e93dd21a2834051714f packages/harness/README.md
bfb2156c609eeaf87c9e209c562576a5ef47535eaeee61d841bb0bd75a76a69d packages/harness/src/runner.mjs
0a27f2c8de7b9191f9c0d4a209331c63ff7630923e669838b41715924b0a64b0 packages/harness/tests/bundle.test.mjs
81d0352f2dcd41eb2e2ef962365d9b97515d7e8d9668a35bb9ce9dce4f1451ea packages/harness/tests/claude-session.test.mjs
b16f33442cefd5b5ae2af85edab33250ee6b31025e437edf0e4eba9fe0afa793 packages/harness/tests/fixtures/fake-adapter.mjs
1e187a2e23a197640d139e6975dfaf83dd7e337c360d128833678dc60f3e2046 packages/harness/tests/pi-session.test.mjs
37ca6ad6467573658c1ac6807b6cd00e3f9da289107fb689190a1872bff9b89c packages/harness/tests/runner.test.mjs
04c24058f37260c9192d5036c8cf28b50faba8e3e3bf57f198d233485d0d57e6 packages/plugins/harness/claude-code/index.mjs
2c1252e8e8db3ba4551bf1e34156ddf4e933cc664fc157105bef9a2705d6ce3b packages/plugins/harness/pi/index.mjs
7e0bea27d4360a39874fa64b3506a85f83888613a285ec0b087d74bf22b76421 packages/plugins/README.md
ec4ec9502ee8e766e3abd04d9f724c0d8ff4ef114394a0f76d5320c2dc715c5c packages/plugins/tests/harness.test.mjs
f6141dac9a8c61573e4198480fed85e093992ee9c19fb55e9bc2752381b7b07a packages/seat/tests/sandbox.test.mjs
09c6cadaf2e27677c95ba5bcb6a359cb6b3ce596ba1db6171b26586d4f9834a8 packages/webui/tests/pinned-copy.test.mjs
e9ff91cd073a1b784f9ea440b18db913ce8107df6246e3b0b0e962eda43f046b scripts/stack-copy.sh
7ee6ed6e6ac55b1348d40a358372e66c86fdebacd90001d447040133e1187882 tools/unslop-hook/claude-hook.js
ce1dfb11d0a8e8efd3d6dae67cc259c2e7443a62c7416bdedd4c71306d8c6df1 tools/unslop-hook/extension.ts
f07d67806f40763b4be87d31c2976448b5cfc34d7328519fae2d8313b1ca3e20 tools/unslop-hook/README.md
dbe3d5c9ae654d2401e3c496a6b03757a438e4a551fd8e460df8c82902bb80a7 tools/unslop-hook/test-unslop-check.js
76be78178a0703b9d20a7df2278ecb13f429dea90cea45180f22302e267ea25e tools/unslop-hook/unslop-check.js
+11
View File
@@ -0,0 +1,11 @@
packages/cli/tests/stack-copy.test.mjs rc=0 ℹ pass 14 ℹ fail 0
packages/harness/tests/bundle.test.mjs rc=0 ℹ pass 22 ℹ fail 0
packages/harness/tests/claude-session.test.mjs rc=0 ℹ pass 10 ℹ fail 0
packages/harness/tests/pi-session.test.mjs rc=0 ℹ pass 10 ℹ fail 0
packages/harness/tests/runner.test.mjs rc=0 ℹ pass 39 ℹ fail 0
packages/plugins/tests/harness.test.mjs rc=0 ℹ pass 9 ℹ fail 0
packages/seat/tests/sandbox.test.mjs rc=0 ℹ pass 10 ℹ fail 0
packages/webui/tests/pinned-copy.test.mjs rc=0 ℹ pass 10 ℹ fail 0
unslop rc=0 28/28 passed
SINGLES_DONE
2026-10-20T23:15:14Z
@@ -196,3 +196,37 @@ a piece. It passed, so the "pm doesn't answer" branch does not apply and
no defect is filed.
Piece 1 is Jason's next real Mosaic Stack request to pm. It is not open yet.
## 2026-10-21T01:14Z dates (sage-1622, Mos mos-1148, Vikunja #442)
Mos accepted the plan with changes C1 and C2. The dates are latest-by markers (Q111).
1. Pieces 1-5 open by 2026-10-23 23:59Z. Mos gives Jason the plain command after tonight's open items with him
(Q13 on PRD 1.8, the 1012 and 1008 boot). I don't ask again.
2. C1: no piece is pinned to a copy. A piece runs on whatever copy is live when Jason sends it. For each piece
this log records the activation id at send. A piece still open across a window gets the new activation id
recorded too.
3. C2: neither the Q175 phase 2 window (10-23) nor the 10-24 re-pin window rolls back or restores while a piece
has history, unless that window has failed. If a restore happens anyway, this log names the pieces to redo.
4. All five closed and reviewed by 2026-10-26 18:00Z. Jason reads the five trails at his gate on 10-26 or
10-27. A pass starts merge plan step 1 under decision 146, and step 4 stays his.
5. Fallback: if fewer than five pieces are open by 10-23, the gate moves to 10-28. No agent opens a piece in
Jason's place.
When Jason sends a piece, I tell Mos its message id.
Correction (2026-10-21T01:06Z): the heading above says 01:14Z. The clock read 2026-10-21T01:05:56Z when I wrote that entry, so 01:06Z is the right time.
## 2026-10-21T01:20Z Jason's pre-S7 checks (mos-1154)
Between 01:11:06Z and 01:13:03Z Jason sent pm five requests with `mosaic talk pm`: ed9e1e14, 51c19692,
94f5d62c, 0645a3b7, f75449c0 (bus seq 1583 to 1591). pm run rc9e993da24cf answered each in 4 to 16 s. They were
two jokes, the sandwich, the purpose of Mosaic Stack and pm's mission. pm made no task and raised no decision
for any of them, and said so each time. Mos's reading, which I take: none is a piece, because a piece has to
become work that gets closed and reviewed. They are recorded as Jason's pre-S7 checks. The five pieces are
still open, latest-by 10-23 23:59Z. Activation at send: copy 4429a104f, as in the 01:06Z entry (C1). Source: Mos
(mos-1154) read the bus. I didn't read the message bodies.
pm also told Jason that its state snapshot lists only the first 50 of 198 open tasks and that it pages the rest
with list_tasks. That is the row 109 (#1603) design: broker.mjs `state` returns `cap: TASK_PAGE` (50) with
`total`, and list_tasks pages up to TASK_PAGE_MAX (200).
@@ -0,0 +1,27 @@
{
"at": "2026-10-20T23:19:35.714Z",
"comments": [
{
"task": 413,
"status": 201,
"id": 411,
"readBack": 200,
"found": true
},
{
"task": 424,
"status": 201,
"id": 412,
"readBack": 200,
"found": true
},
{
"task": 414,
"status": 201,
"id": 413,
"readBack": 200,
"found": true
}
],
"problems": []
}
@@ -0,0 +1,51 @@
// Sage, 2026-10-20: row 192 (Q175 phase 1, #1690) landed. One comment on P19 412, per mos-0882 (every
// landing or decision gets a Vikunja comment). Sage's own key (bot user 13); the sync key reads back. Comments only:
// no task edits, labels, assignees or bucket moves. Checks the task's title before posting. Prints no secret.
// Usage: node comment-412-r192-landed.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
import { readFileSync, writeFileSync, existsSync } from "node:fs";
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
const PROJECT = 19;
const PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
const ps = (xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(PSIG)}</p>`;
const POSTS = [
{ id: 412, title: /^Console SSO: zero-trust OIDC verification/, body: [
"Row 192 (Q175 phase 1, Gitea #1690) landed and pushed. Landing commit 4429a104f (Launchpad's launchpad/row192 53bb59462..37582e820, manifest round 2, 18 paths; Webby approved). Patch-id 4a486902614d matches the reviewed diff. Queue revs 2429-2430 (landing note and done) are in 305857c8f, range 32b54ac3b..305857c8f on refactor. #1690 is closed.",
"Sage gate on 088087ac9: package suites 2758 pass 0 fail, scripts/test-*.sh 429 pass 0 fail, mutants 22 of 23 killed (#23, the second origin check, ruled equivalent). The Console now verifies SSO itself under --sso as an OIDC relying party on oauth4webapi 3.8.8, pinned exactly. mosaic-config.mjs load is now async, so in-flight rows that call it rebase and await it (row 190 round 3 is doing this).",
"Not live. The live stack still runs copies/490e2771d3d0. The new dependency needs npm ci in a new stack copy, so the live re-pin is a window with Mos (mos-1099 item 3); Sage sends Mos the plan. Row 200 (phase 2) follows.",
] },
];
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
async function api(method, path, body, auth) {
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
return { status: r.status, json };
}
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
const rec = { at: new Date().toISOString(), comments: [], problems: [] };
const save = () => {
const out = JSON.stringify(rec, null, 1) + "\n";
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
writeFileSync(OUT, out);
};
for (const P of POSTS) {
const t = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
if (t.status !== 200 || t.json?.project_id !== PROJECT || !P.title.test(t.json?.title ?? "")) throw new Error(`task ${P.id}: ${t.status}, title check failed; nothing posted`);
}
writeFileSync(OUT, "", { flag: "wx" });
for (const P of POSTS) {
const c = await api("PUT", `/api/v1/tasks/${P.id}/comments`, { comment: ps(P.body) }, key);
const e = { task: P.id, status: c.status, id: c.json?.id ?? null };
rec.comments.push(e);
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) { rec.problems.push(`comment ${P.id}: ${c.status} ${c.json?.message ?? ""}`); save(); continue; }
const back = await api("GET", `/api/v2/tasks/${P.id}/comments?per_page=50`, undefined, sync);
const found = list(back.json).find((x) => x.id === c.json.id);
Object.assign(e, { readBack: back.status, found: Boolean(found) });
if (back.status !== 200 || !found) rec.problems.push(`comment ${P.id} read-back: ${back.status}`);
save();
}
save();
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
if (rec.problems.length) process.exit(2);
@@ -0,0 +1,59 @@
// Sage, 2026-10-20: catch-up landing and status comments on P19 413 (row 189), 424 (row 210) and 414 (rows 201
// and 204), per mos-0882. Sage's own key (bot user 13); the sync key reads back. Comments only: no task edits.
// Checks each task's title before posting. Prints no secret.
// Usage: node comment-catchup-2026-10-20.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
import { readFileSync, writeFileSync, existsSync } from "node:fs";
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
const PROJECT = 19;
const PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
const ps = (xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(PSIG)}</p>`;
const POSTS = [
{ id: 413, title: /^stack-release\.sh activate --no-send/, body: [
"Row 189 landed on refactor as ace27dc5e (Gitea #1687): stack-release.sh activate --no-send drops the Console's --send and keeps the pointer and the unit in step. Dewey approved (comment 29485), and Sage's gate passed.",
"The stack activation log shows the same-copy activate on 490e2771d3d0 after the landing: activation 20261020T192956Z-490e2771d3d0, result ok at 19:30:00Z, with backup 20261020T192956Z-490e2771d3d0. Since then the live Console runs without --send. The re-pin to 4429a104f at 22:02Z (row 192) carried the pointer's flags, no --send and no --sso, as its W3 plan check required.",
] },
{ id: 424, title: /^Credential store: per-agent host access/, body: [
"Status, 2026-10-20 23:20Z. Row 210 (Gizmoduck) builds this task. Its design r3 is approved, and the machine-grant design final r6 f9039682e is approved for code once row 210 lands (mos-1141, mos-1143). Every grant name sits under grant.* behind one guard (mos-1144). No agent creates a grant: the chief requests, Jason approves, and protected code creates it (Q188).",
"Order (mos-1135, mos-1137): row 210 lands only after row 199, the per-agent credential store. Row 199's gate is green on c6868ba41. Megavolt fixed five surviving mutants and is rerunning all 28 now on 7c3cdd940, before the Duckworth review. Rows 190, 199 and 210 go live together in the one 10-24 window with the 3i-3j migration (mos-1145 line 9), backup first, under Q173.",
] },
{ id: 414, title: /^MVP platform: the Given list for Cases 1 and 2/, body: [
"RBAC rows landed (Given list, RBAC enforcement). Row 201, FED R1: access.json loader and validator with covers, afad4993f (Gitea #1699), Gizmoduck, Duckworth approved (comment 29581). Gate: business 173/0, scripts/test-*.sh 422/0, mutants 44 of 44 killed.",
"Row 204, 193a: the access level check widened to viewer, d0bbc5c1c (Gitea #1702), Morgana, Duckworth approved (comment 29595). LEVELS gains collaborator and viewer, covers() takes a required least level, and an unknown grant level covers nothing. Gate: packages and scripts 2789/0, mutants 7 of 7 killed.",
"Neither changes the live stack. Rows 205-208 (193b1 to 193d) follow, and 206 adds a schema step, so it goes live only in a --migrate window.",
] },
];
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
async function api(method, path, body, auth) {
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
return { status: r.status, json };
}
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
const rec = { at: new Date().toISOString(), comments: [], problems: [] };
const save = () => {
const out = JSON.stringify(rec, null, 1) + "\n";
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
writeFileSync(OUT, out);
};
for (const P of POSTS) {
const t = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
if (t.status !== 200 || t.json?.project_id !== PROJECT || !P.title.test(t.json?.title ?? "")) throw new Error(`task ${P.id}: ${t.status}, title check failed; nothing posted`);
}
writeFileSync(OUT, "", { flag: "wx" });
for (const P of POSTS) {
const c = await api("PUT", `/api/v1/tasks/${P.id}/comments`, { comment: ps(P.body) }, key);
const e = { task: P.id, status: c.status, id: c.json?.id ?? null };
rec.comments.push(e);
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) { rec.problems.push(`comment ${P.id}: ${c.status} ${c.json?.message ?? ""}`); save(); continue; }
const back = await api("GET", `/api/v2/tasks/${P.id}/comments?per_page=50`, undefined, sync);
const found = list(back.json).find((x) => x.id === c.json.id);
Object.assign(e, { readBack: back.status, found: Boolean(found) });
if (back.status !== 200 || !found) rec.problems.push(`comment ${P.id} read-back: ${back.status}`);
save();
}
save();
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
if (rec.problems.length) process.exit(2);
@@ -0,0 +1,60 @@
// Sage, 2026-10-21: re-plan comments on P19 405-408 and 411 (due 10-22). Sage's own key (bot user 13); the sync key reads back. Comments only: no task edits.
// Checks each task's title before posting. Prints no secret.
// Usage: node comment-p19-405-411-replan.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
import { readFileSync, writeFileSync, existsSync } from "node:fs";
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
const PROJECT = 19;
const PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
const ps = (xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(PSIG)}</p>`;
const POSTS = [
{ id: 405, title: /^stack-release --plan: no test plans a missing store/, body: [
"Re-plan (Sage, 2026-10-21, due 10-22). Tasks 405, 406, 407, 408 and 411 become one queue row, a stack-release follow-up row. All five touch scripts/stack-release.sh, scripts/webui-service.sh or docs/TOOLS.md, and rows 232 (Darkwing) and 238 (Launchpad) are changing stack-release.sh now. So I brief the row after 232 and 238 land, from their landing, which avoids base drift (the row 68 rule). It lands after the 10-24 window, because that window holds the four service scripts at NEW (P5). 411's hold was the Part B window, and Part B is superseded (Q175, mos-0970), so that hold is gone. Each item keeps the test and mutant its task names. I'll name the owner when I brief it, either the seat that frees up first of Darkwing and Launchpad, or me.",
] },
{ id: 406, title: /^stack-release --plan: backup line printed/, body: [
"Re-plan (Sage, 2026-10-21, due 10-22). Tasks 405, 406, 407, 408 and 411 become one queue row, a stack-release follow-up row. All five touch scripts/stack-release.sh, scripts/webui-service.sh or docs/TOOLS.md, and rows 232 (Darkwing) and 238 (Launchpad) are changing stack-release.sh now. So I brief the row after 232 and 238 land, from their landing, which avoids base drift (the row 68 rule). It lands after the 10-24 window, because that window holds the four service scripts at NEW (P5). 411's hold was the Part B window, and Part B is superseded (Q175, mos-0970), so that hold is gone. Each item keeps the test and mutant its task names. I'll name the owner when I brief it, either the seat that frees up first of Darkwing and Launchpad, or me.",
] },
{ id: 407, title: /^stack-release\.sh: the proof_for_target comment/, body: [
"Re-plan (Sage, 2026-10-21, due 10-22). Tasks 405, 406, 407, 408 and 411 become one queue row, a stack-release follow-up row. All five touch scripts/stack-release.sh, scripts/webui-service.sh or docs/TOOLS.md, and rows 232 (Darkwing) and 238 (Launchpad) are changing stack-release.sh now. So I brief the row after 232 and 238 land, from their landing, which avoids base drift (the row 68 rule). It lands after the 10-24 window, because that window holds the four service scripts at NEW (P5). 411's hold was the Part B window, and Part B is superseded (Q175, mos-0970), so that hold is gone. Each item keeps the test and mutant its task names. I'll name the owner when I brief it, either the seat that frees up first of Darkwing and Launchpad, or me.",
] },
{ id: 408, title: /^docs\/TOOLS\.md: the row 178 paragraph/, body: [
"Re-plan (Sage, 2026-10-21, due 10-22). Tasks 405, 406, 407, 408 and 411 become one queue row, a stack-release follow-up row. All five touch scripts/stack-release.sh, scripts/webui-service.sh or docs/TOOLS.md, and rows 232 (Darkwing) and 238 (Launchpad) are changing stack-release.sh now. So I brief the row after 232 and 238 land, from their landing, which avoids base drift (the row 68 rule). It lands after the 10-24 window, because that window holds the four service scripts at NEW (P5). 411's hold was the Part B window, and Part B is superseded (Q175, mos-0970), so that hold is gone. Each item keeps the test and mutant its task names. I'll name the owner when I brief it, either the seat that frees up first of Darkwing and Launchpad, or me.",
] },
{ id: 411, title: /^stack-release\.sh and webui-service\.sh: the proof file repository check/, body: [
"Re-plan (Sage, 2026-10-21, due 10-22). Tasks 405, 406, 407, 408 and 411 become one queue row, a stack-release follow-up row. All five touch scripts/stack-release.sh, scripts/webui-service.sh or docs/TOOLS.md, and rows 232 (Darkwing) and 238 (Launchpad) are changing stack-release.sh now. So I brief the row after 232 and 238 land, from their landing, which avoids base drift (the row 68 rule). It lands after the 10-24 window, because that window holds the four service scripts at NEW (P5). 411's hold was the Part B window, and Part B is superseded (Q175, mos-0970), so that hold is gone. Each item keeps the test and mutant its task names. I'll name the owner when I brief it, either the seat that frees up first of Darkwing and Launchpad, or me.",
] },
];
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
async function api(method, path, body, auth) {
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
return { status: r.status, json };
}
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
const rec = { at: new Date().toISOString(), comments: [], problems: [] };
const save = () => {
const out = JSON.stringify(rec, null, 1) + "\n";
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
writeFileSync(OUT, out);
};
for (const P of POSTS) {
const t = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
if (t.status !== 200 || t.json?.project_id !== PROJECT || !P.title.test(t.json?.title ?? "")) throw new Error(`task ${P.id}: ${t.status}, title check failed; nothing posted`);
}
writeFileSync(OUT, "", { flag: "wx" });
for (const P of POSTS) {
const c = await api("PUT", `/api/v1/tasks/${P.id}/comments`, { comment: ps(P.body) }, key);
const e = { task: P.id, status: c.status, id: c.json?.id ?? null };
rec.comments.push(e);
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) { rec.problems.push(`comment ${P.id}: ${c.status} ${c.json?.message ?? ""}`); save(); continue; }
const back = await api("GET", `/api/v2/tasks/${P.id}/comments?per_page=50`, undefined, sync);
const found = list(back.json).find((x) => x.id === c.json.id);
Object.assign(e, { readBack: back.status, found: Boolean(found) });
if (back.status !== 200 || !found) rec.problems.push(`comment ${P.id} read-back: ${back.status}`);
save();
}
save();
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
if (rec.problems.length) process.exit(2);
@@ -0,0 +1,53 @@
// Sage, 2026-10-21: landing comments on P19 412 (row 200) and 424 (row 199), per mos-0882. Sage's own key (bot user 13); the sync key reads back. Comments only: no task edits.
// Checks each task's title before posting. Prints no secret.
// Usage: node comment-r200-r199-landed.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
import { readFileSync, writeFileSync, existsSync } from "node:fs";
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
const PROJECT = 19;
const PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
const ps = (xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(PSIG)}</p>`;
const POSTS = [
{ id: 412, title: /^Console SSO: zero-trust OIDC verification/, body: [
"Row 200 landed on refactor as 9e1085816 (Gitea #1698, closed): stack-release.sh activate --sso, the phase 2 prerequisite. Launchpad built it and Webby approved r3. Sage's run killed 13 of 14 mutants (M12 is the equivalent ruled in round 2). Gate: 15 package suites and 10 scripts/test-*.sh suites, all green, at 00:07:57Z.",
"Follow-up N1 is row 238 (Gitea #1744, Launchpad, Webby). The sso-config hint will carry --instance NAME for a named instance. Nothing is live until a window activates a copy that includes 9e1085816.",
] },
{ id: 424, title: /^Credential store: per-agent host access/, body: [
"Row 199 landed on refactor as cb5fdb064 (Gitea #1697, closed). It adds the per-agent credential store, the agent's own git identity and pushes to a remote. Megavolt built it and Duckworth approved r1 (comment 29779). Gate on 9e1085816 plus the patch: packages 2735/0, scripts 429/0.",
"Row 210 (Gizmoduck) now rebases onto cb5fdb064 on a new branch (mos-1135, mos-1137). Rows 234 and 235 follow it. Rows 190, 199 and 210 go live together in the 10-24 window with the 3i-3j migration, backup first, under Q173 (mos-1145).",
] },
];
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
async function api(method, path, body, auth) {
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
return { status: r.status, json };
}
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
const rec = { at: new Date().toISOString(), comments: [], problems: [] };
const save = () => {
const out = JSON.stringify(rec, null, 1) + "\n";
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
writeFileSync(OUT, out);
};
for (const P of POSTS) {
const t = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
if (t.status !== 200 || t.json?.project_id !== PROJECT || !P.title.test(t.json?.title ?? "")) throw new Error(`task ${P.id}: ${t.status}, title check failed; nothing posted`);
}
writeFileSync(OUT, "", { flag: "wx" });
for (const P of POSTS) {
const c = await api("PUT", `/api/v1/tasks/${P.id}/comments`, { comment: ps(P.body) }, key);
const e = { task: P.id, status: c.status, id: c.json?.id ?? null };
rec.comments.push(e);
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) { rec.problems.push(`comment ${P.id}: ${c.status} ${c.json?.message ?? ""}`); save(); continue; }
const back = await api("GET", `/api/v2/tasks/${P.id}/comments?per_page=50`, undefined, sync);
const found = list(back.json).find((x) => x.id === c.json.id);
Object.assign(e, { readBack: back.status, found: Boolean(found) });
if (back.status !== 200 || !found) rec.problems.push(`comment ${P.id} read-back: ${back.status}`);
save();
}
save();
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
if (rec.problems.length) process.exit(2);
@@ -0,0 +1,66 @@
// Sage, 2026-10-20: W11 of the re-pin window 490e2771d to 4429a104f (STATE.md in commit 99a406ed3) on P19 386, 412
// and 393, and the row 190 landing (00d4cc1f6, #1688) on P19 414, per mos-0882. Sage's own key (bot user 13); the
// sync key reads back. Comments only: no task edits. Checks each task's title before posting. Prints no secret.
// Usage: node comment-w11-r190-2026-10-20.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
import { readFileSync, writeFileSync, existsSync } from "node:fs";
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
const PROJECT = 19;
const PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
const ps = (xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(PSIG)}</p>`;
const STATE = "agents/sage/work/window-repin-4429a104f/STATE.md in commit 99a406ed3 (pushed), sha256 a4d0a7b12f431f15";
const POSTS = [
{ id: 386, title: /^Q173 test system: tester writes/, body: [
"Q173 re-pin window, 490e2771d to 4429a104f (row 192), done. STATE report: " + STATE + ".",
"Activation 20261020T220206Z-4429a104f6d6 with --pm, exit 0, every gate check including pm_launched. Store stays on 3i, migration none. Backup ~/.mosaic-dev/backups/stack/20261020T220206Z-4429a104f6d6 (SHA256SUMS b61d6484), rollback set beside it; rollback would go to 490e2771d. Business file unchanged (sha256 1e49bdf0). Console flags unchanged: no --send, no --sso, --tester-port 7333.",
"W0 to W10 passed, nothing rolled back. W10 stopped once on two browser checks; both were checks written before row 192 (the hidden #sign-out link, and a chat check that read only the first project list). Mos passed them (mos-1135) and the corrected checks are in the plan. Matrix line g is 6575 bytes, not 6454, because row 192 grows index.html by 121 bytes; Mos passed it (mos-1137).",
"S1 to S2 store deltas include task_snapshots +7 and task_comment_cursors +5. The writer is the bus's Vikunja poll (packages/tasks/src/adapter.mjs tick, sync.mjs recordTask), source 'poll', not the window. Nobody acted as Jason: no --by jason, no pi, no Save, no mosaic name, bus.tester untouched.",
] },
{ id: 412, title: /^Console SSO: zero-trust OIDC verification/, body: [
"Row 192 (Q175 phase 1) is live in copy 4429a104f since activation 20261020T220206Z (Q173 window with Mos). The sign-in code is in the copy but dormant: the Console runs without --sso, and the proof pair stays unset. The new oauth4webapi 3.8.8 dependency came in through npm ci in the new copy and imports cleanly from it.",
"STATE report: " + STATE + ". The webui journal since activation shows one 'Proxy: proxy proof off' line and no 'Sign-in:' line, as expected without --sso.",
] },
{ id: 393, title: /^dragon-lin: seven legacy user units in failed state/, body: [
"Current failure times read during the Q173 re-pin window W11 (systemctl --user show, CDT with UTC): mosaic-standup-pm.service failed 2026-10-19 19:00:28 CDT (10-20 00:00:28Z), exit 1. mosaic-install-drift.service failed 2026-10-20 00:12:54 CDT (05:12:54Z), exit 9. [email protected] failed 2026-10-20 13:16:12 CDT (18:16:12Z), exit 4.",
"The marker's journal at 18:16:12Z says its tmux send to jarvis failed ('tmux target not found', socket mosaic-fleet), so that page never arrived; the record is in ~/.local/state/mosaic-unit-failures/20261020.jsonl. mosaic-reconcile.service itself last ran OK at 17:16:27 CDT (22:16:27Z), so the marker is stale until reset.",
"All three predate the re-pin at 22:02Z and come from the legacy ~/.mosaic and jarvis-brain setup. The window changed none of them. Disabling or resetting any still needs Jason or ops-01's charter. Details: " + STATE + ", section 'Failed user units'.",
] },
{ id: 414, title: /^MVP platform: the Given list for Cases 1 and 2/, body: [
"Row 190 (several Mosaic Stack instances on one host, MVP T1.1, Gitea #1688) landed and pushed: 00d4cc1f6 on refactor (99a406ed3..00d4cc1f6). Squash of Darkwing's darkwing/row190-r3 176a6bf4b..df65a3d4c, 25 paths, manifest cc0ea9e8, Filbert approved round 3; patch-id matches the branch diff.",
"Sage gate on d0bbc5c1c with the patch: 15 package suites 2664 pass 0 fail, 10 scripts/test-*.sh suites 429 pass 0 fail. Mutants 20 of 20 killed. Not live: the stack runs copy 4429a104f until a re-pin. Row 230 (C2-13a) now carries row 190's @CONFIG_ENV@ and @SESSION_ENV@ tokens (gosalyn-0058 item 4).",
] },
];
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
async function api(method, path, body, auth) {
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
return { status: r.status, json };
}
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
const rec = { at: new Date().toISOString(), comments: [], problems: [] };
const save = () => {
const out = JSON.stringify(rec, null, 1) + "\n";
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
writeFileSync(OUT, out);
};
for (const P of POSTS) {
const t = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
if (t.status !== 200 || t.json?.project_id !== PROJECT || !P.title.test(t.json?.title ?? "")) throw new Error(`task ${P.id}: ${t.status}, title check failed; nothing posted`);
}
writeFileSync(OUT, "", { flag: "wx" });
for (const P of POSTS) {
const c = await api("PUT", `/api/v1/tasks/${P.id}/comments`, { comment: ps(P.body) }, key);
const e = { task: P.id, status: c.status, id: c.json?.id ?? null };
rec.comments.push(e);
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) { rec.problems.push(`comment ${P.id}: ${c.status} ${c.json?.message ?? ""}`); save(); continue; }
const back = await api("GET", `/api/v2/tasks/${P.id}/comments?per_page=50`, undefined, sync);
const found = list(back.json).find((x) => x.id === c.json.id);
Object.assign(e, { readBack: back.status, found: Boolean(found) });
if (back.status !== 200 || !found) rec.problems.push(`comment ${P.id} read-back: ${back.status}`);
save();
}
save();
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
if (rec.problems.length) process.exit(2);
@@ -0,0 +1,66 @@
// Sage, 2026-10-21: files row 237's review follow-ups N1 and N3 (duckworth-0069, #1743 comment 29810) as two P19 tasks.
// Sage's own key (bot user 13); the sync key reads back. No labels, assignees, due dates or bucket moves. Refuses if
// the title exists. Prints no secret.
// Usage: node file-r237-followups.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
import { readFileSync, writeFileSync, existsSync } from "node:fs";
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
const PROJECT = 19;
const SIG = "Created by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
const ps = (sig, xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(sig)}</p>`;
const TASKS = [
{
title: "Bus publish: a retry after publish-timeout can push a second fork ref while the first push still runs (row 237 N1)",
description: ps(SIG, [
"Duckworth's row 237 review N1 (#1743 comment 29810, duckworth-0069). Row 237 makes checkPublish refuse publish-busy while an open ticket holds the same business and decision. publishRefused drops the ticket on publish-timeout, but the bus process's push may still be running, so a retry to another branch can push a second ref to the fork.",
"The fix belongs in packages/bus/src/runtime.mjs, in `remotes`: either keep the ticket until the push process exits, or kill the push on timeout and wait for it. A test should start a slow push, time it out, retry to another branch, and expect one ref on the fork. Not in row 237's scope. Megavolt's BUILD.md residual 2 (agents/megavolt/work/queue-237/BUILD.md). Needs a queue row once 237 lands.",
]),
},
{
title: "Bus land: check whether checkLand has the same open-ticket race that row 237 fixed in checkPublish (row 237 N3)",
description: ps(SIG, [
"Row 237 residual 3 (Megavolt's BUILD.md) and Duckworth's N3 (#1743 comment 29810): checkLand has the same ticket shape as checkPublish had before row 237. Two lands on the same decision may both pass the check before either records. Duckworth didn't check it.",
"First step: read checkLand and its ticket code in packages/bus/src/broker.mjs against row 237's fix (broker.mjs:1038-1043). If the race exists, file a queue row with a race test like publish-socket.test.mjs's; if not, record why and close this task.",
]),
},
];
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
async function api(method, path, body, auth) {
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
return { status: r.status, json };
}
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
async function existing() {
const out = [];
for (let page = 1; page < 20; page++) {
const r = await api("GET", `/api/v2/projects/${PROJECT}/tasks?per_page=50&page=${page}`, undefined, sync);
if (r.status !== 200) throw new Error(`list tasks: ${r.status}`);
out.push(...list(r.json));
if (list(r.json).length < 50) return out;
}
throw new Error("more than 19 pages");
}
const rec = { at: new Date().toISOString(), project: PROJECT, created: null, problems: [] };
const save = () => {
const out = JSON.stringify(rec, null, 1) + "\n";
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
writeFileSync(OUT, out);
};
const have = await existing();
for (const T of TASKS) if (have.some((t) => t.title === T.title)) throw new Error("title exists; refusing");
writeFileSync(OUT, "", { flag: "wx" });
rec.created = [];
for (const T of TASKS) {
const r = await api("PUT", `/api/v1/projects/${PROJECT}/tasks`, { title: T.title, description: T.description }, key);
const c = { status: r.status, id: r.json?.id ?? null };
rec.created.push(c);
if (r.status >= 300 || !Number.isSafeInteger(r.json?.id)) { rec.problems.push(`create: ${r.status} ${r.json?.message ?? ""}`); continue; }
const back = await api("GET", `/api/v2/tasks/${r.json.id}`, undefined, sync);
Object.assign(c, { readBack: back.status, done: back.json?.done });
if (back.status !== 200 || back.json?.title !== T.title || back.json?.done) rec.problems.push(`read-back differs for ${r.json.id}`);
}
save();
console.log(JSON.stringify(rec));
@@ -0,0 +1,41 @@
{
"at": "2026-10-21T01:06:25.071Z",
"comments": [
{
"task": 405,
"status": 201,
"id": 416,
"readBack": 200,
"found": true
},
{
"task": 406,
"status": 201,
"id": 417,
"readBack": 200,
"found": true
},
{
"task": 407,
"status": 201,
"id": 418,
"readBack": 200,
"found": true
},
{
"task": 408,
"status": 201,
"id": 419,
"readBack": 200,
"found": true
},
{
"task": 411,
"status": 201,
"id": 420,
"readBack": 200,
"found": true
}
],
"problems": []
}
@@ -0,0 +1,13 @@
{
"at": "2026-10-20T21:35:49.240Z",
"comments": [
{
"task": 412,
"status": 201,
"id": 395,
"readBack": 200,
"found": true
}
],
"problems": []
}
@@ -0,0 +1,20 @@
{
"at": "2026-10-21T00:28:32.038Z",
"comments": [
{
"task": 412,
"status": 201,
"id": 414,
"readBack": 200,
"found": true
},
{
"task": 424,
"status": 201,
"id": 415,
"readBack": 200,
"found": true
}
],
"problems": []
}
@@ -0,0 +1,19 @@
{
"at": "2026-10-21T00:38:47.170Z",
"project": 19,
"created": [
{
"status": 201,
"id": 440,
"readBack": 200,
"done": false
},
{
"status": 201,
"id": 441,
"readBack": 200,
"done": false
}
],
"problems": []
}
@@ -0,0 +1,16 @@
// Sage, 2026-10-21: read-only. Prints id, done, title, the last update and the description as plain text for the
// given P19 tasks, with the sync key. Writes nothing. Prints no secret.
// Usage: node read-tasks.mjs https://tasks.woltje.com SYNC_TOKEN_FILE ID...
import { readFileSync } from "node:fs";
const [ORIGIN, SYNC, ...IDS] = process.argv.slice(2);
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
const sync = readFileSync(SYNC, "utf8").trim();
const text = (h) => (h ?? "").replace(/<\/p>|<br\s*\/?>|<\/li>/g, "\n").replace(/<[^>]+>/g, "").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").trim();
for (const id of IDS) {
const r = await fetch(`${ORIGIN}/api/v2/tasks/${id}`, { headers: { Authorization: `Bearer ${sync}` }, redirect: "error" });
const j = await r.json().catch(() => null);
if (r.status !== 200 || j?.project_id !== 19) { console.log(`== ${id}: status ${r.status}, not P19`); continue; }
const out = `== ${id} done=${j.done} updated=${j.updated}\n${j.title}\n${text(j.description)}\n`;
if (out.includes(sync)) throw new Error("secret in output");
console.log(out);
}
@@ -0,0 +1,34 @@
{
"at": "2026-10-20T22:51:44.218Z",
"comments": [
{
"task": 386,
"status": 201,
"id": 406,
"readBack": 200,
"found": true
},
{
"task": 412,
"status": 201,
"id": 407,
"readBack": 200,
"found": true
},
{
"task": 393,
"status": 201,
"id": 408,
"readBack": 200,
"found": true
},
{
"task": 414,
"status": 201,
"id": 409,
"readBack": 200,
"found": true
}
],
"problems": []
}
@@ -0,0 +1,85 @@
# 10-24 window: re-pin 4429a104f to the row 234 landing, with the 3i-3j migration (draft 0)
Sage, 2026-10-21T01:05Z, draft 0, for mos-1145 under DECISIONS Q173. This is the skeleton. NEW isn't fixed yet,
so Mos doesn't check this draft. Draft 1 names NEW and goes to Mos for the P0 check. The step layout follows
`repin-4429a104f.md`. Steps that match are named, not copied.
## Scope (mos-1145, mos-1134)
- One window carries the code of rows 190, 199 and 210, plus row 234 (row a) with its schema step 3i to 3j
through `activate --migrate`, agent-run under Q173, backup first.
- Row a lands before the .53 and .54 installs, by 10-25 (mos-1134). If row 234 hasn't landed by 10-24 12:00Z,
I ask Mos whether to run the window without it (no migration) or to move it.
- Jason has no step. Nobody acts as Jason. No pi (P19 383). No Save, no `mosaic name`. Nothing is pruned.
- `--sso` stays off. Row 200's `activate --sso` is in NEW as a tool. Turning it on changes who reaches the Console
from outside, so that's Jason's (Q173 exclusion). It has its own window, the Q175 phase 2 window on 10-23
(mos-1148). That window runs first, so this window's OLD may be the phase 2 copy, and P1 reads it from
`status`.
## Names
- OLD, live: `4429a104f6d65ccf165f6c83b5d1dce3147dfde0`, schema 3i, pointer `pm: true` (record 99a406ed3).
P1 re-reads the activation id and the Console flags from `status`. I don't copy them from the last window.
- NEW: open. It's the row 234 landing, or a later code landing if Mos rules. Candidates in landing order:
194, 234 (210, 237, 232, 241 and 242 have landed).
- REPO, `P`, `WIN=agents/sage/work/window-repin-<NEW12>/`, `SNAP`: as in `repin-4429a104f.md`, with the
sha checks at P0 and before S1, S2 and any D3.
## What NEW carries over OLD (so far, refactor 04ce8dfd5)
`git log --first-parent 4429a104f..HEAD`, path-limited to packages, scripts, roles, contracts, adapters, skills
and the package files:
| Row | Commit | Live effect to check |
|---|---|---|
| 201 | afad4993f | business: access.json loader. Check: the live business has no access.json, and load stays LOAD_OK (P1). |
| 204 | d0bbc5c1c | business: 193a viewer level. Same check. |
| 190 | 00d4cc1f6 | Several instances per host. Touches `stack-release.sh`, `webui-service.sh`, `bus-service.sh`, `stack-copy.sh`. The default instance must render the same units: P2 diffs the rendered units OLD vs NEW for `mosaic-stack`, and any difference stops the window until it's explained. |
| 200 | 9e1085816 | Tool: `activate --sso`. Off here. `--plan` must show no `--sso` line. |
| 199 | cb5fdb064 | Credential store per agent. Check: bus start with no store entries for the live agents changes nothing, and the chief's and pm's launches still pass. The P3 rehearsal covers start. |
| N3 | 390ea5261 | README only. |
| 230 | c8bc5cf4c | Case 2 scripts, tools only. Not run live. |
| 210 | f5753c0e8 | Host access. The live business has no `hosts`, so nothing is granted. Bus start runs the host-run sweep (M34d). P3 must show it removes nothing. |
| 237 | d7d943199 | Publish busy check (199 N1). |
| 232 | e65a3ab4d | Installer I1: `stack-release.sh --units external` and `stack-copy.sh copy --bundle`, `wrapper --create`. Both are opt-in, and this window uses neither. The default units path must render the same units, so the P2 unit diff covers it with row 190. P0 checks that no window command line carries `--units external`. |
| 241 | 5d4a61776 | Test only (bus land.test.mjs). No live effect. |
| 242 | 5fb736b92 | Bus publish hold: after publish-timeout the bus holds the decision until its push ends, then records a late push. The live business has no publish in flight at W1, and P3 shows none open. Its follow-up fc1cb856b is comments only (git-socket.mjs). |
| 234 | open | Machine grant row a and the 3i to 3j schema step. `--plan` must say `migration: 3i -> 3j`. |
| PRD 1.7+1.8 | 0adf8ddfe | `docs/prd/mosaic-stack.md` and `packages/tasks/requirements.json` move to version 1.8 (mos-1154, mos-1156, DECISIONS Q192). The live pm cites 1.6 until this window. Check after: the pm's requirements read answers version 1.8. |
Service script drift OLD to HEAD is 415+/91- over the four scripts, mostly row 190. P5 holds them at NEW.
## Backups and the migration
- W5: the stopped store backup that every activate takes (row 185), with SHA256SUMS, SCHEMA 3i and STAMP,
plus the rollback set. Before `--migrate` writes, I check that SCHEMA reads 3i and the backup SHA256SUMS hash.
- W0b: the business file copy, as in the last window.
- Rollback after a migration isn't the plain rollback. OLD is 3i and the store is 3j, so going back needs
`--restore <W5 activation>`, and that loses the events written after W5. The row 181 restore guard counts
11 tables, and the untimed ones need the D3 snapshot. Mos decides any restore past W5, as before.
- Grant look-alikes (row 234 A22b, gizmoduck-0107). `3i-3j.sql` adds `events_grant_actor` but doesn't check
rows already in the store, so after the migration only the actor filter in `GRANT_EVIDENCE` (actor_role NULL,
actor_run 'grant') ignores an old session-written row. P3, and again just before `--migrate`: on the
backup copy, count the `action.allowed` events whose body.action or body.operation starts with `grant.`, or
whose body has `hostLineLeft`. Expect 0, since no grant code has run live. Any nonzero count stops the window
until I've read those rows.
- P3 is the rehearsal with `--migrate` on a copy of the live backup. It must show the 3j step and the row
counts before and after.
## Row 42 (S7) interaction
Row 42's pieces may be in flight at window start (sage-1622, mos-1148 C1 and C2). W1 adds: list open pm tasks, and either pm
finishes or parks them before W5, or I record which piece crosses the window. No restore while a piece has
history unless the window fails.
## Open before draft 1
- Q1: NEW, once 234 lands. Do 194 and 237 go in if they land first? I recommend yes for 237 (small, bus
only) and asking Mos about 194, since 194 changes the lifecycle verbs and roles/chief-of-staff.*.
- Q2, settled by mos-1148: phase 2 is its own window on 10-23. Draft 1 takes OLD and the Console flags,
`--sso` included, from `status` after that window.
- Q3: who operates. If Darkwing is free, Darkwing operates and I check. Otherwise I operate as in the last
window.
- The P-step evidence list for rows 199 and 210 at bus start. Both have landed (cb5fdb064, f5753c0e8), so
it's due in draft 1. I take each log line and exit code from a P3 rehearsal run, not from the code.
- Rows 241 and 242 (bus): P3 shows no publish or land decision open before W5.
@@ -0,0 +1,125 @@
# Re-pin 490e2771d to 4429a104f (row 192, Q175 phase 1), with npm ci in the new copy
Sage, 2026-10-20, draft 1, for mos-1099 item 3 under DECISIONS Q173. Mos checks this file at its commit before
P0. Any change to this file or to NEW after the check means Mos checks again (mos-0849/0850).
This is a smaller window than `repin-rows-177-185.md` (draft 2, which Mos checked in mos-0929). Where a step is
the same, this file names that file's step and says what differs. The 7332 matrix, the snapshot, the W9 list and
the rollback compare are reused unchanged.
## Who, and what nobody does
- Sage operates, as the tester. Darkwing holds the gate lane for row 190 round 3 tonight, so I take the
operator seat. Mos verifies each step's output before the next one and runs `rehearse` (P3), which reads the
live config and a live backup (`docs/TOOLS.md` `rehearse`: Jason or Mos runs it).
- Jason has no step. Q173 covers a re-pin of this test system with a backup first and a STATE report after.
Nothing is pruned.
- Nobody acts as Jason: no `--by jason`, no human shell or tmux pane, every bus read and write carries
`--tester`, and no request carries `jason.woltje`. `bus.tester` stays as it is; if it isn't `true`, stop at P1.
- No pi (mos-0865 D2, P19 #383). No Save in the human Console, no `mosaic name`.
- Outside reach doesn't change. The Console keeps its current flags. `--sso` stays off: row 192's sign-in code is
dormant without the flag, and `cli.mjs:57` refuses `--sso` with the preview flags the pointer carries anyway.
The proof pair stays unset. The tunnel still forwards 7332 only (P4 and W10 check it).
## Names
- OLD, live: `490e2771d3d057ab76991e2c7859cd251857e64c`, activation `20261020T192956Z-490e2771d3d0`, pointer
`pm: true`, schema 3i, Console `--port 7332 --public-host mosaic.woltje.com --auth-header X-authentik-username
--allow-user jason.woltje --tester-port 7333` (no `--send`, read from `stack-release.sh status` between 21:36Z and 21:39Z).
- NEW: `4429a104f6d65ccf165f6c83b5d1dce3147dfde0`, row 192's landing, 103 commits past OLD (`git rev-list --count 490e2771d..4429a104f`).
The 106 that `status` showed is OLD to HEAD 305857c8f, so it also counts the three record commits below.
The commits after it (5b8def2f1 plans, 176a6bf4b sessions, 305857c8f queue) are records and don't move NEW. If
another code row lands before W0 (row 190 is the likely one), NEW stays here unless Mos rules otherwise.
- REPO: the canonical checkout. `activate` runs REPO's `webui-service.sh`, so the four service scripts are held
at NEW for the window (P5).
- `P=/mnt/storage/scratch/sage-pin-de352dfaf` (`business-check.mjs`, `console-check.sh`, checked against
`agents/sage/work/window-plan/repin-de352dfaf/SCRIPTS.sha256`).
- `WIN=agents/sage/work/window-repin-4429a104f/`, where every transcript goes.
- `SNAP=$WIN/snapshot.mjs`, a copy of `agents/darkwing/work/window-rows-177-185/snapshot.mjs`, sha256
`639fd28a2674aaa03ce824e3394b4052f21cdef7d6fa49bae61a9927ed341e67` (mos-0899). I check the sha before S1, S2
and any D3.
## What NEW carries over OLD
The path-limited log over `packages`, `scripts`, `roles`, `contracts`, `adapters`, `skills` and the package files:
| Row | Commit | Live effect |
|---|---|---|
| 186 | 269134d8d | Tool only: the activate plan's backup line says what the run will do. |
| 187 | 9a71e4e80 | Tool: activate sets, carries and drops the proof pair. The pointer has no pair, so nothing changes. |
| 188 | 8fc0ee216 | Tool: proof file checks on a carried pair. No pair, no effect. |
| 163 | 8ecc7a239 | Tool: `rehearse`, which P3 uses, and `bus start --no-launch`. CLI `cli.mjs` and `launcher.mjs` gain the `--no-launch` path; the default start is unchanged. |
| 189 | ace27dc5e | Tool: `activate --no-send`. Live already runs without `--send`; the pointer carries that. |
| 192 | 4429a104f | Console `--sso` (off here), `scripts/mosaic-config.mjs` load becomes async, `webui-service.sh` renders `--sso` only when given, and a new dependency, oauth4webapi 3.8.8. `cli.mjs` imports `sso.mjs` at the top, so the Console won't start from a copy without that module. |
No schema step: `SCHEMA_VERSION` (`packages/bus/src/store.mjs:17`) is 3i at OLD and NEW, and no migration was
added. `--plan` must say `migration: none`. No roles, skills, contracts, adapters or plug-in kinds changed. So: no
harness set, no skill carry, no `pm-on`. One `activate --pm`.
## npm ci in the new copy
mos-1099 asks for npm ci in the new copy. `stack-copy.sh copy` already does it: it extracts the archive to a temp
dir, refuses without `package.json` and `package-lock.json` (:55), runs `npm ci --no-audit --no-fund` there and
writes no copy if it fails (:56). W0 checks the result: `node_modules/oauth4webapi/package.json` in the copy reads
3.8.8, and `node --input-type=module -e "await import('oauth4webapi')"` from the copy exits 0. The live copy is
163M; `/home` has 30G free (94% used), enough for one more.
## Backups
| Before | Backup | Taken by |
|---|---|---|
| W5 | the stopped store, `$DATA/backups/stack/<W5 activation id>/` with SHA256SUMS, DATA_ROOT, SCHEMA 3i, STAMP | `stack-release.sh` (row 185: every activate) |
| W5 | the rollback set (units, wrapper, pointer) | `stack-release.sh` |
| W9 | the business file, `cp -p` to `~/.config/mosaic-dev/businesses/mosaic-stack.json.pre-window-<stamp>` with its sha256, as evidence nothing wrote it | Sage, in W0b |
## Before the window (nothing stopped)
| # | Who | Step |
|---|-----|------|
| P0 | Mos | The `$P` sha check, as in the 177-185 plan P0. |
| P1 | Sage | The business file, read only, the same values as the 177-185 plan P1. I report its sha256 now rather than assume the 06:15Z value. `business-check.mjs` for OLD now and for NEW after W0: LOAD_OK both. |
| W0 | Sage | `cd $REPO && scripts/stack-copy.sh copy $NEW`, in a gap between gate-lane runs, since npm ci is heavy. Then the two npm checks above, and `diff <(git -C $REPO show $NEW:package-lock.json) $COPIES/$NEW/package-lock.json` empty. W0 writes only under `copies/` and changes nothing live, so it moves before P2 and P3, which need the copy. |
| P2 | Sage | `console-check.sh $NEW` against a stub Console of NEW on a scratch port, once without and once with `--tester-port` on a second scratch port. CONSOLE_PASS both. |
| P3 | Mos | `scripts/stack-release.sh rehearse $NEW mosaic-stack --backup 20261020T192956Z-490e2771d3d0 --port <scratch>`. It must pass every gate check but the PM's. It stops no unit and writes no pointer. Transcript in `$WIN/p3/`. Correction (mos-1107): `rehearse` starts NEW's Console with only `--port` and `--business`, not the pointer's flags. So Mos also starts NEW's Console by hand with the pointer's flags on two scratch ports and reads healthz and `/api/config` on both, then stops it (`$WIN/p3/flags-check.txt`, `flags-console.log`). |
| P4 | Sage | The tunnel read, as in the 177-185 plan P4. |
| P5 | Sage | `git -C $REPO diff --quiet $NEW HEAD -- scripts/stack-release.sh scripts/webui-service.sh scripts/bus-service.sh scripts/stack-copy.sh` and an empty `git status --porcelain` on the same four. Row 190 round 3 touches `webui-service.sh`; if it lands before W5, P5 fails and the window waits for Mos's ruling. |
| P6 | Sage | The 7332 matrix (a)-(g) on OLD, as in the 177-185 plan, saved as `$WIN/matrix-old.txt`. |
## The window
| # | Who | Step |
|---|-----|------|
| W0b | Sage | The business file copy. |
| W1 | Sage | Idle check (`mosaic agents --business mosaic-stack --tester`, `mosaic launches list`), the start stamp from `date -u`, then S1 `node $SNAP --store "$DATA/bus/bus.sqlite" --held`. |
| W2 | Sage | `stack-release.sh status mosaic-stack`: pointer on OLD, `pm: true`, 3i, last line ok, unit flags match. |
| W3 | Sage | `stack-release.sh activate $NEW mosaic-stack --pm --plan`. Must show from OLD to NEW, `migration: none`, the backup line, and the Console line with OLD's flags exactly: no `--send`, no `--sso`, no proof pair, `--tester-port 7333`. Any other difference stops here. |
| W4 | Sage | The unit reads, as in the 177-185 plan W4. |
| W5 | Sage, on Mos's go | `stack-release.sh activate $NEW mosaic-stack --pm`. Exit 0, `result ok`, every gate check including `pm_launched`. Record the activation id, the backup path and STAMP, and the backup SHA256SUMS sha256. Then `status`. |
| W9 | Sage, after Mos verifies W5 | The 177-185 plan's W9 list unchanged, the chief turn and the one `assistant` thread turn included (mos-0929 Q3), plus two lines: the webui journal since the W5 stamp (with `UTC`) has one `Proxy: proxy proof off` and no `Sign-in:` line, and `ls -l /proc/<Console pid>/cwd` names the NEW copy. |
| W10 | Sage | The browser check on 127.0.0.1:7333 and the matrix again as `$WIN/matrix-new.txt`, identical to `matrix-old.txt`. Then P4 again. Corrections after the window (mos-1135, mos-1137): the matrix line for a file the pin changes isn't "identical". Line (g) serves `index.html`, which row 192 grows by 121 bytes (6470 to 6591), so (g) is 200 non-json at 6575 bytes, not 6454. The two browser checks come from the 177-185 window, before row 192 and before a second thread project. "Only tester forms" now allows `#sign-out` while it is `hidden` and `/api/config` has no `sso: true`. "Chat lists the W9 thread" now reads every `.chat-projects section.panel` list (chat.js:93 renders one per project), not the first. |
| W11 | Sage | STATE report in `$WIN/STATE.md`, posted on P19 386 and P19 412 and on Gitea #1690. |
## Stop conditions and rollback
The same as the 177-185 plan. Two failures in one window: stop and report (mos-0823).
Rollback: `stack-release.sh rollback mosaic-stack --plan`, then the real run. The previous distinct copy in the
log is 490e2771d, both are 3i, so no `--restore` and no data loss. OLD takes the same Console flags. The R1 and
R1post compares are as in the 177-185 plan. A restore past W5 follows that plan's D3 steps, with Mos deciding.
Keep every copy. Prune nothing.
## Questions for the plan check
- Q1: NEW is row 192 alone, not bundled with row 190. Row 190 (several instances) changes `stack-release.sh`
and `webui-service.sh` and deserves its own window. I recommend running this one first.
- Q2: Sage operates instead of Darkwing, who is on the row 190 gate. OK?
- Q3: W0 before P2 and P3. It writes a copy only. OK?
## What Sage ran for this plan
Read only, 2026-10-20 between 21:36Z and 21:39Z (clock read 21:39:50Z at the commit): `stack-release.sh status mosaic-stack` (pointer, last five log
lines, unit flags match, 106 commits of drift), `git rev-list --count`, `SCHEMA_VERSION` at both ends, the
path-limited log, `git diff --stat` on the service scripts, CLI and config loader, `stack-copy.sh:23-56`,
`webui/src/cli.mjs:15-120`, `docs/TOOLS.md` on `rehearse`, `du -sh` of the live copy and `df -h /home`. Nothing
live was changed.
@@ -0,0 +1,3 @@
chief rbb708846b605 claude-code since 2026-10-20T19:29:59.053Z
pm rc36b17ef3788 claude-code since 2026-10-20T20:20:56.278Z
# exit 0
@@ -0,0 +1,256 @@
{
"settings loaded": {
"pass": true
},
"settings": {
"banner": "Tester Console. Changes are recorded as tester, never as the human.",
"h1": "Settings",
"forms": [
"sign-out",
"conv-form",
"routes-form",
"instance-add-form",
"instance-change-form",
"tester-name-form",
"tester-name-form",
"tester-name-form",
"tester-name-form",
"tester-name-form",
"tester-name-form"
],
"convFormHidden": true,
"panels": [
"Routing",
"Role instances",
"Seat names"
],
"instancesTable": true,
"nameForms": 6,
"routesForm": true,
"routesSubmitEnabled": true,
"forbidden": [],
"tokenLimitsPanel": false
},
"tester banner": {
"pass": true,
"detail": "Tester Console. Changes are recorded as tester, never as the human."
},
"instance, routes and name editors present": {
"pass": true
},
"only tester forms (conv-form is index.html:21, global and hidden)": {
"pass": false,
"detail": [
"sign-out",
"conv-form",
"routes-form",
"instance-add-form",
"instance-change-form",
"tester-name-form",
"tester-name-form",
"tester-name-form",
"tester-name-form",
"tester-name-form",
"tester-name-form"
]
},
"no launch settings, assistant, limit reset or send": {
"pass": true,
"detail": []
},
"no Token limits panel (plan W10 item 2, amended)": {
"pass": true
},
"board": {
"busViewHidden": true,
"banner": true,
"headings": [
"Waiting on you –",
"All sessions –"
],
"unavailable": false,
"length": 179,
"forms": [
"sign-out",
"conv-form"
]
},
"board reads": {
"pass": true,
"detail": {
"busViewHidden": true,
"banner": true,
"headings": [
"Waiting on you –",
"All sessions –"
],
"unavailable": false,
"length": 179,
"forms": [
"sign-out",
"conv-form"
]
}
},
"chat": {
"hash": "#/chat",
"banner": true,
"threads": [
{
"href": "#/chat/threads/d0ba7a66-aa3f-4918-9518-94132bce6b28",
"name": "w9",
"instance": "assistant"
}
],
"conversationOpen": false,
"forms": [
"sign-out",
"conv-form",
"chat-project-form"
]
},
"chat lists the W9 thread": {
"pass": false,
"detail": [
{
"href": "#/chat/threads/d0ba7a66-aa3f-4918-9518-94132bce6b28",
"name": "w9",
"instance": "assistant"
}
]
},
"W9 thread page not opened": {
"pass": true,
"detail": "#/chat"
},
"tester banner on chat": {
"pass": true
},
"requests": [
{
"method": "GET",
"url": "/api/config",
"refused": false
},
{
"method": "GET",
"url": "/api/config",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/messages",
"refused": false
},
{
"method": "GET",
"url": "/api/bus-status",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/messages",
"refused": false
},
{
"method": "GET",
"url": "/api/settings",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/usage",
"refused": false
},
{
"method": "GET",
"url": "/api/settings",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/usage",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/names",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/names",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/instances",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/instances",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/routes",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/routes",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/limits",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/limits",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/inbox",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/agents",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/messages",
"refused": false
},
{
"method": "GET",
"url": "/api/chat/projects",
"refused": false
},
{
"method": "GET",
"url": "/api/chat/threads",
"refused": false
},
{
"method": "GET",
"url": "/api/business",
"refused": false
}
],
"no write through the listener": {
"pass": true,
"detail": []
},
"no request to the W9 thread messages or open": {
"pass": true,
"detail": []
}
}
W10 browser: FAIL
# exit 1
@@ -0,0 +1,200 @@
{
"config": {
"sso": null
},
"forms": [
{
"id": "sign-out",
"hidden": true,
"action": "/auth/logout",
"method": "post"
},
{
"id": "conv-form",
"hidden": true,
"action": null,
"method": null
},
{
"id": "routes-form",
"hidden": false,
"action": null,
"method": null
},
{
"id": "instance-add-form",
"hidden": false,
"action": null,
"method": null
},
{
"id": "instance-change-form",
"hidden": false,
"action": null,
"method": null
},
{
"id": "tester-name-form",
"hidden": false,
"action": null,
"method": null
},
{
"id": "tester-name-form",
"hidden": false,
"action": null,
"method": null
},
{
"id": "tester-name-form",
"hidden": false,
"action": null,
"method": null
},
{
"id": "tester-name-form",
"hidden": false,
"action": null,
"method": null
},
{
"id": "tester-name-form",
"hidden": false,
"action": null,
"method": null
},
{
"id": "tester-name-form",
"hidden": false,
"action": null,
"method": null
}
],
"chat": {
"hash": "#/chat",
"open": false,
"projects": [
{
"project": "window-20261020-0801",
"threads": [
"#/chat/threads/d0ba7a66-aa3f-4918-9518-94132bce6b28"
]
},
{
"project": "window-20261020-2205",
"threads": [
"#/chat/threads/446201de-e8b5-4df3-9356-d0d5422cc899"
]
}
]
},
"requests": [
{
"method": "GET",
"url": "/api/config",
"refused": false
},
{
"method": "GET",
"url": "/api/config",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/messages",
"refused": false
},
{
"method": "GET",
"url": "/api/bus-status",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/messages",
"refused": false
},
{
"method": "GET",
"url": "/api/settings",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/usage",
"refused": false
},
{
"method": "GET",
"url": "/api/settings",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/usage",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/names",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/names",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/instances",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/instances",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/routes",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/routes",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/limits",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/limits",
"refused": false
},
{
"method": "GET",
"url": "/api/config",
"refused": false
},
{
"method": "GET",
"url": "/api/chat/projects",
"refused": false
},
{
"method": "GET",
"url": "/api/chat/threads",
"refused": false
},
{
"method": "GET",
"url": "/api/business",
"refused": false
}
]
}
sign-out hidden true, sso null; W9 thread listed true; thread page open false; writes 0
# exit 0
@@ -0,0 +1,8 @@
a 200 ok=true
b 401 error="sign-in required"
c 401 error="sign-in required"
d 403 error="proxy Host refused"
e 403 error="tester listener required"
f 403 error="proxy header refused"
g 200 non-json 6575 bytes
# exit 0
@@ -0,0 +1,7 @@
# P4 2026-10-20T22:31:29Z
DropInPaths=
ExecStart={ path=/home/jwoltje/.local/bin/chisel ; argv[]=/home/jwoltje/.local/bin/chisel client --fingerprint ce+ISFvMAhuoCRDuc7riIf+hYz87TLHk/C9C/H8QsUg= --keepalive 25s --max-retry-interval 10s http://10.1.1.227:8080 R:0.0.0.0:7332:127.0.0.1:7332 ; ignore_errors=no ; start_time=[n/a] ; stop_time=[n/a] ; pid=0 ; code=(null) ; status=0/0 }
# exit 0
# R: specs (1): R:0.0.0.0:7332:127.0.0.1:7332
P4 PASS: one R: spec R:0.0.0.0:7332:127.0.0.1:7332, DropInPaths empty
# exit 0
@@ -0,0 +1,6 @@
ActiveState=active
DropInPaths=
MainPID=2950923
ExecStart={ path=/home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0/scripts/mosaic ; argv[]=/home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0/scripts/mosaic bus start mosaic-stack --pm ; ignore_errors=no ; start_time=[Tue 2026-10-20 17:02:08 CDT] ; stop_time=[n/a] ; pid=2950923 ; code=(null) ; status=0/0 }
WorkingDirectory=/home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0
# exit 0
@@ -0,0 +1,12 @@
● jarvis-curation-gate.service loaded failed failed Jarvis curation-gate liveness check (wake the agent if staged pulls sit uncurated)
● jarvis-retro-check.service loaded failed failed Jarvis weekly retrospective ARTIFACT check (did Sunday's tick produce a retro?)
● jarvis-retro.service loaded failed failed Jarvis weekly retrospective tick
● mosaic-install-drift.service loaded failed failed Assert the installed Mosaic binaries still match the committed source (drift, not failure)
● mosaic-standup-pm.service loaded failed failed Coordinator standup trigger (evening)
● [email protected] loaded failed failed Escalate failed Mosaic unit jarvis-curation-gate.service to the local agent seat
● [email protected] loaded failed failed Escalate failed Mosaic unit jarvis-retro-check.service to the local agent seat
● [email protected] loaded failed failed Escalate failed Mosaic unit jarvis-retro.service to the local agent seat
● [email protected] loaded failed failed Escalate failed Mosaic unit mosaic-install-drift.service to the local agent seat
● [email protected] loaded failed failed Escalate failed Mosaic unit mosaic-reconcile.service to the local agent seat
● setspark-tmp-guard.service loaded failed failed SetSpark: /tmp and scratch guard
# exit 0
@@ -0,0 +1,5 @@
ActiveState=active
SubState=running
ActiveEnterTimestamp=Tue 2026-10-13 08:40:19 CDT
MainPID=1317
# exit 0
@@ -0,0 +1,6 @@
ActiveState=active
DropInPaths=
MainPID=2950925
ExecStart={ path=/usr/bin/node ; argv[]=/usr/bin/node /home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0/packages/webui/src/cli.mjs serve --checkout /mnt/storage/src/mosaic-stack --port 7332 --public-host mosaic.woltje.com --auth-header X-authentik-username --allow-user jason.woltje --tester-port 7333 ; ignore_errors=no ; start_time=[Tue 2026-10-20 17:02:08 CDT] ; stop_time=[n/a] ; pid=2950925 ; code=(null) ; status=0/0 }
WorkingDirectory=/home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0
# exit 0
@@ -0,0 +1,2 @@
chief r097436c91816 claude-code since 2026-10-20T22:02:08.951Z
# exit 0
@@ -0,0 +1,7 @@
PASS markdown-preview.js is served and equals the copy's
PASS /api/config on loopback: send=false edit=false
PASS project.files through the Console: head=ae0b17c9586e51da7bc1b7e548c6acf913a55665 files=2 spec=true error=none
PASS project.file spec/north-star.md through the Console: commit=ae0b17c9586e51da7bc1b7e548c6acf913a55665 length=99 error=none
PASS loopback save refused: HTTP 403 error=read-only sent=false
CONSOLE_PASS
# exit 0
@@ -0,0 +1,19 @@
[
{
"business": "mosaic-stack",
"instance": "chief",
"run": "r097436c91816",
"launchedBy": "jason",
"harness": "claude-code",
"model": "claude-opus-5-5",
"provider": "anthropic",
"modelKey": "opus",
"pid": 2951160,
"startTime": "63491912",
"runnerPid": 2951161,
"runnerStartTime": "63491912",
"runDir": "/home/jwoltje/.mosaic-dev/launches/mosaic-stack/r097436c91816",
"startedAt": "2026-10-20T22:02:08.941Z",
"live": true
}
]
@@ -0,0 +1,2 @@
r097436c91816 mosaic-stack/chief claude-code claude-opus-5-5 by jason since 2026-10-20T22:02:08.941Z running
# exit 0
@@ -0,0 +1,2 @@
[]
# exit 0
@@ -0,0 +1,2 @@
created project window-20261020-2205 id 77e7b45f-8c34-49a3-b2fa-9cf565177c30
# exit 0
@@ -0,0 +1 @@
1792533965
@@ -0,0 +1,21 @@
pointer: /home/jwoltje/.mosaic-dev/state/stack-active.json
{"copy":"4429a104f6d65ccf165f6c83b5d1dce3147dfde0","business":"mosaic-stack","pm":true,"claude":"/home/jwoltje/.local/bin/claude","console":["--port","7332","--public-host","mosaic.woltje.com","--auth-header","X-authentik-username","--allow-user","jason.woltje","--tester-port","7333"],"schema":"3i","activation":"20261020T220206Z-4429a104f6d6","at":"2026-10-20T22:02:09Z"}
log (last five lines):
{"at":"2026-10-20T07:58:02Z","op":"activate","from":"89af38a83afa22a8a4f0d368a0a4030ad15a0efa","to":"490e2771d3d057ab76991e2c7859cd251857e64c","backup":"20261020T075800Z-490e2771d3d0","activation":"20261020T075800Z-490e2771d3d0","result":"ok"}
{"at":"2026-10-20T19:29:56Z","op":"activate","from":"490e2771d3d057ab76991e2c7859cd251857e64c","to":"490e2771d3d057ab76991e2c7859cd251857e64c","backup":null,"activation":"20261020T192956Z-490e2771d3d0","result":"started"}
{"at":"2026-10-20T19:30:00Z","op":"activate","from":"490e2771d3d057ab76991e2c7859cd251857e64c","to":"490e2771d3d057ab76991e2c7859cd251857e64c","backup":"20261020T192956Z-490e2771d3d0","activation":"20261020T192956Z-490e2771d3d0","result":"ok"}
{"at":"2026-10-20T22:02:06Z","op":"activate","from":"490e2771d3d057ab76991e2c7859cd251857e64c","to":"4429a104f6d65ccf165f6c83b5d1dce3147dfde0","backup":null,"activation":"20261020T220206Z-4429a104f6d6","result":"started"}
{"at":"2026-10-20T22:02:09Z","op":"activate","from":"490e2771d3d057ab76991e2c7859cd251857e64c","to":"4429a104f6d65ccf165f6c83b5d1dce3147dfde0","backup":"20261020T220206Z-4429a104f6d6","activation":"20261020T220206Z-4429a104f6d6","result":"ok"}
check:
bus: /home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0
Console: /home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0
CLI: /home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0
result: pass
bus status:
host: mosaic-stack, pid 2950923, running, since 2026-10-20T22:02:08.133Z, notifier off
socket: present
writer.lock: pid 2950981
boot: organization file loaded at 2026-10-20T22:02:08.125Z
unit flags: match the pointer
drift: 6 commit(s) on refactor since the pointer's copy
# exit 0
@@ -0,0 +1,4 @@
sent c5b43f04 to chief
chief (RESULT, re c5b43f04) c3e236d7:
Got the re-pin of 4429a104f6d65ccf165f6c83b5d1dce3147dfde0 from the tester; I took no action because a tester message authorizes nothing, and I launched and changed nothing.
# exit 0
@@ -0,0 +1,4 @@
sent b4b66ced to window-20261020-2205/w9
assistant (RESULT, re b4b66ced) cfa7ea99:
Assistant (run r497f1c817d4d) got the [mos test] message and is replying in one line as asked, without treating it as Jason's instruction or acting on it.
# exit 0
@@ -0,0 +1,32 @@
[
{
"seq": 570,
"id": "b4b66ced-d088-4b9c-a944-306f164028f9",
"at": "2026-10-20T22:05:58.283Z",
"business": "mosaic-stack",
"from_role": "tester",
"from_run": "tester-cli",
"to_role": "assistant",
"class": "REQUEST",
"in_reply_to": null,
"decision": null,
"corrects": null,
"body": "[mos test] reply with one line, then stop",
"to_name": null
},
{
"seq": 571,
"id": "cfa7ea99-7ccb-4225-80a6-9a903540e337",
"at": "2026-10-20T22:06:04.247Z",
"business": "mosaic-stack",
"from_role": "assistant",
"from_run": "r497f1c817d4d",
"to_role": "tester",
"class": "RESULT",
"in_reply_to": "b4b66ced-d088-4b9c-a944-306f164028f9",
"decision": null,
"corrects": null,
"body": "Assistant (run r497f1c817d4d) got the [mos test] message and is replying in one line as asked, without treating it as Jason's instruction or acting on it.",
"to_name": null
}
]
@@ -0,0 +1,2 @@
created window-20261020-2205/w9 id 446201de-e8b5-4df3-9356-d0d5422cc899 instance assistant
# exit 0
@@ -0,0 +1,3 @@
window-20261020-0801/w9 assistant d0ba7a66
window-20261020-2205/w9 assistant 446201de
# exit 0
@@ -0,0 +1 @@
446201de-e8b5-4df3-9356-d0d5422cc899
@@ -0,0 +1,165 @@
{
"total": {
"input": 62,
"output": 9623,
"cacheWrite": 186650,
"cacheRead": 404132,
"unknownTurns": 1
},
"byRun": [
{
"run": "r8fba7e598156",
"input": 0,
"output": 0,
"cacheWrite": 0,
"cacheRead": 0,
"unknownTurns": 1
},
{
"run": "r4a96e2142e4d",
"input": 2,
"output": 52,
"cacheWrite": 11750,
"cacheRead": 0,
"unknownTurns": 0
},
{
"run": "rc78c9700c8e9",
"input": 14,
"output": 1519,
"cacheWrite": 18187,
"cacheRead": 89559,
"unknownTurns": 0
},
{
"run": "r293f33bcbc63",
"input": 2,
"output": 44,
"cacheWrite": 18595,
"cacheRead": 0,
"unknownTurns": 0
},
{
"run": "r34520a581318",
"input": 6,
"output": 323,
"cacheWrite": 26349,
"cacheRead": 14307,
"unknownTurns": 0
},
{
"run": "r7e99838f9f93",
"input": 2,
"output": 44,
"cacheWrite": 9858,
"cacheRead": 0,
"unknownTurns": 0
},
{
"run": "rc964611b2092",
"input": 4,
"output": 120,
"cacheWrite": 26138,
"cacheRead": 23870,
"unknownTurns": 0
},
{
"run": "rbb708846b605",
"input": 12,
"output": 2089,
"cacheWrite": 17285,
"cacheRead": 73823,
"unknownTurns": 0
},
{
"run": "rc36b17ef3788",
"input": 16,
"output": 5221,
"cacheWrite": 36717,
"cacheRead": 202573,
"unknownTurns": 0
},
{
"run": "r097436c91816",
"input": 2,
"output": 131,
"cacheWrite": 11915,
"cacheRead": 0,
"unknownTurns": 0
},
{
"run": "r497f1c817d4d",
"input": 2,
"output": 80,
"cacheWrite": 9856,
"cacheRead": 0,
"unknownTurns": 0
}
],
"byInstance": [
{
"instance": "chief",
"input": 36,
"output": 4114,
"cacheWrite": 85486,
"cacheRead": 177689,
"unknownTurns": 1
},
{
"instance": "coder",
"input": 2,
"output": 44,
"cacheWrite": 18595,
"cacheRead": 0,
"unknownTurns": 0
},
{
"instance": "assistant",
"input": 4,
"output": 124,
"cacheWrite": 19714,
"cacheRead": 0,
"unknownTurns": 0
},
{
"instance": "pm",
"input": 20,
"output": 5341,
"cacheWrite": 62855,
"cacheRead": 226443,
"unknownTurns": 0
}
],
"byProvider": [
{
"provider": "anthropic",
"input": 62,
"output": 9623,
"cacheWrite": 186650,
"cacheRead": 404132,
"unknownTurns": 1
}
],
"byModel": [
{
"provider": "anthropic",
"model": "claude-opus-5-5",
"input": 62,
"output": 9623,
"cacheWrite": 186650,
"cacheRead": 404132,
"unknownTurns": 1
}
],
"byAccount": [
{
"account": null,
"input": 62,
"output": 9623,
"cacheWrite": 186650,
"cacheRead": 404132,
"unknownTurns": 1
}
]
}
# exit 0
@@ -0,0 +1,2 @@
r097436c91816 mosaic-stack/chief claude-code claude-opus-5-5 by jason since 2026-10-20T22:02:08.941Z running
# exit 0
@@ -0,0 +1,19 @@
[
{
"business": "mosaic-stack",
"instance": "chief",
"run": "r097436c91816",
"launchedBy": "jason",
"harness": "claude-code",
"model": "claude-opus-5-5",
"provider": "anthropic",
"modelKey": "opus",
"pid": 2951160,
"startTime": "63491912",
"runnerPid": 2951161,
"runnerStartTime": "63491912",
"runDir": "/home/jwoltje/.mosaic-dev/launches/mosaic-stack/r097436c91816",
"startedAt": "2026-10-20T22:02:08.941Z",
"live": true
}
]
@@ -0,0 +1,153 @@
{
"schema": "3i",
"tables": {
"decision_events": {
"rows": 21,
"maxSeq": 21,
"sha256": "28dd95056663dd5eb7442a3d815b73a4a3cd311a0e854fd82dc54f7dc3e6ef26"
},
"decisions": {
"rows": 21,
"maxSeq": 21,
"sha256": "fef1058660b0d128ef21089dda5e0fba7f71763d8325148fe521de0d3f57e4ad"
},
"deliveries": {
"rows": 1726,
"maxSeq": 1726,
"sha256": "8667571f4a05ce3621b04cf2dc5eb0311bac3d3aff1858584c78445c5c4c2fb4"
},
"events": {
"rows": 1525,
"maxSeq": 1525,
"sha256": "e51827e31f2fa5a9a50993e5a10f37f1dee383b7d673f793006bd2ba1e9a5573"
},
"instance_names": {
"rows": 0,
"maxSeq": null,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"message_threads": {
"rows": 2,
"maxSeq": null
},
"messages": {
"rows": 567,
"maxSeq": 567
},
"meta": {
"rows": 3,
"maxSeq": null,
"sha256": "89fec56e7293d19e0b2f6cd8c0ef745ebf99e4115abeaea7fbdbc98a261332d9"
},
"projects": {
"rows": 1,
"maxSeq": 1,
"sha256": "a3945f7462895e0e302029c542d384a6d33e559da900e6461d78bbcd7cc28b16"
},
"role_claims": {
"rows": 98,
"maxSeq": 98,
"sha256": "ac6694fd0b365469f7486ae1f31486cd52b31be7bc0399938b7361f22454362e"
},
"task_comment_cursors": {
"rows": 215,
"maxSeq": 215,
"sha256": "7f80df8efb90b502a9530a7ed5068d59be59b2eb318aa4e64ce6bf481762a5e9"
},
"task_snapshots": {
"rows": 401,
"maxSeq": 401,
"sha256": "cced8c7f90ecaa60eee3010d987013d8c0301563cb888860fd91b8c5a02f0a00"
},
"thread_claims": {
"rows": 2,
"maxSeq": 2,
"sha256": "6046c7088779aa2f6e9b3fd2c7520404b1678a4fcae71c2b9157a008bba22a5f"
},
"thread_opens": {
"rows": 0,
"maxSeq": null,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"threads": {
"rows": 1,
"maxSeq": 1,
"sha256": "3043721efd386f23dfdaad4acf41e0d553ff6182f0068c2133a615141bcf772a"
},
"usage_records": {
"rows": 30,
"maxSeq": 30,
"sha256": "7ecbc0606273043b017d778bb23880377bfe367f788d234289b7c132caa588f2",
"byRun": [
{
"business": "mosaic-stack",
"run": "r8fba7e598156",
"input": "0",
"output": "0",
"unknownTurns": 1
},
{
"business": "mosaic-stack",
"run": "r4a96e2142e4d",
"input": "2",
"output": "52",
"unknownTurns": 0
},
{
"business": "mosaic-stack",
"run": "rc78c9700c8e9",
"input": "14",
"output": "1519",
"unknownTurns": 0
},
{
"business": "mosaic-stack",
"run": "r293f33bcbc63",
"input": "2",
"output": "44",
"unknownTurns": 0
},
{
"business": "mosaic-stack",
"run": "r34520a581318",
"input": "6",
"output": "323",
"unknownTurns": 0
},
{
"business": "mosaic-stack",
"run": "r7e99838f9f93",
"input": "2",
"output": "44",
"unknownTurns": 0
},
{
"business": "mosaic-stack",
"run": "rc964611b2092",
"input": "4",
"output": "120",
"unknownTurns": 0
},
{
"business": "mosaic-stack",
"run": "rbb708846b605",
"input": "12",
"output": "2089",
"unknownTurns": 0
},
{
"business": "mosaic-stack",
"run": "rc36b17ef3788",
"input": "16",
"output": "5221",
"unknownTurns": 0
}
]
}
},
"untimed": [
"message_threads",
"meta",
"task_comment_cursors"
]
}
@@ -0,0 +1,167 @@
{
"schema": "3i",
"tables": {
"decision_events": {
"rows": 21,
"maxSeq": 21,
"sha256": "28dd95056663dd5eb7442a3d815b73a4a3cd311a0e854fd82dc54f7dc3e6ef26"
},
"decisions": {
"rows": 21,
"maxSeq": 21,
"sha256": "fef1058660b0d128ef21089dda5e0fba7f71763d8325148fe521de0d3f57e4ad"
},
"deliveries": {
"rows": 1740,
"maxSeq": 1740,
"sha256": "554ce5b377ffacc72bfde11c99c920cd5d30337c8bfa217275d0073b587e2169"
},
"events": {
"rows": 1543,
"maxSeq": 1543,
"sha256": "0151673193d0286ce8f3b234d7d21fa2b0f43f2ceec55644cccaa5cbd77603dd"
},
"instance_names": {
"rows": 0,
"maxSeq": null,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"message_threads": {
"rows": 4,
"maxSeq": null
},
"messages": {
"rows": 571,
"maxSeq": 571
},
"meta": {
"rows": 3,
"maxSeq": null,
"sha256": "89fec56e7293d19e0b2f6cd8c0ef745ebf99e4115abeaea7fbdbc98a261332d9"
},
"projects": {
"rows": 2,
"maxSeq": 2,
"sha256": "3435f574ab94bc2fcc65fb8605251f30d4e9e63baf38e6fdf3c39c126531aa63"
},
"role_claims": {
"rows": 101,
"maxSeq": 101,
"sha256": "fb47114defbb8a57dd148a06fbaf61b30f791a81f14b1510b8c32690506b4308"
},
"task_comment_cursors": {
"rows": 220,
"maxSeq": 220,
"sha256": "df03c84094999299499de01107ddd08636df6156f5c9ccd2ff6e8c43e459f7e4"
},
"task_snapshots": {
"rows": 408,
"maxSeq": 408,
"sha256": "e2243a89388afba07e8d096b4961deeb2d17c656cdcace54b46eac7001393814"
},
"thread_claims": {
"rows": 4,
"maxSeq": 4,
"sha256": "088b8236570d8afe03364b85803fdc5034cce2cef6ed7b16f8b99bf6366524ad"
},
"thread_opens": {
"rows": 0,
"maxSeq": null,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"threads": {
"rows": 2,
"maxSeq": 2,
"sha256": "819fd24c64e279d0debad5ab5dbbe533d8e6f26de58277c69510b9cbbaadbc3a"
},
"usage_records": {
"rows": 34,
"maxSeq": 34,
"sha256": "de614f3d62c9b3819757c5659bee40085cb86cc873b7bfe381b13e254ddb7cad",
"byRun": [
{
"business": "mosaic-stack",
"run": "r8fba7e598156",
"input": "0",
"output": "0",
"unknownTurns": 1
},
{
"business": "mosaic-stack",
"run": "r4a96e2142e4d",
"input": "2",
"output": "52",
"unknownTurns": 0
},
{
"business": "mosaic-stack",
"run": "rc78c9700c8e9",
"input": "14",
"output": "1519",
"unknownTurns": 0
},
{
"business": "mosaic-stack",
"run": "r293f33bcbc63",
"input": "2",
"output": "44",
"unknownTurns": 0
},
{
"business": "mosaic-stack",
"run": "r34520a581318",
"input": "6",
"output": "323",
"unknownTurns": 0
},
{
"business": "mosaic-stack",
"run": "r7e99838f9f93",
"input": "2",
"output": "44",
"unknownTurns": 0
},
{
"business": "mosaic-stack",
"run": "rc964611b2092",
"input": "4",
"output": "120",
"unknownTurns": 0
},
{
"business": "mosaic-stack",
"run": "rbb708846b605",
"input": "12",
"output": "2089",
"unknownTurns": 0
},
{
"business": "mosaic-stack",
"run": "rc36b17ef3788",
"input": "16",
"output": "5221",
"unknownTurns": 0
},
{
"business": "mosaic-stack",
"run": "r097436c91816",
"input": "2",
"output": "131",
"unknownTurns": 0
},
{
"business": "mosaic-stack",
"run": "r497f1c817d4d",
"input": "2",
"output": "80",
"unknownTurns": 0
}
]
}
},
"untimed": [
"message_threads",
"meta",
"task_comment_cursors"
]
}
@@ -0,0 +1,193 @@
# Re-pin window 490e2771d to 4429a104f (row 192, Q175 phase 1), STATE report (W11)
Operator and tester: Sage. Plan: `agents/sage/work/window-plan/repin-4429a104f.md` at f41729178, sha256
e1272d1a391e00fb1291d47da9973c0ba342d65fcd42f583ea007e883c1f32b9 (checked before W0b, W9). The plan file in
this commit carries three corrections made after the window. They are listed at the end, so its sha256 differs
now. Tool blob 3b068b1546d4 (`scripts/stack-release.sh`). OLD 490e2771d3d057ab76991e2c7859cd251857e64c, NEW
4429a104f6d65ccf165f6c83b5d1dce3147dfde0. Every stamp below comes from `date -u` or from the transcript `W.txt`
in this directory. Mos's gos: mos-1113 (W5), mos-1114 (W9 to W11), mos-1128 (W9b accepted), mos-1135 (W10
browser part), mos-1137 (line g, post W11).
## Outcome
NEW is live for mosaic-stack with its PM, and the store stays on 3i. Row 192's sign-in code is in the copy but
dormant, because the Console runs without `--sso`. W0 to W10 passed, and nothing was rolled back. W10 stopped
once on two failed browser checks. I diagnosed both read-only as checks written before row 192, not faults in
NEW, and Mos passed them (mos-1135). Matrix line (g) isn't byte-identical, for a known cause, and Mos passed it
(mos-1137). Nobody acted as Jason. There was no `--by jason`, no pi, no Save and no `mosaic name`, and
`bus.tester` was not touched.
## Stamps and activation
| Step | Start (UTC) | Result |
| --- | --- | --- |
| P1 values, business-check OLD | file written 21:43:13Z | values as planned, LOAD_OK (`p1.txt`) |
| W0 copy NEW with npm ci, business-check NEW | 21:45:57Z | copied, lockfile diff empty, LOAD_OK (`w0.txt`) |
| P2 console-check on a stub of NEW | 21:46:09Z | CONSOLE_PASS without and with `--tester-port` (`p2.txt`) |
| P3 rehearsal (Mos) | 21:52:31Z to 21:52:34Z | REHEARSAL_PASS, rc 0 (`p3/`) |
| P3 flags check (Mos) | 21:53:52Z to 21:53:56Z | NEW Console with the pointer flags on 7341/7342: healthz 200 both, then gone, 0 listeners |
| P4, P5, P6 | 22:00:17Z | P4 PASS, P5 PASS (HEAD 4385b9ec1), `matrix-old.txt` |
| W0b business file copy | 22:00:49Z | sha256 1e49bdf0 |
| W1 idle check, S1 | 22:00:49.758Z | chief idle; S1 a7649760 |
| W2 status | 22:00:49.819Z | pointer OLD, pm true, 3i, unit flags match |
| W3 plan | 22:00:50.068Z | exit 0 (`W3-plan.txt` 45de68d4) |
| W4 unit read | 22:00:50.238Z | OLD units, no drop-ins, bus `--pm`, no quote or backslash |
| W5 activate (mos-1113) | 22:02:06.776Z, end 22:02:09.556Z | exit 0, gate passed, activate ok |
| W5 status | 22:02:13.885Z | pointer NEW, pm true, 3i, unit flags match |
| W9 checks and turns (mos-1114) | 22:05:51Z to 22:06:06Z | pass |
| W9b run end and S2 | 22:21:05.171Z, S2 22:21:07Z | ended by the launcher (thread-idle); S2 1a444f5a |
| W10 browser (stop), diagnosis | 22:29:14Z, 22:30:02Z | 10 of 12, then both failures traced to the checks |
| W10 matrix, P4 (mos-1135) | 22:31:29Z | a-f identical, g +121 bytes; P4 PASS |
| W11 reads | 22:44:33Z to 22:44:34Z | units, tunnel, business file, failed units |
Activation id 20261020T220206Z-4429a104f6d6. Pointer `at` 2026-10-20T22:02:09Z.
## Backups and hashes
- Store backup: `~/.mosaic-dev/backups/stack/20261020T220206Z-4429a104f6d6`, SHA256SUMS sha256
b61d64843c9e2a153842606353450838da9cb778c8f3b9c27cc9366dd72ec0c9.
- Rollback set: `~/.mosaic-dev/state/stack-rollback/20261020T220206Z-4429a104f6d6`. Rollback goes to 490e2771d.
- Business file: 1e49bdf05e62607972958f5b7298cfd600c7825812370e26d7c2a842f2af9b93 at P1, at W0b and again at
W11. The W0b copy is `~/.config/mosaic-dev/businesses/mosaic-stack.json.pre-window-20261020T220049Z` with the
same hash. Nothing in the window wrote it. `backups.txt` holds the W0b and W5 lines.
## Pointer before and after
Before (W2): copy 490e2771d3d0, pm true, schema 3i, activation 20261020T192956Z-490e2771d3d0. Console flags
`--port 7332 --public-host mosaic.woltje.com --auth-header X-authentik-username --allow-user jason.woltje
--tester-port 7333`.
After (W5, W9 and W11): copy 4429a104f6d6, pm true, schema 3i, activation 20261020T220206Z-4429a104f6d6. The
Console flags are the same, with no `--send`, no `--sso` and no proof pair. Bus pid 2950923 and Console pid
2950925, both since 22:02:08Z. Status shows drift of 6 commits on refactor since the pointer's copy (records).
## Unit ExecStart lines (no Environment values)
W11, after. Both units active, DropInPaths empty, WorkingDirectory the NEW copy:
```
bus: ~/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0/scripts/mosaic bus start mosaic-stack --pm (MainPID 2950923)
webui: /usr/bin/node ~/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0/packages/webui/src/cli.mjs serve --checkout /mnt/storage/src/mosaic-stack --port 7332 --public-host mosaic.woltje.com --auth-header X-authentik-username --allow-user jason.woltje --tester-port 7333 (MainPID 2950925)
```
W4 read the same two lines on OLD's copy (W.txt 329-344). The bus Environment has 2 assignments with no quote or
backslash. The values are not recorded. The Console's cwd is the NEW copy (W9). The webui journal since W5 has one
"Proxy: proxy proof off" line and no "Sign-in:" line.
Tunnel: `mosaic-preview-tunnel.service` active, running, MainPID 1317, since 2026-10-13 08:40:19 CDT. The window
didn't touch it. P4 before and after shows one R: spec, `R:0.0.0.0:7332:127.0.0.1:7332`.
## S1, S2 and the store
S1 (`S1.json`, sha256 a7649760c3439705c8b5f3af26d0c21aea1081c1eb295fd02aef3a642064194b) is W1's held read at
22:00:49Z. S2 (`agents/sage/work/window-repin-4429a104f/S2.json`, sha256
1a444f5ac0049bc78e397b6928e1008cfe8cbc30989bbf2042b4ed27f56a4173) is the held read after the assistant run
ended, at 22:21:07Z. Both are 3i. SNAP (`snapshot.mjs`, 639fd28a) was checked before each.
| Table | S1 rows | S2 rows | S2 vs S1 |
| --- | --- | --- | --- |
| decisions, decision_events | 21, 21 | 21, 21 | same sha256 |
| meta, instance_names, thread_opens | 3, 0, 0 | 3, 0, 0 | same sha256 |
| deliveries | 1726 | 1740 | +14 |
| events | 1525 | 1543 | +18 |
| role_claims | 98 | 101 | +3 |
| messages | 567 | 571 | +4 (counts only) |
| message_threads | 2 | 4 | +2 (counts only) |
| usage_records | 30 | 34 | +4 |
| projects, threads, thread_claims | 1, 1, 2 | 2, 2, 4 | +1, +1, +2 |
| task_snapshots | 401 | 408 | +7 |
| task_comment_cursors | 215 | 220 | +5 |
For every grown table with a seq, maxSeq grew by exactly the row delta.
The task_snapshots and task_comment_cursors rows (mos-1128 item 2). The writer is the bus's own Vikunja poll, not
an agent. `packages/bus/src/process.mjs:104` loads the tasks adapter. Its timer runs `tick` every `pollSeconds`
(`packages/tasks/src/adapter.mjs:192-197`), and `sync.mjs:169` calls `broker.recordTask` with no cap
(`packages/bus/src/broker.mjs:1123`). That writes `source 'poll'` with role and run null (`broker.mjs:1159-1174`)
and the comment cursors (`:1131-1138`). The adapter logs only on an error (`adapter.mjs:115`), so the bus journal
has no line for a good tick. That's why there's no sync line between 22:05Z and 22:22Z. The rows, read
read-only after the window:
- task_snapshots 402-404 at 22:05:08Z: P19 436, 435 and 434, all updated 22:04:55-56Z. 405-406 at 22:12:38Z: 438
and 437. 407 at 22:17:08Z: 438 again. 408 at 22:19:08Z: 431. All have `source poll`, `via cursor`, role null
and run null. These are tracker edits made by seats during the window, which the poll picked up.
- task_comment_cursors 216-220: tasks 424, 432, 438, 431 and 436, last comments 397 to 401. These are the
comments seats posted on P19 in the same minutes (398 is Mos's comment on #432).
Rows after S2 (task_snapshots 409 at 22:33Z, cursors 221-223) are the same poll and are outside the window.
## W9 output
- Status, console-check (CONSOLE_PASS), the journal counts, the Console cwd and one running chief from this
start (r097436c91816) all passed.
- `talk chief --tester` (c5b43f04): the chief replied that a tester message authorizes nothing and it took no
action.
- Project `window-20261020-2205`, thread 446201de-e8b5-4df3-9356-d0d5422cc899. `talk --thread` (b4b66ced) got one
assistant reply from run r497f1c817d4d. `thread read` shows the tester REQUEST and the assistant RESULT.
- `threads`, `names` (empty) and `usage` read fine. The human-thread probe was skipped: S1's one thread is the
177-185 window's tester thread d0ba7a66, so no human thread exists.
- W9b: the launcher logged launch at 22:06:00.766Z, thread-idle at 22:21:05.149Z and end at 22:21:05.171Z
(reason stopped, exit 0). Nobody stopped it by hand.
## W10
The browser check (`w10-browser.mjs`, dda57ccf) ran against 127.0.0.1:7333 with fetch and XHR limited to GET and
HEAD. It made no writes and sent no W9 thread request. 10 of 12 checks passed. The two that failed:
1. "Only tester forms": `#sign-out` was in the list. It is row 192's form at `packages/webui/src/public/index.html:12`,
`hidden`. `app.js:479` shows it only when `/api/config` has `sso: true`. The live config has no `sso`
key, and the form stayed hidden.
2. "Chat lists the W9 thread": the check read only the first `ul.chat-thread-list`. `chat.js:93` renders one list
per project, and W9 made the second project. The diagnosis (`w10-diag.mjs`, 42d0844b) read every panel and
found 446201de listed.
Both checks come from the 177-185 window (Darkwing's `window-rows-177-185/w10-browser.mjs`), written before row
192 and before a second thread project existed. Corrected, they now read:
- Only tester forms, allowing `#sign-out` when it is `hidden` and `/api/config` has no `sso: true`.
- The W9 thread is in some `.chat-projects section.panel` thread list, not only the first.
Both corrections are in the plan's W10 line, for the next window that copies this one.
The matrix (`matrix-new.txt` against `matrix-old.txt`). Lines a to f are byte-identical. Line g is 200 non-json
both times, but 6454 bytes on OLD and 6575 on NEW. `index.html` is 6470 bytes at 490e2771d and 6591 at 4429a104f,
+121, and the only hunk is the line 12 form. The served page grew by the same 121. The plan's W10 line now says a
line for a file the pin changes isn't "identical", with this case as its example. P4 again at 22:31:29Z: PASS.
## Failed user units (mos-1128 item 3)
All three predate the re-pin at 22:02Z and come from the legacy `~/.mosaic` and jarvis-brain setup, not the
stack. P19 #393, "dragon-lin: seven legacy user units in failed state", covers all three (filed by Mos on
10-20 at about 01:42Z). It has no comments yet. The current failure times are later than the ones #393 lists,
so I added a comment with them. Times are from `systemctl --user show` (CDT) with UTC alongside.
| Unit | Runs | Failed | #393 |
| --- | --- | --- | --- |
| mosaic-standup-pm.service | `~/.mosaic/tools/coordinator/standup_trigger.py pm` | 2026-10-19 19:00:28 CDT (10-20 00:00:28Z), exit 1 | listed |
| mosaic-install-drift.service | `~/src/jarvis-brain/scripts/alerting/mosaic-install-drift` | 2026-10-20 00:12:54 CDT (05:12:54Z), exit 9 | listed, with an older time |
| mosaic-unit-failed@mosaic-reconcile.service | `~/.local/bin/mosaic-unit-failed mosaic-reconcile.service` | 2026-10-20 13:16:12 CDT (18:16:12Z), exit 4 | listed, with an older time |
The marker's journal at 18:16:12Z says the escalation's tmux send to jarvis failed ("tmux target not found",
socket mosaic-fleet), so the page wasn't delivered. The record is on disk in
`~/.local/state/mosaic-unit-failures/20261020.jsonl`. `mosaic-reconcile.service` failed at that time (exit 7)
but last ran OK at 17:16:27 CDT (22:16:27Z). The marker stays failed until something resets it. The journal no
longer has lines for the other two units. The window changed none of these units, and touching them is
outside it (`~/.mosaic` is off limits, and #393 needs Jason or ops-01's charter to disable any). W11's list also
shows `setspark-tmp-guard.service` failed. It belongs to another project, and I've left it.
## Corrections to the plan, made after the window
- NEW is 103 commits past OLD (`git rev-list --count 490e2771d..4429a104f`), not 106. The 106 in `status` was OLD
to HEAD 305857c8f, so it also counted the three record commits.
- P3: `rehearse` starts NEW's Console with only `--port` and `--business`, not the pointer's flags. Mos's
separate flags check covers that (`p3/flags-check.txt`, `p3/flags-console.log`). The line was wrong as
written (mos-1107).
- W10: the matrix line g size change and the two corrected browser checks, as above.
## Files
`W.txt` (transcript), `S1.json`, `S2.json`, `matrix-old.txt`, `matrix-new.txt`, `backups.txt`, `W3-plan.txt`,
`W5.txt`, `W5-status.txt`, `w0.txt`, `p1.txt`, `p2.txt`, `p3/` (Mos's rehearsal and flags check), `p456.sh`,
`w9.sh`, `w9b.sh`, `w9.out`, `w9b.out`, `w10-browser.mjs`, `w10-diag.mjs` with their outputs (the three
screenshots stay on disk in this directory, uncommitted, since the push check refuses binaries),
and the `.w9*`, `.w10*` and `.w11*` reads. The store snapshot tool is `snapshot.mjs` (639fd28a).
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,116 @@
plan: activate for mosaic-stack
from: 490e2771d3d057ab76991e2c7859cd251857e64c
to: 4429a104f6d65ccf165f6c83b5d1dce3147dfde0
store schema: 3i (read from the store the running bus holds)
target schema: 3i
migration: none; the store is at the target's version
backup: the run backs up the stopped store to /home/jwoltje/.mosaic-dev/backups/stack/<activation>/ before any migration
commits: the run adds 103 (newest first, at most 200):
4429a104f webui: Q175 phase 1, the Console verifies SSO itself under --sso, OIDC RP with oauth4webapi 3.8.8 (row 192, #1690)
32b54ac3b plans: Case 2 row briefs from Gosalyn's 51006710a (webby-0034 N1/N2 and webby-0035: row A README :711 one line per variable, C2-13 owns README :713 and says the precheck prints only variable names)
6025ddd9c plans: Case 2 row briefs from Gosalyn's a2d94b2c2 (Webby r9 notes, webby-0032: row A README line on claude-session.env and the dist/providers/data/zai.json path, C2-13 XDG_DATA_HOME precheck and last-assignment-wins key file parse)
ecbfd6deb plans: Case 2 row briefs from Gosalyn's 29e85196b (design r9 for mos-1090: row A out of scope points at design 10 step 2d, C2-13 precheck checks the hub key file by name and mode, claude marker check only for claude-code harness, EnvironmentFile= fixed in case2-unit.sh)
0498c0613 agents: Gosalyn seat dir, README from gosalyn/case2-design, so Case 2 rows can name the seat as owner (#416)
49d1f3a23 plans: Case 2 slice row briefs plus rows A and B, from Gosalyn's f325657c7 on gosalyn/case2-design (#416, design r8, C2-13 copy id and profile asks for row 195, C2-20 count-only note, row A ZAI_API_KEY per mos-1086/mos-1089, row B #428 after 10-27)
628e2ca5c sessions: Filbert row 190 manifest round 1 CHANGES (base drift on row 189)
088087ac9 queue: revs 2248-2361+. Sage: rows 210 (#424 host access, Gizmoduck) and 211 (#426 force-push guard, Rocko) added and briefed; row 195 re-briefed to the Case 1 install on .53; review requests for 200 r1/r2 and 202; plus earlier Sage revs. Other seats' revs: Launchpad 2254, 2283-2284, 2286, 2311-2313, 2328-2329, 2344, 2347-2348 (rows 192/200); Honker 2255; Morgana 2257, 2320, 2326-2327 (row 204); Gander 2262; Herb 2265; Dewey 2268; Bushroot 2273; Megavolt 2276; Webby 2303, 2316, 2339, 2355 (row 192/200 verdicts); Gizmoduck 2304, 2318-2319 and the row 210 claim (rows 201/210); Quackerjack 2317, 2341-2342 (row 202); Duckworth 2323, 2335, 2353 (rows 201/204/202 verdicts); Darkwing 2354, 2357 (row 190 in-review)
a0fbefd48 plans: pre-push force-push hook and failing busy check brief (Vikunja #426, mos-1078, mos-1083; owner Rocko, reviewer Dewey)
f24f2cd40 plans: row 195 brief becomes the Case 1 install on 10.1.1.53 (Q181 hosts, Q182/Q183 recorded acceptance, Q185 existing account, chained on rows 190 and C2-13; ops-01 root steps under #423 step 1; sage-1439)
9ba31b5b3 agents: Honker's PRD 1.7 draft r2 on refactor for the prd.approve raise (11cb0423b, sha256 e896746df595d1b5155130ca163fbf63b0dc38a87a8dd154af77f1a6d1328647, Duckworth approved duckworth-0039, Vikunja #420)
9db578ff5 plans: agent host access with sudo through the credential store brief (#1707, Vikunja #424, Q183/Q184, chained on row 199)
8eb9ddd12 plans: FED R8 brief to r4.11 (b93f098f0, recorded acceptance, Duckworth N1/N2, #416 date), R5 context source (sage-1419), 193a makes covers level required (duckworth-0033 N1)
ae3c36f24 agents: Gizmoduck seat dir and roster row (FED R1 owner, #414)
a2af7ab49 plans: FED R1, R5, R2 and R8 briefs (#414, r4.9 a1daa3c07) and row 193a-d build briefs (#1691); Quackerjack seat dir and roster row
aa1bd209e gate-lane: row 189 gate timing table for row 197 (Herb)
ace27dc5e row 189 (#1687): stack-release.sh activate --no-send drops the Console's --send (Sage; Dewey approve 29485; Sage gate passed)
a4a42dbac plans: stack-release --sso brief (#1698, Launchpad, reviewer Webby, row 192 F3; chained on 192)
a31f1a0f8 plans: row 198 brief records Mos's call, in-process board reads behind the Console's auth (mos-1019)
992650232 plans: agent credentials and git identity brief (MVP T1 item 6, #1697, Megavolt, reviewer Duckworth); Megavolt seat dir
a7661ada0 plans: row 195 acceptance script takes Gizmoduck's gate item list; B8 issuer and TLS front end (mos-1011)
f871cb309 plans: retire the control board writer on 7331 brief (#1696, Bushroot, reviewer Webby, mos-1008 item 2); Bushroot seat dir
0bbd23c61 plans: row 195 brief adds the identity provider check and flags any new model account for Jason (mos-1008)
0217b71c8 plans: row 195 brief rewritten as the hub on its own host, host left to Jason (#1693, mos-0998, mos-1005)
c6f528a32 plans: second gate lane brief (#1695, Herb, reviewer Webby, mos-0977 item 6); Herb seat dir; gate template and busy check copies for the row
96f243faf plans: human boundary row brief (P19 333, #1694, Gander, reviewer Duckworth, mos-0989); Gander seat dir; seat READMEs name the credentials row, not row 195 (now 190b)
c86269935 sessions: Filbert row 190 design review r3, approved
48ebee0ff plans: row 190 brief scopes isolation to addressing on one uid, points to row 190b (Filbert N7, mos-0986)
66bdd84b8 sessions: Filbert row 190 design review r2, changes requested
6966164f0 plans: row 190b brief, Melanie's hub under its own Linux user (#1693, Darkwing, reviewer Duckworth), mos-0986
53bb59462 sage: common start for MVP T1 builder seats (mos-0974, mos-0977, mos-0981)
5e01c3683 agents: seat dirs and roster rows for Launchpad, Morgana, Honker (Sage's T1 builders) and Webby, Duckworth (Mos's reviewers); row 193 brief takes Mos's B1 starting rule (mos-0985)
9f56d2098 plans: rows 192 and 194 review by Webby, not Filbert or Dewey (mos-0981)
0599b119f plans: MVP T1 briefs, SSO phase 1 (#1690, Launchpad), human users (#1691, Morgana), agent lifecycle (#1692, Honker), mos-0974/0977
b416d2ede sage: Q175 SSO design, Mos C1 condition (mos-0977): straggler check before Jason step 4 in the phase 2 window
5fc73a729 revs 2237-2247: row 189 added, briefed, started, in-review round 1 (Dewey, comment 29457); rows 190 (Darkwing) and 191 (Rocko) added and briefed for MVP T1 (mos-0974); darkwing-190-start (rev 2245, Darkwing's) and rocko-191-start (rev 2244, made for Rocko)
99772421a sage: row 189 candidate manifest for Dewey's review (d6116771f)
da230ca48 plans: MVP T1 briefs, several instances per host (#1688, Darkwing) and the kill switch (#1689, Rocko), mos-0974
4f2d1be68 sessions: Filbert Q175 SSO design D1/D2 ack
d55d86bb0 sage: Q175 SSO design r3 fix, Filbert r3 D1 (loader checks slot shape only) and D2 (slot from config dir, scratch Console dataRoot and unreachable issuer)
c56ccf916 sessions: Filbert Q175 SSO design review r3, approve with D1
817e8f51b sage: Q175 SSO design r3 for #412, Filbert r2 C1 (shared loader, config.json inside the phase 2 window, rollback order, test 25) and C2 (401 signed-out page, test 24), notes and Q-E
5d019d876 sessions: Filbert Q175 SSO design review r2, changes
59475fc5a sage: row 163 landing record, P19 comments 372/373 on 337/412, and P19 #413 filed for the --no-send row
1a268249e sage: Q175 SSO design r2 for #412, Filbert r1 B1-B6, rulings Q-B/Q-C/Q-D, config in config.json (mos-0973)
def2e7932 plans: brief for stack-release activate --no-send (Q175 interim, mos-0972/0973, #1687, P19 413)
c30b36fff sessions: Filbert Q175 SSO design review r1, changes
9ae8c73ac queue: revs 2235-2236: row 163 Sage landing note (8ecc7a239, patch-id b8d5f8a37c24, gate passed on eaa0f2666) and done
8ecc7a239 row 163 (#1660): stack-release.sh rehearse, the D2 rehearsal on a scratch root from a backup, and bus start --no-launch (Sage; Filbert approve 29447; Sage gate passed)
7f57419b3 sage: Q175 SSO design for #412, OIDC relying party on oauth4webapi 3.8.8, for Filbert review (mos-0970, mos-0971)
4ca8d6e94 sage: row 42 run log, S7 reachability pass (51cebff7 answered by cff1e9ad after a chief launch, mos-0969)
22e5e825b sage: row 42 run log, plain mosaic on a pinned host supersedes the 10-12 full-path line (mos-0968)
e6dea74b2 sessions: Sage row 163 r2 approval intake; P19 337 comment 368 script and record
cd2b377bb queue: rev 2234: row 163 r2 approve from Filbert (op filbert-163-r2, comment 29447, candidate c6f4f4c12596)
ceccb0ef2 sessions: Filbert row 163 r2 review, approve
10ab8ea62 sessions: Sage row 163 r2 in-review; P19 337 comment 367 script and record
c539995cf queue: revs 2231-2233: row 163 r2 in-review from Sage (ops sage-163-r2-start, sage-163-inreview-2, .outcome; request comment 29446, reviewer Filbert, manifest c6f4f4c12596)
92cc6011c queue: rev 2230: row 163 r1 changes from Filbert (op filbert-163-r1, comment 29445, candidate 430212299beb)
7af166574 sage: row 163 r2 packet (candidate 2c090eefd on code 2a5f3a3a0, base 4f02486d5: diff, manifest c6f4f4c12596, gate passed, mutants K34-K41 killed)
2244e8baf sessions: Filbert row 163 r1 review, changes
bde0fdec9 sessions: Sage row 163 r1 in-review; P19 337 comment 366 script and record
def677bc7 queue: revs 2228-2229: row 163 r1 in-review from Sage (ops sage-163-inreview-1, .outcome; request comment 29444, reviewer Filbert, manifest 430212299beb)
51f67f1f8 sage: row 163 packet (candidate 4be3f6aec on 580500699: diff, manifest 430212299beb, gate passed, mutants K1-K33 killed after four survivors got tests)
5b298bd00 sessions: Dewey row 66 r1 (changes) and r2 (approve) reviews
565bc32c2 queue: revs 2223-2227: row 66 r2 round from Sage (2223-2225, ops sage-r66-r2-start, sage-r66-inreview-2); r2 verdicts from Filbert (2226, op filbert-66-r2, approve) and Dewey (2227, op dewey-66-r2, approve)
5140aa9e8 sessions: Filbert row 66 r2 review, approve
973cb5a87 queue: revs 2220-2222: row 163 Sage start (2220); row 66 r1 verdicts from Filbert (2221, op filbert-66-r1) and Dewey (2222, op dewey-66-r1)
b82ec9353 sage: row 66 r2 packet (candidate 0f4a57956: diff, manifest cee63e06fdea, gate, mutants K1-K4 killed, singles rerun)
3560c450a sessions: Filbert row 66 r1 review, changes
4f02486d5 sage: row 66 r1 packet (candidate 8ec03b45a: diff, manifest e68c6bce5ec4, gate r2), row 42 run log (trigger (a), mos-0960), SESSIONS line
263ea8a9a queue: revs 2215-2219: row 66 Sage start (2215), in-review round 1 on candidate manifest e68c6bce5ec4 (8ec03b45a) and request comment 29437 (2216-2217); row 163 brief re-pinned to b1500d96ad27 (6020bb43a) and briefed (2218-2219)
6020bb43a plans: D2 brief owner is sage (row 163, #1660), and D2 lands after the Part B window (mos-0958, mos-0959)
5487ed39d sage: row 188 landing record: P19 411 (N1) and comments 361-363 script and record, 409/410 description restore after a partial done update, SESSIONS line
31e328958 queue: revs 2209-2214: row 188 Darkwing start, in-review and request 29432 (2209-2211); Filbert approve 29433 (2212); Sage landing note (8fc0ee216, patch-id c9a56b167c07, stack-release.sh blob 2586a2a37cd2, gate passed) and done (2213-2214)
8fc0ee216 row 188 (#1686, P19 409, 410): stack-release T54, the proof file checks on a carried pair and a proof file inside the repository (Darkwing; Filbert approve 29433; Sage gate passed)
23a609c5c sessions: Filbert row 188 r1 review, approve
b9c1c9fe2 sage: Part B draft 2 A2 passed (mos-0958), row 188 launched, Vikunja comments 358-360 script and record, SESSIONS line
525990ecb queue: revs 2207-2208: row 188 add and briefed (Sage), #1686, P19 409/410, gates Part B (mos-0956)
148a9503d sage: Part B window file draft 2 for mos-0957 (F1: B6 and B-R1 name the stop's endLaunch rows, PB5 counts per step; F2: PB6, PB7 and B-R1 matrices run (a)-(h)); Q2 answered, Q1 to Jason's list
240f21650 brief: stack-release tests for a carried proof pair and a proof file inside the repository (P19 409, 410, #1686, mos-0956)
742e98976 sage: Part B window file, the read window's end stamp from the clock (10:29Z, not 10:40Z)
288ab1b5a sage: Part B window file for Mos's A2 check (live-copy B3 and B-R1, Q1 on J2 order), SESSIONS lines for Darkwing (row 187, 10:25:22Z) and Sage
a843ae284 sage: row 187 landing, P19 407-410 filed and P19 385 comment 357, #1685 closed (comment 29425), SESSIONS line
652d6f3a8 queue: revs 2201-2206: row 187 Darkwing start, in-review and request comment 29423 (2201-2203); Filbert approve comment 29424 (2204); landing note (9a71e4e80, patch-id 6b57c47bc661, gate passed) and done (Sage 2205-2206)
9a71e4e80 row 187 (#1685): activate sets, carries and drops the Console's proof pair
d17328a55 sessions: Filbert row 187 r1 review, approve
f81ebfbae sage: row 186 follow-ups filed as P19 405 and 406, P19 404 closed (mos-0955)
9760d4967 sage: row 186 landing, P19 404 comment 354 and SESSIONS line, with Darkwing's row 186 SESSIONS line (09:13:21Z)
16a936c79 queue: revs 2198-2200: row 186 Filbert approve comment 29419 (2198); landing note (269134d8d, patch-id c97b9f6e25cd, gate passed) and done (Sage 2199-2200)
269134d8d row 186 (#1684): the activate plan's backup line says what the run will do
3c5da3a91 sessions: Filbert row 186 r1 review, approve
b367f5730 sage: rows 186-187 Vikunja comments (P19 404 351, P19 385 352-353) and SESSIONS line (mos-0954)
5a768d82c queue: row 187 added and briefed (Part B A0, #1685), with Darkwing's row 186 start (rev 2193)
6de3c86be plans: brief for row 187, activate carries the row 178 proof pair (mos-0928 Part B A0, #1685)
1d7fd75e2 queue: row 186 added and briefed (P19 404, #1684)
f5c72a5c3 plans: brief for row 186, the activate plan's backup line (P19 404, #1684)
b724b7b0b sessions: Sage Part A window line, with Darkwing's 06:53:53Z and 08:28:01Z lines
e24a13ad8 sage: Vikunja scripts and read-back records 2026-10-13 to 10-20 (decision 110, mos-0882)
21aa1cab0 darkwing: re-pin window rows 177-185 packet, STATE.md (sha256 b586f11c9b6f1329), W.txt (edd4622c5c45ef3f), S1/S2/S2b/B snapshots, matrices, W10 browser check and P3 rehearsals (#1683; P19 386)
9ddd8a92c plans: repin-rows-177-185 draft 4, R1 snapshot after the plain rollback (mos-0943)
e7637b99c plans: repin-rows-177-185 draft 3, R1post expects one tester action.refused per refused read (mos-0935, mos-0942)
2ea05700b plans: mos-0928 plan draft 2, NEW is the row 185 landing 490e2771d
010666074 queue: revs 2185-2190: row 185 start and in-review on bf87dbba with request comment 29403 (Darkwing 2185-2187); Filbert approve comment 29404 (2188); landing note (490e2771d, patch-id 774cd23186d5, gate passed) and done (Sage 2189-2190)
bus: scripts/bus-service.sh install --copy 4429a104f6d65ccf165f6c83b5d1dce3147dfde0 --copies /home/jwoltje/.local/share/mosaic-stack/copies --pm --claude /home/jwoltje/.local/bin/claude
proof: off
send: off
Console: scripts/webui-service.sh install --copy 4429a104f6d65ccf165f6c83b5d1dce3147dfde0 --copies /home/jwoltje/.local/share/mosaic-stack/copies --port 7332 --public-host mosaic.woltje.com --auth-header X-authentik-username --allow-user jason.woltje --tester-port 7333
CLI: scripts/stack-copy.sh wrapper 4429a104f6d65ccf165f6c83b5d1dce3147dfde0 --copies /home/jwoltje/.local/share/mosaic-stack/copies --bin /home/jwoltje/.npm-global/bin
@@ -0,0 +1,21 @@
pointer: /home/jwoltje/.mosaic-dev/state/stack-active.json
{"copy":"4429a104f6d65ccf165f6c83b5d1dce3147dfde0","business":"mosaic-stack","pm":true,"claude":"/home/jwoltje/.local/bin/claude","console":["--port","7332","--public-host","mosaic.woltje.com","--auth-header","X-authentik-username","--allow-user","jason.woltje","--tester-port","7333"],"schema":"3i","activation":"20261020T220206Z-4429a104f6d6","at":"2026-10-20T22:02:09Z"}
log (last five lines):
{"at":"2026-10-20T07:58:02Z","op":"activate","from":"89af38a83afa22a8a4f0d368a0a4030ad15a0efa","to":"490e2771d3d057ab76991e2c7859cd251857e64c","backup":"20261020T075800Z-490e2771d3d0","activation":"20261020T075800Z-490e2771d3d0","result":"ok"}
{"at":"2026-10-20T19:29:56Z","op":"activate","from":"490e2771d3d057ab76991e2c7859cd251857e64c","to":"490e2771d3d057ab76991e2c7859cd251857e64c","backup":null,"activation":"20261020T192956Z-490e2771d3d0","result":"started"}
{"at":"2026-10-20T19:30:00Z","op":"activate","from":"490e2771d3d057ab76991e2c7859cd251857e64c","to":"490e2771d3d057ab76991e2c7859cd251857e64c","backup":"20261020T192956Z-490e2771d3d0","activation":"20261020T192956Z-490e2771d3d0","result":"ok"}
{"at":"2026-10-20T22:02:06Z","op":"activate","from":"490e2771d3d057ab76991e2c7859cd251857e64c","to":"4429a104f6d65ccf165f6c83b5d1dce3147dfde0","backup":null,"activation":"20261020T220206Z-4429a104f6d6","result":"started"}
{"at":"2026-10-20T22:02:09Z","op":"activate","from":"490e2771d3d057ab76991e2c7859cd251857e64c","to":"4429a104f6d65ccf165f6c83b5d1dce3147dfde0","backup":"20261020T220206Z-4429a104f6d6","activation":"20261020T220206Z-4429a104f6d6","result":"ok"}
check:
bus: /home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0
Console: /home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0
CLI: /home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0
result: pass
bus status:
host: mosaic-stack, pid 2950923, running, since 2026-10-20T22:02:08.133Z, notifier off
socket: present
writer.lock: pid 2950981
boot: organization file loaded at 2026-10-20T22:02:08.125Z
unit flags: match the pointer
drift: 6 commit(s) on refactor since the pointer's copy
# exit 0
@@ -0,0 +1,8 @@
stack-release: captured the rollback set: /home/jwoltje/.mosaic-dev/state/stack-rollback/20261020T220206Z-4429a104f6d6
stack-release: stopped both units
stack-release: stopped store schema: 3i
stack-release: backup: /home/jwoltje/.mosaic-dev/backups/stack/20261020T220206Z-4429a104f6d6
stack-release: installed 4429a104f6d65ccf165f6c83b5d1dce3147dfde0
stack-release: gate passed on 4429a104f6d65ccf165f6c83b5d1dce3147dfde0
stack-release: activate ok: 4429a104f6d65ccf165f6c83b5d1dce3147dfde0 is live for mosaic-stack
# exit 0
@@ -0,0 +1,2 @@
W0b 2026-10-20T22:00:49Z business file copy /home/jwoltje/.config/mosaic-dev/businesses/mosaic-stack.json.pre-window-20261020T220049Z sha256 1e49bdf05e62607972958f5b7298cfd600c7825812370e26d7c2a842f2af9b93 (source /home/jwoltje/.config/mosaic-dev/businesses/mosaic-stack.json sha256 1e49bdf05e62607972958f5b7298cfd600c7825812370e26d7c2a842f2af9b93)
W5 2026-10-20T22:02:13Z activation 20261020T220206Z-4429a104f6d6 backup /home/jwoltje/.mosaic-dev/backups/stack/20261020T220206Z-4429a104f6d6 SHA256SUMS sha256 b61d64843c9e2a153842606353450838da9cb778c8f3b9c27cc9366dd72ec0c9
@@ -0,0 +1,7 @@
a 200 ok=true
b 401 error="sign-in required"
c 401 error="sign-in required"
d 403 error="proxy Host refused"
e 403 error="tester listener required"
f 403 error="proxy header refused"
g 200 non-json 6575 bytes
@@ -0,0 +1,8 @@
# P6 2026-10-20T22:00:17Z
a 200 ok=true
b 401 error="sign-in required"
c 401 error="sign-in required"
d 403 error="proxy Host refused"
e 403 error="tester listener required"
f 403 error="proxy header refused"
g 200 non-json 6454 bytes
@@ -0,0 +1,4 @@
copy 490e2771d: roles 6, projects 1, arbiters 2
HOLDS chief instance.name yes
HOLDS pm instance.name no
LOAD_OK
@@ -0,0 +1,19 @@
{
"bus.tester": true,
"plugins": {
"usage-source": [
"transcript"
],
"channel": [
"cli",
"webui"
],
"harness": [
"claude-code"
]
},
"launch.by": "chief",
"launch.instances has assistant": true,
"roles with pi harness": [],
"sha256": "1e49bdf05e62607972958f5b7298cfd600c7825812370e26d7c2a842f2af9b93"
}
@@ -0,0 +1,35 @@
start 2026-10-20T21:46:09Z
business-check.mjs: OK
console-check.sh: OK
1ba3c7efbc84a1d7eb8ee0068d32b7917f4e75cdc4066703944cea4c20db5b93 /mnt/storage/scratch/sage-pin-de352dfaf/fixture/console-stub.mjs
a4500094e7c7a6dc21797eaf2d696f9814f272ece945c2735b79e4593f9c9503 /mnt/storage/scratch/sage-p2-4429/console-stub-tester.mjs
== run 1: pinned stub, no tester listener
stub pid 2495939 port 39561
PASS markdown-preview.js is served and equals the copy's
PASS /api/config on loopback: send=false edit=false
PASS project.files through the Console: head=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa files=2 spec=true error=none
PASS project.file spec/north-star.md through the Console: commit=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa length=4 error=none
PASS loopback save refused: HTTP 403 error=read-only sent=false
CONSOLE_PASS
PORT 39561
CALLS []
run 1 console-check rc=0
== run 2: main listener plus a tester listener on a second port
stub pid 2496085 port 35213 tester port 35945
PASS markdown-preview.js is served and equals the copy's
PASS /api/config on loopback: send=false edit=false
PASS project.files through the Console: head=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa files=2 spec=true error=none
PASS project.file spec/north-star.md through the Console: commit=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa length=4 error=none
PASS loopback save refused: HTTP 403 error=read-only sent=false
CONSOLE_PASS
PASS main refuses x-mosaic-tester: HTTP 403 "tester listener required"
PASS tester /api/config: HTTP 200 "tester":true
PASS tester /api/config send and edit off: HTTP 200 "send":false,"edit":false
PASS tester refuses the main auth header: HTTP 403 "proxy header refused"
PASS tester refuses project-file save: HTTP 403
PORT 35213
TPORT 35945
CALLS []
TESTER_PASS
run 2 console-check rc=0 tester rc=0
end 2026-10-20T21:46:10Z
@@ -0,0 +1 @@
2026-10-20T21:52:34Z
@@ -0,0 +1,10 @@
flags-check start 2026-10-20T21:53:52Z
pid 2729699 alive: yes
127.0.0.1:7341
127.0.0.1:7342
main /healthz: 200
tester /healthz: 200
main /api/config: {"board":"http://127.0.0.1:7331","send":false,"edit":false}
pid 2729699 after TERM: gone
listeners after: 0
flags-check end 2026-10-20T21:53:56Z
@@ -0,0 +1,7 @@
Mosaic Console: http://127.0.0.1:7341/
Board: http://127.0.0.1:7331
Bus: mosaic-stack
Preview: https://mosaic.woltje.com/ through the proxy, signed-in user from x-authentik-username, read-only
Proxy: proxy proof off
Tester Console: http://127.0.0.1:7342/ (binds the tester)
Ctrl-C stops this server.
@@ -0,0 +1 @@
rc=0
@@ -0,0 +1,20 @@
PASS copy: 4429a104f6d65ccf165f6c83b5d1dce3147dfde0, schema 3i
PASS port: 127.0.0.1:7341 is free
PASS backup: /home/jwoltje/.mosaic-dev/backups/stack/20261020T192956Z-490e2771d3d0 matches its SHA256SUMS
PASS scratch-root: /home/jwoltje/.mosaic-dev/rehearsals/20261020T215231Z-4429a104f6d6/data from the backup, config and business file under /home/jwoltje/.mosaic-dev/rehearsals/20261020T215231Z-4429a104f6d6/config, business.diff beside them
PASS data-root-live: /home/jwoltje/.mosaic-dev/rehearsals/20261020T215231Z-4429a104f6d6/data is not the live data root
PASS sessions-live: no running session named in the data root
PASS data-root-scratch
PASS notify-off: binding null, so no Discord bridge
PASS tracker-configured: bootConfig sets no trackers
PASS credential-ref-live: 9 reference(s), each a placeholder
PASS migration: not needed: the backup is already 3i
PASS gate: host-up, bus-status, console-up, healthz and copy-match on 4429a104f6d65ccf165f6c83b5d1dce3147dfde0
PASS no-session: no sessions.json and no launches under /home/jwoltje/.mosaic-dev/rehearsals/20261020T215231Z-4429a104f6d6/data
PASS console-stopped: pid 2675437
PASS host-stopped: pid 2675436
PASS host-released-store
PASS backup-unchanged
PASS live-config-unchanged
REHEARSAL_PASS
rehearsal directory: /home/jwoltje/.mosaic-dev/rehearsals/20261020T215231Z-4429a104f6d6
@@ -0,0 +1 @@
2026-10-20T21:52:31Z
@@ -0,0 +1,7 @@
# P4 2026-10-20T22:00:17Z
DropInPaths=
ExecStart={ path=/home/jwoltje/.local/bin/chisel ; argv[]=/home/jwoltje/.local/bin/chisel client --fingerprint ce+ISFvMAhuoCRDuc7riIf+hYz87TLHk/C9C/H8QsUg= --keepalive 25s --max-retry-interval 10s http://10.1.1.227:8080 R:0.0.0.0:7332:127.0.0.1:7332 ; ignore_errors=no ; start_time=[n/a] ; stop_time=[n/a] ; pid=0 ; code=(null) ; status=0/0 }
# exit 0
# R: specs (1): R:0.0.0.0:7332:127.0.0.1:7332
P4 PASS: one R: spec R:0.0.0.0:7332:127.0.0.1:7332, DropInPaths empty
rc 0
@@ -0,0 +1,67 @@
#!/usr/bin/env bash
# P4-P6 of agents/sage/work/window-plan/repin-4429a104f.md (f41729178), adapted from Darkwing's window-rows-177-185/p456.sh, read only.
# P4 the tunnel unit, P5 the four REPO scripts against NEW, P6 the 7332 matrix on OLD (matrix-old.txt).
# usage: p456.sh p4|p5|p6
set -uo pipefail
REPO=/mnt/storage/src/mosaic-stack
WIN=$REPO/agents/sage/work/window-repin-4429a104f
NEW=4429a104f6d65ccf165f6c83b5d1dce3147dfde0
FILES=(scripts/stack-release.sh scripts/webui-service.sh scripts/bus-service.sh scripts/stack-copy.sh)
# The plan's <32 letters>: a fixed dummy, not a secret. matrix-new.txt uses the same value.
PROOF=abcdefghijklmnopqrstuvwxyzabcdef
p4() {
echo "# P4 $(date -u +%FT%TZ)"
local out; out="$(systemctl --user show mosaic-preview-tunnel.service -p ExecStart -p DropInPaths)"; local rc=$?
printf '%s\n' "$out"; echo "# exit $rc"
local specs; specs="$(grep -oE 'R:[^ ;]+' <<<"$out")"
local n; n="$(grep -c . <<<"$specs")"
echo "# R: specs ($n): $(tr '\n' ' ' <<<"$specs")"
local drop; drop="$(sed -n 's/^DropInPaths=//p' <<<"$out")"
if [[ $rc -eq 0 && $n -eq 1 && "$specs" == "R:0.0.0.0:7332:127.0.0.1:7332" && -z "$drop" ]]; then
echo "P4 PASS: one R: spec R:0.0.0.0:7332:127.0.0.1:7332, DropInPaths empty"
else
echo "P4 FAIL"; return 3
fi
}
p5() {
echo "# P5 $(date -u +%FT%TZ); HEAD $(git -C $REPO rev-parse HEAD)"
git -C $REPO diff --quiet $NEW HEAD -- "${FILES[@]}"; local d=$?
echo "# git diff --quiet $NEW HEAD -- ${FILES[*]}: exit $d"
local st; st="$(git -C $REPO status --porcelain -- "${FILES[@]}")"; local s=$?
echo "# git status --porcelain (exit $s): [${st}]"
for f in "${FILES[@]}"; do echo "# $f HEAD blob $(git -C $REPO rev-parse HEAD:$f) NEW blob $(git -C $REPO rev-parse $NEW:$f)"; done
if [[ $d -eq 0 && $s -eq 0 && -z "$st" ]]; then echo "P5 PASS"; else echo "P5 FAIL"; return 3; fi
}
# one matrix line: label, then curl arguments; prints "<label> <code> <error or ok field>"
req() {
local label="$1"; shift
local body; body="$(mktemp -p "${TMPDIR:-/tmp}")"
local code; code="$(curl -s -m 10 -o "$body" -w '%{http_code}' "$@")"
local field; field="$(node -e '
let s = require("fs").readFileSync(process.argv[1], "utf8"), j;
try { j = JSON.parse(s); } catch { console.log("non-json " + s.length + " bytes"); process.exit(0); }
console.log(j.error !== undefined ? "error=" + JSON.stringify(j.error) : "ok=" + JSON.stringify(j.ok));
' "$body")"
rm -f "$body"
echo "$label $code $field"
}
p6() {
req a http://127.0.0.1:7332/healthz
req b -H 'Host: mosaic.woltje.com' http://127.0.0.1:7332/
req c -H 'Host: mosaic.woltje.com' -H "X-Mosaic-Proof: $PROOF" http://127.0.0.1:7332/
req d -H 'X-authentik-username: tester-probe' http://127.0.0.1:7332/
req e -H 'x-mosaic-tester: 1' http://127.0.0.1:7332/
req f -H 'X-authentik-username: tester-probe' http://127.0.0.1:7333/
req g -H "X-Mosaic-Proof: $PROOF" http://127.0.0.1:7333/
}
case "${1:-}" in
p4) p4 ;;
p5) p5 ;;
p6) p6 ;;
*) echo "usage: p456.sh p4|p5|p6" >&2; exit 2 ;;
esac
@@ -0,0 +1,9 @@
# P5 2026-10-20T22:00:17Z; HEAD 4385b9ec183a2da4136ccd639d2a6b041eb9c68f
# git diff --quiet 4429a104f6d65ccf165f6c83b5d1dce3147dfde0 HEAD -- scripts/stack-release.sh scripts/webui-service.sh scripts/bus-service.sh scripts/stack-copy.sh: exit 0
# git status --porcelain (exit 0): []
# scripts/stack-release.sh HEAD blob 3b068b1546d4020cffde5d73929d9130afe91911 NEW blob 3b068b1546d4020cffde5d73929d9130afe91911
# scripts/webui-service.sh HEAD blob 0a32547c54ac233e75a21f4817ae230855dbc088 NEW blob 0a32547c54ac233e75a21f4817ae230855dbc088
# scripts/bus-service.sh HEAD blob f71250204dfeceb51beeea5941ebbf3a33ce4b92 NEW blob f71250204dfeceb51beeea5941ebbf3a33ce4b92
# scripts/stack-copy.sh HEAD blob 24bbb02a6aa8d7857da7be3b6815105a9fef629f NEW blob 24bbb02a6aa8d7857da7be3b6815105a9fef629f
P5 PASS
rc 0
@@ -0,0 +1,235 @@
// Merged window B+C, S1, S2 and D3 (repin-merged-bc.md "S1 and S2"): a read-only snapshot of every
// table in a bus store, so the 3h to 3i migration and a restore can be checked without the bus.
// OLD's broker doesn't take --tester on usage, names or threads.
//
// usage: node snapshot.mjs --store /abs/bus.sqlite (--held | --stopped) [--after STAMP] [--backup /abs/bus.sqlite]
// --held the running bus holds the store: a plain readOnly open, which adds no file to a WAL
// store that already has -wal and -shm (as stack-release.sh does for a held store).
// Refuses store-not-held when either is absent
// --stopped the bus is stopped: refuses store-unclean when -wal or -shm is present, else opens
// with immutable=1 and readOnly, which creates no file and leaves the store's mtime
// and sha256 alone
// --after also counts each timed table's rows with `at` after STAMP (mos-0838 D3, before a
// --restore). STAMP is a backup's STAMP file, in now_ms form (stack-release.sh:43,
// 2026-10-20T00:11:14.123Z). The count is a text compare, as the tool's own heldcount
// does, so every `at` in every timed table must have that form: a table with any
// other value refuses at-format and names the table (sage-1125)
// --backup also compares every table with a stack-release backup's bus.sqlite, which opens as
// --stopped opens a store: immutable=1 and readOnly, refusing backup-unclean when -wal
// or -shm is present, so nothing writes into the backup set
//
// It prints one JSON object: the schema version, then for every table in sqlite_master (type
// table, less sqlite_*) the row count, the max seq and a sha256 of the rows (in seq order, else in
// column order). `untimed` names the tables with no `at` column. For usage_records it adds per-run
// input and output sums, counted as `mosaic usage` counts them (usage.mjs turnRecords). messages
// and message_threads hold bodies and names, so they print counts only, no sha256 (sage-1125). It
// prints counts, hashes and table and meta key names only: no message body, no instance, project
// or thread name, no credential and no file content. It reads no config: the store path is always
// given.
//
// With --backup, each table gets `vsBackup` and `stopTables` names every table other than events
// whose rows differ from the backup's (sage-1123): a row with `at` after STAMP, a row past the
// backup's max seq, a changed row at or below it, a changed untimed table, or a table only one
// side has. meta counts only when a key other than schema_version and schema_digest changed (the
// 3h to 3i migration sets those two). Events are left to the tool's own --events N.
import { DatabaseSync } from 'node:sqlite';
import { createHash } from 'node:crypto';
import { existsSync, lstatSync } from 'node:fs';
import { isAbsolute } from 'node:path';
import { pathToFileURL } from 'node:url';
// usage_records is hashed on these columns in this order, never SELECT * (mos-0888 F1), as
// 3h-3i.sql:9 copies it. Every other table is hashed on its own columns in name order, so a
// rebuild that reorders columns keeps the hash. usage_records with any other column set refuses.
const USAGE_COLUMNS = ['seq', 'at', 'business', 'run', 'instance', 'provider', 'model', 'account', 'turn', 'source',
'phase', 'status', 'code', 'cursor', 'input', 'output', 'cache_write', 'cache_read'];
const COUNTS_ONLY = new Set(['messages', 'message_threads']);
const SCHEMA_KEYS = ['schema_digest', 'schema_version'];
const STAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
function refuse(code, detail) {
process.stderr.write(`snapshot: ${code}: ${detail}\n`);
process.exit(1);
}
function args(argv) {
const o = {};
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '--store' || a === '--after' || a === '--backup') {
if (i + 1 >= argv.length) refuse('usage', `${a} needs a value`);
o[a.slice(2)] = argv[++i];
} else if (a === '--held' || a === '--stopped') {
if (o.mode) refuse('usage', 'give one of --held and --stopped');
o.mode = a.slice(2);
} else refuse('usage', `unknown argument ${a}`);
}
if (!o.store || !o.mode) refuse('usage', 'node snapshot.mjs --store /abs/bus.sqlite (--held | --stopped) [--after STAMP] [--backup /abs/bus.sqlite]');
if (!isAbsolute(o.store)) refuse('usage', '--store must be an absolute path');
if (o.backup !== undefined && !isAbsolute(o.backup)) refuse('usage', '--backup must be an absolute path');
if (o.after !== undefined && !(STAMP.test(o.after) && Number.isFinite(Date.parse(o.after))))
refuse('usage', '--after must be a STAMP like 2026-10-20T00:11:14.123Z');
return o;
}
function file(path, code) {
if (!existsSync(path) || !lstatSync(path).isFile()) refuse(code, `${path} is not a file`);
}
function immutable(path, code) {
for (const f of [path + '-wal', path + '-shm'])
if (existsSync(f)) refuse(code, `${f} is present; an immutable read would skip the WAL`);
return new DatabaseSync(pathToFileURL(path).href + '?immutable=1', { readOnly: true });
}
function open({ store, mode }) {
file(store, 'store-missing');
if (mode === 'stopped') return immutable(store, 'store-unclean');
// A plain readOnly open of a WAL store with no -wal and -shm creates them, so a store that
// nothing holds refuses here instead of gaining two files.
for (const f of [store + '-wal', store + '-shm'])
if (!existsSync(f)) refuse('store-not-held', `${f} is absent; use --stopped for a store the bus doesn't hold`);
return new DatabaseSync(store, { readOnly: true });
}
const q = (name) => `"${name.replaceAll('"', '""')}"`;
const value = (v) => (typeof v === 'bigint' ? String(v) : v instanceof Uint8Array ? { hex: Buffer.from(v).toString('hex') } : v);
// The same records `mosaic usage` counts: per (run, turn, source) the latest row, an end row
// winning; a turn is known only when that row is an end row with status known.
function byRun(rows) {
const turns = new Map();
for (const row of rows) {
const key = JSON.stringify([row.business, row.run, row.turn, row.source]);
if (row.phase === 'end' || !turns.has(key)) turns.set(key, row);
}
const runs = new Map();
for (const row of turns.values()) {
const key = JSON.stringify([row.business, row.run]);
if (!runs.has(key)) runs.set(key, { business: row.business, run: row.run, input: 0n, output: 0n, unknown: new Set() });
const r = runs.get(key);
if (row.phase === 'end' && row.status === 'known') {
r.input += BigInt(row.input);
r.output += BigInt(row.output);
} else r.unknown.add(row.turn);
}
return [...runs.values()].map(({ unknown, input, output, ...r }) => ({
...r, input: String(input), output: String(output), unknownTurns: unknown.size,
}));
}
// meta is compared by key: the names of the keys whose value differs, never the values.
function metaKeys(db) {
return new Map(db.prepare('SELECT key, value FROM meta').all().map((r) => [String(r.key), String(r.value)]));
}
// One table: rows, max seq and sha256; rows after `after`; with `priorMax` (the backup's max
// seq) a second hash of the rows at or below it and the count of rows above it. The hashes stay
// inside: a COUNTS_ONLY table's `t` gets none.
function read(db, table, { after, priorMax } = {}) {
const cols = db.prepare('SELECT name FROM pragma_table_info(?)').all(table).map((c) => c.name);
if (!cols.length) refuse('table-missing', table);
let hashed = [...cols].sort();
if (table === 'usage_records') {
if (JSON.stringify(hashed) !== JSON.stringify([...USAGE_COLUMNS].sort()))
refuse('columns-changed', `usage_records has columns ${hashed.join(',')}`);
hashed = USAGE_COLUMNS;
}
const seq = cols.includes('seq');
const at = cols.includes('at');
const stmt = db.prepare(`SELECT ${hashed.map(q).join(',')} FROM ${q(table)} ORDER BY ${seq ? 'seq' : hashed.map(q).join(',')}`);
stmt.setReadBigInts(true);
const full = createHash('sha256');
const prior = createHash('sha256');
const limit = priorMax === undefined ? undefined : BigInt(priorMax ?? 0);
let rows = 0;
let rowsAfter = 0;
let badAt = 0;
let newRows = 0;
const kept = [];
for (const row of stmt.iterate()) {
rows++;
const line = JSON.stringify(hashed.map((c) => value(row[c]))) + '\n';
full.update(line);
if (seq && limit !== undefined) {
if (row.seq <= limit) prior.update(line);
else newRows++;
}
if (after !== undefined && at) {
if (typeof row.at !== 'string' || !STAMP.test(row.at)) badAt++;
else if (row.at > after) rowsAfter++;
}
if (table === 'usage_records') kept.push(row);
}
if (badAt) refuse('at-format', `${table} has ${badAt} row(s) whose at is not in the STAMP form, so a text compare with --after would miscount`);
const sha256 = full.digest('hex');
const t = { rows, maxSeq: seq ? (db.prepare(`SELECT max(seq) AS m FROM ${q(table)}`).get().m ?? null) : null };
if (!COUNTS_ONLY.has(table)) t.sha256 = sha256;
if (after !== undefined && at) t.rowsAfter = rowsAfter;
if (table === 'usage_records') t.byRun = byRun(kept);
return { t, sha256, cols: hashed, seq, at, newRows, priorSha: seq && limit !== undefined ? prior.digest('hex') : null };
}
// Tables only: no index, view or trigger, and none of SQLite's own sqlite_* tables.
const tablesOf = (db) => db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite\\_%' ESCAPE '\\' ORDER BY name")
.all().map((r) => r.name);
const o = args(process.argv.slice(2));
if (o.backup !== undefined) file(o.backup, 'backup-missing');
const db = open(o);
const bdb = o.backup === undefined ? null : immutable(o.backup, 'backup-unclean');
const out = {};
try {
db.exec('BEGIN');
out.schema = db.prepare("SELECT value FROM meta WHERE key='schema_version'").get()?.value ?? null;
const names = tablesOf(db);
const base = new Map();
if (bdb) {
out.backupSchema = bdb.prepare("SELECT value FROM meta WHERE key='schema_version'").get()?.value ?? null;
for (const name of tablesOf(bdb)) base.set(name, read(bdb, name));
}
out.tables = {};
out.untimed = [];
const stop = [];
for (const name of names) {
const b = base.get(name);
const r = read(db, name, { after: o.after, priorMax: b ? b.t.maxSeq : undefined });
if (!r.at) out.untimed.push(name);
if (bdb) {
const v = { backupRows: b ? b.t.rows : null };
let changed;
if (!b) changed = true;
else if (name === 'meta') {
const now = metaKeys(db);
const then = metaKeys(bdb);
v.keysChanged = [...new Set([...now.keys(), ...then.keys()])].filter((k) => now.get(k) !== then.get(k)).sort();
changed = v.keysChanged.some((k) => !SCHEMA_KEYS.includes(k));
} else if (r.seq) {
v.backupMaxSeq = b.t.maxSeq;
v.newRows = r.newRows;
v.priorSame = JSON.stringify(r.cols) === JSON.stringify(b.cols) && r.priorSha === b.sha256;
changed = r.newRows > 0 || !v.priorSame;
} else {
v.same = JSON.stringify(r.cols) === JSON.stringify(b.cols) && r.sha256 === b.sha256;
changed = !v.same;
}
if ((r.t.rowsAfter ?? 0) > 0) changed = true;
r.t.vsBackup = v;
if (changed && name !== 'events') stop.push(name);
}
out.tables[name] = r.t;
}
if (bdb) {
out.onlyInBackup = [...base.keys()].filter((n) => !names.includes(n));
out.stopTables = [...stop, ...out.onlyInBackup].sort();
}
if (o.after !== undefined) {
out.after = o.after;
out.eventsAfter = out.tables.events?.rowsAfter ?? null;
}
} finally {
try { db.exec('ROLLBACK'); } catch {}
db.close();
bdb?.close();
}
process.stdout.write(JSON.stringify(out, null, 2) + '\n');
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
# Re-pin 4429a104f window step wrapper (copied from Darkwing's window-rows-177-185/w.sh): w.sh FILE TITLE CMD... runs CMD in REPO under a clean env (no ANTHROPIC_*, CLAUDE_*,
# MOSAIC_* or pi variables), appends the title, command, output, exit code and date -u stamps to window/FILE,
# prints the output, and exits with CMD's exit code.
set -uo pipefail
W=/mnt/storage/src/mosaic-stack/agents/sage/work/window-repin-4429a104f
f="$W/$1"; title="$2"; shift 2
E=(env -i HOME="$HOME" PATH="$HOME/.local/bin:/usr/local/bin:/usr/bin:/bin" LANG=C.UTF-8
XDG_RUNTIME_DIR="$XDG_RUNTIME_DIR" DBUS_SESSION_BUS_ADDRESS="${DBUS_SESSION_BUS_ADDRESS:-unix:path=$XDG_RUNTIME_DIR/bus}")
t0=$(date -u +%FT%T.%3NZ)
{ echo; echo "## $title"; echo "# start $t0"; echo "# command: (cd \$REPO && <clean env> $*)"; } >> "$f"
out=$(cd /mnt/storage/src/mosaic-stack && "${E[@]}" "$@" 2>&1); rc=$?
printf '%s\n' "$out" >> "$f"
echo "# exit $rc, end $(date -u +%FT%T.%3NZ)" >> "$f"
printf '%s\n# exit %s\n' "$out" "$rc"
exit $rc
@@ -0,0 +1,9 @@
npm warn deprecated [email protected]: Use your platform's native DOMException instead
added 128 packages in 2s
npm warn install-scripts 3 packages had install scripts blocked because they are not covered by allowScripts:
npm warn install-scripts @google/[email protected] (preinstall: echo 'preinstall: no-op')
npm warn install-scripts [email protected] (postinstall: node install.js)
npm warn install-scripts [email protected] (postinstall: node scripts/postinstall)
npm warn install-scripts
npm warn install-scripts Run `npm install-scripts ls` to review, or `npm install-scripts approve <pkg>` to allow.
@@ -0,0 +1 @@
copied: /home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0

Some files were not shown because too many files have changed in this diff Show More