diff --git a/.github/workflows/evals-marathon.yml b/.github/workflows/evals-marathon.yml index 9aa0ba98c..430186927 100644 --- a/.github/workflows/evals-marathon.yml +++ b/.github/workflows/evals-marathon.yml @@ -283,25 +283,47 @@ jobs: # PIPESTATUS[0], NOT $?: the default step shell has no pipefail. echo "exit=${PIPESTATUS[0]}" >> "$GITHUB_OUTPUT" - # One tracking issue for the whole lane (never one per week). - - name: Upsert tracking issue on failure + # A red marathon run writes its report to the step summary and the + # marathon-report artifact on every ref. Only main upserts the lane's ONE + # tracking issue (never one per week) and closes it on the next green + # run; a branch-dispatched run never touches main's issue. + - name: Write the marathon report if: always() && (steps.reconcile.outputs.exit != '0' || needs.eval-slices.result != 'success') + run: | + set -euo pipefail + { + echo "Marathon report (non-blocking lane): branch ${GITHUB_REF_NAME} @ ${GITHUB_SHA}, event ${GITHUB_EVENT_NAME}, run ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" + echo + echo "A census went red: triage with ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/blob/${GITHUB_SHA}/docs/evals/census-red.md" + echo + echo "- reconciliation exit: ${{ steps.reconcile.outputs.exit }}" + echo "- marathon slices job: ${{ needs.eval-slices.result }}" + echo + { tail -c 6000 /tmp/report.txt 2>/dev/null || echo "(no reconciliation output)"; } \ + | bun --no-install -e 'import { publishedFence } from "./scripts/lib/published-text"; + const lines = (await Bun.stdin.text()).split("\n").filter(Boolean); + console.log(publishedFence(lines.length ? lines : ["(none)"]).join("\n"));' + } > /tmp/marathon-report.md + cat /tmp/marathon-report.md >> "$GITHUB_STEP_SUMMARY" + + - name: Upload the marathon report + if: always() && (steps.reconcile.outputs.exit != '0' || needs.eval-slices.result != 'success') + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: marathon-report-a${{ github.run_attempt }} + path: /tmp/marathon-report.md + if-no-files-found: ignore + retention-days: 90 + + - name: Upsert tracking issue on failure + if: always() && github.ref == 'refs/heads/main' && (steps.reconcile.outputs.exit != '0' || needs.eval-slices.result != 'success') env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail TITLE="Weekly marathon evals: red lane needs triage" - BODY_FILE=/tmp/issue-body.md - { - echo "Automated weekly marathon report (non-blocking lane) — run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" - echo - echo "- reconciliation exit: ${{ steps.reconcile.outputs.exit }}" - echo "- marathon slices job: ${{ needs.eval-slices.result }}" - echo - echo '```' - tail -c 6000 /tmp/report.txt 2>/dev/null || echo "(no reconciliation output)" - echo '```' - } > "$BODY_FILE" + BODY_FILE=/tmp/marathon-report.md + [ -s "$BODY_FILE" ] || echo "Marathon report missing; run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" > "$BODY_FILE" EXISTING=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open --search "in:title \"$TITLE\"" --json number --jq '.[0].number // empty') if [ -n "$EXISTING" ]; then gh issue comment "$EXISTING" --repo "$GITHUB_REPOSITORY" --body-file "$BODY_FILE" @@ -310,6 +332,18 @@ jobs: gh issue create --repo "$GITHUB_REPOSITORY" --title "$TITLE" --body-file "$BODY_FILE" fi + - name: Close the tracking issue on a green run + if: always() && github.ref == 'refs/heads/main' && steps.reconcile.outputs.exit == '0' && needs.eval-slices.result == 'success' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + TITLE="Weekly marathon evals: red lane needs triage" + EXISTING=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open --search "in:title \"$TITLE\"" --json number --jq '.[0].number // empty') + if [ -n "$EXISTING" ]; then + gh issue close "$EXISTING" --repo "$GITHUB_REPOSITORY" --comment "Green marathon run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" + fi + - name: Fail the workflow when reconciliation failed if: always() && (steps.reconcile.outputs.exit != '0' || needs.eval-slices.result != 'success') run: exit 1 diff --git a/.github/workflows/evals-periodic.yml b/.github/workflows/evals-periodic.yml index 027533301..464b2b45d 100644 --- a/.github/workflows/evals-periodic.yml +++ b/.github/workflows/evals-periodic.yml @@ -627,7 +627,7 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set +e - bun run eval:pass-rates --gate --runs 10 > /tmp/pass-rates.txt 2>&1 + bun run eval:pass-rates --gate --runs 10 --branch "$GITHUB_REF_NAME" > /tmp/pass-rates.txt 2>&1 echo "exit=$?" >> "$GITHUB_OUTPUT" cat /tmp/pass-rates.txt @@ -652,10 +652,13 @@ jobs: echo "redispatch=false" >> "$GITHUB_OUTPUT" fi - # A red weekly lane nobody must action is waste — upsert ONE tracking - # issue (never a new issue per week) with the headline and failure block - # of both lanes, and close it on the next green run. - - name: Upsert tracking issue on failure + # A red census writes one report on every ref: the step summary and the + # census-report artifact. Only main (scheduled weekly, or dispatched on + # main) upserts ONE tracking issue (never a new issue per week) and + # closes it on the next green run; a branch-dispatched census never + # comments on or closes main's issue. Every report-derived line goes + # through the one published-text sanitizer (scripts/lib/published-text.ts). + - name: Write the census report if: always() && (steps.reconcile.outputs.exit != '0' || steps.gate-reconcile.outputs.exit != '0' || steps.pass-rates.outputs.exit != '0' || needs.eval-slices.result != 'success' || !contains(fromJSON('["success","skipped"]'), needs.eval-codex-slices.result) || needs.gate-census.result != 'success') env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -663,17 +666,28 @@ jobs: REDISPATCH_OF: ${{ inputs.redispatch_of }} run: | set -euo pipefail - TITLE="Weekly periodic evals: red lane needs triage" - BODY_FILE=/tmp/issue-body.md + published() { # $1 fence|line: stdin lines through the published-text sanitizer + bun --no-install -e 'import { publishedFence, sanitizePublishedText } from "./scripts/lib/published-text"; + const lines = (await Bun.stdin.text()).split("\n").filter(Boolean); + console.log(process.argv[1] === "line" ? sanitizePublishedText(lines.join(" ")) : publishedFence(lines.length ? lines : ["(none)"]).join("\n"));' "$1" + } RUN_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" + GUIDE_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/blob/${GITHUB_SHA}/docs/evals/census-red.md" + bun run eval:pass-rates --reds --runs 10 --branch "$GITHUB_REF_NAME" > /tmp/pass-rates-reds.txt 2>&1 \ + || echo "history unavailable: eval:pass-rates --reds exited $?" >> /tmp/pass-rates-reds.txt + bun run eval:pass-rates --headroom --runs 10 --branch "$GITHUB_REF_NAME" > /tmp/pass-rates-headroom.txt 2>&1 \ + || echo "history unavailable: eval:pass-rates --headroom exited $?" >> /tmp/pass-rates-headroom.txt { - echo "Automated weekly report — run: ${RUN_URL}" + echo "Periodic census report: branch ${GITHUB_REF_NAME} @ ${GITHUB_SHA}, event ${GITHUB_EVENT_NAME}, run ${RUN_URL}" + echo + echo "A census went red: triage with ${GUIDE_URL}" if [ -n "$REDISPATCH_OF" ]; then echo; echo "This run is the one INFRA/INCOMPLETE re-dispatch of run ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${REDISPATCH_OF}; both runs are reported."; fi if [ "$REDISPATCH" = "true" ]; then echo; echo "Every red verdict is machine-classified INFRA/INCOMPLETE: re-dispatching once as a new run (EVAL_POLICY.infraRedispatch). This run stays red and reported."; fi echo echo "- periodic reconciliation exit: ${{ steps.reconcile.outputs.exit }} (slices job: ${{ needs.eval-slices.result }}, Codex slices job: ${{ needs.eval-codex-slices.result }})" echo "- gate census reconciliation exit: ${{ steps.gate-reconcile.outputs.exit }} (census job: ${{ needs.gate-census.result }})" echo "- pass-rate history gate exit: ${{ steps.pass-rates.outputs.exit }}" + echo "- $({ grep -E '^all-green probability' /tmp/pass-rates-reds.txt || echo 'all-green probability: unavailable (see the red ledger)'; } | published line)" echo echo "### Periodic lane" cat /tmp/paid-report/report-summary.md 2>/dev/null || echo "(no periodic report summary)" @@ -682,23 +696,44 @@ jobs: cat /tmp/gate-census-report/report-summary.md 2>/dev/null || echo "(no gate census report summary)" echo echo "### Pass-rate history (ACTION REQUIRED)" - echo '```' - { grep -E 'ACTION REQUIRED|history unavailable' /tmp/pass-rates.txt || echo "(no pass-rate alarms)"; } | sed 's/@/@\xe2\x80\x8b/g' | head -c 6000 - echo '```' + { awk '/^ACTION REQUIRED/ { on = 1; print; next } on && /^ \[/ { print; next } { on = 0 } /history unavailable/ { print }' /tmp/pass-rates.txt 2>/dev/null \ + | grep . || echo "(no pass-rate alarms)"; } | head -c 6000 | published fence + echo + echo "### Red ledger: bun run eval:pass-rates --reds --runs 10 --branch ${GITHUB_REF_NAME}" + { head -n 1 /tmp/pass-rates-reds.txt; tail -n +2 /tmp/pass-rates-reds.txt | tail -n 60; } | cut -c1-400 | published fence + echo + echo "### Session headroom: bun run eval:pass-rates --headroom --runs 10 --branch ${GITHUB_REF_NAME}" + head -n 40 /tmp/pass-rates-headroom.txt | cut -c1-400 | published fence echo echo "
Full reconciliation output" echo - echo '```' - tail -c 6000 /tmp/report.txt 2>/dev/null | sed 's/@/@\xe2\x80\x8b/g' || echo "(no reconciliation output)" - echo '```' + { tail -c 6000 /tmp/report.txt 2>/dev/null || echo "(no reconciliation output)"; } | published fence echo - echo '```' - tail -c 6000 /tmp/gate-report.txt 2>/dev/null | sed 's/@/@\xe2\x80\x8b/g' || echo "(no gate census reconciliation output)" - echo '```' + { tail -c 6000 /tmp/gate-report.txt 2>/dev/null || echo "(no gate census reconciliation output)"; } | published fence echo "
" echo echo "Policy: EVAL_POLICY and CASE_QUARANTINE in test/helpers/periodic-exclude-data.ts; history: \`bun run eval:pass-rates\`." - } > "$BODY_FILE" + } > /tmp/census-report.md + cat /tmp/census-report.md >> "$GITHUB_STEP_SUMMARY" + + - name: Upload the census report + if: always() && (steps.reconcile.outputs.exit != '0' || steps.gate-reconcile.outputs.exit != '0' || steps.pass-rates.outputs.exit != '0' || needs.eval-slices.result != 'success' || !contains(fromJSON('["success","skipped"]'), needs.eval-codex-slices.result) || needs.gate-census.result != 'success') + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: census-report-a${{ github.run_attempt }} + path: /tmp/census-report.md + if-no-files-found: ignore + retention-days: 90 + + - name: Upsert tracking issue on failure + if: always() && github.ref == 'refs/heads/main' && (steps.reconcile.outputs.exit != '0' || steps.gate-reconcile.outputs.exit != '0' || steps.pass-rates.outputs.exit != '0' || needs.eval-slices.result != 'success' || !contains(fromJSON('["success","skipped"]'), needs.eval-codex-slices.result) || needs.gate-census.result != 'success') + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + TITLE="Weekly periodic evals: red lane needs triage" + BODY_FILE=/tmp/census-report.md + [ -s "$BODY_FILE" ] || echo "Periodic census report missing; run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" > "$BODY_FILE" EXISTING=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open --search "in:title \"$TITLE\"" --json number --jq '.[0].number // empty') if [ -n "$EXISTING" ]; then gh issue comment "$EXISTING" --repo "$GITHUB_REPOSITORY" --body-file "$BODY_FILE" @@ -708,7 +743,7 @@ jobs: fi - name: Close the tracking issue on a green run - if: always() && steps.reconcile.outputs.exit == '0' && steps.gate-reconcile.outputs.exit == '0' && steps.pass-rates.outputs.exit == '0' && needs.eval-slices.result == 'success' && contains(fromJSON('["success","skipped"]'), needs.eval-codex-slices.result) && needs.gate-census.result == 'success' + if: always() && github.ref == 'refs/heads/main' && steps.reconcile.outputs.exit == '0' && steps.gate-reconcile.outputs.exit == '0' && steps.pass-rates.outputs.exit == '0' && needs.eval-slices.result == 'success' && contains(fromJSON('["success","skipped"]'), needs.eval-codex-slices.result) && needs.gate-census.result == 'success' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} REDISPATCH_OF: ${{ inputs.redispatch_of }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4ba76804b..bb6ddc4cb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -274,6 +274,13 @@ this policy. Quarantine (`CASE_QUARANTINE`) and history are described in `docs/TESTING_INTERNALS.md`; `bun run eval:pass-rates --case ` shows a case's per-trial pass rate with its Wilson interval. +**A census went red?** Follow [docs/evals/census-red.md](docs/evals/census-red.md): +read the red line, inspect its evidence with `bun run eval:pass-rates --run `, +check `--reds` and `--headroom`, repair with a free regression test, and only +then spend one paid run with the line's `after a repair:` command. A red census +on `main` is reported on the weekly tracking issue; a census dispatched on a +branch writes the same report to its run summary and `census-report` artifact. + CI enables verified first-attempt reuse for 16 workflow quality judges for 24 hours within the same PR. The cookie workflow's custom input and the other 11 quality cases stay fresh. PR-profile E2E shards that run once (no retry, so the @@ -446,6 +453,9 @@ bun run eval:list # list all eval runs (turns, duration, cost per run bun run eval:compare # compare two runs — shows per-test deltas + Takeaway commentary bun run eval:summary # aggregate stats + per-test efficiency averages across runs bun run eval:pass-rates # per-case trial pass rates + Wilson intervals from recent weekly runs (--case, --runs, --dir, --backfill, --json, --gate); eval:flake-rank is an alias +bun run eval:pass-rates --reds # verdict reds per census by failure class and cause, all-green probability +bun run eval:pass-rates --headroom # slowest session per case vs its armed budget (alarm above 85% in --gate) +bun run eval:pass-rates --run # one census's reds with their values and fetched transcript evidence ``` **Detached runs for agents and long suites.** When an agent (or you, for a run diff --git a/README.md b/README.md index 75e18080d..5f6a7b728 100644 --- a/README.md +++ b/README.md @@ -186,8 +186,12 @@ for capture, judge, and benchmark model selection. The design binary (`$D`) uses OpenAI's `gpt-5.5` by default, with image generation through `gpt-image-2`. Set `GSTACK_DESIGN_MODEL=` to use another gpt-5-class model for both image generation and screenshot analysis; -if OpenAI rejects it, the error names `GSTACK_DESIGN_MODEL`. Check a key -against the defaults with `bun run design/scripts/live-model-check.ts`. +if OpenAI rejects it, the error names `GSTACK_DESIGN_MODEL`. Set +`GSTACK_DESIGN_IMAGE_MODEL=` to change the image tool model; a +value that is not a gpt-image model name is refused before any request. Check a +key against the defaults with `bun run design/scripts/live-model-check.ts`, +which always tests the default models and ignores both overrides; the weekly +periodic census runs the same check. **Want to add support for another agent?** See [docs/ADDING_A_HOST.md](docs/ADDING_A_HOST.md). Rendering a new agent is one TypeScript config file; installing it also needs a diff --git a/docs/TESTING_INTERNALS.md b/docs/TESTING_INTERNALS.md index 2b4559e61..80e308ef7 100644 --- a/docs/TESTING_INTERNALS.md +++ b/docs/TESTING_INTERNALS.md @@ -96,9 +96,13 @@ against the previous finalized run (in-flight `_partial` files are never used as a baseline, so a run can't compare against itself). The periodic overlay fixtures use a versioned behavior gate with efficacy -reported separately. See [Overlay benchmark contract v2](OVERLAY_BENCHMARK_CONTRACT.md) +reported separately. See [Overlay benchmark contract v4](OVERLAY_BENCHMARK_CONTRACT.md) for exact correctness requirements, retired fanout cases, immutable evidence, -and the limits of a passing result. +and the limits of a passing result. Under v4 a completed wrong answer is a +measured correctness result in either arm (`taskCorrect: false` with +`answerError`); only an overlay-ON wrong answer fails the case, so an +overlay-OFF wrong answer stays in the comparison. v3 verdicts keep their +original meaning. ## Coverage ownership @@ -365,7 +369,22 @@ report and tracking issue, and nothing requires it. evals-periodic.yml runs ALL periodic-tier files weekly (the coverage contract) minus the reasoned exclusions in `test/helpers/periodic-exclude-data.ts` (reason + tracking required per entry; removal re-activates the file), plus a weekly -`EVALS_ALL` gate census, plus a tracking-issue UPSERT on red weeks. The CI +`EVALS_ALL` gate census. A red census writes one report on every ref, to the +step summary and the `census-report-a` artifact: the run's branch, sha +and event, both lanes' summaries, every pass-rate alarm line, the red ledger +and session headroom (`eval:pass-rates --reds` / `--headroom --runs 10 --branch +`), the all-green estimate and a link to the red-census guide, with +report-derived text passed through `scripts/lib/published-text.ts`. Only a run +on `refs/heads/main` comments on (or opens) the one tracking issue and closes it +on the next green run; the marathon lane does the same with its own issue and +`marathon-report-a` artifact. `test/evals-tracking-issue.test.ts` +pins the main-only guard and runs both report scripts. The host-run Codex job +also carries the live Codex multi-block case (`codex-multiblock-live`: `./setup +--host codex` from the job's checkout into a fresh `CODEX_HOME`, the router in +`codex debug prompt-input`, and a later /learn block rooted by its own +prelude), and the periodic lane runs the design default-model smoke +(`design-model-smoke`), a visible SKIPPED with no coverage credit when +`OPENAI_API_KEY` is absent. The CI image pins the claude CLI to an exact version (`.github/docker/Dockerfile.ci`, enforced by `test/ci-image-cli-pin.test.ts` — bumps ride PRs that run the PTY gate), and every eval-store run records `claude --version`, resolved once in @@ -373,6 +392,7 @@ the runner parent and handed to shard children as `GSTACK_CLAUDE_CLI_VERSION` (never spawned on a test thread), so a TUI-drift flake hunt is a grep, not archaeology. + **Eval verdict policy** (`EVAL_POLICY` version 1 in `test/helpers/periodic-exclude-data.ts`, pre-registered 2026-09-29). Paid evals never retry. Each live case has exactly one kind in `E2E_KINDS` @@ -397,6 +417,11 @@ and the kind fixes its trials before the run: refusal counts as an unscored panel only when every sample refused. `callJudge`'s 429 backoff happens before any model output and is transport, not a verdict retry. The workflow-judge cache stores whole panels only. + The `rule` case auq-matrix uses the same panel inside its one execution: each + skill's single native capture has its recommendation substance scored by 3 + judge samples, gated on their mean against the unchanged minimum of 4, and a + failed sample fails the panel without resampling (approved 2026-10-04 for + censuses after the change). `panelVerdict()` (`test/helpers/eval-store.ts`) is the single verdict function the report, `collector-outcomes.json`, the PR comment and pass-rates @@ -414,6 +439,58 @@ reported. Changing any `EVAL_POLICY` constant after seeing census results needs Garry's re-approval, a `version` bump and a fresh census; `test/periodic-exclude-policy.test.ts` pins the approved values. + +**Failure causes and details** (diagnostic only; verdicts unchanged under v1). +Every failed trial record keeps `failure_class` (`assertion`, `contract`, +`timeout`, `infra`: the field verdicts, INFRA re-dispatch and pass rates read) +and its 300-character `error` exactly as before. `failureCauseOf()` +(`test/helpers/eval-store.ts`, called by both record builders, including the +JUnit census for PTY and JUnit-only cases) adds an optional `failure_cause` +with a one-line `failure_cause_evidence`. It never changes `failure_class`. +The highest-precedence cause with evidence wins: + +| `failure_cause` | Recorded when | First step | +|---|---|---| +| `contract` | `expectContract()` stamped a contract violation | fix the product; a violation fails a behavior panel outright | +| `pre_turn_infra` | the trial failed before the first model turn (`failure_class: 'infra'`) | check the runner and API status; an all-INFRA census is re-dispatched once | +| `api_error` | a session ended on an API error (for PTY, the `API Error:` panel on an idle turn) or the runner's exit reason was `error_api` | read the evidence line; a provider outage is a named red | +| `refusal` | the structured stop reason was a refusal (phrases only as a last resort; quoted refusal text in a tool result does not count) | check whether the fixture prompt invites the refusal | +| `provider_stall` | a session that streamed partial messages saw no event of any kind, thinking deltas included, for `STALL_WINDOW_MS` (120 s) while a model request was in flight and no tool, permission prompt, hook or subagent was outstanding; sessions without partial messages are never stalls | read the session ledger row; still a failed trial under v1 | +| `session_timeout` | a runner's armed session timeout fired | `eval:pass-rates --headroom`; cut work, never raise the budget | +| `observer_timeout` | the test's observer gave up waiting for an outcome (`outcome=timeout`) | read the transcript tail: a missed detector or a product that never answered | +| `assertion` | a check failed after the session ran (`session completed; check failed` when it ended normally) | the Expected/Received values or judge dimensions | +| `unknown` | the trial failed with no evidence for any cause above | inspect the shard directory | + +A detector, harness or product fault is a human diagnosis, never a cause. +Readers before this field accept the new records (a frozen v1 reader runs over +them in `test/eval-store.test.ts`); an unknown cause from a newer gstack is +rejected with `unknown failure_cause "" (written by a newer gstack; update +this checkout to read it)`. + +`failure_detail` carries the values `error` cannot hold: a failed Bun matcher's +`{expected, received}`, or for a judge one `{dimension, mean, threshold, +samples, rationale}` entry per failing dimension, each part capped at 200 +characters. `sessions[]` (at most 32) summarizes the trial's session-ledger +rows. `cost_known: false` marks a record or trial whose harness captured no +billing: PTY and Codex sessions never bill, and an SDK session bills only with a +terminal `total_cost_usd`. An absent field means known. Census and marathon +cost lines print the known sum and `N of M trial(s) cost unknown`, never a bare +total over unknowns. + + +**Session ledger.** Every runner (session-runner, the Agent SDK runner +including `runSharedInteractive`, the PTY session and `runRecordedCodexEval`) +appends one best-effort line per armed session to +`$GSTACK_EVAL_DIR/session-ledger.jsonl` (`test/helpers/session-ledger.ts`): +a stable key `: