static-fire 0.0.0-stage → 0.54.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +236 -2
  3. package/bin/static-fire.js +46 -0
  4. package/package.json +30 -4
  5. package/pricing.yaml +20 -0
  6. package/src/adapters/index.ts +153 -0
  7. package/src/api.ts +250 -0
  8. package/src/cli.ts +383 -0
  9. package/src/core/apkit-install.ts +47 -0
  10. package/src/core/authored.ts +47 -0
  11. package/src/core/batch-state.ts +25 -0
  12. package/src/core/batch.ts +450 -0
  13. package/src/core/batches.ts +221 -0
  14. package/src/core/checks.ts +63 -0
  15. package/src/core/commands.ts +43 -0
  16. package/src/core/compare.ts +206 -0
  17. package/src/core/config.ts +25 -0
  18. package/src/core/copying.ts +18 -0
  19. package/src/core/coverage.ts +84 -0
  20. package/src/core/db.ts +608 -0
  21. package/src/core/derived.ts +74 -0
  22. package/src/core/engine.ts +85 -0
  23. package/src/core/facets.ts +143 -0
  24. package/src/core/grade.ts +197 -0
  25. package/src/core/grading-versions.ts +281 -0
  26. package/src/core/identity.ts +263 -0
  27. package/src/core/init.ts +321 -0
  28. package/src/core/judge-schema.ts +37 -0
  29. package/src/core/judge.ts +166 -0
  30. package/src/core/label.ts +43 -0
  31. package/src/core/lock.ts +40 -0
  32. package/src/core/migrate.ts +288 -0
  33. package/src/core/new-task.ts +85 -0
  34. package/src/core/outside-tools.ts +129 -0
  35. package/src/core/plan.ts +84 -0
  36. package/src/core/profiles.ts +42 -0
  37. package/src/core/project.ts +140 -0
  38. package/src/core/regrade.ts +242 -0
  39. package/src/core/score.ts +148 -0
  40. package/src/core/settings.ts +44 -0
  41. package/src/core/task-file.ts +95 -0
  42. package/src/core/task-page.ts +54 -0
  43. package/src/core/trial-detail.ts +73 -0
  44. package/src/core/types.ts +195 -0
  45. package/src/core/view-query.ts +102 -0
  46. package/src/core/view.ts +576 -0
  47. package/src/guide.md +624 -0
  48. package/src/server.ts +28 -0
  49. package/web/api.ts +6 -0
  50. package/web/app.tsx +1162 -0
  51. package/web/brand/offset-tokens.css +126 -0
  52. package/web/charts.tsx +190 -0
  53. package/web/compare.tsx +317 -0
  54. package/web/encoding.tsx +55 -0
  55. package/web/filter-menu.ts +195 -0
  56. package/web/how-it-works.tsx +92 -0
  57. package/web/index-switches.ts +29 -0
  58. package/web/index.html +17 -0
  59. package/web/levels.tsx +15 -0
  60. package/web/notices.ts +74 -0
  61. package/web/output-view.ts +61 -0
  62. package/web/pages.ts +42 -0
  63. package/web/run-status.ts +51 -0
  64. package/web/styles.css +368 -0
  65. package/web/task-card.ts +6 -0
  66. package/web/tasks.tsx +122 -0
  67. package/web/wording.ts +38 -0
package/src/guide.md ADDED
@@ -0,0 +1,624 @@
1
+ # Static Fire: the guide for agents
2
+
3
+ Static Fire compares agent configurations (harness, model, effort) on tasks from the user's own work. It
4
+ runs each configuration on the same task, grades every output the same way, and shows the scores against
5
+ the cost on a dashboard.
6
+
7
+ You are the agent that takes the user from a goal, such as "I want to test which agent is the best at making
8
+ thumbnails", to a dashboard with data. The user gives the goal, examples, what good output looks like, and
9
+ approvals. You do the rest: you make the files, run the commands and read the results. Ask the user only for
10
+ what you cannot find out yourself. This guide is the whole manual. You do not need the engine source.
11
+
12
+ The first part is the path, one step at a time. Each step says what it is for, the facts you need, and the
13
+ safe default. How you talk with the user is yours to decide. The second part is the reference: every file
14
+ format and command.
15
+
16
+ Run every command as `static-fire <command>`, or `npx static-fire <command>` when `static-fire` is not
17
+ installed.
18
+
19
+ **Which project a command uses.** A command uses the nearest project above the current folder (a folder
20
+ with `static-fire.yaml`, the current one included). Outside any project it uses the home, the one project
21
+ the user keeps all their tasks and results in, and prints `using the home project: <folder>` on stderr.
22
+ `static-fire home` prints the home; `static-fire home <folder>` sets it.
23
+
24
+ ## The rule above all: realism
25
+
26
+ A trial must be as close as possible to the user's real job: the same inputs, tools, context and
27
+ instructions they would give their agent, and graders that read the output as its real readers would. A
28
+ score is worth something only when it predicts how a configuration does on the user's real work. Each
29
+ difference from the real job is a known gap. Write each gap of a task in the project's `README.md` (make
30
+ the file when the first gap comes up), so the user can see what the score does not cover.
31
+
32
+ ## What spends
33
+
34
+ These start agent runs or judge calls, which cost money or use up the user's plan:
35
+
36
+ - `static-fire run`: agent runs, and judge calls to grade each trial.
37
+ - `static-fire rerun <batch>`: the failed trials of the batch again, with their judge calls.
38
+ - `static-fire regrade`: judge calls only, for each trial it grades again.
39
+ - On the dashboard: New run on the Runs page, and Regrade on the Results page.
40
+
41
+ The rule: get the user's approval before each one starts, every time, with what it will run (step 6). An
42
+ approval covers the one command it was given for. `run --plan` and `regrade --plan` print the plan and
43
+ spend nothing.
44
+
45
+ ## The path
46
+
47
+ ### 1. Learn the goal, the examples and what good looks like
48
+
49
+ **Why.** A task is only as good as its link to real work. The user knows the job, its inputs and its readers.
50
+ You do not.
51
+
52
+ **What you need from the user:**
53
+
54
+ - What job they want to test, and why it matters to them.
55
+ - One or more real examples of the inputs: the notes, brief, files or links they would give their agent.
56
+ - What good output looks like, and what makes an output fail. A past output they liked, or one they did not
57
+ like, helps a lot.
58
+ - Who reads the output in real life, and how. A viewer who sees a thumbnail at 320 px, an editor who skims
59
+ the headings, a client who checks every number.
60
+ - Which configurations they want to compare, if they know. `init` finds the harnesses on the machine.
61
+
62
+ **Facts.** Tasks from the user's real work are the point of Static Fire and its value: the scores then say
63
+ how agents do on the work the user cares about. Any source is allowed. A public benchmark item or a made-up
64
+ job is fine when the user wants it. The user and you own the quality of the task. Static Fire does not judge
65
+ it.
66
+
67
+ **Safe default.** Ask for real examples first. When the user has none at hand, offer to look for material
68
+ on the machine with their permission, or to build the task from a description and record that as a gap.
69
+ Do not invent the user's taste: ask what good looks like.
70
+
71
+ ### 2. Set up the project and settle the block list
72
+
73
+ **Why.** A project is the folder that holds the tasks, the configurations and the records. Most users need
74
+ one: the home. A trial runs with no permission prompts, so nobody is there to approve a tool that sends
75
+ mail, posts a message, spends money or deploys. The block list stops those tools in trials.
76
+
77
+ **Do.** Run `static-fire init --json`. It never waits for input and never changes a file that exists.
78
+
79
+ - With no home, outside any project, it makes the project in `~/static-fire` and saves it as the home.
80
+ `static-fire init <folder> --json` makes it in that folder instead, and saves that as the home.
81
+ - With a home, outside any project, it writes nothing and reports the home. `static-fire init <folder>`
82
+ then makes a second project in that folder, with its own records and dashboard. The home does not change.
83
+ - Inside a project, it writes nothing and reports that project.
84
+
85
+ Tell the user where the home is, or will be, before you add their task to it. `static-fire home` prints it,
86
+ or says none is set, and writes nothing; with none set, the home will be `~/static-fire` unless the user
87
+ names another folder. The report's `project` is the project's folder, `created` says if this run made it,
88
+ `home` is the home's folder (or null), `isHome` says if `project` is the home, and `homeSaved` says if this
89
+ run saved the home.
90
+
91
+ **Safe default.** Put the task in the home. Make a second project only when the user asks for a separate
92
+ one.
93
+
94
+ A new project has:
95
+
96
+ - `configs/<harness>.yaml`: one agent configuration for each harness it found on the `PATH` (`claude`,
97
+ `codex`, `agy`, `pi`), with a default model.
98
+ - `judges.yaml`: one judge, from the first harness found that can judge (`claude`, then `codex`, then `agy`).
99
+ Pi cannot be a judge. When no harness that can judge is found, `judges.yaml` lists none, and no batch can
100
+ run until one is added (see the reference).
101
+ - `evals/`, `suites/`, `static-fire.yaml`, a `.gitignore` for `runs/` and `.cache/`, and `AGENTS.md`.
102
+
103
+ The JSON report also has, for each harness: `found` and `path`, `blockable`, `tools` (each with `name`,
104
+ `kind`, `foundIn` and `block`, the entries that block it) and `notes`. It also has `judge`, `written`, `kept`
105
+ and `notes`. Run `static-fire init --json` again in the project at any time: it changes nothing and lists the
106
+ tools again.
107
+
108
+ **Settle the block list.** `init` finds the tools; it does not choose and does not ask. For each tool in the
109
+ report, decide if it acts on an outside service: sends or posts something, changes data in an account,
110
+ spends money, or deploys. Then the user decides. Before they decide, they need to see:
111
+
112
+ - Every tool found, for each harness, with its kind and where it was found. They cannot judge a list they
113
+ have not seen.
114
+ - Which tools you propose to block, each with its reason.
115
+ - Which tools stay on.
116
+ - Their choices: approve your list, change it, or block nothing.
117
+
118
+ Blocking nothing is a valid choice. Its facts: a trial then runs with every tool of the user's setup and no
119
+ permission prompts, which is the closest to how their agents really run. And a tool that acts on an outside
120
+ service can then act in a trial, with nobody there to stop it.
121
+
122
+ Write the entries of the user's choice under `trials.blockedTools.<harness>` in `static-fire.yaml`, copied
123
+ from each tool's `block` list.
124
+
125
+ **Facts.**
126
+
127
+ - Static Fire ships no list of outside services and blocks nothing by default. Your proposal is the list.
128
+ - Blocking works for Claude Code and Codex trials. Antigravity (`agy`) and Pi trials cannot block a tool
129
+ yet: they run with every tool of the user's setup. Tell the user this before they choose those harnesses.
130
+ - A plugin with no MCP server has nothing to block (its `block` list is empty).
131
+ - The claude.ai connectors in the list are the ones Claude Code last saw, so the list can be out of date.
132
+
133
+ When the report lists no tools (no harness found, or none with outside tools), there is nothing to block yet.
134
+ Run `static-fire init --json` again after the user installs a harness.
135
+
136
+ **Safe default.** When you are not sure if a tool acts on an outside service, propose to block it. The
137
+ result of a fast "yes" from the user must be safe. Tools that only read (search, docs, the local file
138
+ system) can stay on, because the real job has them.
139
+
140
+ **Configurations.** Show the user the configurations in the project, and change them to what they want to
141
+ compare: other models, other effort levels, more than one configuration per harness. A configuration is a
142
+ small YAML file (see the reference). `init` writes a current model id for each harness it found. For another
143
+ model, take its id from the harness itself (its model list or its documentation). Static Fire does not check
144
+ a model id: a wrong one shows up as a failed trial in step 5.
145
+
146
+ A trial runs with the user's own setup for that harness: their instructions, settings, skills and MCP
147
+ servers, minus the block list. That is on purpose: it is how their agents really run.
148
+
149
+ ### 3. Write the task
150
+
151
+ **Why.** The task is the job the configurations do and the questions their output is graded on. A score is
152
+ only as good as its questions.
153
+
154
+ **Do.** Run `static-fire new task <id>`, with a short kebab-case id. It makes `evals/<id>/task.yaml`, with a
155
+ comment on each part, and an empty `evals/<id>/inputs/`. Put the example inputs in `inputs/`. Fill in
156
+ `task.yaml` (see the reference for every field).
157
+
158
+ **Facts.**
159
+
160
+ - The prompt is the only message the agent gets, as the user would give it. The agent's working folder holds
161
+ `inputs/` (at the same path as in the task folder), the files of `project/` at its top level, and an empty
162
+ `out/`. Name the input files and the output path in the prompt, as the user would.
163
+ - The output is one file. Text or an image both work. A judge sees a PNG or JPEG output as an image.
164
+ - Material that only graders may see goes in `references/`, `sources/` or `heldOut`, never in `inputs/`.
165
+ - Each criterion is one yes or no question about the output, where yes is a pass. A good criterion names
166
+ what to look at and where the line between pass and fail is, so two careful graders give the same answer.
167
+ "Does it sound human?" is not a criterion. "Is every paragraph three sentences or fewer?" is.
168
+ - A criterion may limit what its grader sees only to act like a real reader, never to make grading easier.
169
+ - Use a check (regex, word count, frontmatter field, shell command) when a rule can decide it. Use a judge
170
+ when it takes reading. Do not use the `human` grader: it is not built yet, so its verdict is always an
171
+ error and every grading of the task is invalid.
172
+ - A check is cheap to test before any trial: write a sample output by hand and run the check's command or
173
+ pattern on it yourself. A `regex` is a JavaScript regular expression.
174
+ - Test each derived evidence script the same way before the first trial: run it on a sample output, see
175
+ that it ends well inside its 60-second limit, and open the PNG it writes. Every file in the task's
176
+ `grader/` folder is part of the grading version, so a fix after a trial costs a regrade (step 5).
177
+ - Give each criterion a level: `floor` (most configurations should pass), `strong` (good ones pass) or
178
+ `ceiling` (only the best pass). A mix makes the scores spread out. These three names are the ones the
179
+ dashboard shows.
180
+ - `static-fire tasks` warns under 15 criteria in all, under 3 in a measure, or when a level is missing.
181
+ These are advice. With 15 criteria, one criterion moves the score by about 7 points.
182
+ - A gate is a criterion outside the measures: when one fails, the task score is 0. "The output file exists
183
+ and is not empty" is always a gate, added by the engine.
184
+
185
+ **Confirm the criteria with the user.** The criteria decide every score, so the user approves them as their
186
+ own step, apart from any approval to run. Show each criterion in plain words with its level, and explain the
187
+ levels in plain words with their names: floor, strong, ceiling. Offer to change any criterion, and get the
188
+ user's yes on the criteria before you go on.
189
+
190
+ **Safe default.** Turn each thing the user said about good and bad output into one or more criteria. Write
191
+ each gap between the task and the real job in the project's `README.md`.
192
+
193
+ ### 4. Check the task and put it in a suite
194
+
195
+ **Why.** A task with an error cannot run. A task counts toward the Agent Performance Index only when it is in a
196
+ suite.
197
+
198
+ **Do.** Run `static-fire tasks`. Each error stops the task from running: fix every one. Warnings are advice.
199
+ Then add the task to a suite, a file `suites/<id>.yaml` (see the reference), and run `static-fire suites`,
200
+ `static-fire configs` and `static-fire judges` to see that each file loads.
201
+
202
+ **Safe default.** One suite per kind of work, for example "Thumbnails". Put the task in a new suite when no
203
+ suite fits.
204
+
205
+ ### 5. Run one trial and look at the result
206
+
207
+ **Why.** The first trial shows that the task works: the agent finds its inputs, writes the output where the
208
+ task expects it, and each verdict is right for the right reason. It is the least spend that shows this.
209
+
210
+ **Before you run.** A trial spends: the agent run, and the judge calls that grade it. Show the user its plan
211
+ (add `--plan` to the command below, as in step 6) and get their approval first.
212
+
213
+ **Do.** Run one trial of one configuration, usually the one the user works with:
214
+ `static-fire run --tasks <task> --configs <config> --trials 1`. A trial can take several minutes (the limit
215
+ is 20 minutes). The command prints a line for each trial, with its score and the agent's cost, and ends with
216
+ `batch <id> done`.
217
+
218
+ **Look at the result:**
219
+
220
+ - The output: `runs/trials/<trial-id>/workspace/`, at the task's output path. The agent's log is
221
+ `runs/trials/<trial-id>/agent.log`.
222
+ - The verdicts: `static-fire show --tasks <task> --json`. Each trial in `trials` has `grades.verdicts`
223
+ (each criterion's score, each vote's pass and reason, or an `error`), `result.costUsd` with
224
+ `result.costSource`, and `grades.judgeCostUsd`.
225
+ - A trial that `failed` did not run (the harness could not start, a login expired, it timed out); its
226
+ `result.error` says why. A grading that is `invalid` had a grader error; a regrade grades it again.
227
+
228
+ When a verdict is wrong because of the task (the question is unclear, a check has a bug, the prompt names
229
+ the wrong path), fix the task and run one trial again. When a verdict is wrong because the output is bad,
230
+ the task works. Show the user the output and the verdicts, so they can say whether the grading matches
231
+ their own view.
232
+
233
+ **Facts.** A change to the prompt, the output path, or a file in `inputs/` or `project/` makes a new task
234
+ hash: earlier trials no longer share a chart with new ones. A change to a criterion, `context`, a reference,
235
+ any file in `grader/` (a check's helper script or a derived evidence script) or the judge makes a new
236
+ grading version: earlier trials are left out of the view until a `static-fire regrade` grades them again.
237
+ That regrade spends judge calls and needs the user's approval like any other spend. So settle the task, its
238
+ grader scripts included, before the batch.
239
+
240
+ ### 6. Work out the batch cost, get approval, run the batch
241
+
242
+ **Why.** The batch is where most of the spend goes. The user approves it knowing what it uses.
243
+
244
+ **The plan in runs comes first.** Add `--plan` to the `run` command you mean to start, for example
245
+ `static-fire run --suites <suite> --configs <a>,<b> --trials 3 --plan --json`. It starts nothing and writes
246
+ nothing. It gives the trials, the agent runs for each configuration (a human configuration needs none),
247
+ the judge calls for the judge as a range, and what the range leaves out (`judgeCalls.notCounted`). These
248
+ counts are what a user on a subscription can judge: their plan counts runs and usage, not dollars. `static-fire regrade <flags> --plan --json` gives the plan of a
249
+ regrade the same way. `rerun` has no plan: it runs the failed trials of the batch again, each with its
250
+ grading.
251
+
252
+ **Facts.**
253
+
254
+ - Judge calls are often the larger part of a batch, in count and in cost. Their number per trial comes
255
+ from the judge criteria, put in groups: criteria with the same evidence and the same derived evidence
256
+ script are one group; each `isolate` criterion, each other set of evidence, each other derived script and
257
+ the judged gates make more groups. Each group gets one judge call per vote. With 3 votes (the default,
258
+ `--votes`), 2 calls run first and the third only when they disagree. That is the range: the low end when
259
+ every first pair agrees, the high end when none does.
260
+ - Outside the range: a judge answer that does not parse is tried once more, and the plan says that this
261
+ call is not counted. A trial with no output, or with a derived evidence script that fails, gets fewer calls.
262
+ - Dollar figures are a second fact, and they are API prices, not what a subscription is charged. The cost
263
+ of a configuration is known only once it has run, and configurations on other models can cost very
264
+ different amounts for the same task. A trial's judge cost is in `grades.judgeCostUsd`.
265
+ - Claude Code and Pi report their own cost (`costSource: reported`). Codex and Antigravity costs are
266
+ estimated from tokens and the engine's price list (`estimated`). A model that the price list does not have
267
+ gives no cost (`unknown`, shown as `?` in the `run` output). A project can have its own `pricing.yaml`.
268
+ - Cost that is not in the harness's own report, such as image generation by an outside service, is not
269
+ counted. Static Fire enforces no spending limit.
270
+
271
+ **Safe default.** The user sees the plan in runs before the batch, with the dollar figures you have as API
272
+ prices, what is measured made clear from what is scaled up, and approves it. Nothing runs before that
273
+ approval. A batch that starts prints the same plan first.
274
+
275
+ **Do.** Run the batch, for example `static-fire run --suites <suite> --configs <a>,<b> --trials 3`, or
276
+ `--configs all` for every configuration. It runs 4 trials at a time by default (`--concurrency`). A batch can
277
+ take a long time, so start it in the background and follow it with `static-fire list` or on the dashboard's
278
+ Runs page. When trials fail for a reason outside the task (a usage limit, a login),
279
+ `static-fire rerun <batch>` runs the failed ones again in the same batch, after the user approves it.
280
+
281
+ ### 7. Open the dashboard
282
+
283
+ **Why.** The dashboard is where the user sees which configuration does the work best and what it costs, and
284
+ why each score is what it is.
285
+
286
+ **Do.** Run `static-fire serve` in the project and keep it running in the background: it is a server and does
287
+ not exit. It prints `Static Fire → http://localhost:4477 (records: <path>)`. Give the user that address.
288
+ `--port <n>` picks another port. You can start it at any time; trials show up as they are graded.
289
+
290
+ **What the user sees.** The Results page shows the score of each configuration against its cost per task, as
291
+ a chart or a table, for the Agent Performance Index or one task. The index covers the suites the user turns on with
292
+ the suite buttons. With none picked, it covers the best set the data supports, and one line says which suites it
293
+ covers and why each other suite is left out. `static-fire show` uses the same set and prints the same line. With one
294
+ task in one suite, the index and the task show the same score. Each point is the mean of the valid trials with its 95% confidence
295
+ interval. Opening a trial shows its output, with each verdict and its reason in the Verdicts tab. The Runs page lists each batch and its trials,
296
+ and can start a new run. The Compare page shows what changed between two grading versions of a task.
297
+
298
+ Before any trial is graded, the Results page says "No trials in this view". The Task section of the Filter menu
299
+ lists every task of the project, also a task that is in no suite; `static-fire show --tasks <task>` shows one
300
+ task as text. The Tasks page shows each suite and task: its description, what the agent gets and how it is
301
+ graded.
302
+
303
+ ## Reference
304
+
305
+ ### The project folder
306
+
307
+ | Path | What |
308
+ |---|---|
309
+ | `static-fire.yaml` | Marks the project. The tools that trials may not use |
310
+ | `configs/<id>.yaml` | One agent configuration, or a human configuration |
311
+ | `judges.yaml` | The judges. The first is the default |
312
+ | `suites/<id>.yaml` | A suite: a named list of tasks |
313
+ | `evals/<id>/task.yaml` | A task, with its folders next to it |
314
+ | `pricing.yaml` | Optional. Token prices that replace the engine's |
315
+ | `runs/static-fire.db` | The record store: every batch, trial, grading and verdict. `STATIC_FIRE_DB` moves it |
316
+ | `runs/trials/<trial-id>/` | Each trial's `workspace/`, `agent.log` and `gradings/<grading-id>/` |
317
+ | `.cache/` | Profiles installed for a batch |
318
+
319
+ The record store is the only copy of the records. Do not edit or delete it. To copy it, use
320
+ `sqlite3 runs/static-fire.db ".backup '<dest>'"`, or `static-fire export <path>` for JSON.
321
+
322
+ In the examples below, a code block that starts with `yaml file=<path>` is a whole file, and the path is
323
+ where it goes in the project. The examples are one small, valid project.
324
+
325
+ ### Task: `evals/<id>/task.yaml`
326
+
327
+ ```yaml file=evals/thumbnail-text/task.yaml
328
+ id: thumbnail-text
329
+ title: Thumbnail text for a video
330
+ description: Writes the words on a YouTube thumbnail from the video's outline.
331
+ prompt: |
332
+ Read the outline of my next video in inputs/outline.md. Write the text for its
333
+ thumbnail: at most 4 words, in capitals, that make a viewer want to click.
334
+ Save it to out/thumbnail.md with YAML frontmatter that has a `title` field
335
+ (the video title you would pair with it), then the thumbnail text.
336
+ inputs: ./inputs
337
+ output: ./out/thumbnail.md
338
+ context: The viewer scrolls a YouTube home page and sees the thumbnail for about one second.
339
+ references: ./references
340
+ gates:
341
+ - id: has-title
342
+ question: Does the file have frontmatter with a non-empty title?
343
+ level: floor
344
+ grader: check
345
+ check: { type: frontmatter, field: title, minChars: 1 }
346
+ measures:
347
+ - id: readable
348
+ label: Readable at a glance
349
+ criteria:
350
+ - id: four-words-max
351
+ question: Does the text after the frontmatter have 4 words or fewer?
352
+ level: floor
353
+ grader: check
354
+ check: { type: words, max: 4 }
355
+ - id: no-hashtags
356
+ question: Is the file free of hashtags?
357
+ level: floor
358
+ grader: check
359
+ check: { type: regex, pattern: "#\\w", max: 0 }
360
+ - id: no-filler
361
+ question: Is every word needed, with no filler such as "the", "a" or "very"?
362
+ level: strong
363
+ grader: judge
364
+ - id: gets-clicked
365
+ label: Gets clicked
366
+ criteria:
367
+ - id: names-the-payoff
368
+ question: Does the text name the specific result the video shows, not a vague promise?
369
+ level: strong
370
+ grader: judge
371
+ evidence: [output, inputs]
372
+ - id: not-the-title
373
+ question: Does the text add something the title does not already say?
374
+ level: strong
375
+ grader: judge
376
+ - id: beats-past-work
377
+ question: Is it at least as specific as the best past thumbnail in the references?
378
+ level: ceiling
379
+ grader: judge
380
+ evidence: [output, references]
381
+ ```
382
+
383
+ Here `inputs/outline.md` and one or more files in `references/` sit next to `task.yaml`. This example has 6
384
+ criteria, so `static-fire tasks` warns that it has fewer than 15; a real task would have more.
385
+
386
+ | Field | Required | What |
387
+ |---|---|---|
388
+ | `id` | yes | The task's folder name under `evals/` |
389
+ | `title` | yes | A short name, shown on the dashboard |
390
+ | `description` | no | One line on what the task tests |
391
+ | `version` | no | A whole number, a readable label only. It decides nothing |
392
+ | `prompt` | yes | The only message the agent gets, as the user would give it |
393
+ | `inputs` | yes | A folder in the task folder (for example `./inputs`). Copied into the agent's working folder at the same path |
394
+ | `output` | yes | The file the agent must write, relative to its working folder (for example `./out/answer.md`). `out/` exists at the start |
395
+ | `project` | no | A folder whose files the agent would find in the real project. Its content is copied to the top of the working folder. Judges can see it as evidence |
396
+ | `context` | no | One paragraph every judge reads before the criteria |
397
+ | `references` | no | A folder of past work that judges can compare with. The agent never sees it |
398
+ | `sources` | no | A folder of saved copies of the pages the inputs link to, for fact checks. The agent never sees it |
399
+ | `heldOut` | no | A file: the real original the task was built from. An output that shares more than 30% of its runs of 8 words with it makes the trial invalid. The agent never sees it |
400
+ | `gates` | no | A list of criteria outside the measures. When one fails, the task score is 0 |
401
+ | `measures` | yes | At least one measure |
402
+
403
+ A measure has `id` (kebab-case, unique), `label` (shown on the dashboard), `weight` (optional, a positive
404
+ number, 1 by default) and `criteria` (at least one). The task score is 0 when a gate failed, else the
405
+ weighted mean of the measure scores, from 0 to 100. A measure's score is the share of its criteria passed.
406
+
407
+ The engine adds the gate `output-exists` (the output file exists and is not empty) to every task, so no
408
+ criterion can use that id.
409
+
410
+ Files whose names start with `.` are left out of every task folder. Each trial and grading uses a copy of the
411
+ task folder made when it starts, so editing the task while a batch runs does not change that batch's trials;
412
+ a trial whose files changed after the batch started fails and says so.
413
+
414
+ ### Criterion
415
+
416
+ | Field | For | What |
417
+ |---|---|---|
418
+ | `id` | all | Kebab-case, unique in the task |
419
+ | `question` | all | A yes or no question, where yes means pass. Names what to look at and where the line is |
420
+ | `level` | all | `floor`, `strong` or `ceiling` |
421
+ | `grader` | all | `check` or `judge`. (`human` is accepted but not built: its verdict is always an error) |
422
+ | `check` | check | One check (see below) |
423
+ | `evidence` | judge | What the judge reads: a list of `output`, `inputs`, `project`, `references`, `sources`. Default `[output]`. `project`, `references` and `sources` need that field on the task |
424
+ | `isolate` | judge | `true` gives the criterion its own judge call. By default, judge criteria with the same evidence share one call |
425
+ | `derived` | judge | A script in the task's `grader/` folder that makes an image of the output for the judge (see "Derived evidence") |
426
+
427
+ A judge always sees the task's prompt and `context`, then the evidence, then the questions. It gives each
428
+ criterion several votes (3 by default, `--votes` on `run`); the criterion's score is the share of votes that
429
+ passed. A text file is shown inline. An image is attached for a Claude Code or Codex judge, and given as a
430
+ file to open for an Antigravity judge. A judge runs without the user's setup, so grading is the same every
431
+ time.
432
+
433
+ ### Checks
434
+
435
+ Each check gives one pass or fail. A check that cannot run (a bad pattern, a command that times out) gives
436
+ an error, and the grading is invalid. The output file is read as text.
437
+
438
+ | Type | Fields | Passes when |
439
+ |---|---|---|
440
+ | `regex` | `pattern`, `flags` (optional), `min`, `max` | The JavaScript regular expression matches at least `min` and at most `max` times in the whole file. With neither, at least once. `g` is always on; add `m` for line anchors, `i` for any case |
441
+ | `words` | `min`, `max` | The word count of the text after any YAML frontmatter is in range. A word is a run of characters with a letter or digit |
442
+ | `frontmatter` | `field`, `minChars`, `maxChars` | The file starts with YAML frontmatter (`---` lines) whose `field` is a non-empty string, with its length in range |
443
+ | `command` | `run`, `timeoutSec` (default 60) | The shell command (run with `sh -c`) exits with 0 |
444
+
445
+ A `command` check runs in the trial's working folder, with these environment variables: `OUTPUT` (the
446
+ output file), `WORKSPACE` (the working folder) and `TASK_DIR` (a copy of the task folder, so it can read a
447
+ script or data you put there). Its last line of output becomes the reason shown with the verdict. For
448
+ example: `check: { type: command, run: 'python3 "$TASK_DIR/grader/check_links.py" "$OUTPUT"' }`. The command
449
+ runs with the user's own `PATH`: a program it calls, such as `python3` or `bun`, must be installed on the
450
+ machine that grades. The Bun that the package brings is not on that `PATH`.
451
+
452
+ ### Derived evidence
453
+
454
+ A judge criterion can name a script that makes an image from the output. The judge then sees the work as
455
+ its real reader does (a thumbnail at 320 px wide), or with what a reader cannot see marked on it.
456
+
457
+ ```yaml
458
+ - id: readable-small
459
+ question: Can every word on the thumbnail be read at this size?
460
+ level: floor
461
+ grader: judge
462
+ derived: scale-320.py # evals/<task>/grader/scale-320.py
463
+ evidence: [] # the judge sees only the derived image, not the full-size output
464
+ ```
465
+
466
+ The contract for a script:
467
+
468
+ - **Name.** `derived:` is a path relative to the task's `grader/` folder, one script per criterion. Only a
469
+ judge criterion can have one. `static-fire tasks` reports a script that does not exist.
470
+ - **How it runs.** By its extension: `.ts` and `.js` with the Bun that runs static-fire (the package brings
471
+ it, so no `bun` on the `PATH` is needed), `.py` with `uv` as a single-file script (declare packages such as
472
+ Pillow in the script's inline `# /// script` header), `.sh` with `sh`. `uv` must be on the `PATH` of the
473
+ machine that grades.
474
+ - **Input.** Argument 1 and `$OUTPUT` are the output file. `$TASK_DIR` is the task folder, so the script can
475
+ read other task files. `$WORKSPACE` is the working folder, and the script runs in it. The time limit is 60
476
+ seconds.
477
+ - **At the same time.** Gradings run at the same time: up to `--concurrency` (4 by default) in a batch or a
478
+ regrade, each in its own trial folder. In one grading, every script it needs runs at once. So a script
479
+ writes only to its argument 2 path and keeps nothing in a fixed path that another run could share.
480
+ - **Output.** Write one PNG to the path in argument 2 (also `$DERIVED`). A non-zero exit, a time-out, no file
481
+ or a file that is not a PNG makes the verdict an error, so the grading is invalid.
482
+ - **Where it stays.** `runs/trials/<trial-id>/gradings/<grading-id>/_derived/<script>.png`, with the
483
+ script's output next to it as `.log`.
484
+ - **Sharing.** Criteria that name the same script share one image, and one judge call when their other
485
+ evidence is the same. A new script name or new script content gives a new grading version: test it before
486
+ the first trial (step 3).
487
+
488
+ An example script, for an image output:
489
+
490
+ ```python
491
+ # /// script
492
+ # dependencies = ["pillow"]
493
+ # ///
494
+ import sys
495
+ from PIL import Image
496
+
497
+ src, dest = sys.argv[1], sys.argv[2]
498
+ img = Image.open(src)
499
+ img.thumbnail((320, 320))
500
+ img.save(dest, "PNG")
501
+ ```
502
+
503
+ ### Agent configuration: `configs/<id>.yaml`
504
+
505
+ ```yaml file=configs/claude-opus.yaml
506
+ harness: claude
507
+ model: claude-opus-5-5
508
+ effort: medium
509
+ ```
510
+
511
+ | Field | What |
512
+ |---|---|
513
+ | `harness` | `claude` (Claude Code), `codex` (Codex), `agy` (Antigravity) or `pi` (Pi) |
514
+ | `model` | The model id as the harness takes it. Antigravity model ids carry the effort (`gemini-4-hydrogen-medium`) |
515
+ | `effort` | Optional. The effort level as the harness takes it (`low`, `medium`, `high`) |
516
+ | `provider` | Pi only: the provider the user is logged in to in Pi (for example `openai-codex`) |
517
+ | `profile` | Optional. An apkit Profile name; needs `apkit` on the machine |
518
+
519
+ The file name is the id that `--configs` takes. Do not add a `label`: the label is built from the fields
520
+ ("Claude Code · Opus 5.5 · medium"). A configuration with the same fields under another file name keeps its
521
+ records.
522
+
523
+ The user can also compete as a **human configuration**: the file they wrote for a task is graded like an
524
+ agent's output, with no agent cost.
525
+
526
+ ```yaml file=configs/me.yaml
527
+ harness: human
528
+ outputs:
529
+ thumbnail-text: ~/Documents/thumbnail-text.md
530
+ ```
531
+
532
+ `outputs` maps a task id to a file path (absolute, `~/`, or relative to the project). Each run copies the file
533
+ as it is then, and only the latest trial of a human configuration counts.
534
+
535
+ ### Judges: `judges.yaml`
536
+
537
+ ```yaml file=judges.yaml
538
+ judges:
539
+ - id: claude-sonnet-5-5
540
+ label: Claude Code · Sonnet 5.5 · medium
541
+ harness: claude
542
+ model: claude-sonnet-5-5
543
+ effort: medium
544
+ ```
545
+
546
+ `harness` is `claude`, `codex` or `agy`. Pi cannot judge. The first judge is the default; `--judge <id>` on
547
+ `run` or `regrade` picks another. A batch needs a judge even when every criterion is a check. Another judge
548
+ gives another grading version, so the scores of two judges are never mixed. With no judge in the file,
549
+ `static-fire judges` prints nothing.
550
+
551
+ ### Suite: `suites/<id>.yaml`
552
+
553
+ ```yaml file=suites/thumbnails.yaml
554
+ title: Thumbnails
555
+ description: Packaging for my YouTube videos.
556
+ tasks:
557
+ - thumbnail-text
558
+ ```
559
+
560
+ | Field | What |
561
+ |---|---|
562
+ | `title` | Shown on the dashboard ("Thumbnails Index") |
563
+ | `description` | Optional. Shown on the Tasks page |
564
+ | `tasks` | The task ids |
565
+ | `weights` | Optional. Task id to weight; 1 by default |
566
+
567
+ The suite's index is the mean task score over its tasks. A configuration gets an index only with a valid
568
+ score on every task of the suite.
569
+
570
+ ### Settings: `static-fire.yaml`
571
+
572
+ ```yaml file=static-fire.yaml
573
+ trials:
574
+ blockedTools:
575
+ claude:
576
+ - mcp__example-mail
577
+ codex:
578
+ - example-mail
579
+ ```
580
+
581
+ Copy each entry from the `block` list of a tool in `static-fire init --json`. For Claude Code an entry is a
582
+ tool rule, and `mcp__<server>` blocks every tool of that server. For Codex it is an MCP server name from its
583
+ `config.toml`, or a plugin as `name@marketplace`. Entries for `agy` and `pi` do nothing yet. Each trial
584
+ records the list it ran with.
585
+
586
+ ### Commands
587
+
588
+ | Command | What |
589
+ |---|---|
590
+ | `static-fire init [<folder>] [--json]` | Make the home (`~/static-fire`, or `<folder>`), or report the home or the current project; with a home, `<folder>` makes a second project. Lists the harnesses and outside tools |
591
+ | `static-fire guide` | This guide |
592
+ | `static-fire home [<folder>]` | Show the home, the project a command uses outside any project, or save a project folder as the home |
593
+ | `static-fire new task <id>` | A new task folder with a commented `task.yaml` |
594
+ | `static-fire tasks` | Each task, with its errors and warnings |
595
+ | `static-fire configs` | Each configuration and its label |
596
+ | `static-fire judges` | Each judge; the default is marked |
597
+ | `static-fire suites` | Each suite and its tasks |
598
+ | `static-fire run` | One batch. `--tasks <a,b>` or `--suites <a,b>`, `--configs <a,b>` or `all` (all by default), `--trials <n>` (3), `--judge <id>`, `--votes <n>` (3), `--concurrency <n>` (4). `--plan` prints the plan and starts nothing; `--plan --json` for JSON |
599
+ | `static-fire rerun <batch>` | The failed trials of a batch again, as new trials. `--concurrency <n>` |
600
+ | `static-fire regrade` | Grade stored outputs again under the current grading, with judge calls only. `--tasks`, `--configs`, `--batch <id>`, `--judge`, `--votes`, `--only-invalid`, `--concurrency`, `--plan` (with `--json`) |
601
+ | `static-fire list` | Each batch, newest first, with its trial counts |
602
+ | `static-fire show` | The scores as text. `--tasks` or `--suites`, `--judge`, `--grading <hash>`, `--since <date>`, `--until <date>`, `--json` |
603
+ | `static-fire serve [--port <n>]` | The dashboard, at port 4477 by default |
604
+ | `static-fire export [path]` | Every record as JSON |
605
+ | `static-fire migrate [--from <folder>]` | One-off import of an experiment folder from before version 0.25.0 |
606
+ | `static-fire --version` | The installed version |
607
+
608
+ A flag that a command does not take is an error. A list is comma-separated: `--configs a,b`.
609
+
610
+ ### Limits to know
611
+
612
+ - **Trials run as the user really works.** Each trial runs headless with no permission prompts, with the
613
+ user's own setup for that harness and every tool, including the web, except the block list. It runs in a
614
+ new folder in the system temp folder, outside any repo.
615
+ - **No blocking for Antigravity and Pi trials** yet: they can use every tool of the user's setup.
616
+ - **Pi cannot be a judge.** A judge runs on Claude Code, Codex or Antigravity.
617
+ - **No judge, no batch.** When `init` finds no harness that can judge, add one to `judges.yaml` first.
618
+ - **No human grader yet.** Use checks and judges.
619
+ - **A trial is one shot.** The agent gets the prompt once, with no feedback and nobody to answer its
620
+ questions.
621
+ - **The copy check reads text only.** An image output that copies a published original is not caught.
622
+ - **Cost** is reported by Claude Code and Pi, estimated for Codex and Antigravity, and unknown for a model
623
+ that has no price. Judge cost is not part of a configuration's cost. Before a batch there is the plan in
624
+ runs (`--plan`), not a dollar estimate. There is no spending limit.
package/src/server.ts ADDED
@@ -0,0 +1,28 @@
1
+ // Local server: the dashboard page and the JSON API (src/api.ts) over the record store. Trials written by
2
+ // CLI processes show up live, because the event stream watches the database rather than this process.
3
+ import index from "../web/index.html";
4
+ import { apiRoutes } from "./api";
5
+ import { openStore } from "./core/db";
6
+ import type { ProjectPaths } from "./core/project";
7
+ import { projectCatalog } from "./core/view";
8
+ import { PAGES } from "../web/pages";
9
+
10
+ /**
11
+ * Serves the dashboard of `project`: its store, and its configs, judges, suites and tasks. The page is bundled
12
+ * once, minified and cached, unless `development` asks for Bun's development mode (bundled on request, with hot
13
+ * reload), which is for work on the engine (`static-fire serve --dev`, `bun run dev`).
14
+ */
15
+ export function serve(port: number, project: ProjectPaths, { development = false }: { development?: boolean } = {}) {
16
+ const { root, dbPath, cache } = project;
17
+ const store = openStore(dbPath);
18
+ const api = apiRoutes({ store, dbPath, catalog: () => projectCatalog(root), root, cacheDir: cache });
19
+ const server = Bun.serve({
20
+ port,
21
+ development,
22
+ idleTimeout: 0,
23
+ // Every page path serves the one page; it shows the page its path names (web/pages.ts).
24
+ routes: { ...Object.fromEntries(PAGES.map((p) => [p.path, index])), ...api.routes },
25
+ });
26
+ console.log(`Static Fire → http://localhost:${server.port} (records: ${dbPath})`);
27
+ return server;
28
+ }
package/web/api.ts ADDED
@@ -0,0 +1,6 @@
1
+ /** GET JSON; rejects with the server's error message on a failed request, so no caller reads `{ error }` as data. */
2
+ export const api = <T,>(p: string): Promise<T> => fetch(p).then(async (r) => {
3
+ const body = await r.json().catch(() => null);
4
+ if (!r.ok || (body && typeof body === "object" && "error" in body)) throw new Error(body?.error ?? `${r.status} ${r.statusText}`);
5
+ return body as T;
6
+ });