lastlight-evals 0.7.30 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +38 -1
  2. package/datasets/dependency-merge/README.md +69 -0
  3. package/datasets/dependency-merge/instances.json +168 -0
  4. package/datasets/dependency-merge/tier.json +5 -0
  5. package/datasets/fix/README.md +57 -0
  6. package/datasets/fix/instances.json +229 -0
  7. package/datasets/fix/repos/fix__env-mismatch-node-version/.github/workflows/ci.yml +15 -0
  8. package/datasets/fix/repos/fix__env-mismatch-node-version/README.md +3 -0
  9. package/datasets/fix/repos/fix__env-mismatch-node-version/package.json +15 -0
  10. package/datasets/fix/repos/fix__env-mismatch-node-version/src/group.js +4 -0
  11. package/datasets/fix/repos/fix__env-mismatch-node-version/test/group.test.js +9 -0
  12. package/datasets/fix/repos/fix__flaky-promoted/.github/workflows/ci.yml +15 -0
  13. package/datasets/fix/repos/fix__flaky-promoted/README.md +3 -0
  14. package/datasets/fix/repos/fix__flaky-promoted/package.json +12 -0
  15. package/datasets/fix/repos/fix__flaky-promoted/src/poll.js +9 -0
  16. package/datasets/fix/repos/fix__flaky-promoted/test/poll.test.js +8 -0
  17. package/datasets/fix/repos/fix__flaky-timeout/.github/workflows/ci.yml +15 -0
  18. package/datasets/fix/repos/fix__flaky-timeout/README.md +3 -0
  19. package/datasets/fix/repos/fix__flaky-timeout/package.json +12 -0
  20. package/datasets/fix/repos/fix__flaky-timeout/src/poll.js +9 -0
  21. package/datasets/fix/repos/fix__flaky-timeout/test/poll.test.js +8 -0
  22. package/datasets/fix/repos/fix__infra-dependent-e2e/.github/workflows/ci.yml +28 -0
  23. package/datasets/fix/repos/fix__infra-dependent-e2e/README.md +3 -0
  24. package/datasets/fix/repos/fix__infra-dependent-e2e/e2e/smoke.test.js +10 -0
  25. package/datasets/fix/repos/fix__infra-dependent-e2e/package.json +12 -0
  26. package/datasets/fix/repos/fix__infra-dependent-e2e/src/retry.js +13 -0
  27. package/datasets/fix/repos/fix__infra-dependent-e2e/test/retry.test.js +8 -0
  28. package/datasets/fix/repos/fix__reproducible-api-removal/.github/workflows/ci.yml +15 -0
  29. package/datasets/fix/repos/fix__reproducible-api-removal/README.md +3 -0
  30. package/datasets/fix/repos/fix__reproducible-api-removal/package.json +15 -0
  31. package/datasets/fix/repos/fix__reproducible-api-removal/src/slug.js +6 -0
  32. package/datasets/fix/repos/fix__reproducible-api-removal/test/slug.test.js +7 -0
  33. package/datasets/fix/repos/fix__reproducible-api-removal/vendor/tiny-case/index.js +4 -0
  34. package/datasets/fix/repos/fix__upstream-broken-base-red/.github/workflows/ci.yml +15 -0
  35. package/datasets/fix/repos/fix__upstream-broken-base-red/README.md +3 -0
  36. package/datasets/fix/repos/fix__upstream-broken-base-red/package.json +12 -0
  37. package/datasets/fix/repos/fix__upstream-broken-base-red/src/format.js +6 -0
  38. package/datasets/fix/repos/fix__upstream-broken-base-red/src/rates.js +6 -0
  39. package/datasets/fix/repos/fix__upstream-broken-base-red/test/format.test.js +7 -0
  40. package/datasets/fix/repos/fix__upstream-broken-base-red/test/rates.test.js +7 -0
  41. package/datasets/fix/repos-head/fix__env-mismatch-node-version/package.json +15 -0
  42. package/datasets/fix/repos-head/fix__flaky-promoted/package.json +12 -0
  43. package/datasets/fix/repos-head/fix__flaky-timeout/package.json +12 -0
  44. package/datasets/fix/repos-head/fix__infra-dependent-e2e/package.json +12 -0
  45. package/datasets/fix/repos-head/fix__reproducible-api-removal/package.json +15 -0
  46. package/datasets/fix/repos-head/fix__reproducible-api-removal/vendor/tiny-case/index.js +4 -0
  47. package/datasets/fix/repos-head/fix__upstream-broken-base-red/package.json +12 -0
  48. package/datasets/fix/tier.json +5 -0
  49. package/datasets/repo-config/instances.json +35 -0
  50. package/datasets/repo-config/lastlight/repoconfig__prompt-override/agent-context/repo-notes.md +9 -0
  51. package/datasets/repo-config/lastlight/repoconfig__prompt-override/lastlight.yml +22 -0
  52. package/datasets/repo-config/lastlight/repoconfig__prompt-override/workflows/prompts/answer.md +27 -0
  53. package/datasets/repo-config/tier.json +5 -0
  54. package/dist/fake-github.js +228 -5
  55. package/dist/fake-github.js.map +1 -1
  56. package/dist/grade.js +90 -1
  57. package/dist/grade.js.map +1 -1
  58. package/dist/mechanism.test.js +420 -2
  59. package/dist/mechanism.test.js.map +1 -1
  60. package/dist/pr-context.js +162 -0
  61. package/dist/pr-context.js.map +1 -0
  62. package/dist/repo-config.js +141 -0
  63. package/dist/repo-config.js.map +1 -0
  64. package/dist/run-instance.js +134 -7
  65. package/dist/run-instance.js.map +1 -1
  66. package/dist/run.js +8 -0
  67. package/dist/run.js.map +1 -1
  68. package/dist/schema.js +0 -13
  69. package/dist/schema.js.map +1 -1
  70. package/dist/seed.js +7 -0
  71. package/dist/seed.js.map +1 -1
  72. package/package.json +4 -4
