Bare `mosaic` on a terminal opens the app. It has Inbox, Tasks, Agents
and Trail screens, help, and a decide overlay, and it uses the alternate
screen, raw mode, resize and SIGTERM, and restores the terminal on exit.
draw(state) in screens.mjs is pure. cli.mjs exports resolveDecision,
which `mosaic decide` and the app share, so both write the same lines
and ask the same question. A decision resolves on `y` only while
decideShown() says the whole decide box is on screen. The app reads that
at the key. A box that doesn't fit is replaced by "Too small to show this
decision", and any key answers No. scripts/mosaic sends no arguments to
packages/cli (a decided scope addition), so the queue dispatch test
follows. The cli README and docs/TOOLS.md describe the app.
Darkwing built it (agents/darkwing/work/queue-55), in three rounds.
Round 3 was approved by Dewey (27308) and Filbert (27309) on manifest
59f1b9b4, 9 paths. Their non-blocking notes go to row 69 (#1562): a
narrow-terminal test for the decideShown MIN_COLS guard, a way out from
the hidden box, a U+18D08 width test, a cut box keeping Esc back, and
the README row count stated as an example.
Sage's gate on 3634d06e, sequential and teed: node suites cli 110 and
queue 150, 0 failed; test-auth 15, test-conductor 17, test-config 24,
test-discord 66, test-extension-package 18, test-foundation 44,
test-queue 27, test-release 14, test-task 98, all 0 failed (Docker on).
122b1223..3634d06e touches none of the 9 paths.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
429 lines
22 KiB
Markdown
429 lines
22 KiB
Markdown
# CLI
|
|
|
|
Jason's front door to the bus (slice 1 row S4, #1521): `mosaic inbox`,
|
|
`decide`, `tasks`, `agents` and `trail`, and the bus host `mosaic bus
|
|
start|stop|status` with its Discord notifier (row S4, #1521); the session
|
|
launcher in the host and `mosaic talk`, `stop` and `launches` (row S6,
|
|
#1523); and `mosaic` with no argument, the full-screen app (queue row 55,
|
|
#1544). Briefs: `docs/plans/2026-10-04_slice-1.md`, rows S4 and S6, and
|
|
`docs/plans/2026-10-10_design-implementation.md`, "Full-screen terminal
|
|
app". Rulings: lead decisions 70 and 77 in
|
|
`docs/plans/2026-09-26_lead-decisions.md`.
|
|
|
|
Every command reads through the broker. None opens the SQLite file.
|
|
|
|
```sh
|
|
scripts/mosaic # the app, on a terminal
|
|
scripts/mosaic inbox
|
|
scripts/mosaic decide 3f2a9c1e yes --note "ship it"
|
|
scripts/mosaic trail 3f2a9c1e-… # then: scripts/mosaic trail vikunja:1/7
|
|
scripts/mosaic bus status
|
|
node --test 'packages/cli/tests/*.test.mjs'
|
|
```
|
|
|
|
## Human commands
|
|
|
|
```
|
|
mosaic inbox [--business <id>] [--json]
|
|
mosaic decide <decision> <option> [--note <text>] [--yes] [--business <id>]
|
|
mosaic tasks [--business <id>] [--json]
|
|
mosaic agents [--business <id>] [--json]
|
|
mosaic trail <task|decision> [--business <id>] [--json]
|
|
mosaic talk <role> <text> [--wait <seconds>] [--business <id>]
|
|
mosaic stop <run>
|
|
mosaic launches off|on [--business <id>]
|
|
mosaic launches list [--json]
|
|
```
|
|
|
|
- The business is `--business`, or else the business of the bus host running
|
|
on this data root. With neither, the command exits 4.
|
|
- The transport is `packages/bus/src/human-cli.mjs <socket>`, run as a child.
|
|
The command writes `{business, verb, args}` on its stdin and reads the JSON
|
|
reply on its stdout. The child proves a human shell: it re-executes itself
|
|
with a nonce, and the broker checks its process and ancestry for agent
|
|
markers. The command carries no capability and the bus is unchanged.
|
|
- Each command refuses with exit 3 before it touches the bus when an agent
|
|
marker is in its environment (REQ-DEC-3). The broker would refuse anyway;
|
|
this check only gives a clear message first. **Agent seats never run
|
|
these commands.** The tests use a stand-in transport.
|
|
- `inbox` lists the open decisions routed to the human, gated first, in
|
|
broker order. For a gated decision it prints what approving authorizes:
|
|
the action, its target, and the one choice that authorizes it.
|
|
- `decide` takes the full id, or a unique prefix of at least 8 characters,
|
|
from the inbox. It prints the decision and whether the choice authorizes
|
|
or declines, then asks `[y/N]` on a terminal. Without a terminal it needs
|
|
`--yes`, else it exits 4. On `outcome-unknown` it does not resend: it
|
|
exits 1 and asks you to check `mosaic inbox` or `mosaic trail <id>`
|
|
first.
|
|
- `trail` prints rows in the broker's order (at, table, seq). A decision's
|
|
trail ends with its `task_ref` and `follow with: mosaic trail <task>`. It
|
|
does not pull in the task's rows.
|
|
- `talk` sends `<text>` to the role as a REQUEST, then polls the human's
|
|
inbox every 2 s for up to `--wait` seconds (default 900). It prints every
|
|
message that arrives and marks it read, and stops at the reply to its
|
|
REQUEST. No reply in time exits 1; the reply still lands in the inbox.
|
|
`--wait 0` only sends.
|
|
- `launches off` and `on` are the broker's `launch.revoke` and
|
|
`launch.restore`. While off, every `role.launch` refuses with
|
|
`launch-revoked`; running sessions keep running. Stop them with `stop`.
|
|
`bus start --pm` doesn't ask the broker (`launchPm` skips
|
|
`authorizeLaunch`): the human launches the PM, so `launches off` doesn't
|
|
stop it. The other checks in "Sessions" below still apply.
|
|
- `stop` and `launches list` read `<dataRoot>/bus-host/sessions.json`, not
|
|
the bus (see "Sessions" below). `stop` refuses inside an agent run;
|
|
`launches list` does not, because the file is readable to the same user
|
|
anyway.
|
|
|
|
## The app
|
|
|
|
```
|
|
mosaic
|
|
```
|
|
|
|
With no argument, on a terminal, `mosaic` opens a full-screen app over the
|
|
same broker reads as the verbs: Inbox, Tasks, Agents, Trail and Help.
|
|
`src/app.mjs` handles keys and reads; `src/screens.mjs` draws the frames
|
|
and nothing else.
|
|
|
|
- In an agent run it exits 3 with the same refusal as the verbs, before
|
|
anything else. Without a terminal on both stdin and stdout it exits 4
|
|
and prints the seat usage (`mosaic launch`, `mosaic seat task`) and the
|
|
verbs above. A script never gets the app.
|
|
- The business is the one of the bus host on this data root. With no
|
|
host the app says so and `:business <id>` picks one.
|
|
- Keys: `c i t a r` switch views (Talk, Inbox, Tasks, Agents, Trail). `↑↓`
|
|
or `j k` move, PgUp and PgDn page, Enter opens the selection whole, `v`
|
|
opens its trail, `:` the command line, `?` help, Esc closes a box, `q`
|
|
or Ctrl-C quits. On the command line, Esc or Ctrl-C closes it and the
|
|
arrows type nothing. Commands: `:decide <decision> <option>`, `:trail
|
|
<task|decision>`, `:business <id>`, `:q`.
|
|
- Deciding: in the Inbox, digit `n` picks the selected decision's `n`th
|
|
option and runs `resolveDecision` in `src/cli.mjs`, the code `mosaic
|
|
decide` runs. The box shows the same lines and the same `[y/N]`
|
|
question. `y` or `Y` resolves; any other key answers No, and so does
|
|
quitting. Keys that arrive within 200 ms of the question being drawn
|
|
are type-ahead and are dropped: a paste such as `1 yes`, or keys typed
|
|
while a read held the screen, never answer it. After `outcome unknown` the app shows the CLI's sentence and
|
|
offers the trail (`v`) or a re-read (`r`). No key sends the choice
|
|
again.
|
|
- Reads happen on entering a view, after a decision, and every 30 s while
|
|
no box is open. A failed read keeps the last list on screen and marks
|
|
it stale. A refused read (exit 3 codes) says refused. Broker text goes
|
|
through `visible()` from `packages/conversation/src/terminal.mjs`, so
|
|
control characters and bidi overrides show as `^[` and `<U+202E>`.
|
|
- Talk (`c`) points to `mosaic talk` for now. The conversation pane and
|
|
`stop` come in a later row.
|
|
- 16 ANSI colours. Under `NO_COLOR` the only escape codes in a frame are
|
|
reverse video on the selected row (the design's tokens keep selection
|
|
visible without colour). Frames fit 80x24; the layout is designed at
|
|
120x36, and under 100 columns the nav shows keys only and the Inbox
|
|
drops RAISED BY. Below 40x10 the frame only says the terminal is too
|
|
small. If the decide question is open then, the frame says so, and any
|
|
key answers it No, `y` included. Widths count terminal cells: East
|
|
Asian Width W and F characters and emoji 2, a one-cell emoji base with
|
|
U+FE0F 2. ZWJ sequences and flags count more than they draw, so such a
|
|
row can come out short, never too wide. Wrapping breaks between
|
|
graphemes, so U+FE0F and combining marks stay with their base.
|
|
- A decide box taller than the screen cuts the end of the question and
|
|
nothing else: the decision line, the options, the recommendation, the
|
|
action and target, what the choice authorizes, the task, your choice
|
|
and the `[y/N]` question stay. The cut line points to `mosaic inbox
|
|
--json`. When even those lines don't fit, the box says only "Too small
|
|
to show this decision. Any key answers No." `y` resolves only while the
|
|
frame shows the whole box apart from the cut, and the app checks the
|
|
size at each key, so a terminal shrunk after the question appeared
|
|
answers No. A gated decision with a task needs 20 rows at 80 columns
|
|
(21 with a cut question), and 29 rows at 40 columns.
|
|
- The transport is the verbs' `spawnSync` child, so the screen does not
|
|
redraw while a read is in flight (up to its 30 s timeout).
|
|
- On quit, end of input, SIGTERM, SIGINT, SIGHUP or an error (one in a
|
|
resize redraw included) the app restores the screen, the cursor and
|
|
cooked mode. After anything but an error it then prints
|
|
`(app closed with q; N decisions resolved this session)`; an error
|
|
goes to stderr instead.
|
|
|
|
## Bus host
|
|
|
|
```
|
|
mosaic bus start <business> [--pm]
|
|
the host; run by the unit, not by hand.
|
|
--pm also launches the business's PM
|
|
mosaic bus stop SIGTERM to the recorded host, then wait
|
|
mosaic bus status [--json] host state file, socket, writer.lock
|
|
```
|
|
|
|
`bus start` does the following:
|
|
|
|
1. It reads the system config through `scripts/mosaic-config.mjs validate`,
|
|
the business file and the role files, and builds the broker's
|
|
`{op: 'boot'}` message (`src/config.mjs`). The message has `launches: []`
|
|
and `readers: [<business>]`. It gets `config.trackers` (below) when a
|
|
tracker is set.
|
|
2. It reads the notifier config (below).
|
|
3. It forks `packages/bus/src/process.mjs` and waits up to 30 s for the boot
|
|
reply. If the broker answers `{ok: false, error}`, the host exits 3 and
|
|
prints `broker refused to start: <code>`. A timeout or an early exit
|
|
gives exit 1.
|
|
4. It forks `src/notifier-process.mjs` and hands it the reader capability
|
|
over IPC in `{op: 'start'}`. A notifier refusal closes the broker, and
|
|
the host exits 3.
|
|
5. It writes `<dataRoot>/bus-host/host.json` (0600): the pid, the process
|
|
start time, the business, the start time as text and the notifier
|
|
binding. The file holds no capability.
|
|
6. It starts the session launcher (`src/launcher.mjs`, "Sessions" below)
|
|
and, with `--pm`, launches the business's `launch.by` instance. A PM
|
|
launch that refuses closes the host with the refusal's exit code.
|
|
|
|
No capability appears in argv, stdout, the environment or a file.
|
|
`startHost()` in `src/host.mjs` is also the in-process API the launcher
|
|
uses: `bindLaunch(record)` binds a launched run's process identity
|
|
(`pid`, `startTime`) and returns `{business, run, cap}`; `op(message)` sends
|
|
one of the broker process's launch ops (`identity`, `authorizeLaunch`,
|
|
`refuse`, `endLaunch`, `credentialStatus`); `beforeClose(fn)` runs `fn`
|
|
before the broker closes, so the launcher stops its sessions first.
|
|
Requests to the broker process go one at a time. Each carries an id, and
|
|
the broker echoes it on the reply. If a reply doesn't arrive within 10 s, or
|
|
arrives with an id no request is waiting for, the channel is broken: the
|
|
waiting request refuses with `broker-channel-broken`, so does every later
|
|
one, and the host stops with exit 1 for the unit to restart. A late reply
|
|
never answers a later request, and a launch waiting on one refuses.
|
|
|
|
SIGTERM or SIGINT stops the notifier after its poll in flight, then closes
|
|
the broker and removes `host.json`. If either child dies on its own, the
|
|
host goes down with exit 1. The unit then restarts both. The host watches
|
|
the children once both have started, and it checks each child's exit state
|
|
at that point too, so a broker that dies while the notifier starts still
|
|
takes the host down.
|
|
|
|
`bus stop` signals only a pid that still runs with the recorded start time
|
|
and whose command line is `…/packages/cli/src/cli.mjs bus start`.
|
|
`bus status` never removes a lock. A `writer.lock` whose pid is gone means
|
|
a broker was killed hard. Check, then remove the lock by hand
|
|
(`packages/bus/README.md`).
|
|
|
|
### Sessions
|
|
|
|
The launcher (`src/launcher.mjs`) starts managed role sessions
|
|
(`packages/seat/src/session.mjs`, `packages/harness`). It serves
|
|
`<dataRoot>/bus-host/launch.sock` (0600), which the PM's `launch` tool
|
|
calls through its runner. One request is one line,
|
|
`{"cap": "…", "instance": "<role instance>"}`, at most 4096 bytes; the reply
|
|
is `{"ok": true, "result": {run, instance, harness, model}}` or
|
|
`{"ok": false, "error": "<code>"}`. Launches run one at a time. In order:
|
|
|
|
| Check | Refusal |
|
|
|---|---|
|
|
| the request is one JSON line with a string instance | `invalid-request` |
|
|
| the capability is live and names a role of this business | `unauthenticated`, `not-a-role` |
|
|
| the business file has a `launch` block | `launch-not-configured` |
|
|
| `launch.instances` lists the instance (for `--pm`: it is `launch.by`) | `instance-not-listed` |
|
|
| the instance isn't running | `instance-running` |
|
|
| the instance resolves to `pi` or `claude-code` and a model | `unsupported-harness` |
|
|
| its role has a contract file | `no-role-contract` |
|
|
| its model names exactly one `launch.max` family | `unknown-model-family` |
|
|
| that family has room; every running session counts, the PM's too | `capacity-full` |
|
|
| the broker authorizes `role.launch` for the caller | the broker's code (`launch-revoked`, `decision-required`, …) |
|
|
| the bundle builds and the runner starts | `launch-failed` |
|
|
|
|
Every refusal is an `action.refused` event (the broker's `refuse`, or its
|
|
own refusal for the last broker check) and a line in the launch log. Then
|
|
the launcher reads the instance's credential status from the broker
|
|
(metadata only), builds the bundle in the run directory, spawns the runner
|
|
under `unshare`, binds the outer process's pid and start time, adds the
|
|
session to `sessions.json`, and writes the capability as one line on the
|
|
runner's stdin. The capability is never on disk, in argv or in the
|
|
environment. When the runner exits, the launcher sends `endLaunch` with the
|
|
end reason from its exit code (`packages/harness/README.md`), which writes
|
|
`session.ended`, releases the role and kills the run's capabilities.
|
|
|
|
The manifest records the Pi version from the repository's pin (the
|
|
`@earendil-works/pi-coding-agent` dependency in the root `package.json`)
|
|
and, for Claude Code, `claude --version`.
|
|
|
|
On disk, under the data root: `launches/<business>/<run>/` (0700) is the run
|
|
directory; `launches/<business>.jsonl` (0600, append-only) logs every launch,
|
|
refusal and end; `workspaces/<business>/<instance>/` (0700) is kept across
|
|
launches; `bus-host/sessions.json` (0600) lists the running sessions.
|
|
|
|
When the host closes, the launcher stops taking requests and drops every
|
|
open `launch.sock` connection, so a client that never ends its side can't
|
|
hold the close. It sends SIGTERM to
|
|
every runner (again each second, see "Limits"), waits up to 30 s, then
|
|
SIGKILLs what is left and ends those launches as `killed`.
|
|
|
|
A host that starts reads the `sessions.json` the last host left. An entry
|
|
for this business means that host died without ending the launch, and its
|
|
run would keep holding the role. Before the launch socket opens, the
|
|
launcher SIGKILLs each such session that still runs (same start time,
|
|
`unshare` on its command line), binds the run again with the recorded
|
|
identity and ends it as `host-lost`, which releases the role. A
|
|
`sessions.json` that can't be read refuses the host start with exit 3; move
|
|
it aside after checking which sessions still run.
|
|
|
|
`mosaic stop <run>` checks that the runner in `sessions.json` still runs with
|
|
the recorded start time and that its command line is the runner's, then
|
|
sends SIGTERM until it exits (30 s). A stale entry prints "not running" and
|
|
exits 0; an unknown run exits 2.
|
|
|
|
### Trackers
|
|
|
|
`config.trackers[<business>]` is `{baseUrl, project, pollSeconds,
|
|
reconcileMinutes}`, taken from the `tracker.*` variables. `tracker.project` is
|
|
a project-layer variable, so the entry comes from the one declared project
|
|
whose `.mosaic/project.json` sets it. The resulting entry depends on the
|
|
variables:
|
|
|
|
| Variables | Result |
|
|
|---|---|
|
|
| No `tracker.baseUrl` | No entry and no task verbs |
|
|
| `tracker.baseUrl` set, no project setting `tracker.project` | A warning on stderr and no entry |
|
|
| Two projects setting `tracker.project` | Refusal with exit 3, because the boot shape holds one tracker project per business |
|
|
|
|
### Unit
|
|
|
|
`scripts/bus-service.sh render|install|uninstall|status` installs the
|
|
template `systemd/mosaic-bus.service.in` as the systemd user unit
|
|
`[email protected]`, one instance per business (`mosaic-bus@<business>`).
|
|
The template's file name has no `@` because queue candidate manifests refuse
|
|
it. It is modelled on `scripts/discord-service.sh`:
|
|
|
|
- `install [--dir DIR] [--no-reload]` writes the unit through a temp file and
|
|
a rename, then runs `systemctl --user daemon-reload`.
|
|
- `uninstall` refuses while an instance is active.
|
|
|
|
The unit settings:
|
|
|
|
- `ExecStart=@REPO@/scripts/mosaic bus start %i`
|
|
- `Restart=on-failure`
|
|
- `RestartPreventExitStatus=2 3 4`, so refusals are never retried
|
|
- `KillSignal=SIGTERM`
|
|
- `TimeoutStopSec=60`
|
|
|
|
The unit has no `network-online.target` ordering, since a user unit cannot
|
|
order on that system target. The notifier needs no network at start: a
|
|
send that fails is journaled and retried.
|
|
|
|
## Notifier
|
|
|
|
The notifier is a child of the host. Each poll (every 30 s) it reads the
|
|
inbox through the reader capability and does two things:
|
|
|
|
- **DMs.** It DMs each open blocking decision once. The DM holds the
|
|
question, the options, the recommendation, what approving authorizes, the
|
|
task, and `mosaic decide <short id> <option>`.
|
|
- **Digest.** It sends one digest a day at 08:00 America/Chicago. The hour
|
|
comes from the IANA zone, so daylight saving time is handled. If the host
|
|
starts after 08:00 and the day has no digest yet, the digest goes at once.
|
|
The digest lists the inbox. It marks each blocking decision with one of
|
|
three states: "DM sent", "DM pending" or "DM refused, not retried". An
|
|
empty inbox gets one line. Each message stays within Discord's 2000
|
|
characters.
|
|
|
|
The notifier only reads the bus. Its memory is the journal
|
|
`<dataRoot>/notify/<business>/sent.jsonl` (directory 0700, file 0600), with
|
|
one line per send attempt:
|
|
|
|
```json
|
|
{"at": "…", "kind": "dm", "decision": "<id>", "outcome": "confirmed", "messageId": "…"}
|
|
{"at": "…", "kind": "digest", "decision": null, "day": "2026-10-08", "outcome": "unknown", "messageId": null}
|
|
```
|
|
|
|
- A decision, or a day's digest, counts as sent once it has a `confirmed`
|
|
line.
|
|
- A `refused` or `unknown` send is retried. After an `unknown` outcome the
|
|
wait starts at 30 s and doubles up to 30 min. A duplicate costs less than
|
|
a miss. Every retry
|
|
carries the same Discord nonce, so a retry inside Discord's dedupe window
|
|
returns the first message. A DM's nonce comes from the decision id. A
|
|
digest's comes from the business and the day.
|
|
- A definite refusal is a DM refused with an HTTP 4xx other than 429. The
|
|
next attempt waits the full 30 min, counted from the refusal's `at` in
|
|
the journal, so a restart does not shorten it. After 5 of them for one
|
|
decision, at least 2 hours apart end to end, the notifier appends one
|
|
`gave-up` line, logs it once and never sends that DM again. The digest
|
|
marks the decision "DM refused, not retried". The count comes from the
|
|
journal, so a restart keeps it. An `unknown` outcome (network, 5xx or
|
|
429) retries without a limit (lead decisions 72 and 73).
|
|
- Recovery after a give-up is manual. Fixing the binding does not resend
|
|
the DM, which stays given up. The decision stays open in `mosaic inbox`,
|
|
the digest lists it, and the operator decides it with `mosaic decide`.
|
|
- On open, a final line without its newline is a torn write. The notifier
|
|
copies those bytes to `torn-<UTC stamp>.bin` in the same directory (0600,
|
|
a new file, synced), then truncates `sent.jsonl` to its last newline and
|
|
syncs it. It logs both steps. A crash between the two leaves the torn
|
|
tail in place, and the next open repairs it with a second copy. The next
|
|
append therefore starts on a line of its own.
|
|
- A malformed complete line refuses with exit 3 and changes nothing. Each
|
|
line is type-checked: `at` an ISO timestamp, `kind` `dm` or `digest`,
|
|
`decision` a string (required for `dm`, null for `digest`), `day` a real
|
|
`YYYY-MM-DD` date (required for `digest`), `outcome` one of `confirmed`,
|
|
`refused`, `unknown` or `gave-up` (`gave-up` only for `dm`), `messageId`
|
|
a string (required when confirmed) or null, `status` an integer when
|
|
present.
|
|
- The journal refuses with exit 3 when its directory is looser than 0700,
|
|
not yours, a symlink (dangling or not) or not writable, or cannot be
|
|
created (for example, a parent is a file). It also refuses when
|
|
`sent.jsonl` is a symlink, a directory or not writable, or is not a
|
|
regular 0600 file you own. The message names the path.
|
|
- No Discord channel or user id goes in the journal, a log line or an
|
|
error.
|
|
|
|
The Discord side is `packages/discord/src/notify.mjs`. It uses the
|
|
connector's REST client and the binding's `tokenFile`, and sends to the
|
|
binding's fixed `dmRecipient`, who must be one of the binding's `users`.
|
|
|
|
### Notifier config
|
|
|
|
`<dataRoot>/notify/<business>/notify.json`, mode 0600, owned by you. The
|
|
journal shares the directory, so create it 0700 first (`mkdir -m 0700 -p
|
|
<dataRoot>/notify/<business>`). A host with a binding refuses with exit 3 on
|
|
a looser directory.
|
|
|
|
```json
|
|
{"notifyVersion": 1, "binding": "sage-seat"}
|
|
```
|
|
|
|
`"binding": null` runs the host without DMs. A missing file refuses with
|
|
exit 3, so a host never starts until someone decides whether it DMs.
|
|
|
|
## Exit codes
|
|
|
|
| Code | Meaning |
|
|
|---|---|
|
|
| 0 | ok |
|
|
| 1 | failed, or outcome unknown: check before retrying |
|
|
| 2 | invalid input: unknown option, no such open decision, decision already closed, no such run, not a runner |
|
|
| 3 | refused or config problem: inside an agent run, human proof failed, unknown business, bad config, broker or notifier refused to start |
|
|
| 4 | usage |
|
|
|
|
## Limits
|
|
|
|
- **One broker per data root.** The bus store takes
|
|
`<dataRoot>/bus/writer.lock`. The unit is a template per business, but
|
|
only one instance can run on one data root.
|
|
- **`digest.sent` is unused.** The bus schema has this event kind, but
|
|
decision 70 puts the notifier's memory in the journal, and a reader
|
|
capability cannot write events.
|
|
- **The real human transport is untested here.** No test runs the real
|
|
`human-cli.mjs`, because its proof needs a human shell. The live run
|
|
covers it.
|
|
- **`stop` is same-UID process control.** It checks the start time and the
|
|
command line, not authority. Any process of the same user can signal a
|
|
runner directly.
|
|
- **Signals before the runner's handlers.** A session's runner is pid 1 of
|
|
its PID namespace, and pid 1 ignores a signal it has no handler for. The
|
|
runner installs its handlers first, but Node's startup leaves a short
|
|
window, so the launcher and `stop` resend SIGTERM every second.
|
|
- **If the host dies hard,** its sessions keep running until their broker
|
|
polls fail, then exit 23, and their launches stay open in the bus until
|
|
the next host starts and ends them as `host-lost` ("Sessions" above).
|
|
With no next host, nothing ends them.
|
|
- **`talk` and the launches verbs are tested only with the in-process
|
|
transport,** as the other human commands are.
|
|
- **Tracker boot is tested only without trackers.** The `trackers` boot case
|
|
is tested once S3's broker change lands.
|
|
|
|
## Not in this piece
|
|
|
|
The WebUI (S5).
|