Merge wave/w-followups

This commit is contained in:
garrytan committed 2026-10-04 19:05:29 +00:00
commit 4ea2c6eed7
16 files changed
+1056 -177

No files matched your search

+47 -13
View File
@@ -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
+54 -19
View File
@@ -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 "<details><summary>Full reconciliation output</summary>"
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 "</details>"
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 }}
+10
View File
@@ -274,6 +274,13 @@ this policy. Quarantine (`CASE_QUARANTINE`) and history are described in
`docs/TESTING_INTERNALS.md`; `bun run eval:pass-rates --case <id>` 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 <id>`,
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 <id> # 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
+6 -2
View File
@@ -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=<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=<gpt-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
+107 -8
View File
@@ -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<attempt>` 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
<ref>`), 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<attempt>` 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.
<a id="eval-verdict-policy"></a>
**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.
<a id="failure-causes"></a>
**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 "<x>" (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.
<a id="session-ledger"></a>
**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 `<runner>:<label>#n`, the exact `budget_ms` it armed, monotonic
`elapsed_ms`, the `end` reason, sanitized evidence, `billed`, and a compact
liveness summary (longest in-request silence, the last event kind, any tool open
at the end). `claude -p` sessions stream partial messages for this summary;
transcripts keep their shape and size (full stream diagnostics only with
`publicStreamDiagnostics`). A failed ledger write never fails a trial; a trial
without ledger rows has no `sessions` and its headroom is `unknown`, never
scored. The ledger rides in each slice artifact beside the shard results.
**Quarantine** (`CASE_QUARANTINE`, same file). An entry needs: a per-trial rate
below 95% over at least 10 post-policy trials of the case's current input
identity (pre-policy backfill may justify only an initial entry, labeled as
@@ -430,8 +507,11 @@ weekly runs old, or when a tier is over its cap.
**Pass-rate history** (`bun run eval:pass-rates`, `scripts/eval-flake-rank.ts`).
It reads the `trial-outcomes` artifact of the last N completed
`evals-periodic.yml` runs on the current branch and `main` (flags: `--case`,
`--runs N`, `--branch`, `--dir`, `--backfill`, `--json`, `--gate`) and prints
`evals-periodic.yml` runs on the given `--branch` (default: the current branch)
and `main` (flags: `--case`, `--runs N`, `--branch`, `--repo`, `--workflow`,
`--dir`, `--backfill`, `--json`, `--gate`, and the views `--headroom`, `--reds`,
`--run <id>`; `--help` works offline, and an unknown flag or case id exits 2
before anything is fetched; every view prints its `scope:` line) and prints
per-case per-trial pass rates with 95% Wilson intervals. A series is one case
under one input identity, the hash of its own touchfiles minus
`GLOBAL_TOUCHFILES` (harness edits do not restart it), per model, Claude CLI
@@ -447,8 +527,26 @@ only, when a non-quarantined blocking case meets the entry rule (proposing an
entry), when a `rule` case does (rule case behaving like behavior: fix or
reclassify), when a blocking case's current identity is significantly below its
previous one (one-sided Fisher exact, α = 0.05, at least 6 trials each side,
Holm-controlled across the cases tested), and on the quarantine rules above.
History that cannot be fetched fails the gate closed.
Holm-controlled across the cases tested), on the quarantine rules above, and
with a `[headroom]` alarm when a case's slowest session ran above
`HEADROOM_FAIL` (85%) of the budget it armed. History that cannot be fetched
fails the gate closed.
The views (`scripts/lib/eval-history.ts`): `--headroom` lists each case's
slowest armed session against its budget (warn above `HEADROOM_WARN`, 75%;
`insufficient` below 3 samples; timed-out samples marked censored; `unknown`
without a session ledger, with the case wall beside it as a labelled upper
bound) and the census critical path from the slice job timings. `--reds` lists
verdict reds per census lane by failure class and cause, pooled and shrunk
per-case red rates, and the all-green probability Π(1 − p_i) over the latest
census's cases with an interval, labelled an approximation because verdicts are
not proven independent. `--run <id>` is one census's triage: each red with its
values and history, and the transcript evidence it downloads from only the
slices its reds name (a slice artifact over 64 MB is reported, not fetched).
The three thresholds `HEADROOM_WARN`, `HEADROOM_FAIL` and `STALL_WINDOW_MS`
live in `test/helpers/eval-budgets.ts`, outside `EVAL_POLICY`; a slow case gets
less work, never a larger budget. The red-census walkthrough is
[docs/evals/census-red.md](evals/census-red.md).
**The arithmetic.** With per-trial pass rate p, the chance a single case goes
red (a false red while the product works, the catch rate once it has
@@ -483,7 +581,8 @@ and judge verdicts at p_rule:
The rule term dominates: a green lane on a working product needs rule cases to
be near-deterministic (0.999), which is why failing detectors are converted to
outcome checks and product defects are fixed or named, and why each census
reports its expected lane false-red from the measured rates.
reports its all-green probability from the measured rates (`eval:pass-rates
--reds`, carried into the census report).
**Timeout policy.** Paid tests use the tiers in
`test/helpers/eval-budgets.ts` (JUDGE/CAPTURE/CAPTURE_LONG/PTY/PTY_LONG);
+134
View File
@@ -0,0 +1,134 @@
# A census went red
A periodic census (`evals-periodic.yml`: the weekly periodic lane plus the
gate census) or a marathon run reports each red verdict as one line. This page
takes one red line to a diagnosis, using free commands first. A paid run is
allowed only after a repair.
Where the report is:
- **Main** (the scheduled weekly run, or a dispatch on `main`): a comment on the
open "Weekly periodic evals: red lane needs triage" issue (marathon: "Weekly
marathon evals: red lane needs triage").
- **Any branch**: the run's step summary and its `census-report-a<attempt>`
artifact (marathon: `marathon-report-a<attempt>`). A branch census never
comments on or closes main's issue.
## Prerequisites
- A checkout of the census branch with `bun install` done. Bun 1.4.0 is the
tested version.
- `gh` authenticated with read access to the repository's Actions (or
`GH_TOKEN` set). `eval:pass-rates` downloads artifacts through the REST zip
route, which also works where `gh run download` is blocked.
- The run id: the number at the end of the run URL in the report's first line.
## 1. Read the red line
A red line in the census report looks like this (one line; a behavior panel
with one failed trial shown):
```
✗ plan-review-prosons-neutral-neg behavior FAIL 1/3 (✗✗✓) t1: assertion (session completed; check failed) — expect(received).not.toMatch(expected) · cause assertion: session completed; check failed Expected: not /taste call/i · Received: "a taste call"; t2: assertion [t1@slice 7, t2@slice 9, t3@slice 9, attempt 1] evidence: paid-slice-7-a1/shards/skill-e2e-plan-prosons--plan-review-prosons-neutral-neg.t1 (fetch: bun run eval:pass-rates --run 37193478719 --case plan-review-prosons-neutral-neg) after a repair: bun run scripts/test-paid-shards.ts --tier periodic --case plan-review-prosons-neutral-neg --trials 3
```
| Field | What it says |
|---|---|
| case, kind, verdict | the registry id, its kind (`rule`, `behavior`, `judge`) and the verdict with each trial's outcome; the brackets name the slice and attempt of each trial |
| `failure_class` | the class verdicts read: `assertion`, `contract`, `timeout` or `infra` |
| `cause` | what the machine observed (`failure_cause`, glossary in [TESTING_INTERNALS](../TESTING_INTERNALS.md#failure-causes)), with its evidence line |
| Expected / Received | the failed matcher's values; for a judge, each failing dimension's mean, threshold, sample count and lowest rationale |
| `evidence:` | the slice artifact and shard directory holding the transcript |
| `after a repair:` | the command that runs only this case, once a repair exists |
The cause is a label. Under EVAL_POLICY v1 it never changes a verdict: a
`provider_stall` or `refusal` is still a failed trial. Whether an assertion
failure was a detector, harness or product fault is your diagnosis, not a
machine cause.
## 2. Inspect the evidence (free)
```bash
bun run eval:pass-rates --run <run-id> # every red of that census
bun run eval:pass-rates --run <run-id> --case <id> # one case
```
Expected output: one block per red, with its history and a local evidence path.
```
run 37198445662 (main @ 1a2b3c4d5, 2026-10-04T11:29:00Z): 2 verdict red(s)
✗ qa-only/SKILL.md workflow periodic/full FAIL (history: 7/8 verdicts green)
t1: assertion / cause assertion: session completed; check failed clarity 3.67 < 4 (3 samples) "…"
evidence: ~/.gstack/eval-pass-rates-cache/garrytan-gstack/37198445662/paid-slice-5-a1/shards/skill-llm-eval
```
Only the slices the reds name are downloaded, into
`<state root>/eval-pass-rates-cache/<owner>-<repo>/<run-id>/`. The shard
directory holds the eval record JSON (transcript, judge scores), `junit.xml`
and, for runs after the session ledger landed, `session-ledger.jsonl` (each
session's armed budget, elapsed time, end reason and liveness summary). A slice
artifact over 64 MB is reported as `too large to fetch`; download it from the
run page or with `gh api repos/<owner>/<repo>/actions/artifacts/<id>/zip > slice.zip`.
## 3. Put it in context (free)
```bash
bun run eval:pass-rates --reds --runs 10 --branch <branch> # reds per census by class and cause, all-green probability
bun run eval:pass-rates --headroom --runs 10 --branch <branch> # slowest session per case vs its armed budget
bun run eval:pass-rates --case <id> --runs 10 # the case's pass rate per input series
```
`--reds` ends with the all-green probability: Π(1 − p_i) over the latest
census's cases, with per-case red rates shrunk toward the pooled rate. It is an
approximation (verdicts are not proven independent). A case that is red in
several censuses is a repeat offender; its fix is structural (a replay corpus,
a structured signal or a speedup), not a one-off spelling patch.
What incomplete data means:
| Shown | Meaning |
|---|---|
| headroom `unknown (no session ledger)` | the trials predate the session ledger, or the runner wrote none; the case wall beside it is an upper bound, not a session clock |
| headroom `insufficient` | fewer than 3 session samples; never read as a pass |
| headroom `censored` | a sample timed out, so its real duration is at least the budget |
| cause `unrecorded` | the trial was written before `failure_cause` existed |
| cause `unknown` | the trial failed with no evidence for any other cause |
| `cost unknown` | the harness (PTY, Codex) records no billing; only the known sum is shown |
| `history unavailable (...)` | the GitHub history could not be read; `--gate` fails closed |
## 4. Replay and repair (free)
Diagnose before changing code: product defect, test or detector defect, harness
defect, or infrastructure. Then reproduce with the smallest free check:
```bash
bun test test/detector-corpus-<case>.test.ts # cases with a replay corpus
bun run eval:pass-rates --reds --dir <evidence dir> # re-read downloaded artifacts offline
```
The repair lands with a free regression test that fails before it and passes
after it (`AGENTS.md`, "Validation discipline").
## 5. After a repair: one paid run
Run the line's `after a repair:` command, which selects only that case:
```bash
bun run scripts/test-paid-shards.ts --tier periodic --case <id> # a rule case: one diagnostic trial
bun run scripts/test-paid-shards.ts --tier periodic --case <id> --trials 3 # a behavior case: its 3-trial panel
```
A judge prints an `EVALS_JUDGE_SELECTION_JSON=... bun test <file> -t '<pattern>'`
command instead; a changed judge gets one 3-sample panel.
Rules (see [TESTING_INTERNALS](../TESTING_INTERNALS.md#eval-verdict-policy)):
- A paid run follows a concrete repair. A red census is never rerun on
unchanged inputs.
- A diagnostic run never forms a verdict. The next census on the repaired head
decides.
- A census whose every red is machine-classified INFRA or INCOMPLETE is
re-dispatched once automatically; both runs stay reported.
- An unresolved red is listed as a named red with its diagnosis. A case enters
`CASE_QUARANTINE` only under its entry rule, and a product defect is never
quarantined.
+168 -132
View File
@@ -242,6 +242,35 @@ contains no parsable version. /ship stops instead of inventing `0.0.0.0`.
**Expected result.** `gstack-version-bump classify` prints the current version.
<a id="review-base-stale"></a>
### `BASE_REFRESH: stale <rev>` / `Base coverage: stale at <rev>`
**Meaning.** /review could not fetch the base branch (offline, no credentials,
or a read-only `.git` such as Codex's sandbox), so it reviewed against your
local `origin/<base>` at `<rev>`.
**Fix.** Run `git fetch origin <base>` outside the sandbox and rerun /review if
the base has moved.
<a id="review-fingerprint-tmpdir"></a>
### `gstack-wtree: cannot create a temp index under <dir>; set TMPDIR to a writable directory` / `gstack-review-log: cannot create a private object directory under <dir>; no working-tree fingerprint. Set TMPDIR to a writable directory.`
**Meaning.** /review fingerprints the working tree in a private temporary
object directory (Codex's sandbox keeps `.git` read-only). The temp directory
could not be created, so this review has no fingerprint and later steps cannot
reuse its evidence.
**Fix.** `export TMPDIR=<a writable directory>`, then run /review again.
<a id="review-fingerprint-stage"></a>
### `gstack-wtree: cannot stage the working tree: <git error>`
**Meaning.** The fingerprint's objects could not be written. Standalone
`gstack-wtree` writes into the repository's object store.
**Fix.** Run it through /review (`gstack-review-log` supplies a private object
directory), or make `.git/objects` writable.
<a id="plan-audit-not-run"></a>
### `Plan completion audit: not run (no plan is bound to this branch and no docs/designs/ file matches). Fix: ...`
@@ -307,30 +336,6 @@ Or stop the reminder: `gstack-slug --adopt-legacy --dismiss <slug>`.
---
<a id="review-base-stale"></a>
### `BASE_REFRESH: stale <rev>` / `Base coverage: stale at <rev>`
**Meaning.** /review could not fetch the base branch (offline, no credentials,
or a read-only `.git` such as Codex's sandbox), so it reviewed against your
local `origin/<base>` at `<rev>`.
**Fix.** Run `git fetch origin <base>` outside the sandbox and rerun /review if
the base has moved.
<a id="review-fingerprint-objects"></a>
### `gstack-review-log: cannot create a private object directory under <dir>; no working-tree fingerprint.` / `gstack-wtree: cannot create a temp index under <dir>`
**Meaning.** The working-tree fingerprint needs a writable temp directory.
**Fix.** `export TMPDIR=<writable dir>` and rerun.
### `gstack-wtree: cannot stage the working tree: ...` / `gstack-wtree: cannot write the tree: ...`
**Meaning.** The object directory could not be written. `gstack-review-log`
supplies a private one; standalone `gstack-wtree` writes to `.git/objects`.
**Fix.** Run it through `gstack-review-log`, or make `.git/objects` writable.
## Design skills
<a id="design-not-available"></a>
@@ -353,6 +358,15 @@ If the message names a missing timeout tool, install coreutils (`gtimeout`) or p
**Expected result.** The next design skill prints `DESIGN_READY`.
<a id="design-image-model-invalid"></a>
### `GSTACK_DESIGN_IMAGE_MODEL="<value>" is not a gpt-image model name (expected something like gpt-image-2); fix it or unset it to use gpt-image-2`
**Meaning.** The design binary refuses an image tool model override that is not
a `gpt-image-*` model name, before sending any request.
**Fix.** `export GSTACK_DESIGN_IMAGE_MODEL=gpt-image-2` (or another gpt-image
model your key can use), or `unset GSTACK_DESIGN_IMAGE_MODEL`.
<a id="design-taste-profile-unavailable"></a>
### `TASTE_PROFILE_UNAVAILABLE: could not resolve the project slug (gstack-slug failed). Fix: run ./setup.`
@@ -375,15 +389,6 @@ pick a new `--output`), then copy the recovery file to where you want it.
---
<a id="design-image-model-invalid"></a>
### `GSTACK_DESIGN_IMAGE_MODEL="..." is not a gpt-image model name`
**Meaning.** The image model override must look like `gpt-image-2`; anything
else is refused before a request is sent. The live model check always tests
the defaults, whatever this is set to.
**Fix.** Set a gpt-image model name, or `unset GSTACK_DESIGN_IMAGE_MODEL`.
## Codex and other env-var hosts
<a id="gstack-no-install-found"></a>
@@ -446,30 +451,35 @@ stopped instead of touching your project.
## Setup and auto-update
<a id="bun-too-old"></a>
### `gstack needs Bun 1.3.3 or newer (1.4.0 recommended); found X at <path>. Nothing was installed or changed.`
### `gstack needs Bun 1.3.3 or newer (1.4.0 recommended); found <version> at <path>. Nothing was installed or changed.`
Also: `warning: gstack is tested on Bun 1.4.0 (CI pin); found X` and
`warning: could not read the Bun version (...); continuing.`
**Meaning.** Bun older than 1.3.3 accepts but ignores the flags that stop
**Meaning.** Bun older than 1.3.3 silently ignores the build flags that stop
gstack's compiled tools from reading a project's `.env`, so setup refuses it
before writing anything. Bun 1.3.3 to 1.3.x works but is untested; an
unreadable version only warns.
before writing anything. The same link appears on two warnings that do not stop
setup: `warning: gstack is tested on Bun 1.4.0 (CI pin); found <version>` (1.3.3
up to 1.4.0 works but is untested) and `warning: could not read the Bun version
(...)` (setup continued; check `bun --version`).
**Fix.** `bun upgrade`, then `./setup`.
**Fix.**
```bash
bun upgrade
./setup
```
**Expected result.** `bun --version` prints 1.4.0 or newer and setup finishes
with no Bun warning.
<a id="auto-update-bun-too-old"></a>
### `gstack auto-update: update held (bun-too-old: found Bun X at <path>; gstack V needs F or newer); nothing was installed or changed.`
### `gstack auto-update: update held (bun-too-old: found Bun <version> at <path>; gstack <version> needs <floor> or newer); nothing was installed or changed. Fix now: ...`
Also `/gstack-upgrade`'s `BUN_TOO_OLD: bun-too-old: ...; nothing was changed`.
**Meaning.** Team-mode auto-update fetched a release whose setup needs a newer
Bun than the one setup would run. It left your checkout and installed skills at
the current revision and prints this line once per session start. (An older
auto-updater running its first update cannot make this check.)
**Meaning.** The incoming release needs a newer Bun than the one setup would
run, so the update stopped before moving your checkout. Your installed version
keeps working. The first update run by an auto-updater older than this check
cannot do it.
**Fix.** `bun upgrade`. The next session start resumes the update, or run
`cd <gstack checkout> && git pull --ff-only && ./setup` (or `/gstack-upgrade`).
**Fix.** `bun upgrade`; the next session start resumes the update. To update
now: `bun upgrade && cd <gstack checkout> && git pull --ff-only && ./setup`.
<a id="auto-update-incomplete"></a>
### `gstack auto-update: setup did not finish (<reason>); installed skills may be out of date. gstack retries automatically. Fix now: cd <dir> && ./setup`
@@ -581,16 +591,19 @@ after `--`).
**Fix.** `cd <gstack checkout> && ./setup` (or `/unfreeze`).
---
<a id="browse-chromium-pid-unrecorded"></a>
### `[browse] Could not record the Chromium PID (CDP SystemInfo.getProcessInfo: ...); browse stop cannot reap a surviving Chromium.`
### `[browse] Could not record the Chromium PID (CDP SystemInfo.getProcessInfo: <error>); browse stop cannot reap a surviving Chromium.`
**Meaning.** gstack reads the PID of a browser it launched over CDP so `browse
stop` can clean up a Chromium that outlives the server. That read failed, so
this session's browser is not reaped automatically.
**Meaning.** For a headless Chromium it launched, browse reads the browser's
process id over CDP so `browse stop` can kill a browser that outlives the
server. That read failed or took longer than 2 seconds, so this session's
browser is not recorded. Browsers gstack did not launch are never recorded or
killed.
**Fix.** After `browse stop`, check `ps` for a leftover Chromium and kill it by hand.
**Fix.** After `browse stop`, check for a leftover browser with
`ps aux | grep -i chrom` and end it with `kill <pid>`.
---
## Memory and gbrain
@@ -665,6 +678,104 @@ gstack-gbrain-sync --prune-gone-worktrees
---
## Eval reports and pass-rates
These come from the paid eval census reports and `bun run eval:pass-rates`.
The walkthrough for a red census is [docs/evals/census-red.md](evals/census-red.md).
<a id="pass-rates-unknown-flag"></a>
### `eval:pass-rates: unknown flag <flag>` / `eval:pass-rates: unknown case <id> (not an E2E, judge or paid test file id)`
**Meaning.** The flag or case id is not recognized. The command exits 2 before
fetching anything.
**Fix.** `bun run eval:pass-rates --help` lists the flags. `--case` takes a
registry id from `test/helpers/touchfiles-data.ts` or a paid test file path.
<a id="failure-cause-newer"></a>
### `unknown failure_cause "<x>" (written by a newer gstack; update this checkout to read it)`
**Meaning.** A trial record names a failure cause this checkout does not know.
A newer gstack wrote it.
**Fix.** `git pull` (or rebase your branch onto `main`), then rerun the command.
<a id="paid-case-several-owners"></a>
### `--case <id>: registered by <file>, <file>; it needs exactly one`
**Meaning.** More than one paid test file registers the case, so
`test-paid-shards.ts --case` cannot pick one file.
**Fix.** Run the red shard's file directly; the report's `after a repair:` line
prints it: `EVALS=1 EVALS_TIER=<tier> bun test <file>`.
<a id="evidence-too-large"></a>
### `evidence: <artifact> too large to fetch (<N> MB)`
**Meaning.** The slice artifact holding this red's transcript is over 64 MB, so
`eval:pass-rates --run` does not download it.
**Fix.** Download it from the run page, or
`gh api repos/<owner>/<repo>/actions/artifacts/<artifact id>/zip > slice.zip`.
<a id="headroom-alarm"></a>
### `[headroom] <case> session <key>: max <s> of <s> (<pct>) over <n> sample(s), above the 85% cap. Cut work in the skill or fixture; budgets are never raised ...`
**Meaning.** `eval:pass-rates --gate` (the weekly report) found a case whose
slowest session used more than 85% of the timeout it armed. It is one timeout
away from a red.
**Fix.** Cut work in the skill or fixture. Budgets are never raised. Check the
case with `bun run eval:pass-rates --headroom --case <id>`.
<a id="cost-unknown"></a>
### `cost unknown (no billing captured for any of <N> trial(s))` / `cost $<x> known + <N> of <M> trial(s) cost unknown`
**Meaning.** PTY and Codex sessions record no billing, so their cost is
unknown. Only the known sum is shown.
**Fix.** None needed; this is informational.
<a id="cause-provider-stall"></a>
### `· cause provider_stall: no stream event for <N>s ...`
**Meaning.** The session streamed partial messages, then received no event of
any kind for at least 120 seconds while a model request was in flight and no
tool, permission prompt, hook or subagent was outstanding. Under EVAL_POLICY v1
it is still a failed trial.
**Fix.** Inspect it with `bun run eval:pass-rates --run <run id> --case <id>`.
A paid rerun happens only after a repair.
<a id="overlay-wrong-answer"></a>
### Overlay record with `taskCorrect: false` and `answerError` (contract v4)
**Meaning.** The trial completed with a wrong answer. Under overlay contract v4
that is a valid measurement in the comparison; only an overlay-ON wrong answer
fails the case.
**Fix.** Read `answerError` in the trial JSON. No rerun is needed.
<a id="detector-corpora-stale"></a>
### `[detector-corpora] <N> stale entries (refresh from a current census when one fails)`
**Meaning.** Some replay-corpus entries came from an older input series of
their case. This is informational; the corpus still replays them.
**Fix.** When that case goes red in a census, add the new capture to its corpus
and check it with `bun test test/detector-corpus-<case>.test.ts`.
<a id="auq-substance-panel"></a>
### `recommendation substance mean <x> < 4 over samples [<a>,<b>,<c>] (boilerplate/weak)`
**Meaning.** The auq-matrix case scores each captured question's recommendation
with a 3-sample judge panel; their mean was below 4.
**Fix.** Read the logged samples and the captured question. A fix goes in the
skill text, followed by one diagnostic run of auq-matrix.
---
## Push guard (redaction)
<a id="redact-postgres-default-pair"></a>
@@ -685,78 +796,3 @@ Bypass once: `GSTACK_REDACT_PREPUSH=skip git push`.
**Fix.** Write it as `version: 1.2.3.4`, `"version": "..."` or `v1.2.3.4`.
MEDIUM findings do not block pushes.
---
## Evals and census reports (contributors)
<a id="pass-rates-unknown-flag"></a>
### `eval:pass-rates: unknown flag --x` / `unknown case <id>`
**Meaning.** The flag or case id is not recognized; it exits 2 before fetching anything.
**Fix.** `bun run eval:pass-rates --help`; use a registry case id or a paid test file path.
<a id="unknown-failure-cause"></a>
### `unknown failure_cause "<x>" (written by a newer gstack; update this checkout to read it)`
**Fix.** `git pull` (or rebase your branch) and rerun.
<a id="case-registered-by-several"></a>
### `--case <id>: registered by a, b; it needs exactly one`
**Fix.** Run the red shard's file: `EVALS=1 EVALS_TIER=<tier> bun test <file>`
(the report's "after a repair:" line prints it).
<a id="evidence-too-large"></a>
### `evidence: <artifact> too large to fetch (N MB)`
**Fix.** Download it from the run page, or
`gh api repos/<owner>/<repo>/actions/artifacts/<id>/zip > slice.zip`.
<a id="headroom-alarm"></a>
### `[headroom] <case> session <key>: max Xs of Ys (Z%) ... above the 85% cap`
**Meaning.** The case's slowest session used more than 85% of the timeout it armed.
**Fix.** Cut work in the skill or fixture. Budgets are never raised.
<a id="cost-unknown"></a>
### `cost unknown (no billing captured for any of N trial(s))` / `... N of M trial(s) cost unknown`
**Meaning.** PTY and Codex harnesses record no billing, so only the known sum is
shown. Informational.
<a id="provider-stall"></a>
### Red line `· cause provider_stall: no stream event for Ns ...`
**Meaning.** The model request went silent for at least 120 s with no tool,
permission prompt, hook or subagent outstanding. Under EVAL_POLICY v1 this is
still a failure.
**Fix.** Inspect it with `bun run eval:pass-rates --run <id> --case <id>`. A
paid rerun only after a repair.
<a id="overlay-contract-v4"></a>
### `overlay ... contract-v4 ... taskCorrect: false` with `answerError` set
**Meaning.** The trial completed with a wrong answer; that is a valid
measurement in the comparison. Only an overlay-on wrong answer fails the case.
**Fix.** Read `answerError` in the trial JSON. No rerun needed.
<a id="detector-corpora-stale"></a>
### `[detector-corpora] N stale entries (refresh from a current census when one fails)`
**Meaning.** Corpus entries came from an older input series of their case. Informational.
**Fix.** When that case goes red in a census, add the new capture and check it
with `bun test test/detector-corpus-<case>.test.ts`.
<a id="auq-substance-mean"></a>
### `recommendation substance mean X < 4 over samples [a,b,c]` (auq-matrix)
**Meaning.** The 3-sample judge panel's mean was below the threshold.
**Fix.** Read the logged samples and the captured question; fix the skill
text, then run one diagnostic.
+1
View File
@@ -263,6 +263,7 @@ export const CODEX_CI_FILES: readonly string[] = [
'test/skill-e2e-outside-voice.test.ts',
'test/skill-e2e-outside-plan-disabled.test.ts',
'test/skill-e2e-safety-codex-boundary.test.ts',
'test/codex-e2e-multiblock-live.test.ts',
];
/**
+2
View File
@@ -122,6 +122,7 @@
"test/carve-section-loading-review.test.ts": 132284,
"test/carve-section-loading-setup-gbrain.test.ts": 73575,
"test/carve-section-loading-spec.test.ts": 137613,
"test/codex-e2e-multiblock-live.test.ts": 180000,
"test/codex-e2e-recommendation-substance.test.ts": 14617,
"test/codex-e2e-shared-libs.test.ts": 49563,
"test/codex-e2e-sol-scope.test.ts": 32228,
@@ -142,6 +143,7 @@
"test/skill-e2e-coverage-audit.test.ts": 1000,
"test/skill-e2e-cso.test.ts": 303949,
"test/skill-e2e-deploy.test.ts": 1000,
"test/skill-e2e-design-model-smoke.test.ts": 90000,
"test/skill-e2e-design.test.ts": 817000,
"test/skill-e2e-design.test.ts#design-consultation-core": 173162,
"test/skill-e2e-design.test.ts#design-consultation-existing": 225306,
+45
View File
@@ -0,0 +1,45 @@
/**
* The red-census docs stay true to the code they describe: the failure-cause
* glossary in docs/TESTING_INTERNALS.md lists exactly FAILURE_CAUSES, the
* guide's commands use flags eval:pass-rates and test-paid-shards accept, and
* the guide is linked from CONTRIBUTING.md and both census reports.
*/
import { describe, test, expect } from 'bun:test';
import * as fs from 'fs';
import * as path from 'path';
import { FAILURE_CAUSES } from './helpers/eval-store';
import { parsePassRatesArgs } from '../scripts/lib/eval-history';
const ROOT = path.join(import.meta.dir, '..');
const read = (rel: string) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
const internals = read('docs/TESTING_INTERNALS.md');
const guide = read('docs/evals/census-red.md');
describe('red-census docs', () => {
test('the failure-cause glossary has one row per FAILURE_CAUSES value, in precedence order', () => {
const section = internals.slice(internals.indexOf('<a id="failure-causes"></a>'));
const rows = [...section.slice(0, section.indexOf('\n\n', section.indexOf('| `failure_cause` |'))).matchAll(/^\| `([a-z_]+)` \|/gm)]
.map(m => m[1]).filter(cause => cause !== 'failure_cause');
expect(rows).toEqual([...FAILURE_CAUSES]);
});
test('every eval:pass-rates command in the guide parses', () => {
const commands = [...guide.matchAll(/^bun run eval:pass-rates ([^\n#]*)/gm)].map(m => m[1]!.trim());
expect(commands.length).toBeGreaterThanOrEqual(6);
for (const command of commands) {
const args = command.replace(/<[^>]+>/g, 'x').split(/\s+/).filter(Boolean)
.map((arg, i, all) => (all[i - 1] === '--run' || all[i - 1] === '--runs' ? '10' : arg));
const parsed = parsePassRatesArgs(args, () => true);
expect('error' in parsed ? parsed.error : null, command).toBeNull();
}
});
test('the guide is linked from CONTRIBUTING, TESTING_INTERNALS and both census reports', () => {
expect(read('CONTRIBUTING.md')).toContain('docs/evals/census-red.md');
expect(internals).toContain('(evals/census-red.md)');
for (const workflow of ['evals-periodic.yml', 'evals-marathon.yml']) {
expect(read(`.github/workflows/${workflow}`)).toContain('/docs/evals/census-red.md');
}
for (const anchor of ['failure-causes', 'eval-verdict-policy']) expect(internals).toContain(`<a id="${anchor}"></a>`);
});
});
+116
View File
@@ -0,0 +1,116 @@
/**
* Live Codex multi-block check (E6, periodic, host-run Codex job).
*
* Codex runs every shell block of a skill in a fresh shell. This case runs
* `./setup --host codex` once from the job's own checkout into a fresh HOME
* and CODEX_HOME, then checks what a Codex user gets:
* 1. `codex debug prompt-input` lists the `gstack` router, and the router
* it names is a real file (Codex skips a symlinked SKILL.md);
* 2. `codex exec` running /learn's "Show recent" block, a later block that
* only its own prelude can root, runs the installed helper and prints the
* learning seeded before the run; no block reports a missing install or
* an unresolved gstack helper path.
* Auth is the CI login home's auth.json only (scopeCodexAccess hands this
* file the CLI on PATH and that home as CODEX_HOME).
*/
import { describe, test, afterAll, expect } from 'bun:test';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { spawn, spawnSync } from 'child_process';
import { CAPTURE_MS } from './helpers/eval-budgets';
import { e2eTierEnabled } from './helpers/e2e-gate';
import { hermeticChildEnv } from './helpers/hermetic-env';
import { parseCodexJSONL, type CodexResult } from './helpers/codex-session-runner';
import { CODEX_EVAL_FINALIZE_MS, createCodexEvalCollector, runRecordedCodexEval } from './helpers/codex-eval';
import { MULTIBLOCK_SENTINEL_KEY, multiblockProblems, routerPathFromPromptInput } from './helpers/codex-multiblock';
import { CODEX_FRONTIER_MODEL } from '../scripts/resolvers/constants';
const ROOT = path.resolve(import.meta.dir, '..');
const CASE = 'codex-multiblock-live';
const SETUP_MS = 300_000;
const CODEX_AVAILABLE = spawnSync('which', ['codex'], { timeout: 30_000 }).status === 0;
const ENABLED = e2eTierEnabled('periodic') && CODEX_AVAILABLE;
if (e2eTierEnabled('periodic') && !CODEX_AVAILABLE) process.stderr.write(`\n${CASE}: SKIPPED: codex binary not on PATH\n`);
const collector = ENABLED ? createCodexEvalCollector('codex-e2e-multiblock-live') : null;
afterAll(async () => { await collector?.finalize(); });
/** `codex exec --json` with stdout/stderr in files (no pipe can outlive the process group), killed on abort. */
function codexExec(args: string[], env: NodeJS.ProcessEnv, cwd: string, scratch: string, signal: AbortSignal): Promise<CodexResult> {
const started = Date.now();
const outFile = path.join(scratch, 'exec.jsonl');
const errFile = path.join(scratch, 'exec.stderr');
const out = fs.openSync(outFile, 'w');
const err = fs.openSync(errFile, 'w');
const proc = spawn('codex', args, { cwd, env, stdio: ['ignore', out, err], detached: true });
fs.closeSync(out);
fs.closeSync(err);
const kill = () => { try { process.kill(-proc.pid!, 'SIGKILL'); } catch { /* already gone */ } };
signal.addEventListener('abort', kill, { once: true });
return new Promise(resolve => {
const finish = (exitCode: number) => {
signal.removeEventListener('abort', kill);
kill();
const rawLines = fs.readFileSync(outFile, 'utf8').split('\n').filter(line => line.trim());
resolve({ ...parseCodexJSONL(rawLines), rawLines, exitCode: signal.aborted ? 124 : exitCode,
durationMs: Date.now() - started, stderr: fs.readFileSync(errFile, 'utf8') });
};
proc.once('error', () => finish(1));
proc.once('exit', (code, sig) => finish(code ?? (sig ? 128 + (os.constants.signals[sig] ?? 0) : 1)));
});
}
(ENABLED ? describe : describe.skip)('live Codex multi-block install (E6)', () => {
test(CASE, async () => {
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'e6-'));
try {
const home = path.join(scratch, 'home');
const codexHome = path.join(home, '.codex');
const repo = path.join(scratch, 'repo');
// Codex will not create its sandbox helper aliases under its temp dir, so TMPDIR sits beside CODEX_HOME.
const tmp = path.join(home, 'tmp');
for (const dir of [codexHome, repo, tmp]) fs.mkdirSync(dir, { recursive: true });
const ciHome = process.env.CODEX_HOME ?? path.join(os.homedir(), '.codex');
if (fs.existsSync(path.join(ciHome, 'auth.json'))) fs.copyFileSync(path.join(ciHome, 'auth.json'), path.join(codexHome, 'auth.json'));
const env = hermeticChildEnv({ HOME: home, CODEX_HOME: codexHome, TMPDIR: tmp }, { extraAllow: ['OPENAI_API_KEY', 'CODEX_*'] });
const setupStarted = Date.now();
const setup = spawnSync('./setup', ['--host', 'codex'], { cwd: ROOT, env, encoding: 'utf8', timeout: SETUP_MS });
console.log(`${CASE}: ./setup --host codex exit ${setup.status} in ${Math.round((Date.now() - setupStarted) / 1000)}s`);
expect(setup.status, `${setup.stdout.slice(-2000)}\n${setup.stderr.slice(-2000)}`).toBe(0);
const promptInput = spawnSync('codex', ['debug', 'prompt-input'], { cwd: repo, env, encoding: 'utf8', timeout: 60_000 });
expect(promptInput.status, promptInput.stderr).toBe(0);
const router = routerPathFromPromptInput(promptInput.stdout);
expect(router, 'codex debug prompt-input does not list the gstack router').toBe(path.join(codexHome, 'skills', 'gstack', 'SKILL.md'));
expect(fs.lstatSync(router!).isFile()).toBe(true);
for (const args of [['init', '-q'], ['remote', 'add', 'origin', 'https://github.com/example/e6-fixture.git']]) {
expect(spawnSync('git', args, { cwd: repo, env, timeout: 30_000 }).status).toBe(0);
}
const seed = spawnSync(path.join(codexHome, 'skills', 'gstack', 'bin', 'gstack-learnings-log'), [JSON.stringify({ skill: 'learn',
type: 'pitfall', key: MULTIBLOCK_SENTINEL_KEY, insight: 'Seeded by the E6 live Codex check', confidence: 9, source: 'user-stated' })],
{ cwd: repo, env, encoding: 'utf8', timeout: 30_000 });
expect(seed.status, seed.stderr).toBe(0);
const result = await runRecordedCodexEval({
name: CASE,
suite: 'codex-e2e-multiblock-live',
budgetMs: CAPTURE_MS,
run: signal => codexExec(['exec', '--json', '-s', 'read-only', '--skip-git-repo-check',
'--model', process.env.GSTACK_CODEX_MODEL ?? CODEX_FRONTIER_MODEL,
'Use the gstack learn skill to show this project\'s recent learnings. Run its "Show recent" step, then list what it printed.'],
env, repo, scratch, signal),
validate: run => {
const problems = multiblockProblems(run.rawLines);
if (problems.length) throw new Error(problems.join('\n'));
},
record: entry => collector?.addTest(entry),
});
console.log(`${CASE}: ${result.toolCalls.length} commands, ${result.tokens} tokens, ${Math.round(result.durationMs / 1000)}s`);
} finally {
fs.rmSync(scratch, { recursive: true, force: true });
}
}, SETUP_MS + 120_000 + CAPTURE_MS + CODEX_EVAL_FINALIZE_MS);
});
+61
View File
@@ -0,0 +1,61 @@
/**
* Free replays for the live Codex multi-block oracles (E6): synthetic
* `codex exec --json` events in the shape codex-cli 0.160 writes (see
* test/fixtures/codex-sandbox/exec-json-healthy.jsonl) and a
* `codex debug prompt-input` skills block.
*/
import { describe, test, expect } from 'bun:test';
import { MULTIBLOCK_SENTINEL_KEY, multiblockProblems, routerPathFromPromptInput } from './helpers/codex-multiblock';
const PRELUDE = '[ -d "${GSTACK_ROOT:-/-}/bin" ]&&[ -d "$GSTACK_ROOT/lib" ]||{ _r=$(git rev-parse --show-toplevel 2>/dev/null)/.agents/skills/gstack;'
+ '[ -d "$_r/bin" ]||_r=${CODEX_HOME:-~/.codex}/skills/gstack;[ -d "$_r/bin" ]||{ echo "gstack: no install found (tried $_r). Fix: ./setup --host codex from your gstack checkout; ./setup --status shows it.">&2;exit 1;};GSTACK_ROOT=$_r;}';
const SHOW_RECENT = `${PRELUDE}\nSLUG=$($GSTACK_ROOT/bin/gstack-slug --get SLUG 2>/dev/null)\n$GSTACK_ROOT/bin/gstack-learnings-search --limit 20 2>/dev/null || echo "No learnings yet."`;
const LISTING = `LEARNINGS: 1 loaded (1 pitfall)\n\n## Pitfalls\n- [${MULTIBLOCK_SENTINEL_KEY}] (confidence: 9/10, user-stated, 2026-10-04)\n Seeded by the E6 live Codex check\n`;
const command = (cmd: string, output: string, exitCode: number) => JSON.stringify({ type: 'item.completed',
item: { id: 'item_2', type: 'command_execution', command: `/bin/bash -lc ${JSON.stringify(cmd)}`, aggregated_output: output, exit_code: exitCode, status: exitCode === 0 ? 'completed' : 'failed' } });
const run = (...lines: string[]) => ['{"type":"thread.started","thread_id":"t"}', ...lines, '{"type":"turn.completed","usage":{"input_tokens":1,"output_tokens":1}}'];
const preamble = command(`${PRELUDE}\nGSTACK_BIN=$GSTACK_ROOT/bin\n"$GSTACK_BIN/gstack-skill-start" --skill "learn"`, 'BRANCH: main\n', 0);
describe('multiblockProblems', () => {
test('a later block rooted by its own prelude that prints the seeded learning passes', () => {
expect(multiblockProblems(run(preamble, command(SHOW_RECENT, LISTING, 0)))).toEqual([]);
});
test('a block that cannot find the install fails, even when another block succeeded', () => {
const missing = command(SHOW_RECENT, 'gstack: no install found (tried /root/.codex/skills/gstack). Fix: ./setup --host codex', 1);
const problems = multiblockProblems(run(preamble, missing, command(SHOW_RECENT, LISTING, 0)));
expect(problems).toHaveLength(1);
expect(problems[0]).toStartWith('a block could not find the install');
});
test('the pre-prelude symptom (an empty root, /bin/gstack-slug) fails', () => {
const problems = multiblockProblems(run(command('SLUG=$($GSTACK_ROOT/bin/gstack-slug --get SLUG)\n$GSTACK_ROOT/bin/gstack-learnings-search',
'bash: line 1: /bin/gstack-slug: No such file or directory\nbash: line 2: /bin/gstack-learnings-search: No such file or directory\n', 127)));
expect(problems.some(p => p.startsWith('a gstack helper path did not resolve: /bin/gstack-slug: No such file or directory'))).toBe(true);
expect(problems.some(p => p.startsWith('no learnings-search block rooted by its own prelude printed the seeded learning'))).toBe(true);
});
test('a preamble-only run, a run without the seeded learning, or a hand-set root does not count', () => {
expect(multiblockProblems(run(preamble))).toEqual(['no block ran gstack-learnings-search (the skill\'s "Show recent" block)']);
expect(multiblockProblems(run(preamble, command(SHOW_RECENT, 'No learnings yet.\n', 0)))[0]).toContain('exit 0, sentinel absent');
const handSet = command('GSTACK_ROOT=/home/u/.codex/skills/gstack\n$GSTACK_ROOT/bin/gstack-learnings-search --limit 20', LISTING, 0);
expect(multiblockProblems(run(preamble, handSet))[0]).toContain('GSTACK_ROOT set by hand');
});
});
describe('routerPathFromPromptInput', () => {
const promptInput = (entries: string[]) => JSON.stringify([{ type: 'message', role: 'developer', content: [{ type: 'input_text',
text: ['<skills_instructions>', '## Skills', '### Skill roots', '- `r0` = `/h/.codex/skills`', '- `r1` = `/h/.codex/skills/.system`',
'### Available skills', '- skill-creator: Create or update a Codex skill. (file: r1/skill-creator/SKILL.md)', ...entries].join('\n') }] }], null, 2);
test('names the router file through the skill roots table', () => {
const listed = promptInput(['- gstack: Route a task to the right gstack skill. (gstack) (file: r0/gstack/SKILL.md)',
'- learn: Manage project learnings. (gstack) (file: r0/gstack-learn/SKILL.md)']);
expect(routerPathFromPromptInput(`WARNING: proceeding without PATH aliases\n${listed}`)).toBe('/h/.codex/skills/gstack/SKILL.md');
});
test('is null when Codex lists gstack skills but not the router', () => {
expect(routerPathFromPromptInput(promptInput(['- learn: Manage project learnings. (gstack) (file: r0/gstack-learn/SKILL.md)']))).toBeNull();
});
});
+193
View File
@@ -0,0 +1,193 @@
/**
* The weekly tracking issues (evals-periodic.yml, evals-marathon.yml).
*
* Only main posts to or closes a tracking issue: a branch-dispatched census
* writes the same report to its step summary and a report artifact instead.
* The periodic report carries the full pass-rate alarm lines, the red ledger,
* session headroom and the all-green estimate, read with an explicit
* --branch, and every report-derived line passes the published-text
* sanitizer. The write steps run here as shell scripts with a fake
* `eval:pass-rates`; the sanitizer is the real one.
*/
import { describe, test, expect } from 'bun:test';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { spawnSync } from 'child_process';
const ROOT = path.join(import.meta.dir, '..');
type Step = { name?: string; if?: string; run?: string; uses?: string; with?: Record<string, string> };
type Workflow = { jobs: Record<string, { steps?: Step[] }> };
const WORKFLOWS = ['evals-periodic.yml', 'evals-marathon.yml'] as const;
const parse = (name: string) => Bun.YAML.parse(fs.readFileSync(path.join(ROOT, '.github/workflows', name), 'utf8')) as Workflow;
const MAIN_ONLY = "github.ref == 'refs/heads/main'";
describe('tracking issues are main-only', () => {
for (const name of WORKFLOWS) {
test(`${name}: only the upsert and close steps call gh issue, and both run on main only`, () => {
const issueSteps = Object.values(parse(name).jobs).flatMap(job => job.steps ?? []).filter(step => /\bgh issue\b/.test(step.run ?? ''));
expect(issueSteps.map(step => step.name).sort()).toEqual(['Close the tracking issue on a green run', 'Upsert tracking issue on failure']);
for (const step of issueSteps) expect(step.if, step.name).toContain(`always() && ${MAIN_ONLY} && `);
const upsert = issueSteps.find(step => step.name === 'Upsert tracking issue on failure')!;
expect(upsert.run).toMatch(/gh issue comment .*--body-file "\$BODY_FILE"/);
expect(upsert.run).toMatch(/gh issue create .*--body-file "\$BODY_FILE"/);
expect(issueSteps.find(step => step.name === 'Close the tracking issue on a green run')!.run).toContain('gh issue close');
});
test(`${name}: every ref writes the report to the step summary and an artifact`, () => {
const steps = parse(name).jobs.report!.steps!;
const write = steps.find(step => /^Write the (census|marathon) report$/.test(step.name ?? ''))!;
const upload = steps.find(step => /^Upload the (census|marathon) report$/.test(step.name ?? ''))!;
const upsert = steps.find(step => step.name === 'Upsert tracking issue on failure')!;
expect(write.if).not.toContain('github.ref');
expect(upload.if).toBe(write.if);
expect(upsert.if).toBe(write.if!.replace('always() && ', `always() && ${MAIN_ONLY} && `));
expect(write.run).toContain('>> "$GITHUB_STEP_SUMMARY"');
expect(write.run).toContain('docs/evals/census-red.md');
expect(write.run).toContain('./scripts/lib/published-text');
const reportFile = /> (\/tmp\/[\w-]+\.md)\n/.exec(write.run!)![1]!;
expect(upload.uses).toStartWith('actions/upload-artifact@');
expect(upload.with!.path).toBe(reportFile);
expect(upload.with!.name).toMatch(/^(census|marathon)-report-a\$\{\{ github\.run_attempt \}\}$/);
expect(upsert.run).toContain(`BODY_FILE=${reportFile}`);
});
}
test('evals-periodic.yml: every pass-rates read names its branch explicitly', () => {
const runs = parse('evals-periodic.yml').jobs.report!.steps!.map(step => step.run ?? '').join('\n');
const calls = [...runs.matchAll(/^\s*bun run eval:pass-rates [^\n>]*/gm)].map(m => m[0].trim());
expect(calls.length).toBe(3);
for (const call of calls) expect(call).toContain('--runs 10 --branch "$GITHUB_REF_NAME"');
expect(calls.some(call => call.includes('--gate'))).toBe(true);
expect(calls.some(call => call.includes('--reds'))).toBe(true);
expect(calls.some(call => call.includes('--headroom'))).toBe(true);
});
});
/** Run a workflow step's script with its expressions and /tmp paths bound to a scratch dir. */
function runStep(workflow: string, stepName: string, files: Record<string, string>, expressions: Record<string, string>) {
const step = parse(workflow).jobs.report!.steps!.find(s => s.name === stepName)!;
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'tracking-issue-'));
const work = path.join(dir, 'tmp');
const bin = path.join(dir, 'bin');
fs.mkdirSync(bin);
for (const [rel, body] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(work, rel)), { recursive: true });
fs.writeFileSync(path.join(work, rel), body);
}
const log = path.join(dir, 'pass-rates-calls.log');
fs.writeFileSync(path.join(bin, 'bun'), [
'#!/bin/sh',
'if [ "$1" = run ] && [ "$2" = eval:pass-rates ]; then',
` echo "$*" >> '${log}'`,
` case " $* " in *" --reds "*) cat '${work}/fake-reds.txt';; *" --headroom "*) cat '${work}/fake-headroom.txt';; esac`,
' exit 0',
'fi',
`exec '${process.execPath}' "$@"`,
].join('\n'), { mode: 0o755 });
const script = step.run!
.replace(/\$\{\{ ([^}]+) \}\}/g, (_, expr: string) => expressions[expr.trim()] ?? `<${expr.trim()}>`)
.replaceAll('/tmp/', `${work}/`);
const summary = path.join(dir, 'step-summary.md');
const result = spawnSync('bash', ['-c', script], {
cwd: ROOT, encoding: 'utf8', timeout: 60_000,
env: { PATH: `${bin}:${process.env.PATH}`, HOME: dir, GITHUB_STEP_SUMMARY: summary, GITHUB_REF_NAME: 'feature/x',
GITHUB_SHA: 'abc1234', GITHUB_EVENT_NAME: 'workflow_dispatch', GITHUB_SERVER_URL: 'https://github.com',
GITHUB_REPOSITORY: 'garrytan/gstack', GITHUB_RUN_ID: '42', REDISPATCH: 'false', REDISPATCH_OF: '' },
});
const read = (file: string) => (fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '');
const reports = Object.fromEntries(fs.readdirSync(work).filter(name => name.endsWith('.md')).map(name => [name, read(path.join(work, name))]));
const out = { status: result.status, stderr: result.stderr, summary: read(summary), calls: read(log), body: (name: string) => reports[name] ?? '' };
fs.rmSync(dir, { recursive: true, force: true });
return out;
}
const PERIODIC_EXPRESSIONS = {
'steps.reconcile.outputs.exit': '1', 'steps.gate-reconcile.outputs.exit': '0', 'steps.pass-rates.outputs.exit': '1',
'needs.eval-slices.result': 'failure', 'needs.eval-codex-slices.result': 'success', 'needs.gate-census.result': 'success',
};
describe('the periodic census report', () => {
const passRates = [
'scope: garrytan/gstack evals-periodic.yml on feature/x + main, last 10 completed run(s) per branch: 1, 2',
'pass-rates: policy v1, 40 post-policy trial(s), 0 pre-policy (display only)',
' label kind tier current series pre-policy manual case',
' ACTION rule gate 9/10 [59.6%–98.2%] - 0 noisy-case',
'ACTION REQUIRED (3):',
' [drift] noisy-case passes 9/10 (below 95% over >= 10 trials): fix it, or propose a CASE_QUARANTINE entry with a written diagnosis',
' [headroom] slow-case session sdk:slow-case#1: max 520s of 600s (87%) above the 85% cap; cut work, never raise the budget',
' [drift] mention @attacker ``` </details><img src=x onerror=alert(1)> ghp_' + 'a'.repeat(36),
'free-suite flaky-passes (/home/x/.gstack/flake-ledger.jsonl):',
' 2x test/unrelated.test.ts',
].join('\n');
const reds = [
'scope: garrytan/gstack evals-periodic.yml on feature/x + main, last 10 completed run(s) per branch: 1, 2',
'reds: verdict reds per census lane by failure class and machine cause',
' 1 periodic/full census: 1 red of 146 verdicts — class: assertion 1; cause: provider_stall 1',
' ✗ noisy-case FAIL t1 assertion/provider_stall — Error: expect(received).toBe(expected)',
'pooled: 1 red of 146 verdicts = 0.7% per verdict [95% 0.1%–3.8%]',
'all-green probability (approximation, assumes independent verdicts): 81.2% for the latest census\'s 146 verdicts [55%–95% with the pooled rate\'s 95% interval as prior]; formula Π(1 - p_i) over its cases; rerun: bun run eval:pass-rates --reds',
].join('\n');
const headroom = [
'scope: garrytan/gstack evals-periodic.yml on feature/x + main',
'headroom: slowest armed session per case (warn > 75%, alarm > 85%; insufficient below 3 samples; censored = timed out)',
' ALARM 520s/600s (87%) 3 540s slow-case',
].join('\n');
const files = {
'pass-rates.txt': passRates, 'fake-reds.txt': reds, 'fake-headroom.txt': headroom,
'paid-report/report-summary.md': '## Periodic summary\n✗ noisy-case', 'report.txt': 'reconcile tail',
};
test('carries the full alarm lines, red ledger, headroom and all-green estimate, sanitized, for an explicit branch', () => {
const run = runStep('evals-periodic.yml', 'Write the census report', files, PERIODIC_EXPRESSIONS);
expect(run.stderr).toBe('');
expect(run.status).toBe(0);
const body = run.body('census-report.md');
expect(run.summary).toBe(body);
expect(body).toStartWith('Periodic census report: branch feature/x @ abc1234, event workflow_dispatch, run https://github.com/garrytan/gstack/actions/runs/42');
expect(body).toContain('triage with https://github.com/garrytan/gstack/blob/abc1234/docs/evals/census-red.md');
expect(body).toContain('- periodic reconciliation exit: 1 (slices job: failure, Codex slices job: success)');
expect(body).toContain('- all-green probability (approximation, assumes independent verdicts): 81.2% for the latest census');
expect(body).toContain('## Periodic summary');
// Every alarm line, not just the header; the flaky-pass block is not an alarm.
expect(body).toContain('ACTION REQUIRED (3):');
expect(body).toContain('[drift] noisy-case passes 9/10 (below 95% over >= 10 trials): fix it');
expect(body).toContain('[headroom] slow-case session sdk:slow-case#1: max 520s of 600s (87%) above the 85% cap');
expect(body).not.toContain('test/unrelated.test.ts');
expect(body).toContain('✗ noisy-case FAIL t1 assertion/provider_stall — Error: expect(received).toBe(expected)');
expect(body).toContain('ALARM 520s/600s (87%)');
// Hostile alarm text stays literal inside a fence it cannot close; mentions and tokens are neutralized.
const hostile = body.split('\n').findIndex(line => line.includes('mention @'));
const fence = body.split('\n').slice(0, hostile).reverse().find(line => /^`{3,}$/.test(line))!;
expect(fence.length).toBeGreaterThan(3);
expect(body).toContain('@\u200battacker');
expect(body).not.toContain('ghp_' + 'a'.repeat(36));
expect(run.calls.trim().split('\n')).toEqual([
'run eval:pass-rates --reds --runs 10 --branch feature/x',
'run eval:pass-rates --headroom --runs 10 --branch feature/x',
]);
});
test('missing history becomes a visible line, never a failed step', () => {
const run = runStep('evals-periodic.yml', 'Write the census report', { 'fake-reds.txt': '', 'fake-headroom.txt': '' }, PERIODIC_EXPRESSIONS);
expect(run.status).toBe(0);
const body = run.body('census-report.md');
expect(body).toContain('all-green probability: unavailable (see the red ledger)');
expect(body).toContain('(no pass-rate alarms)');
expect(body).toContain('(no periodic report summary)');
});
});
describe('the marathon report', () => {
test('fences the sanitized reconciliation output and links the guide', () => {
const run = runStep('evals-marathon.yml', 'Write the marathon report', { 'report.txt': 'red slice\nping @someone ```' },
{ 'steps.reconcile.outputs.exit': '1', 'needs.eval-slices.result': 'success' });
expect(run.status).toBe(0);
const body = run.body('marathon-report.md');
expect(run.summary).toBe(body);
expect(body).toStartWith('Marathon report (non-blocking lane): branch feature/x @ abc1234, event workflow_dispatch');
expect(body).toContain('docs/evals/census-red.md');
expect(body).toContain('- reconciliation exit: 1');
expect(body).toContain('````\nred slice\nping @\u200bsomeone ```\n````');
});
});
+3 -3
View File
@@ -377,11 +377,11 @@ describe('panel verdict surfaces (eval reliability policy)', () => {
const gate = report.steps.find(step => step.id === 'pass-rates')!;
expect(gate.run).toContain('bun run eval:pass-rates --gate --runs 10');
expect(gate.if).toBe('always()');
for (const name of ['Upsert tracking issue on failure', 'Fail the workflow when reconciliation failed']) {
for (const name of ['Write the census report', 'Upsert tracking issue on failure', 'Fail the workflow when reconciliation failed']) {
expect(report.steps.find(step => step.name === name)!.if).toContain("steps.pass-rates.outputs.exit != '0'");
}
const upsert = report.steps.find(step => step.name === 'Upsert tracking issue on failure')!;
expect(upsert.run).toContain('report-summary.md');
// The report body is written once on every ref; the main-only upsert posts it (test/evals-tracking-issue.test.ts).
expect(report.steps.find(step => step.name === 'Write the census report')!.run).toContain('report-summary.md');
expect(report.steps.find(step => step.name === 'Close the tracking issue on a green run')!.run).toContain('gh issue close');
expect(report.steps.filter(step => step.with?.name?.startsWith('trial-outcomes-')).length).toBe(2);
const redispatch = jobs.redispatch!;
+68
View File
@@ -0,0 +1,68 @@
/**
* Oracles for the live Codex multi-block case (E6,
* test/codex-e2e-multiblock-live.test.ts). Codex runs every shell block in a
* fresh shell, so a gstack skill works there only when each later block
* resolves the install on its own (the env-var host prelude). The case
* installs gstack with `./setup --host codex` into a fresh CODEX_HOME, seeds
* one learning, and runs /learn's "Show recent" block through `codex exec`.
* Pure functions: the free test replays synthetic `exec --json` events.
*/
export const MULTIBLOCK_SENTINEL_KEY = 'e6-multiblock-sentinel';
interface CommandEvent { command: string; output: string; exitCode: number | null }
function completedCommands(rawLines: readonly string[]): CommandEvent[] {
const commands: CommandEvent[] = [];
for (const line of rawLines) {
try {
const event = JSON.parse(line);
if (event?.type !== 'item.completed' || event.item?.type !== 'command_execution') continue;
commands.push({ command: String(event.item.command ?? ''), output: String(event.item.aggregated_output ?? ''),
exitCode: typeof event.item.exit_code === 'number' ? event.item.exit_code : null });
} catch { /* not an event line */ }
}
return commands;
}
/**
* Problems with a live run: any block that could not find the install, a
* gstack helper path that did not resolve, or no later block (the skill's
* learnings-search block, which only the prelude can root) that ran the
* installed helper and printed the seeded learning.
*/
export function multiblockProblems(rawLines: readonly string[], sentinel = MULTIBLOCK_SENTINEL_KEY): string[] {
const commands = completedCommands(rawLines);
const problems: string[] = [];
for (const { command, output } of commands) {
if (output.includes('gstack: no install found')) problems.push(`a block could not find the install: ${output.trim().slice(0, 200)}`);
const unresolved = /\S*\/bin\/gstack-[\w-]+: (?:No such file or directory|command not found|not found)/.exec(output);
if (unresolved) problems.push(`a gstack helper path did not resolve: ${unresolved[0]} (in: ${command.slice(0, 120)})`);
}
const search = commands.filter(c => c.command.includes('gstack-learnings-search'));
if (search.length === 0) problems.push('no block ran gstack-learnings-search (the skill\'s "Show recent" block)');
else if (!search.some(c => c.exitCode === 0 && c.output.includes(sentinel) && !/GSTACK_ROOT=["']?\//.test(c.command))) {
problems.push(`no learnings-search block rooted by its own prelude printed the seeded learning ${sentinel}: `
+ search.map(c => `exit ${c.exitCode}, ${c.output.includes(sentinel) ? 'sentinel present' : 'sentinel absent'}`
+ `${/GSTACK_ROOT=["']?\//.test(c.command) ? ', GSTACK_ROOT set by hand' : ''}`).join('; '));
}
return problems;
}
/**
* The `gstack` router entry from `codex debug prompt-input` output (a JSON
* array of input items): the skill path it names, resolved through the skill
* roots table, or null when Codex does not list the router.
*/
export function routerPathFromPromptInput(promptInput: string): string | null {
let text = promptInput;
try {
const items = JSON.parse(promptInput.slice(promptInput.indexOf('[')));
text = (Array.isArray(items) ? items : []).flatMap((item: { content?: Array<{ text?: string }> }) => item.content ?? [])
.map(part => part.text ?? '').join('\n');
} catch { /* scan the raw text */ }
const entry = /^- gstack: .*\(file: (r\d+)\/gstack\/SKILL\.md\)$/m.exec(text);
if (!entry) return null;
const root = new RegExp(`^- \`${entry[1]}\` = \`([^\`]+)\`$`, 'm').exec(text);
return root ? `${root[1]}/gstack/SKILL.md` : null;
}
+41
View File
@@ -0,0 +1,41 @@
/**
* Design model smoke (E5, periodic): runs design/scripts/live-model-check.ts,
* which sends the design binary's real request bodies with the DEFAULT
* models (overrides are ignored), so a retired or unentitled default fails
* here before users hit it. Without OPENAI_API_KEY the case is a Bun skip:
* the report lists it as SKIPPED with zero coverage credit, never a pass.
* With a key, exit 0 passes; exit 2 (the script saw no key) and every other
* exit fail with the script's output attached.
*/
import { describe, test, expect } from 'bun:test';
import { spawnSync } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { e2eTierEnabled } from './helpers/e2e-gate';
const ROOT = path.resolve(import.meta.dir, '..');
const CASE = 'design-model-smoke';
const KEY_PRESENT = !!process.env.OPENAI_API_KEY?.trim();
const CHECK_MS = 400_000;
if (e2eTierEnabled('periodic') && !KEY_PRESENT) {
process.stderr.write(`\n${CASE}: SKIPPED: OPENAI_API_KEY is not set; the design model defaults were not checked (no coverage)\n`);
}
describe.skipIf(!e2eTierEnabled('periodic') || !KEY_PRESENT)('design live model defaults (E5)', () => {
test(CASE, () => {
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'design-model-smoke-'));
try {
const run = spawnSync(process.execPath, ['run', 'design/scripts/live-model-check.ts'], {
cwd: ROOT, encoding: 'utf8', timeout: CHECK_MS,
env: { ...process.env, HOME: home, GSTACK_HOME: path.join(home, '.gstack') },
});
const output = `${run.stdout ?? ''}${run.stderr ?? ''}`.trim();
console.log(output);
if (run.status === 2) throw new Error(`live-model-check saw no OPENAI_API_KEY although the test had one; nothing was checked:\n${output}`);
expect(run.status, output).toBe(0);
} finally {
fs.rmSync(home, { recursive: true, force: true });
}
}, CHECK_MS + 10_000);
});