ccqa 1.22.0 → 1.24.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
@@ -4556,6 +4556,7 @@ const PerspectiveSpecSchema = z.object({
4556
4556
  testCondition: z.string().optional(),
4557
4557
  preconditions: z.array(z.string().min(1)).optional(),
4558
4558
  status: PerspectiveStatusSchema,
4559
+ changedAt: z.string().optional(),
4559
4560
  note: z.string().optional()
4560
4561
  }).strip();
4561
4562
  const PerspectiveFeatureSchema = z.object({
@@ -7038,6 +7039,7 @@ const SpecRerunSchema = z.object({
7038
7039
  driftLabel: DriftLabelSchema.exclude(["UNKNOWN"]).optional(),
7039
7040
  auditAssumedReached: RerunUnknownReasonSchema.optional(),
7040
7041
  executionAssumedReached: RerunUnknownReasonSchema.optional(),
7042
+ specChangedSince: z.string().optional(),
7041
7043
  heldBy: SpecLockSchema.nullable(),
7042
7044
  lastRun: SpecLedgerEntrySchema.nullable(),
7043
7045
  lastGreen: SpecLedgerEntrySchema.nullable(),
@@ -15315,16 +15317,18 @@ const DEFAULT_CONCURRENCY$1 = 3;
15315
15317
  * `cli/run` calls this with just the failing specs after vitest.
15316
15318
  */
15317
15319
  async function analyzeDrift(input) {
15318
- const { targets, cwd, blocks, concurrency = DEFAULT_CONCURRENCY$1, model, language, guidance, onSpecStart } = input;
15320
+ const { targets, cwd, blocks, concurrency = DEFAULT_CONCURRENCY$1, model, language, guidance, onSpecStart, onSpecDone } = input;
15319
15321
  return runPool(targets, concurrency, async (target) => {
15320
15322
  onSpecStart?.(target);
15321
- return checkSpec(target, {
15323
+ const result = await checkSpec(target, {
15322
15324
  cwd,
15323
15325
  blocks,
15324
15326
  model,
15325
15327
  language,
15326
15328
  guidance
15327
15329
  });
15330
+ await onSpecDone?.(result);
15331
+ return result;
15328
15332
  });
15329
15333
  }
15330
15334
  async function checkSpec(target, opts) {
@@ -15513,37 +15517,35 @@ function specStatus(result, threshold) {
15513
15517
  return "passed";
15514
15518
  }
15515
15519
  /**
15516
- * Adapts `ccqa audit` results into the shared RunReportData shape so they can
15517
- * be pushed to the hub (`ccqa audit --report-to-hub`) and rendered by the same report
15518
- * UI as `ccqa run`/`ccqa live`. Browser-execution fields (testCounts,
15519
- * evidence, liveRun, ...) don't apply to a drift audit and are always null —
15520
- * which is why `mode` is carried separately: nothing ran, but which surfaces
15521
- * were audited is still a fact about the row.
15520
+ * One audited spec as a report row. Browser-execution fields don't apply to an
15521
+ * audit and stay empty which is why `mode` is carried separately: nothing
15522
+ * ran, but which surfaces were audited is still a fact about the row. The
15523
+ * diagnosis goes into `analysis` because for a `kind: "drift"` report the
15524
+ * diagnosis IS the row's verdict, so it renders through the same card a failed
15525
+ * `ccqa run` spec does.
15522
15526
  *
15523
- * Each result's diagnosis goes into `analysis`: for a `kind: "drift"` report
15524
- * the diagnosis IS the row's verdict, so it renders through the same diagnosis
15525
- * card a failed `ccqa run` spec does. `reasoning` has no drift-audit
15526
- * equivalent (the audit gives one headline, not a deliberation) so it is
15527
- * filled with an empty string to satisfy `FailureAnalysisSchema`.
15527
+ * Shared by the incremental push and the final report: the hub upserts by
15528
+ * feature/spec, so two mappings would let the closing patch rewrite history.
15528
15529
  */
15529
- function driftResultsToReport(results, meta) {
15530
- const specResults = results.map((result) => ({
15531
- feature: result.target.featureName,
15532
- spec: result.target.specName,
15533
- title: result.title ?? null,
15530
+ function driftResultToRow(result, threshold) {
15531
+ return {
15532
+ ...emptySpecRow({
15533
+ feature: result.target.featureName,
15534
+ spec: result.target.specName,
15535
+ title: result.title ?? null,
15536
+ status: specStatus(result, threshold)
15537
+ }),
15534
15538
  ...result.live === void 0 ? {} : { mode: result.live ? "live" : "deterministic" },
15535
- status: specStatus(result, meta.threshold),
15536
- testCounts: null,
15537
- durationMs: null,
15538
- assertions: null,
15539
- analysis: result.drift ? result.drift : null,
15540
- analysisSkipped: null,
15541
- failureLogExcerpt: null,
15542
- diffExcerpt: null,
15543
- specYaml: null,
15544
- evidence: null,
15545
- liveRun: null
15546
- }));
15539
+ analysis: result.drift ?? null
15540
+ };
15541
+ }
15542
+ /**
15543
+ * Adapts `ccqa audit` results into the shared RunReportData shape so they can
15544
+ * be pushed to the hub (`ccqa audit --report-to-hub`) and rendered by the same
15545
+ * report UI as `ccqa run`/`ccqa live`.
15546
+ */
15547
+ function driftResultsToReport(results, meta) {
15548
+ const specResults = results.map((result) => driftResultToRow(result, meta.threshold));
15547
15549
  return {
15548
15550
  schemaVersion: 1,
15549
15551
  kind: "drift",
@@ -15823,8 +15825,13 @@ async function runAudit(specPath, opts) {
15823
15825
  requireReportToHubConnection(opts);
15824
15826
  let results;
15825
15827
  let promptCtx;
15828
+ let push = null;
15826
15829
  try {
15827
15830
  promptCtx = await resolveAuditPromptContext(opts, cwd);
15831
+ if (opts.reportToHub) {
15832
+ push = await openDriftPush(opts, cwd);
15833
+ if (format === "text") info(`hub: incremental drift run opened (${push.runId})`);
15834
+ }
15828
15835
  results = await analyzeDrift({
15829
15836
  targets,
15830
15837
  cwd,
@@ -15835,16 +15842,18 @@ async function runAudit(specPath, opts) {
15835
15842
  guidance: promptCtx.guidance,
15836
15843
  onSpecStart: (t) => {
15837
15844
  if (format === "text") info(`checking ${t.featureName}/${t.specName}`);
15845
+ },
15846
+ onSpecDone: async (r) => {
15847
+ if (push) await sendDriftRow(push, r, threshold);
15838
15848
  }
15839
15849
  });
15840
15850
  } finally {
15841
15851
  if (holder) await releaseSpecs(hub, hubProject, opts.hubProfile, holder);
15842
15852
  }
15843
15853
  process.stdout.write(renderDrift(results, format, cwd));
15844
- if (opts.reportToHub) await pushDriftResults({
15854
+ if (push) await sealDriftPush(push, {
15845
15855
  results,
15846
15856
  threshold,
15847
- cwd,
15848
15857
  opts,
15849
15858
  format,
15850
15859
  baseRef,
@@ -15893,59 +15902,92 @@ function requireReportToHubConnection(opts) {
15893
15902
  process.exit(2);
15894
15903
  }
15895
15904
  /**
15896
- * Push a finished drift audit to a ccqa hub as a `kind: "drift"` run, so it
15897
- * shows up alongside `ccqa run` runs in the hub UI. A missing hub connection
15898
- * is a usage error, not a silent skip a CI job that asked to publish and
15899
- * did not must say so.
15905
+ * Open the drift run this sweep patches into. A failure here is fatal, as it
15906
+ * is for `ccqa run`: a job that asked to publish and cannot reach the hub has
15907
+ * not done what it was told, and the audit has no local artifact to fall back
15908
+ * on the hub is its only output. Raised before any spec is checked, so
15909
+ * nothing is wasted. Not retried: a dropped response after the hub committed
15910
+ * would leave a second orphan running run.
15900
15911
  *
15901
- * `resolveHub` is injectable so tests can supply a fake `HubClient` without
15902
- * a real hub connection; it defaults to the real flag/env resolution.
15912
+ * `resolveHub` is injectable for tests.
15903
15913
  */
15904
- async function pushDriftResults(args, resolveHub = resolveHubClient) {
15905
- const { results, threshold, cwd, opts, format, baseRef, promptCtx } = args;
15914
+ async function openDriftPush(opts, cwd, resolveHub = resolveHubClient) {
15906
15915
  const hub = resolveHub(opts);
15907
- if (!hub) {
15908
- error("--report-to-hub requires a hub connection (--hub-url/--hub-token or CCQA_HUB_URL/CCQA_HUB_TOKEN)");
15909
- process.exit(2);
15916
+ if (!hub) throw new RunUsageError("--report-to-hub requires a hub connection (--hub-url/--hub-token or CCQA_HUB_URL/CCQA_HUB_TOKEN)");
15917
+ const project = resolveProject({
15918
+ project: opts.project,
15919
+ cwd
15920
+ });
15921
+ const [branch, gitHead] = await Promise.all([detectBranch(cwd), getGitHead(cwd)]);
15922
+ const ciRunId = githubRunId();
15923
+ const runUrl = githubRunUrl();
15924
+ try {
15925
+ return {
15926
+ hub,
15927
+ runId: (await hub.openRun({
15928
+ project,
15929
+ kind: "drift",
15930
+ ...branch ? { branch } : {},
15931
+ ...opts.hubProfile ? { profile: opts.hubProfile } : {},
15932
+ ...gitHead ? { gitHead } : {},
15933
+ ...ciRunId ? { ciRunId } : {},
15934
+ ...runUrl ? { runUrl } : {}
15935
+ })).id,
15936
+ gitHead
15937
+ };
15938
+ } catch (err) {
15939
+ throw new RunUsageError(`--report-to-hub: could not open a run on the hub (${errMessage(err)})`);
15910
15940
  }
15941
+ }
15942
+ /**
15943
+ * Send one finished spec. A failure is warned and swallowed rather than
15944
+ * thrown: the seal resends every row, so a dropped patch costs freshness for
15945
+ * the rest of the sweep, not the record.
15946
+ */
15947
+ async function sendDriftRow(push, result, threshold) {
15911
15948
  try {
15912
- const project = resolveProject({
15913
- project: opts.project,
15914
- cwd
15949
+ await push.hub.patchRun(push.runId, {
15950
+ rows: [driftResultToRow(result, threshold)],
15951
+ reportMeta: { cost: currentReportCost() }
15915
15952
  });
15916
- const [branch, head] = await Promise.all([detectBranch(cwd), getGitHead(cwd)]);
15917
- const report = driftResultsToReport(results, {
15918
- threshold,
15919
- git: {
15920
- head,
15921
- base: baseRef ?? null
15922
- },
15923
- customPromptVersion: promptCtx?.customPromptVersion ?? null,
15924
- triageUserPromptHash: promptCtx?.triageUserPromptHash ?? null
15953
+ } catch (err) {
15954
+ warn(`hub: could not push ${result.target.featureName}/${result.target.specName}: ${errMessage(err)}`);
15955
+ }
15956
+ }
15957
+ /**
15958
+ * Close the run with every row and the envelope metadata. Resends all rows
15959
+ * rather than only the unsent ones, so this one call also repairs any row
15960
+ * whose mid-sweep patch failed. Status is left to the hub, which derives it
15961
+ * from the rows.
15962
+ */
15963
+ async function sealDriftPush(push, args) {
15964
+ const { results, threshold, opts, format, baseRef, promptCtx } = args;
15965
+ const report = driftResultsToReport(results, {
15966
+ threshold,
15967
+ git: {
15968
+ head: push.gitHead,
15969
+ base: baseRef ?? null
15970
+ },
15971
+ customPromptVersion: promptCtx?.customPromptVersion ?? null,
15972
+ triageUserPromptHash: promptCtx?.triageUserPromptHash ?? null
15973
+ });
15974
+ try {
15975
+ await push.hub.patchRun(push.runId, {
15976
+ rows: report.results,
15977
+ done: true,
15978
+ reportMeta: {
15979
+ git: report.git,
15980
+ promptVersion: report.promptVersion,
15981
+ customPromptVersion: report.customPromptVersion,
15982
+ ...report.triageUserPromptHash ? { triageUserPromptHash: report.triageUserPromptHash } : {},
15983
+ cost: report.cost
15984
+ }
15925
15985
  });
15926
- const dir = await mkdtemp(join(tmpdir(), "ccqa-drift-push-"));
15927
- try {
15928
- await writeFile(join(dir, "report.json"), JSON.stringify(report, null, 2), "utf8");
15929
- const archive = await packDirToTarGz(dir);
15930
- const run = await hub.pushRun(archive, {
15931
- project,
15932
- ...branch ? { branch } : {},
15933
- kind: "drift"
15934
- });
15935
- if (format === "text") info(`pushed drift result to hub: ${(opts.hubUrl ?? process.env.CCQA_HUB_URL ?? "").replace(/\/+$/, "")}/#/runs/${run.id}`);
15936
- } finally {
15937
- await rm(dir, {
15938
- recursive: true,
15939
- force: true
15940
- });
15941
- }
15942
15986
  } catch (err) {
15943
- if (err instanceof HubApiError) {
15944
- error(`hub request failed (${err.status} ${err.code}): ${err.message}`);
15945
- process.exit(2);
15946
- }
15947
- throw err;
15987
+ error(`hub: could not close the drift run ${push.runId}: ${errMessage(err)}`);
15988
+ process.exit(2);
15948
15989
  }
15990
+ if (format === "text") info(`pushed drift result to hub: ${(opts.hubUrl ?? process.env.CCQA_HUB_URL ?? "").replace(/\/+$/, "")}/#/runs/${push.runId}`);
15949
15991
  }
15950
15992
  /**
15951
15993
  * Adapts `AuditOptions` to `resolveHubContext`'s flat option shape. Unlike
@@ -16140,6 +16182,53 @@ For each test case above, write a 1–2 sentence factual \`summary\` of what it
16140
16182
  `;
16141
16183
  }
16142
16184
  //#endregion
16185
+ //#region src/spec/spec-changed-at.ts
16186
+ /**
16187
+ * One `git log` over the spec tree, walked newest-first. The first time a spec's
16188
+ * directory appears is that spec's last edit.
16189
+ *
16190
+ * Best-effort: outside a repository (or with no history) this returns an empty
16191
+ * map and every caller falls back to what it did before. A missing timestamp
16192
+ * must never make a spec look fresher than it is.
16193
+ */
16194
+ async function readSpecChangedAt(cwd) {
16195
+ const out = /* @__PURE__ */ new Map();
16196
+ let stdout;
16197
+ try {
16198
+ ({stdout} = await execFileP("git", [
16199
+ "log",
16200
+ "--pretty=format:%x00%cI",
16201
+ "--name-only",
16202
+ "--",
16203
+ ".ccqa/features"
16204
+ ], {
16205
+ cwd,
16206
+ maxBuffer: 64 * 1024 * 1024
16207
+ }));
16208
+ } catch {
16209
+ return out;
16210
+ }
16211
+ let when = "";
16212
+ for (const line of stdout.split("\n")) {
16213
+ if (line.startsWith("\0")) {
16214
+ when = line.slice(1).trim();
16215
+ continue;
16216
+ }
16217
+ const key = specKeyOf(line.trim());
16218
+ if (key && when && !out.has(key)) out.set(key, when);
16219
+ }
16220
+ return out;
16221
+ }
16222
+ /**
16223
+ * "feature/spec" for a path under the spec tree, or null for anything else.
16224
+ * Every file in a case's directory counts: the generated code moving is as
16225
+ * much a change to the test as the spec.yaml moving.
16226
+ */
16227
+ function specKeyOf(path) {
16228
+ const m = /^\.ccqa\/features\/([^/]+)\/test-cases\/([^/]+)\//.exec(path);
16229
+ return m ? `${m[1]}/${m[2]}` : null;
16230
+ }
16231
+ //#endregion
16143
16232
  //#region src/cli/perspectives.ts
16144
16233
  const perspectivesCommand = addHubOptions(addLanguageOption(new Command("perspectives").description("Generate/update the project's perspectives document on the hub — a factual inventory of existing test coverage (no severity, no gap analysis)").option("--instruction <text>", "Hint to steer how summaries are written").option("-y, --yes", "Apply without asking [y/N]", false).option("--verify", "Check the hub document against the local specs (mechanical fields only) and exit 1 when it is stale. No Claude calls — cheap enough for CI.", false).option("-m, --model <name>", "Claude model alias ('sonnet'|'opus'|'haiku') or full ID").option("--project <name>", "Hub project to store the document under (default: cwd directory name)"))).action(withHubErrors(async (opts) => {
16145
16234
  await withCostReporting("perspectives", () => opts.verify ? runPerspectivesCheck(opts) : runPerspectives(opts));
@@ -16290,17 +16379,20 @@ async function cleanupLegacyLocalFiles() {
16290
16379
  */
16291
16380
  async function buildSkeleton(tree) {
16292
16381
  const config = await loadProjectConfig(process.cwd()).catch(() => null);
16382
+ const changedAt = await readSpecChangedAt(process.cwd());
16293
16383
  return (await Promise.all(tree.map(async (feature) => {
16294
16384
  const specs = await Promise.all(feature.specs.filter((s) => s.hasSpecFile).map(async (s) => {
16295
16385
  const specYaml = await tryReadSpecFile(feature.featureName, s.specName);
16296
16386
  const meta = readSpecMeta(s.specName, specYaml);
16297
16387
  const plugin = resolveSpecTarget(specYaml, config);
16298
16388
  const status = await deriveStatus(feature.featureName, s.specName, meta.mode, plugin);
16389
+ const lastEdit = changedAt.get(`${feature.featureName}/${s.specName}`);
16299
16390
  return {
16300
16391
  specName: s.specName,
16301
16392
  title: meta.title,
16302
16393
  summary: "",
16303
- status
16394
+ status,
16395
+ ...lastEdit ? { changedAt: lastEdit } : {}
16304
16396
  };
16305
16397
  }));
16306
16398
  return {
@@ -17157,23 +17249,20 @@ function createPatchRunHandler(config) {
17157
17249
  };
17158
17250
  });
17159
17251
  if (evidence) for (const [relPath, b64] of Object.entries(evidence)) await config.storage.artifacts.putFile(id, relPath, Buffer.from(b64, "base64"));
17160
- const patch = done ? {
17161
- status: finalStatus ?? (specs.failed > 0 ? "failed" : "passed"),
17252
+ const patch = {
17162
17253
  specs,
17163
17254
  costUsd,
17164
17255
  ...run.kind === "drift" ? { drift: summarizeDrift(mergedResults) } : {},
17165
- ...reportMeta?.git?.head ? { gitHead: reportMeta.git.head } : {},
17166
- ...reportMeta?.promptVersion ? { promptVersion: reportMeta.promptVersion } : {},
17167
- ...await deployHeadMovedDuringRun(config.storage, run) ? { deployedShaAmbiguous: true } : {}
17168
- } : {
17169
- specs,
17170
- costUsd
17256
+ ...done ? {
17257
+ status: finalStatus ?? (specs.failed > 0 ? "failed" : "passed"),
17258
+ ...reportMeta?.git?.head ? { gitHead: reportMeta.git.head } : {},
17259
+ ...reportMeta?.promptVersion ? { promptVersion: reportMeta.promptVersion } : {},
17260
+ ...await deployHeadMovedDuringRun(config.storage, run) ? { deployedShaAmbiguous: true } : {}
17261
+ } : {}
17171
17262
  };
17172
17263
  const updated = await config.storage.runs.update(id, patch);
17173
- if (done) {
17174
- await updateSpecLedger(config.storage, updated, mergedResults);
17175
- await updateDriftLedger(config.storage, updated, mergedResults);
17176
- }
17264
+ await updateDriftLedger(config.storage, updated, mergedResults);
17265
+ if (done) await updateSpecLedger(config.storage, updated, mergedResults);
17177
17266
  sendJson(ctx.res, 200, updated);
17178
17267
  };
17179
17268
  }
@@ -17801,7 +17890,11 @@ function readSpecTargets(doc) {
17801
17890
  for (const spec of specs) {
17802
17891
  const specName = prop(spec, "specName");
17803
17892
  if (typeof specName !== "string") continue;
17804
- out.push({ key: `${featureName}/${specName}` });
17893
+ const changedAt = prop(spec, "changedAt");
17894
+ out.push({
17895
+ key: `${featureName}/${specName}`,
17896
+ ...typeof changedAt === "string" && changedAt ? { changedAt } : {}
17897
+ });
17805
17898
  }
17806
17899
  }
17807
17900
  return out;
@@ -17922,9 +18015,40 @@ function createReleaseLocksHandler(storage) {
17922
18015
  }
17923
18016
  //#endregion
17924
18017
  //#region src/hub/core/rerun.ts
18018
+ /**
18019
+ * When each deployed commit reached the environment. A baseline read at that
18020
+ * commit cannot have seen anything committed after it was deployed.
18021
+ */
18022
+ function deployedAt(log) {
18023
+ const out = /* @__PURE__ */ new Map();
18024
+ for (const entry of log.entries) out.set(entry.sha, entry.at);
18025
+ return out;
18026
+ }
18027
+ /**
18028
+ * Has the spec moved since the baseline was taken?
18029
+ *
18030
+ * A verdict is a claim about a (spec, product) pair, so either side moving
18031
+ * invalidates it. The deploy log covers the product side; this covers the
18032
+ * other one. Without it a spec repaired and merged stays `needsRepair` until
18033
+ * a deploy happens to reach it, and a run that passed against the previous
18034
+ * spec keeps answering `verified` for the new one.
18035
+ *
18036
+ * Compared against when the baseline commit was *deployed*, not when the audit
18037
+ * or run happened: the tree read at that commit predates its deployment, so an
18038
+ * edit after it is definitely not in it. Falls back to the baseline's own
18039
+ * timestamp when the log cannot place the commit.
18040
+ *
18041
+ * One-directional. A later edit time proves the baseline is stale; an earlier
18042
+ * one proves nothing, and this answers false rather than guessing.
18043
+ */
18044
+ function specMovedSince(changedAt, baselineSha, baselineAt, deployTimes) {
18045
+ if (!changedAt) return null;
18046
+ return changedAt > (baselineSha && deployTimes.get(baselineSha) || baselineAt) ? changedAt : null;
18047
+ }
17925
18048
  function computeRerun(input) {
17926
18049
  const { specs, ledger, log, touchIndex, drift, locks, now } = input;
17927
18050
  const range = buildRange(log, touchIndex);
18051
+ const deployTimes = deployedAt(log);
17928
18052
  const out = {};
17929
18053
  for (const spec of specs) {
17930
18054
  const coords = {
@@ -17932,11 +18056,17 @@ function computeRerun(input) {
17932
18056
  lastGreen: ledger.green[spec.key] ?? null,
17933
18057
  lastRed: ledger.red[spec.key] ?? null
17934
18058
  };
17935
- const audit = auditState(drift, spec.key, range);
17936
- const execution = executionState(coords, (sha) => freshness(sha, spec.key, range));
18059
+ let audit = auditState(drift, spec.key, range);
18060
+ let execution = executionState(coords, (sha) => freshness(sha, spec.key, range));
18061
+ const driftEntry = drift.specs[spec.key];
18062
+ const auditMoved = specMovedSince(spec.changedAt, driftEntry?.gitHead ?? null, driftEntry?.at ?? "", deployTimes);
18063
+ const runMoved = specMovedSince(spec.changedAt, coords.lastRun?.deployedSha ?? null, coords.lastRun?.at ?? "", deployTimes);
18064
+ if (auditMoved && audit.audit !== "due") audit = { audit: "due" };
18065
+ if (runMoved && execution.execution === "passed") execution = { execution: "stale" };
17937
18066
  const held = heldBy(locks, spec.key, now);
17938
18067
  out[spec.key] = {
17939
18068
  verdict: decide(audit.audit, execution.execution, held),
18069
+ ...auditMoved || runMoved ? { specChangedSince: auditMoved ?? runMoved } : {},
17940
18070
  ...audit,
17941
18071
  ...execution,
17942
18072
  heldBy: held,
@@ -286,6 +286,7 @@ declare const RerunReportSchema: z.ZodObject<{
286
286
  deployedShaNotInLog: "deployedShaNotInLog";
287
287
  gapInRange: "gapInRange";
288
288
  }>>;
289
+ specChangedSince: z.ZodOptional<z.ZodString>;
289
290
  heldBy: z.ZodNullable<z.ZodObject<{
290
291
  kind: z.ZodEnum<{
291
292
  run: "run";
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.22.0",
3
+ "version": "1.24.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.22.0",
3
+ "version": "1.24.0",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {