Skip to content

feat(workflow): run benchmark suites as workflows - #1379

Draft
SaulMoro wants to merge 67 commits into
Tencent:mainfrom
SaulMoro:feat/1180-benchmark-suites
Draft

SaulMoro wants to merge 67 commits into
Tencent:mainfrom
SaulMoro:feat/1180-benchmark-suites

Conversation

@SaulMoro

@SaulMoro SaulMoro commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

A team could not tell whether a tool, a model or its own assets fix its real tasks better. Now a suite in the team repo runs as a workflow: every arm works the same tasks in fresh clones and is scored by a check it never sees. An arm can change the tool, the model or the team assets, and a matrix crosses them. The dashboard's Benchmarks page charts every run, and compares arms that differ in one thing only.

How much do the team's assets help each tool? A matrix crosses its values into one arm each:

# benchmarks/onboarding-docs/suite.yaml
defaults: { tool: claude }
matrix:
  tool: [claude, codex]
  team: [pulled, none]          # pulled: the revision you pulled; none: no team assets

Does the team's next branch beat main, and what does one skill add? Arms on other team assets:

# benchmarks/fix-auth/suite.yaml
defaults: { tool: claude, model: strong }
arms:
  - name: main                          # the revision you pulled
  - name: next
    team: next                          # a branch, tag or commit of the team repo, before you merge it
  - name: no-auth-tokens
    team: { without: [auth-tokens] }    # the pulled revision without one skill
  - name: bare
    team: none

Is the cheap model good enough once it has our assets?

# benchmarks/migrate-db/suite.yaml
defaults: { tool: claude }
matrix:
  model: [strong, fast]         # aliases from models/aliases.yaml
  team: [pulled, none]

Which tool does best on one model alias, with today's assets and with the last release's?

# benchmarks/review-pr/suite.yaml
defaults: { tool: claude, model: strong }
matrix:
  tool: [claude, codex, opencode]
  team: [pulled, v2.3.0]        # a tag of the team repo
Suite Arms it makes What Compare compares
onboarding-docs claude-pulled, claude-none, codex-pulled, codex-none team on each tool (2), tool on each team (2)
fix-auth main, next, no-auth-tokens, bare team, every pair (6): none, then without, then a ref, then pulled
migrate-db strong-pulled, strong-none, fast-pulled, fast-none team on each model (2), model on each team (2)
review-pr claude-pulled, claude-v2-3-0, codex-pulled, codex-v2-3-0, opencode-pulled, opencode-v2-3-0 team on each tool (3), tool on each team (6): one alias counts as one model

Two arms that differ in more than one thing are not compared. Start refuses a ref the team repo lacks, a skill the pulled revision lacks, and none on a machine whose user-scope install would hand the trial the team's assets.

Compare: how much the team's assets add, trial by trial (onboarding-docs, a seeded workspace).

Compare tab: pulled team against no team assets

Team changes: one tool and model across the team revisions it ran on, with the files each changed.

Team changes tab: claude on three team revisions

Arms: every arm over time, as before.

Arms tab, fix-auth

Question Tab, panel What it showed
How much do our assets help on onboarding docs? Compare, by Team assets claude +41 pp · 95% +16 pp to +59 pp over 27 paired trials, −60% cost per passed trial
Do they help codex as much? Compare, Difference per pair of arms +15 pp, interval crosses zero: not separated
Did the auth-tokens skill help claude? Team changes 67%, 62%, 70% over three revisions, none told apart; 67ee2ce changed 3 files, session-fixation 1/12 to 3/12
Switch our auth fixes to claude? Arms, Leading arm claude 67% · 95% 57%–75% · not separated from codex
Which model is worth paying for on migrate-db? Arms, Pass rate against cost sonnet 73% at 0.31 USD a trial, opus 78% at 0.89 USD; the frontier joins haiku, sonnet, opus
Light theme, Cost axis, zh-CN, tablet and phone

Compare, light

Team changes, light

migrate-db, pass rate against cost per trial

Arms, zh-CN

Compare, tablet Team changes, tablet Compare, phone Team changes, phone Arms, phone
# benchmarks/fix-auth/tasks/expired-token.yaml
schemaVersion: v1alpha1
title: Reject expired refresh tokens
prompt: Refresh tokens past their expiry are still accepted. Fix it.
startingCommit: 3f2c9a7d1e0b4c6a8f5d2e1b0c9a8f7e6d5c4b3a
check:
  run: npx vitest run test/auth/refresh.test.ts
  protect: ['test/**', 'package.json', 'package-lock.json']
  add: [test/auth/refresh.test.ts]       # #1401: written from source.fix before the check; the agent never sees it
source: { platform: claude-code, sessionId: 7c1e2b9a, fix: 9a1d4e7c2b8f0a3d6e5c1b9f8a7d2e4c6b0a1f3e }
$ teamai workflow start onboarding-docs --benchmark --driver local --sandbox
| Arm           | Tool   | Model (source)      | Team           | Planned | Passed | Failed | Pass rate |
| claude-pulled | claude | tool default (team) | pulled 3f9c2ab | 6       | 5      | 1      | 83% (5/6) |
| claude-none   | claude | tool default (team) | none           | 6       | 2      | 4      | 33% (2/6) |
| codex-pulled  | codex  | tool default (team) | pulled 3f9c2ab | 6       | 4      | 2      | 67% (4/6) |
| codex-none    | codex  | tool default (team) | none           | 6       | 3      | 3      | 50% (3/6) |
These results hold for these tasks, this run and this machine.

The Error, Unscored, Not run, Stopped by a limit, Deviations and Not verified columns, all 0, are left out here. The Team column appears once an arm runs on other assets than the pulled revision.

$ teamai workflow start fix-auth --benchmark --try ../team/benchmarks/fix-auth/tasks/expired-token.yaml --driver local --sandbox
{"data":{"try":{"task":"expired-token","message":"Task \"expired-token\": the check fails at 3f2c9a7 and passes at the fix 9a1d4e7, with the protected paths as 3f2c9a7 has them."}}}
How a run is built, how an arm gets its team assets, and how the page compares
start
  resolveTeam(arm)                      # src/workflow/benchmark/start.ts, before the run exists
    pulled  -> the revision this checkout pulled
    ref     -> resolveTeamRef: a branch of origin, a tag, or a commit on a branch; else arm_team_unresolved
    without -> the pulled revision; each skill must be one of it (skills/**/<name>/SKILL.md)
    none    -> no install; refused (arm_team_unclean) when the user-scope install delivers to the arm's tool
compilePlan(plan, committedSuite)       # src/workflow/benchmark/plan.ts
  baselines.<task>                      # fresh clone, check.add written, setup, check: must fail
  trials.r<rep>.<task>.<arm>
    prompt                              # fresh clone (no remotes), the arm's team install, setup; trial.limits
    check                               # protect restored from startingCommit, check.add written, check.run
  settled                               # waits for required judges, freezes verdicts

GET /api/benchmarks?workspace=<id>&window=90&suite=onboarding-docs
benchmarkHistory                        # src/dashboard/benchmarks.ts, records cached by size and mtime
  BenchmarkHistory.of({ runs, query })  # src/workflow/benchmark/history.ts (pure)
    comparisons                         # current arms that differ in team, tool or model only (an alias counts as one model);
                                        # trials paired by run, task, repetition; Newcombe interval of the paired difference
    teamChanges                         # each tool+model pooled per team revision it ran on
  teamCommits, teamDiffs                # git log / diff --numstat in the team clone, cached per commit
check.add forms and trial limits per tool
check:
  protect: ['test/**', 'fixtures/**']
  add:
    - test/auth/refresh.test.ts                                     # a file of source.fix, at its own path
    - { from: check-files/token.test.js, to: test/token.test.js }   # a file of the suite directory
    - { from: fixtures/, to: fixtures/ }                            # a directory: every file under it, modes kept
Limit Claude OpenCode Codex CodeBuddy
timeout process group stopped same same same
max_turns --max-turns -> limit: max_turns unenforced unenforced unenforced
budget stopped mid-run from streamed usage stopped mid-run from step_finish unenforced; a final total past it records limit: budget same as Codex

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature causing existing behavior to change)
  • Documentation only
  • Refactor / internal cleanup

Test Plan

  • npx tsc --noEmit passes
  • npm run lint passes
  • npx vitest run passes
  • Added/updated tests for the change
