static-fire 0.0.0-stage → 0.44.0

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