@hackerrank/astra-cli 0.1.30 → 0.1.31

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hackerrank/astra-cli",
3
- "version": "0.1.30",
3
+ "version": "0.1.31",
4
4
  "description": "Minimal zero-dependency AI coding agent for the HackerRank AI Gateway.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/bench.js CHANGED
@@ -51,11 +51,6 @@ export const METRIC_COLUMNS = [
51
51
  "last_context_tokens",
52
52
  "cost_usd",
53
53
  "cost_source",
54
- "verification_status",
55
- "verification_score",
56
- "solved_score",
57
- "verification_hard_pass",
58
- "failure_owner",
59
54
  ];
60
55
 
61
56
  /**
package/src/ledger.js CHANGED
@@ -90,7 +90,7 @@ export function transitionCell(ledger, cellKey, status, patch = {}) {
90
90
  const cell = ledger.cells[cellKey] || localCell;
91
91
  if (!cell) throw new Error(`unknown benchmark cell: ${cellKey}`);
92
92
  const next = { ...cell, ...patch, status, updatedAt: new Date().toISOString() };
93
- if (["generating", "verifying"].includes(status) && patch.leaseAt === undefined) next.leaseAt = next.updatedAt;
93
+ if (["generating"].includes(status) && patch.leaseAt === undefined) next.leaseAt = next.updatedAt;
94
94
  ledger.cells[cellKey] = next;
95
95
  atomicWrite(ledger.path, { version: LEDGER_VERSION, cells: ledger.cells });
96
96
  return next;
@@ -103,7 +103,7 @@ export function selectWork(ledger, desiredCells, now = Date.now(), { force = fal
103
103
  if (!existing) return true;
104
104
  if (["completed", "failed"].includes(existing.status)) return false;
105
105
  if (sessionId && existing.sessionId !== sessionId) return false;
106
- if (["generating", "verifying"].includes(existing.status)) {
106
+ if (["generating"].includes(existing.status)) {
107
107
  return force || !existing.leaseAt || now - Date.parse(existing.leaseAt) > STALE_AFTER_MS;
108
108
  }
109
109
  return !sessionId || existing.sessionId === sessionId;
@@ -5,7 +5,6 @@ import { matrixCells } from "./project.js";
5
5
  import { runCell as defaultRunCell, writeMetrics } from "./bench.js";
6
6
  import { candidateTreeHash, mergeRunResult } from "./result-contract.js";
7
7
  import { loadLedger, claimCell, selectWork, transitionCell } from "./ledger.js";
8
- import { runVerifier as defaultRunVerifier } from "./verifier-runner.js";
9
8
  import { newSessionId } from "./session.js";
10
9
  import { refreshReport } from "./report.js";
11
10
 
@@ -14,7 +13,7 @@ function writeJson(file, value) {
14
13
  fs.writeFileSync(file, JSON.stringify(value, null, 2) + "\n");
15
14
  }
16
15
 
17
- export async function runProjectBench({ project, apiKey, baseUrl, quiet = false, resume = false, resumeSessionId = null, runCellFn = defaultRunCell, verifierFn = defaultRunVerifier, onCell = () => {} } = {}) {
16
+ export async function runProjectBench({ project, apiKey, baseUrl, quiet = false, resume = false, resumeSessionId = null, runCellFn = defaultRunCell, onCell = () => {} } = {}) {
18
17
  const root = project.outputRoot;
19
18
  const ledger = loadLedger(path.join(root, "benchmark.json"));
20
19
  const desired = matrixCells({ project });
@@ -25,7 +24,7 @@ export async function runProjectBench({ project, apiKey, baseUrl, quiet = false,
25
24
  const record = claimCell(ledger, cell);
26
25
  const runDir = path.join(root, record.runPath);
27
26
  const candidateDir = path.join(runDir, "workspace");
28
- const reuseFrozen = ["candidate-frozen", "verifying"].includes(record.status);
27
+ const reuseFrozen = ["candidate-frozen"].includes(record.status);
29
28
  const isResume = Boolean(record.sessionId && ["generating", "interrupted"].includes(record.status));
30
29
  const sessionId = record.sessionId || newSessionId();
31
30
 
@@ -42,7 +41,7 @@ export async function runProjectBench({ project, apiKey, baseUrl, quiet = false,
42
41
  } catch (error) {
43
42
  const failure = { status: "error", failureOwner: "infrastructure", error: String(error?.message || error) };
44
43
  transitionCell(ledger, cell.cellKey, "failed", { failureOwner: "infrastructure", error: failure.error });
45
- results.push({ cell, generation: { status: "error" }, verification: failure });
44
+ results.push({ cell, generation: { status: "error" } });
46
45
  continue;
47
46
  }
48
47
  onCell({ phase: "candidate-frozen", cell, record });
@@ -70,7 +69,7 @@ export async function runProjectBench({ project, apiKey, baseUrl, quiet = false,
70
69
  } catch (error) {
71
70
  const failure = { status: "error", failureOwner: "infrastructure", error: String(error?.message || error) };
72
71
  transitionCell(ledger, cell.cellKey, "failed", { failureOwner: "infrastructure", error: failure.error });
73
- results.push({ cell, generation: { status: "error" }, verification: failure });
72
+ results.push({ cell, generation: { status: "error" } });
74
73
  continue;
75
74
  }
76
75
 
@@ -81,53 +80,10 @@ export async function runProjectBench({ project, apiKey, baseUrl, quiet = false,
81
80
 
82
81
  const candidateSha256 = record.candidateSha256 || candidateTreeHash(candidateDir);
83
82
 
84
- let verification = {
85
- status: "not_configured",
86
- failureOwner: null,
87
- score: null,
88
- solvedScore: "NA",
89
- reason: "task verifier is not present or not configured",
90
- };
91
- if (project.verification && generation.status === "completed") {
92
- transitionCell(ledger, cell.cellKey, "verifying", { candidateSha256 });
93
- try {
94
- verification = await verifierFn({
95
- project,
96
- cell,
97
- runDir,
98
- candidateDir,
99
- config: project.verification,
100
- gateway: { apiKey, baseUrl },
101
- expected: { taskId: project.id, taskVersion: project.version, candidateSha256 },
102
- });
103
- } catch (error) {
104
- verification = {
105
- status: "error",
106
- failureOwner: "infrastructure",
107
- score: null,
108
- solvedScore: "NA",
109
- error: String(error?.message || error),
110
- };
111
- }
112
- if (verification.score && typeof verification.score.percentage === "number") {
113
- verification.solvedScore = verification.score.percentage;
114
- }
115
- writeJson(path.join(runDir, "verifier.json"), verification);
116
- } else if (generation.status !== "completed") {
117
- verification = {
118
- status: "not_run",
119
- failureOwner: "candidate",
120
- score: null,
121
- solvedScore: "NA",
122
- reason: "generation did not submit a candidate; verification was skipped",
123
- };
124
- }
125
-
126
83
  const merged = mergeRunResult({
127
84
  task: { id: project.id, version: project.version, type: project.type || "brownfield" },
128
85
  cell,
129
86
  generation: { ...generation, candidateSha256 },
130
- verification,
131
87
  artifacts: { runPath: record.runPath, candidateSha256 },
132
88
  });
133
89
  writeMetrics({
@@ -137,15 +93,10 @@ export async function runProjectBench({ project, apiKey, baseUrl, quiet = false,
137
93
  ...generation,
138
94
  model: cell.model,
139
95
  reasoning: cell.reasoning,
140
- verification_status: verification.status,
141
- verification_score: verification.score?.percentage ?? "",
142
- solved_score: verification.solvedScore ?? "NA",
143
- verification_hard_pass: verification.score?.hardFailPassed ?? "",
144
- failure_owner: verification.failureOwner ?? "",
145
96
  },
146
97
  });
147
98
  writeJson(path.join(runDir, "result.json"), merged);
148
- transitionCell(ledger, cell.cellKey, "completed", { candidateSha256, resultPath: path.join(record.runPath, "result.json"), failureOwner: verification.failureOwner });
99
+ transitionCell(ledger, cell.cellKey, "completed", { candidateSha256, resultPath: path.join(record.runPath, "result.json") });
149
100
  results.push(merged);
150
101
  try { refreshReport(root); } catch { /* report regeneration is best-effort per cell */ }
