Repository navigation
v0.2.0
A release with breaking changes that closes the loop in CI: critical: true tasks, argument matchers and per-task given, run --baseline with what changed per task, passed, failed, flaky and skipped verdicts with errored trials skipped, exit 1 for a failed gate and 2 for an error, init drafts that keep their sources' facts, show their plan and cost with --dry-run and print what they cost, a simulated caller that says it doesn't know instead of guessing, and a Claude Code plugin installed from the release; read the seven Breaking: entries under Changed before upgrading.
Added
- Domain spec: a task's
calledandnot_calledentries can say which call, as{ tool, args }with a where clause on the call's arguments, such ascalled: [{ tool: transfer_to_human, args: { mc_number: "100101" } }]ornot_called: [{ tool: book_load, args: { load_id: L-1004 } }]. A failed check says how the closest call differed, andvalidaterejects an argument the tool doesn't have or an unknown operator. Plain tool names work as before, and the library exports the entry's schema asCallExpectSpec(#25). - Critical tasks:
critical: trueon a task marks a release blocker.sandtable runexits 1 when any trial of a critical task fails, with or without--fail-under, and names those tasks. The terminal summary and the HTML report list critical tasks first and mark them,results.jsoncarries"critical": trueon them, and the library exportscriticalFailures. The bundled freight and clinic examples mark three refusals each as critical, so their naive agents' runs now exit 1 (#28). - Domain spec: a task can change the world it starts in with
given, instead of a seed row only it uses or a copy of the domain.given.rowsmerges rows into the seed by each table's key for that task only: a seeded key patches the fields listed (the same carrier withinsurance_status: expired), a new key adds a row.given.context, such as a caller ID or a trigger payload, is given to the agent and not to the simulated user: it follows the policy in the default system prompt, and an agent over HTTP or a JS/TS module agent receives it ascontext.lintandregradeuse the patched world, and a value in the context counts as shown fornever_disclose(#37). sandtable run --baseline <results.json>compares the run with an earlier run of the same domain, such as the last run onmain. The summary lists each task that broke, got worse, got fixed or got better, new failures first, counts added, removed and unchanged tasks, and shows pass^1 and pass^k before and after over the tasks both runs ran. A task whose test changed since the baseline is listed aschangedinstead of fixed or broken, and left out of the scores: each task inresults.jsoncarries two short hashes,inputs(what the agent and caller are given and the world they act in) andgrading(its expectations and the invariants), and a task whose hash differs from the baseline's changed. The summary also names what else differs: the agent, the simulated user, k, and how many tasks' tests changed; a baseline written by 0.1.x has no such hashes, and the summary says it can't tell. The baseline is read before any trial runs, so a wrong file or another domain's run costs no model calls, and the exit code is unchanged.results.jsonkeeps the baseline's per-task results asbaseline, the HTML report opens with the changes and marks regressed tasks, andreportandregradeshow them again without the baseline's file. The library exportscompareRunsandbaselineSnapshot(#12, #309).sandtable run: each task gets a verdict over its graded trials,passed,failed,flakyorskipped, and a 95% Wilson interval for its pass rate, and runs report pass@k next to pass^k, the flip rate (the share of tasks that are flaky) and the flaky tasks. The terminal summary and the HTML report show them, andresults.jsonaddspassAt,flipRate,flakyandskipped, and per taskgraded,skipped,verdictandinterval. The library exportspassAtK,wilsonInterval,trialStatusand theVerdicttype (#27).- Domain spec: a
referencecall can be one the API rejects, markedexpect_error: trueor with text its error contains, such asexpect_error: not found.lintthen requires that call to fail, so a refusal task whose correct agent looks up an unknown ID is provensolvableinstead ofsolvable ?, and a domain edit that drops the rejection is caught. A call with arguments the tool rejects is still alinterror. The freight example'sunknown-mc-numberuses it, and the library exportsReferenceCallSpec(#274). sandtable init: after--sopand after--transcripts, it prints the step's token usage per model and an estimated cost at list prices, asrundoes: one Claude call for the SOP and one per transcript, so an engineer can see what a draft cost before drafting from many more transcripts (#297).sandtable init --dry-run: with--sopor--transcripts, prints a plan line (the model calls, 1 for the SOP and 1 per transcript, the model, the provider and region, and a cost bound at list prices) and stops: no model call, nothing written, and no API key, AWS credentials or Bedrock package needed. With--openapi, it lists the files it would write. A real run prints the same plan line before its first model call (#296).init --transcripts: each drafted task getsreferencecalls, the tool calls a correct agent makes in order, solintcan prove it solvable instead of reportingsolvable ?.initlints each drafted task, replaying its reference, and keeps one that fails for review, with a warning and a# TODO(review): lint: ...comment above it. References list only calls that succeed, so a refusal whose correct path includes a call the API rejects may have lookups only. A drafted range, such as a negotiated rate between two bounds, now keeps both bounds; before, only the last condition on a field was kept (#278).
Changed
- Breaking:
sandtable run: a trial that ends in an error, such as a rate limit or timeout that outlasted the retries or an agent endpoint that went down, is now skipped instead of failed, unless the agent had already broken a hard invariant or called anot_calledtool. A skipped trial is left out of pass^k, pass@k and the per-tag scores, carriesskipped: trueinresults.jsonand is listed under Skipped trials, andrunexits 2 when any trial was skipped, even without--fail-under, because the run is incomplete. A CI job that passed with a few errored trials now fails: fix the cause, such as credentials or a lower--concurrency, and rerun (#27). - Breaking: library: in the exported
Tasktype,expect.calledandexpect.not_calledentries arestring | { tool, args }(the newCallExpect) instead ofstring, since a task can match a call's arguments. TypeScript code that reads them as tool names must handle the object form, e.g.typeof e === "string" ? e : e.tool. Domains that list tool names are unchanged (#25). - Breaking:
examples/freightandexamples/clinic: three refusal tasks in each are nowcritical: true, so a run of their naive scripted agents (--agent scripted:naiveorscripted:clinic-naive) exits 1 instead of 0. A script that renders their reports should accept a failed gate;demoand the careful agents are unaffected (#28). - Breaking:
sandtable validate: a domain with an unknown operator,$inor$ninwithout a list, or a top-level$key in any where clause now fails validation, so no command loads it. On 0.1.x it loaded, then crashed mid-run or silently matched nothing. Fix the clause at the pathvalidatenames (#279). - Breaking: Simulated caller: its prompt changed, and the prompt is part of a run's
inputshash. Sosandtable regraderefuses a run stored by 0.1.x, andsandtable leaderboardstops without running anything when a model's stored results come from 0.1.x. Regrade such a run with the version that made it (npx sandtable-ai@0.1.2 regrade …), and for a board, move those models' folders away or pass--no-resume, which reruns every model and costs model calls. Scores of new runs can differ from 0.1.x runs of the same agent, since the caller now says it doesn't know a fact its instructions leave out;run --baselineagainst a 0.1.x run says the prompts changed. Review tasks whose caller instructions leave out a fact the agent will ask about, such as an item's condition, a receipt or a date, and add it touser.instructions(#287). - Breaking: CLI exit codes: a failed gate exits 1 and an error exits 2, as
grepanddiffdo. 1 means a gate failed on a complete result:runbelow--fail-underor with a failed critical task,linterrors,validate --strictwith pending items, andvalidateorlinton a domain that exists but is invalid. 2 means the command couldn't do its job: a usage error (an unknown command or option, a missing argument, an invalid value, or no command at all), a missing or unreadable file, no domain at the path, an invalid domain for any other command, an adapter that can't be built, arunordemo --livein which every trial ended in an error, arunwith skipped trials (even when a gate failed too), and aleaderboardon which every model failed. Before, all of these exited 1. Help,--versionand 141 for a closed pipe are unchanged. A CI step that treats any non-zero status as a failure needs no change; one that checks for 1 to mean an error must accept 2, and one that lets a failing agent through, to render its report, can accept only 1 with|| [ $? -eq 1 ](#282). - Status: the README, the docs and the PyPI classifier describe sandtable as beta instead of early alpha. It works and is tested, and the spec and outputs may still change before 1.0 (#267).
- Leaderboard docs: the freight board's history includes its re-render at v0.1.2, which ran no model and changed no verdict, and
leaderboard.jsonis described as recording 0.1.2 ateb4cfd2(#260). - Roadmap: the v0.3 section of
ROADMAP.mdand sandtable.dev/roadmap separates what is done onmainand not yet released (run --baseline, reliability verdicts, critical tasks, argument matchers andgiven) from what is left, including the follow-ups filed since: a regression gate, exit codes, per-tag floors, the leaderboard anddemo --live, andsandtable compare(#288). - Claude Code plugin: after a fix,
running-and-reading-resultsand thefailure-triagersubagent confirm a change to grading only (an invariant, an expectation, anever_discloserule) withsandtable regrade, which calls no model, and rerun with--tasksonly the tasks whose caller instructions, tools, policy or agent changed. They fix grading and regrade before changing a caller's instructions, sinceregraderefuses a run once any caller prompt changed (#300). - Claude Code plugin: attribution no longer blames an agent that stops because the records contradict the request:
running-and-reading-resultschecks the seed rows andgiven.rowsagainst the caller's instructions, and a disagreement is the task's fault. A fact the caller states that its instructions lack is fixed in the task's instructions. Before recommending a model from a leaderboard, the skill hasfailure-triagerattribute every failure of the top candidates, since a cheaper model's only failure may be the task's (#299). - Breaking:
sandtable report -o: a path that doesn't end in.htmlis now a folder, and the report goes in it asreport.html, asrun -oandregrade -otake a folder. Before,report results.json -o runs/rerenderwrote an HTML file namedrerenderwith no extension. A path ending in.htmlstill names the file itself, so a script that passes a file name without.htmlshould add it (#317). sandtable demo --live: the simulated caller now defaults to Claude Haiku 5.5 instead of the agent's Claude Opus 5.5, about half the cost of a first live run, as the docs already recommended with--user-model claude-haiku-5-5.--user-modelstill picks another caller, the agent keeps its default, andrun --user claudeis unchanged (#329).- Claude Code plugin: the
drafting-a-domainskill says how to install the CLI with its optional packages. Prefer a project install (npm install --save-dev sandtable-ai @anthropic-ai/bedrock-sdk, thennpx sandtable-ai <command>). It lists which package each path needs:@anthropic-ai/bedrock-sdkfor--provider bedrock,@aws-sdk/client-bedrock-runtimefor abedrock:agent and@aws-sdk/credential-providersfor--aws-profile. Without an install, the form isnpx -p sandtable-ai -p <package> sandtable <command>, where the command is the bin,sandtable. It also says to keep runs inside the project. An agent no longer has to read sandtable's compiled code to runiniton Bedrock (#295). - Claude Code plugin: before
init --soporinit --transcripts, thedrafting-a-domainskill has the agent run the same command with--dry-runand show the engineer its plan line (model calls, model, provider and a cost bound), even when model steps are already authorized. When they are, it goes on to the paid run after the plan, instead of reporting the cost afterwards; otherwise it asks. It suggests-m claude-sonnet-5-5for a first draft of many transcripts.initthen prints what the step actually cost (#296). - Claude Code plugin: the review checklists of the
drafting-a-domainskill, and the review workflow docs, gain the checks a run on real support chats needed. A drafted task keeps its transcript's facts and outcome, every row it touches agrees with the transcript, and its caller's instructions state every fact the agent will ask about. An ambiguous case in the SOP goes to the customer, and until then the task grades only what every reading agrees on. A drafted invariant that requires a step the agent can skip while still deciding correctly becomessoft: true, or checks the decision itself (#298).
Fixed
- Docs: sandtable.dev documents the latest release instead of
main, so a user of the published CLI no longer finds flags and spec keys it doesn't have yet, such as--baselineorcritical: truewhile npm served 0.1.2. The site deploys from adocs-livebranch that each stable release moves to its tag (#313). sandtable validate: an unknown operator such as$like,$inor$ninwithout a list, or a top-level key such as$or, now fails validation in every where clause, with the clause's path: a tool'slistop andchecks, invariantrequire.whereandnever.when, and a task'sexpect.statewhereandfields. Before, only acalled/not_calledentry'sargswere checked: elsewhere,lintandrunstopped on a bare "Unknown operator" without writing results, or a clause matched nothing and a valid call was rejected (#279).- CLI: a path that names nothing now says which argument or option it came from, instead of Node's
ENOENT: no such file or directory, open '…':report,regrade,run --pricesand--baseline,leaderboard -mand--prices, andinit --openapi,--sopand--transcripts, e.g.--prices: no file at prices.json. A directory given where a file is expected says so, and points at theresults.jsoninside it when there is one.initreads every input before drafting, so a wrong--transcriptspath no longer fails after--sophas called a model, and a--transcriptsdirectory with no.txtor.mdfile is named.report -ocreates the directory it writes into, asrun -odoes (#280). init --transcripts: a drafted task keeps the facts its transcript states. An existing row is reused only when every fact the transcript states about it matches, so a second caller with the same name but another membership level or order gets a new row instead of a task rewritten to fit the first one. When a draft still reuses a row that contradicts its transcript,initkeeps the task for review, prints a warning naming the row and each differing field, and writes it as a# TODO(review):comment above the task (#276).init --sop: every rule the SOP states now ends up as an invariant or aSoft rulecomment. Before, a rule it couldn't express, such as a cancellation window with an exception, was in neither list. Claude lists the SOP's rules first and maps each one. Each soft rule comment says why no invariant checks it, such asneeds today's date,depends on a fact no record holdsorneeds either of several tools first. A rule whose drafted invariant was dropped, or that the draft mapped nowhere, is listed too, with a warning (#277).- Simulated caller: a fact its instructions don't state about the caller, its account or what it is calling about is now unknown to it, so it answers "I'm not sure" instead of guessing, also to yes-or-no questions such as an item's packaging or a receipt. It also no longer describes the agent or explains how it is answering. Reports flag the turns where it still steps out of its role, talking about its instructions, saying its goal is met or talking about the agent in the third person: the terminal summary and the HTML report list them as Caller notes and mark them in the transcript, and
results.jsonhascallerNotesper trial and a count per run. They never change a verdict. The library exportscallerNotes. Runs stored by 0.1.x can't be regraded or resumed by this version: see the Breaking: entry under Changed (#287). - Claude Code plugin: the marketplace installs the plugin from
plugin/at the latest release's tag instead of frommain, so its skills describe the CLI thatnpx sandtable-airuns and its version is that release's. Before, they could document features onlymainhad, such asgivenandcritical: trueunder 0.1.2, and an agent that found them missing guessed which features existed. Existing installs get the pinned plugin with/plugin marketplace update sandtable. The skills now say to learn what the installed CLI supports fromnpx sandtable-ai --version,--helpandvalidate, never from sandtable's own code, in npx's cache or in the project'snode_modules/sandtable-ai, and that a Claude model on Bedrock takes its plain name (-m claude-sonnet-5-5), which sandtable maps to the region's inference profile. Thewriting-good-tasksandrunning-and-reading-resultsskills also load for requests about release-blocker (critical) tasks and CI gates, so their guidance reaches the agent when it's asked to make a task a release blocker. To try the skills from a clone, start Claude Code withclaude --plugin-dir ./plugin(#301). sandtable run --agent <module>: a.tsor.jsagent that Node loads as CommonJS, as in a project made withnpm init("type": "commonjs"), now fails with what to do: rename it to.mtsor.mjs, or set"type": "module"in the nearest package.json. Before, it showed only Node'sUnexpected token 'export'. On a Node too old to load TypeScript, the error says it needs Node 22.18 or later. The bring-your-agent guide and the README say a module agent must be an ES module (#305).sandtable runanddemo --live: before the first trial, they check that the agent and the simulated user can start, and when one can't, they name it and the problem, run nothing, write nothing and exit 2. Before, a missingANTHROPIC_API_KEYmade every trial fail one by one (demo --liveskipped all 50 before saying why), and when only one side lacked credentials, the other side's calls were made and billed in every trial. The checks cost nothing: Claude on the Anthropic API lists one model, Claude on Bedrock andbedrock:agents resolve their AWS credentials, region and optional package locally, and an HTTP agent must accept a connection, with no request sent (#316).- Domain errors: a schema or cross-reference error in an included file, such as a task in
tasks.yamlwith an unknown key or an argument its tool doesn't have, now names that file instead ofsandtable.yaml, as YAML syntax errors already did. Problems in several files are listed under each one.lintsays "needs a row inorders" instead of "needs a orders row" (#317). - Agents and callers on other models: reasoning a model writes into its reply text no longer reaches the other side of the conversation or the graders. The
openai:adapter removed only<think>blocks, so gpt-oss through Bedrock's OpenAI-compatible endpoint, which writes<reasoning>, leaked its reasoning: as the agent it was scored as speech (never_discloseincluded), and as the simulated caller it quoted its hidden instructions to the agent. The Bedrock Converse agent (bedrock:<model-id>) removed only<reasoning>, and missed gpt-oss's occasional<analysis>. Both adapters, for the agent and for theopenai:caller, now remove<think>,<thinking>,<reasoning>and<analysis>blocks, a closing tag whose block opened before the reply, and an opening tag that never closes with everything after it.usagecounts the replies that had reasoning removed asreasoningStripped, shown in the terminal summary and the HTML report. Runs of such models through these adapters scored leaked reasoning as speech before;regradecan't change that, since the stored transcript holds the leaked text and the conversation would have gone otherwise, so rerun them. On the published freight leaderboard, one of gpt-oss-120b'snever-disclose-ceilingfailures came from an<analysis>block (#308). init --sop: a drafted invariant's conditions can use$in,$ninand$exists, so a rule that a call must include an argument in some cases, such as a refund by check needing the mailing address, drafts as aneverwithwhen: { method: { $in: [...] }, mailing_address: { $exists: false } }instead of a soft rule that says it can't be expressed. Two conditions on the same field, such as a lower and an upper bound, are both kept; before, the second replaced the first (#314).docs/quickstart: the toy HTTP agent, in Node and in Python, remembers the carrier it verified under thesession_idsandtable sends with every step, so after a verified MC number it says a rep will finish the booking instead of asking for the MC number again on every turn. It still scores pass^1 36.0% on the freight example (#270).init --transcripts: the drafting prompt now shows each tool param's allowed values, whether it's optional and its description, asinit --sop's does, so drafted references use the API's values (by_mail) instead of the transcript's wording ("By Mail"), which failed the draft-time lint. A drafted value that differs from exactly one allowed value only in case, spaces or dashes is replaced by it, with a warning, and drafted end-state conditions can usein,ninandexistslikeinit --sop's (#336).- Claude Code plugin: the drafting and results skills say what to do when a command fails with
OpenSSL configuration error … openssl.cnf … Operation not permitted: Claude Code's sandbox blocked Node from reading Homebrew OpenSSL's config, so rerun it withOPENSSL_CONF=/dev/nullin front (#334).
What's Changed
Other changes
- docs(docs): the leaderboard page records the board's re-render at v0.1.2 by @Uliseis in #264
- docs: sandtable is described as beta, not early alpha by @Uliseis in #268
- feat(grading): expect.called and expect.not_called can match tool arguments by @Uliseis in #269
- feat(grading): a critical task fails the run when any of its trials fails by @Uliseis in #271
- feat(spec): a task can patch seed rows and give the agent call context with given by @Uliseis in #272
- feat(run): run --baseline shows which tasks broke or got fixed since a previous run by @Uliseis in #273
- feat(grading)!: errored trials are skipped instead of failed, and tasks get passed, failed, flaky or skipped verdicts by @Uliseis in #275
- docs: the roadmap shows which v0.3 items are done and which are left by @Uliseis in #289
- fix(spec): validate rejects an unknown operator or $in without a list in any where clause by @Uliseis in #290
- fix(cli): a missing file names the option it came from instead of printing ENOENT by @Uliseis in #291
- fix(init): init --transcripts keeps a transcript's facts and warns when a reused row contradicts them by @Uliseis in #292
- fix(init): init --sop lists every rule it can't check as a soft rule, with the reason by @Uliseis in #293
- fix(adapters): the simulated caller says it doesn't know what its instructions don't state, and reports flag turns where it steps out of role by @Uliseis in #294
- docs: the changelog marks every change since 0.1.2 that can break an existing setup by @Uliseis in #304
- fix(plugin): the marketplace installs the plugin from the latest release's tag, so its skills match the CLI npx runs by @Uliseis in #306
- feat(cli)!: a failed gate exits 1 and an error exits 2, as grep and diff do by @Uliseis in #307
- docs(plugin): the skills confirm a grading-only fix with regrade, and rerun only tasks whose inputs changed by @Uliseis in #310
- docs(plugin): attribution doesn't blame an agent that stops because the records contradict the request by @Uliseis in #311
- fix(report): run --baseline labels a task whose test changed as changed, not fixed or broken by @Uliseis in #326
- fix(adapters): a module agent that Node loads as CommonJS says to use .mts or type module by @Uliseis in #319
- fix(run): run and demo --live check the agent's and caller's credentials before the first trial by @Uliseis in #325
- fix(cli)!: report -o takes a folder as run -o does, and domain errors name the file they're in by @Uliseis in #328
- fix(adapters): reasoning a model writes into its reply never reaches the other side, in the openai: adapter and Converse by @Uliseis in #322
- feat(spec): a reference call can expect the API to reject it, so lint proves refusal tasks solvable by @Uliseis in #323
- fix(init): init --sop drafts "a call like this must include Y" as never with $in and $exists by @Uliseis in #321
- feat(init): init prints each drafting step's token usage and estimated cost by @Uliseis in #324
- feat(init): init --transcripts drafts reference calls and lints each drafted task by @Uliseis in #327
- docs(docs): the quickstart's toy agent remembers the verified carrier per session instead of asking for the MC number every turn by @Uliseis in #320
- feat(cli): demo --live plays the caller with Claude Haiku 5.5 by default by @Uliseis in #330
- fix(docs): sandtable.dev documents the latest release, deployed from a docs-live branch each release moves by @Uliseis in #331
- docs(plugin): the drafting skill says how to install the CLI with its optional packages by @Uliseis in #312
- docs(plugin): before a paid init step, the drafting skill states the calls, the model and a cost bound by @Uliseis in #315
- docs(plugin): the review checklists check a drafted task against its transcript, and a step-only rule becomes soft by @Uliseis in #318
- fix(init): init --transcripts shows the model each param's allowed values, so drafted references use them by @Uliseis in #337
- fix(plugin): the plugin evals run the CLI in Claude Code's sandbox, and the skills say what to do on an OpenSSL config error by @Uliseis in #338
- docs(plugin): the skills never read sandtable's own code to learn what the CLI supports, and say how Bedrock model names work by @Uliseis in #340
- feat(init): init --dry-run prints the drafting plan and its cost bound, and the drafting skill runs it before a paid init by @Uliseis in #339
- chore(release): v0.2.0 by @Uliseis in #335
Full Changelog: v0.1.2...v0.2.0