Compare commits

..
213 changed files with 1923 additions and 25914 deletions
+6 -11
View File
@@ -25,15 +25,10 @@ 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**: 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).
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).
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
@@ -182,8 +177,8 @@ Full reference — usage, fields, exit codes, safety notes:
## Data map (canon)
- `~/.config/mosaic-dev/config.json`, or `~/.config/mosaic-NAME/config.json`
per named instance: system config (user-authored; never auto-written).
- `~/.config/mosaic-dev/config.json` — 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
-1
View File
@@ -5174,4 +5174,3 @@ 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.
-104
View File
@@ -1,104 +0,0 @@
# 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.
-216
View File
@@ -1,216 +0,0 @@
# 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
@@ -1,416 +0,0 @@
# 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
@@ -1,75 +0,0 @@
# 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.
-105
View File
@@ -1,105 +0,0 @@
# 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 bounded wait at :311 for one allowed
event) 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.
- 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.
-8
View File
@@ -1,8 +0,0 @@
# 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).
@@ -1,35 +0,0 @@
{
"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
@@ -1,37 +0,0 @@
[
{
"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
@@ -1,13 +0,0 @@
[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
@@ -1,8 +0,0 @@
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
@@ -1,5 +0,0 @@
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
-21
View File
@@ -1,21 +0,0 @@
# 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.
@@ -1,23 +0,0 @@
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
@@ -1,11 +0,0 @@
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,37 +196,3 @@ 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).
@@ -1,27 +0,0 @@
{
"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": []
}
@@ -1,51 +0,0 @@
// 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);
@@ -1,59 +0,0 @@
// 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);
@@ -1,60 +0,0 @@
// 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);
@@ -1,53 +0,0 @@
// 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);
@@ -1,66 +0,0 @@
// 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);
@@ -1,66 +0,0 @@
// 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));
@@ -1,41 +0,0 @@
{
"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": []
}
@@ -1,13 +0,0 @@
{
"at": "2026-10-20T21:35:49.240Z",
"comments": [
{
"task": 412,
"status": 201,
"id": 395,
"readBack": 200,
"found": true
}
],
"problems": []
}
@@ -1,20 +0,0 @@
{
"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": []
}
@@ -1,19 +0,0 @@
{
"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": []
}
@@ -1,16 +0,0 @@
// 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);
}
@@ -1,34 +0,0 @@
{
"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": []
}
@@ -1,74 +0,0 @@
# 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:
210 (r4 due 10-21), 237, 194, 234.
- 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 c8bc5cf4c)
`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 | open | 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 | open | Publish busy check (199 N1). |
| 234 | open | Machine grant row a and the 3i to 3j schema step. `--plan` must say `migration: 3i -> 3j`. |
| PRD 1.7+1.8 | open | `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.
- 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, written once both have landed.
@@ -1,125 +0,0 @@
# 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.
@@ -1,3 +0,0 @@
chief rbb708846b605 claude-code since 2026-10-20T19:29:59.053Z
pm rc36b17ef3788 claude-code since 2026-10-20T20:20:56.278Z
# exit 0
@@ -1,256 +0,0 @@
{
"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
@@ -1,200 +0,0 @@
{
"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
@@ -1,8 +0,0 @@
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
@@ -1,7 +0,0 @@
# 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
@@ -1,6 +0,0 @@
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
@@ -1,12 +0,0 @@
● 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
@@ -1,5 +0,0 @@
ActiveState=active
SubState=running
ActiveEnterTimestamp=Tue 2026-10-13 08:40:19 CDT
MainPID=1317
# exit 0
@@ -1,6 +0,0 @@
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
@@ -1,2 +0,0 @@
chief r097436c91816 claude-code since 2026-10-20T22:02:08.951Z
# exit 0
@@ -1,7 +0,0 @@
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
@@ -1,19 +0,0 @@
[
{
"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
}
]
@@ -1,2 +0,0 @@
r097436c91816 mosaic-stack/chief claude-code claude-opus-5-5 by jason since 2026-10-20T22:02:08.941Z running
# exit 0
@@ -1,2 +0,0 @@
[]
# exit 0
@@ -1,2 +0,0 @@
created project window-20261020-2205 id 77e7b45f-8c34-49a3-b2fa-9cf565177c30
# exit 0
@@ -1 +0,0 @@
1792533965
@@ -1,21 +0,0 @@
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
@@ -1,4 +0,0 @@
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
@@ -1,4 +0,0 @@
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
@@ -1,32 +0,0 @@
[
{
"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
}
]
@@ -1,2 +0,0 @@
created window-20261020-2205/w9 id 446201de-e8b5-4df3-9356-d0d5422cc899 instance assistant
# exit 0
@@ -1,3 +0,0 @@
window-20261020-0801/w9 assistant d0ba7a66
window-20261020-2205/w9 assistant 446201de
# exit 0
@@ -1 +0,0 @@
446201de-e8b5-4df3-9356-d0d5422cc899
@@ -1,165 +0,0 @@
{
"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
@@ -1,2 +0,0 @@
r097436c91816 mosaic-stack/chief claude-code claude-opus-5-5 by jason since 2026-10-20T22:02:08.941Z running
# exit 0
@@ -1,19 +0,0 @@
[
{
"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
}
]
@@ -1,153 +0,0 @@
{
"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"
]
}
@@ -1,167 +0,0 @@
{
"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"
]
}
@@ -1,193 +0,0 @@
# 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
@@ -1,116 +0,0 @@
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
@@ -1,21 +0,0 @@
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
@@ -1,8 +0,0 @@
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
@@ -1,2 +0,0 @@
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
@@ -1,7 +0,0 @@
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
@@ -1,8 +0,0 @@
# 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
@@ -1,4 +0,0 @@
copy 490e2771d: roles 6, projects 1, arbiters 2
HOLDS chief instance.name yes
HOLDS pm instance.name no
LOAD_OK
@@ -1,19 +0,0 @@
{
"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"
}
@@ -1,35 +0,0 @@
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
@@ -1 +0,0 @@
2026-10-20T21:52:34Z
@@ -1,10 +0,0 @@
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
@@ -1,7 +0,0 @@
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.
@@ -1 +0,0 @@
rc=0
@@ -1,20 +0,0 @@
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
@@ -1 +0,0 @@
2026-10-20T21:52:31Z
@@ -1,7 +0,0 @@
# 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
@@ -1,67 +0,0 @@
#!/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
@@ -1,9 +0,0 @@
# 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
@@ -1,235 +0,0 @@
// 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');
@@ -1,16 +0,0 @@
#!/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
@@ -1,9 +0,0 @@
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.
@@ -1 +0,0 @@
copied: /home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0
@@ -1,16 +0,0 @@
start 2026-10-20T21:45:57Z
stack-copy rc=0
copied: /home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0
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.
oauth4webapi 3.8.8
IMPORT_OK
LOCK_MATCH
163M /home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0
/dev/nvme1n1p5 458G 405G 30G 94% /home
copy 4429a104f: roles 6, projects 1, arbiters 2
HOLDS chief instance.name yes
HOLDS pm instance.name no
LOAD_OK
end 2026-10-20T21:45:59Z
@@ -1,109 +0,0 @@
// W10 of repin-4429a104f.md (e1272d1a), adapted from Darkwing's window-rows-177-185/w10-browser.mjs: the tester listener
// on 127.0.0.1:7333 through the webui browser harness (packages/webui/tests/browser.mjs, same blob at HEAD and NEW). Reads only: before any page
// script runs, fetch and XMLHttpRequest are wrapped so every call is logged and any method other than
// GET or HEAD is refused in the page, and the script submits no form and clicks no button. The chat
// check reads the #/chat list only and never opens the W9 thread page (the launcher reads lastOpen).
import { browser } from '/mnt/storage/src/mosaic-stack/packages/webui/tests/browser.mjs';
const OUT = '/mnt/storage/src/mosaic-stack/agents/sage/work/window-repin-4429a104f';
const BASE = 'http://127.0.0.1:7333/';
const W9_THREAD = '446201de-e8b5-4df3-9356-d0d5422cc899';
const GUARD = `(() => {
const log = window.__w10 = [];
const ok = m => m === 'GET' || m === 'HEAD';
const f = window.fetch;
window.fetch = (input, init = {}) => {
const method = String(init.method || (input instanceof Request ? input.method : 'GET')).toUpperCase();
const url = String(input instanceof Request ? input.url : input);
log.push({ method, url, refused: !ok(method) });
return ok(method) ? f(input, init) : Promise.reject(new TypeError('w10 refused ' + method));
};
const open = XMLHttpRequest.prototype.open, send = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function (method, url, ...rest) { this.__m = String(method).toUpperCase(); this.__u = String(url); return open.call(this, method, url, ...rest); };
XMLHttpRequest.prototype.send = function (...a) { log.push({ method: this.__m, url: this.__u, xhr: true, refused: !ok(this.__m) }); if (!ok(this.__m)) throw new TypeError('w10 refused ' + this.__m); return send.apply(this, a); };
})();`;
const b = await browser({ machine: false });
const result = {};
let failed = false;
const check = (name, pass, detail) => { result[name] = { pass, ...(detail === undefined ? {} : { detail }) }; if (!pass) failed = true; };
try {
await b.call('Page.addScriptToEvaluateOnNewDocument', { source: GUARD });
await b.viewport(1280, 1600);
const wait = expr => b.evaluate(`(async()=>{for(let n=0;n<300;n++){if(${expr})return true;await new Promise(r=>setTimeout(r,50));}return false;})()`);
await b.navigate(BASE + '#/settings');
const ready = await wait('!!document.querySelector("#tester-banner") && !!document.querySelector("#routes-form") && !!document.querySelector(".tester-name-form")');
check('settings loaded', ready);
const s = await b.evaluate(`(() => {
const q = x => document.querySelector(x);
return {
banner: q('#tester-banner')?.textContent.trim() ?? null,
h1: q('#bus-view h1')?.textContent.trim() ?? null,
forms: [...document.querySelectorAll('form')].map(f => f.id || f.className),
convFormHidden: q('#conv-form')?.hidden ?? null,
panels: [...document.querySelectorAll('#bus-view h2')].map(h => h.textContent.trim()),
instancesTable: !!q('#instances-table'),
nameForms: document.querySelectorAll('.tester-name-form').length,
routesForm: !!q('#routes-form'),
routesSubmitEnabled: q('#routes-form button[type=submit]')?.disabled === false,
forbidden: [...document.querySelectorAll('#launch-settings-form, .assistant-form, #token-limits-table [data-limit-reset], #send-section')].map(e => e.id || e.className || e.tagName),
tokenLimitsPanel: !!q('#token-limits-title') || !!q('#token-limits-table'),
};
})()`);
result.settings = s;
check('tester banner', /tester/i.test(s.banner ?? ''), s.banner);
check('instance, routes and name editors present', s.instancesTable && s.routesForm && s.nameForms > 0);
check('only tester forms (conv-form is index.html:21, global and hidden)', s.convFormHidden === true && s.forms.every(f => f === 'conv-form' || f === 'routes-form' || /tester-name-form/.test(f) || /^instance-(add|change)-form$/.test(f)), s.forms);
check('no launch settings, assistant, limit reset or send', s.forbidden.length === 0, s.forbidden);
check('no Token limits panel (plan W10 item 2, amended)', s.tokenLimitsPanel === false);
await b.screenshot(`${OUT}/w10-settings.png`);
await b.navigate(BASE + '#/');
// The Board is index.html:22 #board-view; bus.js hides #bus-view on it.
const board = await wait('document.querySelector("#bus-view")?.hidden === true && !!document.querySelector("#board-view #waiting-title")');
await new Promise(r => setTimeout(r, 1500));
const bd = await b.evaluate(`(() => { const v = document.querySelector('#board-view'); return {
busViewHidden: document.querySelector('#bus-view')?.hidden ?? null,
banner: !!document.querySelector('#tester-banner'),
headings: [...v.querySelectorAll('h2')].map(h => h.textContent.replace(/\\s+/g, ' ').trim()),
unavailable: /unavailable|refused|not authorized/i.test(v.textContent),
length: v.textContent.length,
forms: [...document.querySelectorAll('form')].map(f => f.id || f.className),
}; })()`);
result.board = bd;
check('board reads', board && bd.busViewHidden === true && !bd.unavailable && bd.headings.length > 0, bd);
await b.screenshot(`${OUT}/w10-board.png`);
await b.navigate(BASE + '#/chat');
// chat.js threads(): ul.chat-thread-list with <a href="#/chat/threads/<id>">. Read only; no link is followed.
const chatReady = await wait('!!document.querySelector("ul.chat-thread-list") || /No threads yet/.test(document.body.textContent)');
const c = await b.evaluate(`(() => {
const list = document.querySelector('ul.chat-thread-list');
return {
hash: location.hash,
banner: !!document.querySelector('#tester-banner'),
threads: list ? [...list.querySelectorAll('li')].map(li => ({ href: li.querySelector('a')?.getAttribute('href') ?? null, name: li.querySelector('a')?.textContent.trim() ?? null, instance: li.querySelector('.muted')?.textContent.trim() ?? null })) : [],
conversationOpen: !!document.querySelector('.chat-conversation'),
forms: [...document.querySelectorAll('form')].map(f => f.id || f.className),
};
})()`);
result.chat = c;
check('chat lists the W9 thread', chatReady && c.threads.some(t => t.href === '#/chat/threads/' + W9_THREAD), c.threads);
check('W9 thread page not opened', c.hash === '#/chat' && c.conversationOpen === false, c.hash);
check('tester banner on chat', c.banner);
await b.screenshot(`${OUT}/w10-chat.png`);
const log = await b.evaluate('window.__w10');
result.requests = log;
check('no write through the listener', log.every(r => !r.refused && (r.method === 'GET' || r.method === 'HEAD')), log.filter(r => r.refused));
check('no request to the W9 thread messages or open', !log.some(r => r.url.includes(W9_THREAD)), log.filter(r => r.url.includes(W9_THREAD)));
} catch (e) {
failed = true;
result.error = String(e?.message ?? e);
} finally {
await b.close();
}
console.log(JSON.stringify(result, null, 2));
console.log(failed ? 'W10 browser: FAIL' : 'W10 browser: pass');
process.exit(failed ? 1 : 0);
@@ -1,30 +0,0 @@
// W10 diagnosis after the two failed checks of w10-browser.mjs (sign-out form listed; chat list missing the W9 thread).
// Read only, with the same GET/HEAD guard as w10-browser.mjs. Records each form's hidden flag on Settings, the /api/config
// sso value the page sees, and every ul.chat-thread-list on #/chat (chat.js:93 renders one per project). No screenshots.
import { browser } from '/mnt/storage/src/mosaic-stack/packages/webui/tests/browser.mjs';
import { readFileSync } from 'node:fs';
const src = readFileSync(new URL('./w10-browser.mjs', import.meta.url), 'utf8');
const GUARD = eval(src.slice(src.indexOf('const GUARD = ') + 14, src.indexOf('`;', src.indexOf('const GUARD = ')) + 1));
const BASE = 'http://127.0.0.1:7333/';
const W9_THREAD = '446201de-e8b5-4df3-9356-d0d5422cc899';
const b = await browser({ machine: false });
const out = {};
try {
await b.call('Page.addScriptToEvaluateOnNewDocument', { source: GUARD });
const wait = expr => b.evaluate(`(async()=>{for(let n=0;n<300;n++){if(${expr})return true;await new Promise(r=>setTimeout(r,50));}return false;})()`);
await b.navigate(BASE + '#/settings');
await wait('!!document.querySelector("#routes-form")');
out.config = await b.evaluate(`fetch('/api/config').then(r=>r.json()).then(j=>({sso:j.sso??null}))`);
out.forms = await b.evaluate(`[...document.querySelectorAll('form')].map(f=>({id:f.id||f.className,hidden:f.hidden,action:f.getAttribute('action'),method:f.getAttribute('method')}))`);
await b.navigate(BASE + '#/chat');
await wait('document.querySelectorAll("ul.chat-thread-list").length >= 1');
await new Promise(r => setTimeout(r, 1000));
out.chat = await b.evaluate(`({hash:location.hash,open:!!document.querySelector('.chat-conversation'),projects:[...document.querySelectorAll('.chat-projects section.panel')].map(s=>({project:s.querySelector('h2')?.textContent.trim(),threads:[...s.querySelectorAll('ul.chat-thread-list li a')].map(a=>a.getAttribute('href'))}))})`);
out.requests = await b.evaluate('window.__w10');
} finally { await b.close(); }
const signOut = out.forms.find(f => f.id === 'sign-out');
const listed = out.chat.projects.some(p => p.threads.includes('#/chat/threads/' + W9_THREAD));
const writes = out.requests.filter(r => r.refused || !['GET', 'HEAD'].includes(r.method));
console.log(JSON.stringify(out, null, 2));
console.log(`sign-out hidden ${signOut?.hidden}, sso ${out.config.sso}; W9 thread listed ${listed}; thread page open ${out.chat.open}; writes ${writes.length}`);
process.exit(signOut?.hidden === true && listed && !out.chat.open && writes.length === 0 ? 0 : 1);
@@ -1,277 +0,0 @@
# W9 go mos-1114, start 2026-10-20T22:05:51Z
# plan e1272d1a, tool blob 3b068b1546d4, console-check.sh d068fe02, wrapper on NEW: checked
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
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
# journal since W5: proxy proof off 1, Sign-in 0
# Console pid 2950925 cwd /home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0
r097436c91816 mosaic-stack/chief claude-code claude-opus-5-5 by jason since 2026-10-20T22:02:08.941Z running
# exit 0
[
{
"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
}
]
# exit 0
# one running chief from this start: run r097436c91816
chief r097436c91816 claude-code since 2026-10-20T22:02:08.951Z
# exit 0
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
created project window-20261020-2205 id 77e7b45f-8c34-49a3-b2fa-9cf565177c30
# exit 0
created window-20261020-2205/w9 id 446201de-e8b5-4df3-9356-d0d5422cc899 instance assistant
# exit 0
# project window-20261020-2205, thread 446201de-e8b5-4df3-9356-d0d5422cc899
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
# reply received 2026-10-20T22:06:05Z
[
{
"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
}
]
# exit 0
# thread read: 2 messages: tester->assistant REQUEST, assistant->tester RESULT
window-20261020-0801/w9 assistant d0ba7a66
window-20261020-2205/w9 assistant 446201de
# exit 0
[]
# exit 0
{
"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
# human-thread probe: skipped. S1 has 1 threads row, and threads --tester lists the 177-185 window's tester thread d0ba7a66, so no human thread exists to read
# W9 checks and turns end 2026-10-20T22:06:06Z; the assistant run wait and S2 are w9b.sh
@@ -1,93 +0,0 @@
#!/usr/bin/env bash
# Re-pin 4429a104f, W9 checks and the turns (repin-4429a104f.md at f41729178, sha256 e1272d1a; the W9 list of
# repin-rows-177-185.md plus the journal Sign-in line and the Console cwd), on Mos's go mos-1114.
# Adapted from Darkwing's window-rows-177-185/w9.sh. Every call goes through w.sh into W.txt; every bus call carries
# --tester. A failed check stops the script. The wait for the assistant run and S2 are in w9b.sh, so the interim
# report to Mos goes out between the two (report rule, waits over 5 minutes).
# usage: w9.sh
set -uo pipefail
REPO=/mnt/storage/src/mosaic-stack
WIN=$REPO/agents/sage/work/window-repin-4429a104f
NEW=4429a104f6d65ccf165f6c83b5d1dce3147dfde0
W5_STAMP="2026-10-20 22:02:06 UTC"
W5_MS=$(date -d "2026-10-20T22:02:06Z" +%s)000
PLAN=$REPO/agents/sage/work/window-plan/repin-4429a104f.md
PLAN_SHA=e1272d1a391e00fb1291d47da9973c0ba342d65fcd42f583ea007e883c1f32b9
TOOL_BLOB=3b068b1546d4020cffde5d73929d9130afe91911
CC=/mnt/storage/scratch/sage-pin-de352dfaf/console-check.sh
CC_SHA=d068fe02da897b700110a162d13d9671fe5994f053c45aff99113fade737094c
CONSOLE_PID=2950925
PRIOR_TID=d0ba7a66-aa3f-4918-9518-94132bce6b28
MOSAIC=$HOME/.npm-global/bin/mosaic
C_NEW=(node "$HOME/.local/share/mosaic-stack/copies/$NEW/packages/cli/src/cli.mjs")
BZ=(--business mosaic-stack)
W=$WIN/W.txt
note() { echo "# $*" >> "$W"; echo "# $*"; }
stop() { note "STOP: $*"; exit 1; }
sha() { sha256sum < "$1" | cut -c1-64; }
step() { local f="$1"; shift; $WIN/w.sh W.txt "$@" > "$WIN/$f"; local rc=$?; cat "$WIN/$f"; return $rc; }
note "W9 go mos-1114, start $(date -u +%FT%TZ)"
[ "$(sha $PLAN)" = "$PLAN_SHA" ] || stop "the plan file is not e1272d1a"
[ "$(git -C $REPO rev-parse HEAD:scripts/stack-release.sh)" = "$TOOL_BLOB" ] || stop "tool blob moved"
[ "$(sha $CC)" = "$CC_SHA" ] || stop "console-check.sh is not d068fe02 (SCRIPTS.sha256)"
grep -q "/copies/$NEW/scripts/mosaic" "$MOSAIC" || stop "the wrapper is not pinned to NEW"
note "plan e1272d1a, tool blob 3b068b1546d4, console-check.sh d068fe02, wrapper on NEW: checked"
# checks
step .w9-status.txt "W9 status" scripts/stack-release.sh status mosaic-stack || stop "W9 status exit"
grep -q "\"copy\":\"$NEW\"" $WIN/.w9-status.txt && grep -q '"pm":true' $WIN/.w9-status.txt && grep -q '"schema":"3i"' $WIN/.w9-status.txt || stop "W9 status: pointer not on NEW, pm true, 3i"
grep -q 'unit flags: match the pointer' $WIN/.w9-status.txt || stop "W9 status: unit flags"
step .w9-cc.txt "W9 console-check NEW ($CC, d068fe02)" bash $CC $NEW || stop "console-check exit"
grep -qx CONSOLE_PASS $WIN/.w9-cc.txt || stop "console-check not CONSOLE_PASS"
J=$(journalctl --user -u mosaic-webui.service --since "$W5_STAMP" --no-pager)
N=$(printf '%s\n' "$J" | grep -c 'Proxy: proxy proof off'); S=$(printf '%s\n' "$J" | grep -c 'Sign-in:')
{ echo; echo "## W9 webui journal since $W5_STAMP: 'Proxy: proxy proof off' and 'Sign-in:' counts"; echo "proxy proof off $N, Sign-in $S"; } >> "$W"
note "journal since W5: proxy proof off $N, Sign-in $S"
[ "$N" = 1 ] || stop "proxy proof off count is $N, not 1"
[ "$S" = 0 ] || stop "Sign-in count is $S, not 0"
systemctl --user show -p MainPID --value mosaic-webui.service | grep -qx "$CONSOLE_PID" || stop "Console MainPID is not $CONSOLE_PID"
CWD=$(readlink /proc/$CONSOLE_PID/cwd)
{ echo; echo "## W9 Console pid $CONSOLE_PID cwd"; ls -l /proc/$CONSOLE_PID/cwd | sed 's/^.* -> /-> /'; } >> "$W"
note "Console pid $CONSOLE_PID cwd $CWD"
[ "$CWD" = "$HOME/.local/share/mosaic-stack/copies/$NEW" ] || stop "Console cwd is not the NEW copy"
step .w9-launches.txt "W9 launches list" $MOSAIC launches list || stop "launches list"
step .w9-launches.json "W9 launches list --json" $MOSAIC launches list --json || stop "launches list --json"
sed -i '$d' $WIN/.w9-launches.json
CHIEF=$(node -e '
const l=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8")),since=Number(process.argv[2]);
const c=l.filter(x=>x.business==="mosaic-stack"&&x.instance==="chief"&&x.live===true);
if(c.length!==1||!(Date.parse(c[0].startedAt)>=since)){console.error(`running chiefs ${c.length}`);process.exit(1)}
process.stdout.write(c[0].run)' $WIN/.w9-launches.json "$W5_MS") || stop "launches: not one running chief from this start"
note "one running chief from this start: run $CHIEF"
step .w9-agents.txt "W9 agents --tester" $MOSAIC agents "${BZ[@]}" --tester || stop "agents"
grep -E '(^|[[:space:]])chief[[:space:]].*working' $WIN/.w9-agents.txt && stop "the chief is working"
# turns
step .w9-talk-chief.txt "W9 talk chief --tester" $MOSAIC talk chief "[mos test] re-pin $NEW: reply with one line, no staffing request" "${BZ[@]}" --tester || stop "talk chief"
PROJ=window-$(date -u +%Y%m%d-%H%M)
step .w9-project.txt "W9 project create $PROJ --tester" $MOSAIC project create $PROJ "${BZ[@]}" --tester || stop "project create"
step .w9-thread.txt "W9 thread create $PROJ w9 --tester" $MOSAIC thread create $PROJ w9 "${BZ[@]}" --tester || stop "thread create"
TID=$(sed -n "s/^created $PROJ\/w9 id \([0-9a-f-]*\) instance .*$/\1/p" $WIN/.w9-thread.txt)
[ -n "$TID" ] || stop "no thread id"
note "project $PROJ, thread $TID"
echo "$TID" > $WIN/.w9-tid.txt
step .w9-talk-thread.txt "W9 talk --thread --wait 180 --tester" $MOSAIC talk --thread "$TID" "[mos test] reply with one line, then stop" --wait 180 "${BZ[@]}" --tester || stop "talk --thread"
note "reply received $(date -u +%FT%TZ)"
date -u +%s > $WIN/.w9-reply-at.txt
step .w9-thread-read.json "W9 thread read --json --tester" $MOSAIC thread read "$TID" --json "${BZ[@]}" --tester || stop "thread read"
sed -i '$d' $WIN/.w9-thread-read.json
node -e '
const t=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));
console.log(`# thread read: ${t.length} messages: ${t.map(m=>`${m.from_role}->${m.to_role} ${m.class}`).join(", ")}`);
process.exit(t.length===2&&t[0].from_role==="tester"&&t[1].from_role==="assistant"?0:1)' $WIN/.w9-thread-read.json | tee -a "$W" || stop "thread read is not the tester message plus one assistant reply"
step .w9-threads.txt "W9 threads --tester" $MOSAIC threads "${BZ[@]}" --tester || stop "threads"
grep -q "$TID\|$PROJ" $WIN/.w9-threads.txt || stop "threads does not list the W9 thread"
step .w9-names.json "W9 C_NEW names --tester --json" "${C_NEW[@]}" names "${BZ[@]}" --tester --json || stop "names"
step .w9-usage.json "W9 usage --tester --json" $MOSAIC usage "${BZ[@]}" --tester --json || stop "usage"
if grep -q "${PRIOR_TID:0:8}" $WIN/.w9-threads.txt; then
note "human-thread probe: skipped. S1 has 1 threads row, and threads --tester lists the 177-185 window's tester thread ${PRIOR_TID:0:8}, so no human thread exists to read"
else
note "human-thread probe: S1's one thread is not in threads --tester; probe it by hand before S2"
fi
note "W9 checks and turns end $(date -u +%FT%TZ); the assistant run wait and S2 are w9b.sh"
@@ -1,185 +0,0 @@
# W9b start 2026-10-20T22:06:27Z: waiting for the launcher to end r497f1c817d4d (thread 446201de-e8b5-4df3-9356-d0d5422cc899), no stop by hand
# r497f1c817d4d not live in launches list --json by 2026-10-20T22:21:07Z (5 s poll)
r097436c91816 mosaic-stack/chief claude-code claude-opus-5-5 by jason since 2026-10-20T22:02:08.941Z running
# exit 0
## W9b launches log lines for r497f1c817d4d (at, event, instance, thread, run, reason, exitCode, signal only)
at=2026-10-20T22:06:00.766Z event=launch instance=assistant thread=446201de-e8b5-4df3-9356-d0d5422cc899 run=r497f1c817d4d
at=2026-10-20T22:21:05.149Z event=thread-idle instance=assistant thread=446201de-e8b5-4df3-9356-d0d5422cc899 run=r497f1c817d4d
at=2026-10-20T22:21:05.171Z event=end instance=assistant thread=446201de-e8b5-4df3-9356-d0d5422cc899 run=r497f1c817d4d reason=stopped exitCode=0 signal=null
## W9b bus and webui journal since 2026-10-20 22:05:00 UTC, lines naming r497f1c817d4d or thread-idle
2026-10-20T17:21:05.171444-05:00 dragon-lin mosaic[2950923]: mosaic-bus: run r497f1c817d4d (assistant) ended: stopped
(grep exit 0)
# SNAP sha256 639fd28a checked before S2 2026-10-20T22:21:07Z
{
"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"
]
}
# exit 0
# S2.json sha256 1a444f5ac0049bc78e397b6928e1008cfe8cbc30989bbf2042b4ed27f56a4173
# W9b end 2026-10-20T22:21:07Z
rc=0
@@ -1,57 +0,0 @@
#!/usr/bin/env bash
# Re-pin 4429a104f, W9 tail (adapted from Darkwing's window-rows-177-185/w9b.sh, under mos-0948's ruling for that window: the launcher
# ends the assistant run at threadIdleSeconds 900 after the 22:06:05Z reply, about 22:21:05Z). Nothing is stopped by hand. Report if live at 22:27Z.
# usage: w9b.sh
set -uo pipefail
REPO=/mnt/storage/src/mosaic-stack
WIN=/mnt/storage/src/mosaic-stack/agents/sage/work/window-repin-4429a104f
RUN=r497f1c817d4d
TID=446201de-e8b5-4df3-9356-d0d5422cc899
DATA=/home/jwoltje/.mosaic-dev
SNAP=$WIN/snapshot.mjs
SNAP_SHA=639fd28a2674aaa03ce824e3394b4052f21cdef7d6fa49bae61a9927ed341e67
PLAN=$REPO/agents/sage/work/window-plan/repin-4429a104f.md
PLAN_SHA=e1272d1a391e00fb1291d47da9973c0ba342d65fcd42f583ea007e883c1f32b9
TOOL_BLOB=3b068b1546d4020cffde5d73929d9130afe91911
MOSAIC=$HOME/.npm-global/bin/mosaic
DEADLINE=$(date -d "2026-10-20T22:27:00Z" +%s)
MARK18=$(date -d "2026-10-20T22:22:00Z" +%s)
W=$WIN/W.txt
note() { echo "# $*" >> "$W"; echo "# $*"; }
stop() { note "STOP: $*"; exit 1; }
sha() { sha256sum < "$1" | cut -c1-64; }
step() { local f="$1"; shift; $WIN/w.sh W.txt "$@" > "$WIN/$f"; local rc=$?; cat "$WIN/$f"; return $rc; }
# launches log lines for the run: at, event, instance, thread, run, reason, exitCode, signal only
runlog() { node -e '
const keep=["at","event","instance","thread","run","reason","exitCode","signal"];
for(const s of require("fs").readFileSync(process.argv[1],"utf8").trim().split("\n")){const o=JSON.parse(s);if(o.run!==process.argv[2])continue;
console.log(keep.filter(k=>k in o).map(k=>`${k}=${o[k]}`).join(" "))}' "$DATA/launches/mosaic-stack.jsonl" "$RUN"; }
note "W9b start $(date -u +%FT%TZ): waiting for the launcher to end $RUN (thread $TID), no stop by hand"
[ "$(sha $PLAN)" = "$PLAN_SHA" ] || stop "the plan file is not e1272d1a"
[ "$(git -C $REPO rev-parse HEAD:scripts/stack-release.sh)" = "$TOOL_BLOB" ] || stop "tool blob moved"
END=; M18=
while :; do
$MOSAIC launches list --json > $WIN/.w9b-poll.json 2>&1 || stop "launches poll"
LIVE=$(node -e 'const l=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));process.stdout.write(l.filter(x=>x.run===process.argv[2]&&x.live===true).map(x=>x.run).join(" "))' $WIN/.w9b-poll.json $RUN)
[ -z "$LIVE" ] && { END=$(date -u +%FT%TZ); break; }
NOW=$(date +%s)
if [ -z "$M18" ] && [ $NOW -ge $MARK18 ]; then M18=1; note "22:22Z mark $(date -u +%FT%TZ): $RUN still live; launches log lines for it:"; runlog | tee -a "$W"; fi
[ $NOW -ge $DEADLINE ] && break
sleep 5
done
if [ -z "$END" ]; then
note "STOP: $RUN still live at $(date -u +%FT%TZ) (22:27Z mark); a stop is Mos's call. S2 waits."
runlog | tee -a "$W"
exit 4
fi
note "$RUN not live in launches list --json by $END (5 s poll)"
step .w9b-launches-after.txt "W9b launches list after the assistant run" $MOSAIC launches list
{ echo; echo "## W9b launches log lines for $RUN (at, event, instance, thread, run, reason, exitCode, signal only)"; runlog; } | tee -a "$W"
{ echo; echo "## W9b bus and webui journal since 2026-10-20 22:05:00 UTC, lines naming $RUN or thread-idle"; journalctl --user -u [email protected] -u mosaic-webui.service --since "2026-10-20 22:05:00 UTC" --no-pager -o short-iso-precise | grep -E "$RUN|thread-idle|threadIdle"; echo "(grep exit ${PIPESTATUS[1]})"; } | tee -a "$W"
[ "$(sha $SNAP)" = "$SNAP_SHA" ] || stop "SNAP sha before S2"
note "SNAP sha256 639fd28a checked before S2 $(date -u +%FT%TZ)"
step S2.json "S2 snapshot --held" node $SNAP --store "$DATA/bus/bus.sqlite" --held || stop "S2"
sed -i '$d' $WIN/S2.json
note "S2.json sha256 $(sha $WIN/S2.json)"
note "W9b end $(date -u +%FT%TZ)"
@@ -1,43 +0,0 @@
# W0b-W4 start 2026-10-20T22:00:49Z
# plan e1272d1a, tool blob 3b068b1546d4, P5 four scripts at NEW and clean, business-check.mjs eece4d46: checked 2026-10-20T22:00:49Z
# W0 copy present, w0.txt LOAD_OK
# W0b: /home/jwoltje/.config/mosaic-dev/businesses/mosaic-stack.json.pre-window-20261020T220049Z, sha256 1e49bdf05e62607972958f5b7298cfd600c7825812370e26d7c2a842f2af9b93, recorded in backups.txt
rbb708846b605 mosaic-stack/chief claude-code claude-opus-5-5 by jason since 2026-10-20T19:29:59.045Z running
rc36b17ef3788 mosaic-stack/pm claude-code claude-opus-5-5 by chief since 2026-10-20T20:20:56.252Z running
# exit 0
# W1 start stamp 2026-10-20T22:00:49Z
# SNAP sha256 639fd28a checked before S1
# S1.json sha256 a7649760c3439705c8b5f3af26d0c21aea1081c1eb295fd02aef3a642064194b
pointer: /home/jwoltje/.mosaic-dev/state/stack-active.json
{"copy":"490e2771d3d057ab76991e2c7859cd251857e64c","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":"20261020T192956Z-490e2771d3d0","at":"2026-10-20T19:30:00Z"}
log (last five lines):
{"at":"2026-10-20T01:41:05Z","op":"pm-on","from":"89af38a83afa22a8a4f0d368a0a4030ad15a0efa","to":"89af38a83afa22a8a4f0d368a0a4030ad15a0efa","backup":null,"activation":"20261020T014103Z-89af38a83afa","result":"ok","pm":"on"}
{"at":"2026-10-20T07:58:00Z","op":"activate","from":"89af38a83afa22a8a4f0d368a0a4030ad15a0efa","to":"490e2771d3d057ab76991e2c7859cd251857e64c","backup":null,"activation":"20261020T075800Z-490e2771d3d0","result":"started"}
{"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"}
check:
bus: /home/jwoltje/.local/share/mosaic-stack/copies/490e2771d3d057ab76991e2c7859cd251857e64c
Console: /home/jwoltje/.local/share/mosaic-stack/copies/490e2771d3d057ab76991e2c7859cd251857e64c
CLI: /home/jwoltje/.local/share/mosaic-stack/copies/490e2771d3d057ab76991e2c7859cd251857e64c
result: pass
bus status:
host: mosaic-stack, pid 3229540, running, since 2026-10-20T19:29:57.662Z, notifier off
socket: present
writer.lock: pid 3229657
boot: organization file loaded at 2026-10-20T19:29:57.661Z
unit flags: match the pointer
drift: 109 commit(s) on refactor since the pointer's copy
# exit 0
# W3-plan.txt sha256 45de68d42b4cf88640fe0986d071dba4fa076b7db86ff4ed34de4bb6fa2915c6
DropInPaths=
ExecStart={ path=/home/jwoltje/.local/share/mosaic-stack/copies/490e2771d3d057ab76991e2c7859cd251857e64c/scripts/mosaic ; argv[]=/home/jwoltje/.local/share/mosaic-stack/copies/490e2771d3d057ab76991e2c7859cd251857e64c/scripts/mosaic bus start mosaic-stack --pm ; ignore_errors=no ; start_time=[n/a] ; stop_time=[n/a] ; pid=0 ; code=(null) ; status=0/0 }
WorkingDirectory=/home/jwoltje/.local/share/mosaic-stack/copies/490e2771d3d057ab76991e2c7859cd251857e64c
# exit 0
DropInPaths=
ExecStart={ path=/usr/bin/node ; argv[]=/usr/bin/node /home/jwoltje/.local/share/mosaic-stack/copies/490e2771d3d057ab76991e2c7859cd251857e64c/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=[n/a] ; stop_time=[n/a] ; pid=0 ; code=(null) ; status=0/0 }
WorkingDirectory=/home/jwoltje/.local/share/mosaic-stack/copies/490e2771d3d057ab76991e2c7859cd251857e64c
# exit 0
# W4 checks: DropInPaths empty on both, both ExecStart name the OLD copy, bus has --pm, no quote or backslash in either ExecStart
# W4 bus Environment: 2 assignments; quote or backslash: none (values not recorded)
# W0b-W4 end 2026-10-20T22:00:50Z
@@ -1,80 +0,0 @@
#!/usr/bin/env bash
# Re-pin 4429a104f, W0b-W4 (repin-4429a104f.md at f41729178, sha256 e1272d1a...32b9; mos-1105, mos-1107 go). W0 ran before P2.
# Adapted from Darkwing's window-rows-177-185/wsteps.sh.
# Nothing is stopped. Each step goes through w.sh into W.txt; a failed check stops the script.
# usage: wsteps.sh
set -uo pipefail
REPO=/mnt/storage/src/mosaic-stack
WIN=$REPO/agents/sage/work/window-repin-4429a104f
OLD=490e2771d3d057ab76991e2c7859cd251857e64c
NEW=4429a104f6d65ccf165f6c83b5d1dce3147dfde0
PLAN=$REPO/agents/sage/work/window-plan/repin-4429a104f.md
PLAN_SHA=e1272d1a391e00fb1291d47da9973c0ba342d65fcd42f583ea007e883c1f32b9
TOOL_BLOB=3b068b1546d4020cffde5d73929d9130afe91911
BC=$REPO/agents/sage/work/window-plan/repin-de352dfaf/business-check.mjs
BC_SHA=eece4d4671e1ce15ba81e1654c641925ac4988b343687b062897392285a8a1a7
BIZ=$HOME/.config/mosaic-dev/businesses/mosaic-stack.json
BIZ_SHA=1e49bdf05e62607972958f5b7298cfd600c7825812370e26d7c2a842f2af9b93
DATA=/home/jwoltje/.mosaic-dev
SNAP=$WIN/snapshot.mjs
SNAP_SHA=639fd28a2674aaa03ce824e3394b4052f21cdef7d6fa49bae61a9927ed341e67
MOSAIC=$HOME/.npm-global/bin/mosaic
W=$WIN/W.txt
note() { echo "# $*" >> "$W"; echo "# $*"; }
stop() { note "STOP: $*"; exit 1; }
sha() { sha256sum < "$1" | cut -c1-64; }
note "W0b-W4 start $(date -u +%FT%TZ)"
[ "$(git -C $REPO show f41729178:agents/sage/work/window-plan/repin-4429a104f.md | sha256sum | cut -c1-64)" = "$PLAN_SHA" ] || stop "plan at f41729178 is not e1272d1a"
[ "$(sha $PLAN)" = "$PLAN_SHA" ] || stop "the plan file in REPO is not e1272d1a"
[ "$(git -C $REPO rev-parse HEAD:scripts/stack-release.sh)" = "$TOOL_BLOB" ] || stop "tool blob moved"
git -C $REPO diff --quiet $NEW HEAD -- scripts/stack-release.sh scripts/webui-service.sh scripts/bus-service.sh scripts/stack-copy.sh || stop "P5: the four scripts differ from NEW"
[ -z "$(git -C $REPO status --porcelain -- scripts/stack-release.sh scripts/webui-service.sh scripts/bus-service.sh scripts/stack-copy.sh)" ] || stop "P5: the four scripts are dirty"
[ "$(sha $BC)" = "$BC_SHA" ] || stop "business-check.mjs sha"
note "plan e1272d1a, tool blob 3b068b1546d4, P5 four scripts at NEW and clean, business-check.mjs eece4d46: checked $(date -u +%FT%TZ)"
# W0 ran at 21:45:57Z (w0.txt): the copy must still be there with LOAD_OK recorded.
[ -f "$HOME/.local/share/mosaic-stack/copies/$NEW/scripts/mosaic" ] || stop "W0 copy missing"
grep -qx LOAD_OK $WIN/w0.txt || stop "w0.txt has no LOAD_OK"
note "W0 copy present, w0.txt LOAD_OK"
# W0b
[ "$(sha $BIZ)" = "$BIZ_SHA" ] || stop "business file sha256 is not P1's 1e49bdf0"
STAMP=$(date -u +%Y%m%dT%H%M%SZ)
COPY=$BIZ.pre-window-$STAMP
[ ! -e "$COPY" ] || stop "$COPY exists"
cp -p "$BIZ" "$COPY" || stop "W0b cp"
[ "$(sha "$COPY")" = "$BIZ_SHA" ] || stop "W0b copy sha256"
echo "W0b $(date -u +%FT%TZ) business file copy $COPY sha256 $(sha "$COPY") (source $BIZ sha256 $(sha "$BIZ"))" >> $WIN/backups.txt
note "W0b: $COPY, sha256 $(sha "$COPY"), recorded in backups.txt"
# W1
$WIN/w.sh W.txt "W1 agents --tester" $MOSAIC agents --business mosaic-stack --tester > $WIN/.w1-agents.txt || stop "W1 agents"
grep -E '(^|[[:space:]])chief[[:space:]].*working' $WIN/.w1-agents.txt && stop "W1: the chief is working"
$WIN/w.sh W.txt "W1 launches list" $MOSAIC launches list || stop "W1 launches list"
note "W1 start stamp $(date -u +%FT%TZ)"
[ "$(sha $SNAP)" = "$SNAP_SHA" ] || stop "SNAP sha before S1"
note "SNAP sha256 639fd28a checked before S1"
$WIN/w.sh W.txt "S1 snapshot --held" node $SNAP --store "$DATA/bus/bus.sqlite" --held > $WIN/S1.json || stop "S1"
sed -i '$d' $WIN/S1.json # w.sh prints "# exit N" last; S1.json keeps the snapshot only
note "S1.json sha256 $(sha $WIN/S1.json)"
# W2
$WIN/w.sh W.txt "W2 status" scripts/stack-release.sh status mosaic-stack || stop "W2 status"
# W3
$WIN/w.sh W.txt "W3 activate NEW --pm --plan" scripts/stack-release.sh activate $NEW mosaic-stack --pm --plan > $WIN/W3-plan.txt || stop "W3 plan exit"
sed -i '$d' $WIN/W3-plan.txt
note "W3-plan.txt sha256 $(sha $WIN/W3-plan.txt)"
# W4
$WIN/w.sh W.txt "W4 bus unit (Environment omitted from the transcript)" systemctl --user show [email protected] -p ExecStart -p WorkingDirectory -p DropInPaths || stop "W4 bus"
$WIN/w.sh W.txt "W4 webui unit" systemctl --user show mosaic-webui.service -p ExecStart -p WorkingDirectory -p DropInPaths || stop "W4 webui"
BUSL=$(systemctl --user show [email protected] -p ExecStart -p DropInPaths -p WorkingDirectory)
WEBL=$(systemctl --user show mosaic-webui.service -p ExecStart -p DropInPaths -p WorkingDirectory)
for u in BUSL WEBL; do v=${!u}; grep -qx 'DropInPaths=' <<< "$v" || stop "W4 $u DropInPaths not empty"; grep '^ExecStart=' <<< "$v" | grep -q "copies/$OLD/" || stop "W4 $u ExecStart does not name the OLD copy"; grep '^ExecStart=' <<< "$v" | grep -q "copies/$NEW" && stop "W4 $u ExecStart names NEW"; grep '^ExecStart=' <<< "$v" | grep -q "[\"'\\\\]" && stop "W4 $u ExecStart has a quote or backslash"; done
grep '^ExecStart=' <<< "$BUSL" | grep -q -- ' --pm' || stop "W4 bus ExecStart has no --pm"
note "W4 checks: DropInPaths empty on both, both ExecStart name the OLD copy, bus has --pm, no quote or backslash in either ExecStart"
ENVL=$(systemctl --user show [email protected] -p Environment)
note "W4 bus Environment: $(tr ' ' '\n' <<< "${ENVL#Environment=}" | grep -c =) assignments; quote or backslash: $(grep -q "[\"'\\\\]" <<< "$ENVL" && echo FOUND || echo none) (values not recorded)"
note "W0b-W4 end $(date -u +%FT%TZ)"
-30
View File
@@ -1364,33 +1364,3 @@ are never rewritten or removed; corrections are new entries.
2026-10-20T18:57:10Z | Filbert (Opus 5.5, 048f3c63) | row 190 design review r2 (#1688, DESIGN.md sha256 5493c9df3ec0) | CHANGES to Darkwing, cc Sage: B1 claim check on MOSAIC_CONFIG-only selection, B2 shared prefix drop-in dirs; N1-N7 notes; static read, no slot, no Gitea or queue record
2026-10-20T19:00:04Z | Filbert (Opus 5.5, 048f3c63) | row 190 design review r3 (#1688, DESIGN.md sha256 98236a1aff98) | APPROVE to Darkwing, cc Sage: B1 and B2 closed with M13 and M14, N7 settled by Sage's 48ebee0ff; static read, no slot; manifest round gets a queue review record (sage-1337)
2026-10-20T20:40:15Z | Filbert (Opus 5.5, 048f3c63) | row 190 (#1688) manifest round 1 review, candidate 8862feccc (static) | CHANGES: B1 base drift, row 189 ace27dc5e conflicts in stack-release.sh, rebase + full gate rerun; N1-N7 notes; Gitea 29634, queue rev 2362 (uncommitted, left for Sage)
2026-10-20T21:29:20Z | Filbert (Opus 5.5, 048f3c63) | row 190 (#1688) manifest round 2 review, candidate 537a02392 (static) | CHANGES: B1 base drift, row 192 4429a104f conflicts in mosaic-config.mjs and webui-service.sh (async load); round 2 content fine; Gitea 29685, queue rev 2447 (uncommitted, left for Sage)
2026-10-20T22:16:20Z | Filbert (Opus 5.5, 048f3c63) | row 190 (#1688) manifest round 3 review, candidate df65a3d4c on darkwing/row190-r3 | APPROVE, Gitea 29713, queue rev 2504 (left for Sage's queue-commit); static review, no slot used
2026-10-20T22:53:10Z | Darkwing (Opus 5.5, 7525c8e4) | row 190 (#1688) rounds 1-3; row 195 (#1693) design r5-r6 | 190 landed by Sage as 00d4cc1f6 (patch-id matches 176a6bf4b..df65a3d4c, gate 2664/0 + 429/0, rev 2508 done, #1688 closed, not live). My r3 gate 2042/0 and mutants 20/20 in slot sage-1512; Filbert APPROVE 29713; sha sent to Rocko for row 191. 195 r6 b7239bb66 approved (duckworth-0055); build held for the Q187 installer (Vikunja #432)
2026-10-20T23:20:31Z | Filbert (Opus 5.5, 048f3c63) | row 66 (#1559) round 3 rebase review, candidate 2369c00e3 (manifest 42e3876ca168) | APPROVE, Gitea 29770, queue rev 2526 (left for Sage's queue-commit); static review, no slot used
2026-10-20T18:45:06Z | Darkwing (Opus 5.5, 7525c8e4) | row 188 (#1686, P19 409/410): stack-release tests for a carried proof pair and a proof file inside the repository (late line; the row went done 11:14Z) | landed: 8fc0ee216 (Sage, pushed with 31e328958); candidate 86aa549bad75 matches (patch-id c9a56b167c07); script blob 2586a2a37cd2 unchanged; stack-release 151/151, control 19/19, M8/M9/M10 killed by T54 (T53 passes under each); readings 1 (refused log line) and 2 (hard link keeps the watched inode) accepted in sage-1282; Filbert approve 29433 (his M11 killed by test 4)
2026-10-20 18:51:10 UTC | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 #1689 design submitted to Dewey, Sage copied | MVP context read-only; worktree /mnt/storage/scratch/rocko-r191 at da230ca48, immediate node_modules link; durable per-root control/termination design ad6736e0; no product edits or tests, waits design approval.
2026-10-20 18:57:51 UTC | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 #1689 design r2 submitted | Dewey comment 29465 all eight items mapped; DESIGN-r2.md e480c76a, r1 unchanged; scope reduction to named/labelled Docker run retaining spawnSync recommended to Sage; no source edits/tests/live actions.
2026-10-20 19:04:45 UTC | Launchpad (Claude Opus 5.5, 28f6cc53) | row 192 #1690 Q175 SSO phase 1 build | started by Sage (sage-1332) under mos-0974/0977, worktree launchpad/row192, reviewer Webby
2026-10-20 19:04:45 UTC | Morgana (Claude Opus 5.5, 99d5ef45) | row 193 #1691 human users and roles, design | started by Sage (sage-1333) under mos-0974/0977, worktree morgana/row193, reviewer Duckworth
2026-10-20 19:04:45 UTC | Honker (Claude Opus 5.5, ecd17786) | row 194 #1692 agent lifecycle, design | started by Sage (sage-1334) under mos-0974/0977, worktree honker/row194, reviewer Webby
2026-10-20 19:04:45 UTC | Gander (Codex gpt-6.1-sol, 0bf412c3) | row 196 #1694 human boundary across uids (P19 333), design | started by Sage (sage-1339) under mos-0989, worktree gander/row196, reviewer Duckworth
2026-10-20 19:04:45 UTC | Herb (Codex gpt-6.1-sol, f6cf4287) | row 197 #1695 second gate lane, design | started by Sage (sage-1344) under mos-0977 item 6, worktree herb/row197, reviewer Webby
2026-10-20 19:04:45 UTC | Sage (Claude Opus 5.5, 1ef1e4f8) | MVP T1 staffing | rows 192-197 briefed (190b as 195, P19 333 as 196, gate lane as 197), issues 1690-1695, seats Launchpad/Morgana/Honker/Gander/Herb started; refactor pushed to 96f243faf
2026-10-20T19:08:56Z | Dewey (Opus 5.5, 84fb346c) | row 189 r1 review (#1687), row 191 design review r1+r2 (#1689) | 189 approve (29485, rev 2268; single 174/174, cli 480/480, D1-D5 killed); 191 design r1 changes (29465), r2 approve (29473)
2026-10-20T19:50:56Z | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 #1689 kill switch | approved design implementation in scratch; state/broker/launcher, Discord, tracked task fixtures and HTTP/UI node checks pass singly; browser, suites, mutants and final packet pending; no live action or commit.
2026-10-20 20:31:30 UTC | Rocko (d99326cc) | row 191 #1689 focused browser stage | scripted busy gate passed; processing browser 2/2 and script-list browser regression 1/1; awk total 3/3; cwd node/chromium sweep clear; slot returned to Sage, no live action.
2026-10-20T22:30:20Z | Morgana (Claude Opus 5.5, 99d5ef45) | row 204 (193a, #1702) and row 213 (C2-08, #1714) | 204 landed by Sage as d0bbc5c1c (patch-id 92c18aea matches, gate 2789/0), done rev 2506, #1702 closed 29724; 213 r2 aee32d185 on morgana/row213, Webby APPROVE (webby-0042, manifest b5ff0ac0), suites and 31 mutants wait on Sage's go; line left uncommitted for Sage's queue-commit
2026-10-20T22:43:28Z | Dewey (Opus 5.5, 84fb346c) | #436b design r1 review (Megavolt, 714c07304, sage-1545) | CHANGES: 4 blocking (foreign check by id + crash gap, append-only events and unreserved grant.* records, seat-token rotation rows, approval note on admin reach), 8 notes; agents/dewey/work/436b/design-r1-verdict.md; dewey-0754/0755 |
2026-10-20T22:54:15Z | Dewey (Opus 5.5, 84fb346c) | #436b design r2 review (Megavolt 6db7cc45d, sha256 9f1978a0) | CHANGES: r1 items 1-12 closed; blocks B1 step 0 grant.failed reads as spent under #spent, B2 settle seat-token introspection unproven and non-401 outcomes undefined, B3 step 5 reach vs forks from earlier grants; notes N1-N6. Verdict agents/dewey/work/436b/design-r2-verdict.md (bfdc0b9f), dewey-0756 to Megavolt, dewey-0757 to Sage
2026-10-20 22:58:45 UTC | Rocko (d99326cc) | row 191 #1689 rebase stage | new branch rocko/row191-r2-00d4 on 00d4cc1f6; 26 touched files 249/249 and 15 overlapping Client readers 102/102 singly; candidate a4f780c2, 78 paths; packet 185 files verified; no own cwd node/chromium; mutants and owner gate pending; no commit/push/live action.
2026-10-20T23:21:32Z | Dewey (Opus 5.5, 84fb346c) | row 66 (#1559) round 3 review, rebase of r2 onto c40d846cb (sage/row66-r3 2369c00e3, manifest 42e3876ca168) | APPROVE 29771, rev 2529: patch-id 6d085621 equals r2, range-diff =, 23 manifest hashes match, no drift past the base, four drifted files sit beside rows 190/192 cleanly, no hash pins; singles are Sage's (not rerun). Also #436b Q-B4 answered by mos-1147, verdict addendum
2026-10-20T23:31:31Z | Dewey (Opus 5.5, 84fb346c) | #436b design r3 review (megavolt-0050, DESIGN.md fa5658941) | CHANGES: restart reconcile spends step 0 refusals (#436a r6 X3 conflict), settle unknown id keeps unverified token in use; N7 secretCheck; B1-B3, N1-N6, D12 closed; verdict design-r3-verdict.md 4c87d859; dewey-0759 Megavolt, dewey-0760 Sage |
2026-10-20T23:32:42Z | Sage (Claude Opus 5.5, 1ef1e4f8) | machine grant rows, slot relay, queue sweep | rows 234 (#1740, 3i-3j) and 235 (#1741) added, row 206 moved to 3j-3k after 234 (f519fb1d2); row 199 r1 request as jarvis 29778 (manifest 35f900fd rebuilt from git objects); #436b r3 item 1 ruled (b), sage-1578; queue-commit 1c46d0abf revs 2456-2539; slot Megavolt to Gosalyn row 230
2026-10-21T01:28:05Z | Darkwing (Opus 5.5, 7525c8e4) | row 232 I1 build (sage-1558, sage-1561 ruling a) | rebased onto cb5fdb064 (row 200), gate green and 17/17 mutants killed on Sage's slot; in review round 1, manifest 7a3b4ea1 at 84f1a91e6 on darkwing/row232-r1, comment 29825, revs 2595-2596; Filbert woken
2026-10-21T01:34:13Z | Filbert (Opus 5.5, 048f3c63) | row 232 installer I1 review r1 (#1734, manifest 7a3b4ea133cc) | APPROVE, Gitea 29829, queue rev 2600 (left for Sage's queue-commit); static review, gate and mutants are Darkwing's receipts; 3 non-blocking notes
2026-10-21T01:49:55Z | Sage (Opus 5.5, 1ef1e4f8) | row 237 landed d7d943199 (#1743, gate green, M1 killed); PRD 1.7+1.8 landed 0adf8ddfe (Honker 2884ee496, Duckworth approve bf52628f9, tasks 97/0 bus 478/0 on the tip, DECISIONS Q192); rows 239 (#1745) and 240 (#1746) added; 233 briefed; queue revs 2540-2603 committed 584e2bd82 | landed, push next
2026-10-21T02:51:46Z | Sage (Opus 5.5, 1ef1e4f8) | row 210 landing (r5 0485384e9, Duckworth r3 29851) | landed f5753c0e8, patch-id 723b1c83 = r5 diff, manifest 688d38b9 OK; gate 2796/0 packages, 429/0 scripts; mutants 52/53 (M22b equivalent); #1707 closed (29853), Vikunja 424 comment 429; queue revs 2604-2617 c084d8d42; row 234 briefed; Megavolt has the slot for 241 (sage-1668)
+11 -126
View File
@@ -15,7 +15,7 @@ and changes nothing.
| Command | Purpose | Notes |
|---|---|---|
| `scripts/bootstrap.sh [--instance NAME]` | Create `~/.config/mosaic-dev/config.json` if absent; with `--instance NAME`, `~/.config/mosaic-NAME/config.json` and the claimed root `~/.mosaic-NAME` | Idempotent; existing config validated, never rewritten. See [Stack instances](#stack-instances---instance-name-row-190) |
| `scripts/bootstrap.sh` | Create `~/.config/mosaic-dev/config.json` if absent | Idempotent; existing config validated, never rewritten |
| `scripts/build.sh` | Build the release image | Tag derived from `RELEASE` + pinned pi version |
| `scripts/hello.sh` | One-shot startup request | Prints model response on stdout |
| `scripts/verify.sh` | Full gated test | Exit 0 only on exact `MOSAIC_HELLO_OK`; `EXPECTED_MARKER` overrides for negative drills |
@@ -238,12 +238,6 @@ Exit codes: `0` ok · `2` invalid file or credential reference · `3` system
config problem · `4` usage or a required file missing. Details:
`packages/business/README.md`.
`mosaic credentials status|issue|rotate|revoke|delete` (row 199) goes to
`packages/cli` and the bus's human-only credential verbs: an agent's own
token in `<configDir>/secrets/agents/<business>/<agent>/<kind>`, read on
stdin, never printed. The tester may run `status` and `revoke`. Details:
`packages/cli/README.md`.
`instances` and `instance add|set` (row 165) go to `packages/cli` and the
bus's human-only `instances` and `instances.set` verbs, so they need a live
bus host and refuse inside an agent session. `instances` prints the revision,
@@ -276,9 +270,9 @@ scripts/mosaic follow --thread <thread> [--tester]
scripts/mosaic stop <run>
scripts/mosaic launches off | on [--business <id>]
scripts/mosaic launches list [--json]
scripts/bus-service.sh [--instance NAME] render [--copy ID [--copies DIR]] [--pm] [--claude PATH]
scripts/bus-service.sh [--instance NAME] install [--copy ID [--copies DIR]] [--pm] [--dir DIR] [--no-reload] [--claude PATH]
scripts/bus-service.sh [--instance NAME] uninstall | status <business>
scripts/bus-service.sh render [--copy ID [--copies DIR]] [--pm] [--claude PATH]
scripts/bus-service.sh install [--copy ID [--copies DIR]] [--pm] [--dir DIR] [--no-reload] [--claude PATH]
scripts/bus-service.sh uninstall | status <business>
```
The human commands read and resolve through the broker's human transport
@@ -327,89 +321,12 @@ stdin and stdout it exits 4 with the usage.
Exit codes: `0` ok · `1` failed or outcome unknown · `2` invalid input ·
`3` refused or config problem · `4` usage. Details: `packages/cli/README.md`.
## Stack instances (`--instance NAME`, row 190)
Several stacks can run on one host, each with its own config, data root,
units, Console port and CLI wrapper. The default instance is today's stack
and keeps every name it had. A named instance is created once:
```
scripts/bootstrap.sh --instance NAME
```
NAME matches `^[a-z][a-z0-9]{1,15}$`; anything else exits 4
(`instance-name-invalid`), and `--instance dev` is the default. Bootstrap
writes `~/.config/mosaic-NAME/config.json` (0644) and claims `~/.mosaic-NAME`
with the marker `instance.json`, `{"instanceVersion":1,"name":"NAME"}`. It
creates either directory 0700; one that already exists keeps its mode. It refuses with exit 3 and writes
nothing when the root exists, is not empty and holds no marker naming NAME.
A second run changes nothing. `~` is the home of the OS user that runs the
instance.
`--instance NAME` is taken, once and in any position, by `bootstrap.sh`,
`bus-service.sh`, `webui-service.sh` (all but `copy`), `discord-service.sh`,
`stack-copy.sh` (`wrapper` and `check`) and `stack-release.sh`. Copies serve
every instance, so both `copy` verbs refuse it with exit 4. Inside a process the config
travels as `MOSAIC_CONFIG`. Selection, checked before any read or write:
| Given | Config | Names |
|---|---|---|
| neither | `~/.config/mosaic-dev/config.json` | the default's |
| `MOSAIC_CONFIG` only | that file | the default's |
| `--instance NAME` only | `~/.config/mosaic-NAME/config.json` | NAME's |
| both | must be one file (a symlink to it agrees), else exit 4 `instance-config-mismatch` | NAME's |
Every selection checks the data root's marker. A named instance whose root
lacks its own marker refuses `instance-root-unclaimed`; a root whose marker
names another instance refuses `instance-root-mismatch`, also when a config
reaches it with no flag. Both exit 3 before anything is read or changed. A
missing config exits 3 with the bootstrap command that creates it.
| | Default | Instance NAME |
|---|---|---|
| bus unit | `[email protected]` | `[email protected]` |
| Console unit | `mosaic-webui.service` | `mosaic-webui-NAME.service` |
| Discord unit | `[email protected]` | `[email protected]` |
| CLI wrapper | `$BIN/mosaic` (Q81 marker) | `$BIN/mosaic-NAME`, marker `# mosaic-cli wrapper, instance NAME (row 190)`, exports `MOSAIC_CONFIG` |
| bus session env | `%h/.config/mosaic-dev/secrets/claude-session.env` | `~/.config/mosaic-NAME/secrets/claude-session.env` |
| Console port | `--port`, 7332 when absent | `--port` required; 7330, 7332 and 7333 exit 4 `port-reserved` |
A named instance's units add `Environment=MOSAIC_CONFIG=<its config>` and
` [NAME]` after the Description, and the Console unit's comment on the
variables it sets names `MOSAIC_CONFIG`. The default's renders are
byte-identical to the templates' renders before row 190. `stack-copy.sh
wrapper --instance NAME` creates `$BIN/mosaic-NAME` or replaces one that
carries NAME's marker, and refuses any other file. `check --instance NAME`
needs `--business ID`, and fails with `CLI: <copy> (MOSAIC_CONFIG mismatch)`
when the wrapper does not export NAME's config.
For every instance, a Console `--port` or `--tester-port` that another
installed Console unit in the unit directory already takes refuses
`port-claimed`, naming that unit: `webui-service.sh install` exits 1, and
`stack-release.sh activate` and `rollback` exit 3 before anything stops
(`--plan` lists it). A unit with no `--port` holds 7332.
`stack-release.sh --instance NAME` reads only NAME's pointer, log, store and
backups. Its busy check sees only NAME's bus units. Its drop-in scan adds
`mosaic-bus-NAME@*.service.d` and the shared `mosaic-bus-.service.d`. Its
rollback set holds NAME's two unit files, their own `.d` directories and
`$BIN/mosaic-NAME`; it never captures or restores the shared prefix
directories (`mosaic-bus-.service.d`, `mosaic-webui-.service.d`,
`mosaic-discord-.service.d`). The live-session guard refuses a path inside
any named instance's config directory or `~/.mosaic-NAME`, even when that
instance's config is unreadable.
All instances run under one OS user for now. That user's processes can
read and control every instance; a separate user per instance is row 195.
A real second instance on this host is a Q173 window, and tunnel or Traefik
exposure of a named Console waits for Jason after Q175 SSO.
## Stack copies (`scripts/stack-copy.sh`)
```
scripts/stack-copy.sh copy COMMIT [--copies DIR] [--allow-worktree]
scripts/stack-copy.sh wrapper ID [--instance NAME] [--copies DIR] [--bin DIR]
scripts/stack-copy.sh check [--instance NAME] [--copies DIR] [--bin DIR] [--business ID]
scripts/stack-copy.sh wrapper ID [--copies DIR] [--bin DIR]
scripts/stack-copy.sh check [--copies DIR] [--bin DIR] [--business ID]
```
One read-only tree supplies the bus, Console and human CLI (row 85,
@@ -451,11 +368,11 @@ reports checkout or copy identity and refactor drift, and writes nothing.
```
scripts/stack-release.sh adopt BUSINESS
scripts/stack-release.sh activate ID BUSINESS [--pm | --pm-after-harness] [--migrate] [--tester-port N]
[--proof-header NAME --proof-file PATH | --no-proof | --sso] [--no-send] [--timeout S] [--plan]
[--proof-header NAME --proof-file PATH | --no-proof] [--no-send] [--timeout S] [--plan]
scripts/stack-release.sh pm-on BUSINESS [--timeout S]
scripts/stack-release.sh rollback BUSINESS [--restore ACTIVATION [--events N]] [--timeout S] [--plan]
scripts/stack-release.sh status BUSINESS
common options: --copies DIR, --bin DIR, --instance NAME
common options: --copies DIR, --bin DIR
--events N is the total of the per-table counts the post-backup-events refusal prints: events and
every table in COUNTED.
```
@@ -571,36 +488,6 @@ rollbacks carry the pointer, so send stays off. No flag puts `--send` back.
`--plan` prints one `send:` line: `dropped`, `off` with or without the
`--no-send` note, or `carried`.
`activate ... --sso` (row 200, #1698) starts the Console with Q175 sign-in
(row 192). It drops `--public-host`, `--auth-header`, `--allow-user` and any
proof pair from the pointer's Console flags and the unit, and puts `--sso` after
the other flags, before `--send` and `--tester-port`. It may name the live copy
once, so a copy already at row 192 or later turns sign-in on without a copy
change. Later activates, `pm-on` and rollbacks to a copy with `--sso` carry it.
`--sso` with a proof pair or `--no-proof`, or on any other verb, is a usage
error. Before anything stops, a run whose Console flags hold `--sso` refuses
`sso-target` for a target Console without it, `sso-config` when the target's own
`checkConsole` refuses the config's `console` key (or there is none, in which
case, from another copy, it names the first step: activate the target without
`--sso`, then write `console.sso`), and `sso-slot` for each provider's slot file
that is missing, a symlink, not a regular file, not owned by this uid, has group
or other bits, resolves inside the repository or the copies, or is empty. It
uses `lstat`, `stat` and `realpath` on the slot and never reads it. `--plan`
prints one `sso:` line: `on` with what it drops and carries, `carried`, or
`dropped` on a rollback.
A rollback from an `--sso` pointer to a copy whose Console lacks `--sso` takes
the Console flags from the rollback set of the activation that moved the stack
onto the pointer's copy from the target, past any same-copy re-pin, checked
against the set's `SHA256SUMS`. `--send` and `--tester-port` follow the pointer.
When that set is missing, fails its sums, holds no readable argv or already ran
`--sso`, the run refuses `sso-rollback-flags`. Any activate or rollback also
refuses `config-console-key` before anything stops when the config holds a
`console` key the target's `scripts/mosaic-config.mjs validate` refuses (a copy
before row 192 refuses the key). The message says to remove the key first. So a
window re-pins to row 192 or later before it writes the key, and a rollback past
row 192 removes the key first.
#### A target past c216f47db: the PM after the harness kind
From c216f47db on, the bus launches the PM only with a `harness` kind in the
@@ -715,7 +602,6 @@ its runs alone. No unit passes it, and `--pm` with it is a usage error.
```bash
scripts/webui-service.sh copy COMMIT [--copies DIR] # make a pinned copy
# render, install, uninstall and status also take --instance NAME (row 190); copy refuses it (exit 4)
scripts/webui-service.sh render [--copy ID [--copies DIR]] [FLAGS] # print the unit
scripts/webui-service.sh install --copy ID [--copies DIR] [--dir DIR] [--no-reload] [FLAGS]
scripts/webui-service.sh uninstall [--dir DIR] [--no-reload]
@@ -793,9 +679,8 @@ Each value is held to a plain character set the unit can carry (no space,
`%`, `$`, quote, backslash, `&` or `|`), and the preview values must start
with a letter or digit; anything else is refused. With `--auth-header
X-authentik-uid`, in any letter case, `--allow-user` must be the Authentik
uid, `^[0-9a-f]{64}$` (lead decision 101). The default's unit sets no `MOSAIC_*`,
`CLAUDE*`, `CODEX*` or `PI_*` variable; a named instance's sets only
`MOSAIC_CONFIG` (row 190). Serve's exit 2 (usage, config, or a
uid, `^[0-9a-f]{64}$` (lead decision 101). The unit sets no `MOSAIC_*`,
`CLAUDE*`, `CODEX*` or `PI_*` variable. Serve's exit 2 (usage, config, or a
port already in use) is not restarted.
The update/rollback sequence is [Pinned stack update](#pinned-stack-update)
@@ -820,7 +705,7 @@ scripts/discord.sh stop <binding>
scripts/discord.sh unlock <binding>
scripts/discord.sh recover <binding>
scripts/discord.sh reload <binding>
scripts/discord-service.sh [--instance NAME] render | install [--dir DIR] [--no-reload] | uninstall [--dir DIR] [--no-reload] | status <binding>
scripts/discord-service.sh render | install [--dir DIR] [--no-reload] | uninstall [--dir DIR] [--no-reload] | status <binding>
```
One seat's conversation reachable from listed channels of one Discord
@@ -55,92 +55,3 @@ code is written.
Duckworth approves DESIGN.md, Mos rules on the authority model, then Duckworth approves the manifest. The bus,
business, cli and tasks suites and every `scripts/test-*.sh` are green on Sage's gate, and the design's
mutants are killed.
## Brief: #436b Gitea grant path, build (schema 3j-3k)
Sage, 2026-10-20, for Vikunja P19 #436 (#436b), mos-1124 (Q190: each seat gets its own Gitea account through
this row) and mos-1145 (Gitea moves to 10-27, at risk). Owner Megavolt, reviewer Dewey, gate owner Sage.
Gitea #1742.
The design is `agents/megavolt/work/436b/DESIGN.md` r5 at 1be535aca on `megavolt/row199`, sha256
`68df02cdf890564c6ed0a5426204114389ba7eb0eea1cf04f1bf5938b6de45f4`, which Dewey approved in dewey-0763. Section 7
was approved by Duckworth at fa5658941 (duckworth-0067) and is unchanged since. It is not reopened here. The
build also takes `agents/megavolt/work/436b/BUILD-CARRY.md` at 3330c901b (Dewey's N9 and N10), and its BUILD.md
cites that file.
Scope: the Gitea half only. The binding (section 2), the executor and its admin credential (sections 3 and 4),
failure and restart (section 5, with sage-1578's option (b): a step 0 refusal stays unspent only until the
next start), revoke and sweep (section 6), the store records (section 7) and section 9's tests and mutants. The
GitHub half (section 10, Bushroot) is not in this row.
Order and schema:
- It builds on row 210's landing (the design: code waits on 210) and on machine grant row a (row 234), whose
section 7 rule and `system/grant` writer it shares.
- This row owns `SCHEMA_VERSION` 3k: its two tables go in `3j-3k.sql` on row 234's landing. Row 206 (193b2)
moves to 3k-3l and rebases onto this row's landing (Sage, 2026-10-20: row 205 is still queued behind the
federation rows, so this row lands first).
Boundaries:
- Live use waits for Jason. He approves section 8's notes (the admin token can take over any account on that
Gitea) and does the one bootstrap step himself (3e): he creates `mosaic-grant`, its TOTP and its token. No
agent creates a Gitea account or token (Q188).
- No test reaches git.mosaicstack.dev or any provider. Tests use a fake Gitea and fake tokens in scratch roots.
- Seats keep pushing as jason.woltje until this row is live and Jason grants their accounts (mos-1124).
Gate: Dewey approves the manifest. The bus, business and cli suites and every `scripts/test-*.sh` are green on
Sage's gate, and section 9's mutants are killed, N9's restart test included.
## Brief: row 199 follow-up N1, one decision publishes once
Sage, 2026-10-20, from Duckworth's row 199 round 1 note N1 (duckworth-0068, megavolt-0058). Owner Megavolt,
reviewer Duckworth, gate owner Sage. Builds on row 199's landing. Gitea #1743.
One decision can push its commit to two fork refs, because `checkPublish` doesn't refuse while another ticket
holds the decision. Pick one fix and say why in BUILD.md: refuse `publish-busy` in `checkPublish`, or recheck
`#spent` in `publishTarget`. Add a test where two tickets race on one decision and exactly one push happens,
plus a mutant that removes the check.
Gate: Duckworth approves the manifest. The bus suite and every `scripts/test-*.sh` are green on Sage's gate,
and the mutant is killed.
## Brief: row 237 follow-up, one decision lands once, with tests at the broker
Sage, 2026-10-21, from Megavolt's checkLand analysis (megavolt-0068, megavolt-0069) and sage-1651. Owner
Megavolt, reviewer Duckworth, gate owner Sage. Builds on row 237's landing d7d943199. Vikunja P19 441.
Landing already has three guards against one decision landing twice. `land-busy` at
`packages/cli/src/git-socket.mjs:351` allows one landing per socket at a time. `finishLand` moves main by
compare-and-swap (`packages/seat/src/working.mjs:391`, refusing `base-moved`). `recordLand` reruns
`#landFacts`, which refuses `decision-consumed` (`packages/bus/src/broker.mjs:945`). So no end-to-end race
exists, and no code change is planned. What's missing is a test that pins each guard.
- In `packages/bus/tests/land.test.mjs`, add a test where two tickets pass `checkLand` on one decision. The
first `recordLand` succeeds and the second refuses `decision-consumed`, with nothing recorded for it.
- Run three mutants and list them in BUILD.md: drop the `#spent` line in `#landFacts`, drop the old value
from the CAS `update-ref` in `finishLand`, and drop the `land-busy` throw at git-socket.mjs:351. Each must
be killed by a named test. If the CAS mutant survives `packages/seat/tests/working.test.mjs:555`, add a
test to working.test.mjs that kills it. Otherwise leave that file alone.
- If a mutant survives with no test that can reach it, stop and send me the analysis. Don't change source
to make a test reach it.
Gate: Duckworth approves the manifest. The bus, seat and cli suites and every `scripts/test-*.sh` are green
on Sage's gate, and the three mutants are killed.
## Brief: row 237 follow-up N1, a retry after publish-timeout pushes once
Sage, 2026-10-21, from Duckworth's row 237 note N1 (#1743 comment 29810, duckworth-0069) and Megavolt's
row 237 BUILD.md residual 2. Owner Megavolt, reviewer Duckworth, gate owner Sage. Builds on row 237's landing
d7d943199. Vikunja P19 440.
The git socket gives up with `publish-timeout` (`packages/cli/src/git-socket.mjs:433`) while the bus
process's push may still be running (`publishPush` in `packages/bus/src/runtime.mjs`, entry `running`).
`publishRefused` then drops the ticket, so row 237's `publish-busy` check no longer holds the decision. A
retry to another branch can then push a second ref to the fork.
Pick one fix and say why in BUILD.md. Either keep the decision held until the bus's push ends, or kill the
push on timeout and wait for it to exit before the ticket goes. Add a test that starts a slow push, times
it out, retries to another branch, and finds exactly one ref on the fork. Add a mutant that removes the
fix. Don't change the `publish-timeout` refusal text.
Gate: Duckworth approves the manifest. The bus and cli suites and every `scripts/test-*.sh` are green on
Sage's gate, and the mutant is killed.
-31
View File
@@ -33,16 +33,6 @@ code.
states the hook that row 193 fills.
5. Per instance. Row 190 runs one instance per OS user. Lifecycle verbs act on their own instance only.
6. The kill switch (row 191, Rocko) refuses every launch path. These new launch paths check it too.
7. A created agent's credential entry (Sage, 2026-10-20, from row 199 note N2, duckworth-0068). Row 199's
`issue` works only for an entry the bus loaded at start. Create either registers the new entry with the
running bus or states that the agent needs a restart before `issue`, and a test covers the one chosen.
Honker chose the restart route (honker-0044, plan at 457fb8635 on `honker/row194-b2`). Sage accepted it
because it keeps row 194 D3's fixed-at-start rule and row 199's reviewed store unchanged.
8. Revoke on fire and delete (Sage, 2026-10-20, row 199 D13, honker-0044). Item 7 lets a human create a
credentialed agent, so `fire` and `delete` call `credentials.revoke(business, agent, {reason, actor})`
before they change the business file. A failed revoke stops the verb with `credential-revoke-failed`
and leaves the business file unchanged. Tests cover a revoke that succeeds and a revoke that fails, for
each verb. Mutants drop the revoke call, move it after the file write, and ignore its failure.
## Boundaries
@@ -55,24 +45,3 @@ code.
Webby approves DESIGN.md, then the manifest. The bus, cli, business and webui suites and every
`scripts/test-*.sh` are green on Sage's gate, and the design's mutants are killed.
## Brief: row 194 follow-up, the revoke under the writer's lock and a fire test
Sage, 2026-10-21, from Duckworth's row 194 b3 N1 (duckworth-0070, duckworth-0074) and Mos's ruling mos-1158.
Owner Honker, reviewer Duckworth, gate owner Sage. Builds on row 194's landing.
Row 194 b3 checks the revision in `instance-settings.mjs` `commit()` before `beforeWrite`, which is the revoke on
fire and delete. A window is still open. If a write lands between that read and the writer's lock, a fire or
delete revokes and then refuses `revision-conflict`. An `outcome-unknown` after a revoke also doesn't say the
tokens are gone. Two changes:
1. Run `beforeWrite` inside `writer.write`, under its lock, after the `organization.mjs:188` revision check. A
refusal after a revoke that ran says the tokens may already be revoked. That covers `revision-conflict` and
`outcome-unknown`, in the CLI and the Console texts. A refusal before any revoke keeps today's text.
2. Add a fire test for a stale revision, using an injected writer or whatever the design picks. The test checks
that nothing is revoked and the business file is unchanged. Keep T21 for delete.
Mutants: run `beforeWrite` before the lock. Drop the fire refusal. Drop the "may already be revoked" text.
Gate: Duckworth approves the manifest. The bus, business, cli and webui suites and every `scripts/test-*.sh`
are green on Sage's gate, and the mutants are killed.
+98 -347
View File
@@ -18,9 +18,9 @@ packages adapters roles scripts` touches only `scripts/stack-release.sh` and its
is in either.
Order (design 8, "Parallel waves"):
1. Start now: C2-01, C2-08, C2-11a, C2-13a.
1. Start now: C2-01, C2-08, C2-11a.
2. On C2-01's candidate: C2-04, C2-05a, C2-06, C2-12. On C2-08's: C2-09a, C2-10a, C2-20.
3. Then C2-03a and C2-07. Then C2-05b, C2-10b and C2-13b.
3. Then C2-03a and C2-07. Then C2-05b, C2-10b and C2-13.
4. Last: C2-14.
A row whose parent hasn't landed builds on the parent's candidate and rebases onto its landing (Q177).
@@ -34,7 +34,7 @@ Rules every row keeps:
text wherever a test checks that output holds none.
- No credential value in any output, test, log or message (CONSTITUTION gate 13). Tests use a fixture
token file with a made-up value.
- No live stack change. Nothing here re-pins, restarts or activates anything. C2-13a and C2-13b's runs on the host
- No live stack change. Nothing here re-pins, restarts or activates anything. C2-13's run on the host
needs Jason's go (design 10).
- Before in-review, grep `packages/*/tests` and `scripts/test-*.sh` for each changed path and for any
64-hex pin of it, and run every changed or touched test file on its own. Suites need Sage's slot.
@@ -98,55 +98,6 @@ every `scripts/test-*.sh`.
---
## C2-01b survey escapes, list item lines and attachment embeds
### Problem
Webby's row 212 review (webby-0041, `agents/webby/work/queue-212/REVIEW-r1.md`, approve on bdf18272a)
left three notes that change C2-01's code. Row 212 lands as approved, and these come after it.
- N1. The comment at `packages/library/src/survey.mjs:76` is false. `JSON.stringify` leaves C1 and bidi
controls raw, so a folder name can carry them into the survey output.
- N2. A front matter link reports its key's line, not its list item's (`reader.mjs:156`, `:162-165`). Two
links under `relationships:` both point at the key, and so do their R4, R5 and R6 problems.
- N3. `links.mjs:2` reads `![[x]]` embeds as links, so an embedded image that is unmapped gives R4 "a link
to no file". Design 1.3 says nothing about embeds.
### Owner and reviewer
- Owner: Gosalyn.
- Reviewer: Webby.
- After: C2-01.
### Files owned
`packages/library/src/survey.mjs`, `src/reader.mjs`, `src/links.mjs`, `packages/library/README.md` where
it describes these, and `packages/library/tests/`.
### What ships
- N1. The survey prints U+0080 to U+009F, U+200E, U+200F, U+202A to U+202E and U+2066 to U+2069 in any
name as `\uXXXX` escapes, and the comment says what the code does.
- N2. A link in a front matter block list reports its list item's line. A link in a scalar or flow value
keeps the key's line.
- N3 (sage-1517, Gosalyn's proposal). An embed whose target has an extension other than `.md` is an
attachment. It is neither resolved nor indexed, and it never gives R4. The survey still counts it as an
embed. An embed with no extension or with `.md` resolves as a link, so an unresolved one is R4.
Tests: one fixture folder name per escaped range prints escaped. Two links under one block list key report
two different lines, and their problems carry those lines. `![[cover.png]]` gives no problem and counts as
an embed, `![[missing-note]]` gives R4, and `![[note.md]]` resolves.
### Out of scope
The index and queries (C2-05a), and webby-0041 N4 and N5, which go to C2-12 and C2-10b.
### Gate
Webby approves the manifest. The library tests are green on Sage's gate, along with every
`scripts/test-*.sh`.
---
## C2-08 creative roles, persona file and block, M1, M2, M6, M10, prompt passed by file
### Problem
@@ -221,7 +172,7 @@ Tests, all named in the design 8 C2-08 row:
### Out of scope
Thread creation per instance and the launcher (C2-09a), the lookup tools (C2-05b), the cited-reply guard
(C2-06). Checking the host's claude for `--system-prompt-file` is C2-13a's precheck. If the C2-20 dry run
(C2-06). Checking the host's claude for `--system-prompt-file` is C2-13's precheck. If the C2-20 dry run
finds heading lines in a real persona, the fence rule changes in a follow-up to this row, reviewed,
before the import (design 9.1).
@@ -279,7 +230,7 @@ from changing `projects`, and nothing creates a mirror.
### Owner and reviewer
- Owner: Herb, gpt-6.1-sol. Herb replaces Rocko, who holds five other rows (sage-1482).
- Owner: Rocko, gpt-6.1-sol.
- Reviewer: Filbert.
- After: C2-01.
@@ -431,8 +382,6 @@ The setup sheet in plain words:
- That a short link to a name that also exists in another of her repos can point somewhere else for the
agents, and that a full-path link avoids this.
- What changes for Bean and Chet: short cited items, no passages (design 4).
- That a list of links in a note's properties uses the quoted form Obsidian's property editor writes,
`appears_in: ["[[A]]", "[[B]]"]`. The unquoted `[[[A]], [[B]]]` is R3 (webby-0041 N4).
The survey run sheet gives the exact `library survey` command with C2-01's real flags, and states what
the output holds: names and counts only.
@@ -616,14 +565,9 @@ and tests in `packages/library/tests` and `packages/bus/tests`. If the helper si
### What ships
Design 7.2's slice:
- `syncCommand(kind, url)`: argv is `SAFE`, then `-c protocol.https.allow=always`, `-c
http.followRedirects=false`, `-c credential.helper=`, then `-C <mirror>`, then the command. Its env is
`gitEnv()` plus `LC_ALL=C` and `GIT_ASKPASS` naming a helper outside the mirror, which reads the
`library-gitea` slot. The helper answers only the exact Username and Password prompts for the configured
host and user, checked with row 199's HOST and NAME patterns, and exits 1 on anything else. The URL
carries no userinfo. (Sage, 2026-10-20, ruling the askpass departure: megavolt-0060 to -0063, dewey-0765,
gosalyn-0068. It closes the redirect, host-check and username gaps that row 199's helper closes, and keeps
`GIT_ASKPASS` with the slot path, never the value, per sage-1487.)
- `syncCommand(kind, url)`: its argv is `SAFE`, exactly one `-c protocol.https.allow=always`, `-C
<mirror>`, then the command. Its env is `gitEnv()` plus `GIT_ASKPASS` naming a helper outside the
mirror, which reads the `library-gitea` slot.
- An `https://` URL only.
- The served ref `refs/heads/served`, and `library-diverged`, held until a human `library.sync --accept
<commit>`.
@@ -631,10 +575,7 @@ Design 7.2's slice:
- `library.sync`, human only. A test reaches a local bare repo only through the module parameter that
replaces `syncCommand`.
Tests: the C2-03a tests in design 7.2, with the argv test checking the fixed sequence above rather than
"exactly one `-c`". Also dewey-0765's tests: the username prompt, a wrong host, a wrong port, another user, a
302, and the slot path against the value. Also gosalyn-0068's test that the env holds `LC_ALL=C`. A mutant
that sets `protocol.allow=always` fails the argv test, and dewey-0765's four mutants are killed.
Tests: the C2-03a tests in design 7.2. A mutant that sets `protocol.allow=always` fails the argv test.
### Out of scope
@@ -643,7 +584,7 @@ Push and every bus write (C2-03b, C2-02). Creating the token slot, which is desi
### Gate
Dewey approves the manifest. The library and bus suites are green on Sage's gate, along with every
`scripts/test-*.sh`, and the mutants are killed. A search of the test run's output for the fixture token
`scripts/test-*.sh`, and the mutant is killed. A search of the test run's output for the fixture token
finds nothing.
---
@@ -749,11 +690,9 @@ Design 7.4. The chat view renders a cited reply through `renderItems`: each item
with a scope badge (canon, playground or web) and "changed since" when the file's last commit is newer
than the cited one. Each notice shows its fixed line. A status line reads `library.status`: the served
commit and its age, `library-diverged`, a fetch error, and the count of R-problems with a link to the
list. A web cite's `url` renders as a link only when its scheme is `https`, and as plain text otherwise.
`renderItems` doesn't check the scheme (`packages/library/src/render.mjs:14-16`, webby-0041 N5).
list.
Tests: webui tests for items, scope badges, notices, "changed since" and the status line, and a web cite
with an `http:` and a `javascript:` url that each render as text.
Tests: webui tests for items, scope badges, notices, "changed since" and the status line.
### Out of scope
@@ -767,276 +706,27 @@ Dewey approves the manifest. The webui suite is green on Sage's gate, along with
---
## C2-13a unit render and host precheck
Gitea #1732. Installer I2 with C2-13b, Vikunja P19 #432 (mos-1118). Re-briefed 2026-10-20 for Q189
(the Console starts at install and at boot) and row 190's landing.
## C2-13 install script and precheck for the host
### Problem
The hub runs on 10.1.1.54 as `mosaic` under `/srv/mosaic`, as two system units,
`mosaic-bus@<business>.service` and `mosaic-webui.service` (design 9 and 10 step 2c, r11). Nothing
renders those units for a system install, and nothing checks the host before the start. Row 195 needs
the same render and precheck for Case 1 on 10.1.1.53, so this part comes first and has no library
dependency (sage-1480, Q177).
### Owner and reviewer
- Owner: Gosalyn, Opus 5.5. The scripts run on a host only under Jason's go, by him or his delegate
(design 10).
- Reviewer: Webby.
- After: nothing. Row 195 chains on this row's candidate.
### Files owned
`scripts/case2-unit.sh` and `scripts/case2-precheck.sh` (new), `packages/cli/tests/case2-unit.test.mjs`
(new), and one line of `packages/cli/README.md`: at `:713`, "nothing in the repository creates or reads
it" changes to say that nothing creates it, that only the Case 2 precheck reads it, and that the
precheck prints only variable names, never values (webby-0034 N2, webby-0035).
### What ships: `case2-unit.sh`
`case2-unit.sh` is the 2c step. Root runs it from a root-owned checkout of the reviewed commit outside
`/srv/mosaic`, never from the release, because `mosaic` can write the release.
Inputs. `--profile factory|library` (default `library`), `--user`, `--home`, `--business`, `--copy ID`,
`--node PATH`, with `factory` only `--pm` and `--claude PATH`, and the Console's serve flags after `--`.
Row 195 calls it with those names. `library` refuses `--pm` and `--claude`.
- `--node` must be an absolute path to a root-owned regular file outside `<home>`.
- `--claude` follows the `--node` rule: an absolute path to a root-owned regular file outside `<home>`, so
nothing running as `<user>` can replace the binary the bus runs (sage-1517, darkwing on gosalyn-0057).
Row 195 installs claude under a root-owned prefix to match.
- The copy id must be exactly 40 lowercase hex characters naming a directory that holds
`scripts/mosaic`, directly under `<home>/.local/share/mosaic-stack/copies`. That is the rule
`copy_root` uses (`scripts/stack-copy.sh:8`, `:22`). Anything else refuses before the render.
- The serve flags. The script copies the checks from `serve_flags` (`scripts/webui-service.sh:201-264`)
into its own function, with its own tests. It doesn't source or change that script, because its
proof-file check compares the owner to `id -u` (`:257`), which is root here, and its render finds
node with `command -v` (`:294`), not `--node` (webby-0036 N3). The library profile takes exactly
these flags, each once, and refuses any other:
- `--port N`, with N in 1..1023 (design 9.3).
- `--business ID`, equal to `--business`.
- `--public-host NAME`, `--auth-header HEADER` and `--allow-user ID`, checked as
`webui-service.sh` checks them. Their values come from design 10 step 5.
- `--send`, because C2-10b's chat needs it.
- It refuses `--sso`. For 10-27 the library Console signs people in through the proxy, with the
preview set and the proof pair above. `webui-service.sh` refuses `--sso` with either (`:221-225`).
Taking `--sso` here waits for Q175 phase 2 (row 200 and its window), as a follow-up (gosalyn-0065 G2).
- `--proof-header NAME` and `--proof-file PATH`. The proof is required, not optional (design 9.3).
The file must be a regular file, not a symlink, owned by `<user>`'s uid from `getent passwd`, mode
0600 or 0400, under `<home>/.config/mosaic-dev/secrets/`. That keeps it out of the data root, where
a session's run directory is. The script stats it and never opens it.
- It refuses `--tester-port`. The tester listener takes instance, route, name and chat writes from
loopback with no sign-in (`packages/webui/src/serve.mjs:482`, `:552-553`).
- It refuses `--checkout`. Her host has no canonical checkout, and the queue store refuses a read
from the copy, so the queue panel isn't part of Case 2 (webby-0036 N4).
- It refuses `--board`. Case 2 runs no control board, and design 9.3 says why the default stays.
The factory profile (gosalyn-0057 items 1-3, sage-1517) takes `--port N` in 1..1023 and `--business ID`,
both required, and an optional `--tester-port N` in 1024..65535, never equal to `--port`, because
Case 1's acceptance uses the tester. .53 has no proxy, so `factory` refuses `--public-host`,
`--auth-header`, `--allow-user`, `--proof-header`, `--proof-file` and `--send`. It refuses `--checkout`
and `--board` as `library` does, and any other flag.
The render. It renders two system units from the checkout's existing templates and changes neither:
`[email protected]` from `packages/cli/systemd/mosaic-bus.service.in`, enabled as
`mosaic-bus@<business>.service`, and `mosaic-webui.service` from
`packages/webui/systemd/mosaic-webui.service.in`. Every token has one value (webby-0036 B2):
- `@REPO@` is the copy directory.
- `@NODE@` is `--node`.
- `@PATH@` is the directory of `--node` first, then whichever of `/usr/local/bin`, `/usr/bin` and `/bin`
it isn't, in that order. The bus's `ExecStart=` is `scripts/mosaic`, which runs `exec node` from
PATH (`scripts/mosaic:22-40`), so this line picks the bus's node. It differs from
`scripts/webui-service.sh:303-305` in one case on purpose. That script leaves the list alone when the
node's directory is already in it, so with `--node /usr/bin/node` a node in `/usr/local/bin` would
come first and the bus would run it.
- `@FLAGS@` is the checked serve flags, in the order given.
- `@PM_FLAGS@` is ` --pm` when `factory` is given `--pm`, as `scripts/bus-service.sh:82` renders it,
and empty otherwise.
- `@CLAUDE_ENV@` is the line `Environment=MOSAIC_CLAUDE_BIN=<--claude>` when `factory` is given
`--claude`, and empty otherwise.
- `@CONFIG_ENV@` and `@SESSION_ENV@`, which row 190 added to the templates (landed 00d4cc1f6). The
render refuses an unknown token, and row 190 landed first, so this row renders both, with tests
(gosalyn-0057 item 4). Fill them as `scripts/instance.sh:85-107` does: for the default instance
`dev` the `@CONFIG_ENV@` line is deleted and `@SESSION_ENV@` is the session env path under `%h`, which
the `%h` rule below then makes literal. This row renders the default instance only, with the
unsuffixed unit names below, and takes no `--instance`. A named instance on a Case 2 host is a
follow-up (gosalyn-0065 G1). The bus template's `EnvironmentFile=` line is now
`mosaic-bus.service.in:25`, `EnvironmentFile=-@SESSION_ENV@`, and the checks below match it there.
- `%h` becomes the literal `--home`, because in a system unit `%h` is root's home.
- After the render it refuses any `@[A-Z_]+@` or `%h` left in either unit.
Lines it adds or replaces. At the end of `[Service]` in both units it adds `User=<user>`,
`Group=<user's primary group>` and these four lines, in this order (webby-0036 B3):
```
ProtectSystem=strict
ReadWritePaths=<home>
ProtectHome=yes
PrivateTmp=yes
```
The templates' own `NoNewPrivileges=yes` stays. The Console unit also gets
`AmbientCapabilities=CAP_NET_BIND_SERVICE` and `CapabilityBoundingSet=CAP_NET_BIND_SERVICE`, so it can
bind its port below 1024 (design 9.3). The bus unit gets no capability line. It adds nothing else. No
line restricts namespaces or system calls. A session's bwrap needs a user, PID and IPC namespace
(`packages/seat/src/sandbox.mjs:67`) and a mount namespace, and `RestrictNamespaces=`,
`PrivateUsers=` or `SystemCallFilter=` can each take one away, so it refuses a template or render that
holds any of them.
- `[Install]`. Each unit renders `WantedBy=multi-user.target` as its one `WantedBy=` line. In the bus
unit it replaces `WantedBy=default.target`, not followed by a second line (webby-0036 N1). The
Console unit gets the same line, replacing `WantedBy=default.target` at `mosaic-webui.service.in:41` (Q189, mos-1122, mos-1123, gosalyn-0064 item 1). That reverses
mos-1097 item 1 and webby-0036 B1's "no `[Install]` on the Console".
- Root's 2c step enables both units, then starts the bus and then the Console (gosalyn-0064 item 2,
design ruling after r11 at 8614ba87c). Both come back after a boot on their own. Design 10 step 9 has
no hand start (item 3), and nothing in I1 or I3 starts the Console by hand either.
- No `After=` or `Wants=`. serve calls the bus per request, so the Console starts without the bus
(webby-0036 N2), and a boot that brings the Console up first is safe. The port below 1024, its
capability lines and its precheck stay (mos-1110).
Checks. It refuses unless `User=`, `Group=`, `ExecStart=`, the bus unit's `EnvironmentFile=`, the
hardening lines and the Console's two capability lines match fixed values. `EnvironmentFile=` is the
line in `packages/cli/systemd/mosaic-bus.service.in:25` (`:24` before row 190), with the literal home. The Console unit has
none, and it refuses one. It also refuses:
- any `PermissionsStartOnly=`, and any `Exec*=` value whose leading run of prefix characters (`@`, `-`,
`:`, `+`, `!`) holds a `+` or `!`. systemd takes those prefixes in any order, so `-+/srv/mosaic/x`
runs as root even with `User=mosaic`. The named-line check doesn't read these lines (webby-0026 N1,
webby-0027, defence in depth).
- on a re-install, an installed pair whose two units name different copy ids (webby-0036 N5). The bus
accepts only its own `human-cli.mjs` path, so a split pair breaks the human proof
(`scripts/webui-service.sh:324-348`).
- on a re-install, an installed unit that differs from the render in anything but the copy id. So the
node path and serve flags must equal the installed ones. The copy id is the one value that may
change, so a re-pin or a rollback to another copy re-renders both units together (row 195). With
`factory`, `--pm` and `--claude` must also match the installed bus unit.
It writes both units under `/etc/systemd/system/` as `root:root` 0644. It never uses `systemctl link`,
and it refuses a symlink at either unit path. It opens no file under `<home>`. It only stats the copy
directory, its `scripts/mosaic` and the proof file.
### What ships: `case2-precheck.sh`
It runs as `<user>` and takes `--profile factory|library`, default `library`. C2-13a ships the host
checks below. C2-13b adds the library checks to the same file. It refuses unless all of these hold:
- The config validates.
- `sudo -n true` fails as `<user>`, and `id -G` prints exactly one entry, its primary group. A system
user always has one, so the check is for supplementary groups.
- `bus.tester`: `library` refuses it on. `factory` requires it on, because Case 1's acceptance uses the
tester. `factory` refuses a business file that has a `kind: "library"` project, with
`profile-library-business`, so Case 2 can't reach a tester-on check through a wrong flag.
- The run root is 0700.
- `XDG_DATA_HOME` is unset or is `<home>/.local/share`. `copy_root` reads the copies dir from it
(`scripts/stack-copy.sh:8`), and the unit fixes `<home>/.local/share`, so any other value would put
the copy where the unit refuses it (webby-0032 N2).
- `node --version` is 24 or later, because every package's `engines` field says `>=24` (row 195: .53
has v22.22.2).
- `bwrap --unshare-all --ro-bind / / true` exits 0 as `<user>`. Without it no session starts. Row 195
found no bwrap on .53, and `kernel.apparmor_restrict_unprivileged_userns=1`, and .54 may be the same.
On a refusal, the precheck prints the sysctl's value and whether `bwrap` is on PATH. This runs in
`<user>`'s shell, outside the unit's hardening, so it can't show that the unit lets bwrap run. Design
10 step 9 checks that.
- `net.ipv4.ip_unprivileged_port_start` is above the Console's `--port`, read from the installed
Console unit. It refuses when the floor is at or below the port, because a lowered floor lets any
`mosaic` process bind the port, a session included, and makes the Console's capability pointless
(design 9.3, mos-1100).
- When some instance's harness is claude-code, one marker prompt run through the host's claude with
`--system-prompt-file` shows that it replaces the default prompt. The claude version is printed. The
host's claude is the installed bus unit's `MOSAIC_CLAUDE_BIN` when the unit sets one, as `factory`
with `--claude` does, and `claude` on PATH otherwise. With
the design's model path, Bean and Chet run pi, so this check is skipped unless she adds a claude-code
specialist.
- When an instance's provider is `zai`, `<configDir>/secrets/claude-session.env` exists, is a regular
file owned by `<user>` at 0600, and sets a nonempty `ZAI_API_KEY` (design 10 step 2d). It parses the
file the way systemd does, so the last assignment of a name wins (webby-0032 N3). A pi launch has no
credential check, so without this the first turn is where a missing key shows. The precheck prints
the name and never the value.
It also prints free memory and verifies that HOME is `<home>`.
### Tests
`case2-unit.sh` runs against a scratch unit directory. A clean render writes 0644 files, and these hold:
- the bus unit's `Environment=PATH=` starts with the `--node` directory, both for a `--node` outside the
default list and for `/usr/bin/node`, which gives `/usr/bin:/usr/local/bin:/bin`. Its `ExecStart=` is
the copy's `scripts/mosaic`
- the Console unit's `ExecStart=` starts with `--node` and holds the flags in order
- each unit has exactly one `WantedBy=`, `multi-user.target`
- against a fake `systemctl` that records its argv, a clean run enables both units, then starts the bus,
then the Console, in that order. A failed bus start refuses with the Console not started
- both units hold the four hardening lines once, in order, and only the Console holds the two
capability lines
- a check of the script for `systemctl link` finds nothing
- with `factory`, `--pm` renders an `ExecStart=` ending in `bus start <business> --pm` with one `--pm`,
and `--claude` renders exactly one `Environment=MOSAIC_CLAUDE_BIN=` line with that path. Without them
both tokens render empty
These cases refuse:
- an installed unit edited to `User=root`
- an edited `ExecStart=`
- an installed unit with a hardening line removed, and a template with `RestrictNamespaces=`,
`PrivateUsers=` or `SystemCallFilter=` added
- a bus template with the `EnvironmentFile=` line removed or pointing elsewhere, and a Console template
with one added
- a symlink at either unit path
- a `--node` that is relative, not root-owned, or under `<home>`
- a re-install with a different `--node` or different serve flags
- an installed pair whose units name different copy ids
- a template holding an unknown `@TOKEN@`, and an unrendered `%h` left in either unit
- a template with `ExecStartPre=+/srv/mosaic/x`, and one with `ExecStartPre=-+/srv/mosaic/x`
- a copy id that is short, uppercase, holds `/` or `..`, or names no directory
- with `library`: `--port 7332`, a missing proof pair, a proof file owned by root, at 0644, a symlink, or
under the data root, `--tester-port`, `--checkout`, `--board`, a `--business` different from the
unit's, `--pm`, `--claude`, and an unknown flag. With `factory`: `--tester-port` equal to `--port`, a
missing `--port` or `--business`, each of `--public-host`, `--auth-header`, `--allow-user`,
`--proof-header`, `--proof-file` and `--send`, a `--claude` that is relative, not root-owned or under
`<home>`, and a re-install with a different `--pm` or `--claude`.
The precheck runs against a scratch data root. Each refusal has a case. A fake `node` printing v22
refuses, a fake `bwrap` that exits 1 refuses, and an unprivileged port start at or below the Console's
port refuses. The claude check runs against a fake claude that appends rather than replaces, and that
case refuses. With no claude-code instance the claude check doesn't run, and a test shows a missing
claude passes. When the bus unit sets `MOSAIC_CLAUDE_BIN`, the check runs that binary: a replacing fake
there passes while an appending fake sits first on PATH, and the swap refuses. The key file refuses when missing, at 0644, a symlink, with `ZAI_API_KEY=` empty, and
with `ZAI_API_KEY=x` followed by `ZAI_API_KEY=`. A search of the test output for the fixture key value
finds nothing. `factory` passes with the tester on and refuses a business file with a library project.
`library` refuses with the tester on.
The bind test (mos-1100). A session can't take the Console's port. The test builds the argv a real
session gets, with `binds()` and `sandboxArgv()` from `packages/seat/src/sandbox.mjs`, around a node
one-liner that binds `127.0.0.1` on a port below 1024 and prints the error code. Inside bwrap the bind
fails with `EACCES`. The control runs the same one-liner in the same sandbox on a free port of 1024 or
above, and that bind succeeds, so the failure comes from the port floor and not from a broken sandbox.
The test reads `net.ipv4.ip_unprivileged_port_start` first. If the test host's floor is at or below the
chosen port, the test fails and says so, rather than skipping.
### Out of scope
Running either script on a host. Design 10 steps 2 to 9 are Jason's, and the root step 2c is ops-01's
over ssh as `localadmin`. The library checks and the install are C2-13b.
### Gate
Webby approves the manifest. The test file and the cli suite are green on Sage's gate, along with every
`scripts/test-*.sh`. A run on a host is a separate window with Jason's go.
---
## C2-13b install script and library precheck
Gitea #1727. Installer I2 with C2-13a, Vikunja P19 #432 (mos-1118).
### Problem
Nothing installs a library business on her host, and C2-13a's precheck doesn't yet check the library
parts.
The hub runs on 10.1.1.54 as `mosaic` under `/srv/mosaic`, as `mosaic-stack.service` with
`ProtectHome` (design 9, Q181). Nothing installs a library business there, or checks the host before it
starts.
### Owner and reviewer
- Owner: Gosalyn, Opus 5.5. The script runs only under Jason's go, by him or his delegate (design 10).
- Reviewer: Webby.
- After: C2-13a, C2-04, C2-03a, C2-20.
- After: C2-04, C2-03a, C2-20.
### Files owned
`scripts/case2-install.sh` (new), `scripts/case2-precheck.sh` (the library checks only) and
`packages/cli/tests/case2-install.test.mjs` (new).
`scripts/case2-install.sh`, `scripts/case2-precheck.sh` and `scripts/case2-unit.sh` (new), the template
`packages/cli/systemd/mosaic-stack.service.in` (new), `packages/cli/tests/case2-install.test.mjs`
(new), and one line of `packages/cli/README.md`: at `:713`, "nothing in the repository creates or reads
it" changes to say that nothing creates it, that only the Case 2 precheck reads it, and that the
precheck prints only variable names, never values (webby-0034 N2, webby-0035).
### What ships
@@ -1046,31 +736,92 @@ path. It runs as `<user>` through `sudo -u mosaic -H` from `localadmin`, and `mo
doesn't create `<user>` and writes no unit file. Root does those in design 10 steps 2a and 2c. Every
host value is an input, so a later host move changes only inputs.
Row 195 runs the install by input for Case 1 on 10.1.1.53 (darkwing to gosalyn, design at
`agents/darkwing/work/queue-195/DESIGN.md` section 2). It doesn't copy it. So the install takes
`--profile factory|library`, default `library`, as the precheck does:
Row 195 runs these scripts by input for Case 1 on 10.1.1.53 (darkwing to gosalyn, design at
`agents/darkwing/work/queue-195/DESIGN.md` section 2). They don't copy them. So the install and the
precheck take `--profile factory|library`, default `library`:
- `library` is everything this brief lists.
- `factory` skips the library mapping, the persona files and digests, the token slot, and
`library.sync` and `library.status`.
`library.sync` and `library.status`. It requires `bus.tester` on, because Case 1's acceptance uses the
tester.
- `factory` refuses a business file that has a `kind: "library"` project, with
`profile-library-business`. So Case 2 can't reach a tester-on check through a wrong flag.
- `library` refuses with `bus.tester` on, as before.
The precheck's library profile adds these checks to C2-13a's:
- The business file, `.history`, `changes.jsonl` and persona files have the modes in design 4, and each
persona digest matches.
`case2-unit.sh` is the 2c step. Root runs it from a root-owned checkout of the reviewed commit outside
`/srv/mosaic`, never from the release, because `mosaic` can write the release. It does the following:
- It renders the unit from the checkout's template, with the user, the home and the copy id as inputs.
- It refuses unless `User=`, `Group=`, `ExecStart=`, `EnvironmentFile=` and the hardening lines match
fixed values. `EnvironmentFile=` is the line in `packages/cli/systemd/mosaic-bus.service.in:24`. The
copy id is the one part of `ExecStart=` that varies, so a re-pin to a new copy can re-render the
unit (row 195). It must be exactly 40 lowercase hex characters naming a copy that holds `scripts/mosaic`,
directly under `<home>/.local/share/mosaic-stack/copies`. That is the rule `copy_root` uses
(`scripts/stack-copy.sh:6`, `:20`). Anything else refuses before the render.
- It refuses any `PermissionsStartOnly=`, and any `Exec*=` value whose leading run of prefix
characters (`@`, `-`, `:`, `+`, `!`) holds a `+` or `!`. systemd takes those prefixes in any order,
so `-+/srv/mosaic/x` runs as root even with `User=mosaic`. The named-line check doesn't read these
lines (webby-0026 N1, webby-0027, defence in depth).
- It writes `/etc/systemd/system/mosaic-stack.service` as `root:root` 0644. It never uses
`systemctl link`, and it refuses a symlink at the unit path.
- On a re-install, it refuses an installed unit that differs from the render.
- It reads nothing from under `<user>`'s home.
The precheck refuses unless all of these hold:
- The config validates.
- `sudo -n true` fails as `<user>`, and `id -G` prints exactly one entry, its primary group. A system
user always has one, so the check is for supplementary groups. `bus.tester` is not on.
- The business file, `.history`, `changes.jsonl` and persona files have the modes in design 4, and
each persona digest matches.
- The run root is 0700.
- `XDG_DATA_HOME` is unset or is `<home>/.local/share`. `copy_root` reads the copies dir from it
(`scripts/stack-copy.sh:6`), and the unit fixes `<home>/.local/share`, so any other value would
put the copy where the unit refuses it (webby-0032 N2). A test sets it elsewhere and the precheck
refuses.
- `node --version` is 24 or later, because every package's `engines` field says `>=24` (row 195: .53
has v22.22.2).
- `bwrap --unshare-all --ro-bind / / true` exits 0 as `<user>`. Without it no session starts. Row 195
found no bwrap on .53, and `kernel.apparmor_restrict_unprivileged_userns=1`, and .54 may be the same.
On a refusal, the precheck prints the sysctl's value and whether `bwrap` is on PATH.
- When some creative instance's harness is claude-code, one marker prompt run through the host's claude
with `--system-prompt-file` shows that it replaces the default prompt. The claude version is printed.
With design r9's model path, Bean and Chet run pi, so this check is skipped unless she adds a
claude-code specialist.
- When an instance's provider is `zai`, `<configDir>/secrets/claude-session.env` exists, is a regular
file owned by `<user>` at 0600, and sets a nonempty `ZAI_API_KEY` (design 10 step 2d). It parses the
file the way systemd does, so the last assignment of a name wins (webby-0032 N3). A pi launch
has no credential check, so without this the first turn is where a missing key shows. The precheck
prints the name and never the value.
- `library.sync` fetches her repo read-only, and `library.status` lists R-problems.
- No Discord binding exists.
### Tests
It also prints free memory and verifies HOME is `/srv/mosaic`.
They run against a scratch data root and a local bare repo. Each new precheck refusal has a case: a
persona file at 0644, a digest mismatch, a business file at the wrong mode, a sync failure and a
Discord binding. The install's `factory` profile writes no library mapping, persona file or token slot.
Tests run against a scratch data root and a local bare repo. Each precheck refusal has a case. The
claude check runs against a fake claude that appends rather than replaces, and that case refuses. With
no claude-code instance the claude check doesn't run, and a test shows a missing claude passes. The key
file refuses when missing, at 0644, a symlink, with `ZAI_API_KEY=` empty, and with `ZAI_API_KEY=x`
followed by `ZAI_API_KEY=`. A search of the test
output for the fixture key value finds nothing.
`case2-unit.sh` runs against a scratch unit directory. These cases refuse:
- an installed unit edited to `User=root`
- an edited `ExecStart=`
- a template with a hardening line removed
- a template with the `EnvironmentFile=` line removed or pointing elsewhere
- a symlink at the unit path
- a template with `ExecStartPre=+/srv/mosaic/x`
- a template with `ExecStartPre=-+/srv/mosaic/x`
- a copy id that is short, uppercase, holds `/` or `..`, or names no directory
Precheck cases for the new lines: a fake `node` printing v22 refuses, and a fake `bwrap` that exits 1
refuses. `--profile factory` passes with the tester on and no library items, and refuses a business
file with a library project. `--profile library` refuses with the tester on.
A clean render writes a 0644 file. A test checks the script for `systemctl link`.
### Out of scope
Running it on the host. Design 10 steps 2 to 7 are Jason's. Step 1 no longer waits on #421 (Q183), and
Mos lifted its hold (mos-1074). The root steps 2a and 2c are ops-01's, over ssh as `localadmin`, so the
install waits on #423 step 1 and on nothing else in #423. The Gitea bot account and the token slot are
steps 3 and 4. Stopping Hermes is step 9 and her choice.
Mos lifted its hold (mos-1074). The root steps 2a and 2c are ops-01's, over ssh as `localadmin`,
so the install waits on #423 step 1 and on nothing else in #423. The Gitea bot account and the token slot are steps 3 and 4. Stopping Hermes
is step 9 and her choice.
### Gate
@@ -1145,7 +896,7 @@ its first turn, as `packages/cli/README.md:439-444` says.
shell inside any of them can read it. Where the README names `claude-session.env` ("Unit", `:711`),
"The file holds one line" becomes "The file holds one line per variable", followed by a sentence
saying it holds every session provider key, not only Claude's (webby-0032, webby-0034 N1). Leave
`:713`, "nothing in the repository creates or reads it", alone. C2-13a owns that line.
`:713`, "nothing in the repository creates or reads it", alone. C2-13 owns that line.
- One test in `session.test.mjs`: `ENV_ALLOW` includes `ZAI_API_KEY`, and
`sessionEnv({ ZAI_API_KEY: "fixture-zai", ZAI_TOKEN: "t" }, run)` returns `ZAI_API_KEY` with the fixture
value and no `ZAI_TOKEN`. The existing allowlist test (`:51-59`) stays as it is.
-39
View File
@@ -68,42 +68,3 @@ Tests use scratch sshd fixtures on 127.0.0.1 or fakes, never a real host.
Duckworth approves DESIGN.md, then the manifest. The bus, business and cli suites and every
`scripts/test-*.sh` are green on Sage's gate, and the mutants are killed (one per refusal, plus one that leaks
the key into the agent's output and one that skips the host key pin).
## Machine grant row a: enrollment, store records and migration 3i-3j
Sage, 2026-10-20, for Vikunja P19 #436 (#436a), mos-1141 and mos-1143. Owner Gizmoduck, reviewer Duckworth,
gate owner Sage. The design is `agents/gizmoduck/work/machine-grant/DESIGN.md` r6 at f9039682e on
`gizmoduck/machine-grant-design`, which Duckworth approved for code once row 210 lands (duckworth-0063,
mos-1141). It is not reopened here. Section 12 Q-S4 defines the split. Estimate 1.5 days. Gitea #1740.
Row a ships enrollment as the approved request (section 3), the four `tools/host` files, the store records
and their migration `3i-3j.sql` (section 5), `host.run` reading the records, and `hosts status`. It also
moves row 210's tests onto the store. Every grant name is under `grant.*` behind one guard, as
`action.allowed` bodies (mos-1144, gizmoduck-0086, duckworth-0065 O1). No agent creates a grant (Q188).
Order and timing:
- It builds on row 210's landing, after row 199 (mos-1135, mos-1137).
- This row owns `SCHEMA_VERSION` 3j. Any other row with a schema step goes 3j to 3k or later and rebases
onto this row's landing: the #436b build takes 3j to 3k, then row 206 (193b2) 3k to 3l.
- It lands by 10-25 and before the .53 and .54 installs (mos-1134, mos-1145). Sage flags a slip past 10-25
to Mos. Live on dragon-lin comes in the one 10-24 window with rows 190, 199 and 210, as an agent-run
`--migrate` under Q173, backup first (mos-1145 line 9). The run sheet strips row 210's `hosts` keys and
role grants from the business file before that re-pin (design r5 N9).
- It needs the PRD 1.8 line before landing (Q-S4).
Gate: Duckworth approves the manifest. The bus, business and cli suites and every `scripts/test-*.sh` are
green on Sage's gate. The design's section 10 tests and mutants for row a pass, the migration test included.
## Machine grant row b: raise binding, executor, reconcile and revoke
Same owner, reviewer, gate owner and design as row a. Estimate 1.75 days. Chained on row a's landing.
Gitea #1741.
Row b ships the raise binding, the executor (section 5), reconcile, and revoke with the `hosts.revoke`
verb and its marker (section 6), plus row 210's per-call decision as manual mode (section 7). After step
5 commits, the executor ignores an abort, and both undo paths update only an existing record (r6 C1, X3,
X5). At start, after the grant reconcile, the bus unlinks every `host.<id>` key file that has no
`host_grants` record (r6 N1, G27). It adds no schema step. If it needs one, Sage re-briefs it first and
assigns the next free step (#436b holds 3j to 3k, row 206 3k to 3l).
Gate: as row a, with section 10's row b tests and mutants, G27, G28 and the upsert mutant included.

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