npm run build                                 ok
npx vitest run                                19076 passed, 24 skipped; 3 timeouts under load in workflow-reconciliation and workflow-settle-cost pass alone (13)
npm run test:e2e -- benchmark-*               14 files, 120 passed
npm run test:e2e -- dashboard-workflow-runs dashboard-unified   2 files, 5 passed

Real CLI, built dist/index.js:

  • --driver local --sandbox with the host's logged-in claude, on a task whose hidden test comes from check.add { from, to }:

    trial Result Limit Tokens Duration Cost
    none passed — 821 38.9 s 0.1773 USD
    { max_turns: 1 } failed max_turns 186 19.0 s 0.084 USD
    { budget: { tokens: 150 } } failed budget 187 12.7 s unknown (stopped before Claude's result)
  • Team arms, e2e/benchmark-team-arms.test.ts, through the built CLI with a stub tool:

    • a team: [pulled, none, { without: [fixer] }] matrix: each arm's trial saw its own skills, and none saw no .claude/;
    • a ref arm on an earlier commit got that revision's skills;
    • a namespaced skill in without;
    • the refusals: unknown ref, missing skill, none under a user-scope install.

Each real run: baseline valid, the member's git status unchanged, no trial clone left. Codex and OpenCode with real models, the gitlab and github providers, and Windows are left to CI.

Issue criteria and the tests that cover them
Criterion Test
#1180 1, 13: validation at pull and review --branch benchmark-suite.test.ts, review-branch.test.ts, e2e/benchmark-pull.test.ts
#1180 2: trials, rotation, independent trials benchmark-plan.test.ts, e2e/benchmark-lifecycle.test.ts
#1180 3, 4: own clone, no remotes, frozen team revision, env strip e2e/benchmark-team-assets.test.ts, e2e/benchmark-env.test.ts
#1180 5, 6: protect restore, baselines, check could not run benchmark-check.test.ts, e2e/benchmark-checks.test.ts
#1180 7, 8: frozen model per arm, deviations, not verified e2e/benchmark-models.test.ts, benchmark-table.test.ts
#1180 9, 10: correctness judge, no sandbox, unknown tool e2e/benchmark-judges.test.ts, e2e/benchmark-local.test.ts
#1180 11, 12: pull during a run, external driver contract e2e/benchmark-lifecycle.test.ts, e2e/benchmark-driver-contract.test.ts
--try: each outcome and refusal e2e/benchmark-try.test.ts
#1401 check.add: fix paths, suite files and directories, refusals e2e/benchmark-checks.test.ts, benchmark-suite.test.ts, benchmark-plan.type-test.ts
#1400 limits on the local and an external driver, stop races, deadlines on git and setup e2e/benchmark-limits.test.ts, workflow-local-tool-adapter.test.ts, workflow-setup.test.ts
Arm team and matrix: names, refusals, install per arm, record, Team column benchmark-suite.test.ts, benchmark-table.test.ts, e2e/benchmark-team-arms.test.ts
Benchmarks page: Wilson bounds, paired comparisons and their interval, team revisions in commit order with their diffs, zh-CN benchmark-history.test.ts, dashboard-benchmarks.test.ts, dashboard-ui.test.ts, e2e/dashboard-workflow-runs.test.ts

Adversarial cases, each red before its fix: a protected test hidden with assume-unchanged, a clean filter, git replace or a nested repository; a hook in the clone; two next --prepare calls on one trial; a report killed before acceptance; a protected path inside a submodule; a tool or judge that hangs past the suite timeout; a tool stopped holding git's locks; a config that includes a pipe no one writes; two tools on one model alias; a revision only another tool ran on; a run only one arm of a comparison ran.

Related Issues

Closes #1180, closes #1400, closes #1401. Built on #1304: benchmark runs are routed inside WorkflowClient.next / report. #1402 builds on --try, source.fix and check.add.

Notes for Reviewers

  • Door: two-way for runs; one-way for the suite and task file schema, which is strict, so source.fix, check.add, trial, team and matrix ship in this first release. Blast radius: moderate. Everything new sits behind --benchmark, --try or benchmarks/. Shared paths that change for ordinary runs: review --branch checks every selection's sync, a re-init keeps workflows settings, and the git around workflow setup runs without hooks or fsmonitor and stops at the step deadline.
  • Departs from [feat] Run benchmark suites as workflows #1180: it adds the results page, team arms and 95% intervals the issue left out, without recommending an arm (overlapping intervals read not separated). teamai prepares the trial, so a driver does not run init or pull --team-ref.
  • Start reading at src/workflow/benchmark/plan.ts (what a run is), then trial.ts (what a trial gets), table.ts (how results count) and history.ts (the page, pure).

@SaulMoro
SaulMoro force-pushed the feat/1180-benchmark-suites branch from 5959410 to 824495b Compare October 25, 2026 10:27
@SaulMoro SaulMoro assigned jeff-r2026 and unassigned jeff-r2026 Oct 25, 2026
@github-actions

Copy link
Copy Markdown
  • [P1 blocking] Protected submodules can supply modified checks — src/workflow/benchmark/trial.ts:732. If setup initializes a tests submodule and the task uses protect: [tests], an agent can replace tests/check.js with an unconditional success. The superproject’s clean/checkout-index operations do not restore the submodule’s contents, and holds() accepts any directory for mode 160000. The modified check can therefore mark an unchanged, broken implementation passed. Reject protected gitlinks or restore and verify their contents recursively.

The PR description documents sufficient testing: it includes a built real-CLI verification record and representative E2E results. Deferred provider/agent coverage is not blocking.

Reviewed only the specified diff; ran no PR code.

@github-actions

Copy link
Copy Markdown
  • [P1 blocking] Normalize protected paths before checking submodules — src/workflow/benchmark/trial.ts:713. protect: ["./tests/check.js"] is accepted, but reachesInto() compares the initial . with tests and returns false. If setup initializes a tests submodule, the superproject’s listing and cleanup do not restore that nested file. An agent can replace it with unconditional success and receive passed without fixing the implementation. Normalize or reject dot components before matching.

  • [P1 blocking] Enforce the suite deadline during local tool execution — src/workflow/local-driver.ts:1054. The adapter receives only a signal triggered by terminal signals; lease.expiresAt is unused. With timeout: 30s and a hung tool, the driver remains awaiting adapter.run() and never calls next to reconcile the timeout, leaving the run and clone active indefinitely. Abort the launch at the lease deadline and reconcile afterward.

The earlier exact protect: [tests] submodule case is fixed; the dot-prefixed nested-path bypass above remains.

The PR description documents sufficient testing, including a built real-CLI run. Deferred provider/agent coverage is not blocking.

Reviewed only the specified diff; ran no PR code, builds, or installs.

@SaulMoro SaulMoro assigned jeff-r2026 and unassigned jeff-r2026 Oct 25, 2026
@github-actions

Copy link
Copy Markdown
  • [P2 non-blocking] Update the timeout guidance — docs/guide/benchmarks.md:83 and docs/guide/zh-CN/benchmarks.md:83 still say running tools finish first and hung trials block until CI kills them. The local driver now aborts tools at the suite deadline and records the interrupted trial as error. Update both timeout bullets to match.

  • [P3 nit] Escaped globs bypass the submodule guard — src/workflow/benchmark/trial.ts:717. The accepted glob protect: ['te\sts/**'] matches tests/** in Git, but reachesInto() treats the backslash literally and misses a tests submodule. With that unusual spelling and an initialized submodule, modified checks remain unrestored and can yield a false passed. Reject backslashes or handle Git’s glob escaping.

Earlier findings: The exact-submodule case, dot-prefixed path bypass, and local-tool deadline omission are resolved in the current diff.

Testing: The PR description documents sufficient testing, including a built real-CLI run. Deferred provider/agent coverage is not blocking.

Reviewed only the specified diff; ran no PR code, builds, or installs.

@SaulMoro SaulMoro assigned jeff-r2026 and unassigned jeff-r2026 Oct 25, 2026
@github-actions

Copy link
Copy Markdown
  • [P2 non-blocking] Apply the suite deadline to trial installation — src/workflow/benchmark/trial.ts:547. The installation subprocess uses a fixed ten-minute timeout rather than lease.expiresAt. If a 30s suite expires during a slow installation, next --prepare keeps awaiting it, delaying reconciliation, clone cleanup and the results table. Bound installation by the remaining suite time and propagate cancellation.

Earlier findings: All are resolved: protected-submodule handling, dot-prefixed paths, escaped globs, local-tool deadlines, and both languages’ timeout guidance.

Testing: The PR description includes sufficient built real-CLI verification. Deferred provider/agent coverage is not blocking.

Reviewed only the specified diff; ran no PR code, builds or installs.

@SaulMoro
SaulMoro force-pushed the feat/1180-benchmark-suites branch from 621a620 to 79b2e12 Compare October 25, 2026 22:22
@SaulMoro
SaulMoro force-pushed the feat/1180-benchmark-suites branch from e3d7f4a to f7a7737 Compare October 26, 2026 05:04
@SaulMoro
SaulMoro marked this pull request as ready for review October 26, 2026 06:29
@github-actions

Copy link
Copy Markdown
  • [P2 non-blocking] Apply the suite deadline to judging — src/workflow/scoring.ts:1165. With a 30s suite and a slow judge, the local driver awaits judgeLease() without passing the suite deadline, so it cannot call next to reconcile the timeout or print final results until judging finishes. Bound judging by the remaining suite time.

  • [P2 non-blocking] Handle malformed glob classes during validation — src/workflow/benchmark/suite.ts:112. A task with protect: ['test/['], add: ['test/check.js'] and a valid source.fix throws from new RegExp() inside the Zod transform. This interrupts suite validation instead of producing a per-file diagnostic. Validate the pattern or handle the regex error.

Earlier findings: All are resolved in the current diff, including the protected-submodule bypasses, local-tool deadline, bilingual timeout guidance and installation deadline.

Testing: The PR description documents sufficient built real-CLI verification. Deferred provider/agent coverage is not blocking.

Reviewed only the specified diff; ran no PR code, builds or installs.

@github-actions

Copy link
Copy Markdown

No new confident findings in the current diff.

Earlier findings: All are resolved, including the protected-submodule bypasses, local-tool deadline, bilingual timeout guidance, installation deadline, judging deadline, and malformed-glob validation crash.

Testing: The PR description documents sufficient testing, including built real-CLI verification. Coverage deferred to CI is not blocking.

Reviewed only the specified diff against the trusted base review criteria. Ran no PR code, builds, tests, or installs.

@STiFLeR7 STiFLeR7 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary. Adds --benchmark to teamai workflow start: runs YAML-defined benchmark suites (tasks with a starting commit, an optional known-good fix, and a check or success-criteria judge) across tool/model "arms" and repetitions, each trial in an isolated clone with secrets stripped and git hooks/filters/fsmonitor neutered, scored and tabulated. Adds --try to validate one task file's check against its starting commit and fix before it joins a suite. Rebased on #1304, closes #1180.

Verified.

  • Secret stripping genuinely happens before any tool/driver launch and can't be reintroduced by a step's requires.env: negotiate() (src/workflow/requirements.ts:272-276) filters a step's requires.env to exclude every name in the trial's withhold list (stripped) before building the granted lease's envScope.provide — so provide and withhold are disjoint by construction, not just by call order in stepEnvironment (src/env-exec.ts:347-360). TRIAL_WITHHELD_ENV (terms.ts:24-26) is threaded into the plan, prompt/setup scope, and --try. Confirmed by reading e2e/benchmark-env.test.ts in full: none of the withheld names appear in setup/tool/check env even when the team env declares them, and no git hook/fsmonitor/clean filter in the clone sees them either.
  • Isolation is real: freshClone (trial.ts:482-504) git inits a fresh repo, fetches only startingCommit by SHA (never a branch ref) from the run's checkout or a commit cache, and detaches HEAD there — no git remote add is ever called, and a SHA-only fetch cannot pull descendant commits, so the fix commit is never reachable in the trial's clone during the prompt phase. check.add/restoreProtected only write fix-derived bytes in after the prompt's report is already committed, i.e. after the agent's tool process has exited.
  • check.add/source.fix verification is real, not a naive copy: writeAdded (trial.ts:799-823) and restoreProtected (trial.ts:699-751) read the source commit into a private index via read-tree, write with checkout-index --stdin, and verify every written path byte-for-byte against the commit's blob hash via holds() (trial.ts:840-853), including the executable bit and symlink-vs-file kind. Submodule paths are explicitly refused.
  • Path traversal is blocked at the schema level: RepoPathSchema (suite.ts:30-33) rejects empty/./.. segments, backslashes, and a leading :. check.add paths must additionally fall under a check.protect glob.
  • The per-copy lock genuinely serializes prepare/report/check/sweep on the same trial copy without serializing across different trials — confirmed via e2e/benchmark-trial-copy.test.ts (a second concurrent next --prepare gets busy rather than a corrupted/double clone; a killed report can be retried cleanly).
  • Judge-at-timeout doesn't double-score (e2e/benchmark-judges.test.ts: exactly one judge launch, run ends timeout, trial unscored); "Not logged in" doesn't corrupt subsequent trials (verified both by the PR's real-CLI evidence and the general error path in takePrompt/settle).
  • Spot-checked 3 of the Evidence table's test-file references against their actual bodies (benchmark-env.test.ts, benchmark-checks.test.ts, benchmark-judges.test.ts) — all three test exactly what their row claims, including several of the listed adversarial cases.
  • npx tsc --noEmit and npx oxlint --type-aware --deny-warnings --report-unused-disable-directives both ran clean on this branch.

Findings. None I can back with a concrete failure scenario or a cited rule violation. I specifically looked for races in the per-copy lock, double-scoring at judge timeout, "Not logged in" corrupting later trials, and path traversal via check.protect/check.add — all are handled and tested.

Not covered / unverified.

  • Could not execute npm run build or vitest myself in my environment (a sandbox policy against running an unreviewed PR's own code) — static tsc/oxlint are clean, but I don't have independent confirmation of the claimed 578 files, 16565 passed / 383 passed e2e numbers beyond reading the relevant test sources directly.
  • Didn't re-audit the pre-existing, unmodified src/update.ts file-lock internals (acquireLock/releaseLock) — treated as trusted existing infrastructure since this PR doesn't touch it.
  • Didn't review the other ~110 changed files outside src/workflow/benchmark/*.ts (pull/review/doctor wiring, dashboard, docs) at the same depth — focused the time on the new module and its safety-critical claims.
  • TRIAL_WITHHELD_ENV only covers git/CI credential-shaped names, not generic cloud credentials (AWS_*, NPM_TOKEN, etc.). Looks like a deliberate scope choice per terms.ts's own comment ("credentials a trial could push, publish or authenticate with") rather than an oversight — flagging it as a question rather than a finding, in case it's worth widening in a follow-up.

Verdict. Comment (approve-leaning, no blocking findings). The two most safety-critical claims — the secret-strip-wins-over-requires.env guarantee and the no-remote/ancestors-only clone isolation — both check out at the actual git-plumbing and negotiation-logic level, not just by the PR description, and the adversarial test cases listed genuinely exist and test what they claim.

@SaulMoro

Copy link
Copy Markdown
Collaborator Author

@STiFLeR7 thanks for the thorough review. You found nothing blocking, so could you approve it?

@STiFLeR7 STiFLeR7 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed — nothing blocking. Approving.

…nd task

A team that runs a suite every week needs more than the last run's table: whether an arm's lead holds across runs, which task splits the arms, and what changed when a pass rate moved. BenchmarkHistory turns ended run records into that view: suites with their trend, arm series keyed by name, tool, model and effort, pooled pass rates with a 95% Wilson interval over the runs of the current suite version, median usage, task by arm and task by run rates, and the runs list.

Tested with literal expectations in benchmark-history.test.ts (Wilson bounds, usage medians, unstable task counts, suite selection).
… the team reports

GET /api/benchmarks reads the ended benchmark runs on this machine and the run records on teamai-reports, keeps this machine's copy of a run that is in both, and returns BenchmarkHistory for a window of 30, 90 or 365 days and a suite. Records are cached by path and file stamp, and a file without a benchmark field is skipped before it is parsed, so a reload reads only what changed.

Tested in dashboard-benchmarks.test.ts: a local cancelled run with published records (benchmark, other, broken), a re-read after a file changes or is removed, and the query parser.
Suites with their trend; the leading arm and whether its interval separates it from the next; pass rate by arm with the interval; pass rate against tokens, duration or cost with the arms nothing beats on both; pass rate per run with the runs where the suite or the team assets changed; tasks by arm with unstable runs and tasks not measured; tasks per run; the last run's trials by result; and the runs list. Tables become labelled cards on a phone, the charts are drawn at the width they show, and every label has a zh-CN translation.

Tested in dashboard-ui.test.ts and by screenshots at 1440, 834 and 390 px in dark and light themes.
…age through the real CLI

After teamai workflow start fix --benchmark, teamai dashboard's /api/benchmarks lists suite fix with its one run from this machine, the arm's pooled pass rate, the task, and the last run's table.
Six questions a team asks of its benchmark runs, each with the panel that answers it and what it showed, then how the numbers are counted. benchmarks.md links to the page from Results.
…ad, and check its run id

A read that failed was kept as not a benchmark run under the file's size and modification time, so a published record, which never changes, stayed hidden until the dashboard restarted. Now only a file that was read is kept. A record's run id is checked as the other readers check it, since the page opens the run by it.

Tested: the new case in dashboard-benchmarks.test.ts fails with the previous reader and passes with this one.
…e counts them

The Benchmarks page counted an arm's cost over every trial that was not not_run, while a run's table counts the trials whose prompt ran. A trial that errored before its prompt ran made the pooled cost incomplete and turned the Cost axis off, while the last run's table showed a known cost. table.ts now exports the one rule, observedTrials, usageOf and deviationsOf, and the history pools with it.

The page also shows an arm's pooled deviations and trials not verified, how many trials each median used, unknown tokens as unknown, and costs to four decimals as the table prints them. A task a run ended before measuring counts as not measured, as its hatched cell already showed. The scatter no longer changes the chosen axis while it draws, and history.ts is a declared entry of the benchmark folder.

Tested: benchmark-history.test.ts compares the pooled usage with the run table's and counts deviations and unmeasured tasks; benchmark-table, dashboard-ui and workflow-module-boundaries pass.
The Benchmarks page took a suite's version from the hash of suite.yaml alone, so a run after a task file changed was pooled with the runs before it, and the history drew no Suite changed line between them. A version is now the run's sourceHash, over the suite file and each task file, from the same runSourceHash a run is started with.

The cost frontier also skips a weaker arm at the same usage as a stronger one, and the e2e reads the unmeasured counts by the names the API returns.

Tested: a new case in benchmark-history.test.ts (same suite.yaml, edited task: two versions); dashboard-workflow-runs e2e passes.
…eadline

untrustedGit lists the hooks a checkout's config names before it runs the requested git, and only the requested git was bounded by the deadline. A config that includes a pipe no one writes, which setup or a tool can configure, held that listing, and so the step's report and cleanup, forever.

Tested: the new case in workflow-setup.test.ts timed out at 20 s before this change and settles at the 2 s deadline with it; benchmark-limits and benchmark-checks e2e pass.
… matrix

A suite could compare tools and models, but not what teamai itself distributes: every arm ran on the team revision this checkout pulled. An arm now sets team: pulled, none, a branch, tag or commit of the team repo, or { without: [<skill>, ...] }. A suite can list matrix instead of arms, and teamai crosses its tool, model and team values into one arm each, so two arms that differ in one value compare that value alone.

Start resolves each arm's team before the run exists, and refuses a ref the team repo does not have, a skill the pulled revision lacks, and a none arm on a machine whose user-scope install delivers to the arm's tool, where its trials would read team assets from the home directory. A none arm installs nothing; a ref arm installs that revision; a without arm excludes the skills through the selection the install already honours. The record and the run's table carry each arm's team, and a trial on other team assets than its arm's counts as a deviation.

Tested: benchmark-suite, benchmark-plan and benchmark-table cover the new keys, names and refusals; the new benchmark-team-arms e2e runs a tool x team matrix, a ref arm and the three refusals through the built CLI.
…w a tool across team revisions

The history pooled each arm on its own, so it could not say how much the team's assets added, or whether a change to them helped. It now lists, for the suite's current version, every two arms that differ only in their team assets, tool or model, pairs their trials by run, task and repetition, and gives the difference a 95% interval for paired proportions (Newcombe, method 10). It also pools each tool and model by the team revision it ran on, for the configurations that ran on more than one, and records each run's tool versions.

Tested: benchmark-history.test.ts covers the comparisons of a tool x team matrix, the paired interval against worked values, team revisions across runs and within one, and tool versions per run.
…one, with the files it changed

A team revision is a SHA in a run record. The Benchmarks API now reads each revision the selected suite ran on from this machine's team clone: its commit date and subject, and the files outside benchmarks/ it changed since the revision before it, relative to the team directory. The revisions are in commit order when the clone has every one, and in run order otherwise. What git answered is kept for the process, since a commit never changes; a revision the clone lacked is asked again, as a pull may bring it.

Tested: two cases in dashboard-benchmarks.test.ts, on a real team clone: revisions that ran newest first come back in commit order with their changed files, and one the clone lacks keeps its run order and no name.
…page

The Benchmarks page answered questions about tools and models. Two tabs beside the existing Arms panels now answer the questions about the team's own assets, under the same sidebar entry.

Compare picks a dimension (team assets, tool or model) and shows the chosen pair's difference with its interval, pass rate, cost per passed trial and tasks up or down; a forest plot of every pair; arrows from one arm to the other on pass rate against tokens, duration or cost; and each task's change. Team changes picks a tool and model and draws its pass rate per team revision in commit order, marking a revision told apart from the one before it; clicking one lists the team files it changed and each task's rate before and after. Pass rate per run also marks a tool update. Each tab explains, with a suite example, how to get data into it when it has none. The chosen tab is remembered, and every new label has its zh-CN text.

Tested: dashboard-ui checks every label has a zh-CN translation; dashboard-workflow-runs and dashboard-unified e2e pass; the tabs were checked at desktop, tablet and phone widths, dark and light, on a seeded workspace.
… changes tabs

Examples first: a suite with an arm on each kind of team assets, a tool x team matrix with the arms it makes, and the questions each new tab answered on a seeded workspace. English and zh-CN.
…team ref did not resolve

A record kept only the model each arm's alias resolved to, so claude and codex on alias strong had different models, and Compare never compared the tools of a suite that named a model, which is what the suite parser asks for when the tool varies. The plan and the record now keep the alias as alias, and two arms on one alias differ only in tool.

Start reported every failure to resolve an arm's team ref as no branch, tag or commit. It now names the team repo's branches when the ref is not one, and refuses with arm_team_unreadable, with git's message, when git could not read the team repo, also for a without arm whose skills git could not list.

Tested: a new case in benchmark-history.test.ts (two tools on alias strong compare by tool; codex on strong and fast by model) failed before the change; benchmark-models e2e checks the alias in the record, and benchmark-team-arms e2e the new message.
…ool ran on before

Team changes compared a revision with the one the chosen tool and model ran on before it, but listed the files changed since the revision before it among all, which another tool may have run alone. The API now returns teamDiffs by <before>..<after> for each two revisions a tool and model ran on in turn, and the panel reads the pair it shows.

Also: the twelve arms a matrix can make get twelve colours, a difference that rounds to zero prints 0 pp without a sign, a cost change from zero shows no percentage, and the change column and the told-apart tags have their zh-CN text.

Tested: a new case in dashboard-benchmarks.test.ts (claude on A and C, codex on B and C: A..C and B..C, not A..B) failed before the change; dashboard-ui passes.
Start looked for skills only at skills/<name>/SKILL.md, so an arm without a skill the team keeps under a role or project namespace, skills/<namespace>/<name>/, was refused though pull delivers it by that name. A skill's name is now the directory that holds its SKILL.md, at any depth under skills/.

Tested: a new benchmark-team-arms e2e case (without: [nested], with skills/dev/nested/SKILL.md) was refused before the change and runs with it.
…ials

Compare drew its arrows and its cost and token cards from each arm's pooled series, which also counts runs the other arm did not run, while its difference counted the paired trials only: the two could disagree. A comparison now carries each arm's usage over the paired trials, as the run table counts it, and the arrows and cards read its paired rates and usage. Cost per passed trial is the cost of those trials over the ones that passed, not the mean cost of a launched trial over the pass rate of planned ones, which inflated it when a trial never launched.

Tested: a new case in benchmark-history.test.ts (claude alone in one run, claude and codex in the next: the comparison's usage is the second run's) failed before the change; dashboard-ui passes, and the Compare tab was checked on a seeded workspace.
 asks

stepEnvironment now takes the team env's values and facts, so a provided key that lost its value gets the env add advice for an undeclared key. A trial's setup, check and pull pass the run's team env; a step environment that only withholds keys passes no team env, and --try the environment's keys as present.

Tested: tsc and lint clean; the benchmark e2e and unit suites run in the gates below.
…encent#1534 lets it fail init

initHttp no longer swallows a local agent setup that fails, and this file's fs mock has no readJson, so the case that checks a re-init keeps the member's workflow settings failed on the setup it does not test.

Tested: init.test.ts passes (85).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants