@workos/quickstudy 0.0.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 +270 -0
- package/examples/harbor-notes/README.md +40 -0
- package/examples/harbor-notes/evals/create-note/EVAL.ts +14 -0
- package/examples/harbor-notes/evals/create-note/PROMPT.md +9 -0
- package/examples/harbor-notes/evals/create-note/local/README.txt +1 -0
- package/examples/harbor-notes/experiments/scripted.ts +6 -0
- package/examples/harbor-notes/package.json +6 -0
- package/examples/harbor-notes/quickstudy.identity.json +1 -0
- package/examples/harbor-notes/runtime.ts +48 -0
- package/examples/harbor-notes/semantic-example.ts +21 -0
- package/images/agent-runtime/Dockerfile +58 -0
- package/images/egress-proxy/Dockerfile +28 -0
- package/images/mcp-proxy/Dockerfile +30 -0
- package/package.json +53 -0
- package/src/adapters/claude.ts +107 -0
- package/src/adapters/codex.ts +107 -0
- package/src/adapters/echo.ts +57 -0
- package/src/adapters/parse.ts +117 -0
- package/src/adapters/types.ts +152 -0
- package/src/build-info.generated.ts +12 -0
- package/src/cli.ts +787 -0
- package/src/completeness.ts +104 -0
- package/src/diagnose/excerpt.ts +106 -0
- package/src/diagnose/prompt.ts +175 -0
- package/src/diagnose/render.ts +55 -0
- package/src/diagnose/run.ts +290 -0
- package/src/diagnose/select.ts +110 -0
- package/src/diagnose/types.ts +88 -0
- package/src/evals/discovery.ts +173 -0
- package/src/evals/prompt.ts +190 -0
- package/src/evals/result.ts +10 -0
- package/src/evals/types.ts +115 -0
- package/src/execution-policy.ts +71 -0
- package/src/experiments/discovery.ts +76 -0
- package/src/experiments/groups.ts +119 -0
- package/src/experiments/types.ts +116 -0
- package/src/export-types.ts +127 -0
- package/src/export.ts +381 -0
- package/src/hash.ts +74 -0
- package/src/identity-diff.ts +30 -0
- package/src/ids.ts +30 -0
- package/src/index.ts +58 -0
- package/src/isolation/docker.ts +639 -0
- package/src/isolation/image-contexts.generated.ts +927 -0
- package/src/isolation/images.ts +138 -0
- package/src/isolation/mcp-proxy/server.ts +260 -0
- package/src/isolation/mcp.ts +144 -0
- package/src/isolation/proxy/allowlist.ts +148 -0
- package/src/isolation/proxy/server.ts +382 -0
- package/src/llm.ts +132 -0
- package/src/manifest.ts +228 -0
- package/src/model-identity.ts +12 -0
- package/src/plan.ts +55 -0
- package/src/probe.ts +426 -0
- package/src/report/pass-at-k.ts +76 -0
- package/src/report/report.ts +731 -0
- package/src/runner/context.ts +96 -0
- package/src/runner/deadline.ts +37 -0
- package/src/runner/execute.ts +992 -0
- package/src/runner/run-lock.ts +32 -0
- package/src/runner/scheduler.ts +62 -0
- package/src/runner/score-worker.ts +107 -0
- package/src/runner/scorer-worker.ts +61 -0
- package/src/runtime/types.ts +89 -0
- package/src/secrets.ts +151 -0
- package/src/semantic.ts +185 -0
- package/src/serve.ts +52 -0
- package/src/source-identity.ts +76 -0
- package/src/store/artifacts.ts +146 -0
- package/src/store/db.ts +318 -0
- package/src/store/schema.ts +39 -0
- package/src/surface-usage.ts +297 -0
- package/src/ui-bundle.generated.ts +12 -0
- package/ui/dist/index.html +32 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 quickstudy contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# quickstudy
|
|
2
|
+
|
|
3
|
+
If you own a developer platform, coding agents raise two questions you can't
|
|
4
|
+
currently answer: **can an agent actually complete a real task with your
|
|
5
|
+
product**, and **does changing the developer surface change that outcome**?
|
|
6
|
+
quickstudy is an open-source, company-agnostic eval harness that measures both.
|
|
7
|
+
It runs coding agents against real integration tasks under different
|
|
8
|
+
**experiment treatments**, grades each attempt in isolation, and reports pass
|
|
9
|
+
rates, per-check outcomes, and raw counts for declared comparison groups.
|
|
10
|
+
|
|
11
|
+
If you want DX investment to go where it measurably moves the needle, this is
|
|
12
|
+
for you. ([Concepts](./docs/concepts.md) unpacks both questions.)
|
|
13
|
+
|
|
14
|
+
## What you get
|
|
15
|
+
|
|
16
|
+
Every run produces a report with per-pair stats, from fact to interpretation:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
run 01J…: 60 attempts — 41 passed, 17 failed, 0 incomplete, 2 errored
|
|
20
|
+
|
|
21
|
+
acme-nextjs × claude-docs 1/10 passed (10% pass rate)
|
|
22
|
+
build_passes 10/10 auth_flow_wired 4/10 login_flow_e2e 1/10 …
|
|
23
|
+
acme-nextjs × claude-mcp 10/10 passed (100% pass rate)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
One row per **eval × experiment** pair: trials passed, per-check pass counts,
|
|
27
|
+
and whether the pair ran all its trials. Comparison groups check recorded
|
|
28
|
+
configuration agreement before presenting raw per-arm counts. Matching a group
|
|
29
|
+
does not establish causation or eliminate timing, environment, service or sampling
|
|
30
|
+
confounds. Incomplete attempts and instrument errors are disclosed separately and
|
|
31
|
+
excluded from scored pass-rate denominators.
|
|
32
|
+
|
|
33
|
+
The same data also renders as an interactive, self-contained static
|
|
34
|
+
[site](#ui--sharing-results) you can publish or hand to a teammate.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
The npm package is **Bun-only**: it ships TypeScript source directly (the
|
|
39
|
+
`quickstudy` bin is a `bun`-shebang script and the library exports `.ts`
|
|
40
|
+
modules), so it requires [Bun](https://bun.sh) ≥ 1.1 — there is no Node
|
|
41
|
+
build. Add it to the repository that holds your evals and experiments:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
bun add quickstudy # Bun >= 1.1 required; no Node build exists
|
|
45
|
+
bunx quickstudy --version
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Your `EVAL.ts` scorers and experiment modules import harness types by package
|
|
49
|
+
name (`import type { EvalContext } from "quickstudy"`) — see
|
|
50
|
+
[Writing evals](#writing-evals) below and
|
|
51
|
+
[docs/writing-an-eval.md](./docs/writing-an-eval.md) for the authoring loop.
|
|
52
|
+
Container-based experiments additionally need the harness images built once
|
|
53
|
+
with `quickstudy images build` (requires Docker).
|
|
54
|
+
|
|
55
|
+
No Bun at all? Download the archive for your OS and architecture from
|
|
56
|
+
[GitHub Releases](https://github.com/workos/quickstudy/releases) instead. Each
|
|
57
|
+
archive contains one self-contained `quickstudy` executable (CLI + UI), with
|
|
58
|
+
no Bun runtime or source checkout required. Release assets are named
|
|
59
|
+
`quickstudy-{linux|darwin}-{x64|arm64}.tar.gz`.
|
|
60
|
+
|
|
61
|
+
To build that binary from source (requires Bun):
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
bun install
|
|
65
|
+
bun run build # → dist/quickstudy (the whole CLI + UI in one file)
|
|
66
|
+
mv dist/quickstudy /usr/local/bin/ # or anywhere on your PATH
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Prefer running from source? Replace `quickstudy` with `bun src/cli.ts` in any
|
|
70
|
+
command below (see [Development](#development)).
|
|
71
|
+
|
|
72
|
+
## Quickstart
|
|
73
|
+
|
|
74
|
+
Only **Docker** and an agent **API key** are needed to run a real agent — the
|
|
75
|
+
first commands need neither. An eval is a directory (`PROMPT.md` + `EVAL.ts`
|
|
76
|
+
scorer + optional `local/` starting state); an experiment is one TypeScript
|
|
77
|
+
module. Author a toy pair and run it end-to-end with the no-op `echo` agent:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
# 1. author a one-check eval and a host echo experiment
|
|
81
|
+
mkdir -p demo-evals/hello demo-experiments
|
|
82
|
+
printf -- '---\nid: hello\nsuite: benchmark\n---\n\nWrite NOTES.md describing the task.\n' > demo-evals/hello/PROMPT.md
|
|
83
|
+
printf -- 'export default async (ctx) => { const ok = await ctx.fileExists("NOTES.md"); return { passed: ok, checks: [{ name: "notes-written", passed: ok }] }; };\n' > demo-evals/hello/EVAL.ts
|
|
84
|
+
printf -- 'export default { id: "echo-demo", agent: { adapter: "echo" }, runtime: { kind: "host" } };\n' > demo-experiments/echo-demo.ts
|
|
85
|
+
|
|
86
|
+
# 2. validate both roots, then run — no Docker, no keys
|
|
87
|
+
quickstudy validate --evals-root demo-evals --experiments-root demo-experiments
|
|
88
|
+
quickstudy run --eval hello --experiment echo-demo --evals-root demo-evals --experiments-root demo-experiments --trials 1
|
|
89
|
+
|
|
90
|
+
# 3. read the report (step 2 prints a run id)
|
|
91
|
+
quickstudy report <run-id>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`echo` is a pipeline smoke, not a solver — it copies the starting state and
|
|
95
|
+
writes NOTES.md, so this toy check passes; against real evals every check
|
|
96
|
+
fails, which is exactly what a negative control should do. Completing the
|
|
97
|
+
run → score → persist loop is what it proves.
|
|
98
|
+
|
|
99
|
+
Real measurements come from a benchmark repository built on quickstudy: an
|
|
100
|
+
`evals/` root of real integration tasks (tiny HTTP apps under `local/`) and
|
|
101
|
+
an `experiments/` root of real agent recipes. From such a checkout, running
|
|
102
|
+
one costs a container image build and an API key:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
export ANTHROPIC_API_KEY=sk-...
|
|
106
|
+
quickstudy images build # one-time: runtime + proxies
|
|
107
|
+
docker build -t acme/nextjs images/frameworks/nextjs
|
|
108
|
+
quickstudy run --eval acme-nextjs --experiment claude-docs --trials 1
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
(Bun auto-loads a gitignored `.env` from the working directory, so keys can
|
|
112
|
+
live there instead of `export` lines.) Model and reasoning pins live in the
|
|
113
|
+
experiment module — changing them changes the experiment's identity, by
|
|
114
|
+
design. See the benchmark repository's `examples/weekly-sweep.yml` for what
|
|
115
|
+
a full scheduled sweep costs and looks like in CI.
|
|
116
|
+
|
|
117
|
+
## How it works
|
|
118
|
+
|
|
119
|
+
Every attempt is one point in a 2-D plan:
|
|
120
|
+
|
|
121
|
+
| Axis | Varies | Defined by |
|
|
122
|
+
| -------------- | ------------------------------------------------------------ | ------------------------------ |
|
|
123
|
+
| **eval** | WHAT is attempted: prompt, starting state, scorer | an `evals/<id>/` directory |
|
|
124
|
+
| **experiment** | HOW it runs: agent, model pins, runtime, treatment | an `experiments/<id>.ts` module |
|
|
125
|
+
|
|
126
|
+
An experiment's **runtime** owns everything the environment offers the agent:
|
|
127
|
+
per-attempt provisioning, the container image, egress policy, MCP servers,
|
|
128
|
+
native-web policy, and a PATH treatment. Experiments sharing a
|
|
129
|
+
`comparisonGroup` are compared as treatments of one configuration; a group
|
|
130
|
+
whose members disagree on anything beyond the declared treatment has
|
|
131
|
+
automatic comparison withheld.
|
|
132
|
+
|
|
133
|
+
Before the first paid attempt, quickstudy writes an immutable run manifest
|
|
134
|
+
covering the harness source, each eval's effective prompt / starting state /
|
|
135
|
+
scorer, and each experiment's agent pins, runtime, and module source.
|
|
136
|
+
Identity drift between runs is diffed, never silently averaged.
|
|
137
|
+
|
|
138
|
+
## Commands
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
quickstudy validate # discover + validate evals/ and experiments/
|
|
142
|
+
quickstudy run [--eval <id>] [--experiment <id>] # execute selected pairs; omit selectors for all
|
|
143
|
+
quickstudy report <run-id> # per-pair stats (--latest for the newest run)
|
|
144
|
+
quickstudy diagnose <run-id> # LLM-diagnose failing pairs into diagnosis.json
|
|
145
|
+
quickstudy ui # serve the matrix/compare/report/diagnosis UI
|
|
146
|
+
quickstudy export <run-id> --out dir # the same UI as a self-contained static site
|
|
147
|
+
quickstudy images build # build the agent-runtime + proxy images
|
|
148
|
+
quickstudy clean # remove all quickstudy-labeled containers/networks
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Run `quickstudy --help` for every flag (`--trials`, `--db`, `--egress-proxy`,
|
|
152
|
+
`--evals-root`, `--experiments-root`, …).
|
|
153
|
+
|
|
154
|
+
### Running a whole benchmark repository
|
|
155
|
+
|
|
156
|
+
Both run selectors are optional. Omitting `--eval` selects every discovered
|
|
157
|
+
eval; omitting `--experiment` selects every discovered experiment. Together,
|
|
158
|
+
this runs the complete eval × experiment cross-product:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
quickstudy validate
|
|
162
|
+
quickstudy run --trials 1
|
|
163
|
+
quickstudy report --latest --strict
|
|
164
|
+
quickstudy export --runs all --out ./site
|
|
165
|
+
quickstudy ui --watch
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`ui` is a blocking local server, so it is normally the final command. `export`
|
|
169
|
+
creates the non-blocking, self-contained site artifact. A complete run can be
|
|
170
|
+
expensive: inspect the counts printed by `validate`, then multiply selected
|
|
171
|
+
evals × experiments × trials before starting. Benchmark repositories are
|
|
172
|
+
encouraged to wrap this sequence in a checked-in script so it can preserve the
|
|
173
|
+
new run id across reporting, diagnosis, export, and UI serving.
|
|
174
|
+
|
|
175
|
+
`diagnose` is the one command that calls a model after the fact: it sends each
|
|
176
|
+
failing pair's harness-selected evidence (per-check tallies, transcript
|
|
177
|
+
excerpts, egress denials) to a structured LLM call and writes `diagnosis.json`
|
|
178
|
+
beside `report.json` — findings are evidence-linked hypotheses, never
|
|
179
|
+
verdicts, keeping the report itself attribution-free. Requires
|
|
180
|
+
`ANTHROPIC_API_KEY`; the UI renders the result as a Diagnosis view.
|
|
181
|
+
|
|
182
|
+
## Writing evals
|
|
183
|
+
|
|
184
|
+
An eval is a directory: `PROMPT.md` (frontmatter + the task prompt), `EVAL.ts`
|
|
185
|
+
(a default-exported scorer), and an optional `local/` tree the attempt starts
|
|
186
|
+
from. The harness core knows nothing company-specific; your evals and
|
|
187
|
+
scorers may name any vendor freely.
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
import type { EvalContext, EvalResult } from "quickstudy";
|
|
191
|
+
|
|
192
|
+
export default async function score(ctx: EvalContext): Promise<EvalResult> {
|
|
193
|
+
const wired = await ctx.fileExists("app/callback/route.ts");
|
|
194
|
+
return { passed: wired, checks: [{ name: "callback_wired", passed: wired }] };
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Scorers get file helpers over the exported workspace, `exec` into the live
|
|
199
|
+
sandbox, and any `query`/`getClient` capabilities the experiment's runtime
|
|
200
|
+
provides. Shared check helpers live in your benchmark repository next to the
|
|
201
|
+
evals that use them. [docs/writing-an-eval.md](./docs/writing-an-eval.md)
|
|
202
|
+
walks the whole authoring loop.
|
|
203
|
+
|
|
204
|
+
To point quickstudy at **your own product**: author an evals root and an
|
|
205
|
+
experiments root in a repository of your own — the toy pair above is the
|
|
206
|
+
smallest complete example, and `--evals-root`/`--experiments-root` point the
|
|
207
|
+
CLI anywhere. The harness core carries no vendor assumptions — a test suite
|
|
208
|
+
enforces that `src/`, `ui/`, and `scripts/` stay vendor-clean — so nothing in
|
|
209
|
+
this repository needs to change.
|
|
210
|
+
|
|
211
|
+
## UI & sharing results
|
|
212
|
+
|
|
213
|
+
One renderer, two delivery modes — both pure static files, no server API. The
|
|
214
|
+
compiled binary carries the UI shell, so there's nothing to build first; when
|
|
215
|
+
running from source, build the bundle once with `bun run ui:build`:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
quickstudy ui # serve every run at http://127.0.0.1:4173
|
|
219
|
+
quickstudy ui --watch # re-export when the results DB changes
|
|
220
|
+
|
|
221
|
+
quickstudy export <run-id> --out ./site # self-contained site
|
|
222
|
+
quickstudy export --runs a,b --out ./site # two runs → browse each run; Compare stays within a run
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
The exported directory works from `file://` (the data index is inlined into
|
|
226
|
+
`index.html`; transcripts and diffs lazy-load when served), so you can zip it,
|
|
227
|
+
attach it, or publish it. GitHub Pages recipe:
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
quickstudy export <run-id> --out ./site
|
|
231
|
+
npx gh-pages -d site # or push ./site to any static host
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Development
|
|
235
|
+
|
|
236
|
+
Run the CLI from source as `bun src/cli.ts <cmd>` (what `quickstudy` is,
|
|
237
|
+
uncompiled).
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
bun install
|
|
241
|
+
bun test # all suites (Docker-gated ones auto-enable when present)
|
|
242
|
+
bun run typecheck # tsc --noEmit (harness) + tsc -p ui
|
|
243
|
+
bun run lint # oxlint
|
|
244
|
+
bun run build # compile the self-contained binary → dist/quickstudy
|
|
245
|
+
bun src/cli.ts validate
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
UI development runs against committed seed data — no Docker, agents, or keys:
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
bun run ui:dev # Vite dev server + ui/dev-data at /data (hot reload)
|
|
252
|
+
bun run ui:dev-data # regenerate ui/dev-data from the seeders
|
|
253
|
+
bun run ui:build # production bundle (ui/dist) + gzip budget check
|
|
254
|
+
bunx playwright install # one-time: the browsers ui:e2e drives
|
|
255
|
+
bun run ui:e2e # Playwright smoke: served routes + offline file:// load
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Documentation
|
|
259
|
+
|
|
260
|
+
- **[docs/concepts.md](./docs/concepts.md)** — what it measures and why.
|
|
261
|
+
- **[docs/getting-started.md](./docs/getting-started.md)** — install to first report.
|
|
262
|
+
- **[docs/writing-an-eval.md](./docs/writing-an-eval.md)** — authoring evals and experiments.
|
|
263
|
+
- **[docs/publishing-a-benchmark.md](./docs/publishing-a-benchmark.md)** — gates for publishing results.
|
|
264
|
+
- **[docs/releasing.md](./docs/releasing.md)** — cutting prebuilt binaries.
|
|
265
|
+
|
|
266
|
+
## License
|
|
267
|
+
|
|
268
|
+
MIT
|
|
269
|
+
|
|
270
|
+
See [extension contracts, identity, resume and complete-sample publication](docs/extensions.md) and the [runnable Harbor Notes example](examples/harbor-notes/README.md).
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Harbor Notes: a complete second-product example
|
|
2
|
+
|
|
3
|
+
This example uses Quickstudy's public package API. Its prompt, fixture, scorer,
|
|
4
|
+
experiment, provisioning runtime, policy, and telemetry targets live here.
|
|
5
|
+
The basic path runs a scripted agent in Alpine and needs Bun plus Docker and
|
|
6
|
+
local `alpine:3` and `quickstudy/egress-proxy` images. No account or key is used.
|
|
7
|
+
|
|
8
|
+
From the Quickstudy checkout, run the isolated consumer smoke:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
bun scripts/portable-smoke.ts
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
It copies this example to a temporary consumer directory, resolves imports by
|
|
15
|
+
package name, validates, runs two trials with concurrency 2, and checks the
|
|
16
|
+
strict report. Provisioning creates a temporary notebook, the scorer queries
|
|
17
|
+
its expected document while the sandbox is alive, and teardown deletes it.
|
|
18
|
+
The script deletes its temporary results when finished.
|
|
19
|
+
|
|
20
|
+
To keep your results, work from this directory: `bun install`, then
|
|
21
|
+
`bunx quickstudy validate`, `bunx quickstudy run --trials 2 --egress-proxy`,
|
|
22
|
+
and `bunx quickstudy report --latest --strict`. Use the printed run ID with
|
|
23
|
+
`bunx quickstudy export RUN_ID --expected-trials 2 --mode hosted --out site`.
|
|
24
|
+
Serve `site/` with any static server. Use `--mode offline` to embed run data for
|
|
25
|
+
file sharing; transcripts and diffs require a local server in either mode.
|
|
26
|
+
|
|
27
|
+
For a real agent, build `quickstudy/agent-runtime` and the two proxy images
|
|
28
|
+
using `quickstudy images build`. Add an experiment using `harborRuntime(false)`
|
|
29
|
+
and `agent: { adapter: "claude", provider: "anthropic", model: "YOUR_EXACT_MODEL_ID",
|
|
30
|
+
reasoning: "high" }`, or the supported Codex adapter with its exact model and
|
|
31
|
+
reasoning pins. Set the corresponding provider key. Select that experiment
|
|
32
|
+
explicitly; it makes paid calls. The runtime offers no product network hosts;
|
|
33
|
+
with `--egress-proxy` the adapter's provider endpoints are the only allowed
|
|
34
|
+
external destinations. `containerImage()` selects the agent image, and all
|
|
35
|
+
other runtime/scorer code is shared with the local demonstration.
|
|
36
|
+
|
|
37
|
+
`semantic-example.ts` demonstrates the optional rubric interface with a fake
|
|
38
|
+
judge. It is deliberately outside the eval discovery tree. See
|
|
39
|
+
[extension contracts](../../docs/extensions.md) for evidence limits, identity,
|
|
40
|
+
error handling and calibration before substituting a live judge.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { EvalScorer } from "quickstudy";
|
|
2
|
+
const score: EvalScorer = async (ctx) => {
|
|
3
|
+
const expected = await ctx.query("expected-note") as { title: string; body: string };
|
|
4
|
+
let saved: unknown;
|
|
5
|
+
try { saved = JSON.parse(await ctx.readFile("note.json")); } catch { saved = null; }
|
|
6
|
+
const document = saved as { title?: string; body?: string } | null;
|
|
7
|
+
const live = await ctx.exec(["test", "-f", "note.json"]);
|
|
8
|
+
const checks = [
|
|
9
|
+
{ name: "note-persisted", passed: document?.title === expected.title && document?.body === expected.body && live.exitCode === 0 },
|
|
10
|
+
{ name: "starting-state-preserved", passed: await ctx.fileExists("README.txt") },
|
|
11
|
+
];
|
|
12
|
+
return { passed: checks.every((check) => check.passed), checks };
|
|
13
|
+
};
|
|
14
|
+
export default score;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: create-note
|
|
3
|
+
suite: benchmark
|
|
4
|
+
product: harbor-notes
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Create a persisted note in `note.json` with title `First note` and body
|
|
8
|
+
`Hello from Harbor`. Keep `README.txt`. The final report should explain the
|
|
9
|
+
saved note. The note file must contain a JSON object with `title` and `body`.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Harbor Notes stores each note as a JSON document.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { defineExperiment } from "quickstudy";
|
|
2
|
+
import { harborRuntime } from "../runtime.ts";
|
|
3
|
+
export default defineExperiment({
|
|
4
|
+
id: "scripted", agent: { adapter: "scripted", provider: "local", model: "script-v1", reasoning: "none", cliVersion: "built-in" },
|
|
5
|
+
runtime: harborRuntime(),
|
|
6
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "version": 1, "sources": ["runtime.ts"] }
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { mkdtemp, rm, writeFile, readFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { Runtime } from "quickstudy";
|
|
5
|
+
|
|
6
|
+
/** A complete local provisioning lifecycle; replace this notebook store with your API. */
|
|
7
|
+
export function harborRuntime(scripted = true): Runtime {
|
|
8
|
+
return {
|
|
9
|
+
kind: "container",
|
|
10
|
+
config: { notebookSchema: 1, scripted },
|
|
11
|
+
containerImage: () => (scripted ? "alpine:3" : "quickstudy/agent-runtime"),
|
|
12
|
+
...(scripted
|
|
13
|
+
? { command: ["sh", "-c", `printf '%s\n' '{"title":"First note","body":"Hello from Harbor"}' > note.json`] }
|
|
14
|
+
: {}),
|
|
15
|
+
egressHosts: () => [],
|
|
16
|
+
webPolicy: () => "native-web-blocked",
|
|
17
|
+
// Observation does not offer a docs or CLI treatment. A scripted run has
|
|
18
|
+
// no supported agent telemetry stream and records usage as unavailable.
|
|
19
|
+
observationTargets: { docsHosts: ["docs.harbor.example"], cliCommands: ["harbor"] },
|
|
20
|
+
async provision(ctx) {
|
|
21
|
+
ctx.signal?.throwIfAborted();
|
|
22
|
+
const notebook = await mkdtemp(join(tmpdir(), "harbor-notebook-"));
|
|
23
|
+
try {
|
|
24
|
+
await writeFile(
|
|
25
|
+
join(notebook, "expected.json"),
|
|
26
|
+
JSON.stringify({ title: "First note", body: "Hello from Harbor" }),
|
|
27
|
+
);
|
|
28
|
+
ctx.signal?.throwIfAborted();
|
|
29
|
+
return {
|
|
30
|
+
env: { HARBOR_NOTEBOOK: notebook },
|
|
31
|
+
teardown: async () => {
|
|
32
|
+
await rm(notebook, { recursive: true, force: true });
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
} catch (error) {
|
|
36
|
+
await rm(notebook, { recursive: true, force: true });
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
scorerCapabilities: (ctx) => ({
|
|
41
|
+
query: async (request) => {
|
|
42
|
+
ctx.signal?.throwIfAborted();
|
|
43
|
+
if (request !== "expected-note") throw new Error("unknown notebook query");
|
|
44
|
+
return JSON.parse(await readFile(join(ctx.env.HARBOR_NOTEBOOK!, "expected.json"), "utf8"));
|
|
45
|
+
},
|
|
46
|
+
}),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineSemanticScorer } from "quickstudy";
|
|
2
|
+
|
|
3
|
+
/** Local demonstration only: the fake judge has no network or credential dependency. */
|
|
4
|
+
export const semanticExample = defineSemanticScorer(
|
|
5
|
+
{
|
|
6
|
+
name: "explains-note",
|
|
7
|
+
rubric: "The final report explains the saved note's title and body.",
|
|
8
|
+
provider: "local-fake",
|
|
9
|
+
model: "rubric-fixture-v1",
|
|
10
|
+
includeFinalReport: true,
|
|
11
|
+
},
|
|
12
|
+
async (request) => ({
|
|
13
|
+
verdict: {
|
|
14
|
+
passed: request.evidence.some(
|
|
15
|
+
(entry) => entry.text.includes("First note") && entry.text.includes("Hello from Harbor"),
|
|
16
|
+
),
|
|
17
|
+
reason: "Local fixture matches the rubric vocabulary; calibrate a real judge separately.",
|
|
18
|
+
evidence: ["agent:final-report"],
|
|
19
|
+
},
|
|
20
|
+
}),
|
|
21
|
+
);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# quickstudy agent-runtime base image.
|
|
2
|
+
#
|
|
3
|
+
# Debian-slim + git + node, with the agent CLIs installed at PINNED versions:
|
|
4
|
+
# vendor CLIs change their stream formats between releases, and the parsers in
|
|
5
|
+
# src/adapters/parse.ts are tested against fixtures captured from these exact
|
|
6
|
+
# versions. Bump the pins deliberately (`quickstudy images build --pull`) and
|
|
7
|
+
# re-capture fixtures when you do.
|
|
8
|
+
#
|
|
9
|
+
# Framework toolchains (Rails, Django, ...) layer on top in Phase 3 — this
|
|
10
|
+
# image stays agent-only.
|
|
11
|
+
|
|
12
|
+
FROM node:22-bookworm-slim
|
|
13
|
+
|
|
14
|
+
ARG CLAUDE_CODE_VERSION=2.1.215
|
|
15
|
+
ARG CODEX_VERSION=0.149.1
|
|
16
|
+
ARG GEMINI_CLI_VERSION=0.51.0
|
|
17
|
+
# cursor-agent is not an npm package: it ships as versioned tarballs (the
|
|
18
|
+
# vendor install script pins the same way). Fixture streams in
|
|
19
|
+
# tests/__fixtures__/streams/cursor/ correspond to this pin.
|
|
20
|
+
ARG CURSOR_AGENT_VERSION=2026.07.17-3e2a980
|
|
21
|
+
|
|
22
|
+
RUN apt-get update \
|
|
23
|
+
&& apt-get install -y --no-install-recommends ca-certificates curl git procps ripgrep \
|
|
24
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
25
|
+
|
|
26
|
+
RUN npm install -g --no-fund --no-audit \
|
|
27
|
+
"@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" \
|
|
28
|
+
"@openai/codex@${CODEX_VERSION}" \
|
|
29
|
+
"@google/gemini-cli@${GEMINI_CLI_VERSION}"
|
|
30
|
+
|
|
31
|
+
RUN arch="$(dpkg --print-architecture)" \
|
|
32
|
+
&& case "$arch" in \
|
|
33
|
+
amd64) cursor_arch=x64 ;; \
|
|
34
|
+
arm64) cursor_arch=arm64 ;; \
|
|
35
|
+
*) echo "unsupported architecture for cursor-agent: $arch" >&2; exit 1 ;; \
|
|
36
|
+
esac \
|
|
37
|
+
&& mkdir -p /opt/cursor-agent \
|
|
38
|
+
&& curl -fsSL "https://downloads.cursor.com/lab/${CURSOR_AGENT_VERSION}/linux/${cursor_arch}/agent-cli-package.tar.gz" \
|
|
39
|
+
| tar --strip-components=1 -xzf - -C /opt/cursor-agent \
|
|
40
|
+
&& ln -s /opt/cursor-agent/cursor-agent /usr/local/bin/cursor-agent
|
|
41
|
+
|
|
42
|
+
# The fixture-baseline commit (and any commits the agent makes) need an
|
|
43
|
+
# identity; system-level config applies to whichever user runs.
|
|
44
|
+
RUN git config --system user.name "quickstudy" \
|
|
45
|
+
&& git config --system user.email "quickstudy@localhost" \
|
|
46
|
+
&& git config --system init.defaultBranch main \
|
|
47
|
+
&& git config --system --add safe.directory /workspace
|
|
48
|
+
|
|
49
|
+
# Claude Code refuses --dangerously-skip-permissions as root unless it can
|
|
50
|
+
# tell it is inside a sandbox. The container IS the security boundary here —
|
|
51
|
+
# that is the entire point of this phase.
|
|
52
|
+
ENV IS_SANDBOX=1
|
|
53
|
+
|
|
54
|
+
WORKDIR /workspace
|
|
55
|
+
|
|
56
|
+
# The harness drives everything through `docker exec`; the main process just
|
|
57
|
+
# keeps the container alive until teardown.
|
|
58
|
+
CMD ["sleep", "infinity"]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# quickstudy egress-proxy sidecar image.
|
|
2
|
+
#
|
|
3
|
+
# One per run: attempt containers sit on an internal Docker network with no
|
|
4
|
+
# default route, and this proxy (attached to BOTH the internal network and
|
|
5
|
+
# the bridge) is their only way out — an HTTP CONNECT / absolute-form forward
|
|
6
|
+
# proxy enforcing the run's host allowlist (src/isolation/proxy/server.ts).
|
|
7
|
+
# No TLS interception: for HTTPS the proxy sees only the CONNECT host:port.
|
|
8
|
+
#
|
|
9
|
+
# Build context is src/isolation/proxy (the image needs exactly two files):
|
|
10
|
+
# docker build -t quickstudy/egress-proxy -f images/egress-proxy/Dockerfile src/isolation/proxy
|
|
11
|
+
# or simply `quickstudy images build`, which builds this alongside the
|
|
12
|
+
# agent-runtime image.
|
|
13
|
+
#
|
|
14
|
+
# Config via env at `docker run`:
|
|
15
|
+
# QUICKSTUDY_EGRESS_ALLOWLIST comma-separated hosts / *.wildcards (required)
|
|
16
|
+
# QUICKSTUDY_EGRESS_PORT listen port (default 3128)
|
|
17
|
+
|
|
18
|
+
FROM oven/bun:1.3-slim
|
|
19
|
+
|
|
20
|
+
WORKDIR /app
|
|
21
|
+
|
|
22
|
+
COPY allowlist.ts server.ts ./proxy/
|
|
23
|
+
|
|
24
|
+
EXPOSE 3128
|
|
25
|
+
|
|
26
|
+
# Decisions (allowed + denied) stream to stdout as JSON lines; the harness
|
|
27
|
+
# reads them back with `docker logs` for per-attempt denial artifacts.
|
|
28
|
+
ENTRYPOINT ["bun", "/app/proxy/server.ts"]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# quickstudy mcp-proxy sidecar image.
|
|
2
|
+
#
|
|
3
|
+
# One per run, only when the runtime's mcpServers declare `auth`: attempt
|
|
4
|
+
# containers speak credential-free HTTP to this proxy, which forwards to the
|
|
5
|
+
# real MCP server injecting a Bearer access token it mints (and re-mints)
|
|
6
|
+
# from a refresh token (src/isolation/mcp-proxy/server.ts). Refresh tokens
|
|
7
|
+
# rotate on use; the current one is persisted inside the container at
|
|
8
|
+
# /run/quickstudy-mcp/ for the harness to read back at teardown.
|
|
9
|
+
#
|
|
10
|
+
# Build context is src/isolation/mcp-proxy (the image needs exactly one file):
|
|
11
|
+
# docker build -t quickstudy/mcp-proxy -f images/mcp-proxy/Dockerfile src/isolation/mcp-proxy
|
|
12
|
+
# or simply `quickstudy images build`, which builds this alongside the
|
|
13
|
+
# agent-runtime and egress-proxy images.
|
|
14
|
+
#
|
|
15
|
+
# Config via env at `docker run` (secrets arrive via --env-file, never argv):
|
|
16
|
+
# QUICKSTUDY_MCP_UPSTREAMS JSON {name: {url, tokenEndpoint, clientId, resource?}}
|
|
17
|
+
# QUICKSTUDY_MCP_REFRESH_TOKEN_<NAME> initial refresh token per server
|
|
18
|
+
# QUICKSTUDY_MCP_PORT listen port (default 8914)
|
|
19
|
+
|
|
20
|
+
FROM oven/bun:1.3-slim
|
|
21
|
+
|
|
22
|
+
WORKDIR /app
|
|
23
|
+
|
|
24
|
+
COPY server.ts ./mcp-proxy/
|
|
25
|
+
|
|
26
|
+
EXPOSE 8914
|
|
27
|
+
|
|
28
|
+
# Events (forwards + token refreshes) stream to stdout as JSON lines; token
|
|
29
|
+
# values never appear in them.
|
|
30
|
+
ENTRYPOINT ["bun", "/app/mcp-proxy/server.ts"]
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@workos/quickstudy",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "An open-source, company-agnostic eval harness for measuring whether coding agents can complete real integration tasks, across an agent x feature x framework x surface matrix.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"quickstudy": "src/cli.ts"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./src/index.ts",
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src",
|
|
16
|
+
"ui/dist",
|
|
17
|
+
"images",
|
|
18
|
+
"examples",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"bun": ">=1.1"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"prepack": "bun run ui:build",
|
|
27
|
+
"typecheck": "tsc --noEmit && tsc -p ui",
|
|
28
|
+
"lint": "oxlint src tests ui scripts",
|
|
29
|
+
"test": "bun test",
|
|
30
|
+
"build": "bun scripts/build-binary.ts",
|
|
31
|
+
"ui:dev": "vite --config ui/vite.config.ts",
|
|
32
|
+
"ui:build": "vite build --config ui/vite.config.ts && bun scripts/check-bundle-size.ts",
|
|
33
|
+
"ui:e2e": "playwright test --config ui/e2e/playwright.config.ts",
|
|
34
|
+
"ui:dev-data": "bun scripts/make-dev-data.ts"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@anthropic-ai/sdk": "^0.124.0",
|
|
38
|
+
"yaml": "^2.9.0",
|
|
39
|
+
"zod": "^4.6.1"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@playwright/test": "^1.63.0",
|
|
43
|
+
"@types/bun": "^1.4.2",
|
|
44
|
+
"@types/react": "^19",
|
|
45
|
+
"@types/react-dom": "^19",
|
|
46
|
+
"@vitejs/plugin-react": "^6.1.1",
|
|
47
|
+
"oxlint": "^1.82.0",
|
|
48
|
+
"react": "^19.3.0",
|
|
49
|
+
"react-dom": "^19.3.0",
|
|
50
|
+
"typescript": "^7.0.2",
|
|
51
|
+
"vite": "^8.3.0"
|
|
52
|
+
}
|
|
53
|
+
}
|