151
102
  onCell({ phase: "completed", cell, record, result: merged });
package/src/project.js CHANGED
@@ -19,7 +19,6 @@ const DEFAULT_BENCH = {
19
19
  command_timeout_seconds: 60,
20
20
  };
21
21
 
22
- const DEFAULT_VERIFICATION = null;
23
22
  const TASK_TYPES = new Set(["brownfield", "greenfield"]);
24
23
 
25
24
  function requireObject(value, name) {
@@ -65,25 +64,6 @@ function repeatOverrides(value) {
65
64
  return result;
66
65
  }
67
66
 
68
- function verificationConfig(value) {
69
- if (value === undefined) return DEFAULT_VERIFICATION;
70
- const verification = requireObject(value, "verification");
71
- return {
72
- command: requireString(verification.command, "verification.command"),
73
- startCommand: verification.start_command === undefined ? null : requireString(verification.start_command, "verification.start_command"),
74
- stopCommand: verification.stop_command === undefined ? null : requireString(verification.stop_command, "verification.stop_command"),
75
- baseUrl: verification.base_url === undefined ? null : requireString(verification.base_url, "verification.base_url"),
76
- readinessUrl: verification.readiness_url === undefined ? null : requireString(verification.readiness_url, "verification.readiness_url"),
77
- tokenEnv: verification.token_env === undefined ? null : requireString(verification.token_env, "verification.token_env"),
78
- evaluatorModel: verification.evaluator_model === undefined ? null : requireString(verification.evaluator_model, "verification.evaluator_model"),
79
- evaluatorReasoning: verification.evaluator_reasoning === undefined ? null : requireString(verification.evaluator_reasoning, "verification.evaluator_reasoning"),
80
- // Zero means no verifier deadline. Tasks with agentic verification choose
81
- // completion over an arbitrary wall-clock cutoff.
82
- timeoutSeconds: positiveInteger(verification.timeout_seconds, "verification.timeout_seconds", 0, { allowZero: true }),
83
- report: verification.report === undefined ? "verifier-report.json" : requireString(verification.report, "verification.report"),
84
- };
85
- }
86
-
87
67
  function taskType(project) {
88
68
  const value = project.type ?? project.profile ?? "brownfield";
89
69
  if (typeof value !== "string" || !TASK_TYPES.has(value)) {
@@ -157,7 +137,6 @@ export function loadProject(projectPath) {
157
137
 
158
138
  const project = requireObject(config.project, "project");
159
139
  const bench = requireObject(config.bench ?? {}, "bench");
160
- let verification = verificationConfig(config.verification);
161
140
  const output = requireObject(config.output ?? {}, "output");
162
141
  const templates = requireObject(config.templates ?? {}, "templates");
163
142
  const extensionConfig = requireObject(config.extensions ?? {}, "extensions");
@@ -181,23 +160,6 @@ export function loadProject(projectPath) {
181
160
  .filter(Boolean)
182
161
  .join("\n\n");
183
162
 
184
- // Verifiers are task-owned. A conventional runner is enough metadata for
185
- // Astra to invoke it; all domain behavior remains under verifier/.
186
- const conventionalVerifier = path.join(root, "verifier", "run_verifier.py");
187
- if (!verification && fs.existsSync(conventionalVerifier)) {
188
- verification = {
189
- command: `python3 verifier/run_verifier.py --report "$ASTRA_VERIFIER_REPORT"`,
190
- startCommand: null,
191
- stopCommand: null,
192
- baseUrl: null,
193
- readinessUrl: null,
194
- tokenEnv: null,
195
- timeoutSeconds: 3600,
196
- report: "verifier-report.json",
197
- discovered: true,
198
- };
199
- }
200
-
201
163
  const baselineSha256 = provenance.baseline_sha256;
202
164
  if (baselineSha256 !== undefined && (typeof baselineSha256 !== "string" || !/^[a-f0-9]{64}$/.test(baselineSha256))) {
203
165
  throw new ProjectConfigError("provenance.baseline_sha256 must be a lowercase SHA-256 hash");
@@ -237,7 +199,6 @@ export function loadProject(projectPath) {
237
199
  wall: positiveInteger(bench.wall_seconds, "bench.wall_seconds", DEFAULT_BENCH.wall_seconds, { allowZero: true }),
238
200
  timeout: positiveInteger(bench.command_timeout_seconds, "bench.command_timeout_seconds", DEFAULT_BENCH.command_timeout_seconds),
239
201
  },
240
- verification,
241
202
  };
242
203
  }
243
204
 
package/src/report.html CHANGED
@@ -74,10 +74,6 @@
74
74
 
75
75
  <div class="kpis" id="kpis"></div>
76
76
 
77
- <section id="section-score-distribution">
78
- <h2>Score distribution</h2>
79
- <div class="score-chart-scroll"><div id="chart-score-distribution" role="img" aria-label="Verified score by model and reasoning configuration"></div></div>
80
- </section>
81
77
 
82
78
  <section>
83
79
  <h2>Run outcomes</h2>
@@ -92,13 +88,6 @@
92
88
  </div>
93
89
  </section>
94
90
 
95
- <section>
96
- <h2>Test-case results</h2>
97
- <div class="toolbar"><label for="verifier-model-select">Model</label><select id="verifier-model-select"></select></div>
98
- <div class="panel">
99
- <table id="tbl-verifier-matrix"></table>
100
- </div>
101
- </section>
102
91
 
103
92
  <section id="section-series">
104
93
  <h2>Trajectory</h2>
@@ -197,8 +186,7 @@
197
186
  var k = DATA.kpis || {};
198
187
  var cards = [
199
188
  ["Solutions", fmt(k.models)],
200
- ["Average score", k.average_score == null ? "n/a" : fmt1(k.average_score) + "%"],
201
- ["Average duration", fmtDuration(k.runs ? k.elapsed_seconds / k.runs : 0)],
189
+ ["Average duration", fmtDuration(k.runs ? k.elapsed_seconds / k.runs : 0)],
202
190
  ["Total cost", fmtUsd(k.cost_usd) + (k.cost_source === "estimated" ? "~" : "")],
203
191
  ["Tokens used", fmtTokens(k.tokens)],
204
192
  ];
@@ -208,37 +196,6 @@
208
196
  });
209
197
  })();
