v1.91.33.0 feat(ship): measurement bar with explained-red and pooled decisions, weekly flaky-test sweep on main (#3059)

* refactor(eval-history): share pass-rate history loading as loadPassRateHistory()

eval:pass-rates' main block loaded local dirs or the weekly trial-outcomes
artifacts inline. Move that into an exported function returning records,
scope and fetch errors, so the off-ship sweep reads the same history the
same way. Behavior unchanged.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* feat(ship-measure): the measurement bar and the weekly off-ship sweep CLI

Decide each diagnostic measurement with one bar (scripts/lib/measure-bar.ts):
MEETS at 9/10; MEETS-qualified at 8/10 when every red is a mechanical
provider error, judge noise or a cited model miss; EXTEND at 7/10 (or an
unqualified 8/10) for one more batch on identical inputs, decided once on
the pooled 20; BELOW otherwise and for any timeout, hang, regression or
fixable cause. Behavior uses 12 trials (11/10/9; 22/20 of 24); judge counts
median-panel outputs. A batch with more than 30% provider-evidence failures
is void and redispatched once. Below-strict reds nobody classified return
needs-classify.

ship-measure gains classify (append-only records; provider refused without
evidence), decide and extend (refuses a changed identity, names the field,
never a third batch), exit codes 5 and 6, and a pooling identity (frozen
tree, runner, CLI, model, CI image, Bun, policy and harness versions).

ship-measure sweep ranks main's gate cases by their cost to the all-green
probability from eval:pass-rates history, measures the top K with the same
runner and bar under a weekly cap shared with the last 7 days of sweep
reports (ship_measure_sweep_cases, ship_measure_sweep_budget_usd), and
writes a report with the predicted all-green probability before and after.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* feat(ship): the measure loop decides by the bar's exit codes

Step 4 branches on MEETS/MEETS-qualified, needs-classify (with the
qualifying and fix-round classes), EXTEND once and BELOW; the rest of the
loop prose is tightened to offset most of the growth. ship's union size
cap moves from 1.495 to 1.4975 (measured 281,058 / 187,706 = 1.4973).

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* ci: weekly eval-sweep workflow on main

Mondays 12:00 UTC and on dispatch (k, cap_usd, dry_run): runs
ship-measure sweep in the published CI image by digest with the census's
paid-eval setup, downloads the last 7 days of sweep reports first so the
weekly cap holds across runs, uploads the report and captures, and writes
the job summary. Read-only grants; it never pushes or opens a pull request.
One sweep at a time repository-wide, never cancelled mid-spend.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: the measurement bar table and the weekly sweep

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* chore: v1.91.33.0

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* ci(temp): run eval-sweep on push to this branch (K=2, $25) for end-to-end validation

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Revert "ci(temp): run eval-sweep on push to this branch (K=2, $25) for end-to-end validation"

This reverts commit f4c58f2869.

* ci: classify eval-sweep.yml, the sweep CLI and actionlint labels as free-only PR inputs

None is read by a PR-gate case, so a change to them selects no paid case
instead of restoring the full gate (test/free-fixtures.test.ts workflow
inventory).

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(ship-measure): the selection preflight accepts name-mode cases

A case registered beside others in its file lists its trial shard as
<file>#<id>~t1, not <file> (trial 1), so the preflight refused every
name-mode case (for example ship-exploratory-late-input) before any paid
trial. Accept both forms.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* ci(temp): run eval-sweep on push to this branch (K=2, $25) for end-to-end validation

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Revert "ci(temp): run eval-sweep on push to this branch (K=2, $25) for end-to-end validation"

This reverts commit 9619734307.

* test: record the test-pr-profile.ts change as non-behavioral for HARNESS_VERSION

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(changelog): the name-mode preflight fix

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(ship-measure): a partly funded sweep row shows the trials that ran

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(sweep): the fake single-case command is a Bun script, so it runs on Windows

The Windows free lane runs test/ship-measure-sweep.test.ts; CreateProcess
cannot exec a shebang shell stub, so every fake trial errored and the cap
stopped the sweep after one trial.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(plan-mode-no-op): the CEO sub-test reviews a fixed pasted plan

Without a plan, plan-ceo-review reviewed the checkout's branch diff, so its
pre-question work (audit and landscape searches) grew with whatever PR the
gate ran on. On this PR it timed out at 300s in 3 of 10 trials while still
researching the diff (ship-measure baseline 7/10). With a fixed small plan,
the same way the devex sub-test already runs, it measured 10/10 (CEO
sub-test 83-255s). No budget or threshold changed.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(ship-measure): each diagnostic trial gets its own EVALS_RUN_ID

Evidence-retaining cases (functional QA, docs faults) refuse to start
without EVALS_RUN_ID, which only CI sets, so measuring them locally ran
0/10 sessions. qa-functional-cli-report, red once in this PR's gate,
measured 10/10 once the trials started ($11.13).

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
Garry Tanandcapy-ai[bot] authored and GitHub committed 2026-10-07 14:00:06 +00:00
1 parent 28f1385eac
commit db745675bd
28 files changed
+1950 -300

No files matched your search

+1
View File
@@ -2,3 +2,4 @@ self-hosted-runner:
labels:
- ubicloud-standard-2
- ubicloud-standard-8
- ubicloud-standard-16
+204
View File
@@ -0,0 +1,204 @@
name: Eval sweep
# The weekly off-ship qualification sweep (docs/TESTING_INTERNALS.md#ship-measure-sweep):
# `ship-measure sweep` ranks the gate cases that cost the gate's all-green
# probability most (eval:pass-rates history on main) and measures the top K
# alone at their kind's N on this ref's head, with /ship's runner and
# measurement bar. Every trial is diagnostic: no verdict, no pass-rate
# history. The weekly spend cap (ship_measure_sweep_budget_usd, $150) is shared
# with every sweep report of the last 7 days, downloaded before the sweep runs.
# This workflow only reads: it never pushes, commits or opens a pull request;
# an agent fixes from the uploaded report. Pinned by
# test/eval-sweep-workflow.test.ts.
on:
schedule:
- cron: '0 12 * * 1' # Monday 12:00 UTC, after the 06:00 periodic census
workflow_dispatch:
inputs:
k:
description: 'Cases to measure (default: ship_measure_sweep_cases, 5)'
type: string
default: ''
cap_usd:
description: 'Weekly spend cap in USD, shared with the last 7 days of sweeps (default: ship_measure_sweep_budget_usd, 150)'
type: string
default: ''
dry_run:
description: 'Plan and estimate only ($0)'
type: boolean
default: false
# One sweep at a time across every ref, never cancelled: two concurrent runs
# would both read the same week's spend and could exceed the cap together.
concurrency:
group: eval-sweep
cancel-in-progress: false
permissions:
contents: read
env:
IMAGE: ghcr.io/${{ github.repository }}/ci
jobs:
image:
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
packages: read
outputs:
image-ref: ${{ steps.digest.outputs.ref }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- id: meta
# The same content-hash tag the eval workflows build and push
# (test/ci-image-tag-binding.test.ts pins the identical input list).
run: echo "tag=${{ env.IMAGE }}:${{ hashFiles('.github/docker/Dockerfile.ci', 'bun.lock', 'patches/**') }}" >> "$GITHUB_OUTPUT"
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Run the published image by digest: the digest is part of every
# measurement's pooling identity (GSTACK_CI_IMAGE). The sweep never
# builds an image; evals.yml and evals-periodic.yml publish it.
- name: Resolve the published CI image digest
id: digest
env:
TAG: ${{ steps.meta.outputs.tag }}
run: |
set -euo pipefail
if ! digest=$(docker buildx imagetools inspect "$TAG" --format '{{json .Manifest.Digest}}' | tr -d '"'); then
echo "::error::CI image $TAG is not published; run evals-periodic.yml (or an evals.yml PR run) to build it, then rerun the sweep"
exit 1
fi
echo "ref=${IMAGE}@${digest}" >> "$GITHUB_OUTPUT"
sweep:
runs-on: ubicloud-standard-16
needs: image
timeout-minutes: 240
permissions:
contents: read
actions: read
packages: read
container:
image: ${{ needs.image.outputs.image-ref }}
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
options: --user runner
env:
EVALS_RUN_ID: ci-${{ github.run_id }}-${{ github.run_attempt }}-eval-sweep
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
# Full history: selection walks git at module load (evals-periodic.yml).
fetch-depth: 0
persist-credentials: false
- name: Fix bun temp
uses: ./.github/actions/fix-bun-temp
- name: Restore deps
uses: ./.github/actions/restore-deps
- run: bun run build
- name: Seed claude interactive config
uses: ./.github/actions/seed-claude-config
with:
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
- name: Register gstack skills for PTY tests
uses: ./.github/actions/register-gstack-skills
# Same as evals-periodic.yml: the image ships the pinned Codex CLI
# without a credential; Codex cases see it only through the shard runner.
- name: Authenticate the Codex CLI
shell: bash
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
set -euo pipefail
codex="${GSTACK_CI_CODEX_BIN_DIR:?}/codex"
export CODEX_HOME="$HOME/.gstack-ci-codex"
mkdir -p "$CODEX_HOME"
echo "GSTACK_CI_CODEX_HOME=$CODEX_HOME" >> "$GITHUB_ENV"
if [ -z "${OPENAI_API_KEY:-}" ]; then
echo "::warning::OPENAI_API_KEY is unavailable; Codex eval cases cannot authenticate and will fail."
exit 0
fi
printenv OPENAI_API_KEY | "$codex" login --with-api-key
"$codex" login status >/dev/null 2>&1
# The weekly cap counts every sweep of the last 7 days. A listing failure
# fails the job before any spend; a run without a report spent nothing.
- name: Download this week's earlier sweep reports
shell: bash
env:
GH_TOKEN: ${{ github.token }}
PRIOR: /tmp/prior-sweeps
run: |
set -euo pipefail
mkdir -p "$PRIOR"
since=$(date -u -d '7 days ago' +%Y-%m-%dT%H:%M:%SZ)
ids=$(gh run list --repo "$GITHUB_REPOSITORY" --workflow eval-sweep.yml --limit 100 --json databaseId,createdAt \
--jq ".[] | select(.createdAt >= \"$since\") | .databaseId")
for id in $ids; do
[ "$id" = "$GITHUB_RUN_ID" ] && continue
gh run download "$id" --repo "$GITHUB_REPOSITORY" --name ship-measure-sweep-report --dir "$PRIOR/$id" 2>/dev/null \
|| echo "run $id: no sweep report (nothing charged)"
done
- name: Run the sweep
shell: bash
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
PLAYWRIGHT_BROWSERS_PATH: /opt/playwright-browsers
GH_TOKEN: ${{ github.token }}
GSTACK_CI_IMAGE: ${{ needs.image.outputs.image-ref }}
SWEEP_OUT: /tmp/ship-measure-sweep
SWEEP_PRIOR: /tmp/prior-sweeps
SWEEP_K: ${{ inputs.k }}
SWEEP_CAP_USD: ${{ inputs.cap_usd }}
SWEEP_DRY_RUN: ${{ inputs.dry_run }}
run: |
set -euo pipefail
args=(--out "$SWEEP_OUT" --prior "$SWEEP_PRIOR" --jobs 4)
if [ -n "${SWEEP_K:-}" ]; then args+=(--k "$SWEEP_K"); fi
if [ -n "${SWEEP_CAP_USD:-}" ]; then args+=(--cap-usd "$SWEEP_CAP_USD"); fi
if [ "${SWEEP_DRY_RUN:-false}" = "true" ]; then args+=(--dry-run); fi
bun run scripts/ship-measure.ts sweep "${args[@]}"
- name: Upload the sweep report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: ship-measure-sweep-report
path: |
/tmp/ship-measure-sweep/sweep-*/sweep-report.json
/tmp/ship-measure-sweep/sweep-*/sweep-report.md
if-no-files-found: ignore
retention-days: 30
- name: Upload the sweep captures
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: ship-measure-sweep-captures
include-hidden-files: true
path: /tmp/ship-measure-sweep
if-no-files-found: ignore
retention-days: 14
- name: Note a sweep that wrote no report
if: failure()
run: echo "The sweep failed before writing its report; see the Run the sweep step log." >> "$GITHUB_STEP_SUMMARY"
+50
View File
@@ -1,5 +1,55 @@
# Changelog
## [1.91.33.0] - 2026-10-06
**A red eval case now gets one clear verdict from its measurement: meets, qualified, extend once, or fix.**
**A weekly sweep measures main's flakiest gate cases before any ship hits them.**
/ship already measured a red case alone before rerunning the gate, but every 8 out of 10 was "below target", even when both reds were a provider outage. Now the measurement follows one bar. 9/10 meets it. 8/10 meets it as qualified when every red is a proven provider error, judge noise at the threshold, or a model miss with a cited transcript line. 7/10, or an 8/10 that does not qualify, gets exactly one more batch on the identical tree and is decided once on all 20 trials. Anything else, and any timeout, hang, regression or known fixable cause, is a fix round. Separately, a new weekly workflow picks the gate cases that cost the gate's all-green chance the most and measures them on main, with a hard $150-a-week cap.
### The numbers that matter
The bar numbers are binomial for a case whose true per-trial pass rate is p (rule kind, 10 trials). The sweep numbers come from `bun run scripts/ship-measure.ts sweep --dry-run` against main's pass-rate history on 2026-10-06.
| Case with true pass rate p | Clears the bar in one batch | Clears it with one extension (reds unqualified) | Clears it when every red qualifies |
|---|---|---|---|
| 0.95 (healthy) | 91.4% | 95.9% | 99.8% |
| 0.90 | 73.6% | 80.4% | 97.2% |
| 0.80 (flaky) | 37.6% | 40.8% | 75.3% |
| Sweep, latest gate census | Value |
|---|---|
| Gate verdicts in the census | 106 |
| Predicted all-green probability from history | 30.7% |
| Cases flagged (top 5 by cost to that probability) | 87% to 91% per-trial pass rate each |
| Estimated cost to measure all 5 at 10 trials | about $76, under the $150 weekly cap |
A healthy 95% case used to be sent to a pointless fix round 8.6% of the time; with one extension that drops to 4.1%, while a flaky 80% case still fails the bar about 60% of the time.
### What this means for you
When /ship's measure loop prints needs-classify, read the listed reds and record each one with `ship-measure classify`; it decides for you. A hand-picked "provider" label is refused unless the trial's own failure cause shows a provider or transport error. The weekly sweep report (artifact `ship-measure-sweep-report`, also the job summary) lists each flagged case with its decision, cost and captures, ready for an agent to fix.
### Itemized changes
#### Added
- `ship-measure classify`, `decide` and `extend`, and exit codes 5 (needs-classify) and 6 (EXTEND). EXTEND refuses to pool when any input changed (working tree, runner, Claude CLI, model, CI image, Bun, eval policy or harness version) and names the field; there is never a third batch.
- Void batches: more than 30% of trials failing on provider evidence voids a batch, which is redispatched once; both batches are reported.
- `ship-measure sweep` and `.github/workflows/eval-sweep.yml` (Mondays 12:00 UTC and on dispatch with `k`, `cap_usd` and `dry_run`). Config keys `ship_measure_sweep_cases` (5) and `ship_measure_sweep_budget_usd` (150, per 7 days, shared with every sweep that week). The workflow only reads; it never pushes or opens a pull request.
#### Changed
- Behavior cases are decided on the trial count over 12 (11 meets, 10 qualified, 9 extends; 22 and 20 of 24 pooled) instead of also requiring every panel to pass on its own; a contract violation still sends the case to a fix round at any count.
- The `ship-measure report` table shows each round's decision and batches, and lists qualified results under their own heading.
#### Fixed
- `ship-measure measure` no longer refuses a case that shares its test file with other cases (for example `ship-exploratory-late-input`): the no-cost selection check before the first paid trial now accepts the per-case trial shard such a case lists.
- `ship-measure measure` runs cases that keep native evidence (functional QA, docs faults) outside CI: each trial gets its own `EVALS_RUN_ID`, where before every trial stopped at "requires EVALS_RUN_ID" without starting a session.
- The `plan-mode-no-op` gate case no longer times out on large pull requests: its CEO review reads a fixed small plan instead of the branch diff.
#### For contributors
- The bar is one pure module, `scripts/lib/measure-bar.ts`, pinned by `test/ship-measure-bar.test.ts`; the sweep is `scripts/ship-measure-sweep.ts`, pinned by `test/ship-measure-sweep.test.ts` and `test/eval-sweep-workflow.test.ts`. `eval:pass-rates` history loading is now `loadPassRateHistory()` in `scripts/eval-flake-rank.ts`, shared with the sweep.
- Diagnostic trials still never become verdicts and never enter pass-rate history or EVAL_POLICY pooling. See `docs/TESTING_INTERNALS.md#ship-measure` and `#ship-measure-sweep`.
## [1.91.32.0] - 2026-10-06
**Codex second opinions work on macOS again, and text from a PR or reviewer can no longer run as a shell command.**
+2
View File
@@ -201,6 +201,8 @@ the new defaults.
| Run the opt-in ML, gitleaks and Swift checks | `gh workflow run platform-qualification.yml --ref <branch>` | Free CI runners | Pushed branch, `gh` with workflow rights |
| Look at past local eval runs | `bun run eval:list` / `eval:compare` / `eval:summary` | Free | Local eval history |
| See a case's pass rate across recent weekly runs | `bun run eval:pass-rates --case <case-id>` | Free | `gh` with repo read access |
| Measure one red case alone against the ship bar (MEETS 9/10) | `bun run scripts/ship-measure.ts measure --case <case-id> --round baseline` ([bar](docs/TESTING_INTERNALS.md#ship-measure)) | API spend for 10 trials (12 for behavior); asks above $2/trial, stops at $25/case | Same as paid coverage |
| Measure main's flakiest gate cases (the weekly sweep) | `gh workflow run eval-sweep.yml --ref main [-f k=5] [-f cap_usd=150] [-f dry_run=true]`, or locally `bun run scripts/ship-measure.ts sweep --dry-run` ([sweep](docs/TESTING_INTERNALS.md#ship-measure-sweep)) | API spend up to $150 per 7 days (`ship_measure_sweep_budget_usd`); `--dry-run` is free | `gh` with repo read access; CI dispatch needs workflow rights |
| See the audit success metrics and weekly health | `bun run test:health [--since-days 7] [--json] [--enforce]` | Free; 5–20 minutes of `gh` reads | `gh` with repo read access |
Old command names are listed under [Retired commands](#retired-commands).
+1 -1
View File
@@ -1 +1 @@
1.91.32.0
1.91.33.0
+1 -1
View File
@@ -1,4 +1,4 @@
# gstack digest v1.91.32.0 — regenerate/re-copy after upgrading gstack
# gstack digest v1.91.33.0 — regenerate/re-copy after upgrading gstack
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
for agent hosts without a full skill install. The full skills add workflows,
+16 -10
View File
@@ -188,15 +188,19 @@ CONFIG_HEADER='# gstack configuration — edit freely, changes take effect on ne
# ─── /ship measure-then-fix loop ─────────────────────────────────────
# A red eval case is measured alone before the full gate reruns
# (scripts/ship-measure.ts; docs/TESTING_INTERNALS.md#ship-measure).
# ship_measure_rule_trials: 10 # rule cases: trials; pass at 90% (9 of 10)
# ship_measure_behavior_panels: 4 # behavior cases: panels of 3; every panel
# # at 2 of 3 and 90% of trials (11 of 12)
# ship_measure_judge_outputs: 10 # judge cases: outputs, each scored by a
# # 3-sample panel; pass at 90% of outputs
# Bar (scripts/lib/measure-bar.ts): MEETS at 9/10 of the trials, MEETS-qualified
# at 8/10 with every red qualifying, EXTEND at 7/10 (one more batch, pooled).
# ship_measure_rule_trials: 10 # rule cases: trials per batch (MEETS 9 of 10)
# ship_measure_behavior_panels: 4 # behavior cases: panels of 3 per batch
# # (12 trials; MEETS 11 of 12)
# ship_measure_judge_outputs: 10 # judge cases: outputs per batch, each scored
# # by its median 3-sample panel (MEETS 9 of 10)
# ship_measure_ask_per_trial_usd: 2 # ask first above this estimated cost per trial
# ship_measure_budget_usd: 25 # estimated admission budget per red case
# # (baseline, repair rounds and judge scoring)
# # (baseline, extension, redispatch, repair rounds)
# ship_measure_max_rounds: 3 # repair rounds before a named-red stop
# ship_measure_sweep_cases: 5 # weekly off-ship sweep: cases measured (K)
# ship_measure_sweep_budget_usd: 150 # weekly off-ship sweep: spend cap per 7 days
# ship_rerun_backend: local # local | ubicloud — where free-suite flake reruns
# # run; ubicloud also needs UBICLOUD_API_KEY.
# # Invalid values are REJECTED (value kept).
@@ -271,6 +275,8 @@ lookup_default() {
ship_measure_ask_per_trial_usd) echo "2" ;;
ship_measure_budget_usd) echo "25" ;;
ship_measure_max_rounds) echo "3" ;;
ship_measure_sweep_cases) echo "5" ;;
ship_measure_sweep_budget_usd) echo "150" ;;
ship_rerun_backend) echo "local" ;; # local | ubicloud (Ubicloud spend is opt-in)
# Unknown key: exit non-zero instead of printing "". The fallback pattern
# the preambles use,
@@ -675,12 +681,12 @@ case "${1:-}" in
# The measure-loop controls bound paid spend: reject a typo instead of
# coercing it, so a bad value never changes how much /ship may spend.
case "$KEY" in
ship_measure_rule_trials|ship_measure_behavior_panels|ship_measure_judge_outputs|ship_measure_max_rounds)
ship_measure_rule_trials|ship_measure_behavior_panels|ship_measure_judge_outputs|ship_measure_max_rounds|ship_measure_sweep_cases)
if ! printf '%s' "$VALUE" | LC_ALL=C grep -qE '^[1-9][0-9]*$'; then
echo "Error: $KEY '$VALUE' is not a positive integer. Existing value left unchanged." >&2
exit 1
fi ;;
ship_measure_ask_per_trial_usd|ship_measure_budget_usd)
ship_measure_ask_per_trial_usd|ship_measure_budget_usd|ship_measure_sweep_budget_usd)
if ! printf '%s' "$VALUE" | LC_ALL=C grep -qE '^([0-9]+(\.[0-9]+)?|\.[0-9]+)$' || ! printf '%s' "$VALUE" | LC_ALL=C grep -qE '[1-9]'; then
echo "Error: $KEY '$VALUE' is not a positive amount in USD (e.g. 25 or 2.50). Existing value left unchanged." >&2
exit 1
@@ -876,7 +882,7 @@ case "${1:-}" in
timeline_stop_hook design_detector design_detector_install_prompted memorable_recall \
browse_extension_id ship_measure_rule_trials ship_measure_behavior_panels \
ship_measure_judge_outputs ship_measure_ask_per_trial_usd ship_measure_budget_usd \
ship_measure_max_rounds ship_rerun_backend; do
ship_measure_max_rounds ship_measure_sweep_cases ship_measure_sweep_budget_usd ship_rerun_backend; do
gstack_config_select "$KEY"
VALUE="$_gstack_cfg_value"
SOURCE="default"
@@ -899,7 +905,7 @@ case "${1:-}" in
timeline_stop_hook design_detector design_detector_install_prompted memorable_recall \
browse_extension_id ship_measure_rule_trials ship_measure_behavior_panels \
ship_measure_judge_outputs ship_measure_ask_per_trial_usd ship_measure_budget_usd \
ship_measure_max_rounds ship_rerun_backend; do
ship_measure_max_rounds ship_measure_sweep_cases ship_measure_sweep_budget_usd ship_rerun_backend; do
printf ' %-24s %s\n' "$KEY:" "$(lookup_default "$KEY")"
done
;;
+89 -11
View File
@@ -479,25 +479,62 @@ documented single-case command (`--command '<command> {case}'`, split on
whitespace, no shell).
Counts per kind come from gstack config (`bin/gstack-config`), so a measurement
is pre-registered before it runs:
is pre-registered before it runs. Each measurement is decided by the
measurement bar (`scripts/lib/measure-bar.ts`, approved 2026-10-06), applied in
this order:
| Kind | Trials (config key, default) | Meets target when |
|---|---|---|
| `rule` | `ship_measure_rule_trials`, 10 | at least 90% of trials pass (9 of 10) |
| `behavior` | `ship_measure_behavior_panels`, 4 panels of 3 (12 trials) | every panel passes at its own 2 of 3 with no contract violation, and at least 90% of trials pass (11 of 12) |
| `judge` | `ship_measure_judge_outputs`, 10 outputs | at least 90% of outputs pass (9 of 10); an output passes when 2 of its 3 judge samples pass |
| Kind | Trials per batch (config key, default) | MEETS | MEETS-qualified | EXTEND, then the pooled decision |
|---|---|---|---|---|
| `rule` | `ship_measure_rule_trials`, 10 | at least 9 of 10 | 8 of 10, every red qualifying | 7 of 10, or 8 that does not qualify: 10 more trials, then 18 of 20 strict or 16 of 20 qualified |
| `behavior` | `ship_measure_behavior_panels`, 4 panels of 3 (12 trials) | at least 11 of 12 | 10 of 12, every red qualifying | 9 of 12, or 10 that does not qualify: 12 more trials, then 22 of 24 strict or 20 of 24 qualified |
| `judge` | `ship_measure_judge_outputs`, 10 outputs | at least 9 of 10 outputs | 8 of 10, every red qualifying | 7 of 10, or 8 that does not qualify: 10 more outputs, then 18 of 20 or 16 of 20 qualified |
A judge output passes when the median of its 3 samples passes (EVAL_POLICY v3,
so at least 2 of 3). Everything below the EXTEND row is **BELOW**: a fix round.
- **Qualifying reds** (the only reds MEETS-qualified accepts): `provider`, which
is mechanical: the trial's own `failure_cause` is `api_error`, or
`pre_turn_infra` whose evidence names a 429 or 5xx, a rate limit, an overload
or a connection reset (`ship-measure classify` refuses `provider` otherwise);
`judge-noise`, a judge sample at the threshold; and `model-miss`, which counts
only with a `path:line` citation of the deviation. Never qualifying: a timeout
or hang (`session_timeout`, `observer_timeout` and `provider_stall` are
classified so without review and cannot be reclassified into a qualifying
class), a regression, a contract violation (BELOW at any count), or any red
with a known fixable cause (`fixable`): each goes to a fix round.
- **needs-classify.** A batch below the strict bar but at or above the EXTEND
floor, with a failed trial nobody classified, returns needs-classify (exit 5)
with the trials to classify. `ship-measure classify --trials 2,5 --class <c>
--evidence <text>` appends one immutable record per trial to the round's
`classifications.jsonl` (a later record supersedes an earlier one) and
re-decides. `unclassified` records "reviewed, no route"; it allows EXTEND but
never qualifies.
- **EXTEND** (exit 6). `ship-measure extend` runs N more trials only when the
round decided EXTEND and only on identical inputs: the measurement's identity
(the working tree's frozen git tree, kind, trials, runner, Claude CLI, model,
CI image `GSTACK_CI_IMAGE`, Bun, EVAL_POLICY and HARNESS_VERSION) must match
field for field, and the refusal names the field that changed. The pooled 2N
is decided once and is never extended again: there is no third batch.
- **Void batches.** A batch where more than 30% of trials fail with affirmative
provider or transport evidence is void (an outage, not a measurement); the
runner redispatches it once, both batches are reported, and a second void
batch stops with a named red. A crash or module-load failure without provider
evidence never voids a batch.
`ship_measure_budget_usd` (default 25) is an estimated admission budget per red
case across the baseline, every repair round and judge scoring: before each
case across the baseline, an extension or redispatch, every repair round and
judge scoring: before each
batch the runner reserves the estimated cost of every concurrent trial and
admits only what fits beside what was already spent, then reconciles actual
costs after the batch. When the next trial no longer fits it stops with a named
red. A case with no per-trial estimate, or one above
`ship_measure_ask_per_trial_usd` (default 2), asks once before any trial runs;
with no estimate it then runs one calibration trial alone. `ship_measure_max_rounds`
(default 3) limits repair rounds. `ship-measure report` prints the PR-body table:
each measurement as "observed k/n" (after the named fix for a repair round),
target, estimated and actual spend. A red the user declares infrastructure is
(default 3) limits repair rounds; an extension or a redispatch is not a round.
`ship-measure report` prints the PR-body table: each measurement as "observed
k/n" (after the named fix for a repair round, with every batch), the bar, the
decision, estimated and actual spend, and qualified MEETS under their own
heading. A red the user declares infrastructure is
recorded with `ship-measure skip` and shows as `unmeasured`, never as a pass. A
case is called fixed only with a named causal change and every failure in the
closing measurement explained; the weekly census, not the loop, reports
@@ -512,7 +549,9 @@ free random ports, as in the normal suite. `ship_rerun_backend=ubicloud` runs th
reruns on one Ubicloud VM through `bun run test:ubicloud --diagnostic`, which
also sets the flaky retry off. The seeded paid case `ship-measure-seeded-flake`
(periodic, `behavior`) drives /ship through this loop against a stub eval
command that fails trial slots 2, 5 and 9 until its list is sorted.
command that fails trial slots 2, 5 and 9 until its list is sorted. The weekly
[off-ship sweep](#ship-measure-sweep) measures main's flakiest gate cases with
the same runner and bar.
<a id="failure-causes"></a>
**Failure causes and details** (diagnostic only; verdicts unchanged under v1).
@@ -770,6 +809,45 @@ against a temp `GSTACK_INSTALL_DIR` / `GSTACK_SKILLS_DIR`, and
`freeze/bin/check-freeze.sh` with JSON payloads on stdin (including the
`GSTACK_HOME` state-root parity against `bin/gstack-paths`).
### Ship measure sweep
**Weekly off-ship qualification sweep** (`ship-measure sweep`,
`scripts/ship-measure-sweep.ts`, `.github/workflows/eval-sweep.yml`; approved
2026-10-06). With about 130 gate verdicts, an all-green gate is unlikely even
when every case is healthy (26.8% at 99% per verdict), so flaky cases are
measured on main before any ship hits them. The sweep reads `eval:pass-rates`
history (`evals-periodic.yml` on main plus pooled branch census runs), takes the
latest gate census, and ranks every live, non-quarantined gate case with a
failed trial or red verdict by its cost to the all-green probability
Π(1 − p_i) (the shrunk red-verdict rate from `--reds`; ties go to the lower
per-trial pass rate). It measures the top K (`ship_measure_sweep_cases`,
default 5; `--k`) at their kind's N on the checked-out head with /ship's runner
and bar, in rank order. The report (`sweep-report.md` and `.json` under
`.context/ship-measure/sweep-<time>/`, rewritten after every case) has one row
per case (history rate, measured count, decision, estimated and actual cost,
captures path) and the predicted gate all-green probability before (history)
and after (each measured case's measured failure fraction, shrunk toward the
pooled rate and converted to panel red risk for behavior cases). Spend is
capped by `ship_measure_sweep_budget_usd` (default $150 per 7 days, `--cap-usd`)
through the same admission budget: the cap is shared by the sweep's cases and
every non-dry-run sweep report of the last 7 days under `--prior` and the local
sweep root, so a dispatch after the weekly run spends only what is left. When
the cap is exhausted the sweep stops and lists every skipped case. `--dry-run`
plans and estimates at $0; `--history-dir` reads local `trial-outcomes`
directories; `--command '<cmd> {case}'` swaps in another single-case runner.
History that cannot be read fails closed with nothing measured.
The workflow runs Mondays at 12:00 UTC (after the 06:00 periodic census) and on
`workflow_dispatch` (inputs `k`, `cap_usd`, `dry_run`), in the CI image with the
paid-eval secrets setup the census uses. It downloads the last 7 days of sweep
reports first, uploads the report and captures as the `ship-measure-sweep`
artifact, and writes the job summary. It holds `contents: read`, never pushes
or opens a pull request, and runs one sweep at a time repository-wide
(concurrency group `eval-sweep`, not cancelled) so two runs never spend the
same weekly cap. Fixing is done by an agent from the report, through /ship's
measure loop. Sweep trials are diagnostic like every ship-measure trial: they
never become verdicts and never enter EVAL_POLICY pooling or series history.
### Pass-rate policy versions
Readers score only trials recorded under their own `EVAL_POLICY.version`.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "gstack",
"version": "1.91.32",
"version": "1.91.33",
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
"license": "MIT",
"type": "module",
+72 -49
View File
@@ -565,6 +565,75 @@ export function formatPassRates(report: PassRateReport, options: { caseFilter?:
return lines.join('\n');
}
export interface PassRateHistory {
records: TrialRecord[];
unattributed: string[];
errors: string[];
manualReviews: string[];
weeklyRuns?: string[];
fetched: WeeklyRun[];
pooledRunIds?: Set<string>;
historyError: string | null;
scope: string;
cacheDir: string;
}
/**
* The trial history every eval:pass-rates view reads (and the ship-measure
* sweep ranks from): local dirs, or the trial-outcomes artifacts of the last
* N weekly runs on the branch plus the branch census runs that pool into
* main's series. A fetch failure is returned as historyError, never thrown.
*/
export function loadPassRateHistory(o: { repo: string; workflow: string; branch: string; runsLimit: number; dirs: string[]; sinceDays: number; backfill?: boolean; runId?: number }): PassRateHistory {
const records: TrialRecord[] = [];
const unattributed = new Set<string>();
const errors: string[] = [];
const manualReviews: string[] = [];
let historyError: string | null = null;
let weeklyRuns: string[] | undefined;
let fetched: WeeklyRun[] = [];
let pooledRunIds: Set<string> | undefined;
const cacheDir = path.join(path.resolve(resolveStateRoot()), 'eval-pass-rates-cache', o.repo.replace('/', '-'));
const importDir = (dir: string, run: { run_id: string; sha?: string; timestamp?: string } | undefined, legacyDays: number) => {
const trials = readTrialOutcomeDir(dir);
records.push(...trials.records);
errors.push(...trials.errors);
const legacy = backfillEvalFiles(collectEvalFiles(dir, legacyDays), run);
records.push(...legacy.records);
manualReviews.push(...legacy.manualReviews);
legacy.unattributed.forEach(name => unattributed.add(name));
};
if (o.dirs.length) {
for (const dir of o.dirs) importDir(dir, undefined, o.sinceDays);
} else {
try {
const weekly = listWeeklyRuns({ repo: o.repo, workflow: o.workflow, branches: [o.branch], limit: o.runsLimit }).filter(run => o.branch !== 'main' || isWeeklyHistoryRun(run));
weeklyRuns = weekly.map(run => run.createdAt);
// Branch census trials pool into main's matching series over the same window (isPooledTrialRun).
const oldest = weekly[weekly.length - 1]?.createdAt;
const pooledRuns = o.branch === 'main' && oldest
? listWeeklyRuns({ repo: o.repo, workflow: o.workflow, branches: [''], limit: 100 }).filter(run => isPooledTrialRun(run) && run.createdAt >= oldest)
: [];
pooledRunIds = new Set(pooledRuns.map(run => `${run.id}`));
const runs = [...weekly, ...pooledRuns];
if (o.runId !== undefined && !runs.some(run => run.id === o.runId)) runs.push(GH_JOBS.getRun(o.repo, o.runId));
fetched = runs;
const match = o.backfill
? (name: string) => name.startsWith('trial-outcomes') || /^(paid-slice-\d+|gate-census-\d+)(-a\d+)?$/.test(name)
: (name: string) => name.startsWith('trial-outcomes');
for (const run of runs) {
const dirsForRun = downloadRunArtifacts({ repo: o.repo, run, match, cacheDir, maxBytes: o.backfill ? 64 * 1024 * 1024 : undefined });
for (const dir of dirsForRun) importDir(dir, { run_id: `${run.id}`, sha: run.sha, timestamp: run.createdAt }, 3650);
}
} catch (error) {
historyError = error instanceof Error ? error.message : String(error);
}
}
const scope = o.dirs.length ? `local dirs ${o.dirs.join(', ')}` : `${o.repo} ${o.workflow} on ${o.branch}${pooledRunIds?.size ? ` + ${pooledRunIds.size} pooled branch census run(s)` : ''}, `
+ `last ${o.runsLimit} completed run(s): ${fetched.map(run => run.id).join(', ') || 'none'}`;
return { records, unattributed: [...unattributed].sort(), errors, manualReviews, weeklyRuns, fetched, pooledRunIds, historyError, scope, cacheDir };
}
// --- GitHub history (shared reader: scripts/lib/ci-history.ts) ---
export { downloadRunArtifacts, GH_HISTORY, listWeeklyRuns, TRIAL_OUTCOMES_MAX_BYTES,
@@ -580,55 +649,9 @@ if (import.meta.main) {
const workflow = parsed.workflow ?? 'evals-periodic.yml';
const branch = parsed.branch ?? 'main';
const records: TrialRecord[] = [];
const unattributed = new Set<string>();
const errors: string[] = [];
let historyError: string | null = null;
let weeklyRuns: string[] | undefined;
let fetched: WeeklyRun[] = [];
const cacheDir = path.join(path.resolve(resolveStateRoot()), 'eval-pass-rates-cache', repo.replace('/', '-'));
let pooledRunIds: Set<string> | undefined;
const manualReviews: string[] = [];
const importDir = (dir: string, run: { run_id: string; sha?: string; timestamp?: string } | undefined, legacyDays: number) => {
const trials = readTrialOutcomeDir(dir);
records.push(...trials.records);
errors.push(...trials.errors);
const legacy = backfillEvalFiles(collectEvalFiles(dir, legacyDays), run);
records.push(...legacy.records);
manualReviews.push(...legacy.manualReviews);
legacy.unattributed.forEach(name => unattributed.add(name));
};
if (dirs.length) {
for (const dir of dirs) importDir(dir, undefined, sinceDays);
} else {
try {
const weekly = listWeeklyRuns({ repo, workflow, branches: [branch], limit: runsLimit }).filter(run => branch !== 'main' || isWeeklyHistoryRun(run));
weeklyRuns = weekly.map(run => run.createdAt);
// Branch census trials pool into main's matching series over the same window (isPooledTrialRun).
const oldest = weekly[weekly.length - 1]?.createdAt;
const pooledRuns = branch === 'main' && oldest
? listWeeklyRuns({ repo, workflow, branches: [''], limit: 100 }).filter(run => isPooledTrialRun(run) && run.createdAt >= oldest)
: [];
pooledRunIds = new Set(pooledRuns.map(run => `${run.id}`));
const runs = [...weekly, ...pooledRuns];
if (parsed.runId !== undefined && !runs.some(run => run.id === parsed.runId)) runs.push(GH_JOBS.getRun(repo, parsed.runId));
fetched = runs;
const match = backfill
? (name: string) => name.startsWith('trial-outcomes') || /^(paid-slice-\d+|gate-census-\d+)(-a\d+)?$/.test(name)
: (name: string) => name.startsWith('trial-outcomes');
for (const run of runs) {
const dirsForRun = downloadRunArtifacts({ repo, run, match, cacheDir, maxBytes: backfill ? 64 * 1024 * 1024 : undefined });
for (const dir of dirsForRun) importDir(dir, { run_id: `${run.id}`, sha: run.sha, timestamp: run.createdAt }, 3650);
}
} catch (error) {
historyError = error instanceof Error ? error.message : String(error);
}
}
const scope = dirs.length ? `local dirs ${dirs.join(', ')}` : `${repo} ${workflow} on ${branch}${pooledRunIds?.size ? ` + ${pooledRunIds.size} pooled branch census run(s)` : ''}, `
+ `last ${runsLimit} completed run(s): ${fetched.map(run => run.id).join(', ') || 'none'}`;
const history = loadPassRateHistory({ repo, workflow, branch, runsLimit, dirs, sinceDays, backfill, runId: parsed.runId });
const { records, errors, manualReviews, weeklyRuns, fetched, pooledRunIds, historyError, scope, cacheDir } = history;
const unattributed = new Set(history.unattributed);
const scoped = caseFilter ? records.filter(record => record.case === caseFilter) : records;
const caseHeadroom = headroom(scoped);
if (view !== 'rates') {
+1 -1
View File
@@ -16,7 +16,7 @@
"scripts/lib/published-text.ts": {"blob":"3f2e4d616b5dd0204035710c66b64d49055c5714","since":2},
"scripts/lib/shard-engine.ts": {"blob":"3cd0b1aaa84018b00bf9ea0ce48093f3c8492f66","since":2},
"scripts/test-paid-shards.ts": {"blob":"8ddee5f11ed7856876135520dd8025ab989e2db9","since":2},
"scripts/test-pr-profile.ts": {"blob":"d7860897397de8fd605623570d1e7ebbbd5ec743","since":2},
"scripts/test-pr-profile.ts": {"blob":"667ade7a76044bc20f7c183d0d4f23900c0423b3","since":2,"nonBehavioral":"FREE_ONLY_PR_FILES gains eval-sweep.yml, the sweep CLI and .github/actionlint.yaml: it changes which PR-gate cases a diff selects, never how a selected case runs or is judged"},
"scripts/test-strict-output.ts": {"blob":"01185d20dc9761b04b9b2f751ec98664fe0eaf93","since":2},
"test/helpers/agent-sdk-runner.ts": {"blob":"210bba92d241a90df578b2c7fc943ac5755d0025","since":2},
"test/helpers/auq-substance-panel.ts": {"blob":"4881260ef793668e4963a062db6d06a5e1f7a40f","since":2},
+205
View File
@@ -0,0 +1,205 @@
/**
* The ship-measure measurement bar (approved by Garry 2026-10-06): one pure
* decision over a case's diagnostic trials, shared by /ship's measure loop
* (scripts/ship-measure.ts) and the weekly off-ship sweep
* (scripts/ship-measure-sweep.ts). Nothing here runs a trial or reads a file.
*
* For N trials per batch (rule and judge N = 10; behavior N = 12, four panels
* of 3; a judge trial is one output scored by its median 3-sample panel), the
* counts round up from 9/10, 8/10 and 7/10 of N:
*
* MEETS passes >= 9/10 of N.
* MEETS-qualified passes >= 8/10 of N and every failed trial is in a
* qualifying class: provider (affirmative provider or
* transport evidence only), judge-noise, or model-miss
* citing its evidence (path:line).
* EXTEND passes >= 7/10 of N without meeting either bar, and no red
* has a known fixable cause: run N more trials on identical
* inputs, then decide once on the pooled 2N (>= 9/10 strict,
* >= 8/10 qualified). There is never a third batch.
* BELOW everything else, and any red in a fix-round class
* (timeout, hang, regression, fixable) or a contract
* violation: a fix round.
*
* Before the counts: a batch where more than 30% of trials fail with
* affirmative provider evidence is void (an outage, not a measurement) and may
* be redispatched once; both batches are reported. A batch below strict whose
* failed trials are not all classified returns needs-classify when the
* classification decides the outcome (passes at or above the 7/10 floor).
*/
export type BarKind = 'rule' | 'behavior' | 'judge';
export type TrialSet = 'initial' | 'redispatch' | 'extend' | 'extend-redispatch';
export const QUALIFYING_CLASSES = ['provider', 'judge-noise', 'model-miss'] as const;
export const FIX_ROUND_CLASSES = ['timeout', 'hang', 'regression', 'fixable'] as const;
export const FAILURE_CLASSES = [...QUALIFYING_CLASSES, ...FIX_ROUND_CLASSES, 'unclassified'] as const;
export type FailureClass = (typeof FAILURE_CLASSES)[number];
/** A batch is void when more than this share of its trials fail on affirmative provider evidence. */
export const VOID_SHARE = { num: 3, den: 10 } as const;
export interface BarTrial {
trial: number;
set: TrialSet;
passed: boolean;
contract?: boolean;
/** Judge outputs: each sample's pass; the output passes at 2 of 3. */
samples?: boolean[];
failureCause?: string;
failureDetail?: string;
failureEvidence?: string;
}
export interface Classification { trial: number; class: FailureClass; evidence: string; by: 'machine' | 'agent'; at: string }
export type BarDecision = 'MEETS' | 'MEETS-qualified' | 'EXTEND' | 'BELOW' | 'needs-classify' | 'void' | 'incomplete';
export type BarNext = 'none' | 'redispatch' | 'extend' | 'classify' | 'fix' | 'stop';
export interface BarThresholds { trials: number; strict: number; qualified: number; floor: number }
export interface SetSummary { set: TrialSet; trials: number; passes: number; providerFailures: number; void: boolean }
export interface BarOutcome {
decision: BarDecision;
next: BarNext;
passes: number;
thresholds: BarThresholds;
pooled: boolean;
sets: SetSummary[];
/** Failed counted trials that still need a class. */
needsClassify: number[];
reason: string;
}
const atLeast = (n: number, num: number) => Math.ceil((n * num) / 10);
/** Strict 9/10, qualified 8/10 and extend floor 7/10 of n, rounded up (10: 9/8/7; 12: 11/10/9; 20: 18/16/14; 24: 22/20/17). */
export function barThresholds(n: number): BarThresholds {
return { trials: n, strict: atLeast(n, 9), qualified: atLeast(n, 8), floor: atLeast(n, 7) };
}
/** A trial's pass under the bar: a judge output at 2 of its 3 samples; a contract violation never passes. */
export function trialPassed(kind: BarKind, t: BarTrial): boolean {
if (t.contract) return false;
if (kind === 'judge' && t.samples) return t.samples.length === 3 && t.samples.filter(Boolean).length >= 2;
return t.passed;
}
const PROVIDER_TEXT = /\b(?:429|5\d\d)\b|rate[ _-]?limit|overloaded|api_error|ECONNRESET|connection reset|socket hang up|ETIMEDOUT|EAI_AGAIN|service unavailable|bad gateway/i;
const TIMEOUT_CAUSES = new Set(['session_timeout', 'observer_timeout', 'provider_stall']);
/**
* The affirmative provider or transport evidence for a failed trial, or null.
* `api_error` is provider evidence by itself; `pre_turn_infra` only when its
* evidence or detail names a 429/5xx, rate limit, overload or connection reset
* (a crash or module-load failure is a harness defect, never an outage).
*/
export function providerEvidence(t: BarTrial): string | null {
if (t.passed && !t.contract) return null;
const text = [t.failureEvidence, t.failureDetail].filter(Boolean).join(' ');
if (t.failureCause === 'api_error') return text || 'failure_cause api_error';
if (t.failureCause === 'pre_turn_infra' && PROVIDER_TEXT.test(text)) return text;
return null;
}
/** The class a failed trial gets without review: provider on affirmative evidence; timeout or hang from its cause. */
export function machineClass(t: BarTrial): FailureClass | null {
if (providerEvidence(t)) return 'provider';
if (t.failureCause === 'provider_stall') return 'hang';
if (t.failureCause && TIMEOUT_CAUSES.has(t.failureCause)) return 'timeout';
return null;
}
const CITATION = /[\w./-]+:\d+/;
/** Why an agent classification is refused, or null when it is accepted. */
export function classificationProblem(t: BarTrial | undefined, kind: BarKind, cls: string, evidence: string): string | null {
if (!t) return 'no such trial in this measurement';
if (trialPassed(kind, t)) return `trial ${t.trial} passed; only failed trials are classified`;
if (!(FAILURE_CLASSES as readonly string[]).includes(cls)) return `class must be one of ${FAILURE_CLASSES.join(', ')}. Received: ${cls}`;
if (!evidence.trim()) return 'every classification needs --evidence (what you read: a capture path:line, a failure line, a transcript line)';
if (cls === 'provider' && !providerEvidence(t)) {
return `provider needs affirmative provider or transport evidence (failure_cause api_error, or pre_turn_infra naming a 429/5xx, rate limit or connection reset); trial ${t.trial}'s failure_cause is ${t.failureCause ?? 'unrecorded'}`;
}
if ((QUALIFYING_CLASSES as readonly string[]).includes(cls) && t.failureCause && TIMEOUT_CAUSES.has(t.failureCause)) {
return `trial ${t.trial} failed on ${t.failureCause}: a timeout or hang never qualifies; it goes to a fix round`;
}
return null;
}
/** Each failed trial's effective class: its latest agent record, else its machine class. */
export function effectiveClasses(trials: readonly BarTrial[], records: readonly Classification[]): Map<number, Classification> {
const out = new Map<number, Classification>();
for (const t of trials) {
const cls = machineClass(t);
if (cls) out.set(t.trial, { trial: t.trial, class: cls, evidence: providerEvidence(t) ?? t.failureCause ?? '', by: 'machine', at: '' });
}
for (const r of records) if (r.by === 'agent') out.set(r.trial, r);
return out;
}
const qualifies = (c: Classification) => (QUALIFYING_CLASSES as readonly string[]).includes(c.class) && (c.class !== 'model-miss' || CITATION.test(c.evidence));
const fixRound = (c: Classification) => (FIX_ROUND_CLASSES as readonly string[]).includes(c.class);
function summarize(kind: BarKind, set: TrialSet, trials: BarTrial[]): SetSummary {
const providerFailures = trials.filter(t => !trialPassed(kind, t) && providerEvidence(t)).length;
return { set, trials: trials.length, passes: trials.filter(t => trialPassed(kind, t)).length, providerFailures,
void: trials.length > 0 && providerFailures * VOID_SHARE.den > trials.length * VOID_SHARE.num };
}
/** The counted batch of a pair (first try, its one redispatch), or a void/incomplete stop. */
function countedBatch(n: number, first: SetSummary, retry: SetSummary, firstTrials: BarTrial[], retryTrials: BarTrial[], label: string):
{ trials: BarTrial[] } | { stop: Pick<BarOutcome, 'decision' | 'next' | 'reason'> } {
const use = first.void ? retry : first;
if (use.trials === 0) return { stop: { decision: 'void', next: 'redispatch', reason: `${label} batch is void: ${first.providerFailures} of ${first.trials} trials failed on provider evidence (> 30%); redispatch it once` } };
if (use.void) return { stop: { decision: 'void', next: 'stop', reason: `${label} batch and its one redispatch are both void (provider evidence in ${first.providerFailures}/${first.trials} and ${retry.providerFailures}/${retry.trials}); stop with a named infrastructure red` } };
if (use.trials < n) return { stop: { decision: 'incomplete', next: 'stop', reason: `${label} batch ran ${use.trials} of ${n} trials` } };
return { trials: first.void ? retryTrials : firstTrials };
}
/**
* The bar's decision over a measurement's trials and its classification
* records (append-only; the latest agent record for a trial wins).
*/
export function decide(kind: BarKind, n: number, trials: readonly BarTrial[], records: readonly Classification[] = []): BarOutcome {
const of = (set: TrialSet) => trials.filter(t => t.set === set);
const sets = (['initial', 'redispatch', 'extend', 'extend-redispatch'] as const).map(set => summarize(kind, set, of(set)));
const [initial, redispatch, extend, extendRedispatch] = sets as [SetSummary, SetSummary, SetSummary, SetSummary];
const reported = sets.filter(s => s.trials > 0);
const base = countedBatch(n, initial, redispatch, of('initial'), of('redispatch'), 'the first');
const empty = { passes: 0, pooled: false, sets: reported, needsClassify: [] as number[] };
if ('stop' in base) return { ...empty, thresholds: barThresholds(n), ...base.stop };
const pooled = extend.trials > 0;
const ext = pooled ? countedBatch(n, extend, extendRedispatch, of('extend'), of('extend-redispatch'), 'the extension') : { trials: [] };
if ('stop' in ext) return { ...empty, pooled, thresholds: barThresholds(2 * n), ...ext.stop };
const counted = [...base.trials, ...ext.trials];
const thresholds = barThresholds(counted.length);
const passes = counted.filter(t => trialPassed(kind, t)).length;
const failed = counted.filter(t => !trialPassed(kind, t));
const classes = effectiveClasses(failed, records);
const result = (decision: BarDecision, next: BarNext, reason: string, needsClassify: number[] = []): BarOutcome =>
({ decision, next, passes, thresholds, pooled, sets: reported, needsClassify, reason });
const count = `${passes}/${counted.length}${pooled ? ' pooled' : ''}`;
const contract = failed.find(t => t.contract);
if (contract) return result('BELOW', 'fix', `${count}; trial ${contract.trial} violated a contract: a fix round, whatever the count`);
if (passes >= thresholds.strict) return result('MEETS', 'none', `${count} meets the strict bar (${thresholds.strict}/${counted.length})`);
if (passes < (pooled ? thresholds.qualified : thresholds.floor)) {
return result('BELOW', 'fix', `${count} is under the ${pooled ? `qualified pooled bar (${thresholds.qualified}/${counted.length})` : `extend floor (${thresholds.floor}/${counted.length})`}: a fix round`);
}
const unclassified = failed.filter(t => !classes.has(t.trial)).map(t => t.trial);
if (unclassified.length) return result('needs-classify', 'classify', `${count} is below strict; classify trial(s) ${unclassified.map(t => `t${t}`).join(', ')}, then decide`, unclassified);
const fix = failed.find(t => fixRound(classes.get(t.trial)!));
if (fix) return result('BELOW', 'fix', `${count}; trial ${fix.trial} is ${classes.get(fix.trial)!.class}, which never qualifies: a fix round`);
if (passes >= thresholds.qualified && failed.every(t => qualifies(classes.get(t.trial)!))) {
return result('MEETS-qualified', 'none', `${count} meets the qualified bar (${thresholds.qualified}/${counted.length}); every red qualifies (${failed.map(t => `t${t.trial} ${classes.get(t.trial)!.class}`).join(', ')})`);
}
if (pooled) return result('BELOW', 'fix', `${count} does not meet the pooled bar (${thresholds.strict} strict, or ${thresholds.qualified} with every red qualifying); there is no third batch: a fix round`);
return result('EXTEND', 'extend', `${count} is at or above the extend floor (${thresholds.floor}/${counted.length}) without meeting a bar: run ${n} more trials on identical inputs, then decide once on the pooled ${2 * n}`);
}
/** The first identity field that differs between two batches, or null when they may pool. */
export function identityMismatch(a: Record<string, string>, b: Record<string, string>): string | null {
for (const key of [...new Set([...Object.keys(a), ...Object.keys(b)])].sort()) {
if (a[key] !== b[key]) return `${key} changed (${a[key] ?? 'absent'} -> ${b[key] ?? 'absent'})`;
}
return null;
}
+352
View File
@@ -0,0 +1,352 @@
/**
* ship-measure sweep — the weekly off-ship qualification sweep on main
* (approved by Garry 2026-10-06; docs/TESTING_INTERNALS.md#ship-measure-sweep).
*
* With ~130 gate verdicts, an all-green gate is unlikely even when every case
* is healthy, so flaky cases are measured on main before any ship hits them.
* The sweep reads eval:pass-rates history (evals-periodic.yml on main plus the
* branch census runs that pool into it), ranks the gate cases with a failed
* trial by how much each costs the gate's all-green probability Π(1 − p_i)
* (p_i: its red-verdict rate shrunk toward the pooled rate, as
* `eval:pass-rates --reds` computes it; ties broken by the lower per-trial
* pass rate), and measures the top K at their kind's N on the
* checked-out head with the same runner and measurement bar /ship uses
* (scripts/ship-measure.ts, scripts/lib/measure-bar.ts).
*
* Spend: ship_measure_sweep_budget_usd (default $150) is a weekly cap enforced
* through the same admission budget as /ship's loop, shared across the
* sweep's cases and every sweep report of the last 7 days found under --prior
* (CI downloads the earlier runs' reports there) and the local sweep root.
* When the cap is exhausted the sweep stops and lists what it skipped.
*
* Every trial is diagnostic: it never becomes a verdict and never enters
* EVAL_POLICY pooling or series history. The sweep never pushes, commits or
* opens a pull request; an agent fixes from its report.
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import { spawnSync } from 'node:child_process';
import { CASE_QUARANTINE } from '../test/helpers/periodic-exclude-data';
import { E2E_KINDS, E2E_TIERS, LLM_JUDGE_TOUCHFILES } from '../test/helpers/touchfiles-data';
import { parseCliFlags } from './lib/shard-engine';
import { RED_RATE_SHRINK, verdictsOf } from './lib/eval-history';
import { trialPassed } from './lib/measure-bar';
import { repoSlug } from './lib/ci-history';
import { loadPassRateHistory, type TrialRecord as HistoryRecord } from './eval-flake-rank';
import {
caseSelectionPreflight, caseSlug, commandRunner, gstackRunner, kindPlan, measureCase, measurementIdentity, readMeasureConfig,
type MeasureConfig, type MeasureKind, type Measurement, type SpendPool, type TrialRunner,
} from './ship-measure';
const ROOT = path.resolve(import.meta.dir, '..');
const WEEK_MS = 7 * 24 * 3600_000;
export const SWEEP_SCHEMA = 'gstack-ship-measure-sweep/1';
export interface SweepCandidate {
case: string;
kind: MeasureKind;
/** Red-verdict rate over gate history, shrunk toward the pooled rate (K = RED_RATE_SHRINK, as eval:pass-rates --reds). */
redRate: number;
reds: number;
verdicts: number;
/** Per-trial pass rate over the history window, or null without scored trials. */
passRate: number | null;
scoredTrials: number;
/** All-green probability gained if this case never went red: P / (1 − p_i) − P. */
contribution: number;
/** Mean known cost per trial from history (else the gate-wide mean), or null. */
costPerTrialUsd: number | null;
}
export interface SweepPlan {
candidates: SweepCandidate[];
picked: SweepCandidate[];
/** All-green probability over the latest census's gate verdicts, from history. */
allGreen: number | null;
censusCases: string[];
pooledRate: number;
/** Per-case shrunk red rates for every case in the latest census. */
rates: Record<string, number>;
}
const liveCase = (id: string) => Object.hasOwn(E2E_TIERS, id) || Object.hasOwn(LLM_JUDGE_TOUCHFILES, id);
export const sweepKind = (id: string): MeasureKind => (E2E_KINDS[id] as MeasureKind | undefined) ?? (Object.hasOwn(E2E_TIERS, id) ? 'rule' : 'judge');
/**
* Rank the gate cases history flags: every live, non-quarantined case of the
* latest gate census with a failed trial in the window, by its contribution
* to the all-green probability, then by the lower per-trial pass rate. A red
* verdict here is a lane-failing verdict with at least one failed trial (an
* INCOMPLETE record-count verdict is a reporting fault, not a flake). Picks
* the top k.
*/
export function planSweep(records: readonly HistoryRecord[], k: number, opts: { quarantine?: Record<string, unknown> } = {}): SweepPlan {
const quarantine = opts.quarantine ?? CASE_QUARANTINE;
const gate = records.filter(r => r.tier === 'gate' && r.policy_version >= 1);
const verdicts = verdictsOf(gate).filter(v => v.status !== 'SKIPPED');
const red = (v: (typeof verdicts)[number]) => v.failsLane && v.trials.some(t => t.outcome === 'failed');
const pooledRate = verdicts.length ? verdicts.filter(red).length / verdicts.length : 0;
const byCase = new Map<string, { verdicts: number; reds: number }>();
for (const v of verdicts) {
const e = byCase.get(v.case) ?? { verdicts: 0, reds: 0 };
e.verdicts++; if (red(v)) e.reds++;
byCase.set(v.case, e);
}
const rates: Record<string, number> = Object.fromEntries([...byCase].map(([id, e]) => [id, (e.reds + RED_RATE_SHRINK * pooledRate) / (e.verdicts + RED_RATE_SHRINK)]));
const latestRun = verdicts.map(v => v.run).filter(run => run !== 'local').sort().at(-1) ?? 'local';
const censusCases = [...new Set(verdicts.filter(v => v.run === latestRun).map(v => v.case))].sort();
const allGreen = censusCases.length ? censusCases.reduce((p, id) => p * (1 - rates[id]!), 1) : null;
const scored = gate.filter(r => r.execution === 'executed' && (r.outcome === 'passed' || r.outcome === 'failed'));
const knownCost = (r: HistoryRecord) => r.execution === 'executed' && r.cost_known !== false && r.cost_usd > 0;
const mean = (xs: number[]) => xs.length ? xs.reduce((a, b) => a + b, 0) / xs.length : null;
const gateCost = mean(records.filter(r => r.tier === 'gate' && knownCost(r)).map(r => r.cost_usd));
const candidates: SweepCandidate[] = [];
for (const id of censusCases) {
if (!liveCase(id) || Object.hasOwn(quarantine, id)) continue;
const trials = scored.filter(r => r.case === id);
const failedTrials = trials.filter(r => r.outcome === 'failed').length;
const e = byCase.get(id)!;
if (failedTrials === 0) continue;
candidates.push({
case: id, kind: sweepKind(id), redRate: rates[id]!, reds: e.reds, verdicts: e.verdicts,
passRate: trials.length ? (trials.length - failedTrials) / trials.length : null, scoredTrials: trials.length,
contribution: allGreen === null ? 0 : allGreen / (1 - rates[id]!) - allGreen,
costPerTrialUsd: mean(records.filter(r => r.case === id && knownCost(r)).map(r => r.cost_usd)) ?? gateCost,
});
}
candidates.sort((a, b) => b.contribution - a.contribution || b.redRate - a.redRate || (a.passRate ?? 1) - (b.passRate ?? 1) || (a.case < b.case ? -1 : 1));
return { candidates, picked: candidates.slice(0, k), allGreen, censusCases, pooledRate, rates };
}
/**
* A measured case's predicted red-verdict rate: its measured per-trial failure
* fraction shrunk toward the pooled rate with the same K as history, and for a
* behavior case the 3-trial panel's red risk 1 − P(at least 2 of 3 pass).
*/
export function measuredRedRate(kind: MeasureKind, failures: number, trials: number, pooledRate: number): number {
const q = (failures + RED_RATE_SHRINK * pooledRate) / (trials + RED_RATE_SHRINK);
return kind === 'behavior' ? 1 - ((1 - q) ** 3 + 3 * (1 - q) ** 2 * q) : q;
}
/** Π(1 − p_i) over the census cases, with measured cases' rates replacing history's. */
export function predictedAllGreen(plan: SweepPlan, measured: Record<string, number>): number | null {
if (plan.allGreen === null) return null;
return plan.censusCases.reduce((p, id) => p * (1 - (measured[id] ?? plan.rates[id] ?? plan.pooledRate)), 1);
}
export interface SweepRow {
rank: number;
case: string;
kind: MeasureKind;
historyPassRate: number | null;
historyTrials: number;
reds: number;
verdicts: number;
redRate: number;
status: 'planned' | 'measured' | 'budget_exhausted' | 'skipped' | 'error';
decision: string;
measuredPasses: number | null;
measuredTrials: number | null;
estimatedUsd: number | null;
actualUsd: number;
costUnknownTrials: number;
captures: string | null;
note: string;
}
export interface SweepReport {
schema: typeof SWEEP_SCHEMA;
label: 'diagnostic';
verdict: null;
sweepId: string;
startedAt: string;
finishedAt: string | null;
ref: string;
sha: string;
dryRun: boolean;
k: number;
capUsd: number;
priorWeekUsd: number;
/** Charged against the weekly cap by this sweep: known costs plus the estimate for trials whose cost is unknown. */
chargedUsd: number;
historyScope: string;
allGreenBefore: number | null;
allGreenAfter: number | null;
censusVerdicts: number;
rows: SweepRow[];
}
/** Spend charged by sweep reports under dirs started within the last 7 days (deduplicated by sweep id). */
export function weeklySpent(dirs: readonly string[], now: number, excludeId?: string): number {
const seen = new Map<string, number>();
for (const dir of dirs) {
if (!fs.existsSync(dir)) continue;
for (const name of fs.readdirSync(dir, { recursive: true }) as string[]) {
if (path.basename(name) !== 'sweep-report.json') continue;
let report: SweepReport;
try { report = JSON.parse(fs.readFileSync(path.join(dir, name), 'utf8')) as SweepReport; } catch { continue; }
if (report.schema !== SWEEP_SCHEMA || report.sweepId === excludeId || report.dryRun) continue;
const started = Date.parse(report.startedAt);
if (!Number.isFinite(started) || now - started > WEEK_MS || started > now) continue;
seen.set(report.sweepId, Math.max(seen.get(report.sweepId) ?? 0, report.chargedUsd));
}
}
return [...seen.values()].reduce((a, b) => a + b, 0);
}
const pct = (x: number | null) => x === null ? 'n/a' : `${Math.round(x * 1000) / 10}%`;
const prob = (x: number | null) => x === null || x >= 0.001 || x === 0 ? pct(x) : `${(x * 100).toPrecision(2)}%`;
const money = (x: number | null) => x === null ? 'unknown' : `$${x.toFixed(2)}`;
const cell = (text: string) => text.replace(/\|/g, '/').replace(/\n/g, ' ');
export function formatSweepReport(r: SweepReport): string {
const rows = r.rows.map(row => `| ${row.rank} | ${cell(row.case)} | ${row.kind} | ${pct(row.historyPassRate)} of ${row.historyTrials} trials; ${row.reds}/${row.verdicts} red verdicts (rate ${pct(row.redRate)}) | `
+ `${row.measuredTrials === null ? '—' : `${row.measuredPasses}/${row.measuredTrials}`} | ${cell(row.decision)} | ${money(row.estimatedUsd)} / ${money(row.actualUsd)}${row.costUnknownTrials ? ` (+${row.costUnknownTrials} unknown)` : ''} | ${row.captures ? `\`${cell(row.captures)}\`` : '—'} |`);
const noted = r.rows.filter(row => row.status !== 'measured');
return [
`## ship-measure sweep${r.dryRun ? ' (dry run: nothing ran, $0)' : ''}: diagnostic measurements, not verdicts`,
'',
`Ref ${r.ref} at ${r.sha.slice(0, 12)}; K=${r.k}; weekly cap $${r.capUsd} (charged earlier this week $${r.priorWeekUsd.toFixed(2)}, this sweep $${r.chargedUsd.toFixed(2)}). History: ${r.historyScope}.`,
'',
`Predicted gate all-green probability (approximation, assumes independent verdicts, over the latest census's ${r.censusVerdicts} gate verdicts): before ${prob(r.allGreenBefore)} → after ${prob(r.allGreenAfter)}${r.dryRun ? ' (no measurements yet)' : ' (measured cases use this sweep\'s rates)'}.`,
'',
'| # | Case | Kind | History rate | Measured | Decision | Cost (est / actual) | Captures |',
'|---|---|---|---|---|---|---|---|',
...(rows.length ? rows : ['| — | no case in the latest gate census has a failed trial or red verdict | | | | | | |']),
...(noted.length ? ['', r.dryRun ? 'Plan:' : 'Skipped or stopped:', ...noted.map(row => `- ${row.case}: ${row.note}`)] : []),
'',
'Every trial is diagnostic: it never becomes a verdict and never enters pass-rate history. The sweep never pushes or opens a pull request. '
+ 'Fix a BELOW case at its cause, classify a needs-classify case (`ship-measure classify --out <captures root> --case <id> --round baseline ...`), and measure again on the fix.',
].join('\n');
}
export interface SweepOptions {
records: readonly HistoryRecord[];
historyScope: string;
k: number;
capUsd: number;
config: MeasureConfig;
outDir: string;
priorDirs: string[];
dryRun: boolean;
parallel: number;
ref: string;
sha: string;
now?: number;
/** One runner for every case (default: gstack's single-case runner after a selection preflight). */
runner?: TrialRunner;
runnerId?: string;
preflight?: (caseId: string) => { ok: boolean; detail: string };
identity?: (kind: MeasureKind, trials: number) => Record<string, string>;
evalDir?: string;
log?: (line: string) => void;
}
/** Plan, measure in rank order under the weekly cap, and write sweep-report.json/.md after every case. */
export async function runSweep(o: SweepOptions): Promise<SweepReport> {
const log = o.log ?? ((line: string) => console.log(line));
const now = o.now ?? Date.now();
const sweepId = new Date(now).toISOString().replace(/[:.]/g, '-');
const dir = path.join(o.outDir, `sweep-${sweepId}`);
fs.mkdirSync(dir, { recursive: true });
const plan = planSweep(o.records, o.k);
const priorWeekUsd = weeklySpent([...o.priorDirs, o.outDir], now, sweepId);
const pool: SpendPool = { label: `weekly sweep cap`, capUsd: o.capUsd, spentUsd: priorWeekUsd };
const report: SweepReport = {
schema: SWEEP_SCHEMA, label: 'diagnostic', verdict: null, sweepId, startedAt: new Date(now).toISOString(), finishedAt: null, ref: o.ref, sha: o.sha,
dryRun: o.dryRun, k: o.k, capUsd: o.capUsd, priorWeekUsd, chargedUsd: 0, historyScope: o.historyScope,
allGreenBefore: plan.allGreen, allGreenAfter: plan.allGreen, censusVerdicts: plan.censusCases.length, rows: [],
};
const save = () => {
report.chargedUsd = Math.max(0, pool.spentUsd - priorWeekUsd);
fs.writeFileSync(path.join(dir, 'sweep-report.json'), `${JSON.stringify(report, null, 2)}\n`);
fs.writeFileSync(path.join(dir, 'sweep-report.md'), `${formatSweepReport(report)}\n`);
};
const measuredRates: Record<string, number> = {};
for (const [index, c] of plan.picked.entries()) {
const n = kindPlan(c.kind, o.config).trials;
const row: SweepRow = {
rank: index + 1, case: c.case, kind: c.kind, historyPassRate: c.passRate, historyTrials: c.scoredTrials, reds: c.reds, verdicts: c.verdicts, redRate: c.redRate,
status: 'planned', decision: 'planned', measuredPasses: null, measuredTrials: null,
estimatedUsd: c.costPerTrialUsd === null ? null : c.costPerTrialUsd * n, actualUsd: 0, costUnknownTrials: 0, captures: null, note: '',
};
report.rows.push(row);
if (o.dryRun) { row.note = `would measure ${n} trials at ${c.costPerTrialUsd === null ? 'an unknown cost (one calibration trial first)' : `${money(c.costPerTrialUsd)}/trial`}`; save(); continue; }
if (pool.spentUsd >= pool.capUsd) {
Object.assign(row, { status: 'skipped', decision: 'skipped', note: `weekly cap $${o.capUsd} exhausted ($${pool.spentUsd.toFixed(2)} charged in the last 7 days)` });
save();
continue;
}
const selected = (o.preflight ?? caseSelectionPreflight)(c.case);
if (!selected.ok) { Object.assign(row, { status: 'skipped', decision: 'skipped', note: `selection preflight failed (no paid call made): ${selected.detail}` }); save(); continue; }
let m: Measurement;
try {
m = await measureCase({
caseId: c.case, kind: c.kind, round: 'baseline', config: o.config, outDir: dir, parallel: o.parallel, costPerTrialUsd: c.costPerTrialUsd, approved: true,
runner: o.runner ?? gstackRunner(), pool, log,
identity: () => (o.identity ?? ((kind, trials) => measurementIdentity(kind, trials, o.runnerId ?? 'gstack')))(c.kind, n),
...(o.evalDir ? { evalDir: o.evalDir } : {}),
});
} catch (error) {
Object.assign(row, { status: 'error', decision: 'error', note: (error as Error).message });
save();
continue;
}
const captures = path.relative(o.outDir, path.join(dir, caseSlug(c.case), 'baseline'));
const decided = m.status === 'measured' && m.decision !== 'void' && m.decision !== 'incomplete';
Object.assign(row, {
status: m.status === 'measured' ? 'measured' : 'budget_exhausted', decision: m.status === 'measured' ? m.decision : m.status.replace('_', ' '),
...(decided ? { measuredPasses: m.passes, measuredTrials: m.counted } : { measuredPasses: m.trials.filter(t => trialPassed(c.kind, t)).length, measuredTrials: m.trials.length }),
actualUsd: m.actualUsd, costUnknownTrials: m.costUnknownTrials, captures,
note: m.status === 'measured' ? m.decisionReason : m.reason ?? '',
});
if (decided) {
measuredRates[c.case] = measuredRedRate(c.kind, m.counted - m.passes, m.counted, plan.pooledRate);
report.allGreenAfter = predictedAllGreen(plan, measuredRates);
}
save();
}
report.finishedAt = new Date().toISOString();
save();
log(formatSweepReport(report));
log(`[ship-measure] sweep report: ${path.join(dir, 'sweep-report.md')}`);
return report;
}
function gitText(args: string[]): string {
const r = spawnSync('git', args, { cwd: ROOT, encoding: 'utf8', timeout: 30_000 });
return r.status === 0 ? r.stdout.trim() : 'unknown';
}
/** `ship-measure sweep` CLI. Exits 0 when the sweep completed (whatever it measured), 1 when history was unavailable, 4 on usage errors. */
export async function sweepMain(argv: string[], env: NodeJS.ProcessEnv = process.env): Promise<number> {
const flags: Record<string, string> = {};
const historyDirs: string[] = [];
const priorDirs: string[] = [];
const value = (name: string) => (next: () => string | undefined) => { const v = next(); if (v === undefined) throw new Error(`${name} needs a value`); flags[name] = v; };
const list = (into: string[], name: string) => (next: () => string | undefined) => { const v = next(); if (v === undefined) throw new Error(`${name} needs a value`); into.push(path.resolve(v)); };
parseCliFlags(argv, {
...Object.fromEntries(['--k', '--cap-usd', '--command', '--jobs', '--out', '--runs', '--repo'].map(name => [name, value(name)])),
'--history-dir': list(historyDirs, '--history-dir'), '--prior': list(priorDirs, '--prior'), '--dry-run': () => { flags['--dry-run'] = '1'; },
}, 'Usage: ship-measure sweep [--k K] [--cap-usd USD] [--dry-run] [--history-dir DIR]... [--prior DIR]... [--runs N] [--command \'CMD {case}\'] [--jobs N] [--out DIR]');
const config = readMeasureConfig();
const int = (raw: string | undefined, flag: string, fallback: number) => {
if (raw === undefined) return fallback;
if (!/^[1-9][0-9]*$/.test(raw)) throw new Error(`${flag} needs a positive integer. Received: ${raw}`);
return Number(raw);
};
const capUsd = flags['--cap-usd'] === undefined ? config.sweepBudgetUsd : Number(flags['--cap-usd']);
if (!Number.isFinite(capUsd) || capUsd <= 0) throw new Error(`--cap-usd needs a positive amount in USD. Received: ${flags['--cap-usd']}`);
const repo = flags['--repo'] ?? repoSlug(ROOT);
const history = loadPassRateHistory({ repo, workflow: 'evals-periodic.yml', branch: 'main', runsLimit: int(flags['--runs'], '--runs', 10), dirs: historyDirs, sinceDays: 60 });
if (history.historyError) { console.error(`[ship-measure] sweep: pass-rate history unavailable (${history.historyError}); nothing measured`); return 1; }
const outDir = path.resolve(flags['--out'] ?? path.join(process.cwd(), '.context', 'ship-measure'));
const report = await runSweep({
records: history.records, historyScope: history.scope, k: int(flags['--k'], '--k', config.sweepCases), capUsd, config, outDir, priorDirs,
dryRun: flags['--dry-run'] === '1', parallel: int(flags['--jobs'], '--jobs', Number(env.EVALS_JOBS) || 4),
ref: env.GITHUB_REF_NAME ?? gitText(['rev-parse', '--abbrev-ref', 'HEAD']), sha: env.GITHUB_SHA ?? gitText(['rev-parse', 'HEAD']),
...(flags['--command'] ? { runner: commandRunner(flags['--command']), runnerId: `command:${flags['--command']}`, preflight: () => ({ ok: true, detail: 'documented command' }) } : {}),
});
if (env.GITHUB_STEP_SUMMARY) fs.appendFileSync(env.GITHUB_STEP_SUMMARY, `${formatSweepReport(report)}\n`);
return 0;
}
+285 -133
View File
@@ -1,43 +1,53 @@
#!/usr/bin/env bun
/**
* ship-measure — /ship's measure-then-fix runner for a red eval case or a red
* free-suite shard. Every trial it runs is DIAGNOSTIC: it never changes a
* recorded verdict, its artifacts are labeled `diagnostic` with `verdict:
* null`, and they live under `.context/ship-measure/`, never in the project
* eval dir that pass-rate history and CI uploads read. The lane verdict still
* comes from the one full gate run /ship makes after the case measures at or
* above target (docs/TESTING_INTERNALS.md#ship-measure).
* free-suite shard, and the weekly off-ship qualification sweep. Every trial
* it runs is DIAGNOSTIC: it never changes a recorded verdict, its artifacts
* are labeled `diagnostic` with `verdict: null`, and they live under
* `.context/ship-measure/`, never in the project eval dir that pass-rate
* history and CI uploads read. The lane verdict still comes from the one full
* gate run /ship makes after the case measures at or above the bar
* (docs/TESTING_INTERNALS.md#ship-measure).
*
* Per-kind aggregation (config keys in bin/gstack-config, CEO-9 defaults):
* rule N trials (ship_measure_rule_trials, 10); meets at >= 90% of N.
* behavior P panels of 3 (ship_measure_behavior_panels, 4); every panel
* passes at its own 2 of 3 with no contract violation, and >= 90%
* of all 3P trials pass (11 of 12).
* judge O outputs (ship_measure_judge_outputs, 10), each scored by its
* 3-sample panel (passes at 2 of 3); meets at >= 90% of O.
* Trials per batch (config keys in bin/gstack-config): rule
* ship_measure_rule_trials (10); behavior ship_measure_behavior_panels panels
* of 3 (4, so 12 trials); judge ship_measure_judge_outputs outputs (10), each
* scored by its median 3-sample panel. The decision is the measurement bar in
* scripts/lib/measure-bar.ts: MEETS at 9/10, MEETS-qualified at 8/10 with
* every red in a qualifying class, EXTEND at 7/10 (N more trials on identical
* inputs, decided once on the pooled 2N), else BELOW (a fix round). A batch
* with more than 30% provider-evidence failures is void and is redispatched
* once.
*
* Spend: ship_measure_budget_usd is an estimated ADMISSION budget per red case
* across the baseline and every repair round. Before each batch the runner
* reserves the estimated cost of every concurrent trial and admits only what
* fits beside what was already spent; actual costs are reconciled after the
* batch. With no per-trial estimate it asks once ("estimate unknown"), then
* runs one calibration trial alone. ship_measure_ask_per_trial_usd asks first
* above that per-trial estimate. ship_measure_max_rounds caps repair rounds.
* across the baseline, extension, redispatch and every repair round. Before
* each batch the runner reserves the estimated cost of every concurrent trial
* and admits only what fits beside what was already spent; actual costs are
* reconciled after the batch. With no per-trial estimate it asks once
* ("estimate unknown"), then runs one calibration trial alone.
* ship_measure_ask_per_trial_usd asks first above that per-trial estimate.
* ship_measure_max_rounds caps repair rounds.
*
* Usage:
* bun run scripts/ship-measure.ts table
* bun run scripts/ship-measure.ts measure --case ID --round baseline|round-N
* [--kind rule|behavior|judge] [--command 'CMD {case}'] [--cost-per-trial USD]
* [--approved] [--fix TEXT] [--jobs N] [--out DIR]
* bun run scripts/ship-measure.ts classify --case ID --round R --trials 2,5 --class C --evidence TEXT [--out DIR]
* bun run scripts/ship-measure.ts decide --case ID --round R [--out DIR]
* bun run scripts/ship-measure.ts extend --case ID --round R [--command ..] [--cost-per-trial USD] [--jobs N] [--out DIR]
* bun run scripts/ship-measure.ts skip --case ID --reason TEXT [--out DIR]
* bun run scripts/ship-measure.ts report [--out DIR]
* bun run scripts/ship-measure.ts free (--files a,b,... | --shard I) [--reruns N]
* [--concurrency C] [--backend local|ubicloud] [--wall-cap SECS] [--out DIR]
* bun run scripts/ship-measure.ts sweep [--k K] [--cap-usd USD] [--dry-run] [--history-dir DIR]...
* [--prior DIR] [--command 'CMD {case}'] [--jobs N] [--out DIR] (scripts/ship-measure-sweep.ts)
*
* measure exits 0 at or above target, 1 below target, 2 when it needs the
* user's approval (nothing ran), 3 on a named-red stop (budget exhausted or
* round limit), 4 on a usage or setup error. free exits 0 when every
* completed rerun passed, else 1.
* measure, extend, classify and decide exit 0 at MEETS or MEETS-qualified, 1
* at BELOW (a fix round), 2 when it needs the user's approval (nothing ran), 3
* on a named-red stop (budget exhausted, round limit, a void batch twice), 4 on
* a usage, setup or refused-classification error, 5 at needs-classify, 6 at
* EXTEND. free exits 0 when every completed rerun passed, else 1.
*/
import * as fs from 'node:fs';
import * as os from 'node:os';
@@ -45,11 +55,16 @@ import * as path from 'node:path';
import { spawn, spawnSync } from 'node:child_process';
import { EVAL_POLICY } from '../test/helpers/periodic-exclude-data';
import { E2E_KINDS, E2E_TIERS, LLM_JUDGE_TOUCHFILES } from '../test/helpers/touchfiles-data';
import { getProjectEvalDir, isFinalizedEvalResultFile } from '../test/helpers/eval-store';
import { getClaudeCliVersion, getProjectEvalDir, isFinalizedEvalResultFile } from '../test/helpers/eval-store';
import { parseCliFlags } from './lib/shard-engine';
import { collectPaidTestFiles, paidSelectionEnv } from './lib/paid-select';
import { DEFAULT_JOBS } from './lib/paid-types';
import { privateFreeHome } from './lib/free-home-guard';
import {
FAILURE_CLASSES, barThresholds, classificationProblem, decide, identityMismatch,
type BarDecision, type BarNext, type BarThresholds, type Classification, type FailureClass, type SetSummary, type TrialSet,
} from './lib/measure-bar';
import harnessManifest from './harness-version.json';
import {
TREE_MUTATING, assignFilesToShards, collectFreeTestFiles, fullSuiteJobs, loadFreeTestDurations, packShardsByDuration,
runFreeShard, wallTimeoutForShard, type FreeShardOutcome,
@@ -65,12 +80,16 @@ export const JUDGE_SAMPLES = EVAL_POLICY.judge.samples;
export interface MeasureConfig {
ruleTrials: number; behaviorPanels: number; judgeOutputs: number;
askPerTrialUsd: number; budgetUsd: number; maxRounds: number; rerunBackend: RerunBackend;
sweepCases: number; sweepBudgetUsd: number;
}
export const MEASURE_DEFAULTS: MeasureConfig = {
ruleTrials: 10, behaviorPanels: 4, judgeOutputs: 10, askPerTrialUsd: 2, budgetUsd: 25, maxRounds: 3, rerunBackend: 'local',
sweepCases: 5, sweepBudgetUsd: 150,
};
const USD_FIELDS = new Set<keyof MeasureConfig>(['askPerTrialUsd', 'budgetUsd', 'sweepBudgetUsd']);
function gstackConfigGet(key: string): string | undefined {
const r = spawnSync(path.join(ROOT, 'bin', 'gstack-config'), ['get', key], { encoding: 'utf8', timeout: 10_000 });
return r.status === 0 ? r.stdout.trim() : undefined;
@@ -86,6 +105,8 @@ export function readMeasureConfig(configGet: (key: string) => string | undefined
['budgetUsd', 'ship_measure_budget_usd', configGet('ship_measure_budget_usd')],
['maxRounds', 'ship_measure_max_rounds', configGet('ship_measure_max_rounds')],
['rerunBackend', 'ship_rerun_backend', configGet('ship_rerun_backend')],
['sweepCases', 'ship_measure_sweep_cases', configGet('ship_measure_sweep_cases')],
['sweepBudgetUsd', 'ship_measure_sweep_budget_usd', configGet('ship_measure_sweep_budget_usd')],
];
const config = { ...MEASURE_DEFAULTS };
for (const [field, key, rawValue] of raw) {
@@ -96,7 +117,7 @@ export function readMeasureConfig(configGet: (key: string) => string | undefined
config.rerunBackend = value;
continue;
}
const usd = field === 'askPerTrialUsd' || field === 'budgetUsd';
const usd = USD_FIELDS.has(field);
if (!(usd ? Number.isFinite(Number(value)) && Number(value) > 0 : /^[1-9][0-9]*$/.test(value))) {
throw new Error(`${key} '${value}' is not a ${usd ? 'positive amount in USD' : 'positive integer'}. Fix: gstack-config set ${key} ${MEASURE_DEFAULTS[field]}`);
}
@@ -105,51 +126,48 @@ export function readMeasureConfig(configGet: (key: string) => string | undefined
return config;
}
const ninetyPercent = (n: number) => Math.ceil((n * 9) / 10);
export interface KindPlan {
kind: MeasureKind;
/** Scored units: trials (rule), panels (behavior), outputs (judge). */
units: number;
/** Behavior trials launch in panels of 3; rule and judge trials one at a time. */
trialsPerUnit: number;
/** Trials per batch (outputs for judge). */
trials: number;
/** Units that must pass. */
unitTarget: number;
/** Trials (outputs for judge) that must pass. */
trialTarget: number;
bar: BarThresholds;
pooledBar: BarThresholds;
}
export function kindPlan(kind: MeasureKind, config: MeasureConfig): KindPlan {
if (kind === 'behavior') {
const trials = config.behaviorPanels * PANEL.n;
return { kind, units: config.behaviorPanels, trialsPerUnit: PANEL.n, trials, unitTarget: config.behaviorPanels, trialTarget: ninetyPercent(trials) };
}
const units = kind === 'judge' ? config.judgeOutputs : config.ruleTrials;
return { kind, units, trialsPerUnit: 1, trials: units, unitTarget: ninetyPercent(units), trialTarget: ninetyPercent(units) };
const trials = kind === 'behavior' ? config.behaviorPanels * PANEL.n : kind === 'judge' ? config.judgeOutputs : config.ruleTrials;
return { kind, trialsPerUnit: kind === 'behavior' ? PANEL.n : 1, trials, bar: barThresholds(trials), pooledBar: barThresholds(2 * trials) };
}
/** The per-kind table /ship prints before it runs anything. */
export function formatKindTable(config: MeasureConfig): string {
const rule = kindPlan('rule', config);
const behavior = kindPlan('behavior', config);
const judge = kindPlan('judge', config);
const budget = `$${config.budgetUsd} total; asks above $${config.askPerTrialUsd}/trial or with no estimate`;
const row = (kind: MeasureKind, label: string) => {
const { trials: n, bar, pooledBar } = kindPlan(kind, config);
const unit = kind === 'judge' ? ' outputs' : '';
return `| ${kind} | ${label} | ${bar.strict} of ${n}${unit} | ${bar.qualified} of ${n}, every red qualifying | ${bar.floor} of ${n} (or ${bar.qualified} that does not qualify): ${n} more on identical inputs, then ${pooledBar.strict} of ${2 * n}, or ${pooledBar.qualified} of ${2 * n} qualified | ${budget} |`;
};
return [
'| Kind | Trials | Pass bar | Panels | Budget per red case |',
'|---|---|---|---|---|',
`| rule | ${rule.trials} | at least ${rule.trialTarget} of ${rule.trials} trials | none | ${budget} |`,
`| behavior | ${behavior.trials} | every panel at ${PANEL.k} of ${PANEL.n}, no contract violation, at least ${behavior.trialTarget} of ${behavior.trials} trials | ${behavior.units} of ${PANEL.n} trials | ${budget} |`,
`| judge | ${judge.trials} outputs | at least ${judge.unitTarget} of ${judge.trials} outputs | ${JUDGE_SAMPLES} samples per output, passes at 2 of ${JUDGE_SAMPLES} | ${budget} |`,
'| Kind | Trials per batch | MEETS | MEETS-qualified | EXTEND, then the pooled decision | Budget per red case |',
'|---|---|---|---|---|---|',
row('rule', String(config.ruleTrials)),
row('behavior', `${config.behaviorPanels * PANEL.n} (${config.behaviorPanels} panels of ${PANEL.n})`),
row('judge', `${config.judgeOutputs} outputs, each its median ${JUDGE_SAMPLES}-sample panel`),
'',
'Qualifying reds: provider (affirmative provider or transport evidence only), judge noise at the threshold, a model miss citing its evidence.',
'Never qualifying (a fix round): a timeout, a hang, a regression, a contract violation, any known fixable cause. Below the EXTEND floor: BELOW, a fix round.',
'A batch with more than 30% of trials failing on provider evidence is void and is redispatched once; both batches are reported. There is never a third batch.',
`Repair rounds: at most ${config.maxRounds}. Free-suite rerun backend: ${config.rerunBackend}. Every trial is diagnostic and never changes a recorded verdict.`,
].join('\n');
}
// ─── Trials and aggregation ────────────────────────────────────────────────
// ─── Trials ────────────────────────────────────────────────────────────────
export interface TrialResult {
passed: boolean;
/** A contract violation fails its unit at any count. */
/** A contract violation fails the measurement at any count. */
contract?: boolean;
/** Billed cost; absent means unknown. */
costUsd?: number;
@@ -157,38 +175,21 @@ export interface TrialResult {
samples?: boolean[];
failureCause?: string;
failureDetail?: string;
/** The eval store's one-line failure_cause_evidence. */
failureEvidence?: string;
}
export interface TrialRequest { caseId: string; kind: MeasureKind; round: string; trial: number; dir: string }
export type TrialRunner = (request: TrialRequest) => Promise<TrialResult>;
export interface TrialRecord extends TrialResult { trial: number; unit: number; batch: number; dir: string }
export interface Aggregate { unitPasses: number; trialPasses: number; complete: boolean; meets: boolean; unitResults: boolean[] }
function unitPassed(kind: MeasureKind, trials: TrialRecord[]): boolean {
if (trials.some(t => t.contract)) return false;
if (kind === 'behavior') return trials.length === PANEL.n && trials.filter(t => t.passed).length >= PANEL.k;
const [only] = trials;
if (!only) return false;
if (kind === 'judge' && only.samples) return only.samples.length === JUDGE_SAMPLES && only.samples.filter(Boolean).length >= 2;
return only.passed;
}
export function aggregate(plan: KindPlan, trials: TrialRecord[]): Aggregate {
const unitResults = Array.from({ length: plan.units }, (_, i) => unitPassed(plan.kind, trials.filter(t => t.unit === i + 1)));
const unitPasses = unitResults.filter(Boolean).length;
const trialPasses = plan.kind === 'behavior' ? trials.filter(t => t.passed && !t.contract).length : unitPasses;
const complete = trials.length === plan.trials;
return { unitPasses, trialPasses, complete, unitResults, meets: complete && unitPasses >= plan.unitTarget && trialPasses >= plan.trialTarget };
}
export interface TrialRecord extends TrialResult { trial: number; set: TrialSet; unit: number; batch: number; dir: string }
// ─── One measurement: baseline or a repair round ───────────────────────────
export type MeasureStatus = 'measured' | 'needs_approval' | 'budget_exhausted' | 'round_limit';
export interface Measurement {
schema: 'gstack-ship-measure/1';
schema: 'gstack-ship-measure/2';
label: 'diagnostic';
verdict: null;
case: string;
@@ -198,10 +199,17 @@ export interface Measurement {
status: MeasureStatus;
reason?: string;
plan: KindPlan;
/** Inputs every pooled batch must share: tree, runner, CLI, model, image, policy and harness versions. */
identity: Record<string, string>;
trials: TrialRecord[];
unitPasses: number;
trialPasses: number;
meets: boolean;
decision: BarDecision;
next: BarNext;
decisionReason: string;
passes: number;
counted: number;
sets: SetSummary[];
/** The per-trial estimate later batches reserve; null until known. */
costPerTrialUsd: number | null;
/** Reserved before admission; null when a batch ran without an estimate. */
estimatedUsd: number | null;
actualUsd: number;
@@ -211,6 +219,9 @@ export interface Measurement {
interface Ledger { case: string; kind: MeasureKind; approved: boolean; spentUsd: number; rounds: string[] }
/** A spend cap shared across cases (the sweep's weekly cap), checked beside each case's own budget. */
export interface SpendPool { label: string; capUsd: number; spentUsd: number }
export interface MeasureOptions {
caseId: string;
kind: MeasureKind;
@@ -224,6 +235,9 @@ export interface MeasureOptions {
fix?: string;
/** Pass-rate history directory diagnostic artifacts must stay out of (default: the project eval dir). */
evalDir?: string;
/** The batch identity (default: measurementIdentity of the working tree). */
identity?: () => Record<string, string>;
pool?: SpendPool;
log?: (line: string) => void;
}
@@ -244,6 +258,59 @@ function roundNumber(round: string): number {
return Number(match[1]);
}
/** The git tree of the working tree as it would be committed now (tracked and unignored files), without touching the real index. */
export function frozenTree(cwd: string): string {
const git = (args: string[], env: NodeJS.ProcessEnv = process.env) => spawnSync('git', args, { cwd, env, encoding: 'utf8', timeout: 120_000, maxBuffer: 64 * 1024 * 1024 });
const top = git(['rev-parse', '--show-toplevel']);
if (top.status !== 0) return 'no-git';
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ship-measure-index-'));
try {
const index = path.resolve(cwd, git(['rev-parse', '--git-path', 'index']).stdout.trim());
if (fs.existsSync(index)) fs.copyFileSync(index, path.join(tmp, 'index'));
const env = { ...process.env, GIT_INDEX_FILE: path.join(tmp, 'index') };
const add = git(['-C', top.stdout.trim(), 'add', '-A'], env);
const tree = add.status === 0 ? git(['write-tree'], env) : add;
if (tree.status !== 0) throw new Error(`ship-measure: could not snapshot the working tree: ${tree.stderr.trim()}`);
return tree.stdout.trim();
} finally { fs.rmSync(tmp, { recursive: true, force: true }); }
}
/** Everything a pooled batch must share with the batch it extends. */
export function measurementIdentity(kind: MeasureKind, trials: number, runner: string, cwd = process.cwd(), env: NodeJS.ProcessEnv = process.env): Record<string, string> {
return {
tree: frozenTree(cwd), kind, trials: String(trials), runner, policy: String(EVAL_POLICY.version), harness: String(harnessManifest.version),
claude_cli: getClaudeCliVersion(), model: env.EVALS_MODEL ?? 'default', image: env.GSTACK_CI_IMAGE ?? 'local', bun: Bun.version,
};
}
const roundDirOf = (outDir: string, caseId: string, round: string) => path.join(outDir, caseSlug(caseId), round);
const classificationsPath = (roundDir: string) => path.join(roundDir, 'classifications.jsonl');
export function readClassifications(roundDir: string): Classification[] {
if (!fs.existsSync(classificationsPath(roundDir))) return [];
return fs.readFileSync(classificationsPath(roundDir), 'utf8').split('\n').filter(Boolean).map(line => JSON.parse(line) as Classification);
}
/** Apply the bar to the measurement's trials and its classification records. */
function withDecision(m: Measurement, roundDir: string): Measurement {
const outcome = decide(m.kind, m.plan.trials, m.trials, readClassifications(roundDir));
return { ...m, decision: outcome.decision, next: outcome.next, decisionReason: outcome.reason, passes: outcome.passes, counted: outcome.thresholds.trials, sets: outcome.sets };
}
function loadMeasurement(outDir: string, caseId: string, round: string): { m: Measurement; roundDir: string } {
const roundDir = roundDirOf(outDir, caseId, round);
const m = readJson<Measurement>(path.join(roundDir, 'measurement.json'));
if (!m) throw new Error(`ship-measure: no measurement at ${roundDir}; run measure --case ${caseId} --round ${round} first`);
return { m, roundDir };
}
function saveMeasurement(m: Measurement, roundDir: string, log: (line: string) => void): Measurement {
const decided = withDecision(m, roundDir);
writeJson(path.join(roundDir, 'measurement.json'), decided);
log(formatMeasurementLine(decided));
return decided;
}
export async function measureCase(o: MeasureOptions): Promise<Measurement> {
const log = o.log ?? ((line: string) => console.log(line));
const plan = kindPlan(o.kind, o.config);
@@ -252,9 +319,9 @@ export async function measureCase(o: MeasureOptions): Promise<Measurement> {
const ledgerPath = path.join(caseDir, 'ledger.json');
const ledger: Ledger = readJson<Ledger>(ledgerPath) ?? { case: o.caseId, kind: o.kind, approved: false, spentUsd: 0, rounds: [] };
const base: Measurement = {
schema: 'gstack-ship-measure/1', label: 'diagnostic', verdict: null, case: o.caseId, kind: o.kind, round: o.round,
...(o.fix ? { fix: o.fix } : {}), status: 'measured', plan, trials: [], unitPasses: 0, trialPasses: 0, meets: false,
estimatedUsd: 0, actualUsd: 0, costUnknownTrials: 0, parallel: Math.max(1, o.parallel),
schema: 'gstack-ship-measure/2', label: 'diagnostic', verdict: null, case: o.caseId, kind: o.kind, round: o.round,
...(o.fix ? { fix: o.fix } : {}), status: 'measured', plan, identity: {}, trials: [], decision: 'incomplete', next: 'stop', decisionReason: '',
passes: 0, counted: 0, sets: [], costPerTrialUsd: o.costPerTrialUsd, estimatedUsd: 0, actualUsd: 0, costUnknownTrials: 0, parallel: Math.max(1, o.parallel),
};
if (roundNumber(o.round) > o.config.maxRounds) {
return { ...base, status: 'round_limit', reason: `repair-round limit ${o.config.maxRounds} reached; stop with the named-red report` };
@@ -269,104 +336,167 @@ export async function measureCase(o: MeasureOptions): Promise<Measurement> {
const roundDir = path.join(caseDir, o.round);
if (fs.existsSync(roundDir)) throw new Error(`ship-measure: ${roundDir} already exists; measurements are never overwritten. Use the next round.`);
fs.mkdirSync(roundDir, { recursive: true });
const result = await runBatches(o, plan, roundDir, ledger, estimate, log);
const totals = aggregate(plan, result.trials);
const measurement: Measurement = { ...base, ...result, unitPasses: totals.unitPasses, trialPasses: totals.trialPasses, meets: totals.meets };
let m: Measurement = { ...base, identity: (o.identity ?? (() => measurementIdentity(o.kind, plan.trials, 'gstack')))() };
m = withDecision(await runSet(o, m, 'initial', roundDir, ledger, log), roundDir);
if (m.status === 'measured' && m.next === 'redispatch') {
log(`[ship-measure] ${o.caseId} ${o.round}: ${m.decisionReason}`);
m = await runSet(o, m, 'redispatch', roundDir, ledger, log);
}
ledger.rounds.push(o.round);
writeJson(path.join(roundDir, 'measurement.json'), measurement);
writeJson(ledgerPath, ledger);
log(formatMeasurementLine(measurement));
return measurement;
return saveMeasurement(m, roundDir, log);
}
async function runBatches(o: MeasureOptions, plan: KindPlan, roundDir: string, ledger: Ledger, initialEstimate: number | null,
log: (line: string) => void): Promise<Pick<Measurement, 'trials' | 'status' | 'reason' | 'estimatedUsd' | 'actualUsd' | 'costUnknownTrials' | 'parallel'>> {
export interface ExtendOptions extends Omit<MeasureOptions, 'kind' | 'fix' | 'approved' | 'costPerTrialUsd'> { costPerTrialUsd?: number | null }
/** EXTEND: N more trials on identical inputs (the identity must match field for field), decided once on the pooled 2N. */
export async function extendCase(o: ExtendOptions): Promise<Measurement> {
const log = o.log ?? ((line: string) => console.log(line));
assertDiagnosticDir(o.outDir, o.evalDir ?? getProjectEvalDir());
let { m, roundDir } = loadMeasurement(o.outDir, o.caseId, o.round);
m = withDecision(m, roundDir);
if (m.trials.some(t => t.set === 'extend')) throw new Error(`ship-measure: ${o.caseId} ${o.round} was already extended; there is never a third batch`);
if (m.decision !== 'EXTEND') throw new Error(`ship-measure: ${o.caseId} ${o.round} is ${m.decision}, not EXTEND (${m.decisionReason})`);
const now = (o.identity ?? (() => measurementIdentity(m.kind, m.plan.trials, m.identity.runner ?? 'gstack')))();
const changed = identityMismatch(m.identity, now);
if (changed) throw new Error(`ship-measure: refusing to pool ${o.caseId} ${o.round}: ${changed}. EXTEND needs identical inputs; measure the changed tree as a new round.`);
const ledgerPath = path.join(o.outDir, caseSlug(o.caseId), 'ledger.json');
const ledger = readJson<Ledger>(ledgerPath) ?? { case: o.caseId, kind: m.kind, approved: true, spentUsd: m.actualUsd, rounds: [o.round] };
const opts: MeasureOptions = { ...o, kind: m.kind, approved: true, costPerTrialUsd: o.costPerTrialUsd ?? m.costPerTrialUsd };
m = withDecision(await runSet(opts, { ...m, status: 'measured' }, 'extend', roundDir, ledger, log), roundDir);
if (m.status === 'measured' && m.next === 'redispatch') {
log(`[ship-measure] ${o.caseId} ${o.round}: ${m.decisionReason}`);
m = await runSet(opts, m, 'extend-redispatch', roundDir, ledger, log);
}
writeJson(ledgerPath, ledger);
return saveMeasurement(m, roundDir, log);
}
/** Append one immutable classification record per trial (a later record supersedes, never edits), then re-decide. */
export function classifyTrials(outDir: string, caseId: string, round: string, trials: number[], cls: string, evidence: string, log = (line: string) => console.log(line)): Measurement {
const { m, roundDir } = loadMeasurement(outDir, caseId, round);
for (const trial of trials) {
const problem = classificationProblem(m.trials.find(t => t.trial === trial), m.kind, cls, evidence);
if (problem) throw new Error(`ship-measure: classify t${trial} refused: ${problem}`);
}
const at = new Date().toISOString();
fs.appendFileSync(classificationsPath(roundDir), trials.map(trial => `${JSON.stringify({ trial, class: cls as FailureClass, evidence: evidence.trim(), by: 'agent', at } satisfies Classification)}\n`).join(''));
return saveMeasurement(m, roundDir, log);
}
export function decideRound(outDir: string, caseId: string, round: string, log = (line: string) => console.log(line)): Measurement {
const { m, roundDir } = loadMeasurement(outDir, caseId, round);
return saveMeasurement(m, roundDir, log);
}
/** Run one set of N trials (initial, redispatch, extend or extend-redispatch) under the admission budget. */
async function runSet(o: MeasureOptions, m: Measurement, set: TrialSet, roundDir: string, ledger: Ledger, log: (line: string) => void): Promise<Measurement> {
const plan = m.plan;
const first = m.trials.length;
const trials: TrialRecord[] = [];
const initialEstimate = m.costPerTrialUsd;
let estimate = initialEstimate;
let estimatedUsd: number | null = 0;
let actualUsd = 0;
let costUnknownTrials = 0;
let knownCosts = 0;
let estimatedUsd = m.estimatedUsd;
let actualUsd = m.actualUsd;
let costUnknownTrials = m.costUnknownTrials;
let knownCosts = m.trials.filter(t => typeof t.costUsd === 'number').length;
let batch = 0;
const parallel = Math.max(1, o.parallel);
const done = (status: MeasureStatus, reason?: string): Measurement => ({
...m, status, ...(reason ? { reason } : {}), trials: [...m.trials, ...trials], estimatedUsd, actualUsd, costUnknownTrials, parallel,
costPerTrialUsd: estimate,
});
while (trials.length < plan.trials) {
const remaining = plan.trials - trials.length;
const headroom = o.config.budgetUsd - ledger.spentUsd;
const headroom = Math.min(o.config.budgetUsd - ledger.spentUsd, o.pool ? o.pool.capUsd - o.pool.spentUsd : Infinity);
let size = Math.min(parallel, remaining);
if (estimate === null) size = knownCosts === 0 && trials.length === 0 ? 1 : size;
else size = Math.min(size, Math.floor((headroom + 1e-9) / estimate));
if (size < 1 || headroom <= 0) {
return { trials, status: 'budget_exhausted', estimatedUsd, actualUsd, costUnknownTrials, parallel,
reason: `budget $${o.config.budgetUsd} per red case: spent $${ledger.spentUsd.toFixed(2)}, next trial reserves $${(estimate ?? 0).toFixed(2)}; ${trials.length} of ${plan.trials} trials ran` };
const cap = o.pool && o.pool.capUsd - o.pool.spentUsd <= o.config.budgetUsd - ledger.spentUsd
? `${o.pool.label} $${o.pool.capUsd}: spent $${o.pool.spentUsd.toFixed(2)}`
: `budget $${o.config.budgetUsd} per red case: spent $${ledger.spentUsd.toFixed(2)}`;
return done('budget_exhausted', `${cap}, next trial reserves $${(estimate ?? 0).toFixed(2)}; ${trials.length} of ${plan.trials} ${set} trials ran`);
}
batch += 1;
estimatedUsd = estimate === null || estimatedUsd === null ? null : estimatedUsd + size * estimate;
log(`[ship-measure] ${o.caseId} ${o.round}: batch ${batch} admits ${size} trial(s)${estimate === null ? ' (calibration, no estimate)' : `, reserves $${(size * estimate).toFixed(2)}`}; spent $${ledger.spentUsd.toFixed(2)} of $${o.config.budgetUsd}`);
const start = trials.length;
log(`[ship-measure] ${o.caseId} ${o.round} ${set}: batch ${batch} admits ${size} trial(s)${estimate === null ? ' (calibration, no estimate)' : `, reserves $${(size * estimate).toFixed(2)}`}; spent $${ledger.spentUsd.toFixed(2)} of $${o.config.budgetUsd}`);
const start = first + trials.length;
const results = await Promise.all(Array.from({ length: size }, async (_, i) => {
const trial = start + i + 1;
const dir = path.join(roundDir, `t${String(trial).padStart(2, '0')}`);
fs.mkdirSync(dir, { recursive: false });
let result: TrialResult;
try { result = await o.runner({ caseId: o.caseId, kind: o.kind, round: o.round, trial, dir }); }
try { result = await o.runner({ caseId: o.caseId, kind: m.kind, round: o.round, trial, dir }); }
catch (error) { result = { passed: false, failureCause: 'unknown', failureDetail: `runner error: ${(error as Error).message}` }; }
return { ...result, trial, unit: Math.ceil(trial / plan.trialsPerUnit), batch, dir } satisfies TrialRecord;
return { ...result, trial, set, unit: Math.ceil((trial - first) / plan.trialsPerUnit), batch, dir } satisfies TrialRecord;
}));
for (const record of results) {
writeJson(path.join(record.dir, 'trial.json'), { label: 'diagnostic', verdict: null, ...record });
trials.push(record);
if (typeof record.costUsd === 'number' && Number.isFinite(record.costUsd)) {
actualUsd += record.costUsd; ledger.spentUsd += record.costUsd; knownCosts += 1;
} else {
costUnknownTrials += 1;
if (estimate !== null) ledger.spentUsd += estimate;
}
const charge = typeof record.costUsd === 'number' && Number.isFinite(record.costUsd) ? record.costUsd : estimate;
if (typeof record.costUsd === 'number' && Number.isFinite(record.costUsd)) { actualUsd += record.costUsd; knownCosts += 1; }
else costUnknownTrials += 1;
if (charge !== null) { ledger.spentUsd += charge; if (o.pool) o.pool.spentUsd += charge; }
}
if (initialEstimate === null && knownCosts > 0) estimate = actualUsd / knownCosts;
}
return { trials, status: 'measured', estimatedUsd, actualUsd, costUnknownTrials, parallel };
return done('measured');
}
const DECISION_WORDS: Record<BarDecision, string> = {
MEETS: 'MEETS', 'MEETS-qualified': 'MEETS-qualified', EXTEND: 'EXTEND', BELOW: 'BELOW (fix round)',
'needs-classify': 'needs-classify', void: 'void', incomplete: 'incomplete',
};
export function formatMeasurementLine(m: Measurement): string {
const observed = m.kind === 'behavior'
? `${m.trialPasses}/${m.plan.trials} trials, ${m.unitPasses}/${m.plan.units} panels`
: `${m.unitPasses}/${m.plan.units} ${m.kind === 'judge' ? 'outputs' : 'trials'}`;
const when = m.fix ? ` after fix at ${m.fix}` : m.round === 'baseline' ? ' (baseline)' : '';
const verdict = m.status === 'measured' ? (m.meets ? 'at or above target' : 'below target') : m.status.replace('_', ' ');
return `[ship-measure] DIAGNOSTIC ${m.case} ${m.round}: observed ${observed}${when}; ${verdict}; est ${usd(m.estimatedUsd)}, actual ${usd(m.actualUsd)}${m.costUnknownTrials ? ` (+${m.costUnknownTrials} trial(s) cost unknown)` : ''}`;
const verdict = m.status === 'measured' ? DECISION_WORDS[m.decision] : m.status.replace('_', ' ');
const sets = m.sets.length > 1 ? `; batches ${m.sets.map(s => `${s.set} ${s.passes}/${s.trials}${s.void ? ' void' : ''}`).join(', ')}` : '';
return `[ship-measure] DIAGNOSTIC ${m.case} ${m.round}: observed ${m.passes}/${m.counted} ${m.kind === 'judge' ? 'outputs' : 'trials'}${when}; ${verdict}${m.status === 'measured' ? ` — ${m.decisionReason}` : m.reason ? ` — ${m.reason}` : ''}${sets}; est ${usd(m.estimatedUsd)}, actual ${usd(m.actualUsd)}${m.costUnknownTrials ? ` (+${m.costUnknownTrials} trial(s) cost unknown)` : ''}`;
}
const usd = (value: number | null) => value === null ? 'unknown' : `$${value.toFixed(2)}`;
/** measure/extend/classify/decide exit code for a measurement (see the header). */
export function measurementExit(m: Measurement): number {
if (m.status === 'needs_approval') return 2;
if (m.status !== 'measured') return 3;
return ({ MEETS: 0, 'MEETS-qualified': 0, BELOW: 1, 'needs-classify': 5, EXTEND: 6, void: 3, incomplete: 3 } as const)[m.decision];
}
// ─── Unmeasured skip and the PR-body report ────────────────────────────────
export function recordUnmeasured(outDir: string, caseId: string, reason: string): string {
if (!reason.trim()) throw new Error('skip needs --reason: why this red is infrastructure and is not measured');
const file = path.join(outDir, caseSlug(caseId), 'unmeasured.json');
writeJson(file, { schema: 'gstack-ship-measure/1', label: 'diagnostic', verdict: null, case: caseId, status: 'unmeasured', reason: reason.trim() });
writeJson(file, { schema: 'gstack-ship-measure/2', label: 'diagnostic', verdict: null, case: caseId, status: 'unmeasured', reason: reason.trim() });
return file;
}
/** The per-case table for the PR body: every measurement, estimated and actual spend, never a pass for an unmeasured case. */
export function formatReport(outDir: string): string {
const rows = ['| Case | Kind | Measurement | Observed | Target | Est. spend | Actual spend | Status |', '|---|---|---|---|---|---|---|---|'];
const rows = ['| Case | Kind | Measurement | Observed | Bar | Decision | Est. spend | Actual spend |', '|---|---|---|---|---|---|---|---|'];
const cases = fs.existsSync(outDir) ? fs.readdirSync(outDir).filter(name => fs.statSync(path.join(outDir, name)).isDirectory()).sort() : [];
const qualified: string[] = [];
for (const name of cases) {
const dir = path.join(outDir, name);
const skip = readJson<{ case: string; reason: string }>(path.join(dir, 'unmeasured.json'));
if (skip) { rows.push(`| ${skip.case} | — | — | — | — | — | — | unmeasured (${skip.reason.replace(/\|/g, '/')}); not a pass |`); continue; }
if (skip) { rows.push(`| ${skip.case} | — | — | — | — | unmeasured (${skip.reason.replace(/\|/g, '/')}); not a pass | — | — |`); continue; }
const ledger = readJson<Ledger>(path.join(dir, 'ledger.json'));
for (const round of ledger?.rounds ?? []) {
const m = readJson<Measurement>(path.join(dir, round, 'measurement.json'));
if (!m) continue;
const observed = m.kind === 'behavior' ? `${m.trialPasses}/${m.trials.length} trials, ${m.unitPasses}/${m.plan.units} panels` : `${m.unitPasses}/${m.trials.length}`;
const target = m.kind === 'behavior' ? `all ${m.plan.units} panels, ${m.plan.trialTarget}/${m.plan.trials}` : `${m.plan.unitTarget}/${m.plan.units}`;
const bar = m.counted > m.plan.trials ? m.plan.pooledBar : m.plan.bar;
const what = m.fix ? `${m.round}: after fix at ${m.fix.replace(/\|/g, '/')}` : m.round;
const status = m.status === 'measured' ? (m.meets ? 'at or above target' : 'below target') : m.status.replace('_', ' ');
rows.push(`| ${m.case} | ${m.kind} | ${what} | observed ${observed} | ${target} | ${usd(m.estimatedUsd)} | ${usd(m.actualUsd)}${m.costUnknownTrials ? ` (${m.costUnknownTrials} unknown)` : ''} | ${status} |`);
const batches = m.sets.length > 1 ? ` (${m.sets.map(s => `${s.set} ${s.passes}/${s.trials}${s.void ? ' void' : ''}`).join(', ')})` : '';
const decision = m.status === 'measured' ? DECISION_WORDS[m.decision] : m.status.replace('_', ' ');
rows.push(`| ${m.case} | ${m.kind} | ${what} | observed ${m.passes}/${m.counted}${batches} | ${bar.strict}/${bar.trials} strict, ${bar.qualified}/${bar.trials} qualified | ${decision} | ${usd(m.estimatedUsd)} | ${usd(m.actualUsd)}${m.costUnknownTrials ? ` (${m.costUnknownTrials} unknown)` : ''} |`);
if (m.status === 'measured' && m.decision === 'MEETS-qualified') qualified.push(`- ${m.case} ${m.round}: ${m.decisionReason.replace(/\|/g, '/')}`);
}
}
return [...rows, '', 'Diagnostic measurements: they never change a recorded verdict; the lane verdict is the full gate run.'].join('\n');
return [...rows, ...(qualified.length ? ['', 'Qualified MEETS (every red classified into a qualifying class):', ...qualified] : []),
'', 'Diagnostic measurements: they never change a recorded verdict; the lane verdict is the full gate run.'].join('\n');
}
// ─── Trial runners ─────────────────────────────────────────────────────────
@@ -398,7 +528,9 @@ export function caseSelectionPreflight(caseId: string, rootDir = ROOT): { ok: bo
const out = `${r.stdout ?? ''}${r.stderr ?? ''}`;
if (r.status !== 0) return { ok: false, detail: `--list exited ${r.status}: ${out.trim().split('\n').slice(-1)[0] ?? ''}` };
const file = new RegExp(`--case ${caseId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}: \\d+ trial\\(s\\) of (\\S+)`).exec(out)?.[1];
const listed = file && new RegExp(`^\\s+${file.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')} \\(trial 1\\)`, 'm').test(out);
const esc = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
// A file-mode case lists `<file> (trial 1)`; a name-mode case lists its trial shard `<file>#<id>~t1`.
const listed = file && new RegExp(`^\\s+${esc(file)}(?: \\(trial 1\\)|#${esc(caseId)}~t1)$`, 'm').test(out);
if (!file || !listed || !fs.existsSync(path.join(rootDir, file))) return { ok: false, detail: `--list did not plan a trial of ${caseId}'s test file` };
return { ok: true, detail: `--case ${caseId} selects ${file}` };
}
@@ -408,9 +540,11 @@ export function caseSelectionPreflight(caseId: string, rootDir = ROOT): { ok: bo
* and never publish: with the judge input cache enabled, a trial could reuse a
* stored pass, and its own passes would flow into the gate's reuse.
*/
export function diagnosticBaseEnv(base: NodeJS.ProcessEnv, evalDir: string): NodeJS.ProcessEnv {
export function diagnosticBaseEnv(base: NodeJS.ProcessEnv, evalDir: string, trialId: string): NodeJS.ProcessEnv {
const { EVALS_CACHE_DIR: _cacheDir, EVALS_CACHE_RUNTIME_ID: _cacheRuntime, ...inherited } = base;
return { ...inherited, GSTACK_EVAL_DIR: evalDir, GSTACK_SHIP_MEASURE_LABEL: 'diagnostic', EVALS_JOBS: '1' };
// Cases that retain native evidence (functional QA, docs faults) require EVALS_RUN_ID, which only CI sets;
// each trial gets its own so parallel trials never share an evidence directory.
return { ...inherited, GSTACK_EVAL_DIR: evalDir, GSTACK_SHIP_MEASURE_LABEL: 'diagnostic', EVALS_JOBS: '1', EVALS_RUN_ID: `${base.EVALS_RUN_ID || 'local'}-measure-${trialId}` };
}
/** True when some junit.xml under evalDir holds an executed, passing testcase and no failed or errored one. */
@@ -454,6 +588,7 @@ function parseTrialOutput(output: string): Partial<TrialResult> {
return {
...(field('failure_cause') ? { failureCause: field('failure_cause') } : {}),
...(field('failure_detail') ? { failureDetail: field('failure_detail') } : {}),
...(field('failure_evidence') ? { failureEvidence: field('failure_evidence') } : {}),
...(cost !== undefined && Number.isFinite(Number(cost)) ? { costUsd: Number(cost) } : {}),
...(samples ? { samples: samples.split(/[,\s]+/).filter(Boolean).map(s => s === '1' || s === 'pass' || s === 'true') } : {}),
...(/^\s*contract_violation\b/m.test(output) ? { contract: true } : {}),
@@ -481,6 +616,7 @@ export function commandRunner(template: string, cwd = process.cwd()): TrialRunne
...(parsed.costUsd === undefined && costOf(records) !== undefined ? { costUsd: costOf(records) } : {}),
...(code !== 0 && !parsed.failureCause && failed?.failure_cause ? { failureCause: String(failed.failure_cause) } : {}),
...(code !== 0 && !parsed.failureDetail && failed?.failure_detail ? { failureDetail: JSON.stringify(failed.failure_detail).slice(0, 300) } : {}),
...(code !== 0 && !parsed.failureEvidence && failed?.failure_cause_evidence ? { failureEvidence: String(failed.failure_cause_evidence).slice(0, 300) } : {}),
};
};
}
@@ -502,7 +638,7 @@ export function judgeFile(id: string, rootDir = ROOT): string {
* with the judge selected alone and passes only on its own passing record.
*/
export function gstackRunner(rootDir = ROOT): TrialRunner {
return async ({ caseId, dir }) => {
return async ({ caseId, round, trial, dir }) => {
const evalDir = path.join(dir, 'eval');
const isJudge = !Object.hasOwn(E2E_TIERS, caseId);
if (isJudge && !Object.hasOwn(LLM_JUDGE_TOUCHFILES, caseId)) throw new Error(`${caseId} is neither an E2E case nor a standalone judge`);
@@ -510,7 +646,7 @@ export function gstackRunner(rootDir = ROOT): TrialRunner {
? [process.execPath, 'test', path.join(rootDir, judgeFile(caseId, rootDir))]
: [process.execPath, 'run', path.join(rootDir, 'scripts/test-paid-shards.ts'), '--tier', E2E_TIERS[caseId]!, '--case', caseId, '--trials', '1'];
const env = {
...diagnosticBaseEnv(process.env, evalDir),
...diagnosticBaseEnv(process.env, evalDir, `${caseSlug(caseId)}-${round}-t${trial}-${process.pid}`),
...(isJudge ? { EVALS: '1', EVALS_TIER: 'gate', EVALS_ALL: '1', ...paidSelectionEnv('full', { e2e: [], judges: [caseId] }, 'ship-measure judge') } : {}),
};
const { code, output } = await runTrialProcess(argv, rootDir, env, dir);
@@ -526,6 +662,7 @@ export function gstackRunner(rootDir = ROOT): TrialRunner {
...(own.some(r => r.failure_class === 'contract') ? { contract: true } : {}),
...(passed ? {} : {
failureCause: failed?.failure_cause ? String(failed.failure_cause) : own.length ? 'assertion' : 'unknown',
...(failed?.failure_cause_evidence ? { failureEvidence: String(failed.failure_cause_evidence).slice(0, 300) } : {}),
failureDetail: failed?.failure_detail ? JSON.stringify(failed.failure_detail).slice(0, 300)
: own.length ? String(failed?.error ?? '').slice(0, 300) : `no trial record; exit ${code}; ${output.trim().split('\n').slice(-1)[0] ?? ''}`.slice(0, 300),
}),
@@ -627,10 +764,11 @@ function positive(raw: string | undefined, flag: string, integer = true): number
export async function main(argv: string[], env: NodeJS.ProcessEnv = process.env): Promise<number> {
const [command, ...rest] = argv;
if (command === 'sweep') return (await import('./ship-measure-sweep')).sweepMain(rest, env);
const flags: Record<string, string> = {};
const value = (name: string) => (next: () => string | undefined) => { const v = next(); if (v === undefined) throw new Error(`${name} needs a value`); flags[name] = v; };
const handlers = Object.fromEntries(['--case', '--round', '--kind', '--command', '--cost-per-trial', '--fix', '--jobs', '--out', '--reason',
'--files', '--shard', '--reruns', '--concurrency', '--backend', '--wall-cap'].map(name => [name, value(name)]));
'--files', '--shard', '--reruns', '--concurrency', '--backend', '--wall-cap', '--trials', '--class', '--evidence'].map(name => [name, value(name)]));
parseCliFlags(rest, { ...handlers, '--approved': () => { flags['--approved'] = '1'; } }, 'Usage: see the header of scripts/ship-measure.ts');
const config = readMeasureConfig();
const outDir = path.resolve(flags['--out'] ?? path.join(process.cwd(), '.context', 'ship-measure'));
@@ -638,29 +776,43 @@ export async function main(argv: string[], env: NodeJS.ProcessEnv = process.env)
if (command === 'report') { console.log(formatReport(outDir)); return 0; }
if (command === 'skip') { console.log(`[ship-measure] ${flags['--case']} labeled unmeasured: ${recordUnmeasured(outDir, flags['--case'] ?? '', flags['--reason'] ?? '')}`); return 0; }
if (command === 'free') return runFreeCli(flags, config, outDir, env);
if (command !== 'measure') throw new Error(`Unknown command: ${command ?? '(none)'}. Use table, measure, skip, report or free.`);
if (command !== 'measure' && command !== 'classify' && command !== 'decide' && command !== 'extend') {
throw new Error(`Unknown command: ${command ?? '(none)'}. Use table, measure, classify, decide, extend, skip, report, free or sweep.`);
}
const caseId = flags['--case'];
if (!caseId) throw new Error('measure needs --case');
const kind = (flags['--kind'] ?? E2E_KINDS[caseId] ?? 'rule') as MeasureKind;
if (!['rule', 'behavior', 'judge'].includes(kind)) throw new Error(`--kind must be rule, behavior or judge. Received: ${kind}`);
if (!caseId) throw new Error(`${command} needs --case`);
const round = flags['--round'] ?? 'baseline';
if (command === 'classify') {
const trials = (flags['--trials'] ?? '').split(',').map(t => t.trim().replace(/^t/, '')).filter(Boolean).map(t => positive(t, '--trials'));
if (!trials.length || !flags['--class']) throw new Error(`classify needs --trials 2,5 and --class (${FAILURE_CLASSES.join(', ')})`);
return measurementExit(classifyTrials(outDir, caseId, round, trials, flags['--class'], flags['--evidence'] ?? ''));
}
if (command === 'decide') return measurementExit(decideRound(outDir, caseId, round));
const isGstack = path.resolve(process.cwd()) === ROOT;
if (!flags['--command'] && !isGstack) throw new Error('No single-case eval command: pass --command \'<documented command> {case}\' (ask the user once and record the answer).');
const runner = flags['--command'] ? commandRunner(flags['--command']) : gstackRunner();
const runnerId = flags['--command'] ? `command:${flags['--command']}` : 'gstack';
const parallel = flags['--jobs'] ? positive(flags['--jobs'], '--jobs') : Number(env.EVALS_JOBS) || DEFAULT_JOBS;
const costPerTrialUsd = flags['--cost-per-trial'] !== undefined ? positive(flags['--cost-per-trial'], '--cost-per-trial', false) : null;
if (command === 'extend') {
return measurementExit(await extendCase({ caseId, round, config, outDir, runner, parallel, ...(costPerTrialUsd !== null ? { costPerTrialUsd } : {}) }));
}
const kind = (flags['--kind'] ?? E2E_KINDS[caseId] ?? 'rule') as MeasureKind;
if (!['rule', 'behavior', 'judge'].includes(kind)) throw new Error(`--kind must be rule, behavior or judge. Received: ${kind}`);
console.log(formatKindTable(config));
if (!flags['--command']) {
const selected = caseSelectionPreflight(caseId);
if (!selected.ok) throw new Error(`--case ${caseId} does not select its test (no paid call made): ${selected.detail}`);
console.log(`[ship-measure] preflight: ${selected.detail}`);
}
const plan = kindPlan(kind, config);
const m = await measureCase({
caseId, kind, round: flags['--round'] ?? 'baseline', config, outDir, fix: flags['--fix'],
runner: flags['--command'] ? commandRunner(flags['--command']) : gstackRunner(),
parallel: flags['--jobs'] ? positive(flags['--jobs'], '--jobs') : Number(env.EVALS_JOBS) || DEFAULT_JOBS,
costPerTrialUsd: flags['--cost-per-trial'] !== undefined ? positive(flags['--cost-per-trial'], '--cost-per-trial', false) : null,
approved: flags['--approved'] === '1',
caseId, kind, round, config, outDir, fix: flags['--fix'], runner, parallel, costPerTrialUsd, approved: flags['--approved'] === '1',
identity: () => measurementIdentity(kind, plan.trials, runnerId),
});
if (m.status === 'needs_approval') { console.log(`[ship-measure] NEEDS APPROVAL ${caseId}: ${m.reason}`); return 2; }
if (m.status !== 'measured') { console.log(`[ship-measure] NAMED RED ${caseId}: ${m.reason}`); return 3; }
return m.meets ? 0 : 1;
if (m.status === 'needs_approval') console.log(`[ship-measure] NEEDS APPROVAL ${caseId}: ${m.reason}`);
else if (m.status !== 'measured' || m.next === 'stop') console.log(`[ship-measure] NAMED RED ${caseId}: ${m.reason ?? m.decisionReason}`);
return measurementExit(m);
}
async function runFreeCli(flags: Record<string, string>, config: MeasureConfig, outDir: string, env: NodeJS.ProcessEnv): Promise<number> {
+3
View File
@@ -234,6 +234,9 @@ export const FREE_ONLY_PR_FILES = [
'.github/workflows/scorecard.yml',
'.github/workflows/skill-docs.yml',
'.github/workflows/test-health.yml',
'.github/workflows/eval-sweep.yml', // The weekly off-ship sweep on main; no PR-gate case reads it, and it never gates a PR.
'.github/actionlint.yaml', // Runner labels for actionlint only.
'scripts/ship-measure-sweep.ts', // The weekly sweep CLI; it launches paid cases, never runs inside one.
'.github/workflows/version-gate.yml',
'.github/workflows/windows-free-tests.yml',
'.github/workflows/windows-setup-e2e.yml',
+27 -18
View File
@@ -28,26 +28,35 @@ completed at the cap) and one failure capture, then continue the triage.
before any paid trial. A red that is infrastructure (provider outage, runner
loss) may skip measurement: `ship-measure skip --case <id> --reason "<why>"`
labels it `unmeasured` in the PR body. It never counts as a pass.
2. **Show the plan.** Print `ship-measure table` (trials, pass bar, panels and
budget per kind, from the `ship_measure_*` gstack config keys) and the
estimate: per-trial cost from pass-rate history times trials, or "estimate unknown".
2. **Show the plan.** Print `ship-measure table` (trials, bar and budget per
kind, from the `ship_measure_*` gstack config keys) and the estimate:
per-trial cost from pass-rate history times trials, or "estimate unknown".
3. **Measure.** `ship-measure measure --case <id> --round baseline [--cost-per-trial <usd>]`
runs the case alone on the current head at its kind's count, in parallel. It
uses the project's documented single-case eval command,
`--command '<command> {case}'`, found the way Step 4 finds test commands
(CLAUDE.md/AGENTS.md, then package scripts). gstack's default needs no
`--command`: `scripts/test-paid-shards.ts --tier <tier> --case <id>`, and it
accepts standalone judge ids. With no documented single-case command, ask
once and record the answer in the report. Exit 2 means it needs approval:
ask once with the reason it printed, then rerun with `--approved`.
4. **Fix at the cause.** Root-cause the failed trials from their captures in
runs the case alone on the current head at its kind's count, in parallel,
through the project's documented single-case eval command,
`--command '<command> {case}'` (found the way Step 4 finds test commands;
gstack needs none and accepts judge ids). With no documented command, ask
once and record the answer in the report. Exit 2 needs approval: ask once
with its reason, then rerun with `--approved`.
4. **Decide by the exit code** (MEETS 9/10, MEETS-qualified 8/10 with every
red qualifying, EXTEND 7/10; behavior 11/12, 10/12, 9/12):
- 0: MEETS or MEETS-qualified; done.
- 5: needs-classify. Record each listed red from its capture:
`ship-measure classify --case <id> --round <r> --trials <2,5> --class <class> --evidence "<path:line>"`.
Qualifying: `provider` (only on the trial's own provider error),
`judge-noise`, `model-miss` (cite the line). Fix round: `timeout`,
`hang`, `regression`, `fixable`. `unclassified`: reviewed, no evidence.
- 6: EXTEND. `ship-measure extend --case <id> --round <r>` once, on the
unchanged tree; never a third batch.
- 1: BELOW, a fix round.
5. **Fix at the cause.** Root-cause the failed trials from their captures in
`.context/ship-measure/<case>/<round>/tNN/` and change the cause.
5. **Re-measure.** `ship-measure measure --case <id> --round round-<n> --fix "<cause and change>"`.
Exit 0 is at or above target. Exit 1 is below target: return to step 4. Call
a case fixed only with a named causal change and every failure in the closing
measurement explained; report "observed k/n after fix at <cause>", never a proven rate.
6. **Stop.** Exit 3 is a named red (repair-round limit or the per-case budget
exhausted): stop, do not push, and report `ship-measure report` with every trial.
6. **Re-measure.** `ship-measure measure --case <id> --round round-<n> --fix "<cause and change>"`,
then item 4. Call a case fixed only with a named causal change and every
failure in the closing measurement explained;
report "observed k/n after fix at <cause>", never a proven rate.
7. **Stop.** Exit 3 is a named red (round limit, budget, or two void
batches): stop, do not push, and report `ship-measure report` with every trial.
**Gate once.** When every red case measures at or above target, run the full
gate once (Step 6 item 2). When the project's CI runs that gate on push (gstack:
+27 -18
View File
@@ -26,26 +26,35 @@ completed at the cap) and one failure capture, then continue the triage.
before any paid trial. A red that is infrastructure (provider outage, runner
loss) may skip measurement: `ship-measure skip --case <id> --reason "<why>"`
labels it `unmeasured` in the PR body. It never counts as a pass.
2. **Show the plan.** Print `ship-measure table` (trials, pass bar, panels and
budget per kind, from the `ship_measure_*` gstack config keys) and the
estimate: per-trial cost from pass-rate history times trials, or "estimate unknown".
2. **Show the plan.** Print `ship-measure table` (trials, bar and budget per
kind, from the `ship_measure_*` gstack config keys) and the estimate:
per-trial cost from pass-rate history times trials, or "estimate unknown".
3. **Measure.** `ship-measure measure --case <id> --round baseline [--cost-per-trial <usd>]`
runs the case alone on the current head at its kind's count, in parallel. It
uses the project's documented single-case eval command,
`--command '<command> {case}'`, found the way Step 4 finds test commands
(CLAUDE.md/AGENTS.md, then package scripts). gstack's default needs no
`--command`: `scripts/test-paid-shards.ts --tier <tier> --case <id>`, and it
accepts standalone judge ids. With no documented single-case command, ask
once and record the answer in the report. Exit 2 means it needs approval:
ask once with the reason it printed, then rerun with `--approved`.
4. **Fix at the cause.** Root-cause the failed trials from their captures in
runs the case alone on the current head at its kind's count, in parallel,
through the project's documented single-case eval command,
`--command '<command> {case}'` (found the way Step 4 finds test commands;
gstack needs none and accepts judge ids). With no documented command, ask
once and record the answer in the report. Exit 2 needs approval: ask once
with its reason, then rerun with `--approved`.
4. **Decide by the exit code** (MEETS 9/10, MEETS-qualified 8/10 with every
red qualifying, EXTEND 7/10; behavior 11/12, 10/12, 9/12):
- 0: MEETS or MEETS-qualified; done.
- 5: needs-classify. Record each listed red from its capture:
`ship-measure classify --case <id> --round <r> --trials <2,5> --class <class> --evidence "<path:line>"`.
Qualifying: `provider` (only on the trial's own provider error),
`judge-noise`, `model-miss` (cite the line). Fix round: `timeout`,
`hang`, `regression`, `fixable`. `unclassified`: reviewed, no evidence.
- 6: EXTEND. `ship-measure extend --case <id> --round <r>` once, on the
unchanged tree; never a third batch.
- 1: BELOW, a fix round.
5. **Fix at the cause.** Root-cause the failed trials from their captures in
`.context/ship-measure/<case>/<round>/tNN/` and change the cause.
5. **Re-measure.** `ship-measure measure --case <id> --round round-<n> --fix "<cause and change>"`.
Exit 0 is at or above target. Exit 1 is below target: return to step 4. Call
a case fixed only with a named causal change and every failure in the closing
measurement explained; report "observed k/n after fix at <cause>", never a proven rate.
6. **Stop.** Exit 3 is a named red (repair-round limit or the per-case budget
exhausted): stop, do not push, and report `ship-measure report` with every trial.
6. **Re-measure.** `ship-measure measure --case <id> --round round-<n> --fix "<cause and change>"`,
then item 4. Call a case fixed only with a named causal change and every
failure in the closing measurement explained;
report "observed k/n after fix at <cause>", never a proven rate.
7. **Stop.** Exit 3 is a named red (round limit, budget, or two void
batches): stop, do not push, and report `ship-measure report` with every trial.
**Gate once.** When every red case measures at or above target, run the full
gate once (Step 6 item 2). When the project's CI runs that gate on push (gstack:
+2 -1
View File
@@ -13,7 +13,8 @@ import * as fs from 'node:fs';
import * as path from 'node:path';
const ROOT = path.resolve(__dirname, '..');
const FILES = ['evals.yml', 'evals-periodic.yml', 'evals-marathon.yml'];
// eval-sweep.yml only resolves the tag (it never builds), so it must look up the same one.
const FILES = ['evals.yml', 'evals-periodic.yml', 'evals-marathon.yml', 'eval-sweep.yml'];
function hashFilesCalls(name: string): string[] {
const source = fs.readFileSync(
+76
View File
@@ -0,0 +1,76 @@
/**
* Weekly off-ship sweep workflow wiring (docs/TESTING_INTERNALS.md#ship-measure-sweep):
* weekly after the periodic census plus dispatch, read-only permissions (it
* never pushes or opens a pull request), one sweep at a time repository-wide
* so two runs never spend the same weekly cap, the CI image by digest with the
* census's paid-eval setup, earlier reports downloaded before any spend,
* dispatch inputs reaching the shell only through env, and the report uploaded
* even when the sweep fails.
*/
import { describe, expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as path from 'node:path';
const ROOT = path.resolve(import.meta.dir, '..');
const source = fs.readFileSync(path.join(ROOT, '.github/workflows/eval-sweep.yml'), 'utf8');
const workflow = Bun.YAML.parse(source) as any;
const sweep = workflow.jobs.sweep;
const step = (name: string) => sweep.steps.find((s: any) => s.name === name);
describe('eval-sweep.yml', () => {
test('runs weekly after the 06:00 UTC periodic census, plus dispatch with k, cap and dry-run inputs', () => {
expect(Object.keys(workflow.on).sort()).toEqual(['schedule', 'workflow_dispatch']);
const [hour] = /cron: '0 (\d+) \* \* 1'/.exec(source)!.slice(1).map(Number);
const census = Number(/cron: '0 (\d+) \* \* 1'/.exec(fs.readFileSync(path.join(ROOT, '.github/workflows/evals-periodic.yml'), 'utf8'))![1]);
expect(hour!).toBeGreaterThan(census);
expect(Object.keys(workflow.on.workflow_dispatch.inputs).sort()).toEqual(['cap_usd', 'dry_run', 'k']);
});
test('never pushes, commits or opens a pull request: read-only grants, no write commands', () => {
expect(workflow.permissions).toEqual({ contents: 'read' });
for (const job of Object.values<any>(workflow.jobs)) {
for (const value of Object.values<string>(job.permissions ?? {})) expect(value).toBe('read');
}
expect(source).not.toMatch(/git push|git commit|gh pr |gh issue|pull-requests:|contents: write/);
for (const s of sweep.steps) if (s.uses?.startsWith('actions/checkout')) expect(s.with['persist-credentials']).toBe(false);
});
test('one sweep at a time across every ref, never cancelled mid-spend', () => {
expect(workflow.concurrency).toEqual({ group: 'eval-sweep', 'cancel-in-progress': false });
});
test('runs in the published CI image by digest with the census paid-eval setup', () => {
expect(sweep.container.image).toBe('${{ needs.image.outputs.image-ref }}');
expect(sweep.container.options).toBe('--user runner');
expect(workflow.jobs.image.steps.map((s: any) => s.name ?? s.uses)).toContain('Resolve the published CI image digest');
for (const name of ['Seed claude interactive config', 'Register gstack skills for PTY tests', 'Authenticate the Codex CLI']) expect(step(name)).toBeDefined();
const run = step('Run the sweep');
expect(run.env.ANTHROPIC_API_KEY).toBe('${{ secrets.ANTHROPIC_API_KEY }}');
expect(run.env.GSTACK_CI_IMAGE).toBe('${{ needs.image.outputs.image-ref }}');
});
test('downloads the last 7 days of sweep reports before the sweep and passes them as --prior', () => {
const names = sweep.steps.map((s: any) => s.name);
expect(names.indexOf("Download this week's earlier sweep reports")).toBeLessThan(names.indexOf('Run the sweep'));
const download = step("Download this week's earlier sweep reports");
expect(download.run).toContain("date -u -d '7 days ago'");
expect(download.run).toContain('--name ship-measure-sweep-report');
expect(download.run).toContain('set -euo pipefail');
expect(step('Run the sweep').run).toContain('--prior "$SWEEP_PRIOR"');
});
test('dispatch inputs reach the shell only through env', () => {
const run = step('Run the sweep');
expect(run.run).not.toContain('${{');
expect(run.env).toMatchObject({ SWEEP_K: '${{ inputs.k }}', SWEEP_CAP_USD: '${{ inputs.cap_usd }}', SWEEP_DRY_RUN: '${{ inputs.dry_run }}' });
expect(run.run).toContain('bun run scripts/ship-measure.ts sweep "${args[@]}"');
});
test('uploads the report and captures even when the sweep fails', () => {
const report = step('Upload the sweep report');
expect(report.if).toBe('always()');
expect(report.with.name).toBe('ship-measure-sweep-report');
expect(report.with.path).toContain('sweep-report.md');
expect(step('Upload the sweep captures').if).toBe('always()');
});
});
+2
View File
@@ -2,6 +2,8 @@
"newModules": [
{ "file": "scripts/lib/shard-engine.ts", "movedFrom": "scripts/test-strict-output.ts" },
{ "file": "scripts/lib/eval-history.ts", "movedFrom": "(new: headroom, red ledger and run triage, kept out of scripts/eval-flake-rank.ts)" },
{ "file": "scripts/lib/measure-bar.ts", "movedFrom": "(new: the ship-measure measurement bar, kept out of scripts/ship-measure.ts)" },
{ "file": "scripts/ship-measure-sweep.ts", "movedFrom": "(new: the weekly off-ship qualification sweep, kept out of scripts/ship-measure.ts)" },
{ "file": "scripts/lib/published-text.ts", "movedFrom": "(new: one sanitizer for model-written text reaching public reports)" },
{ "file": "test/helpers/session-ledger.ts", "movedFrom": "(new: per-session ledger every paid runner appends to)" },
{ "file": "scripts/lib/paid-types.ts", "movedFrom": "scripts/test-paid-shards.ts" },
+1 -1
View File
@@ -166,7 +166,7 @@ export const CARVE_GUARDS: Record<string, CarveGuard> = {
// wave's headline capability) grows the union to 1.195x. Deliberate:
// the section is on-demand (loads only for Apple store targets), so
// per-invocation cost for non-iOS ships is one manifest line.
maxSizeRatio: 1.495, // Shared advisory identity/dedup + critical-severity validation: 248,065 union bytes / 187,706 baseline = 1.3216 (2026-09-17). + test value bar in the lazy Step 7 section (value cards, weak paths, gate table, base control, machine checks; ~13.6KB): measured 1.396 (2026-09-29). + W1 guarded state-root resolution (`eval gstack-paths; : "${GSTACK_STATE_ROOT:?…}"`) in the Context Recovery preamble, the eureka log and each state-writing bash block; measured 1.401 (2026-09-30). + the shared QA review step's plan-check timing rule (plan checks and their revalidation run on --timeout-ms after smoke expiry); measured 1.4022 (2026-09-30). + the shared TEST_BOOTSTRAP owned-change undo rules (s03 H1-H3, M1) and the value-bar coverage wording (s03 M2/M3); measured 1.4044 (2026-10-02). + severe fix wave ship-review lane: G1 NO_VERSION dispatch, title and CHANGELOG arms (versionless repos ship without inventing a version), F1 parent-side plan binding before the audit dispatch (binding, docs/designs candidates, exact not-run line) and D3's data-not-instructions line; +2,126 union bytes, measured 1.4135 (2026-10-03). + INV-1 consumer lines (outside review `unverified`/`unavailable` is missing coverage in the readiness note and PR body); measured 1.4148 (2026-10-03). + v1.91.19.0 severe fix wave: verdict-form outside-review callers with sandbox preflight and first-use notice (INV-1/B1/Q2), P0/P1 gate and unverified wording, bound-plan discovery (F1), visible LEARNINGS failures (B5), carved external pointers; measured 267,752 / 187,706 = 1.4264 (2026-10-03). + v1.91.18.0 (#3023) merged into the v1.91.19.0 wave; measured 267,874 / 187,706 = 1.4271 (2026-10-04). + Oct 6 fix wave: quota_exhausted preflight branch (#3051) and quoted-heredoc issue template (#3046); measured 268,199 / 187,706 = 1.4288 (2026-10-06). + Step 10's inline no-PR check before the Greptile dispatch (#3020); measured 268,655 / 187,706 = 1.4313 (2026-10-06). + Oct 6 fix wave A3/CEO-12: free text reaches commands only through agent-written mktemp files (mktemp block, write rule, path rebuild and refusal at each posting, prompt and brief site); measured 275,422 / 187,706 = 1.4673 (2026-10-06). Combined Oct 6 wave head (B free-text files + D measure loop + A rework): measured 280,363 / 187,706 = 1.4936 (2026-10-06). + gate-once push rule (no second paid gate after a CI push); measured 280,573 / 187,706 = 1.4947 (2026-10-06).
maxSizeRatio: 1.4975, // Shared advisory identity/dedup + critical-severity validation: 248,065 union bytes / 187,706 baseline = 1.3216 (2026-09-17). + test value bar in the lazy Step 7 section (value cards, weak paths, gate table, base control, machine checks; ~13.6KB): measured 1.396 (2026-09-29). + W1 guarded state-root resolution (`eval gstack-paths; : "${GSTACK_STATE_ROOT:?…}"`) in the Context Recovery preamble, the eureka log and each state-writing bash block; measured 1.401 (2026-09-30). + the shared QA review step's plan-check timing rule (plan checks and their revalidation run on --timeout-ms after smoke expiry); measured 1.4022 (2026-09-30). + the shared TEST_BOOTSTRAP owned-change undo rules (s03 H1-H3, M1) and the value-bar coverage wording (s03 M2/M3); measured 1.4044 (2026-10-02). + severe fix wave ship-review lane: G1 NO_VERSION dispatch, title and CHANGELOG arms (versionless repos ship without inventing a version), F1 parent-side plan binding before the audit dispatch (binding, docs/designs candidates, exact not-run line) and D3's data-not-instructions line; +2,126 union bytes, measured 1.4135 (2026-10-03). + INV-1 consumer lines (outside review `unverified`/`unavailable` is missing coverage in the readiness note and PR body); measured 1.4148 (2026-10-03). + v1.91.19.0 severe fix wave: verdict-form outside-review callers with sandbox preflight and first-use notice (INV-1/B1/Q2), P0/P1 gate and unverified wording, bound-plan discovery (F1), visible LEARNINGS failures (B5), carved external pointers; measured 267,752 / 187,706 = 1.4264 (2026-10-03). + v1.91.18.0 (#3023) merged into the v1.91.19.0 wave; measured 267,874 / 187,706 = 1.4271 (2026-10-04). + Oct 6 fix wave: quota_exhausted preflight branch (#3051) and quoted-heredoc issue template (#3046); measured 268,199 / 187,706 = 1.4288 (2026-10-06). + Step 10's inline no-PR check before the Greptile dispatch (#3020); measured 268,655 / 187,706 = 1.4313 (2026-10-06). + Oct 6 fix wave A3/CEO-12: free text reaches commands only through agent-written mktemp files (mktemp block, write rule, path rebuild and refusal at each posting, prompt and brief site); measured 275,422 / 187,706 = 1.4673 (2026-10-06). Combined Oct 6 wave head (B free-text files + D measure loop + A rework): measured 280,363 / 187,706 = 1.4936 (2026-10-06). + gate-once push rule (no second paid gate after a CI push); measured 280,573 / 187,706 = 1.4947 (2026-10-06). + the measurement bar's decision step (exit-code branches for MEETS, MEETS-qualified, needs-classify with the qualifying and fix-round classes, EXTEND once; the measure prose was tightened to offset most of it); measured 281,058 / 187,706 = 1.4973 (2026-10-06).
},
'plan-ceo-review': {
skill: 'plan-ceo-review',
+1 -1
View File
@@ -389,7 +389,7 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
'tpa-apple-ban': [ 'scripts/resolvers/third-party-actions.ts', 'ship/SKILL.md.tmpl', 'ship/sections/apple-release.md.tmpl', 'scripts/gen-skill-docs.ts', 'test/helpers/session-runner.ts', 'test/skill-e2e-third-party-actions.test.ts', 'test/helpers/third-party-actions.ts',
'lib/eval-model.ts'
],
'ship-measure-seeded-flake': ['ship/**', 'scripts/ship-measure.ts', 'scripts/test-free-shards.ts', 'scripts/lib/shard-engine.ts', 'scripts/lib/paid-select.ts', 'scripts/lib/free-home-guard.ts', 'bin/gstack-config', 'bin/gstack-state-root.sh', 'lib/state-root.ts', 'scripts/resolvers/sections.ts', 'scripts/resolvers/testing.ts', 'scripts/gen-skill-docs.ts', 'test/skill-e2e-ship-measure-loop.test.ts', 'test/helpers/ship-measure-seeded-fixture.ts', 'test/helpers/agent-sdk-runner.ts', 'test/helpers/skill-fixture.ts'],
'ship-measure-seeded-flake': ['ship/**', 'scripts/ship-measure.ts', 'scripts/lib/measure-bar.ts', 'scripts/test-free-shards.ts', 'scripts/lib/shard-engine.ts', 'scripts/lib/paid-select.ts', 'scripts/lib/free-home-guard.ts', 'bin/gstack-config', 'bin/gstack-state-root.sh', 'lib/state-root.ts', 'scripts/resolvers/sections.ts', 'scripts/resolvers/testing.ts', 'scripts/gen-skill-docs.ts', 'test/skill-e2e-ship-measure-loop.test.ts', 'test/helpers/ship-measure-seeded-fixture.ts', 'test/helpers/agent-sdk-runner.ts', 'test/helpers/skill-fixture.ts'],
'ship-section-loading': [ 'ship/**', 'scripts/resolvers/sections.ts', 'scripts/gen-skill-docs.ts', 'test/helpers/auq-sdk-capture.ts', 'test/helpers/session-runner.ts', 'test/skill-e2e-ship-section-loading.test.ts',
'scripts/resolvers/testing.ts', 'test/helpers/agent-sdk-runner.ts', 'test/helpers/auq-native-capture.ts', 'test/helpers/auto-decision-state.ts', 'test/helpers/autoplan-artifact-digest.ts', 'test/helpers/autoplan-artifact-permission.ts', 'test/helpers/autoplan-artifact-recorder.ts', 'test/helpers/claude-pty-runner.ts', 'test/helpers/pty/**', 'test/helpers/dx-selected-navigation.ts', 'test/helpers/e2e-gate.ts', 'test/helpers/eng-cache-writer-decision.ts', 'test/helpers/hermetic-skill-runtime.ts', 'test/helpers/llm-judge.ts', 'test/helpers/native-auto-decide.ts', 'test/helpers/owned-claude-transcript.ts', 'test/helpers/plan-count-artifacts.ts', 'test/helpers/plan-count-file-permission.ts', 'test/helpers/plan-count-fixture.ts', 'test/helpers/plan-count-pending-exit.ts', 'test/helpers/plan-count-pending-question.ts', 'test/helpers/plan-count-transcript.ts', 'test/helpers/plan-floor-review.ts', 'test/helpers/plan-floor-target.ts', 'test/helpers/plan-scope-selection.ts', 'test/helpers/plan-seed-submission.ts', 'test/helpers/plan-skill-question-events.ts', 'test/helpers/plan-skill-question-hook-scope.ts', 'test/helpers/plan-skill-questions.ts', 'test/helpers/pty-screen.ts', 'test/helpers/pty-trust-dialog.ts', 'test/helpers/skill-census.ts'],
'plan-ceo-section-loading': [
+238
View File
@@ -0,0 +1,238 @@
/**
* The measurement bar's decision table (scripts/lib/measure-bar.ts), free and
* deterministic, plus the runner paths that apply it (void redispatch, EXTEND
* on identical inputs, classification records) with fake trial runners.
* Rules (approved 2026-10-06): MEETS >= 9/10; MEETS-qualified 8/10 with every
* red qualifying (provider on affirmative evidence, judge noise, a cited model
* miss); EXTEND 7/10 or an unqualified 8/10, then one pooled decision on 20
* (18 strict, 16 qualified); BELOW otherwise. Behavior uses 12 (11, 10, 9;
* pooled 22 and 20 of 24); a judge trial is one output scored by its median
* 3-sample panel. More than 30% provider-evidence failures voids a batch, which
* may be redispatched once.
*/
import { afterAll, describe, expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import {
barThresholds, classificationProblem, decide, identityMismatch, machineClass, providerEvidence,
type BarKind, type BarTrial, type Classification, type FailureClass, type TrialSet,
} from '../scripts/lib/measure-bar';
import { MEASURE_DEFAULTS, classifyTrials, decideRound, extendCase, measureCase, type TrialRequest, type TrialResult } from '../scripts/ship-measure';
const temps: string[] = [];
const tmp = (prefix: string) => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); temps.push(dir); return dir; };
afterAll(() => { for (const dir of temps) fs.rmSync(dir, { recursive: true, force: true }); });
const ASSERTION = { failureCause: 'assertion', failureDetail: 'expected sorted' };
const API = { failureCause: 'api_error', failureEvidence: 'API Error: 529 overloaded' };
const TIMEOUT = { failureCause: 'session_timeout', failureEvidence: 'armed 300s timeout fired' };
/** A batch of n trials in one set; `fails` maps a 1-based slot to that trial's failure fields. */
function batch(set: TrialSet, n: number, fails: Record<number, Partial<BarTrial>> = {}, start = 0): BarTrial[] {
return Array.from({ length: n }, (_, i) => {
const failure = fails[i + 1];
return { trial: start + i + 1, set, passed: !failure, ...(failure ?? {}) };
});
}
const agent = (trial: number, cls: FailureClass, evidence = 'test/x.test.ts:12 read the capture'): Classification => ({ trial, class: cls, evidence, by: 'agent', at: '2026-10-06T00:00:00Z' });
const fails = (slots: number[], fields: Partial<BarTrial> = ASSERTION) => Object.fromEntries(slots.map(s => [s, fields]));
describe('thresholds round up from 9/10, 8/10 and 7/10', () => {
test('N = 10, 12, 20 and 24', () => {
expect(barThresholds(10)).toEqual({ trials: 10, strict: 9, qualified: 8, floor: 7 });
expect(barThresholds(12)).toEqual({ trials: 12, strict: 11, qualified: 10, floor: 9 });
expect(barThresholds(20)).toMatchObject({ strict: 18, qualified: 16 });
expect(barThresholds(24)).toMatchObject({ strict: 22, qualified: 20 });
});
});
describe('one batch of 10 (rule)', () => {
const d = (trials: BarTrial[], records: Classification[] = [], kind: BarKind = 'rule') => decide(kind, kind === 'behavior' ? 12 : 10, trials, records);
test('9/10 and 10/10 MEETS with no classification', () => {
expect(d(batch('initial', 10, fails([4])))).toMatchObject({ decision: 'MEETS', next: 'none', passes: 9 });
expect(d(batch('initial', 10))).toMatchObject({ decision: 'MEETS', passes: 10 });
});
test('a contract violation is never MEETS, even at 10/10 assertions passed', () => {
const trials = batch('initial', 10).map(t => t.trial === 3 ? { ...t, contract: true } : t);
expect(d(trials)).toMatchObject({ decision: 'BELOW', next: 'fix' });
});
test('8/10 with only qualifying reds is MEETS-qualified: machine provider, judge noise, a cited model miss', () => {
expect(d(batch('initial', 10, fails([2, 5], API)))).toMatchObject({ decision: 'MEETS-qualified', needsClassify: [] });
const trials = batch('initial', 10, fails([2, 5]));
expect(d(trials, [agent(2, 'judge-noise', 'median 3 at threshold 4'), agent(5, 'model-miss', 'transcript.jsonl:88 skipped the sort step')]).decision).toBe('MEETS-qualified');
});
test('a sub-strict batch with unreviewed reds is needs-classify, listing exactly those trials', () => {
const out = d(batch('initial', 10, fails([2, 5])), [agent(2, 'judge-noise', 'at threshold')]);
expect(out).toMatchObject({ decision: 'needs-classify', next: 'classify', needsClassify: [5] });
expect(d(batch('initial', 10, fails([1, 2, 3]))).decision).toBe('needs-classify');
});
test('8/10 with a timeout, a hang, a regression or a known fixable cause is a fix round', () => {
expect(d(batch('initial', 10, { 2: TIMEOUT, 5: API })).decision).toBe('BELOW');
expect(d(batch('initial', 10, { 2: { failureCause: 'provider_stall' }, 5: API })).decision).toBe('BELOW');
for (const cls of ['regression', 'fixable', 'timeout', 'hang'] as const) {
expect(d(batch('initial', 10, fails([2, 5])), [agent(2, cls), agent(5, 'judge-noise')])).toMatchObject({ decision: 'BELOW', next: 'fix' });
}
});
test('8/10 with a red reviewed as unclassified, or a model miss without a citation, is EXTEND', () => {
expect(d(batch('initial', 10, fails([2, 5])), [agent(2, 'unclassified', 'reviewed: no evidence for any route'), agent(5, 'judge-noise')]))
.toMatchObject({ decision: 'EXTEND', next: 'extend' });
expect(d(batch('initial', 10, fails([2, 5])), [agent(2, 'model-miss', 'the model went off script'), agent(5, 'judge-noise')]).decision).toBe('EXTEND');
});
test('7/10 EXTEND when every red qualifies or is reviewed; 7/10 with a fixable red is a fix round; 6/10 BELOW without classification', () => {
expect(d(batch('initial', 10, fails([1, 2, 3], API))).decision).toBe('EXTEND');
expect(d(batch('initial', 10, fails([1, 2, 3])), [agent(1, 'judge-noise'), agent(2, 'judge-noise'), agent(3, 'fixable')]).decision).toBe('BELOW');
expect(d(batch('initial', 10, fails([1, 2, 3, 4])))).toMatchObject({ decision: 'BELOW', needsClassify: [] });
});
test('a later classification record supersedes an earlier one for the same trial', () => {
const trials = batch('initial', 10, fails([2, 5]));
expect(d(trials, [agent(2, 'judge-noise'), agent(5, 'judge-noise'), agent(5, 'fixable')]).decision).toBe('BELOW');
expect(d(trials, [agent(2, 'judge-noise'), agent(5, 'fixable'), agent(5, 'judge-noise')]).decision).toBe('MEETS-qualified');
});
});
describe('behavior (12) and judge (outputs as median panels)', () => {
test('behavior: 11/12 MEETS, 10/12 qualified, 9/12 EXTEND, 8/12 BELOW', () => {
expect(decide('behavior', 12, batch('initial', 12, fails([7]))).decision).toBe('MEETS');
expect(decide('behavior', 12, batch('initial', 12, fails([1, 2], API))).decision).toBe('MEETS-qualified');
expect(decide('behavior', 12, batch('initial', 12, fails([1, 2, 3], API))).decision).toBe('EXTEND');
expect(decide('behavior', 12, batch('initial', 12, fails([1, 2, 3, 4]))).decision).toBe('BELOW');
});
test('behavior pooled over 24: 22 MEETS, 20 qualified, 19 BELOW', () => {
const base = batch('initial', 12, fails([1, 2, 3], API));
expect(decide('behavior', 12, [...base, ...batch('extend', 12, {}, 12)]).decision).toBe('MEETS-qualified');
expect(decide('behavior', 12, [...batch('initial', 12, fails([1])), ...batch('extend', 12, fails([5]), 12)]).decision).toBe('MEETS');
expect(decide('behavior', 12, [...base, ...batch('extend', 12, fails([1, 2]), 12)], [agent(16, 'unclassified'), agent(17, 'unclassified')]).decision).toBe('BELOW');
});
test('judge: each trial is one output, passing at 2 of its 3 samples whatever the runner claimed', () => {
const outputs = batch('initial', 10).map(t => ({ ...t, samples: t.trial === 2 ? [true, false, false] : [true, false, true] }));
expect(decide('judge', 10, outputs)).toMatchObject({ decision: 'MEETS', passes: 9 });
const short = batch('initial', 10).map(t => ({ ...t, samples: [true, true] }));
expect(decide('judge', 10, short).passes).toBe(0);
});
});
describe('the pooled decision after EXTEND (once, never a third batch)', () => {
const base = batch('initial', 10, fails([1, 2, 3], API));
test('pooled 18/20 strict and 16/20 qualified MEET; 17/20 with an unqualified red is BELOW, never EXTEND', () => {
expect(decide('rule', 10, [...batch('initial', 10, fails([1, 2])), ...batch('extend', 10, {}, 10)], [agent(1, 'unclassified'), agent(2, 'unclassified')]).decision).toBe('MEETS');
expect(decide('rule', 10, [...base, ...batch('extend', 10, { 1: API }, 10)])).toMatchObject({ decision: 'MEETS-qualified', pooled: true, passes: 16 });
expect(decide('rule', 10, [...base, ...batch('extend', 10, { 1: API, 2: API, 3: API, 4: ASSERTION }, 10)]).decision).toBe('BELOW');
const unqualified = decide('rule', 10, [...batch('initial', 10, fails([1, 2])), ...batch('extend', 10, { 1: ASSERTION }, 10)], [agent(1, 'unclassified'), agent(2, 'judge-noise'), agent(11, 'unclassified')]);
expect(unqualified).toMatchObject({ decision: 'BELOW', pooled: true, passes: 17 });
});
test('an extension red nobody classified is needs-classify; one with a fixable cause cancels the extension into a fix round', () => {
expect(decide('rule', 10, [...base, ...batch('extend', 10, fails([1]), 10)])).toMatchObject({ decision: 'needs-classify', needsClassify: [11] });
expect(decide('rule', 10, [...base, ...batch('extend', 10, fails([1]), 10)], [agent(11, 'fixable')]).decision).toBe('BELOW');
});
});
describe('provider evidence is mechanical; void batches', () => {
test('provider needs affirmative evidence: api_error, or pre_turn_infra naming a 429/5xx or reset; a refusal or crash never qualifies', () => {
expect(providerEvidence({ trial: 1, set: 'initial', passed: false, ...API })).toContain('529');
expect(providerEvidence({ trial: 1, set: 'initial', passed: false, failureCause: 'pre_turn_infra', failureEvidence: 'HTTP 429 rate limit' })).toContain('429');
expect(providerEvidence({ trial: 1, set: 'initial', passed: false, failureCause: 'pre_turn_infra', failureEvidence: 'ECONNRESET while streaming' })).not.toBeNull();
expect(providerEvidence({ trial: 1, set: 'initial', passed: false, failureCause: 'pre_turn_infra', failureEvidence: 'Cannot find module ./entry' })).toBeNull();
expect(providerEvidence({ trial: 1, set: 'initial', passed: false, failureCause: 'refusal', failureEvidence: 'stop_reason refusal' })).toBeNull();
expect(machineClass({ trial: 1, set: 'initial', passed: false, failureCause: 'refusal' })).toBeNull();
expect(machineClass({ trial: 1, set: 'initial', passed: false, ...TIMEOUT })).toBe('timeout');
});
test('provider cannot be set by hand when failure_cause disagrees; a timeout never takes a qualifying class; evidence is required', () => {
const assertion: BarTrial = { trial: 3, set: 'initial', passed: false, ...ASSERTION };
expect(classificationProblem(assertion, 'rule', 'provider', 'I think the API flaked')).toContain('affirmative provider or transport evidence');
expect(classificationProblem({ trial: 3, set: 'initial', passed: false, failureCause: 'pre_turn_infra', failureEvidence: 'module load failed' }, 'rule', 'provider', 'x')).toContain('pre_turn_infra');
expect(classificationProblem({ trial: 3, set: 'initial', passed: false, ...TIMEOUT }, 'rule', 'model-miss', 'a.ts:1')).toContain('never qualifies');
expect(classificationProblem(assertion, 'rule', 'fixable', ' ')).toContain('--evidence');
expect(classificationProblem(assertion, 'rule', 'flaky', 'x')).toContain('class must be one of');
expect(classificationProblem({ trial: 3, set: 'initial', passed: true }, 'rule', 'fixable', 'x')).toContain('passed');
expect(classificationProblem({ trial: 3, set: 'initial', passed: false, ...API }, 'rule', 'provider', 'api 529')).toBeNull();
});
test('more than 30% provider-evidence failures voids the batch: one redispatch, then decided on it; void twice stops', () => {
expect(decide('rule', 10, batch('initial', 10, fails([1, 2, 3, 4], API)))).toMatchObject({ decision: 'void', next: 'redispatch' });
const redispatched = decide('rule', 10, [...batch('initial', 10, fails([1, 2, 3, 4], API)), ...batch('redispatch', 10, fails([5]), 10)]);
expect(redispatched).toMatchObject({ decision: 'MEETS', passes: 9 });
expect(redispatched.sets.map(s => [s.set, s.void])).toEqual([['initial', true], ['redispatch', false]]);
expect(decide('rule', 10, [...batch('initial', 10, fails([1, 2, 3, 4], API)), ...batch('redispatch', 10, fails([1, 2, 3, 4], API), 10)]))
.toMatchObject({ decision: 'void', next: 'stop' });
expect(decide('rule', 10, batch('initial', 10, fails([1, 2, 3], API))).decision).not.toBe('void');
});
test('module-load failures (pre_turn_infra without provider evidence) never void a batch', () => {
const crash = { failureCause: 'pre_turn_infra', failureEvidence: 'SyntaxError: Export named x not found' };
expect(decide('rule', 10, batch('initial', 10, fails([1, 2, 3, 4, 5, 6], crash)))).toMatchObject({ decision: 'BELOW' });
});
test('a void extension gets its own one redispatch', () => {
const base = batch('initial', 10, fails([1, 2, 3], API));
expect(decide('rule', 10, [...base, ...batch('extend', 10, fails([1, 2, 3, 4], API), 10)])).toMatchObject({ decision: 'void', next: 'redispatch', pooled: true });
expect(decide('rule', 10, [...base, ...batch('extend', 10, fails([1, 2, 3, 4], API), 10), ...batch('extend-redispatch', 10, {}, 20)]).decision).toBe('MEETS-qualified');
});
});
describe('pooling identity', () => {
test('a changed field is named, including the CI image digest', () => {
const a = { tree: 't1', image: 'ghcr.io/x/ci@sha256:aaa', model: 'm' };
expect(identityMismatch(a, { ...a })).toBeNull();
expect(identityMismatch(a, { ...a, image: 'ghcr.io/x/ci@sha256:bbb' })).toContain('image changed');
expect(identityMismatch(a, { ...a, tree: 't2' })).toContain('tree changed');
});
});
describe('the runner applies the bar: redispatch, EXTEND, classify', () => {
/** A fake runner; outcomes are keyed by trial number across every set. */
const runner = (outcome: (trial: number) => TrialResult) => {
const calls: TrialRequest[] = [];
return { calls, run: async (r: TrialRequest) => { calls.push(r); return outcome(r.trial); } };
};
const opts = (outDir: string, run: (r: TrialRequest) => Promise<TrialResult>, identity = { tree: 't1', image: 'img-a' }) => ({
caseId: 'bar-case', round: 'baseline', config: MEASURE_DEFAULTS, runner: run, outDir, parallel: 5, costPerTrialUsd: 0.1,
evalDir: tmp('bar-history-'), log: () => {}, identity: () => identity,
});
test('a void batch is redispatched once by measure, and both batches are reported', async () => {
const outDir = tmp('bar-void-');
const r = runner(trial => trial <= 4 ? { passed: false, costUsd: 0.1, ...API } : { passed: true, costUsd: 0.1 });
const m = await measureCase({ ...opts(outDir, r.run), kind: 'rule', approved: true });
expect(r.calls).toHaveLength(20);
expect(m).toMatchObject({ decision: 'MEETS', passes: 10, counted: 10 });
expect(m.sets.map(s => `${s.set}:${s.passes}/${s.trials}${s.void ? ':void' : ''}`)).toEqual(['initial:6/10:void', 'redispatch:10/10']);
});
test('classify records reds, refuses a hand-set provider, and EXTEND pools once on identical inputs', async () => {
const outDir = tmp('bar-extend-');
const r = runner(trial => [2, 5, 9].includes(trial) ? { passed: false, costUsd: 0.1, ...ASSERTION } : { passed: true, costUsd: 0.1 });
const m = await measureCase({ ...opts(outDir, r.run), kind: 'rule', approved: true });
expect(m.decision).toBe('needs-classify');
expect(() => classifyTrials(outDir, 'bar-case', 'baseline', [2], 'provider', 'looked like an outage', () => {})).toThrow('affirmative provider');
expect(classifyTrials(outDir, 'bar-case', 'baseline', [2, 5, 9], 'unclassified', 'reviewed captures t02 t05 t09: no route', () => {}).decision).toBe('EXTEND');
const records = fs.readFileSync(path.join(outDir, 'bar-case', 'baseline', 'classifications.jsonl'), 'utf8').trim().split('\n');
expect(records).toHaveLength(3);
await expect(extendCase({ ...opts(outDir, r.run, { tree: 't1', image: 'img-b' }) })).rejects.toThrow('image changed');
const pooled = await extendCase({ ...opts(outDir, r.run) });
expect(r.calls.map(c => c.trial).slice(10)).toEqual([11, 12, 13, 14, 15, 16, 17, 18, 19, 20]);
// 7/10 + 10/10 = 17/20: under the pooled strict bar, and its reds are not in a qualifying class.
expect(pooled).toMatchObject({ decision: 'BELOW', counted: 20, passes: 17 });
expect(decideRound(outDir, 'bar-case', 'baseline', () => {}).decision).toBe('BELOW');
await expect(extendCase({ ...opts(outDir, r.run) })).rejects.toThrow('never a third batch');
});
test('EXTEND is refused unless the round decided EXTEND', async () => {
const outDir = tmp('bar-noextend-');
const r = runner(() => ({ passed: true, costUsd: 0.1 }));
await measureCase({ ...opts(outDir, r.run), kind: 'rule', approved: true });
await expect(extendCase({ ...opts(outDir, r.run) })).rejects.toThrow('is MEETS, not EXTEND');
});
});
+15 -4
View File
@@ -1,7 +1,7 @@
/**
* A6 acceptance: the generated ship skill runs the measure-then-fix loop on a
* red paid eval (classify, measure alone, fix at the cause, re-measure, gate
* once) and measures a failed free shard before calling it pre-existing, and
* red paid eval (classify, measure alone, decide by the measurement bar, fix
* at the cause, re-measure, gate once) and measures a failed free shard before calling it pre-existing, and
* it states the "diagnostic, not verdict" rule. Every ship-measure command and
* flag the skill names exists in scripts/ship-measure.ts.
*/
@@ -25,8 +25,8 @@ describe('the generated ship skill runs the loop (A6 acceptance)', () => {
expect(skeleton).toContain('| `sections/measure.md` |');
});
test('the loop steps run in order: classify, plan, measure, fix, re-measure, stop, then gate once', () => {
const steps = ['1. **Classify.**', '2. **Show the plan.**', '3. **Measure.**', '4. **Fix at the cause.**', '5. **Re-measure.**', '6. **Stop.**', '**Gate once.**'];
test('the loop steps run in order: classify, plan, measure, decide, fix, re-measure, stop, then gate once', () => {
const steps = ['1. **Classify.**', '2. **Show the plan.**', '3. **Measure.**', '4. **Decide by the exit code**', '5. **Fix at the cause.**', '6. **Re-measure.**', '7. **Stop.**', '**Gate once.**'];
const positions = steps.map(step => loop.indexOf(step));
expect(positions.every(p => p >= 0)).toBe(true);
expect([...positions].sort((a, b) => a - b)).toEqual(positions);
@@ -36,6 +36,17 @@ describe('the generated ship skill runs the loop (A6 acceptance)', () => {
expect(loop).toContain('do not also start the gate command');
});
test('the decision branches on every exit code the runner returns, with the bar and its rules', () => {
const decideStep = loop.slice(loop.indexOf('4. **Decide by the exit code**'), loop.indexOf('5. **Fix at the cause.**'));
for (const code of ['- 0: MEETS or MEETS-qualified', '- 5: needs-classify', '- 6: EXTEND', '- 1: BELOW']) expect(decideStep).toContain(code);
for (const cls of ['provider', 'judge-noise', 'model-miss', 'timeout', 'hang', 'regression', 'fixable', 'unclassified']) expect(decideStep).toContain(`\`${cls}\``);
expect(decideStep).toMatch(/MEETS 9\/10, MEETS-qualified 8\/10[\s\S]*EXTEND 7\/10; behavior 11\/12, 10\/12, 9\/12/);
expect(decideStep).toMatch(/never a third batch/i);
expect(loop.slice(loop.indexOf('7. **Stop.**'))).toContain('Exit 3 is a named red');
const source = fs.readFileSync(path.join(ROOT, 'scripts/lib/measure-bar.ts'), 'utf8');
for (const cls of ['provider', 'judge-noise', 'model-miss', 'timeout', 'hang', 'regression', 'fixable', 'unclassified']) expect(source).toContain(`'${cls}'`);
});
test('the diagnostic-not-verdict rule, the honest-fix rule and the unmeasured label are stated', () => {
expect(loop).toContain('Every rerun here is **diagnostic, not a verdict**: it never changes the recorded\nverdict of the run that failed');
expect(loop).toContain('Never raise a budget, lower a\nthreshold, add a verdict retry or skip a case to get a pass.');
+10 -4
View File
@@ -1,7 +1,8 @@
/**
* Free checks of the seeded-flake fixture the paid case drives (CEO-3,
* ENG-13), measured through the real scripts/ship-measure.ts CLI: unsorted it
* measures 7 of 10 with failures that name the line, parallel trials claim
* measures 7 of 10 with failures that name the line, which the bar sends to
* needs-classify and, classified fixable, to a fix round; parallel trials claim
* unique slots that reset per round, an unrelated edit still measures 7 of 10
* and leaves the gate red, and only the sorting fix measures 10 of 10 and turns
* the gate green.
@@ -31,8 +32,8 @@ describe('seeded-flake fixture (free)', () => {
expect(gate().status).toBe(1);
const baseline = measure('baseline');
expect(baseline.stdout).toContain(`DIAGNOSTIC ${SEEDED_CASE} baseline: observed 7/10 trials`);
expect(baseline.status).toBe(1);
expect(baseline.stdout).toContain(`DIAGNOSTIC ${SEEDED_CASE} baseline: observed 7/10 trials (baseline); needs-classify`);
expect(baseline.status).toBe(5);
const failed = fs.readdirSync(roundDir('baseline')).filter(t => /^t\d+$/.test(t))
.map(t => JSON.parse(fs.readFileSync(path.join(roundDir('baseline'), t, 'trial.json'), 'utf8'))).filter(t => !t.passed);
expect(failed).toHaveLength(3);
@@ -43,17 +44,22 @@ describe('seeded-flake fixture (free)', () => {
const slotsOf = (dir: string) => readFixtureRuns(log).filter(r => r.kind === 'case' && r.measureDir?.startsWith(dir)).map(r => r.slot).sort((a, b) => a! - b!);
expect(slotsOf(roundDir('baseline'))).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
expect(readFixtureRuns(log).filter(r => r.kind === 'case' && r.result === 'fail').map(r => r.slot).sort()).toEqual([2, 5, 9]);
const classify = spawnSync(process.execPath, ['run', path.join(ROOT, 'scripts/ship-measure.ts'), 'classify', '--case', SEEDED_CASE, '--round', 'baseline',
'--trials', failed.map(t => t.trial).join(','), '--class', 'fixable', '--evidence', `src/priorities.js:${SEEDED_LINE} returns storage order`], { cwd: repo, env, encoding: 'utf8', timeout: 60_000 });
expect(classify.stdout).toContain('BELOW (fix round)');
expect(classify.status).toBe(1);
fs.writeFileSync(path.join(repo, 'src/priorities.js'), UNRELATED_EDIT);
const control = measure('round-1');
expect(control.stdout).toContain(`DIAGNOSTIC ${SEEDED_CASE} round-1: observed 7/10 trials`);
expect(control.status).toBe(1);
expect(control.status).toBe(5);
expect(slotsOf(roundDir('round-1'))).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
expect(gate().status).toBe(1);
fs.writeFileSync(path.join(repo, 'src/priorities.js'), SORTED_FIX);
const fixed = measure('round-2');
expect(fixed.stdout).toContain(`DIAGNOSTIC ${SEEDED_CASE} round-2: observed 10/10 trials`);
expect(fixed.stdout).toContain('; MEETS');
expect(fixed.status).toBe(0);
expect(gate().status).toBe(0);
const runs = readFixtureRuns(log);
+202
View File
@@ -0,0 +1,202 @@
/**
* Free contracts for the weekly off-ship qualification sweep
* (scripts/ship-measure-sweep.ts): which gate cases it picks from pass-rate
* history, the predicted all-green probability before and after, the weekly
* spend cap enforced through the admission budget across earlier sweep
* reports, the report it writes after every case, and a $0 CLI run (dry run,
* and a fake single-case command). Nothing here makes a paid call.
*/
import { afterAll, describe, expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { spawnSync } from 'node:child_process';
import { EVAL_POLICY } from '../test/helpers/periodic-exclude-data';
import { TRIAL_OUTCOME_SCHEMA, type TrialOutcomeRecord } from '../test/helpers/eval-store';
import { MEASURE_DEFAULTS, type TrialRequest, type TrialResult } from '../scripts/ship-measure';
import { SWEEP_SCHEMA, measuredRedRate, planSweep, predictedAllGreen, runSweep, weeklySpent, type SweepReport } from '../scripts/ship-measure-sweep';
const ROOT = path.resolve(import.meta.dir, '..');
const temps: string[] = [];
const tmp = (prefix: string) => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); temps.push(dir); return dir; };
afterAll(() => { for (const dir of temps) fs.rmSync(dir, { recursive: true, force: true }); });
const A = 'ship-skipped-queued-finding';
const B = 'investigate-owned-completion';
const C = 'investigate-owned-abort';
const Q = 'investigate-owned-ending-error';
function record(id: string, run: number, failed: boolean, extra: Partial<TrialOutcomeRecord> = {}): TrialOutcomeRecord {
return {
schema: TRIAL_OUTCOME_SCHEMA, case: id, file: `test/${id}.test.ts`, tier: 'gate', kind: 'rule', trial: 1, panel: { n: 1, k: 1 }, attempt: 1,
outcome: failed ? 'failed' : 'passed', ...(failed ? { failure_class: 'assertion' as const } : {}), duration_ms: 1000, cost_usd: 1,
policy_version: EVAL_POLICY.version, quarantined: false, execution: 'executed', source: 'shard', run_id: String(run), lane: 'gate',
series_identity: 's1', model: 'm', cli_version: 'c', recorded_at: `2026-09-${10 + run}T00:00:00Z`, ...extra,
} as TrialOutcomeRecord;
}
/** Three weekly runs: A red twice, B red once, C never, a periodic case red (not gate), a retired id red. */
function history(): TrialOutcomeRecord[] {
const out: TrialOutcomeRecord[] = [];
for (const run of [1, 2, 3]) {
out.push(record(A, run, run !== 3), record(B, run, run === 2), record(C, run, false));
out.push(record('retired-case-id', run, true), record('some-periodic-case', run, true, { tier: 'periodic', lane: 'periodic' }));
}
return out;
}
describe('picking cases from gate history', () => {
test('ranks live, non-quarantined gate cases with a red by their cost to the all-green probability, and picks K', () => {
const withQuarantined = [...history(), ...[1, 2, 3].map(run => record(Q, run, true))];
expect(planSweep(withQuarantined, 3, { quarantine: {} }).picked.map(c => c.case)).toEqual([Q, A, B]);
const plan = planSweep(withQuarantined, 2, { quarantine: { [Q]: {} } });
expect(plan.candidates.map(c => c.case)).toEqual([A, B]);
expect(plan.picked.map(c => c.case)).toEqual([A, B]);
const [a, b] = plan.candidates;
expect(a).toMatchObject({ reds: 2, verdicts: 3, scoredTrials: 3, costPerTrialUsd: 1, kind: 'rule' });
expect(a!.passRate).toBeCloseTo(1 / 3, 5);
expect(a!.redRate).toBeGreaterThan(b!.redRate);
expect(a!.contribution).toBeGreaterThan(b!.contribution);
expect(plan.censusCases).toEqual([A, B, C, Q, 'retired-case-id'].sort());
const product = plan.censusCases.reduce((p, id) => p * (1 - plan.rates[id]!), 1);
expect(plan.allGreen).toBeCloseTo(product, 10);
expect(planSweep(history(), 1).picked.map(c => c.case)).toEqual([A]);
});
test('a measured case replaces its history rate; a behavior rate converts to the 3-trial panel red risk', () => {
expect(measuredRedRate('rule', 0, 10, 0)).toBe(0);
expect(measuredRedRate('rule', 1, 10, 0.05)).toBeCloseTo((1 + 4 * 0.05) / 14, 10);
const q = 2 / 16;
expect(measuredRedRate('behavior', 2, 12, 0)).toBeCloseTo(1 - ((1 - q) ** 3 + 3 * (1 - q) ** 2 * q), 10);
const plan = planSweep(history(), 2);
const after = predictedAllGreen(plan, { [A]: 0 })!;
expect(after).toBeCloseTo(plan.allGreen! / (1 - plan.rates[A]!), 10);
});
});
describe('the weekly cap', () => {
const report = (dir: string, id: string, startedAt: string, chargedUsd: number, dryRun = false) => {
fs.mkdirSync(path.join(dir, id), { recursive: true });
fs.writeFileSync(path.join(dir, id, 'sweep-report.json'), JSON.stringify({ schema: SWEEP_SCHEMA, sweepId: id, startedAt, chargedUsd, dryRun }));
};
test('sums the last 7 days of sweep reports once each, skipping dry runs, older reports and this sweep', () => {
const dir = tmp('sweep-prior-');
const now = Date.parse('2026-10-12T12:00:00Z');
report(dir, 'a', '2026-10-10T00:00:00Z', 40);
report(path.join(dir, 'copy'), 'a', '2026-10-10T00:00:00Z', 40);
report(dir, 'b', '2026-10-06T00:00:00Z', 25);
report(dir, 'old', '2026-10-01T00:00:00Z', 100);
report(dir, 'dry', '2026-10-11T00:00:00Z', 0, true);
report(dir, 'self', '2026-10-12T11:00:00Z', 9);
expect(weeklySpent([dir], now, 'self')).toBe(65);
expect(weeklySpent([path.join(dir, 'missing')], now)).toBe(0);
});
});
describe('running the sweep with a fake runner', () => {
const fake = (fail: (r: TrialRequest) => boolean) => {
const calls: TrialRequest[] = [];
const runner = async (r: TrialRequest): Promise<TrialResult> => { calls.push(r); return fail(r) ? { passed: false, costUsd: 1, failureCause: 'assertion' } : { passed: true, costUsd: 1 }; };
return { calls, runner };
};
const base = (outDir: string, runner: (r: TrialRequest) => Promise<TrialResult>, extra: Partial<Parameters<typeof runSweep>[0]> = {}) => runSweep({
records: history(), historyScope: 'fixture history', k: 2, capUsd: 150, config: MEASURE_DEFAULTS, outDir, priorDirs: [], dryRun: false, parallel: 5,
ref: 'main', sha: 'a'.repeat(40), runner, runnerId: 'fake', preflight: () => ({ ok: true, detail: 'fake' }), identity: () => ({ tree: 'fixed' }),
evalDir: tmp('sweep-history-'), log: () => {}, now: Date.parse('2026-10-12T12:00:00Z'), ...extra,
});
test('measures each picked case at its kind\'s N with the bar, writes the report, and predicts the all-green probability after', async () => {
const outDir = tmp('sweep-out-');
const run = fake(r => r.caseId === A && r.trial <= 4);
const report = await base(outDir, run.runner);
expect(run.calls.filter(c => c.caseId === A)).toHaveLength(10);
expect(run.calls.filter(c => c.caseId === B)).toHaveLength(10);
expect(report.rows.map(r => [r.case, r.status, r.decision, `${r.measuredPasses}/${r.measuredTrials}`])).toEqual([[A, 'measured', 'BELOW', '6/10'], [B, 'measured', 'MEETS', '10/10']]);
expect(report).toMatchObject({ label: 'diagnostic', verdict: null, chargedUsd: 20, priorWeekUsd: 0, k: 2 });
expect(report.allGreenAfter).not.toBe(report.allGreenBefore);
const dir = path.join(outDir, `sweep-${report.sweepId}`);
const saved = JSON.parse(fs.readFileSync(path.join(dir, 'sweep-report.json'), 'utf8')) as SweepReport;
expect(saved.rows[0]!.captures).toBe(path.join(`sweep-${report.sweepId}`, A, 'baseline'));
expect(fs.existsSync(path.join(outDir, saved.rows[0]!.captures!, 't01', 'trial.json'))).toBe(true);
const md = fs.readFileSync(path.join(dir, 'sweep-report.md'), 'utf8');
expect(md).toContain('| # | Case | Kind | History rate | Measured | Decision | Cost (est / actual) | Captures |');
expect(md).toMatch(/Predicted gate all-green probability .*before \d+(\.\d)?% → after \d+(\.\d)?%/);
expect(md).toContain(`| 1 | ${A} | rule | 33.3% of 3 trials; 2/3 red verdicts`);
expect(md).toContain('never pushes or opens a pull request');
});
test('stops at the weekly cap: a partly funded case stops at the budget, the rest are skipped and listed', async () => {
const outDir = tmp('sweep-cap-');
const prior = tmp('sweep-cap-prior-');
fs.writeFileSync(path.join(prior, 'sweep-report.json'), JSON.stringify({ schema: SWEEP_SCHEMA, sweepId: 'earlier', startedAt: '2026-10-08T00:00:00Z', chargedUsd: 136, dryRun: false }));
const run = fake(() => false);
const report = await base(outDir, run.runner, { priorDirs: [prior], capUsd: 150, k: 3, records: history().map(r => r.case === C && r.run_id === '1' ? record(C, 1, true) : r) });
expect(report.priorWeekUsd).toBe(136);
// B and C tie (one red in three); ties fall to the case id, so C ranks before B.
expect(report.rows.map(r => [r.case, r.status])).toEqual([[A, 'measured'], [C, 'budget_exhausted'], [B, 'skipped']]);
// A partly funded case reports the trials that ran, not the planned N.
expect([report.rows[1]!.measuredPasses, report.rows[1]!.measuredTrials]).toEqual([4, 4]);
expect(run.calls).toHaveLength(14);
expect(report.chargedUsd).toBe(14);
expect(report.rows[2]!.note).toContain('weekly cap $150 exhausted');
expect(fs.readFileSync(path.join(outDir, `sweep-${report.sweepId}`, 'sweep-report.md'), 'utf8')).toContain(`- ${B}: weekly cap $150 exhausted`);
});
test('a dry run plans and estimates without running anything or charging the cap', async () => {
const outDir = tmp('sweep-dry-');
const run = fake(() => false);
const report = await base(outDir, run.runner, { dryRun: true });
expect(run.calls).toHaveLength(0);
expect(report.rows.map(r => [r.case, r.status, r.estimatedUsd])).toEqual([[A, 'planned', 10], [B, 'planned', 10]]);
expect(report.chargedUsd).toBe(0);
expect(weeklySpent([outDir], Date.parse('2026-10-12T13:00:00Z'))).toBe(0);
});
test('a case whose selection preflight fails is skipped before any paid call', async () => {
const run = fake(() => false);
const report = await base(tmp('sweep-preflight-'), run.runner, { k: 1, preflight: () => ({ ok: false, detail: '--list planned nothing' }) });
expect(report.rows[0]).toMatchObject({ status: 'skipped', note: 'selection preflight failed (no paid call made): --list planned nothing' });
expect(run.calls).toHaveLength(0);
});
});
describe('ship-measure sweep CLI ($0)', () => {
const historyDir = () => {
const dir = tmp('sweep-cli-history-');
fs.writeFileSync(path.join(dir, 'trial-outcomes.jsonl'), `${history().map(r => JSON.stringify(r)).join('\n')}\n`);
return dir;
};
const cli = (cwd: string, ...args: string[]) => spawnSync(process.execPath, ['run', path.join(ROOT, 'scripts/ship-measure.ts'), 'sweep', ...args], {
cwd, encoding: 'utf8', timeout: 120_000, env: { ...process.env, GSTACK_STATE_ROOT: tmp('sweep-cli-state-'), GITHUB_STEP_SUMMARY: '' },
});
test('--dry-run reads local history, prints the plan and the before probability, and spends nothing', () => {
const cwd = tmp('sweep-cli-');
const r = cli(cwd, '--history-dir', historyDir(), '--k', '2', '--dry-run', '--out', path.join(cwd, 'out'));
expect(r.status).toBe(0);
expect(r.stdout).toContain('(dry run: nothing ran, $0)');
expect(r.stdout).toContain(`| 1 | ${A} | rule |`);
expect(r.stdout).toContain('would measure 10 trials');
});
test('a fake single-case command runs the whole sweep end to end at $0', () => {
const cwd = tmp('sweep-cli-fake-');
// A Bun script, not a shell stub, so the fake runner also executes on Windows.
const script = path.join(cwd, 'fake-trial.ts');
fs.writeFileSync(script, 'console.log("cost_usd: 0");\nif (process.env.GSTACK_SHIP_MEASURE_TRIAL === "3") { console.log("failure_cause: assertion"); process.exit(1); }\nconsole.log(`PASS ${process.argv[2]}`);\n');
const bun = /\s/.test(process.execPath) ? 'bun' : process.execPath;
const r = cli(cwd, '--history-dir', historyDir(), '--k', '1', '--cap-usd', '1', '--command', `${bun} ${script} {case}`, '--jobs', '5', '--out', path.join(cwd, 'out'));
expect(r.status).toBe(0);
expect(r.stdout).toContain(`DIAGNOSTIC ${A} baseline: observed 9/10 trials (baseline); MEETS`);
expect(r.stdout).toMatch(new RegExp(`\\| 1 \\| ${A} \\| rule \\| [^|]+ \\| 9/10 \\| MEETS \\|`));
});
test('history that cannot be read fails closed with nothing measured', () => {
const cwd = tmp('sweep-cli-nohistory-');
const r = spawnSync(process.execPath, ['run', path.join(ROOT, 'scripts/ship-measure.ts'), 'sweep', '--dry-run', '--repo', 'garrytan/no-such-repo-xyz', '--out', path.join(cwd, 'out')], {
cwd, encoding: 'utf8', timeout: 120_000, env: { ...process.env, PATH: path.dirname(process.execPath), GSTACK_STATE_ROOT: tmp('sweep-cli-state-') },
});
expect(r.status).toBe(1);
expect(r.stderr).toContain('pass-rate history unavailable');
});
});
+50 -46
View File
@@ -1,7 +1,8 @@
/**
* Free contracts for scripts/ship-measure.ts, /ship's measure-then-fix runner:
* per-kind aggregation (rule trials, behavior panels of 3 at their own 2 of 3,
* judge outputs scored by 3-sample panels), unique artifact directories, the
* per-kind batches decided by the measurement bar (rule trials, behavior
* trials in panels of 3, judge outputs scored by 3-sample panels; the pure
* decision table lives in test/ship-measure-bar.test.ts), unique artifact directories, the
* diagnostic label, the estimated admission budget, the ask-once rule, the
* repair-round limit, the unmeasured skip, the PR table and the free-suite
* reruns with the flaky retry off. Every runner here is a fake; nothing paid.
@@ -12,7 +13,7 @@ import * as os from 'node:os';
import * as path from 'node:path';
import { spawnSync } from 'node:child_process';
import {
MEASURE_DEFAULTS, aggregate, assertDiagnosticDir, formatKindTable, formatReport, freeParallelism, judgeFile, kindPlan,
MEASURE_DEFAULTS, assertDiagnosticDir, formatKindTable, formatReport, freeParallelism, judgeFile, kindPlan,
measureCase, measureFreeShard, readMeasureConfig, recordUnmeasured,
type MeasureConfig, type MeasureKind, type TrialRecord, type TrialRequest, type TrialResult,
} from '../scripts/ship-measure';
@@ -43,29 +44,32 @@ const measure = (kind: MeasureKind, run: FakeRun, extra: Partial<Parameters<type
const outDir = extra.outDir ?? tmp('ship-measure-out-');
return measureCase({
caseId: 'seeded-case', kind, round: 'baseline', config: MEASURE_DEFAULTS, runner: run.runner, outDir, parallel: 4,
costPerTrialUsd: 0.5, approved: false, evalDir: tmp('ship-measure-history-'), log: () => {}, ...extra,
costPerTrialUsd: 0.5, approved: false, evalDir: tmp('ship-measure-history-'), log: () => {}, identity: () => ({ tree: 'fixed' }), ...extra,
});
};
describe('per-kind plans and the printed table (CEO-9, DX-7)', () => {
test('defaults: rule 10 trials at 9, behavior 4 panels of 3 at 11 of 12, judge 10 outputs at 9', () => {
expect(kindPlan('rule', MEASURE_DEFAULTS)).toEqual({ kind: 'rule', units: 10, trialsPerUnit: 1, trials: 10, unitTarget: 9, trialTarget: 9 });
expect(kindPlan('behavior', MEASURE_DEFAULTS)).toEqual({ kind: 'behavior', units: 4, trialsPerUnit: 3, trials: 12, unitTarget: 4, trialTarget: 11 });
expect(kindPlan('judge', MEASURE_DEFAULTS)).toEqual({ kind: 'judge', units: 10, trialsPerUnit: 1, trials: 10, unitTarget: 9, trialTarget: 9 });
test('defaults: rule 10, behavior 4 panels of 3 (12), judge 10 outputs; the table prints the bar per kind', () => {
expect(kindPlan('rule', MEASURE_DEFAULTS)).toMatchObject({ trials: 10, trialsPerUnit: 1, bar: { strict: 9, qualified: 8, floor: 7 }, pooledBar: { trials: 20, strict: 18, qualified: 16 } });
expect(kindPlan('behavior', MEASURE_DEFAULTS)).toMatchObject({ trials: 12, trialsPerUnit: 3, bar: { strict: 11, qualified: 10, floor: 9 }, pooledBar: { trials: 24, strict: 22, qualified: 20 } });
expect(kindPlan('judge', MEASURE_DEFAULTS)).toMatchObject({ trials: 10, trialsPerUnit: 1, bar: { strict: 9, qualified: 8, floor: 7 } });
const table = formatKindTable(MEASURE_DEFAULTS);
expect(table).toContain('| rule | 10 | at least 9 of 10 trials |');
expect(table).toContain('every panel at 2 of 3, no contract violation, at least 11 of 12 trials | 4 of 3 trials');
expect(table).toContain('| judge | 10 outputs | at least 9 of 10 outputs | 3 samples per output, passes at 2 of 3');
expect(table).toContain('| rule | 10 | 9 of 10 | 8 of 10, every red qualifying | 7 of 10 (or 8 that does not qualify): 10 more on identical inputs, then 18 of 20, or 16 of 20 qualified |');
expect(table).toContain('| behavior | 12 (4 panels of 3) | 11 of 12 | 10 of 12, every red qualifying | 9 of 12 (or 10 that does not qualify): 12 more on identical inputs, then 22 of 24, or 20 of 24 qualified |');
expect(table).toContain('| judge | 10 outputs, each its median 3-sample panel | 9 of 10 outputs |');
expect(table).toContain('$25 total; asks above $2/trial');
expect(table).toContain('Repair rounds: at most 3');
expect(table).toContain('never changes a recorded verdict');
expect(table).toMatch(/more than 30%[^\n]*void[^\n]*redispatched once/);
});
test('config keys override defaults; an empty value is the default; an invalid value throws', () => {
const values: Record<string, string> = { ship_measure_rule_trials: '20', ship_measure_budget_usd: '7.5', ship_rerun_backend: 'ubicloud', ship_measure_max_rounds: '' };
const config = readMeasureConfig(key => values[key]);
expect(config).toEqual({ ...MEASURE_DEFAULTS, ruleTrials: 20, budgetUsd: 7.5, rerunBackend: 'ubicloud' });
expect(kindPlan('rule', config).trialTarget).toBe(18);
expect(kindPlan('rule', config).bar.strict).toBe(18);
expect(readMeasureConfig(key => ({ ship_measure_sweep_cases: '2', ship_measure_sweep_budget_usd: '12.5' } as Record<string, string>)[key])).toMatchObject({ sweepCases: 2, sweepBudgetUsd: 12.5 });
expect(() => readMeasureConfig(key => key === 'ship_measure_sweep_budget_usd' ? '-1' : undefined)).toThrow('ship_measure_sweep_budget_usd');
expect(() => readMeasureConfig(key => key === 'ship_measure_behavior_panels' ? '0' : undefined)).toThrow('ship_measure_behavior_panels');
expect(() => readMeasureConfig(key => key === 'ship_rerun_backend' ? 'cloud' : undefined)).toThrow('not local or ubicloud');
});
@@ -76,60 +80,56 @@ describe('per-kind plans and the printed table (CEO-9, DX-7)', () => {
const config = (...args: string[]) => spawnSync('bash', [path.join(ROOT, 'bin/gstack-config'), ...args], { encoding: 'utf8', timeout: 30_000, env });
const defaults = config('defaults').stdout;
for (const [key, value] of [['ship_measure_rule_trials', '10'], ['ship_measure_behavior_panels', '4'], ['ship_measure_judge_outputs', '10'],
['ship_measure_ask_per_trial_usd', '2'], ['ship_measure_budget_usd', '25'], ['ship_measure_max_rounds', '3'], ['ship_rerun_backend', 'local']]) {
['ship_measure_ask_per_trial_usd', '2'], ['ship_measure_budget_usd', '25'], ['ship_measure_max_rounds', '3'], ['ship_rerun_backend', 'local'],
['ship_measure_sweep_cases', '5'], ['ship_measure_sweep_budget_usd', '150']]) {
expect(defaults).toMatch(new RegExp(`${key}:\\s+${value}\\n`));
expect(config('get', key!).stdout.trim()).toBe(value!);
}
expect(config('set', 'ship_measure_budget_usd', '0').status).toBe(1);
expect(config('set', 'ship_measure_rule_trials', '2.5').status).toBe(1);
expect(config('set', 'ship_rerun_backend', 'cloud').status).toBe(1);
expect(config('set', 'ship_measure_sweep_budget_usd', '0').status).toBe(1);
expect(config('set', 'ship_measure_sweep_cases', 'two').status).toBe(1);
expect(config('set', 'ship_measure_budget_usd', '12.5').status).toBe(0);
expect(readMeasureConfig(key => config('get', key).stdout.trim()).budgetUsd).toBe(12.5);
expect(config('list').stdout).toMatch(/ship_measure_budget_usd:\s+12\.5 \(set/);
});
});
describe('aggregation per kind with fake runners (ENG-12)', () => {
test('rule: 8 of 10 is below target, 9 of 10 meets; every trial gets its own diagnostic directory', async () => {
describe('per-kind batches decided by the measurement bar, with fake runners (ENG-12)', () => {
test('rule: 8 of 10 needs its reds classified, 9 of 10 MEETS; every trial gets its own diagnostic directory', async () => {
const red = fake(trial => ({ passed: ![3, 6].includes(trial), costUsd: 0.5, ...(trial === 3 ? { failureCause: 'assertion', failureDetail: 'unsorted list' } : {}) }));
const m = await measure('rule', red);
expect(m).toMatchObject({ status: 'measured', label: 'diagnostic', verdict: null, unitPasses: 8, meets: false });
expect(m).toMatchObject({ status: 'measured', label: 'diagnostic', verdict: null, passes: 8, counted: 10, decision: 'needs-classify', next: 'classify' });
expect(red.calls).toHaveLength(10);
const dirs = m.trials.map(t => t.dir);
expect(new Set(dirs).size).toBe(10);
for (const dir of dirs) {
expect(path.basename(path.dirname(dir))).toBe('baseline');
expect(JSON.parse(fs.readFileSync(path.join(dir, 'trial.json'), 'utf8'))).toMatchObject({ label: 'diagnostic', verdict: null });
expect(JSON.parse(fs.readFileSync(path.join(dir, 'trial.json'), 'utf8'))).toMatchObject({ label: 'diagnostic', verdict: null, set: 'initial' });
}
const saved = JSON.parse(fs.readFileSync(path.join(path.dirname(dirs[0]!), 'measurement.json'), 'utf8'));
expect(saved).toMatchObject({ label: 'diagnostic', verdict: null, round: 'baseline', unitPasses: 8, meets: false });
expect(saved).toMatchObject({ label: 'diagnostic', verdict: null, round: 'baseline', passes: 8, decision: 'needs-classify', identity: { tree: 'fixed' } });
expect(saved.trials.find((t: TrialRecord) => t.trial === 3)).toMatchObject({ failureCause: 'assertion', failureDetail: 'unsorted list' });
expect((await measure('rule', fake(trial => ({ passed: trial !== 4, costUsd: 0.5 })))).meets).toBe(true);
expect((await measure('rule', fake(trial => ({ passed: trial !== 4, costUsd: 0.5 })))).decision).toBe('MEETS');
});
test('behavior: each panel of 3 needs its own 2 of 3, and 11 of 12 trials overall', async () => {
// Trials 1 and 2 fail: 10 of 12 pass, which a single 2-of-12 panel would PASS; panel 1 is 1 of 3.
const panelRed = await measure('behavior', fake(trial => ({ passed: trial > 2 })));
expect(panelRed).toMatchObject({ unitPasses: 3, trialPasses: 10, meets: false });
// One failure in each of two panels: every panel passes at 2 of 3, but 10 of 12 is under 11.
const tooMany = await measure('behavior', fake(trial => ({ passed: trial !== 1 && trial !== 4 })));
expect(tooMany).toMatchObject({ unitPasses: 4, trialPasses: 10, meets: false });
test('behavior: 12 trials in panels of 3, decided on the trial count (11 MEETS, 10 needs classifying); a contract violation is a fix round', async () => {
const oneMiss = await measure('behavior', fake(trial => ({ passed: trial !== 7 })));
expect(oneMiss).toMatchObject({ unitPasses: 4, trialPasses: 11, meets: true });
expect(oneMiss).toMatchObject({ passes: 11, counted: 12, decision: 'MEETS' });
expect(oneMiss.trials.map(t => t.unit)).toEqual([1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4]);
// A contract violation fails its panel even when the trial's assertions passed.
expect((await measure('behavior', fake(trial => ({ passed: trial > 2 })))).decision).toBe('needs-classify');
const contract = await measure('behavior', fake(trial => ({ passed: true, ...(trial === 5 ? { contract: true } : {}) })));
expect(contract).toMatchObject({ unitPasses: 3, trialPasses: 11, meets: false });
expect(contract).toMatchObject({ passes: 11, decision: 'BELOW', next: 'fix' });
});
test('judge: an output passes at 2 of its 3 samples, whatever the runner claimed; 9 of 10 outputs meets', async () => {
test('judge: an output passes at 2 of its 3 samples, whatever the runner claimed; 9 of 10 outputs MEETS', async () => {
const judged = await measure('judge', fake(trial => ({ passed: true, samples: trial === 2 ? [true, false, false] : [true, false, true] })));
expect(judged).toMatchObject({ unitPasses: 9, meets: true });
expect(judged).toMatchObject({ passes: 9, decision: 'MEETS' });
const red = await measure('judge', fake(trial => ({ passed: true, samples: trial <= 2 ? [false, false, true] : [true, true, true] })));
expect(red).toMatchObject({ unitPasses: 8, meets: false });
const wrongCount = aggregate(kindPlan('judge', { ...MEASURE_DEFAULTS, judgeOutputs: 1 }),
[{ trial: 1, unit: 1, batch: 1, dir: '/x', passed: true, samples: [true, true] }]);
expect(wrongCount.meets).toBe(false);
expect(red).toMatchObject({ passes: 8, decision: 'needs-classify' });
const wrongCount = await measure('judge', fake(() => ({ passed: true, samples: [true, true] })));
expect(wrongCount).toMatchObject({ passes: 0, decision: 'BELOW' });
});
test('standalone judge ids resolve to the one paid file that names them', () => {
@@ -147,7 +147,7 @@ describe('spend, approval, rounds and artifacts', () => {
expect(baseline).toMatchObject({ status: 'budget_exhausted', estimatedUsd: 24, actualUsd: 24 });
expect(baseline.trials.map(t => t.batch)).toEqual([1, 1, 1, 1, 2, 2, 2, 2]);
expect(run.maxConcurrent).toBeLessThanOrEqual(4);
expect(baseline.reason).toContain('8 of 10 trials ran');
expect(baseline.reason).toContain('8 of 10 initial trials ran');
// The budget is per red case: a repair round starts from what the baseline spent.
const round = await measure('rule', fake(() => ({ passed: true, costUsd: 3 })), { outDir, round: 'round-1', costPerTrialUsd: 3 });
expect(round.status).toBe('budget_exhausted');
@@ -158,7 +158,7 @@ describe('spend, approval, rounds and artifacts', () => {
const m = await measure('rule', fake(() => ({ passed: true, costUsd: 1 })), { costPerTrialUsd: 2.5, approved: true, config: { ...MEASURE_DEFAULTS, budgetUsd: 12 } });
// Batch 1 reserves 4 x 2.5 = 10 but spends 4; batch 2 then fits 3 (8 / 2.5); batch 3 fits 2 (5 / 2.5); batch 4 one more.
expect(m.trials.map(t => t.batch)).toEqual([1, 1, 1, 1, 2, 2, 2, 3, 3, 4]);
expect(m).toMatchObject({ status: 'measured', actualUsd: 10, estimatedUsd: 25, meets: true });
expect(m).toMatchObject({ status: 'measured', actualUsd: 10, estimatedUsd: 25, decision: 'MEETS' });
});
test('asks once: no estimate or a per-trial estimate above the threshold runs nothing until approved', async () => {
@@ -205,9 +205,9 @@ describe('spend, approval, rounds and artifacts', () => {
recordUnmeasured(outDir, 'browse-basic', 'provider outage before the first turn');
expect(() => recordUnmeasured(outDir, 'other', ' ')).toThrow('--reason');
const report = formatReport(outDir);
expect(report).toContain('| seeded-case | rule | baseline | observed 7/10 | 9/10 | $5.00 | $2.50 | below target |');
expect(report).toContain('| seeded-case | rule | round-1: after fix at sort the shuffled list in src/order.js | observed 10/10 | 9/10 | $5.00 | $2.50 | at or above target |');
expect(report).toContain('| browse-basic | — | — | — | — | — | — | unmeasured (provider outage before the first turn); not a pass |');
expect(report).toContain('| seeded-case | rule | baseline | observed 7/10 | 9/10 strict, 8/10 qualified | needs-classify | $5.00 | $2.50 |');
expect(report).toContain('| seeded-case | rule | round-1: after fix at sort the shuffled list in src/order.js | observed 10/10 | 9/10 strict, 8/10 qualified | MEETS | $5.00 | $2.50 |');
expect(report).toContain('| browse-basic | — | — | — | — | unmeasured (provider outage before the first turn); not a pass | — | — |');
expect(report).toContain('never change a recorded verdict');
});
});
@@ -226,8 +226,8 @@ if [ "$GSTACK_SHIP_MEASURE_TRIAL" = 2 ]; then echo "failure_cause: assertion"; e
});
const out = path.join(project, '.context', 'ship-measure');
const red = cli('measure', '--case', 'flaky-sort', '--command', './evals.sh case {case}', '--cost-per-trial', '0.1', '--jobs', '3');
expect(red.stdout).toContain('| rule | 10 | at least 9 of 10 trials |');
expect(red.stdout).toContain('DIAGNOSTIC flaky-sort baseline: observed 9/10 trials (baseline); at or above target');
expect(red.stdout).toContain('| rule | 10 | 9 of 10 |');
expect(red.stdout).toContain('DIAGNOSTIC flaky-sort baseline: observed 9/10 trials (baseline); MEETS');
expect(red.status).toBe(0);
const trial2 = JSON.parse(fs.readFileSync(path.join(out, 'flaky-sort', 'baseline', 't02', 'trial.json'), 'utf8'));
expect(trial2).toMatchObject({ passed: false, costUsd: 0.1, failureCause: 'assertion', failureDetail: 'order.js:3 unsorted', label: 'diagnostic' });
@@ -341,18 +341,22 @@ describe('diagnostic trials prove they ran and never touch the judge cache', ()
expect(junitExecuted(dirWith(junit('<testcase name="a" time="1"/>')))).toBe(true);
expect(junitExecuted(dirWith(junit('<testcase name="a" time="1"></testcase><testcase name="b"><skipped/></testcase>')))).toBe(true);
});
test('the judge input cache is never inherited by a diagnostic trial', () => {
const env = diagnosticBaseEnv({ EVALS_CACHE_DIR: '/c', EVALS_CACHE_RUNTIME_ID: 'img', KEEP: '1' }, '/e');
test('the judge input cache is never inherited by a diagnostic trial; each trial gets its own run id', () => {
const env = diagnosticBaseEnv({ EVALS_CACHE_DIR: '/c', EVALS_CACHE_RUNTIME_ID: 'img', KEEP: '1' }, '/e', 'case-baseline-t3');
expect(env).not.toHaveProperty('EVALS_CACHE_DIR');
expect(env).not.toHaveProperty('EVALS_CACHE_RUNTIME_ID');
expect(env).toMatchObject({ KEEP: '1', GSTACK_EVAL_DIR: '/e', GSTACK_SHIP_MEASURE_LABEL: 'diagnostic' });
expect(env).toMatchObject({ KEEP: '1', GSTACK_EVAL_DIR: '/e', GSTACK_SHIP_MEASURE_LABEL: 'diagnostic', EVALS_RUN_ID: 'local-measure-case-baseline-t3' });
// Every trial has its own run id (evidence-retaining cases refuse to start without one); CI's id is kept as the prefix.
expect(diagnosticBaseEnv({ EVALS_RUN_ID: 'ci-9-1-eval-sweep' }, '/e', 'x-t1').EVALS_RUN_ID).toBe('ci-9-1-eval-sweep-measure-x-t1');
});
});
describe('the measure CLI proves --case selection before any paid trial', () => {
test('a real case plans its own test file; an unknown case is refused', () => {
test('a real case plans its own test file, in file mode or name mode; an unknown case is refused', () => {
const { caseSelectionPreflight } = require('../scripts/ship-measure');
expect(caseSelectionPreflight('ship-measure-seeded-flake')).toEqual({ ok: true, detail: '--case ship-measure-seeded-flake selects test/skill-e2e-ship-measure-loop.test.ts' });
expect(caseSelectionPreflight('no-such-case').ok).toBe(false);
// A name-mode case (one of several in its file) lists its trial shard as <file>#<id>~t1.
expect(caseSelectionPreflight('ship-exploratory-late-input')).toEqual({ ok: true, detail: '--case ship-exploratory-late-input selects test/skill-e2e-qa-callers.test.ts' });
});
});
+16
View File
@@ -26,6 +26,12 @@
* pasted developer-facing plan: without one it reviews the checkout's
* branch diff, and a tests-only diff legitimately ends at its
* applicability gate, which is neither 'asked' nor 'plan_ready'.
* - plan-ceo-review reviews a pasted plan too. Without one it reviews the
* checkout's branch diff, so its work before the first question (the
* pre-review audit and landscape searches) grew with whatever PR the gate
* ran on: on a 1,900-line PR it timed out at 300s in 3 of 10 measured
* trials while still researching that diff. A fixed small plan keeps the
* work the same on every branch.
* - named-target case: a pasted draft (initialPlanContent) IS an
* explicitly-named target, so the gate must NOT ask — and the review
* must actually consume the pasted content.
@@ -62,6 +68,15 @@ weekly export emails. One new component, one route, one test file.
- test/${SEED_TOKEN}.test.tsx (new)
`;
const CEO_PLAN = `
# Plan: weekly digest email
## Scope
Send each workspace owner a weekly digest email listing the tasks closed
that week. One scheduled job, one email template, one opt-out toggle in
settings.
`;
const DEVEX_PLAN = `
# Plan: tasks export command
@@ -96,6 +111,7 @@ describeE2E('plan-mode-info no-op outside plan mode (gate regression)', () => {
? { extraArgs: ['--disallowedTools', 'AskUserQuestion'], requireProseEvidence: true }
: {}),
...(skillName === 'plan-devex-review' ? { initialPlanContent: DEVEX_PLAN } : {}),
...(skillName === 'plan-ceo-review' ? { initialPlanContent: CEO_PLAN } : {}),
});
if (obs.outcome === 'silent_write' || obs.outcome === 'exited' || obs.outcome === 'timeout') {