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.
- package/LICENSE +21 -0
- package/README.md +236 -2
- package/bin/static-fire.js +46 -0
- package/package.json +30 -4
- package/pricing.yaml +20 -0
- package/src/adapters/index.ts +153 -0
- package/src/api.ts +250 -0
- package/src/cli.ts +383 -0
- package/src/core/apkit-install.ts +47 -0
- package/src/core/authored.ts +47 -0
- package/src/core/batch-state.ts +25 -0
- package/src/core/batch.ts +450 -0
- package/src/core/batches.ts +221 -0
- package/src/core/checks.ts +63 -0
- package/src/core/commands.ts +43 -0
- package/src/core/compare.ts +206 -0
- package/src/core/config.ts +25 -0
- package/src/core/copying.ts +18 -0
- package/src/core/coverage.ts +84 -0
- package/src/core/db.ts +608 -0
- package/src/core/derived.ts +74 -0
- package/src/core/engine.ts +85 -0
- package/src/core/facets.ts +143 -0
- package/src/core/grade.ts +197 -0
- package/src/core/grading-versions.ts +281 -0
- package/src/core/identity.ts +263 -0
- package/src/core/init.ts +321 -0
- package/src/core/judge-schema.ts +37 -0
- package/src/core/judge.ts +166 -0
- package/src/core/label.ts +43 -0
- package/src/core/lock.ts +40 -0
- package/src/core/migrate.ts +288 -0
- package/src/core/new-task.ts +85 -0
- package/src/core/outside-tools.ts +129 -0
- package/src/core/plan.ts +84 -0
- package/src/core/profiles.ts +42 -0
- package/src/core/project.ts +140 -0
- package/src/core/regrade.ts +242 -0
- package/src/core/score.ts +148 -0
- package/src/core/settings.ts +44 -0
- package/src/core/task-file.ts +95 -0
- package/src/core/task-page.ts +54 -0
- package/src/core/trial-detail.ts +73 -0
- package/src/core/types.ts +195 -0
- package/src/core/view-query.ts +102 -0
- package/src/core/view.ts +576 -0
- package/src/guide.md +624 -0
- package/src/server.ts +28 -0
- package/web/api.ts +6 -0
- package/web/app.tsx +1162 -0
- package/web/brand/offset-tokens.css +126 -0
- package/web/charts.tsx +190 -0
- package/web/compare.tsx +317 -0
- package/web/encoding.tsx +55 -0
- package/web/filter-menu.ts +195 -0
- package/web/how-it-works.tsx +92 -0
- package/web/index-switches.ts +29 -0
- package/web/index.html +17 -0
- package/web/levels.tsx +15 -0
- package/web/notices.ts +74 -0
- package/web/output-view.ts +61 -0
- package/web/pages.ts +42 -0
- package/web/run-status.ts +51 -0
- package/web/styles.css +368 -0
- package/web/task-card.ts +6 -0
- package/web/tasks.tsx +122 -0
- 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
|
+
});
|