ccqa 1.8.1 → 1.8.3

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
@@ -3248,7 +3248,7 @@ function hashTriageUserPrompt(text) {
3248
3248
  */
3249
3249
  const CHANGED_FILE_DIFF_TOOL = "mcp__diff__changed_file_diff";
3250
3250
  function buildFailureAnalysisPrompt(input) {
3251
- const { script, specYaml, failureLog, liveTranscriptExcerpt, diffPatch, changedFiles, baseRef, baseSource = null, range = null, driftIssues, artifactsDir = null, outputLanguage = "auto", triageUserPrompt, customPrompt } = input;
3251
+ const { script, specYaml, failureLog, liveTranscriptExcerpt, diffPatch, changedFiles, baseRef, baseSource = null, range = null, driftIssues, artifactsDir = null, outputLanguage = "auto", triageUserPrompt, customPrompt, baselineMissing = null } = input;
3252
3252
  const lastGreen = baseSource === "last-green";
3253
3253
  const triageUserPromptBlock = buildTriageUserPromptBlock(triageUserPrompt);
3254
3254
  const customPromptBlock = buildCustomPromptBlock(customPrompt);
@@ -3257,7 +3257,13 @@ function buildFailureAnalysisPrompt(input) {
3257
3257
  const baseLabel = lastGreen ? `this spec's last passing commit${baseRef && baseRef !== "last-green" ? ` (${baseRef})` : ""}` : baseRef ?? "base";
3258
3258
  const rangeNote = range ? ` — spans ${range.commitCount} commit${range.commitCount === 1 ? "" : "s"} over ${range.days} day${range.days === 1 ? "" : "s"}` : "";
3259
3259
  let diffBlock;
3260
- if (diffPatch === null) diffBlock = `## Source changes
3260
+ if (baselineMissing) diffBlock = `## Source changes
3261
+
3262
+ No baseline exists for this spec (${baselineMissing}), so there is no source diff. Work from the current repository state instead:
3263
+ - Grep for the exact selector / text / aria-label the failing step targets. Absent or renamed while the user-visible flow the spec describes still exists → the test is stale. The flow itself no longer implemented → the spec is stale.
3264
+ - Without a change window you cannot attribute the failure to a specific change — do not claim a change "introduced" it. State what the current source shows.
3265
+ `;
3266
+ else if (diffPatch === null) diffBlock = `## Source changes
3261
3267
 
3262
3268
  No diff context is available (the base ref could not be resolved, or there are no changes). Classify from the failure log, the spec, and what you can read in the repository — and be correspondingly more conservative: prefer UNKNOWN over a confident SPEC_CHANGE/PRODUCT_BUG call without diff evidence.
3263
3269
  `;
@@ -3284,14 +3290,14 @@ A separate read-only audit compared the spec against the current source. Treat t
3284
3290
 
3285
3291
  ${driftIssues.map((i) => `- [${i.severity}] (${DRAFT_CATEGORY_LABEL[i.category]}${i.stepId ? `, step ${i.stepId}` : ""}) ${i.message}${i.detail ? ` — ${i.detail}` : ""}`).join("\n")}
3286
3292
  ` : "";
3287
- return `You are analyzing a failing E2E regression test against the source changes since a known-good baseline. Your job is a root-cause CALL, not a fix: decide which of three categories explains the failure, using the source diff as your primary context.
3293
+ return `${baselineMissing ? `You are analyzing a failing E2E regression test. No known-good baseline exists for this spec yet, so there is no source diff: your primary context is the failure evidence plus the CURRENT state of the repository, which you can inspect with the read-only tools. Your job is a root-cause CALL, not a fix: decide which of three categories explains the failure.` : `You are analyzing a failing E2E regression test against the source changes since a known-good baseline. Your job is a root-cause CALL, not a fix: decide which of three categories explains the failure, using the source diff as your primary context.`}
3288
3294
 
3289
3295
  ${languageBlock}## The three categories
3290
3296
 
3291
3297
  The question that separates them: **is the behavior the spec describes still what the product intends?**
3292
3298
 
3293
3299
  1. TEST_DRIFT — what the spec verifies is unchanged; only the test code drifted from the source. Typical: a selector/aria-label/placeholder rename, a timing change, an over-tight assertion. The diff shows a change that is invisible to the user's intent but visible to the test.
3294
- 2. SPEC_CHANGE — the thing being verified itself changed: the UI flow, the layout, the feature's intended behavior. The diff deliberately changes what the spec asserts. You MUST cite the diff hunk (file + what changed) as evidence for this label.
3300
+ 2. SPEC_CHANGE — the thing being verified itself changed: the UI flow, the layout, the feature's intended behavior. ${baselineMissing ? "The current source deliberately implements something other than what the spec asserts. You MUST cite the source file you read as evidence for this label." : "The diff deliberately changes what the spec asserts. You MUST cite the diff hunk (file + what changed) as evidence for this label."}
3295
3301
  3. PRODUCT_BUG — neither of the above: the failure is not explained by the diff nor by test staleness. The product regressed.
3296
3302
 
3297
3303
  If the evidence is too weak to choose, answer UNKNOWN — a wrong confident call is worse than an honest UNKNOWN, because humans grade these predictions to measure accuracy.
@@ -3303,19 +3309,24 @@ You can call \`Grep\`, \`Glob\`, and \`Read\` against the current repository (po
3303
3309
  - read the changed files in full when the truncated patch is not enough,
3304
3310
  - check whether the element/flow the spec describes still exists in the source.
3305
3311
 
3306
- You can also call \`${CHANGED_FILE_DIFF_TOOL}\` with a file path to fetch that file's diff hunk for this run's base...HEAD range. The inline patch below is scoped to this spec's relatedPaths — files OUTSIDE that scope still appear in "Changed files (name-status)" but their hunks are not inlined. Before blaming (or ruling out) such a file, fetch its diff with this tool; Read only shows you its post-change state, not what changed.
3312
+ ${baselineMissing ? `There is no diff range for this run, so the \`${CHANGED_FILE_DIFF_TOOL}\` tool has nothing to return — every conclusion must come from the current source state plus the failure evidence.` : `You can also call \`${CHANGED_FILE_DIFF_TOOL}\` with a file path to fetch that file's diff hunk for this run's base...HEAD range. The inline patch below is scoped to this spec's relatedPaths — files OUTSIDE that scope still appear in "Changed files (name-status)" but their hunks are not inlined. Before blaming (or ruling out) such a file, fetch its diff with this tool; Read only shows you its post-change state, not what changed.`}
3307
3313
  ${artifactsDir ? `\nThe test runner wrote this run's artifacts under \`${artifactsDir}\` (relative to the working directory). Read them for failure context the log tail above may not carry — e.g. a Playwright \`error-context.md\` holds the page's accessibility snapshot at the moment of failure, which often shows directly whether the awaited element was present. Do NOT open image/trace binaries.\n` : ""}
3308
3314
  You have **up to 12 tool turns**. Do NOT write, edit, run shell commands, or hit the network.
3309
3315
 
3310
3316
  ## Decision guidance
3311
3317
 
3312
- ${lastGreen ? `The baseline is the commit where THIS spec last passed, so the range strictly covers the window in which it broke: the cause is either inside these changes or outside the code entirely (flaky timing, environment, an external service, test data). The range may mix several unrelated merges — most of the diff is noise; what matters is the specific change you can tie to the failing step.` : `The baseline is a fixed ref (typically the PR base): the spec is NOT guaranteed to have passed there, so the range is not guaranteed to contain the cause.`}
3318
+ ${baselineMissing ? `There is no baseline, so there is no "what changed" evidence at all. Classify from the failure signature checked against the current source:
3319
+
3320
+ - The selector / text / attribute the failing step targets is absent or renamed in the current source, while the user-visible flow the spec describes still exists → TEST_DRIFT (cite the file:line where the renamed/replacement element lives).
3321
+ - The flow or feature the spec describes is no longer implemented — page gone, component removed, copy redefined, feature reworked → SPEC_CHANGE (cite the file you read that shows the new shape).
3322
+ - The flow exists and the test's selectors still match the source, but the observed behavior is wrong (error response, missing side effect, wrong data) → lean PRODUCT_BUG; FIRST rule out environment/data/timing causes (daemon not running, network down, missing credentials, stale test data) — those read as UNKNOWN with low confidence, not PRODUCT_BUG.
3323
+ - Without diff evidence, treat 0.7 as a practical confidence ceiling unless the current source alone is conclusive (e.g. the targeted selector is verifiably gone).` : `${lastGreen ? `The baseline is the commit where THIS spec last passed, so the range strictly covers the window in which it broke: the cause is either inside these changes or outside the code entirely (flaky timing, environment, an external service, test data). The range may mix several unrelated merges — most of the diff is noise; what matters is the specific change you can tie to the failing step.` : `The baseline is a fixed ref (typically the PR base): the spec is NOT guaranteed to have passed there, so the range is not guaranteed to contain the cause.`}
3313
3324
 
3314
3325
  - Diff touches only attributes/identifiers the test selects on (labels, testids, class names, timing) while the user-visible flow is intact → TEST_DRIFT.
3315
3326
  - Diff intentionally removes/reworks the UI or flow that a spec step verifies (component deleted, page restructured, copy redefined, feature flag flipped) → SPEC_CHANGE.
3316
3327
  - Diff UNINTENTIONALLY breaks behavior the spec still intends — e.g. a refactor that drops a side effect, an inverted condition, a regression hiding inside a cleanup commit — → PRODUCT_BUG, citing the diff hunk as evidence. A product bug is often introduced BY the diff; what separates it from SPEC_CHANGE is intent: does the change read as a deliberate redesign of what the spec verifies, or as collateral damage?
3317
3328
  ${lastGreen ? `- No change in the range explains the failing step (after checking the inline patch, the name-status list, and any hunks you fetched) → the cause is outside the code: answer UNKNOWN with low confidence and name the suspected external cause (flaky timing, environment, external service, test data). Do NOT default to PRODUCT_BUG here — under this baseline a product regression must be tied to an in-range change.` : `- Diff is unrelated to the failing step (or there is no relevant diff) and the test was passing before → lean PRODUCT_BUG; first rule out timing/data flakiness and infrastructure errors (daemon not running, network down, missing credentials) — those read as UNKNOWN with low confidence, not PRODUCT_BUG.`}${range ? `
3318
- - This range spans ${range.commitCount} commit${range.commitCount === 1 ? "" : "s"} over ${range.days} day${range.days === 1 ? "" : "s"}. The wider the range, the more unrelated changes are mixed in: SPEC_CHANGE and TEST_DRIFT still require citing the specific hunk — do not infer intent from the bulk of a large diff, and lower confidence when the evidence is spread thin.` : ""}
3329
+ - This range spans ${range.commitCount} commit${range.commitCount === 1 ? "" : "s"} over ${range.days} day${range.days === 1 ? "" : "s"}. The wider the range, the more unrelated changes are mixed in: SPEC_CHANGE and TEST_DRIFT still require citing the specific hunk — do not infer intent from the bulk of a large diff, and lower confidence when the evidence is spread thin.` : ""}`}
3319
3330
  - The drift audit findings (when present) flag spec↔code mismatches; an ERROR there usually supports TEST_DRIFT or SPEC_CHANGE over PRODUCT_BUG.
3320
3331
 
3321
3332
  ## Sub-diagnosis vocabulary
@@ -3355,7 +3366,7 @@ Your **final** assistant message must start with \`{\` and end with \`}\` — a
3355
3366
  - 0.4-0.7: plausible but another category could explain it
3356
3367
  - < 0.4: answer UNKNOWN instead of guessing
3357
3368
 
3358
- Evidence rules: TEST_DRIFT and SPEC_CHANGE require at least one concrete \`file\` reference (diff hunk or file:line you actually read). PRODUCT_BUG should cite the in-range change that unintentionally broke the behavior when one exists; ${lastGreen ? "under this last-green baseline, if no in-range change explains the failure, that is UNKNOWN (external cause), not PRODUCT_BUG" : "when no such change exists, explain why the diff does NOT account for the failure"}.
3369
+ Evidence rules: TEST_DRIFT and SPEC_CHANGE require at least one concrete \`file\` reference (diff hunk or file:line you actually read). ${baselineMissing ? "With no baseline there is no in-range change to cite: PRODUCT_BUG must instead explain why current-state inspection rules out test staleness and spec change." : `PRODUCT_BUG should cite the in-range change that unintentionally broke the behavior when one exists; ${lastGreen ? "under this last-green baseline, if no in-range change explains the failure, that is UNKNOWN (external cause), not PRODUCT_BUG" : "when no such change exists, explain why the diff does NOT account for the failure"}.`}
3359
3370
 
3360
3371
  ## Test Spec (spec.yaml)
3361
3372
  ${specYaml}
@@ -5767,10 +5778,6 @@ function createFailureAnalysisPass(deps) {
5767
5778
  ...fields,
5768
5779
  analysisSkipped: ANALYSIS_DISABLED
5769
5780
  };
5770
- if (!specDiffResult.ok) return {
5771
- ...fields,
5772
- analysisSkipped: specDiffResult.skip
5773
- };
5774
5781
  if (!deps.auth.ok) return {
5775
5782
  ...fields,
5776
5783
  analysisSkipped: deps.auth.reason
@@ -5779,16 +5786,18 @@ function createFailureAnalysisPass(deps) {
5779
5786
  ...fields,
5780
5787
  analysisSkipped: "no spec.yaml found for this spec"
5781
5788
  };
5782
- info(`failure analysis: ${featureName}/${specName}`);
5789
+ const baselineMissing = specDiffResult.ok ? null : specDiffResult.skip;
5790
+ info(`failure analysis: ${featureName}/${specName}${baselineMissing ? " (no baseline — classifying from current source)" : ""}`);
5783
5791
  const outcome = await analyzeFailure({
5784
5792
  script: await input.readScript(),
5785
5793
  specYaml: input.specYaml,
5786
5794
  failureLog: input.failureLog,
5787
- diffPatch: specDiffResult.patch,
5788
- changedFiles: specDiffResult.nameStatus,
5789
- baseRef: specDiffResult.base.ref,
5790
- baseSource: specDiffResult.base.source,
5791
- range: specDiffResult.range,
5795
+ diffPatch: specDiff?.patch ?? null,
5796
+ changedFiles: specDiff?.nameStatus ?? null,
5797
+ baseRef: specDiff?.base.ref ?? null,
5798
+ baseSource: specDiff?.base.source ?? null,
5799
+ range: specDiff?.range ?? null,
5800
+ ...baselineMissing ? { baselineMissing } : {},
5792
5801
  driftIssues: input.driftIssues,
5793
5802
  ...input.artifactsDir ? { artifactsDir: input.artifactsDir } : {},
5794
5803
  ...deps.language ? { outputLanguage: deps.language } : {},
@@ -5797,7 +5806,7 @@ function createFailureAnalysisPass(deps) {
5797
5806
  }, {
5798
5807
  ...deps.model ? { model: deps.model } : {},
5799
5808
  cwd: deps.cwd,
5800
- getFileDiff: specDiffResult.fileDiff
5809
+ getFileDiff: specDiff?.fileDiff ?? (() => null)
5801
5810
  });
5802
5811
  if (!printedHeader) {
5803
5812
  printedHeader = true;
@@ -6923,6 +6932,68 @@ function verifySessionRestores(statePath, verifyUrl) {
6923
6932
  ]);
6924
6933
  }
6925
6934
  }
6935
+ /**
6936
+ * Non-destructive mid-run health probe of an already-running live session.
6937
+ * Reads the current page URL (`eval location.href` — the same read
6938
+ * {@link verifySessionRestores} makes, and it does NOT navigate, so it never
6939
+ * clobbers the page the model is working on) and detects only the unambiguous
6940
+ * signals that agent-browser's daemon was replaced/wedged mid-run:
6941
+ *
6942
+ * - the probe exits non-zero — the daemon is wedged or was replaced
6943
+ * (`spawnAB`'s hard timeout bounds a hung daemon, so this returns rather
6944
+ * than hanging the run);
6945
+ * - the page is blank/absent (`about:blank`, empty, `chrome://…`) — a
6946
+ * restarted daemon comes up with no page and no in-memory auth-state.
6947
+ *
6948
+ * Deliberately NOT flagged: a non-blank page that left the verify URL's origin.
6949
+ * That can't be told apart from a spec legitimately roaming to another origin
6950
+ * mid-flow (e.g. Slack → a separate admin app it logs into at runtime), and a
6951
+ * false "unhealthy" would re-inject the saved state and wipe auth the spec
6952
+ * acquired live — breaking a spec that was fine. Missing a same-origin-ish
6953
+ * sign-in wall just leaves that step failing as before (no regression), so the
6954
+ * asymmetry favours only firing on a provably dead daemon. `verifyUrl` is still
6955
+ * required (it's the re-anchor target for recovery) but no longer compared here.
6956
+ */
6957
+ function checkLiveSessionHealth(sessionName) {
6958
+ const probe = spawnAB([
6959
+ "--session",
6960
+ sessionName,
6961
+ "eval",
6962
+ "location.href"
6963
+ ]);
6964
+ if (probe.status !== 0) return {
6965
+ healthy: false,
6966
+ reason: (probe.stderr || probe.stdout || `probe exited ${probe.status}`).trim()
6967
+ };
6968
+ const href = unwrapEvalString(probe.stdout);
6969
+ if (!href || href === "about:blank" || href.startsWith("chrome://") || href.startsWith("chrome-error://")) return {
6970
+ healthy: false,
6971
+ reason: `blank/absent page (${href || "empty"})`
6972
+ };
6973
+ return { healthy: true };
6974
+ }
6975
+ /**
6976
+ * Recover a live session whose daemon was replaced mid-run (detected by
6977
+ * {@link checkLiveSessionHealth}). The restart drops the in-memory auth-state
6978
+ * injected at run start, so the session fell to a sign-in wall. Re-boot +
6979
+ * re-attach the saved state ({@link loadStateIntoSession} is idempotent —
6980
+ * `state load` is load-only, never writes back) and then navigate to
6981
+ * `verifyUrl`, a known signed-in page, so the retrying model has an
6982
+ * authenticated anchor to continue from instead of a login screen. Returns the
6983
+ * injection result; the trailing `open` is best-effort (a failed nav still
6984
+ * leaves the state attached for the model's own next navigation).
6985
+ */
6986
+ function recoverLiveSession(sessionName, statePath, verifyUrl) {
6987
+ const injected = loadStateIntoSession(sessionName, statePath);
6988
+ if (!injected.ok) return injected;
6989
+ spawnAB([
6990
+ "--session",
6991
+ sessionName,
6992
+ "open",
6993
+ verifyUrl
6994
+ ]);
6995
+ return { ok: true };
6996
+ }
6926
6997
  /** Take the last non-empty line of `agent-browser eval` stdout and JSON-unquote it. */
6927
6998
  function unwrapEvalString(stdout) {
6928
6999
  const lines = stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
@@ -7505,6 +7576,7 @@ async function runLiveExecutor(input) {
7505
7576
  const stepResults = [];
7506
7577
  let overallFailed = false;
7507
7578
  const statePath = input.statePath ?? null;
7579
+ const verifyUrl = input.verifyUrl ?? null;
7508
7580
  const promptPrefix = buildLiveSystemPromptPrefix({
7509
7581
  title: input.spec.title,
7510
7582
  allSteps: input.steps,
@@ -7536,12 +7608,25 @@ async function runLiveExecutor(input) {
7536
7608
  const systemPrompt = promptPrefix + buildLiveSystemPromptStepSection(step$1) + suffixBlock + langDirective;
7537
7609
  const userPrompt = buildLiveUserPrompt(step$1);
7538
7610
  let attempt = 0;
7611
+ let recoveredOnce = false;
7539
7612
  let lastOutcome = null;
7540
- while (attempt <= retries) {
7541
- if (attempt > 0) info(` retry ${attempt}/${retries} for ${step$1.id}`);
7613
+ for (;;) {
7542
7614
  lastOutcome = await executeStepAttempt(step$1, paths, systemPrompt, userPrompt);
7543
7615
  if (lastOutcome.status === "passed") break;
7616
+ if (!recoveredOnce && statePath && verifyUrl) {
7617
+ const health = checkLiveSessionHealth(input.sessionName);
7618
+ if (!health.healthy) {
7619
+ warn(`session lost mid-step for ${step$1.id} (${health.reason}); re-injecting auth-state and retrying`);
7620
+ const rec = recoverLiveSession(input.sessionName, statePath, verifyUrl);
7621
+ if (!rec.ok) warn(`session recovery failed: ${rec.error}`);
7622
+ recoveredOnce = true;
7623
+ attempt++;
7624
+ continue;
7625
+ }
7626
+ }
7627
+ if (attempt >= retries) break;
7544
7628
  attempt++;
7629
+ info(` retry ${attempt}/${retries} for ${step$1.id}`);
7545
7630
  }
7546
7631
  const outcome = lastOutcome;
7547
7632
  stepResults.push({
@@ -8092,6 +8177,7 @@ async function resolveSessionState(names, hubCtx, profile, verify = verifySessio
8092
8177
  const profileFlag = profile ? ` --profile ${profile}` : "";
8093
8178
  const loaded = [];
8094
8179
  const broken = [];
8180
+ let verifyUrl;
8095
8181
  for (const name of names) {
8096
8182
  let state;
8097
8183
  try {
@@ -8106,6 +8192,7 @@ async function resolveSessionState(names, hubCtx, profile, verify = verifySessio
8106
8192
  }
8107
8193
  const embedded = state[SESSION_VERIFY_URL_KEY];
8108
8194
  if (typeof embedded === "string") {
8195
+ verifyUrl ??= embedded;
8109
8196
  const memoKey = `${resolvedProfile}/${name}`;
8110
8197
  if (!verifiedSessions.has(memoKey)) {
8111
8198
  const tmp = await writeMergedTempState(mergeStorageStates([state]));
@@ -8130,6 +8217,7 @@ async function resolveSessionState(names, hubCtx, profile, verify = verifySessio
8130
8217
  return {
8131
8218
  ok: true,
8132
8219
  statePath,
8220
+ ...verifyUrl ? { verifyUrl } : {},
8133
8221
  cleanup: () => removeTempStateDir(statePath)
8134
8222
  };
8135
8223
  }
@@ -8158,6 +8246,7 @@ async function runOneSpec(args) {
8158
8246
  meta("session", sessionName);
8159
8247
  opts.teardown?.trackSession(sessionName);
8160
8248
  let statePath = null;
8249
+ let verifyUrl = null;
8161
8250
  let cleanupSession = null;
8162
8251
  if (spec.session && spec.session.length > 0) {
8163
8252
  const resolution = await resolveSessionState(spec.session, opts.hubContext ?? null, opts.profile);
@@ -8172,6 +8261,7 @@ async function runOneSpec(args) {
8172
8261
  };
8173
8262
  }
8174
8263
  statePath = resolution.statePath;
8264
+ verifyUrl = resolution.verifyUrl ?? null;
8175
8265
  cleanupSession = resolution.cleanup;
8176
8266
  meta("state", spec.session.join(", "));
8177
8267
  }
@@ -8187,6 +8277,7 @@ async function runOneSpec(args) {
8187
8277
  runDir,
8188
8278
  sessionName,
8189
8279
  statePath,
8280
+ verifyUrl,
8190
8281
  systemPromptSuffix: userPromptSuffix,
8191
8282
  model: opts.model,
8192
8283
  language: opts.language,
@@ -8244,26 +8335,24 @@ async function analyzeOneLiveFailure(r, diffProvider, driftForSpec, auth, opts,
8244
8335
  failureLogExcerpt: null,
8245
8336
  diffExcerpt: null
8246
8337
  };
8247
- const specDiff = await diffProvider.forSpec({
8338
+ const specDiffResult = await diffProvider.forSpec({
8248
8339
  featureName: r.featureName,
8249
8340
  specName: r.specName
8250
8341
  });
8251
- if (!specDiff.ok) return {
8252
- analysis: null,
8253
- analysisSkipped: specDiff.skip,
8254
- failureLogExcerpt: excerpt,
8255
- diffExcerpt: null
8256
- };
8257
- if (specDiff.error) info(`failure analysis: source diff unavailable (${specDiff.error}) — analyzing without diff context`);
8342
+ const specDiff = specDiffResult.ok ? specDiffResult : null;
8343
+ const baselineMissing = specDiffResult.ok ? null : specDiffResult.skip;
8344
+ if (baselineMissing) info(`failure analysis: no baseline (${baselineMissing}) — classifying from current source`);
8345
+ else if (specDiff?.error) info(`failure analysis: source diff unavailable (${specDiff.error}) — analyzing without diff context`);
8258
8346
  const customPrompt = resolveCustomPromptForTarget(opts.customPrompt, AGENT_BROWSER_TARGET);
8259
8347
  const outcome = await analyzeFailure({
8260
8348
  liveTranscriptExcerpt: excerpt,
8261
8349
  specYaml: r.specYaml,
8262
- diffPatch: specDiff.patch,
8263
- changedFiles: specDiff.nameStatus,
8264
- baseRef: specDiff.base.ref,
8265
- baseSource: specDiff.base.source,
8266
- range: specDiff.range,
8350
+ diffPatch: specDiff?.patch ?? null,
8351
+ changedFiles: specDiff?.nameStatus ?? null,
8352
+ baseRef: specDiff?.base.ref ?? null,
8353
+ baseSource: specDiff?.base.source ?? null,
8354
+ range: specDiff?.range ?? null,
8355
+ ...baselineMissing ? { baselineMissing } : {},
8267
8356
  driftIssues: driftForSpec,
8268
8357
  ...opts.language ? { outputLanguage: opts.language } : {},
8269
8358
  ...opts.triageUserPrompt ? { triageUserPrompt: opts.triageUserPrompt } : {},
@@ -8271,7 +8360,7 @@ async function analyzeOneLiveFailure(r, diffProvider, driftForSpec, auth, opts,
8271
8360
  }, {
8272
8361
  ...opts.model ? { model: opts.model } : {},
8273
8362
  cwd,
8274
- getFileDiff: specDiff.fileDiff
8363
+ getFileDiff: specDiff?.fileDiff ?? (() => null)
8275
8364
  });
8276
8365
  const pct = Math.round(outcome.analysis.confidence * 100);
8277
8366
  const headline = outcome.analysis.headline.trim() || (outcome.analysis.reasoning.split("\n")[0] ?? "").trim();
@@ -8280,11 +8369,11 @@ async function analyzeOneLiveFailure(r, diffProvider, driftForSpec, auth, opts,
8280
8369
  analysis: outcome.analysis,
8281
8370
  analysisSkipped: null,
8282
8371
  failureLogExcerpt: excerpt,
8283
- diffExcerpt: specDiff.patch,
8284
- analysisBase: {
8372
+ diffExcerpt: specDiff?.patch ?? null,
8373
+ ...specDiff ? { analysisBase: {
8285
8374
  ref: specDiff.base.ref,
8286
8375
  sha: specDiff.base.sha
8287
- },
8376
+ } } : {},
8288
8377
  ...customPrompt ? { customPromptVersion: customPrompt.customPromptVersion } : {}
8289
8378
  };
8290
8379
  }
@@ -11069,7 +11158,7 @@ function buildReportEnvelope(args) {
11069
11158
  },
11070
11159
  model: opts.model ?? null,
11071
11160
  language: opts.language ?? null,
11072
- promptVersion: "7",
11161
+ promptVersion: "8",
11073
11162
  customPromptVersion,
11074
11163
  ...triageUserPromptHash !== null ? { triageUserPromptHash } : {}
11075
11164
  };
@@ -19472,7 +19561,7 @@ function createLearningWorker(deps) {
19472
19561
  const prevCustomPrompt = await loadStoredCustomPrompt(storage, job.project);
19473
19562
  const customPrompt = {
19474
19563
  schemaVersion: 1,
19475
- basePromptVersion: "7",
19564
+ basePromptVersion: "8",
19476
19565
  customPromptVersion: `${generatedAt}-c${fallbackCases.length}`,
19477
19566
  generatedAt,
19478
19567
  guidance: fallbackGuidance ?? "",
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.8.1",
3
+ "version": "1.8.3",
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.8.1",
3
+ "version": "1.8.3",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {