# 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, 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)` | | `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.