@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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +270 -0
  3. package/examples/harbor-notes/README.md +40 -0
  4. package/examples/harbor-notes/evals/create-note/EVAL.ts +14 -0
  5. package/examples/harbor-notes/evals/create-note/PROMPT.md +9 -0
  6. package/examples/harbor-notes/evals/create-note/local/README.txt +1 -0
  7. package/examples/harbor-notes/experiments/scripted.ts +6 -0
  8. package/examples/harbor-notes/package.json +6 -0
  9. package/examples/harbor-notes/quickstudy.identity.json +1 -0
  10. package/examples/harbor-notes/runtime.ts +48 -0
  11. package/examples/harbor-notes/semantic-example.ts +21 -0
  12. package/images/agent-runtime/Dockerfile +58 -0
  13. package/images/egress-proxy/Dockerfile +28 -0
  14. package/images/mcp-proxy/Dockerfile +30 -0
  15. package/package.json +53 -0
  16. package/src/adapters/claude.ts +107 -0
  17. package/src/adapters/codex.ts +107 -0
  18. package/src/adapters/echo.ts +57 -0
  19. package/src/adapters/parse.ts +117 -0
  20. package/src/adapters/types.ts +152 -0
  21. package/src/build-info.generated.ts +12 -0
  22. package/src/cli.ts +787 -0
  23. package/src/completeness.ts +104 -0
  24. package/src/diagnose/excerpt.ts +106 -0
  25. package/src/diagnose/prompt.ts +175 -0
  26. package/src/diagnose/render.ts +55 -0
  27. package/src/diagnose/run.ts +290 -0
  28. package/src/diagnose/select.ts +110 -0
  29. package/src/diagnose/types.ts +88 -0
  30. package/src/evals/discovery.ts +173 -0
  31. package/src/evals/prompt.ts +190 -0
  32. package/src/evals/result.ts +10 -0
  33. package/src/evals/types.ts +115 -0
  34. package/src/execution-policy.ts +71 -0
  35. package/src/experiments/discovery.ts +76 -0
  36. package/src/experiments/groups.ts +119 -0
  37. package/src/experiments/types.ts +116 -0
  38. package/src/export-types.ts +127 -0
  39. package/src/export.ts +381 -0
  40. package/src/hash.ts +74 -0
  41. package/src/identity-diff.ts +30 -0
  42. package/src/ids.ts +30 -0
  43. package/src/index.ts +58 -0
  44. package/src/isolation/docker.ts +639 -0
  45. package/src/isolation/image-contexts.generated.ts +927 -0
  46. package/src/isolation/images.ts +138 -0
  47. package/src/isolation/mcp-proxy/server.ts +260 -0
  48. package/src/isolation/mcp.ts +144 -0
  49. package/src/isolation/proxy/allowlist.ts +148 -0
  50. package/src/isolation/proxy/server.ts +382 -0
  51. package/src/llm.ts +132 -0
  52. package/src/manifest.ts +228 -0
  53. package/src/model-identity.ts +12 -0
  54. package/src/plan.ts +55 -0
  55. package/src/probe.ts +426 -0
  56. package/src/report/pass-at-k.ts +76 -0
  57. package/src/report/report.ts +731 -0
  58. package/src/runner/context.ts +96 -0
  59. package/src/runner/deadline.ts +37 -0
  60. package/src/runner/execute.ts +992 -0
  61. package/src/runner/run-lock.ts +32 -0
  62. package/src/runner/scheduler.ts +62 -0
  63. package/src/runner/score-worker.ts +107 -0
  64. package/src/runner/scorer-worker.ts +61 -0
  65. package/src/runtime/types.ts +89 -0
  66. package/src/secrets.ts +151 -0
  67. package/src/semantic.ts +185 -0
  68. package/src/serve.ts +52 -0
  69. package/src/source-identity.ts +76 -0
  70. package/src/store/artifacts.ts +146 -0
  71. package/src/store/db.ts +318 -0
  72. package/src/store/schema.ts +39 -0
  73. package/src/surface-usage.ts +297 -0
  74. package/src/ui-bundle.generated.ts +12 -0
  75. 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,6 @@
1
+ {
2
+ "name": "harbor-notes-evals",
3
+ "private": true,
4
+ "type": "module",
5
+ "dependencies": { "quickstudy": "file:../.." }
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
+ }