210
198
 
211
- // ---------------------------------------------------------------- score distribution
212
- (function renderScoreDistribution() {
213
- var host = document.getElementById("chart-score-distribution");
214
- if (!host) return;
215
- var rows = (DATA.leaderboard || []).map(function (row) {
216
- var verified = typeof row.average_score === "number";
217
- return { slug: row.slug, score: verified ? Math.max(0, Math.min(100, row.average_score)) : null, verified: verified };
218
- }).sort(function (a, b) {
219
- if (a.verified !== b.verified) return a.verified ? -1 : 1;
220
- return (b.score || 0) - (a.score || 0) || a.slug.localeCompare(b.slug);
221
- });
222
- if (!rows.length) {
223
- document.getElementById("section-score-distribution").style.display = "none";
224
- return;
225
- }
226
- host.className = "score-chart";
227
- host.style.minWidth = Math.max(760, rows.length * 45) + "px";
228
- [["top", "100"], ["mid", "50"], ["zero", "0"]].forEach(function (axis) {
229
- host.appendChild(el("span", { class: "score-axis " + axis[0] }, axis[1]));
230
- });
231
- rows.forEach(function (row) {
232
- var item = el("div", { class: "score-item" });
233
- var bar = el("div", { class: "score-bar", title: row.slug + ": " + (row.verified ? fmt1(row.score) + "%" : "n/a") });
234
- if (row.verified) bar.style.height = Math.max(2, row.score) + "%";
235
- bar.appendChild(el("span", { class: "score-value" }, row.verified ? fmt1(row.score) + "%" : "n/a"));
236
- item.appendChild(bar);
237
- item.appendChild(el("span", { class: "score-label" }, esc(row.slug)));
238
- host.appendChild(item);
239
- });
240
- })();
241
-
242
199
  // ---------------------------------------------------------------- sortable table helper
243
200
  function sortableTable(container, columns, rows, opts) {
244
201
  opts = opts || {};
@@ -333,7 +290,6 @@
333
290
  var columns = [
334
291
  { key: "slug", label: "Model · reasoning", render: function (r) { return esc(r.slug); } },
335
292
  { key: "runs", label: "Runs", render: function (r) { return fmt(r.runs); } },
336
- { key: "average_score", label: "Score", render: function (r) { return r.average_score == null ? '<span class="n-a">n/a</span>' : fmt1(r.average_score) + "%"; } },
337
293
  { key: "sum_steps", label: "Total steps", render: function (r) { return fmt(r.sum_steps); } },
338
294
  { key: "sum_elapsed_seconds", label: "Time taken", render: function (r) { return fmtDuration(r.sum_elapsed_seconds); } },
339
295
  { key: "sum_tokens", label: "Tokens", render: function (r) { return fmtTokens(r.sum_tokens); } },
@@ -349,70 +305,12 @@
349
305
  var selected = Array.prototype.slice.call(options.querySelectorAll("input:checked")).map(function (o) { return o.value; });
350
306
  var rows = selected.length ? (DATA.leaderboard || []).filter(function (r) { return selected.indexOf(r.slug) !== -1; }) : DATA.leaderboard || [];
351
307
  filterLabel.textContent = selected.length ? selected.length + " model" + (selected.length === 1 ? "" : "s") + " selected" : "All models";
352
- sortableTable(container, columns, rows, { defaultKey: "average_score" });
308
+ sortableTable(container, columns, rows, { defaultKey: "runs" });
353
309
  }
354
310
  options.addEventListener("change", draw);
355
311
  draw();
356
312
  })();
357
313
 
358
- // ---------------------------------------------------------------- verifier test-case matrix
359
- (function () {
360
- var container = document.getElementById("tbl-verifier-matrix");
361
- var selector = document.getElementById("verifier-model-select");
362
- if (!container) return;
363
- var matrix = DATA.verifier_matrix || { models: [], criteria: [] };
364
- // Prefer a model with actual verifier evidence. Alphabetical-first often
365
- // selects an unscored gateway failure and makes a healthy report appear
366
- // empty on first open.
367
- var selected = matrix.models.find(function (model) {
368
- return matrix.criteria.some(function (criterion) { return criterion.cells[model]; });
369
- }) || matrix.models[0] || "";
370
- var GROUP_TITLES = {
371
- "scim.discovery-auth": "Discovery & authentication",
372
- "scim.read-existing": "Reading existing resources",
373
- "scim.create-user": "User creation",
374
- "scim.user-lifecycle": "User lifecycle",
375
- "scim.group-lifecycle": "Group lifecycle",
376
- "scim.membership": "Group membership",
377
- "scim.errors-atomicity": "Errors & atomicity"
378
- };
379
- function groupTitle(parentId) {
380
- if (GROUP_TITLES[parentId]) return GROUP_TITLES[parentId];
381
- return String(parentId || "Other checks")
382
- .replace(/[._-]+/g, " ")
383
- .replace(/\b\w/g, function (letter) { return letter.toUpperCase(); });
384
- }
385
- function draw() {
386
- var groups = {};
387
- matrix.criteria.forEach(function (criterion) {
388
- var parentId = criterion.parentId || criterion.id;
389
- (groups[parentId] || (groups[parentId] = [])).push(criterion);
390
- });
391
- var bodies = Object.keys(groups).sort().map(function (parentId) {
392
- var rows = groups[parentId].map(function (criterion) {
393
- var cell = criterion.cells[selected];
394
- if (!cell) return '<tr><td>' + esc(criterion.title || criterion.id) + '</td><td class="n-a">NA</td><td><span class="result-badge unscored">Unscored</span></td></tr>';
395
- var max = cell.maxPoints || 0, earned = cell.earnedPoints || 0;
396
- var passed = cell.passed === cell.total;
397
- return '<tr><td>' + esc(criterion.title || criterion.id) + '</td><td>' + fmt1(earned) + ' / ' + fmt1(max) + '</td><td><span class="result-badge ' + (passed ? 'passed' : 'failed') + '">' + (passed ? 'Passed' : 'Failed') + '</span></td></tr>';
398
- }).join('');
399
- return '<tbody><tr class="criterion-group"><th colspan="3">' + esc(groupTitle(parentId)) + '</th></tr>' + rows + '</tbody>';
400
- }).join('');
401
- container.innerHTML = '<thead><tr><th>Test case</th><th>Points</th><th>Result</th></tr></thead>' + bodies;
402
- }
403
- matrix.models.forEach(function (model) {
404
- var option = el("option", { value: model }, esc(model));
405
- if (model === selected) option.selected = true;
406
- selector.appendChild(option);
407
- });
408
- selector.addEventListener("change", function () { selected = selector.value; draw(); });
409
- if (!matrix.criteria.length) {
410
- container.innerHTML = '<tbody><tr><td class="muted">No verifier criteria have produced results yet.</td></tr></tbody>';
411
- return;
412
- }
413
- draw();
414
- })();
415
-
416
314
  // ---------------------------------------------------------------- token mix stacked bar
417
315
  (function () {
418
316
  var host = document.getElementById("chart-tokens");
package/src/report.js CHANGED
@@ -102,9 +102,6 @@ function latestTaskRuns(runs) {
102
102
  }
103
103
 
104
104
  export function buildSummary(runs) {
105
- runs = runs.map((run) => ({ ...run, criteria: flattenCriteria(run.criteria) }));
106
- const verifierCriteria = summarizeCriteria(runs);
107
- const verifierMatrix = buildVerifierMatrix(runs);
108
105
  const leaderboard = groupBy(runs, (r) => r.slug).map(([slug, rs]) => {
109
106
  const costRuns = rs.filter((r) => r.cost_source);
110
107
  const sources = new Set(costRuns.map((r) => r.cost_source));
@@ -114,7 +111,6 @@ export function buildSummary(runs) {
114
111
  outcomes[k] = (outcomes[k] || 0) + 1;
115
112
  }
116
113
  const completed = rs.filter((r) => r.completed).length;
117
- const verified = rs.filter((r) => r.verification?.score);
118
114
  return {
119
115
  model: rs[0].model,
120
116
  reasoning: rs[0].reasoning,
@@ -122,11 +118,6 @@ export function buildSummary(runs) {
122
118
  runs: rs.length,
123
119
  completed,
124
120
  completion_rate: rs.length ? completed / rs.length : 0,
125
- verification_runs: verified.length,
126
- verification_rate: rs.length ? verified.length / rs.length : 0,
127
- average_score: verified.length ? avg(verified, (r) => r.verification.score.percentage) : null,
128
- hard_passes: verified.filter((r) => r.verification.score.hardFailPassed === true).length,
129
- criteria_pass_rate: criterionRate(rs),
130
121
  outcomes,
131
122
  avg_steps: avg(rs, (r) => r.steps),
132
123
  sum_steps: sum(rs, (r) => r.steps),
@@ -148,18 +139,14 @@ export function buildSummary(runs) {
148
139
  avg_n_retries: avg(rs, (r) => r.n_retries),
149
140
  };
150
141
  });
151
- leaderboard.sort((a, b) => {
152
- const aScored = a.average_score != null;
153
- const bScored = b.average_score != null;
154
- if (aScored !== bScored) return aScored ? -1 : 1;
155
- return (b.average_score ?? 0) - (a.average_score ?? 0)
156
- || a.slug.localeCompare(b.slug);
157
- });
142
+ leaderboard.sort((a, b) =>
143
+ (b.completion_rate ?? 0) - (a.completion_rate ?? 0)
144
+ || a.slug.localeCompare(b.slug)
145
+ );
158
146
 
159
147
  const matrix = groupBy(runs, (r) => `${r.slug}\u0000${r.task_id}`).map(([, rs]) => {
160
148
  const completed = rs.filter((r) => r.completed).length;
161
149
  const costRuns = rs.filter((r) => r.cost_source);
162
- const verified = rs.filter((r) => r.verification?.score);
163
150
  return {
164
151
  model: rs[0].model,
165
152
  reasoning: rs[0].reasoning,
@@ -169,11 +156,6 @@ export function buildSummary(runs) {
169
156
  k: rs.length,
170
157
  completed,
171
158
  completion_rate: rs.length ? completed / rs.length : 0,
172
- verification_runs: verified.length,
173
- verification_rate: rs.length ? verified.length / rs.length : 0,
174
- average_score: verified.length ? avg(verified, (r) => r.verification.score.percentage) : null,
175
- hard_passes: verified.filter((r) => r.verification.score.hardFailPassed === true).length,
176
- criteria_pass_rate: criterionRate(rs),
177
159
  avg_steps: avg(rs, (r) => r.steps),
178
160
  avg_tokens: avg(rs, (r) => r.tokens.total),
179
161
  avg_elapsed_seconds: avg(rs, (r) => r.elapsed_seconds),
@@ -233,7 +215,6 @@ export function buildSummary(runs) {
233
215
 
234
216
  const costRuns = runs.filter((r) => r.cost_source);
235
217
  const completed = runs.filter((r) => r.completed).length;
236
- const verified = runs.filter((r) => r.verification?.score);
237
218
  const sources = new Set(costRuns.map((r) => r.cost_source));
238
219
 
239
220
  return {
@@ -245,10 +226,6 @@ export function buildSummary(runs) {
245
226
  runs: runs.length,
246
227
  completed,
247
228
  completion_rate: runs.length ? completed / runs.length : 0,
248
- verification_runs: verified.length,
249
- verification_rate: runs.length ? verified.length / runs.length : 0,
250
- average_score: verified.length ? avg(verified, (r) => r.verification.score.percentage) : null,
251
- hard_passes: verified.filter((r) => r.verification.score.hardFailPassed === true).length,
252
229
  cost_usd: costRuns.length ? round(sum(costRuns, (r) => r.cost_usd), 6) : null,
253
230
  cost_source: sources.size === 0 ? "unknown" : sources.size === 1 ? [...sources][0] : "mixed",
254
231
  tokens: sum(runs, (r) => r.tokens.total),
@@ -256,8 +233,6 @@ export function buildSummary(runs) {
256
233
  },
257
234
  leaderboard,
258
235
  matrix,
259
- verifier_criteria: verifierCriteria,
260
- verifier_matrix: verifierMatrix,
261
236
  runs,
262
237
  step_series,
263
238
  };
@@ -347,12 +322,6 @@ function readRunDir({ rootDir, slug, runId, dir }) {
347
322
  status,
348
323
  completed: status === "completed",
349
324
  error: null,
350
- verification: result?.verification ?? null,
351
- solved_score: result?.verification?.solvedScore ?? (result?.verification?.score?.percentage ?? "NA"),
352
- verifier_status: result?.verification?.status ?? "not_configured",
353
- verifier_error: result?.verification?.error ?? null,
354
- failure_owner: result?.verification?.failureOwner ?? null,
355
- criteria: flattenCriteria(result?.verification?.criteria),
356
325
  steps: num(metricsRow?.steps ?? traj?.info?.n_steps),
357
326
  n_calls: num(metricsRow?.n_calls ?? traj?.info?.n_calls),
358
327
  n_commands: num(metricsRow?.n_commands),
@@ -503,90 +472,6 @@ function groupBy(arr, keyFn) {
503
472
  return [...m.entries()];
504
473
  }
505
474
 
506
- function criterionRate(runs) {
507
- const values = [];
508
- for (const run of runs) {
509
- for (const criterion of run.criteria || []) {
510
- values.push(criterion.status === "passed" ? 1 : 0);
511
- }
512
- }
513
- return values.length ? sum(values, (value) => value) / values.length : null;
514
- }
515
-
516
- // A task may group related assertions under one scored criterion. Benchmark
517
- // reports should still expose each assertion as a named test case, while score
518
- // ownership remains entirely with the task verifier.
519
- function flattenCriteria(criteria) {
520
- if (!Array.isArray(criteria)) return [];
521
- return criteria.flatMap((criterion) => {
522
- if (!Array.isArray(criterion?.checks) || criterion.checks.length === 0) {
523
- return [criterion];
524
- }
525
- const leafWeight = Math.max(0, Number(criterion.weight) || 0) / criterion.checks.length;
526
- return criterion.checks.map((check, index) => ({
527
- id: `${criterion.id}.${check.name || `check-${index + 1}`}`,
528
- title: check.description || check.name || criterion.title || criterion.id,
529
- status: check.passed === true ? "passed" : check.status === "blocked" ? "blocked" : check.status === "error" ? "error" : "failed",
530
- parentId: criterion.id,
531
- weight: leafWeight,
532
- }));
533
- });
534
- }
535
-
536
- function summarizeCriteria(runs) {
537
- const grouped = new Map();
538
- for (const run of runs) {
539
- for (const criterion of run.criteria || []) {
540
- if (!grouped.has(criterion.id)) grouped.set(criterion.id, { id: criterion.id, title: criterion.title || criterion.id, runs: 0, passed: 0, failed: 0, blocked: 0, error: 0 });
541
- const item = grouped.get(criterion.id);
542
- item.runs += 1;
543
- if (criterion.status === "passed") item.passed += 1;
544
- else if (criterion.status === "blocked") item.blocked += 1;
545
- else if (criterion.status === "error") item.error += 1;
546
- else item.failed += 1;
547
- }
548
- }
549
- return [...grouped.values()].map((item) => ({
550
- ...item,
551
- pass_rate: item.runs ? item.passed / item.runs : 0,
552
- })).sort((a, b) => a.id.localeCompare(b.id));
553
- }
554
-
555
- function buildVerifierMatrix(runs) {
556
- const models = [...new Set(runs.map((run) => run.slug))].sort();
557
- const byCriterion = new Map();
558
- for (const run of runs) {
559
- for (const criterion of run.criteria || []) {
560
- if (!byCriterion.has(criterion.id)) byCriterion.set(criterion.id, {
561
- title: criterion.title || criterion.id,
562
- parentId: criterion.parentId || criterion.id,
563
- byModel: new Map(),
564
- });
565
- const entry = byCriterion.get(criterion.id);
566
- const byModel = entry.byModel;
567
- if (!byModel.has(run.slug)) byModel.set(run.slug, { passed: 0, failed: 0, blocked: 0, error: 0, total: 0, maxPoints: 0, earnedPoints: 0 });
568
- const cell = byModel.get(run.slug);
569
- cell.total += 1;
570
- const weight = Math.max(0, Number(criterion.weight) || 0);
571
- cell.maxPoints += weight;
572
- if (criterion.status === "passed") cell.earnedPoints += weight;
573
- if (criterion.status === "passed") cell.passed += 1;
574
- else if (criterion.status === "blocked") cell.blocked += 1;
575
- else if (criterion.status === "error") cell.error += 1;
576
- else cell.failed += 1;
577
- }
578
- }
579
- return {
580
- models,
581
- criteria: [...byCriterion.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([id, entry]) => ({
582
- id,
583
- title: entry.title,
584
- parentId: entry.parentId,
585
- cells: Object.fromEntries(models.map((model) => [model, entry.byModel.get(model) || null])),
586
- })),
587
- };
588
- }
589
-
590
475
  function sum(arr, fn) {
591
476
  return arr.reduce((a, x) => a + (Number(fn(x)) || 0), 0);
592
477
  }
@@ -27,32 +27,12 @@ function finiteNumber(value) {
27
27
  return typeof value === "number" && Number.isFinite(value);
28
28
  }
29
29
 
30
- export function validateVerifierResult(value, expected = {}) {
31
- if (!value || typeof value !== "object" || Array.isArray(value)) return { ok: false, error: "verifier result must be an object" };
32
- if (value.version !== 1) return { ok: false, error: "unsupported verifier result version" };
33
- if (!["passed", "failed", "blocked", "error"].includes(value.status)) return { ok: false, error: "invalid verifier status" };
34
- if (!Array.isArray(value.criteria)) return { ok: false, error: "verifier criteria must be an array" };
35
- const score = value.score;
36
- if (!score || !finiteNumber(score.points) || !finiteNumber(score.maxPoints) || !finiteNumber(score.percentage)) {
37
- return { ok: false, error: "verifier score must contain finite numbers" };
38
- }
39
- if (score.maxPoints <= 0 || score.points < 0 || score.points > score.maxPoints || score.percentage < 0 || score.percentage > 100) {
40
- return { ok: false, error: "verifier score is outside its valid range" };
41
- }
42
- if (Math.abs(score.percentage - (score.points / score.maxPoints) * 100) > 0.01) return { ok: false, error: "verifier score arithmetic is inconsistent" };
43
- if (typeof score.hardFailPassed !== "boolean" || !Array.isArray(score.hardFailCriteria)) return { ok: false, error: "verifier hard-fail fields are invalid" };
44
- if (value.task && (value.task.id !== expected.taskId || value.task.version !== expected.taskVersion)) return { ok: false, error: "verifier task identity mismatch" };
45
- if (value.candidateSha256 && value.candidateSha256 !== expected.candidateSha256) return { ok: false, error: "verifier candidate identity mismatch" };
46
- return { ok: true, result: value };
47
- }
48
-
49
- export function mergeRunResult({ task = null, cell, generation, verification, artifacts }) {
30
+ export function mergeRunResult({ task = null, cell, generation, artifacts }) {
50
31
  return {
51
32
  schemaVersion: 1,
52
33
  task,
53
34
  cell,
54
35
  generation,
55
- verification,
56
36
  artifacts,
57
37
  };
58
38
  }
@@ -1,222 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { spawn } from "node:child_process";
4
-
5
- import { validateVerifierResult } from "./result-contract.js";
6
-
7
- function redactOutput(value) {
8
- return String(value || "")
9
- .replace(/(authorization\s*:\s*bearer\s+)[^\s,;]+/gi, "$1[REDACTED]")
10
- .replace(/(--token\s+)[^\s,;]+/gi, "$1[REDACTED]")
11
- .slice(-2000);
12
- }
13
-
14
- function runCommand(command, { cwd, env, timeoutSeconds }) {
15
- return new Promise((resolve) => {
16
- const child = spawn(command, { cwd, env, shell: true, detached: process.platform !== "win32", stdio: ["ignore", "pipe", "pipe"] });
17
- let stdout = "";
18
- let stderr = "";
19
- child.stdout.on("data", (chunk) => { stdout += chunk; });
20
- child.stderr.on("data", (chunk) => { stderr += chunk; });
21
- let timedOut = false;
22
- const limit = Number(timeoutSeconds);
23
- const timer = limit > 0 ? setTimeout(() => {
24
- timedOut = true;
25
- stopProcessTree(child, "SIGTERM");
26
- setTimeout(() => stopProcessTree(child, "SIGKILL"), 250);
27
- }, limit * 1000) : null;
28
- child.on("close", (code, signal) => {
29
- if (timer) clearTimeout(timer);
30
- resolve({ code, signal, timedOut, stdout, stderr });
31
- });
32
- child.on("error", (error) => {
33
- if (timer) clearTimeout(timer);
34
- resolve({ code: null, signal: null, timedOut: false, stdout, stderr: `${stderr}${error.message}` });
35
- });
36
- });
37
- }
38
-
39
- function stopProcessTree(child, signal) {
40
- try {
41
- if (process.platform !== "win32" && child.pid) process.kill(-child.pid, signal);
42
- else child.kill(signal);
43
- } catch {
44
- // The command may already have exited between the timer and the signal.
45
- }
46
- }
47
-
48
- function startProcess(command, { cwd, env }) {
49
- const child = spawn(command, { cwd, env, shell: true, detached: process.platform !== "win32", stdio: ["ignore", "pipe", "pipe"] });
50
- let output = "";
51
- child.stdout.on("data", (chunk) => { output += chunk; });
52
- child.stderr.on("data", (chunk) => { output += chunk; });
53
- child.startupOutput = () => redactOutput(output);
54
- return child;
55
- }
56
-
57
- async function acquireVerifierLock(projectRoot, timeoutSeconds) {
58
- const lockPath = path.join(projectRoot, ".astra", "verifier.lock");
59
- fs.mkdirSync(path.dirname(lockPath), { recursive: true });
60
- const limit = Number(timeoutSeconds);
61
- const deadline = limit > 0 ? Date.now() + limit * 1000 : Infinity;
62
- while (Date.now() < deadline) {
63
- try {
64
- const descriptor = fs.openSync(lockPath, "wx");
65
- fs.writeFileSync(descriptor, `${process.pid}\n`);
66
- return () => {
67
- try { fs.closeSync(descriptor); } catch {}
68
- try { fs.rmSync(lockPath, { force: true }); } catch {}
69
- };
70
- } catch (error) {
71
- if (error?.code !== "EEXIST") throw error;
72
- // A killed benchmark worker cannot run its finally block. Reclaim only
73
- // a lock whose recorded owner is no longer alive; active workers keep
74
- // exclusive ownership of fixed Docker ports.
75
- try {
76
- const owner = Number(fs.readFileSync(lockPath, "utf8").trim());
77
- process.kill(owner, 0);
78
- } catch (ownerError) {
79
- if (ownerError?.code === "ESRCH" || !Number.isInteger(Number(fs.readFileSync(lockPath, "utf8").trim()))) {
80
- try { fs.rmSync(lockPath, { force: true }); } catch {}
81
- continue;
82
- }
83
- }
84
- await new Promise((resolve) => setTimeout(resolve, 100));
85
- }
86
- }
87
- throw new Error("timed out waiting for the task verifier lock");
88
- }
89
-
90
- async function waitReady(url, timeoutSeconds, candidateProcess) {
91
- const limit = Number(timeoutSeconds);
92
- const deadline = limit > 0 ? Date.now() + limit * 1000 : Infinity;
93
- while (Date.now() < deadline) {
94
- if (candidateProcess && candidateProcess.exitCode !== null) {
95
- return { ready: false, exited: true, output: candidateProcess.startupOutput?.() || "" };
96
- }
97
- try {
98
- const response = await fetch(url, { signal: AbortSignal.timeout(1000) });
99
- if (response.ok) return { ready: true, exited: false, output: "" };
100
- } catch {
101
- // Candidate is still starting.
102
- }
103
- await new Promise((resolve) => setTimeout(resolve, 250));
104
- }
105
- return { ready: false, exited: false, output: candidateProcess?.startupOutput?.() || "" };
106
- }
107
-
108
- export async function runVerifier({ project, cell, runDir, candidateDir, config, expected = {}, gateway = {} }) {
109
- const started = Date.now();
110
- fs.mkdirSync(runDir, { recursive: true });
111
- const reportPath = path.resolve(runDir, config.report || "verifier.json");
112
- if (reportPath !== path.resolve(runDir) && !reportPath.startsWith(`${path.resolve(runDir)}${path.sep}`)) {
113
- return { status: "error", failureOwner: "infrastructure", solvedScore: "NA", durationSeconds: 0, error: "verifier report must stay inside the run directory" };
114
- }
115
- // Never accept a report left by an earlier interrupted attempt.
116
- try { fs.rmSync(reportPath, { force: true }); } catch (error) {
117
- return { status: "error", failureOwner: "infrastructure", solvedScore: "NA", durationSeconds: 0, error: `cannot clear previous verifier report: ${error.message}` };
118
- }
119
- const env = {
120
- PATH: process.env.PATH,
121
- HOME: process.env.HOME,
122
- ...(process.env.DOCKER_HOST ? { DOCKER_HOST: process.env.DOCKER_HOST } : {}),
123
- ASTRA_VERIFIER_REPORT: reportPath,
124
- ASTRA_CANDIDATE_WORKSPACE: candidateDir,
125
- ASTRA_TASK_ID: project.id,
126
- ASTRA_TASK_VERSION: String(project.version),
127
- ASTRA_TASK_TYPE: project.type || "brownfield",
128
- ASTRA_CELL_KEY: cell.cellKey,
129
- ASTRA_PROJECT_ROOT: project.root,
130
- // The task verifier invokes ASTRA's evaluator bridge for dynamic discovery
131
- // and source judgment. Keep this explicit and task-scoped: the candidate
132
- // command environment is separately scrubbed by the verifier.
133
- ASTRA_EVALUATOR_BIN: new URL("./evaluator-cli.js", import.meta.url).pathname,
134
- ...(gateway.apiKey ? { ASTRA_GATEWAY_API_KEY: gateway.apiKey } : {}),
135
- ...(gateway.baseUrl ? { ASTRA_GATEWAY_BASE_URL: gateway.baseUrl } : {}),
136
- ASTRA_EVALUATOR_MODEL: config.evaluatorModel || cell.model,
137
- ASTRA_EVALUATOR_REASONING: config.evaluatorReasoning || cell.reasoning || "high",
138
- ...(config.baseUrl ? { SCIM_BASE_URL: config.baseUrl } : {}),
139
- ...(config.tokenEnv && process.env[config.tokenEnv] ? { [config.tokenEnv]: process.env[config.tokenEnv] } : {}),
140
- };
141
- let candidateProcess = null;
142
- let releaseLock = null;
143
- try {
144
- // Candidate Docker Compose files commonly claim fixed host ports. Serialize
145
- // verification per task while allowing costly model generation to run in
146
- // parallel safely.
147
- releaseLock = await acquireVerifierLock(project.root, config.timeoutSeconds);
148
- if (config.startCommand) {
149
- candidateProcess = startProcess(config.startCommand, { cwd: candidateDir, env });
150
- // A cold Docker build legitimately exceeds a minute. Give the candidate
151
- // a bounded five-minute startup window, still capped by task timeout.
152
- if (config.readinessUrl) {
153
- const readiness = await waitReady(config.readinessUrl, config.timeoutSeconds > 0 ? Math.min(config.timeoutSeconds, 300) : 0, candidateProcess);
154
- if (!readiness.ready) {
155
- const durationSeconds = (Date.now() - started) / 1000;
156
- const detail = readiness.output;
157
- if (readiness.exited) {
158
- const portCollision = /EADDRINUSE|address already in use|port is already allocated/i.test(detail);
159
- return {
160
- status: "error",
161
- failureOwner: portCollision ? "infrastructure" : "candidate",
162
- solvedScore: "NA",
163
- durationSeconds,
164
- error: detail
165
- ? `${portCollision ? "candidate startup blocked by host-port collision" : "candidate startup failed"}: ${detail}`
166
- : "candidate startup process exited before readiness",
167
- };
168
- }
169
- return {
170
- status: "error",
171
- failureOwner: "infrastructure",
172
- solvedScore: "NA",
173
- durationSeconds,
174
- error: detail ? `candidate readiness timed out: ${detail}` : "candidate readiness timed out",
175
- };
176
- }
177
- }
178
- }
179
- const commandResult = await runCommand(config.command, {
180
- cwd: project.root,
181
- env,
182
- timeoutSeconds: config.timeoutSeconds,
183
- });
184
- const durationSeconds = (Date.now() - started) / 1000;
185
- if (commandResult.timedOut) return { status: "error", failureOwner: "infrastructure", solvedScore: "NA", durationSeconds, error: "verifier timed out" };
186
- if (!fs.existsSync(reportPath)) {
187
- const detail = redactOutput(commandResult.stderr);
188
- return {
189
- status: "error",
190
- failureOwner: "infrastructure",
191
- solvedScore: "NA",
192
- durationSeconds,
193
- error: detail ? `verifier report was not produced: ${detail}` : "verifier report was not produced",
194
- exitCode: commandResult.code,
195
- };
196
- }
197
- let raw;
198
- try {
199
- raw = JSON.parse(fs.readFileSync(reportPath, "utf8"));
200
- } catch (error) {
201
- return { status: "error", failureOwner: "infrastructure", solvedScore: "NA", durationSeconds, error: `invalid verifier JSON: ${error.message}`, exitCode: commandResult.code };
202
- }
203
- const validation = validateVerifierResult(raw, expected);
204
- if (!validation.ok) return { status: "error", failureOwner: "infrastructure", solvedScore: "NA", durationSeconds, error: validation.error, exitCode: commandResult.code };
205
- return {
206
- status: raw.status,
207
- failureOwner: raw.status === "failed" || raw.status === "blocked" ? "candidate" : raw.status === "passed" ? null : "infrastructure",
208
- score: raw.score,
209
- solvedScore: raw.score?.percentage ?? "NA",
210
- criteria: raw.criteria,
211
- reportPath,
212
- durationSeconds,
213
- exitCode: commandResult.code,
214
- };
215
- } finally {
216
- if (config.stopCommand) {
217
- await runCommand(config.stopCommand, { cwd: candidateDir, env, timeoutSeconds: config.timeoutSeconds > 0 ? Math.min(Number(config.timeoutSeconds), 30) : 0 });
218
- }
219
- if (candidateProcess && !candidateProcess.killed) stopProcessTree(candidateProcess, "SIGTERM");
220
- if (releaseLock) releaseLock();
221
- }
222
- }