# S3 live run `run.mjs` takes one task through every verb against a real Vikunja, with the real broker and adapter in one process. It's the row 38 acceptance run against the estate instance, and it was rehearsed against a scratch container first. ```sh node packages/tasks/live/run.mjs setup.json out/ --base https://tasks.example.org ``` `--base` has to repeat `baseUrl` from the setup file, or the run exits 2 before it reads a token. A setup file copied from another install can't send the run to the wrong host without the operator typing that host. ## Setup file ```json { "business": "demo", "baseUrl": "https://tasks.example.org", "project": 12, "sync": {"botId": 41, "file": "/home/op/.config/mosaic-dev/tokens/demo-sync", "expires": "2027-01-01"}, "roles": { "pm": {"botId": 42, "env": "VK_PM", "expires": "2027-01-01"}, "coder": {"botId": 43, "env": "VK_CODER", "expires": "2027-01-01"}, "reviewer": {"botId": 44, "env": "VK_REVIEWER", "expires": "2027-01-01"} }, "labels": {"slice-1": 7} } ``` Each token is a file or an environment variable, never a value in this file. A token file follows the broker's rules: an absolute path, mode 0600, owned by the operator, not a symlink. `expires` is the day the token was minted to expire, as `YYYY-MM-DD`. The project, buckets, bots and labels come from runbook sections 2 and 3 (`docs/guides/slice-1-identities.md`). The first label in `labels` is added at create and removed at schedule. Leave `labels` empty to skip both. ## What it does The run builds a business with an `operator` human and the pm as arbiter for both domains, then starts the broker with the adapter in a fresh data directory under `TMPDIR`. The three roles claim their launches and the adapter runs its startup checks and one reconcile. Then the steps, in order. Each one either succeeds or refuses with the code the step expects: | Step | Role | Verb | Expected | |---|---|---|---| | create-by-coder | coder | `task.create` | `field-writer` | | create | pm | `task.create` | ok, with the label | | assign | pm | `task.assign` to coder | ok | | update-by-unassigned | reviewer | `task.update.assigned` | `not-assigned` | | start | coder | `task.update.assigned` in-progress, 25%, comment | ok | | conflict | pm | `task.priority.change` with a wrong `expect` | `task-conflict` | | priority-undecided | pm | `task.priority.change`, no decision | `decision-required` | | priority | pm | `task.priority.change` with a resolved decision | ok | | priority-reused | pm | the same decision again | `decision-consumed` | | schedule | pm | `task.schedule` due date, label removed | ok | | scope | pm | `task.scope.change` with a resolved decision | ok | | review | coder | `task.update.assigned` in-review, 100% | ok | | reassign | pm | `task.reassign` to reviewer | ok | | close | pm | `task.close` | ok | | after-close | reviewer | `task.update.assigned` | `task-done` | Priority and scope are cross-role for the pm, so each needs a decision. The pm raises it, and the operator resolves it through the broker's human CLI binding. A tick after the start step and a tick and reconcile at the end must each record 0 snapshots, since the only writes were the run's own. The run then prints the event counts by kind and the snapshot counts by source and role, read from `bus.sqlite`. It exits 0 when every step went as expected, 1 when one didn't or the run stopped, and 2 on a bad command line or setup file. The task stays in the project, done, with "Safe to delete" in its description. ## The log `out/live-run.txt`, mode 0600, holds verbs, refusal codes, counts and task refs. It holds no token, title, description or comment. Before it writes, the run checks the log text for every token and for the word "bearer". A hit writes nothing and exits 3. The data directory is removed when the run ends.