ccqa 1.50.0 → 1.51.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.
package/dist/bin/ccqa.mjs CHANGED
@@ -8603,11 +8603,11 @@ async function buildLiveTranscriptExcerpt(result, options = {}) {
8603
8603
  return combined.length > maxBytes ? `${combined.slice(0, maxBytes)}\n…[transcript excerpt truncated at ${maxBytes} bytes]` : combined;
8604
8604
  }
8605
8605
  function formatPreviousStep(step) {
8606
- const reason = oneLine$2(step.reasoning) || "(no reason given)";
8606
+ const reason = oneLine$3(step.reasoning) || "(no reason given)";
8607
8607
  return `[${step.stepId} ${step.status}: ${reason}]`;
8608
8608
  }
8609
8609
  async function formatFailingStep(step, headBytes, tailBytes) {
8610
- const header = `\n>>> Failed step ${step.stepId}\nInstruction: ${oneLine$2(step.instruction)}\nExpected: ${oneLine$2(step.expected)}\nReasoning (Claude's verdict): ${oneLine$2(step.reasoning) || "(none)"}`;
8610
+ const header = `\n>>> Failed step ${step.stepId}\nInstruction: ${oneLine$3(step.instruction)}\nExpected: ${oneLine$3(step.expected)}\nReasoning (Claude's verdict): ${oneLine$3(step.reasoning) || "(none)"}`;
8611
8611
  if (!step.logTxt) return `${header}\n(No assistant log file recorded for this step.)`;
8612
8612
  const raw = await readFile(step.logTxt, "utf-8").catch((err) => {
8613
8613
  return `[log file unreadable: ${err instanceof Error ? err.message : String(err)}]`;
@@ -8617,7 +8617,7 @@ async function formatFailingStep(step, headBytes, tailBytes) {
8617
8617
  const tail = raw.slice(raw.length - tailBytes);
8618
8618
  return `${header}\n--- assistant log (head ${headBytes}B) ---\n${head}\n…[${raw.length - headBytes - tailBytes} bytes omitted]…\n--- assistant log (tail ${tailBytes}B) ---\n${tail}`;
8619
8619
  }
8620
- function oneLine$2(s) {
8620
+ function oneLine$3(s) {
8621
8621
  return s.replace(/\s+/g, " ").trim();
8622
8622
  }
8623
8623
  //#endregion
@@ -10129,10 +10129,10 @@ function describeStep(step) {
10129
10129
  return describeStepBody(step);
10130
10130
  }
10131
10131
  function describeStepBody(step) {
10132
- if (isJudgeBody(step)) return `judge: ${oneLine$1(step.judgeByLlm)}`;
10133
- return `${oneLine$1(step.instruction)} → ${oneLine$1(step.expected)}`;
10132
+ if (isJudgeBody(step)) return `judge: ${oneLine$2(step.judgeByLlm)}`;
10133
+ return `${oneLine$2(step.instruction)} → ${oneLine$2(step.expected)}`;
10134
10134
  }
10135
- function oneLine$1(text) {
10135
+ function oneLine$2(text) {
10136
10136
  return text.trim().replace(/\s+/g, " ");
10137
10137
  }
10138
10138
  function emptyDeployLog() {
@@ -12432,15 +12432,15 @@ function renderRunMarkdown(featureName, specName, result) {
12432
12432
  ].join("\n") + result.steps.map((s) => [
12433
12433
  `## ${s.stepId} — ${s.status}`,
12434
12434
  `- duration: ${(s.durationMs / 1e3).toFixed(1)}s`,
12435
- `- instruction: ${oneLine(s.instruction)}`,
12436
- `- expected: ${oneLine(s.expected)}`,
12437
- `- reasoning: ${oneLine(s.reasoning)}`,
12435
+ `- instruction: ${oneLine$1(s.instruction)}`,
12436
+ `- expected: ${oneLine$1(s.expected)}`,
12437
+ `- reasoning: ${oneLine$1(s.reasoning)}`,
12438
12438
  ...s.beforePng ? [`- before: ${s.beforePng}`] : [],
12439
12439
  ...s.afterPng ? [`- after: ${s.afterPng}`] : [],
12440
12440
  ""
12441
12441
  ].join("\n")).join("\n");
12442
12442
  }
12443
- function oneLine(s) {
12443
+ function oneLine$1(s) {
12444
12444
  return s.replace(/\s+/g, " ").trim();
12445
12445
  }
12446
12446
  //#endregion
@@ -14004,6 +14004,136 @@ async function waitForCdp(port) {
14004
14004
  }
14005
14005
  }
14006
14006
  //#endregion
14007
+ //#region src/prompts/verifies-spec.ts
14008
+ /**
14009
+ * Asks whether a generated test actually decides what its spec claims.
14010
+ *
14011
+ * The generation loop's only bar is "the test goes green", and a rewrite that
14012
+ * weakens an assertion clears that bar as easily as one that keeps it. Green
14013
+ * therefore does not mean checked, and nothing else looks. Observed cases: a
14014
+ * step whose expectation was "the linked page opens" asserting instead that
14015
+ * the *link* is still visible on the page it clicked from; another asserting
14016
+ * on a navigation element unrelated to the step.
14017
+ *
14018
+ * Deliberately narrow. It reads only what the step says and what the code
14019
+ * does, and reports the step as unchecked when the two do not line up. It
14020
+ * does not review style, coverage, or whether the expectation is a good one.
14021
+ */
14022
+ function verifiesSpecPrompt(input) {
14023
+ return [
14024
+ "You are reviewing whether a generated end-to-end test decides what its spec says.",
14025
+ "",
14026
+ "For each step below, the test must contain assertions that could FAIL if the",
14027
+ "step's `expected` stopped holding. Report a step when:",
14028
+ "",
14029
+ "- its assertions cannot fail while the product is broken in the way the",
14030
+ " expectation describes (e.g. the expectation says a page opens, and the",
14031
+ " code only re-checks the element it clicked);",
14032
+ "- what it asserts on is unrelated to what the step did (e.g. a navigation",
14033
+ " element that is present on every page);",
14034
+ "- it depends on something that varies between runs and is not part of the",
14035
+ " expectation (a count, an index, a position, wording that changes);",
14036
+ "- it has no assertion at all.",
14037
+ "",
14038
+ "Do NOT report: style, naming, structure, missing coverage the spec never",
14039
+ "asked for, or an expectation you merely disagree with. A step that checks",
14040
+ "less than you would have written, but still fails when the expectation",
14041
+ "breaks, is fine.",
14042
+ "",
14043
+ "## Steps",
14044
+ "",
14045
+ ...input.steps.map(stepLine),
14046
+ "",
14047
+ "## Generated test",
14048
+ "",
14049
+ "```",
14050
+ input.source,
14051
+ "```",
14052
+ "",
14053
+ "Answer with one json block and nothing else:",
14054
+ "",
14055
+ "```json",
14056
+ "{ \"findings\": [ { \"stepId\": \"step-05\", \"problem\": \"…\" } ] }",
14057
+ "```",
14058
+ "",
14059
+ "`problem` is one sentence naming what the step claims and what the code",
14060
+ "checks instead. Empty `findings` means every step is decided.",
14061
+ languageDirective(input.language)
14062
+ ].join("\n");
14063
+ }
14064
+ function stepLine(step) {
14065
+ if (!isExpandedActionStep(step)) return `- ${step.id}: judged by a model at run time — its claim is asserted by the injected call, so it needs no other assertion.`;
14066
+ return [
14067
+ `- ${step.id}`,
14068
+ ` does: ${oneLine(step.instruction)}`,
14069
+ ` expected: ${oneLine(step.expected)}`
14070
+ ].join("\n");
14071
+ }
14072
+ function oneLine(text) {
14073
+ return text.trim().split("\n").map((l) => l.trim()).join(" ");
14074
+ }
14075
+ //#endregion
14076
+ //#region src/targets/verifies-spec.ts
14077
+ const FindingsSchema = z.object({ findings: z.array(z.object({
14078
+ stepId: z.string(),
14079
+ problem: z.string()
14080
+ })) });
14081
+ /**
14082
+ * Findings in the model's answer, or null when it did not answer in the
14083
+ * agreed shape. Null is not "no findings": the caller says so rather than
14084
+ * reporting a clean review it never got.
14085
+ */
14086
+ function parseVerifiesSpecFindings(answer) {
14087
+ const json = extractJsonBlock(answer);
14088
+ if (!json) return null;
14089
+ try {
14090
+ return FindingsSchema.parse(JSON.parse(json)).findings;
14091
+ } catch {
14092
+ return null;
14093
+ }
14094
+ }
14095
+ /** The warning a finding becomes, phrased so the reader knows the test is green for nothing. */
14096
+ function formatFinding(finding) {
14097
+ return `step ${finding.stepId}: the generated test passes without deciding what this step claims — ${finding.problem}`;
14098
+ }
14099
+ /**
14100
+ * Read the generated test back and ask whether each step is actually decided
14101
+ * (see `verifiesSpecPrompt`). Returns warnings; an empty list means either a
14102
+ * clean review or one that could not be obtained, and the difference is
14103
+ * logged rather than encoded — a review that failed must not read as a pass,
14104
+ * but it must also not fail the generate that produced working files.
14105
+ */
14106
+ async function reviewGeneratedTest(input) {
14107
+ const source = (await Promise.all(input.result.files.filter((f) => f.kind === "test").map((f) => readFile(f.path, "utf8").catch(() => "")))).filter((s) => s.length > 0).join("\n\n");
14108
+ if (source.length === 0) {
14109
+ warn("could not check whether the generated test decides its spec (no test file to read)");
14110
+ return [];
14111
+ }
14112
+ const { result: answer, isError } = await (input.invoke ?? invokeClaudeStreaming)({
14113
+ prompt: verifiesSpecPrompt({
14114
+ steps: input.steps,
14115
+ source,
14116
+ language: input.language
14117
+ }),
14118
+ allowedTools: [],
14119
+ disableThinking: true,
14120
+ maxTurns: 1,
14121
+ silenceBashLog: true,
14122
+ ...input.model ? { model: input.model } : {},
14123
+ cwd: input.cwd
14124
+ }, () => {});
14125
+ if (isError) {
14126
+ warn("could not check whether the generated test decides its spec (Claude returned an error)");
14127
+ return [];
14128
+ }
14129
+ const findings = parseVerifiesSpecFindings(answer);
14130
+ if (findings === null) {
14131
+ warn("could not check whether the generated test decides its spec (no usable answer)");
14132
+ return [];
14133
+ }
14134
+ return findings.map(formatFinding);
14135
+ }
14136
+ //#endregion
14007
14137
  //#region src/targets/playwright/index.ts
14008
14138
  const PLAYWRIGHT_TARGET = "playwright";
14009
14139
  /**
@@ -14075,12 +14205,21 @@ async function generatePlaywrightTest(ctx) {
14075
14205
  });
14076
14206
  const missing = await missingInjectedCalls(result, stepMarkers, judgements);
14077
14207
  for (const w of missing) warn(w);
14208
+ const unchecked = await reviewGeneratedTest({
14209
+ result,
14210
+ steps: expanded,
14211
+ language: ctx.language,
14212
+ ...ctx.model ? { model: ctx.model } : {},
14213
+ cwd: ctx.cwd
14214
+ });
14215
+ for (const w of unchecked) warn(w);
14078
14216
  return {
14079
14217
  ...result,
14080
14218
  warnings: [
14081
14219
  ...result.warnings,
14082
14220
  ...judgeWarnings,
14083
- ...missing
14221
+ ...missing,
14222
+ ...unchecked
14084
14223
  ]
14085
14224
  };
14086
14225
  }
@@ -14525,32 +14664,42 @@ function requireReportToHubConnection(conn) {
14525
14664
  process.exit(2);
14526
14665
  }
14527
14666
  /**
14528
- * Open the run a `--report-to-hub` command patches into. Failure is fatal: a
14529
- * job that asked to publish and cannot reach the hub has not done what it was
14530
- * told. Thrown rather than exited, so a caller's `finally` still runs (the
14531
- * audit releases its spec claims there). Not retried: a dropped response after
14532
- * the hub committed would leave a second orphan running run.
14667
+ * Open the run a `--report-to-hub` command patches into, and name it on stderr
14668
+ * stdout belongs to the report (`--report-format json`), and a caller that
14669
+ * links to the run needs its id before the command ends (docs/hub.md).
14670
+ *
14671
+ * Failure is fatal: a job that asked to publish and cannot reach the hub has
14672
+ * not done what it was told. Thrown rather than exited, so a caller's
14673
+ * `finally` still runs (the audit releases its spec claims there). Not
14674
+ * retried: a dropped response after the hub committed would leave a second
14675
+ * orphan running run.
14533
14676
  */
14534
- async function openHubRun(kind, conn, cwd, profile) {
14535
- const [branch, gitHead] = await Promise.all([detectBranch(cwd), getGitHead(cwd)]);
14677
+ async function openHubRun(kind, conn, cwd, opts = {}) {
14678
+ const [branch, gitHead] = await Promise.all([detectBranch(cwd), opts.gitHead !== void 0 ? opts.gitHead : getGitHead(cwd)]);
14679
+ let opened;
14536
14680
  try {
14537
- const run = await conn.hub.openRun({
14681
+ opened = await conn.hub.openRun({
14538
14682
  project: conn.project,
14539
14683
  kind,
14540
14684
  ...branch ? { branch } : {},
14541
- ...profile ? { profile } : {},
14685
+ ...opts.profile ? { profile: opts.profile } : {},
14542
14686
  ...gitHead ? { gitHead } : {},
14687
+ ...opts.deployedSha ? { deployedSha: opts.deployedSha } : {},
14543
14688
  ...ciProvenance()
14544
14689
  });
14545
- return {
14546
- hub: conn.hub,
14547
- kind,
14548
- runId: run.id,
14549
- gitHead
14550
- };
14551
14690
  } catch (err) {
14552
14691
  throw new RunUsageError(`--report-to-hub: could not open a run on the hub (${errMessage(err)})`);
14553
14692
  }
14693
+ process.stderr.write(`[hub-run] ${JSON.stringify({
14694
+ id: opened.id,
14695
+ kind
14696
+ })}\n`);
14697
+ return {
14698
+ hub: conn.hub,
14699
+ kind,
14700
+ runId: opened.id,
14701
+ gitHead
14702
+ };
14554
14703
  }
14555
14704
  /**
14556
14705
  * Close an open run with its final rows and envelope, answering whether it
@@ -15238,7 +15387,7 @@ async function executeRun(targets, opts) {
15238
15387
  const declared = [...new Set(specs.flatMap(resources))];
15239
15388
  if (declared.length > 0) meta("serial groups", declared.join(", "));
15240
15389
  let waitingOnGroup = [];
15241
- if (hubCtx && rerunProfile !== null) {
15390
+ if (hubCtx && rerunProfile !== null && forExecution) {
15242
15391
  const held = await holdSpecs(hubCtx, rerunProfile, specs, resources, opts.teardown);
15243
15392
  waitingOnGroup = specs.flatMap((spec) => {
15244
15393
  const groups = resources(spec).filter((n) => held.deniedResources.includes(n));
@@ -15298,25 +15447,17 @@ async function executeRun(targets, opts) {
15298
15447
  reportDir: null
15299
15448
  };
15300
15449
  }
15301
- const det = await runDeterministicSpecs(detSpecs, opts, cwd, reportDir, resources);
15302
15450
  let hubRunId = null;
15303
15451
  let hubSink;
15304
15452
  let hubPublishBroken = false;
15305
- if (hubCtx != null && opts.reportToHub) try {
15306
- const branch = await detectBranch(cwd);
15307
- const opened = await hubCtx.hub.openRun({
15308
- project: hubCtx.project,
15309
- ...branch ? { branch } : {},
15453
+ if (hubCtx != null && opts.reportToHub) {
15454
+ const { runId } = await openHubRun("run", hubCtx, cwd, {
15310
15455
  ...opts.hubProfile ? { profile: opts.hubProfile } : {},
15311
- ...git.head ? { gitHead: git.head } : {},
15312
- ...deployedSha ? { deployedSha } : {},
15313
- ...ciProvenance(),
15314
- kind: "run"
15456
+ gitHead: git.head,
15457
+ deployedSha
15315
15458
  });
15316
- hubRunId = opened.id;
15317
- info(`hub: incremental run opened (${opened.id})`);
15318
- await coverage?.linkHubRun(opened.id);
15319
- const runId = opened.id;
15459
+ hubRunId = runId;
15460
+ await coverage?.linkHubRun(runId);
15320
15461
  let hubPatchEverSucceeded = false;
15321
15462
  hubSink = { onUpsert: async (row) => {
15322
15463
  try {
@@ -15334,8 +15475,6 @@ async function executeRun(targets, opts) {
15334
15475
  } else warn(`hub: incremental push failed for ${row.feature}/${row.spec}: ${errMessage(err)}`);
15335
15476
  }
15336
15477
  } };
15337
- } catch (err) {
15338
- throw new RunUsageError(`--report-to-hub: could not open a run on the hub (${errMessage(err)})`);
15339
15478
  }
15340
15479
  const incrementalReport = createIncrementalReport(reportDir, buildReportEnvelope({
15341
15480
  git,
@@ -15360,6 +15499,7 @@ async function executeRun(targets, opts) {
15360
15499
  warn(`hub: could not finalize interrupted run ${hubRunId}: ${errMessage(err)}`);
15361
15500
  }
15362
15501
  });
15502
+ const det = await runDeterministicSpecs(detSpecs, opts, cwd, reportDir, resources);
15363
15503
  const externalRows = await runExternalSpecs(dispatch, {
15364
15504
  cwd,
15365
15505
  reportDir,
@@ -17847,8 +17987,7 @@ async function runRecord(specPath, opts) {
17847
17987
  }
17848
17988
  throw e;
17849
17989
  });
17850
- const push = pushConn ? await openHubRun("record", pushConn, cwdForProfile, opts.hubProfile) : null;
17851
- if (push) info(`hub: record run opened (${push.runId})`);
17990
+ const push = pushConn ? await openHubRun("record", pushConn, cwdForProfile, { ...opts.hubProfile ? { profile: opts.hubProfile } : {} }) : null;
17852
17991
  let recorded = false;
17853
17992
  let sealed = true;
17854
17993
  let tracingStep;
@@ -18926,10 +19065,7 @@ async function runAudit(specPath, opts) {
18926
19065
  let push = null;
18927
19066
  try {
18928
19067
  promptCtx = await resolveAuditPromptContext(opts, cwd);
18929
- if (pushConn) {
18930
- push = await openHubRun("drift", pushConn, cwd, opts.hubProfile);
18931
- if (format === "text") info(`hub: incremental drift run opened (${push.runId})`);
18932
- }
19068
+ if (pushConn) push = await openHubRun("drift", pushConn, cwd, { ...opts.hubProfile ? { profile: opts.hubProfile } : {} });
18933
19069
  results = await analyzeDrift({
18934
19070
  targets,
18935
19071
  cwd,
@@ -20474,7 +20610,7 @@ function createPatchRunHandler(config) {
20474
20610
  sendJson(ctx.res, 200, updated);
20475
20611
  };
20476
20612
  }
20477
- /** GET /api/v1/runs?project&branch&status&kind&since&until&limit */
20613
+ /** GET /api/v1/runs?project&branch&status&kind&ciRunId&since&until&limit */
20478
20614
  function createListRunsHandler(storage) {
20479
20615
  return async (ctx) => {
20480
20616
  const project = ctx.url.searchParams.get("project");
@@ -20482,11 +20618,13 @@ function createListRunsHandler(storage) {
20482
20618
  const status = ctx.url.searchParams.get("status");
20483
20619
  const limitRaw = ctx.url.searchParams.get("limit");
20484
20620
  const kindsRaw = ctx.url.searchParams.get("kind");
20621
+ const ciRunId = boundedParam(ctx.url.searchParams.get("ciRunId"), "ciRunId", 128);
20485
20622
  const runs = await storage.runs.list({
20486
20623
  ...project ? { project } : {},
20487
20624
  ...branch ? { branch } : {},
20488
20625
  ...status ? { status } : {},
20489
20626
  ...kindsRaw ? { kinds: kindsRaw.split(",").map(requireKind) } : {},
20627
+ ...ciRunId ? { ciRunId } : {},
20490
20628
  ...requireWindowParams(ctx.url),
20491
20629
  ...limitRaw ? { limit: Number(limitRaw) } : {}
20492
20630
  });
@@ -24678,12 +24816,15 @@ const CLIENT_JS = `
24678
24816
  // Disconnected: the full-screen login gate is the only thing to show.
24679
24817
  if (!state.token) { showAuthGate(false); return; }
24680
24818
  showAuthGate(true);
24819
+ // A run is addressed by its own id, not through a project, so a link from
24820
+ // outside (a chat message, a CI job) opens it even in a browser that has
24821
+ // never picked one — ahead of the gate below, which would drop the link.
24822
+ var m = parseRunHash(location.hash);
24823
+ if (m) { openRunDetail(m.runId, m.caseKey); return; }
24681
24824
  // With no project chosen yet, the Projects picker is the only useful view —
24682
24825
  // land there (e.g. right after login) instead of an empty Runs list, and
24683
- // gate any deep-linked #/runs or #/secrets to it too.
24826
+ // gate any deep-linked project-scoped view to it too.
24684
24827
  if (location.hash === "#/projects" || !state.project) { openProjects(); return; }
24685
- var m = parseRunHash(location.hash);
24686
- if (m) { openRunDetail(m.runId, m.caseKey); return; }
24687
24828
  if (location.hash === "#/perspectives") { openPerspectives(); return; }
24688
24829
  if (location.hash === "#/coverage") { openCoverage(); return; }
24689
24830
  if (location.hash === "#/secrets") { openSecrets(); return; }
@@ -26267,6 +26408,11 @@ const CLIENT_JS = `
26267
26408
  var runGone = false;
26268
26409
 
26269
26410
  apiFetch("/api/v1/runs/" + encodeURIComponent(runId)).then(function (run) {
26411
+ // Arrived by an outside link: the selected project is whatever the last
26412
+ // visit or the boot auto-select left, which need not be this run's.
26413
+ // Follow the run, so the nav around it answers for the project the page
26414
+ // is actually showing.
26415
+ if (run.project && run.project !== state.project) adoptProject(run.project);
26270
26416
  renderRunHead(run);
26271
26417
  }).catch(function (err) {
26272
26418
  runGone = err.status === 404;
@@ -28344,14 +28490,20 @@ const CLIENT_JS = `
28344
28490
  updateNavGate();
28345
28491
  }
28346
28492
 
28347
- // Scope to a project and land on its Runs view. Shared by the top menu, the
28348
- // Projects grid, and the "new project" flow. Switching project restores the
28349
- // profile last chosen for that project (or "default" if none was saved);
28350
- // the Secrets tab reloads the profile list when opened.
28351
- function chooseProject(p) {
28493
+ // Scope the UI to a project, restoring the profile last chosen for it (or
28494
+ // "default" if none was saved); the Secrets tab reloads the profile list when
28495
+ // opened. Separate from the navigation below because arriving on a run by
28496
+ // link adopts a project without going anywhere.
28497
+ function adoptProject(p) {
28352
28498
  setProject(p);
28353
28499
  storeProject(p);
28354
28500
  setProfile(storedProfileForProject(p) || "default");
28501
+ }
28502
+
28503
+ // Scope to a project and land on its Runs view. Shared by the top menu, the
28504
+ // Projects grid, and the "new project" flow.
28505
+ function chooseProject(p) {
28506
+ adoptProject(p);
28355
28507
  location.hash = "#/runs";
28356
28508
  route();
28357
28509
  }
@@ -30049,7 +30201,7 @@ function createFileRunStore(root) {
30049
30201
  async delete(id) {
30050
30202
  await serialize(runMetaPath(root, id), () => removePath(runDir(root, id)));
30051
30203
  },
30052
- async list({ project, branch, status, kinds, since, until, limit }) {
30204
+ async list({ project, branch, status, kinds, ciRunId, since, until, limit }) {
30053
30205
  const ids = await listSubdirsOrEmpty(runsDir(root));
30054
30206
  const inWindow = windowFilter({
30055
30207
  since,
@@ -30063,6 +30215,7 @@ function createFileRunStore(root) {
30063
30215
  if (branch !== void 0 && run.branch !== branch) continue;
30064
30216
  if (status !== void 0 && run.status !== status) continue;
30065
30217
  if (kinds !== void 0 && !kinds.includes(run.kind)) continue;
30218
+ if (ciRunId !== void 0 && run.ciRunId !== ciRunId) continue;
30066
30219
  if (!inWindow(run.createdAt)) continue;
30067
30220
  runs.push(run);
30068
30221
  }
@@ -1147,9 +1147,10 @@ interface HubClient {
1147
1147
  * `gitHead` stamps the run's commit at open time, so even a run that dies
1148
1148
  * before its final reconcile patch is attributable to a commit.
1149
1149
  *
1150
- * `deployedSha` does the same for the environment's commit. The hub would
1151
- * otherwise read its own deploy-log head here, and the open happens after
1152
- * the deterministic phase, not before the first spec.
1150
+ * `deployedSha` does the same for the environment's commit: the baseline the
1151
+ * caller selected against, which a `--rerun` inherits from an earlier run.
1152
+ * Left unset the hub reads its own deploy-log head, which by the time this
1153
+ * call lands can already name a later deploy.
1153
1154
  */
1154
1155
  openRun(meta: {
1155
1156
  project: string;
@@ -1165,11 +1166,13 @@ interface HubClient {
1165
1166
  }): Promise<Run>;
1166
1167
  /** Add finished spec rows (+ evidence) to a running run; `done` closes it. */
1167
1168
  patchRun(id: string, body: PatchRunRequest): Promise<Run>;
1169
+ /** `ciRunId` answers "which runs did this CI job create", whatever the window. */
1168
1170
  listRuns(q?: {
1169
1171
  project?: string;
1170
1172
  branch?: string;
1171
1173
  status?: RunStatus;
1172
1174
  kind?: Run["kind"];
1175
+ ciRunId?: string;
1173
1176
  limit?: number;
1174
1177
  }): Promise<Run[]>;
1175
1178
  getRun(id: string): Promise<Run>;
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.50.0",
3
+ "version": "1.51.0",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.50.0",
3
+ "version": "1.51.0",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {