# Runs Read-only readers for the run records under `/runs/` and the release state under `/state/` (#1545). `scripts/mosaic-task.mjs list` and `show` read through it, and so will the console's runs view. This README covers what the code does and the limits it accepts. ```sh node --test packages/runs/tests/ ``` ## What it reads | Export | Returns | |---|---| | `listRunIds(dataRoot)` | every name under `runs/` that starts with `r-`, sorted, which is oldest first | | `listRunRecords(dataRoot)` | `[{ runId, result }]` for those names; `result` is `null` for an incomplete or unreadable record | | `readRunRecord(dataRoot, runId)` | `{ runId, result, task, mission, artifacts }`, or `null` when the run doesn't exist or isn't a directory | | `readRunDocument(dataRoot, runId, name)` | `result.json`, `task.json` or `mission.json` from one run, or `null` | | `readActivePointer(dataRoot)` | `state/active.json`, or `null` when no release has been activated | | `readActivationLog(dataRoot, { last })` | `{ entries, malformed }` from `state/activation-log.jsonl`, oldest first | | `isRunId(value)`, `RUN_ID_PATTERN` | the run id shape `mosaic-task.mjs` checks: `r-` then 1 to 64 of `[A-Za-z0-9._-]`, starting with a letter or digit | The caller passes `dataRoot`; this package doesn't read the system config. `artifacts` are names in directory order, as `show` has always printed them. ## Limits - **Nothing writes.** No reader creates, changes, prunes or locks anything. Run records stay write-once evidence; pruning stays in `mosaic-task.mjs prune`. - **Nothing follows a link out of the data root.** The configured data root is trusted as given, even when it is itself a link. Every path below it is resolved, and one that resolves outside it is refused or read as unreadable: - `runs/` or `state/` resolving outside refuses with a `RunsError`; - a run directory resolving outside: `readRunRecord` refuses, and `listRunRecords` gives that run a `null` result; - a run document resolving outside reads as `null`; - `active.json` or `activation-log.jsonl` resolving outside refuses. Only a path that exists can resolve. A link with nothing behind it reads as missing wherever it points. A `state/` link out of the data root to a path that doesn't exist reads as no release and an empty log, and a `runs/` link like that lists nothing. A link that stays inside the data root is followed. - **Run documents are JSON objects or `null`.** A document that is missing, unreadable, not JSON, or JSON but not an object (`null`, `5`, `[]`) reads as `null`. Run records are never validated beyond that, because older records carry older shapes. - **The release pointer is strict.** `active.json` must be the version 1 shape `scripts/release.sh` writes: exactly `pointerVersion` 1 and non-empty strings `release`, `imageTag` and `activatedAt`. Anything else refuses with exit code 2 rather than being guessed at. - **The activation log is read per line.** One bad line never refuses the log. An entry needs string `at`, `event`, `release` and `imageTag`, an optional string `note`, and no other keys. A line that isn't JSON, or is JSON with another shape, is counted in `malformed` and skipped. That is stricter than `release.sh rollback`, which skips only lines that aren't JSON and would act on an entry with an extra key or a non-string field. Blank lines aren't counted. `last` must be a positive integer and keeps the newest entries. - **Errors.** Every refusal is a `RunsError` with `exitCode` 2 (invalid data) or 4 (a file or environment problem), matching `mosaic-task.mjs`. A missing data root, `runs/` or `state/` file is not an error. A path that can't be resolved for another reason (a link loop, a permission error) or a directory that can't be read refuses instead of reading as empty. ## How mosaic-task.mjs uses it `list` and `show` print exactly what they printed before this package existed, for any data root with no link out of it, no run document that is JSON but not an object, no `result.json` with a field of the wrong type (a `status` or `taskId` that isn't a string, or a `workspace` or `session` that is neither a string nor `null`), and no unreadable directory. `agents/rocko/work/queue-56/byte-check.sh` checks that byte for byte against a seeded data root. Where those conditions don't hold, the output changes on purpose: | Case | Before | Now | |---|---|---| | run directory or `result.json` linked out | read through the link | `list`: `unknown`; `show`: refuses (run directory) or `result.json: (missing or unreadable)` | | `runs/` linked out | read through the link | `list` and `show` refuse, exit 4 | | `result.json` is `5` or `[]` | `list` crashed; `show` printed `undefined` fields | `unknown`; `(missing or unreadable)` | | `result.json` is an object whose `status` or `taskId` isn't a string, or whose `workspace` or `session` is neither a string nor `null` (`{}`, a numeric `status`, a `null` `taskId`) | `list` crashed on a `status` or `taskId` that isn't a string, hiding every run after it, and printed a `workspace` or `session` that is neither a string nor `null` through `String` | `list`: `unknown` for the status, `-` for each other field, and every later run still lists; `show` as before | | `task.json` or `mission.json` is JSON but not an object | `show` printed its snapshot line | snapshot line omitted | | run is a regular file | `show` crashed | `run not found`, exit 4 | | `runs/` unreadable | `list` printed nothing | refuses, exit 4 | | run directory unreadable (mode 000) | `show` printed two lines, then crashed, exit 1 | refuses with EACCES, exit 4 | | link loop or a permission error resolving a run | `run not found` | refuses with the error code, exit 4 | `agents/rocko/work/queue-56/delta-check.sh` prints the before and after for each case.