@prismatic-io/lux 0.0.2-preview.3 → 0.0.2-preview.4
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/lib/answerers/terminal/index.js +1 -1
- package/lib/answerers/terminal/index.js.map +1 -1
- package/lib/assertions/core/helpers.d.ts +2 -7
- package/lib/assertions/core/helpers.d.ts.map +1 -1
- package/lib/assertions/core/helpers.js +2 -6
- package/lib/assertions/core/helpers.js.map +1 -1
- package/lib/cli/program.d.ts.map +1 -1
- package/lib/cli/program.js +6 -2
- package/lib/cli/program.js.map +1 -1
- package/lib/cli/render/reporter.d.ts.map +1 -1
- package/lib/cli/render/reporter.js +16 -3
- package/lib/cli/render/reporter.js.map +1 -1
- package/lib/cli/run-options.d.ts +1 -0
- package/lib/cli/run-options.d.ts.map +1 -1
- package/lib/cli/run-options.js.map +1 -1
- package/lib/core/artifact-evidence.d.ts +8 -0
- package/lib/core/artifact-evidence.d.ts.map +1 -1
- package/lib/core/artifact-evidence.js +36 -4
- package/lib/core/artifact-evidence.js.map +1 -1
- package/lib/core/assertion.d.ts +12 -0
- package/lib/core/assertion.d.ts.map +1 -1
- package/lib/core/assertion.js +13 -0
- package/lib/core/assertion.js.map +1 -1
- package/lib/core/events.d.ts +2 -0
- package/lib/core/events.d.ts.map +1 -1
- package/lib/core/events.js +1 -0
- package/lib/core/events.js.map +1 -1
- package/lib/core/run.d.ts +1 -0
- package/lib/core/run.d.ts.map +1 -1
- package/lib/core/wire.d.ts +1 -0
- package/lib/core/wire.d.ts.map +1 -1
- package/lib/core/wire.js +5 -1
- package/lib/core/wire.js.map +1 -1
- package/lib/index.d.ts +0 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +0 -1
- package/lib/index.js.map +1 -1
- package/lib/orchestrator/config.d.ts.map +1 -1
- package/lib/orchestrator/config.js +0 -2
- package/lib/orchestrator/config.js.map +1 -1
- package/lib/orchestrator/grader-experiment.d.ts +12 -0
- package/lib/orchestrator/grader-experiment.d.ts.map +1 -1
- package/lib/orchestrator/grading.d.ts +2 -1
- package/lib/orchestrator/grading.d.ts.map +1 -1
- package/lib/orchestrator/grading.js +16 -4
- package/lib/orchestrator/grading.js.map +1 -1
- package/lib/orchestrator/orchestrator.d.ts +3 -0
- package/lib/orchestrator/orchestrator.d.ts.map +1 -1
- package/lib/orchestrator/orchestrator.js +3 -0
- package/lib/orchestrator/orchestrator.js.map +1 -1
- package/lib/orchestrator/regrade.d.ts +6 -0
- package/lib/orchestrator/regrade.d.ts.map +1 -1
- package/lib/orchestrator/run-execution.d.ts.map +1 -1
- package/lib/orchestrator/run-execution.js +2 -2
- package/lib/orchestrator/run-execution.js.map +1 -1
- package/package.json +1 -2
- package/skills/lux/SKILL.md +7 -3
- package/skills/lux/references/cli.md +25 -2
- package/skills/lux/references/eval-authoring.md +156 -4
- package/skills/lux-answerer/SKILL.md +1 -1
- package/src/answerers/terminal/index.ts +1 -1
- package/src/assertions/core/helpers.ts +3 -6
- package/src/cli/program.ts +9 -2
- package/src/cli/render/reporter.ts +18 -3
- package/src/cli/run-options.ts +1 -0
- package/src/core/artifact-evidence.ts +47 -4
- package/src/core/assertion.ts +15 -0
- package/src/core/events.ts +1 -0
- package/src/core/wire.ts +5 -1
- package/src/index.ts +0 -5
- package/src/orchestrator/config.ts +0 -2
- package/src/orchestrator/grading.ts +21 -2
- package/src/orchestrator/orchestrator.ts +6 -0
- package/src/orchestrator/run-execution.ts +2 -1
- package/lib/answerers/scripted/index.d.ts +0 -8
- package/lib/answerers/scripted/index.d.ts.map +0 -1
- package/lib/answerers/scripted/index.js +0 -163
- package/lib/answerers/scripted/index.js.map +0 -1
- package/src/answerers/scripted/index.ts +0 -186
|
@@ -51,13 +51,71 @@ Prefer deterministic assertions:
|
|
|
51
51
|
- `run-succeeded`: the driver completed; necessary but rarely sufficient.
|
|
52
52
|
- `file-exists`: a required artifact exists.
|
|
53
53
|
- `file-contains`: a stable literal is present.
|
|
54
|
-
- `json-
|
|
54
|
+
- `json-pointer-equals`: structured output contains the expected value.
|
|
55
55
|
- `command-exits-zero`: project-authored validation succeeds.
|
|
56
56
|
|
|
57
|
+
Use the more specific bundled evidence checks when they match the contract:
|
|
58
|
+
|
|
59
|
+
- Files: `file-matches`, `file-regex-set`, `file-regex-count`, and `glob-count`.
|
|
60
|
+
- Output/data: `contains`, `not-contains`, `regex`, and `json-pointer-equals`.
|
|
61
|
+
- Interaction: `interrupt-count`, `tool-called`, `tool-called-with`, and
|
|
62
|
+
`tool-call-count`; use `by: "main"` or `by: "subagent"` when ownership matters.
|
|
63
|
+
- Paired tool evidence: `tool-invocation`, `tool-result-contains`,
|
|
64
|
+
`tool-result-json-pointer-equals`, `tool-result-is-error`, and
|
|
65
|
+
`no-error-results`. Prefer these when result status or payload matters; a tool
|
|
66
|
+
call without a paired result is unresolved, not successful.
|
|
67
|
+
- Budgets: `latency-under`, `tokens-under`, `cost-under`, and
|
|
68
|
+
`artifact-bytes-under`.
|
|
69
|
+
- MCP discovery: `server-info-equals`, `tool-listed`,
|
|
70
|
+
`tools-listed-count`, and `capability-absent`.
|
|
71
|
+
|
|
72
|
+
Most file assertions read captured `artifacts`; set `where: "cwd"` only when
|
|
73
|
+
the durable contract intentionally includes the staged run workspace. Use
|
|
74
|
+
stable assertion `id` values for repetitions, comparisons, annotations, and
|
|
75
|
+
experiments. `weight` affects aggregate score and `threshold` sets an optional
|
|
76
|
+
per-assertion score floor.
|
|
77
|
+
|
|
57
78
|
Use `rubric` for semantic properties such as design quality, completeness, or
|
|
58
79
|
whether a test meaningfully covers behavior. Keep criteria narrow. Avoid words
|
|
59
80
|
like “good,” “proper,” or “best practice” without observable conditions.
|
|
60
81
|
|
|
82
|
+
Bound rubric evidence to what the criterion needs. Include `tool-result` event
|
|
83
|
+
evidence when status matters, and request tool inputs/results only when their
|
|
84
|
+
payload is necessary; this controls judge context and cost.
|
|
85
|
+
|
|
86
|
+
Persona answering and rubric judging have different difficulty profiles. A
|
|
87
|
+
persona usually chooses from facts already supplied in the case, so prefer a
|
|
88
|
+
cheap model and low effort. Semantic grading should use a stronger model and
|
|
89
|
+
enough effort to distinguish real evidence from plausible prose. Configure
|
|
90
|
+
them independently when that tradeoff matters:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
// lux.config.ts
|
|
94
|
+
defaultAnswerer: {
|
|
95
|
+
name: "persona",
|
|
96
|
+
config: {
|
|
97
|
+
provider: "codex",
|
|
98
|
+
model: "YOUR_SMALL_PERSONA_MODEL",
|
|
99
|
+
reasoningEffort: "low",
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
// case assertion
|
|
104
|
+
{
|
|
105
|
+
id: "behavior-quality",
|
|
106
|
+
type: "rubric",
|
|
107
|
+
criteria: "The tests exercise retry exhaustion and recovery.",
|
|
108
|
+
provider: "codex",
|
|
109
|
+
model: "YOUR_STRONG_JUDGE_MODEL",
|
|
110
|
+
reasoningEffort: "high",
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The project `harness` supplies shared defaults to persona and rubric calls;
|
|
115
|
+
answerer config and rubric fields override it independently. Always pin model
|
|
116
|
+
and effort when comparing runs, and inspect answerer versus judge usage in the
|
|
117
|
+
run summary before increasing either budget.
|
|
118
|
+
|
|
61
119
|
Assertions should test user-visible behavior and durable contracts. Avoid
|
|
62
120
|
requiring a specific function name, file layout, or algorithm unless that is
|
|
63
121
|
the contract under evaluation.
|
|
@@ -73,9 +131,51 @@ The persona is simulated user state, not another system prompt. Include:
|
|
|
73
131
|
Do not tell the persona how to help the agent solve the task. If no interaction
|
|
74
132
|
is part of the capability being evaluated, omit the persona or keep it minimal.
|
|
75
133
|
|
|
76
|
-
|
|
77
|
-
handling. Use `textQuestionFallback: true` only when
|
|
78
|
-
models that ask in prose instead of using the
|
|
134
|
+
For Claude Code, use `interactionMode: "defer-resume"` for automated
|
|
135
|
+
question-tool handling. Use `textQuestionFallback: true` only when
|
|
136
|
+
intentionally evaluating models that ask in prose instead of using the
|
|
137
|
+
supported tool. For Codex, keep the default `interactionMode: "app-server"`
|
|
138
|
+
when the case can interrupt; compatibility mode `"exec"` cannot accept
|
|
139
|
+
mid-run responses.
|
|
140
|
+
|
|
141
|
+
Use `--interactive` for a real terminal user or `--claude-answerer` for the
|
|
142
|
+
Claude plugin protocol. Both force sequential execution.
|
|
143
|
+
|
|
144
|
+
## Project extensions
|
|
145
|
+
|
|
146
|
+
For a reusable project-specific assertion, define it with `defineAssertion`
|
|
147
|
+
and a direct Zod object whose `type` is a matching literal. Import `z` from Lux,
|
|
148
|
+
return an ordinary failed result for missing/invalid product evidence, and
|
|
149
|
+
reserve throws or `errorMessage` for evaluator faults. Increment
|
|
150
|
+
`pluginVersion` whenever schema or evaluation behavior changes. Prefer the
|
|
151
|
+
bounded, traversal-safe `readRunArtifactText` helper over direct filesystem
|
|
152
|
+
reads.
|
|
153
|
+
|
|
154
|
+
Build a typed case authoring function once instead of importing plugins in
|
|
155
|
+
every case:
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
const assertions = defineAssertionRegistry(projectAssertion);
|
|
159
|
+
export const defineEvalCase = defineLuxCase.extend({ assertions });
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Case-bound plugins remain plain serialized plans but require the current case
|
|
163
|
+
path for regrading: `lux grade <run> --case <case.ts>`.
|
|
164
|
+
|
|
165
|
+
Use `defineFixtures` for Vitest-style suite/case setup around `await use()` or
|
|
166
|
+
`defineLifecycleHooks` when cleanup-returning callbacks fit better. Setup runs
|
|
167
|
+
in declaration order and teardown in reverse, including failures. Call
|
|
168
|
+
`await use()` exactly once. Fixture `value`/`identity` must be public JSON;
|
|
169
|
+
keep secrets in the prepared resource or environment. Increment
|
|
170
|
+
`pluginVersion` when lifecycle behavior changes. Register globally in
|
|
171
|
+
`lux.config.ts`, or attach selectively with
|
|
172
|
+
`defineEvalCase.extend({ fixtures: [...] })`.
|
|
173
|
+
|
|
174
|
+
Custom drivers that own an in-memory filesystem should persist it with
|
|
175
|
+
`writeArtifactSnapshot` from `@prismatic-io/lux/artifact-snapshot`. Snapshots
|
|
176
|
+
are bounded, traversal-safe, immutable per name, and can use `baseSnapshot`
|
|
177
|
+
for copy-on-write reuse while remaining independently complete. Ordinary file
|
|
178
|
+
assertions then grade the indexed files without a snapshot-specific API.
|
|
79
179
|
|
|
80
180
|
## Experiments for prompts, skills, and agent source
|
|
81
181
|
|
|
@@ -100,6 +200,58 @@ Campaigns should:
|
|
|
100
200
|
- budget calls, tokens, cost, and wall time;
|
|
101
201
|
- use repetitions and confidence gates appropriate to stochastic outcomes;
|
|
102
202
|
- reserve held-out cases for promotion and final confirmation.
|
|
203
|
+
- acknowledge every candidate validation command with
|
|
204
|
+
`unsafeHostExecution: true`; validators must be observational and must not
|
|
205
|
+
mutate the candidate tree.
|
|
206
|
+
|
|
207
|
+
A minimal current agent campaign has this shape (selectors are objects, mutable
|
|
208
|
+
paths are candidate-relative globs, and budgets use call/token/cost limits):
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
export default defineExperiment({
|
|
212
|
+
id: "skill-improvement",
|
|
213
|
+
subject: { root: "..", mutable: ["skills/example/**"] },
|
|
214
|
+
profiles: ["claude-low", "codex-low"],
|
|
215
|
+
splits: {
|
|
216
|
+
train: { tags: ["train"] },
|
|
217
|
+
validation: { tags: ["validation"] },
|
|
218
|
+
test: { tags: ["test"] },
|
|
219
|
+
},
|
|
220
|
+
repetitions: { search: 1, promotion: 3 },
|
|
221
|
+
objectives: {
|
|
222
|
+
constraints: [
|
|
223
|
+
{ metric: "quality.caseRegressions", max: 0 },
|
|
224
|
+
{ metric: "quality.passRate", min: 0.8, split: "test" },
|
|
225
|
+
],
|
|
226
|
+
minimize: [{ metric: "agent.total" }, { metric: "subject.bytes" }],
|
|
227
|
+
},
|
|
228
|
+
optimizer: {
|
|
229
|
+
algorithm: "reflective",
|
|
230
|
+
candidateSelection: "pareto",
|
|
231
|
+
promotionTopK: 2,
|
|
232
|
+
config: {
|
|
233
|
+
provider: "codex",
|
|
234
|
+
model: "YOUR_OPTIMIZER_MODEL",
|
|
235
|
+
reasoningEffort: "low",
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
budget: {
|
|
239
|
+
maxCandidates: 4,
|
|
240
|
+
patience: 1,
|
|
241
|
+
maxMetricCalls: 60,
|
|
242
|
+
maxJudgeCalls: 120,
|
|
243
|
+
maxProposalCalls: 3,
|
|
244
|
+
maxTokens: 2_000_000,
|
|
245
|
+
maxCostUsd: 30,
|
|
246
|
+
maxUnpricedModelCalls: 0,
|
|
247
|
+
maxWallTimeMs: 3_600_000,
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Use the experiment directory printed by the run for `view` and `report`.
|
|
253
|
+
Review its held-out evidence, then pass that same verified directory to
|
|
254
|
+
`apply`; do not guess a directory from the campaign ID.
|
|
103
255
|
|
|
104
256
|
Use `experiment` for authored variants and `optimize` for model-proposed source
|
|
105
257
|
changes. Always run the corresponding `--plan` command first.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: lux-answerer
|
|
3
|
-
version: 0.0.2-preview.
|
|
3
|
+
version: 0.0.2-preview.4
|
|
4
4
|
description: Play the persona for a running Lux orchestrator that uses the claude-code answerer. Read structured events on stdout, decide answers from persona + context, write JSON to its answer channel.
|
|
5
5
|
user-invocable: false
|
|
6
6
|
allowed-tools: Bash, Read
|
|
@@ -70,7 +70,7 @@ class TerminalAnswererImpl implements Answerer {
|
|
|
70
70
|
if (!stdin.isTTY || !stdout.isTTY) {
|
|
71
71
|
throw new Error(
|
|
72
72
|
"terminal answerer: --interactive requires an attached terminal; " +
|
|
73
|
-
"use a
|
|
73
|
+
"use a model answerer in CI or --claude-answerer from the Claude plugin",
|
|
74
74
|
);
|
|
75
75
|
}
|
|
76
76
|
this.interface = createInterface({ input: stdin, output: stdout, terminal: true });
|
|
@@ -3,6 +3,7 @@ import { lstat } from "node:fs/promises";
|
|
|
3
3
|
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { inspectRunArtifact, runArtifactsRoot } from "../../core/artifact-evidence.js";
|
|
6
|
+
import { baseAssertionFields } from "../../core/assertion.js";
|
|
6
7
|
import type { Run, RunEvent } from "../../core/index.js";
|
|
7
8
|
|
|
8
9
|
const FsLocationSchema = z.enum(["artifacts", "cwd"]);
|
|
@@ -175,12 +176,6 @@ export const deepEquals = (a: unknown, b: unknown): boolean => {
|
|
|
175
176
|
* Mirrors {@link Assertion} from the core module while letting each built-in
|
|
176
177
|
* assertion reject misspelled type-specific fields at its own schema boundary.
|
|
177
178
|
*/
|
|
178
|
-
export const baseAssertionFields = {
|
|
179
|
-
id: z.string().min(1).optional(),
|
|
180
|
-
weight: z.number().positive().optional(),
|
|
181
|
-
threshold: z.number().min(0).max(1).optional(),
|
|
182
|
-
name: z.string().optional(),
|
|
183
|
-
} as const;
|
|
184
179
|
|
|
185
180
|
/**
|
|
186
181
|
* Where on the run dir an assertion looks for files. Defaults to
|
|
@@ -190,6 +185,8 @@ export const LocationSchema = FsLocationSchema.default("artifacts");
|
|
|
190
185
|
|
|
191
186
|
// `name` is intentionally excluded: for tool assertions the `name` field is
|
|
192
187
|
// the tool's name, which then doubles as the display label.
|
|
188
|
+
export { baseAssertionFields };
|
|
189
|
+
|
|
193
190
|
export const weightThresholdFields = {
|
|
194
191
|
id: baseAssertionFields.id,
|
|
195
192
|
weight: baseAssertionFields.weight,
|
package/src/cli/program.ts
CHANGED
|
@@ -163,7 +163,7 @@ const registerExperimentCommands = (program: Command): void => {
|
|
|
163
163
|
});
|
|
164
164
|
|
|
165
165
|
program
|
|
166
|
-
.command("run [filters...]"
|
|
166
|
+
.command("run [filters...]")
|
|
167
167
|
.description("discover and run cases; filter by name/path substring or --tag")
|
|
168
168
|
.option(
|
|
169
169
|
"-t, --tag <tag>",
|
|
@@ -184,6 +184,7 @@ const registerExperimentCommands = (program: Command): void => {
|
|
|
184
184
|
"--claude-answerer",
|
|
185
185
|
"delegate questions through the Claude plugin's lux-answerer skill",
|
|
186
186
|
)
|
|
187
|
+
.option("--skip-judge", "skip rubric LLM judge assertions")
|
|
187
188
|
.option("--runs-root <path>", "run-dir root (overrides lux.config.ts)")
|
|
188
189
|
.option("--profile <id>", "named driver/answerer profile from lux.config.ts")
|
|
189
190
|
.option("--subject-root <path>", "root bound into subjectPath() values")
|
|
@@ -623,11 +624,16 @@ export const buildProgram = (): Command => {
|
|
|
623
624
|
.description(
|
|
624
625
|
"Coding-agent evaluation and improvement with deterministic assertions and human-in-the-loop runs",
|
|
625
626
|
)
|
|
626
|
-
.version(PKG_VERSION)
|
|
627
|
+
.version(PKG_VERSION)
|
|
628
|
+
.addHelpText(
|
|
629
|
+
"after",
|
|
630
|
+
"\nAgent skills:\n $ lux skills list\n $ lux skills install codex --project\n $ lux skills install claude --project",
|
|
631
|
+
);
|
|
627
632
|
registerProjectCommands(program);
|
|
628
633
|
registerExperimentCommands(program);
|
|
629
634
|
registerCandidateCommands(program);
|
|
630
635
|
registerReportingCommands(program);
|
|
636
|
+
program.action(() => program.outputHelp());
|
|
631
637
|
return program;
|
|
632
638
|
};
|
|
633
639
|
|
|
@@ -965,6 +971,7 @@ const runCommand = async (filters: string[], options: RunOptions): Promise<void>
|
|
|
965
971
|
runsRoot,
|
|
966
972
|
fixturesRoot: config.fixturesRoot,
|
|
967
973
|
answererOverride: resolveAnswererOverride(options),
|
|
974
|
+
skipJudge: options.skipJudge,
|
|
968
975
|
observer: reporter,
|
|
969
976
|
abortSignal: controller.signal,
|
|
970
977
|
concurrency,
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
createToolOutcomeAccumulator,
|
|
5
5
|
emptyRunUsage,
|
|
6
6
|
type GradingAssertionEntry,
|
|
7
|
+
type GradingReport,
|
|
7
8
|
harnessUsage,
|
|
8
9
|
isUsageEmpty,
|
|
9
10
|
type LifecycleStage,
|
|
@@ -77,6 +78,7 @@ type CaseView = {
|
|
|
77
78
|
assertionTotal: number;
|
|
78
79
|
currentAssertion: string | null;
|
|
79
80
|
assertions: GradingAssertionEntry[];
|
|
81
|
+
skippedAssertions: NonNullable<GradingReport["skippedAssertions"]>;
|
|
80
82
|
usage: RunUsage;
|
|
81
83
|
/** Per-API-call agent usage, in order — the split behind `usage.agent`. */
|
|
82
84
|
agentTurns: TurnUsage[];
|
|
@@ -117,7 +119,10 @@ const statusMark = (status: CaseStatus, theme: Theme): string => {
|
|
|
117
119
|
|
|
118
120
|
const assertionTally = (view: CaseView): string => {
|
|
119
121
|
const passed = view.assertions.filter((a) => a.passed).length;
|
|
120
|
-
|
|
122
|
+
const evaluated = view.assertions.length === 0 ? "—" : `${passed}/${view.assertions.length}`;
|
|
123
|
+
return view.skippedAssertions.length > 0
|
|
124
|
+
? `${evaluated} · ${view.skippedAssertions.length} skipped`
|
|
125
|
+
: evaluated;
|
|
121
126
|
};
|
|
122
127
|
|
|
123
128
|
/**
|
|
@@ -233,6 +238,7 @@ export class ConsoleReporter implements SuiteObserver {
|
|
|
233
238
|
assertionTotal: 0,
|
|
234
239
|
currentAssertion: null,
|
|
235
240
|
assertions: [],
|
|
241
|
+
skippedAssertions: [],
|
|
236
242
|
usage: emptyRunUsage(),
|
|
237
243
|
agentTurns: [],
|
|
238
244
|
phaseDurationMs: null,
|
|
@@ -299,6 +305,7 @@ export class ConsoleReporter implements SuiteObserver {
|
|
|
299
305
|
if (result.errorMessage) view.errorMessage = result.errorMessage;
|
|
300
306
|
if (result.result) {
|
|
301
307
|
view.assertions = result.result.grading.assertions;
|
|
308
|
+
view.skippedAssertions = result.result.grading.skippedAssertions ?? [];
|
|
302
309
|
view.usage = result.result.metadata.usage;
|
|
303
310
|
view.phaseDurationMs = result.result.metadata.phaseDurationMs ?? null;
|
|
304
311
|
const tools = result.result.metadata.toolOutcomes;
|
|
@@ -315,6 +322,7 @@ export class ConsoleReporter implements SuiteObserver {
|
|
|
315
322
|
// Assertions reported before the run failed to persist are not results
|
|
316
323
|
// and must not inflate the closing tally of a case that errored.
|
|
317
324
|
view.assertions = [];
|
|
325
|
+
view.skippedAssertions = [];
|
|
318
326
|
}
|
|
319
327
|
|
|
320
328
|
this.region.staticLine(this.finishedLine(view));
|
|
@@ -452,6 +460,11 @@ export class ConsoleReporter implements SuiteObserver {
|
|
|
452
460
|
body: !entry.passed && entry.comment ? [this.theme.dim(entry.comment)] : [],
|
|
453
461
|
});
|
|
454
462
|
}
|
|
463
|
+
for (const skipped of view.skippedAssertions) {
|
|
464
|
+
nodes.push({
|
|
465
|
+
head: `${this.theme.dim("⊘")} ${skipped.name} ${this.theme.dim("skipped: judge disabled")}`,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
455
468
|
|
|
456
469
|
if (view.toolCalls > 0 || view.toolUnmatchedResults > 0 || view.toolMalformed > 0) {
|
|
457
470
|
const parts = [
|
|
@@ -550,15 +563,17 @@ export class ConsoleReporter implements SuiteObserver {
|
|
|
550
563
|
}
|
|
551
564
|
|
|
552
565
|
const assertions = this.cases.flatMap((c) => c.assertions);
|
|
553
|
-
|
|
566
|
+
const skippedAssertions = this.cases.flatMap((c) => c.skippedAssertions);
|
|
567
|
+
if (assertions.length > 0 || skippedAssertions.length > 0) {
|
|
554
568
|
const ok = assertions.filter((a) => a.passed).length;
|
|
555
569
|
const bad = assertions.length - ok;
|
|
556
570
|
const tally = formatTally(
|
|
557
571
|
[
|
|
558
572
|
[ok, this.theme.green("passed")],
|
|
559
573
|
[bad, this.theme.red("failed")],
|
|
574
|
+
[skippedAssertions.length, this.theme.dim("skipped")],
|
|
560
575
|
],
|
|
561
|
-
assertions.length,
|
|
576
|
+
assertions.length + skippedAssertions.length,
|
|
562
577
|
);
|
|
563
578
|
lines.push(` ${label("Assertions")}${tally}`);
|
|
564
579
|
}
|
package/src/cli/run-options.ts
CHANGED
|
@@ -7,12 +7,15 @@ import type { Run } from "./run.js";
|
|
|
7
7
|
const DEFAULT_ARTIFACT_READ_BYTES = 5 * 1024 * 1024;
|
|
8
8
|
const MAX_ARTIFACT_READ_BYTES = 10 * 1024 * 1024;
|
|
9
9
|
|
|
10
|
+
export type RunArtifactIssueReason = "absent" | "unreadable";
|
|
11
|
+
|
|
10
12
|
export type RunArtifactInspection = {
|
|
11
13
|
target: string;
|
|
12
14
|
root: string;
|
|
13
15
|
artifact?: Artifact;
|
|
14
16
|
stats?: Stats;
|
|
15
17
|
issue?: string;
|
|
18
|
+
reason?: RunArtifactIssueReason;
|
|
16
19
|
};
|
|
17
20
|
|
|
18
21
|
type RunArtifactEvidence = {
|
|
@@ -82,6 +85,7 @@ export const inspectRunArtifact = async (
|
|
|
82
85
|
target: authoredPath,
|
|
83
86
|
root: runArtifactsRoot(run),
|
|
84
87
|
issue: `${authoredPath} contains a null byte`,
|
|
88
|
+
reason: "unreadable",
|
|
85
89
|
};
|
|
86
90
|
}
|
|
87
91
|
const matching = run.artifacts.filter((candidate) => candidate.path === authoredPath);
|
|
@@ -91,6 +95,7 @@ export const inspectRunArtifact = async (
|
|
|
91
95
|
target: resolve(root, authoredPath),
|
|
92
96
|
root,
|
|
93
97
|
issue: `${authoredPath} was not captured in the frozen artifact index`,
|
|
98
|
+
reason: "absent",
|
|
94
99
|
};
|
|
95
100
|
}
|
|
96
101
|
if (matching.some((candidate) => candidate.root !== undefined && !isAbsolute(candidate.root))) {
|
|
@@ -98,6 +103,7 @@ export const inspectRunArtifact = async (
|
|
|
98
103
|
target: authoredPath,
|
|
99
104
|
root: defaultArtifactsRoot(run),
|
|
100
105
|
issue: `${authoredPath} declares a non-absolute artifact root`,
|
|
106
|
+
reason: "unreadable",
|
|
101
107
|
};
|
|
102
108
|
}
|
|
103
109
|
const artifact = matching[0];
|
|
@@ -108,6 +114,7 @@ export const inspectRunArtifact = async (
|
|
|
108
114
|
target: authoredPath,
|
|
109
115
|
root: defaultArtifactsRoot(run),
|
|
110
116
|
issue: `${authoredPath} is ambiguous across multiple artifact roots`,
|
|
117
|
+
reason: "unreadable",
|
|
111
118
|
};
|
|
112
119
|
}
|
|
113
120
|
// A non-default root is trusted only for an exact artifact-index entry.
|
|
@@ -116,7 +123,12 @@ export const inspectRunArtifact = async (
|
|
|
116
123
|
const root = matchingRoots[0] ?? defaultArtifactsRoot(run);
|
|
117
124
|
const target = resolve(root, authoredPath);
|
|
118
125
|
if (pathEscapesRoot(root, target)) {
|
|
119
|
-
return {
|
|
126
|
+
return {
|
|
127
|
+
target,
|
|
128
|
+
root,
|
|
129
|
+
issue: `${authoredPath} escapes the captured artifact root`,
|
|
130
|
+
reason: "unreadable",
|
|
131
|
+
};
|
|
120
132
|
}
|
|
121
133
|
|
|
122
134
|
const path = relative(root, target);
|
|
@@ -134,7 +146,13 @@ export const inspectRunArtifact = async (
|
|
|
134
146
|
} catch (error) {
|
|
135
147
|
const code = (error as NodeJS.ErrnoException).code;
|
|
136
148
|
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
137
|
-
return {
|
|
149
|
+
return {
|
|
150
|
+
target,
|
|
151
|
+
root,
|
|
152
|
+
...identified,
|
|
153
|
+
issue: `${authoredPath} not found`,
|
|
154
|
+
reason: "absent",
|
|
155
|
+
};
|
|
138
156
|
}
|
|
139
157
|
throw error;
|
|
140
158
|
}
|
|
@@ -144,16 +162,17 @@ export const inspectRunArtifact = async (
|
|
|
144
162
|
root,
|
|
145
163
|
...identified,
|
|
146
164
|
issue: `${authoredPath} resolves through a symlink`,
|
|
165
|
+
reason: "unreadable",
|
|
147
166
|
};
|
|
148
167
|
}
|
|
149
168
|
if (index < components.length - 1 && !stats.isDirectory()) {
|
|
150
|
-
return { target, root, ...identified, issue: `${authoredPath} not found
|
|
169
|
+
return { target, root, ...identified, issue: `${authoredPath} not found`, reason: "absent" };
|
|
151
170
|
}
|
|
152
171
|
if (index === components.length - 1) {
|
|
153
172
|
return { target, root, ...identified, stats };
|
|
154
173
|
}
|
|
155
174
|
}
|
|
156
|
-
return { target, root, ...identified, issue: `${authoredPath} not found
|
|
175
|
+
return { target, root, ...identified, issue: `${authoredPath} not found`, reason: "absent" };
|
|
157
176
|
};
|
|
158
177
|
|
|
159
178
|
export type ReadRunArtifactTextOptions = {
|
|
@@ -247,3 +266,27 @@ export const readRunArtifactText = async (
|
|
|
247
266
|
): Promise<string> => {
|
|
248
267
|
return (await readRunArtifactBytes(run, authoredPath, options)).toString("utf8");
|
|
249
268
|
};
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Read optional artifact evidence. Returns null when the artifact was never
|
|
272
|
+
* captured, and still throws when one exists but is unsafe to consume, so a
|
|
273
|
+
* traversal or symlink violation is never mistaken for missing evidence.
|
|
274
|
+
*/
|
|
275
|
+
export const readRunArtifactTextIfPresent = async (
|
|
276
|
+
run: RunArtifactEvidence,
|
|
277
|
+
authoredPath: string,
|
|
278
|
+
options: ReadRunArtifactTextOptions = {},
|
|
279
|
+
): Promise<string | null> => {
|
|
280
|
+
const inspected = await inspectRunArtifact(run, authoredPath);
|
|
281
|
+
if (inspected.reason === "absent") return null;
|
|
282
|
+
if (!inspected.stats?.isFile()) {
|
|
283
|
+
throw new Error(
|
|
284
|
+
inspected.issue ??
|
|
285
|
+
(inspected.stats ? `${authoredPath} is not a file` : `${authoredPath} not found`),
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
const maxBytes = options.maxBytes ?? DEFAULT_ARTIFACT_READ_BYTES;
|
|
289
|
+
return (
|
|
290
|
+
await readInspectedFileBytes(inspected.target, inspected.stats, maxBytes, authoredPath)
|
|
291
|
+
).toString("utf8");
|
|
292
|
+
};
|
package/src/core/assertion.ts
CHANGED
|
@@ -100,6 +100,13 @@ export const assertionPluginIssue = (definition: {
|
|
|
100
100
|
* Define a schema-validated assertion plugin while inferring the config passed
|
|
101
101
|
* to `evaluate` from the schema.
|
|
102
102
|
*/
|
|
103
|
+
export const baseAssertionFields = {
|
|
104
|
+
id: z.string().min(1).optional(),
|
|
105
|
+
weight: z.number().positive().optional(),
|
|
106
|
+
threshold: z.number().min(0).max(1).optional(),
|
|
107
|
+
name: z.string().optional(),
|
|
108
|
+
} as const;
|
|
109
|
+
|
|
103
110
|
export const defineAssertion = <S extends z.ZodObject>(
|
|
104
111
|
definition: AssertionPluginDefinition<S>,
|
|
105
112
|
): AssertionPlugin<SchemaAssertion<S>, SchemaAssertionInput<S>> => {
|
|
@@ -141,11 +148,19 @@ export const GradingAssertionEntrySchema = z.object({
|
|
|
141
148
|
});
|
|
142
149
|
export type GradingAssertionEntry = z.infer<typeof GradingAssertionEntrySchema>;
|
|
143
150
|
|
|
151
|
+
const SkippedGradingAssertionSchema = z.object({
|
|
152
|
+
id: z.string(),
|
|
153
|
+
type: z.string(),
|
|
154
|
+
name: z.string(),
|
|
155
|
+
reason: z.literal("judge-disabled"),
|
|
156
|
+
});
|
|
157
|
+
|
|
144
158
|
export const GradingReportSchema = z.object({
|
|
145
159
|
schemaVersion: z.literal(1),
|
|
146
160
|
casePassed: z.boolean().nullable(),
|
|
147
161
|
caseScore: z.number().nullable(),
|
|
148
162
|
assertions: z.array(GradingAssertionEntrySchema),
|
|
163
|
+
skippedAssertions: z.array(SkippedGradingAssertionSchema).optional(),
|
|
149
164
|
/**
|
|
150
165
|
* Present when the subject run or grading did not complete. Assertions may
|
|
151
166
|
* still contain partial diagnostics, but the report is not a clean verdict.
|
package/src/core/events.ts
CHANGED
|
@@ -82,6 +82,7 @@ export type ErrorEventReason = z.infer<typeof ErrorEventReasonSchema>;
|
|
|
82
82
|
export const ErrorEventSchema = z.object({
|
|
83
83
|
exitReason: ErrorEventReasonSchema,
|
|
84
84
|
reason: z.string(),
|
|
85
|
+
summary: z.unknown().optional(),
|
|
85
86
|
});
|
|
86
87
|
export type ErrorEvent = z.infer<typeof ErrorEventSchema>;
|
|
87
88
|
|
package/src/core/wire.ts
CHANGED
|
@@ -47,6 +47,7 @@ const ErrorEventSchema = z.object({
|
|
|
47
47
|
type: z.literal("error"),
|
|
48
48
|
exitReason: ErrorEventReasonSchema.default("error"),
|
|
49
49
|
reason: z.string(),
|
|
50
|
+
summary: z.unknown().optional(),
|
|
50
51
|
});
|
|
51
52
|
|
|
52
53
|
export const WireEventSchema = z.discriminatedUnion("type", [
|
|
@@ -85,7 +86,10 @@ const wireEventToParsed = (event: WireEvent): ParsedDriverEvent => {
|
|
|
85
86
|
if (event.type === "done") {
|
|
86
87
|
return { tag: "done", done: { exitReason: "done", summary: event.summary } };
|
|
87
88
|
}
|
|
88
|
-
return {
|
|
89
|
+
return {
|
|
90
|
+
tag: "error",
|
|
91
|
+
error: { exitReason: event.exitReason, reason: event.reason, summary: event.summary },
|
|
92
|
+
};
|
|
89
93
|
};
|
|
90
94
|
|
|
91
95
|
/**
|
package/src/index.ts
CHANGED
|
@@ -16,11 +16,6 @@ export {
|
|
|
16
16
|
PersonaAnswererConfigSchema,
|
|
17
17
|
personaAnswerer,
|
|
18
18
|
} from "./answerers/persona/index.js";
|
|
19
|
-
export {
|
|
20
|
-
type ScriptedAnswererConfig,
|
|
21
|
-
ScriptedAnswererConfigSchema,
|
|
22
|
-
scriptedAnswerer,
|
|
23
|
-
} from "./answerers/scripted/index.js";
|
|
24
19
|
export {
|
|
25
20
|
type TerminalAnswererConfig,
|
|
26
21
|
TerminalAnswererConfigSchema,
|
|
@@ -4,7 +4,6 @@ import { pathToFileURL } from "node:url";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { claudeCodeAnswerer } from "../answerers/claude-code/index.js";
|
|
6
6
|
import { personaAnswerer } from "../answerers/persona/index.js";
|
|
7
|
-
import { scriptedAnswerer } from "../answerers/scripted/index.js";
|
|
8
7
|
import { terminalAnswerer } from "../answerers/terminal/index.js";
|
|
9
8
|
import { allCoreAssertions } from "../assertions/core/index.js";
|
|
10
9
|
import { allMcpAssertions } from "../assertions/mcp/index.js";
|
|
@@ -182,7 +181,6 @@ const builtinDrivers: DriverPlugin<never>[] = [
|
|
|
182
181
|
const builtinAnswerers: AnswererPlugin<never>[] = [
|
|
183
182
|
personaAnswerer,
|
|
184
183
|
claudeCodeAnswerer,
|
|
185
|
-
scriptedAnswerer,
|
|
186
184
|
terminalAnswerer,
|
|
187
185
|
];
|
|
188
186
|
export const buildRegistryFromConfig = (config: ResolvedLuxConfig): Registry => {
|
|
@@ -76,6 +76,7 @@ export const graderConfigHash = (input: {
|
|
|
76
76
|
assertions: readonly AssertionEvaluatorIdentity[];
|
|
77
77
|
validatedAssertionsHash: string;
|
|
78
78
|
graderContext?: unknown;
|
|
79
|
+
skipJudge?: boolean;
|
|
79
80
|
}): string =>
|
|
80
81
|
valueHash({
|
|
81
82
|
luxVersion: input.luxVersion,
|
|
@@ -83,6 +84,7 @@ export const graderConfigHash = (input: {
|
|
|
83
84
|
assertions: input.assertions,
|
|
84
85
|
validatedAssertionsHash: input.validatedAssertionsHash,
|
|
85
86
|
graderContext: input.graderContext ?? null,
|
|
87
|
+
skipJudge: input.skipJudge ?? false,
|
|
86
88
|
});
|
|
87
89
|
|
|
88
90
|
export const validateAssertionPlan = async (
|
|
@@ -133,6 +135,7 @@ export const gradeRun = async (
|
|
|
133
135
|
complete: (attemptIds: readonly string[]) => void | Promise<void>;
|
|
134
136
|
},
|
|
135
137
|
validatedConfigs?: readonly unknown[],
|
|
138
|
+
skipJudge = false,
|
|
136
139
|
): Promise<GradingReport> => {
|
|
137
140
|
const guardedObserver = guardRunObserver(observer);
|
|
138
141
|
const assertions = run.case.assertions ?? [];
|
|
@@ -141,12 +144,27 @@ export const gradeRun = async (
|
|
|
141
144
|
}
|
|
142
145
|
|
|
143
146
|
const entries: GradingAssertionEntry[] = [];
|
|
147
|
+
const skippedAssertions: Array<{
|
|
148
|
+
id: string;
|
|
149
|
+
type: string;
|
|
150
|
+
name: string;
|
|
151
|
+
reason: "judge-disabled";
|
|
152
|
+
}> = [];
|
|
144
153
|
let weightedScoreSum = 0;
|
|
145
154
|
let weightSum = 0;
|
|
146
155
|
let allPassed = true;
|
|
147
156
|
let gradingIncomplete = false;
|
|
148
157
|
|
|
149
158
|
for (const [index, a] of assertions.entries()) {
|
|
159
|
+
if (skipJudge && a.type === "rubric") {
|
|
160
|
+
skippedAssertions.push({
|
|
161
|
+
id: a.id ?? `${a.type}:${index + 1}`,
|
|
162
|
+
type: a.type,
|
|
163
|
+
name: describeAssertion(a),
|
|
164
|
+
reason: "judge-disabled",
|
|
165
|
+
});
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
150
168
|
// Grading is the last cancellation point — an LLM judge can run for
|
|
151
169
|
// minutes per assertion. An aborted grade can't claim everything passed.
|
|
152
170
|
if (abortSignal?.aborted) {
|
|
@@ -247,12 +265,13 @@ export const gradeRun = async (
|
|
|
247
265
|
}
|
|
248
266
|
|
|
249
267
|
let caseScore: number | null = null;
|
|
250
|
-
if (!gradingIncomplete
|
|
268
|
+
if (!gradingIncomplete && weightSum > 0) caseScore = weightedScoreSum / weightSum;
|
|
251
269
|
return {
|
|
252
270
|
schemaVersion: 1,
|
|
253
|
-
casePassed: gradingIncomplete ? null : allPassed,
|
|
271
|
+
casePassed: gradingIncomplete || weightSum === 0 ? null : allPassed,
|
|
254
272
|
caseScore,
|
|
255
273
|
assertions: entries,
|
|
274
|
+
...(skippedAssertions.length > 0 ? { skippedAssertions } : {}),
|
|
256
275
|
...(gradingIncomplete
|
|
257
276
|
? { incomplete: { exitReason: "error" as const, phase: "grading" as const } }
|
|
258
277
|
: {}),
|