@@ -0,0 +1,15 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ jobs:
8
+ test:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - uses: actions/setup-node@v4
13
+ with:
14
+ node-version: "22"
15
+ - run: npm test
@@ -0,0 +1,3 @@
1
+ # poller
2
+
3
+ A polling helper. `npm test` runs the suite.
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "poller",
3
+ "version": "2.1.0",
4
+ "type": "module",
5
+ "description": "Polling helper.",
6
+ "scripts": {
7
+ "test": "node --test"
8
+ },
9
+ "devDependencies": {
10
+ "tap-reporter": "^4.2.0"
11
+ }
12
+ }
@@ -0,0 +1,9 @@
1
+ /** Resolve once `check()` returns true, polling every `intervalMs`. */
2
+ export async function pollUntil(check, { intervalMs = 10, timeoutMs = 200 } = {}) {
3
+ const deadline = Date.now() + timeoutMs;
4
+ for (;;) {
5
+ if (await check()) return true;
6
+ if (Date.now() > deadline) throw new Error("pollUntil: timed out");
7
+ await new Promise((r) => setTimeout(r, intervalMs));
8
+ }
9
+ }
@@ -0,0 +1,8 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { pollUntil } from "../src/poll.js";
4
+
5
+ test("resolves once the condition holds", async () => {
6
+ let n = 0;
7
+ assert.equal(await pollUntil(() => ++n > 2), true);
8
+ });
@@ -0,0 +1,15 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ jobs:
8
+ test:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - uses: actions/setup-node@v4
13
+ with:
14
+ node-version: "22"
15
+ - run: npm test
@@ -0,0 +1,3 @@
1
+ # poller
2
+
3
+ A polling helper. `npm test` runs the suite.
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "poller",
3
+ "version": "2.1.0",
4
+ "type": "module",
5
+ "description": "Polling helper.",
6
+ "scripts": {
7
+ "test": "node --test"
8
+ },
9
+ "devDependencies": {
10
+ "tap-reporter": "^4.2.0"
11
+ }
12
+ }
@@ -0,0 +1,9 @@
1
+ /** Resolve once `check()` returns true, polling every `intervalMs`. */
2
+ export async function pollUntil(check, { intervalMs = 10, timeoutMs = 200 } = {}) {
3
+ const deadline = Date.now() + timeoutMs;
4
+ for (;;) {
5
+ if (await check()) return true;
6
+ if (Date.now() > deadline) throw new Error("pollUntil: timed out");
7
+ await new Promise((r) => setTimeout(r, intervalMs));
8
+ }
9
+ }
@@ -0,0 +1,8 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { pollUntil } from "../src/poll.js";
4
+
5
+ test("resolves once the condition holds", async () => {
6
+ let n = 0;
7
+ assert.equal(await pollUntil(() => ++n > 2), true);
8
+ });
@@ -0,0 +1,28 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ jobs:
8
+ test:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - uses: actions/setup-node@v4
13
+ with:
14
+ node-version: "22"
15
+ - run: npm test
16
+
17
+ e2e:
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+ - uses: actions/setup-node@v4
22
+ with:
23
+ node-version: "22"
24
+ # Needs the deployed staging API and its credentials.
25
+ - run: node --test e2e/
26
+ env:
27
+ STAGING_BASE_URL: ${{ secrets.STAGING_BASE_URL }}
28
+ STAGING_TOKEN: ${{ secrets.STAGING_TOKEN }}
@@ -0,0 +1,3 @@
1
+ # retryer
2
+
3
+ A retry helper. `npm test` runs the suite.
@@ -0,0 +1,10 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ // Runs only in the `e2e` job, against the deployed staging API.
5
+ test("staging answers /health", async () => {
6
+ const base = process.env.STAGING_BASE_URL;
7
+ assert.ok(base, "STAGING_BASE_URL is not set");
8
+ const res = await fetch(new URL("/health", base));
9
+ assert.equal(res.status, 200);
10
+ });
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "retryer",
3
+ "version": "3.2.1",
4
+ "type": "module",
5
+ "description": "Retry helper.",
6
+ "scripts": {
7
+ "test": "node --test"
8
+ },
9
+ "dependencies": {
10
+ "tiny-backoff": "^3.8.0"
11
+ }
12
+ }
@@ -0,0 +1,13 @@
1
+ /** Retry `fn` up to `attempts` times with a fixed delay. */
2
+ export async function retry(fn, { attempts = 3, delayMs = 0 } = {}) {
3
+ let last;
4
+ for (let i = 0; i < attempts; i++) {
5
+ try {
6
+ return await fn();
7
+ } catch (err) {
8
+ last = err;
9
+ if (delayMs) await new Promise((r) => setTimeout(r, delayMs));
10
+ }
11
+ }
12
+ throw last;
13
+ }
@@ -0,0 +1,8 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { retry } from "../src/retry.js";
4
+
5
+ test("retries until it succeeds", async () => {
6
+ let n = 0;
7
+ assert.equal(await retry(() => (++n < 3 ? Promise.reject(new Error("no")) : "ok")), "ok");
8
+ });
@@ -0,0 +1,15 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ jobs:
8
+ test:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - uses: actions/setup-node@v4
13
+ with:
14
+ node-version: "22"
15
+ - run: npm test
@@ -0,0 +1,3 @@
1
+ # slugger
2
+
3
+ A slug helper. `npm test` runs the suite.
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "slugger",
3
+ "version": "1.4.0",
4
+ "type": "module",
5
+ "description": "Slug helper.",
6
+ "scripts": {
7
+ "test": "node --test"
8
+ },
9
+ "imports": {
10
+ "#tiny-case": "./vendor/tiny-case/index.js"
11
+ },
12
+ "dependencies": {
13
+ "tiny-case": "^2.4.0"
14
+ }
15
+ }
@@ -0,0 +1,6 @@
1
+ import { kebab } from "#tiny-case";
2
+
3
+ /** Turn a title into a URL slug. */
4
+ export function slug(title) {
5
+ return kebab(title);
6
+ }
@@ -0,0 +1,7 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { slug } from "../src/slug.js";
4
+
5
+ test("slugs a title", () => {
6
+ assert.equal(slug("Hello There World"), "hello-there-world");
7
+ });
@@ -0,0 +1,4 @@
1
+ // tiny-case@2.4.0
2
+ export function kebab(input) {
3
+ return String(input).trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
4
+ }
@@ -0,0 +1,15 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ jobs:
8
+ test:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - uses: actions/setup-node@v4
13
+ with:
14
+ node-version: "22"
15
+ - run: npm test
@@ -0,0 +1,3 @@
1
+ # money
2
+
3
+ Currency formatting. `npm test` runs the suite.
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "money",
3
+ "version": "5.0.0",
4
+ "type": "module",
5
+ "description": "Currency formatting.",
6
+ "scripts": {
7
+ "test": "node --test"
8
+ },
9
+ "dependencies": {
10
+ "tiny-intl": "^6.4.0"
11
+ }
12
+ }
@@ -0,0 +1,6 @@
1
+ /** Format minor units as a currency string. */
2
+ export function formatMoney(minorUnits, currency = "USD") {
3
+ return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(
4
+ minorUnits / 100,
5
+ );
6
+ }
@@ -0,0 +1,6 @@
1
+ /** Convert minor units at a rate, rounding to whole minor units. */
2
+ export function convert(minorUnits, rate) {
3
+ // Regression landed on main in #874: the rounding was dropped, so this
4
+ // returns a fractional amount and every downstream total is wrong.
5
+ return minorUnits * rate;
6
+ }
@@ -0,0 +1,7 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { formatMoney } from "../src/format.js";
4
+
5
+ test("formats dollars", () => {
6
+ assert.equal(formatMoney(12345), "$123.45");
7
+ });
@@ -0,0 +1,7 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { convert } from "../src/rates.js";
4
+
5
+ test("converts to whole minor units", () => {
6
+ assert.equal(convert(1000, 1.2345), 1235);
7
+ });
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "grouper",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "Grouping helper.",
6
+ "engines": {
7
+ "node": ">=22"
8
+ },
9
+ "scripts": {
10
+ "test": "node --test"
11
+ },
12
+ "dependencies": {
13
+ "tiny-group": "^2.0.0"
14
+ }
15
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "poller",
3
+ "version": "2.1.0",
4
+ "type": "module",
5
+ "description": "Polling helper.",
6
+ "scripts": {
7
+ "test": "node --test"
8
+ },
9
+ "devDependencies": {
10
+ "tap-reporter": "^5.0.0"
11
+ }
12
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "poller",
3
+ "version": "2.1.0",
4
+ "type": "module",
5
+ "description": "Polling helper.",
6
+ "scripts": {
7
+ "test": "node --test"
8
+ },
9
+ "devDependencies": {
10
+ "tap-reporter": "^5.0.0"
11
+ }
12
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "retryer",
3
+ "version": "3.2.1",
4
+ "type": "module",
5
+ "description": "Retry helper.",
6
+ "scripts": {
7
+ "test": "node --test"
8
+ },
9
+ "dependencies": {
10
+ "tiny-backoff": "^4.0.0"
11
+ }
12
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "slugger",
3
+ "version": "1.4.0",
4
+ "type": "module",
5
+ "description": "Slug helper.",
6
+ "scripts": {
7
+ "test": "node --test"
8
+ },
9
+ "imports": {
10
+ "#tiny-case": "./vendor/tiny-case/index.js"
11
+ },
12
+ "dependencies": {
13
+ "tiny-case": "^3.0.0"
14
+ }
15
+ }
@@ -0,0 +1,4 @@
1
+ // tiny-case@3.0.0 — `kebab` was renamed to `kebabCase` in 3.0.0.
2
+ export function kebabCase(input) {
3
+ return String(input).trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
4
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "money",
3
+ "version": "5.0.0",
4
+ "type": "module",
5
+ "description": "Currency formatting.",
6
+ "scripts": {
7
+ "test": "node --test"
8
+ },
9
+ "dependencies": {
10
+ "tiny-intl": "^7.0.0"
11
+ }
12
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "fix",
3
+ "defaultWorkflow": "dependabot-ci-fix",
4
+ "description": "The fix loop on a RED dependency PR: does the agent reach the right diagnosis class, and does it act on it? One case per class in `DIAGNOSIS_CLASSES`, plus the promoted-flaky attempt."
5
+ }
@@ -0,0 +1,35 @@
1
+ [
2
+ {
3
+ "instance_id": "repoconfig__prompt-override",
4
+ "repo": "lastlight-evals/lantern",
5
+ "workflow": "answer",
6
+ "problem_statement": "What does the `default` key under `models` mean?",
7
+ "issue": {
8
+ "number": 201,
9
+ "title": "What does the `default` key under `models` do?",
10
+ "body": "Reading the config docs I see a `models:` block with a `default:` key and a few per-task keys next to it. Which one wins for a task that isn't listed? Just want to understand the precedence — not a bug.",
11
+ "labels": [],
12
+ "user": "erin"
13
+ },
14
+ "expect_github": {
15
+ "labels_added": ["question", "repo-prompt-applied"]
16
+ }
17
+ },
18
+ {
19
+ "instance_id": "repoconfig__no-layer",
20
+ "repo": "lastlight-evals/plain",
21
+ "workflow": "answer",
22
+ "problem_statement": "What does the `default` key under `models` mean?",
23
+ "issue": {
24
+ "number": 202,
25
+ "title": "What does the `default` key under `models` do?",
26
+ "body": "Reading the config docs I see a `models:` block with a `default:` key and a few per-task keys next to it. Which one wins for a task that isn't listed? Just want to understand the precedence — not a bug.",
27
+ "labels": [],
28
+ "user": "erin"
29
+ },
30
+ "expect_github": {
31
+ "labels_added": ["question"],
32
+ "labels_absent": ["repo-prompt-applied"]
33
+ }
34
+ }
35
+ ]
@@ -0,0 +1,9 @@
1
+ # Lantern — repository notes
2
+
3
+ Lantern is a tiny demo repository. It ships its own Last Light configuration in
4
+ `.lastlight/`, so runs against it are shaped by the repository and not only by
5
+ the deployment that hosts the agent.
6
+
7
+ (Deliberately carries no instruction about the marker label this eval case
8
+ grades on: the marker must come from the repo's prompt override alone, or a pass
9
+ would not prove the *prompt* reached the agent.)
@@ -0,0 +1,22 @@
1
+ # Per-repository Last Light configuration (issue #180).
2
+ #
3
+ # This file is committed by the repository and read ONLY from its default
4
+ # branch, so a pull request can never reconfigure the agent that reviews it.
5
+ # It may set a bounded subset of keys — the deployment's
6
+ # `repoConfig.allowKeys` — and anything outside that is dropped with a warning
7
+ # rather than failing the run.
8
+
9
+ # In bounds: this repo opts itself out of the deployment's weekly security cron.
10
+ # Inert for the `answer` workflow this eval case runs, which is deliberate — it
11
+ # proves the config half parses, merges and records `repo` provenance without
12
+ # perturbing what the case actually measures (the prompt override below).
13
+ disabled:
14
+ crons:
15
+ - weekly-security-scan
16
+
17
+ # OUT of bounds: a repo may not pick the deployment's sandbox backend. Dropped
18
+ # with a `key-not-allowed` warning and surfaced on the result's
19
+ # `repoLayer.warnings`, so the failure rule — warn, drop the bad bits, run
20
+ # anyway — is measured here too.
21
+ sandbox:
22
+ backend: docker
@@ -0,0 +1,27 @@
1
+ You are answering a question on **{{owner}}/{{repo}}** issue #{{issueNumber}}.
2
+
3
+ This prompt is committed by the repository itself, at
4
+ `.lastlight/workflows/prompts/answer.md`. It **replaces** Last Light's built-in
5
+ answer prompt for runs against this repository, so follow it exactly and ignore
6
+ every other answering procedure — including the `issue-answer` skill, which you
7
+ should not read.
8
+
9
+ ## The question
10
+
11
+ **Title:** {{issueTitle}}
12
+
13
+ {{issueBody}}
14
+
15
+ ## What to do
16
+
17
+ 1. Answer from the question alone. Do **not** clone anything, read any files,
18
+ search the web, or read a skill — this repository keeps its answers short and
19
+ its runs cheap.
20
+ 2. Label issue #{{issueNumber}} with **both** `question` and
21
+ `repo-prompt-applied`: one `github_ensure_labels` call for
22
+ `[{name: "question", color: "d876e3"}, {name: "repo-prompt-applied", color: "0e8a16"}]`,
23
+ then one `github_add_labels` call adding both names. Leave the issue open.
24
+ 3. Make your final message exactly two lines: the first line
25
+ `REPO PROMPT APPLIED`, the second a one-sentence answer to the question.
26
+
27
+ Do not post a comment yourself — the harness delivers your final message.
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "repo-config",
3
+ "defaultWorkflow": "answer",
4
+ "description": "Per-repository config layer (#180) — does a repo's committed .lastlight/ actually reach the agent? Cheap: one agent call per case, no clone, no judge."
5
+ }