ccqa 1.21.0 → 1.23.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
@@ -6842,10 +6842,26 @@ const SpecLedgerEntrySchema = z.object({
6842
6842
  deployedSha: z.string().nullable().optional(),
6843
6843
  deployedShaAmbiguous: z.boolean().optional()
6844
6844
  });
6845
+ /**
6846
+ * A red-bucket entry: where the failure happened, plus what it was. The cause
6847
+ * is copied off the run report's `analysis` so a reader learns why a spec is
6848
+ * red without fetching the report of every red spec.
6849
+ *
6850
+ * Only the red bucket carries it. A pass has no cause, so putting these on
6851
+ * `SpecLedgerEntry` would invite writing them where they cannot exist.
6852
+ *
6853
+ * Both fields are optional and mean the same thing by their absence — nothing
6854
+ * is on record. Failure analysis is opt-in (`--on-fail-explain`), and entries
6855
+ * written before this release have neither field.
6856
+ */
6857
+ const SpecRedLedgerEntrySchema = SpecLedgerEntrySchema.extend({
6858
+ label: PredictedLabelSchema.optional(),
6859
+ headline: z.string().optional()
6860
+ });
6845
6861
  z.object({
6846
6862
  green: z.record(z.string(), SpecLedgerEntrySchema).default({}),
6847
6863
  run: z.record(z.string(), SpecLedgerEntrySchema).default({}),
6848
- red: z.record(z.string(), SpecLedgerEntrySchema).default({})
6864
+ red: z.record(z.string(), SpecRedLedgerEntrySchema).default({})
6849
6865
  });
6850
6866
  /** One deploy, as the consumer's deploy job reported it (ADR-0010). */
6851
6867
  const DeployEntrySchema = z.object({
@@ -7025,7 +7041,7 @@ const SpecRerunSchema = z.object({
7025
7041
  heldBy: SpecLockSchema.nullable(),
7026
7042
  lastRun: SpecLedgerEntrySchema.nullable(),
7027
7043
  lastGreen: SpecLedgerEntrySchema.nullable(),
7028
- lastRed: SpecLedgerEntrySchema.nullable(),
7044
+ lastRed: SpecRedLedgerEntrySchema.nullable(),
7029
7045
  touchedBy: z.array(z.string()).optional(),
7030
7046
  touchedByDeploy: DeployRefSchema.nullable().optional()
7031
7047
  });
@@ -7075,7 +7091,7 @@ z.object({
7075
7091
  z.object({
7076
7092
  entries: z.record(z.string(), SpecLedgerEntrySchema),
7077
7093
  lastRun: z.record(z.string(), SpecLedgerEntrySchema).default({}),
7078
- lastRed: z.record(z.string(), SpecLedgerEntrySchema).default({})
7094
+ lastRed: z.record(z.string(), SpecRedLedgerEntrySchema).default({})
7079
7095
  });
7080
7096
  /**
7081
7097
  * One spec's last drift audit, as recorded by `ccqa audit --report-to-hub`. Unlike the
@@ -15299,16 +15315,18 @@ const DEFAULT_CONCURRENCY$1 = 3;
15299
15315
  * `cli/run` calls this with just the failing specs after vitest.
15300
15316
  */
15301
15317
  async function analyzeDrift(input) {
15302
- const { targets, cwd, blocks, concurrency = DEFAULT_CONCURRENCY$1, model, language, guidance, onSpecStart } = input;
15318
+ const { targets, cwd, blocks, concurrency = DEFAULT_CONCURRENCY$1, model, language, guidance, onSpecStart, onSpecDone } = input;
15303
15319
  return runPool(targets, concurrency, async (target) => {
15304
15320
  onSpecStart?.(target);
15305
- return checkSpec(target, {
15321
+ const result = await checkSpec(target, {
15306
15322
  cwd,
15307
15323
  blocks,
15308
15324
  model,
15309
15325
  language,
15310
15326
  guidance
15311
15327
  });
15328
+ await onSpecDone?.(result);
15329
+ return result;
15312
15330
  });
15313
15331
  }
15314
15332
  async function checkSpec(target, opts) {
@@ -15497,37 +15515,35 @@ function specStatus(result, threshold) {
15497
15515
  return "passed";
15498
15516
  }
15499
15517
  /**
15500
- * Adapts `ccqa audit` results into the shared RunReportData shape so they can
15501
- * be pushed to the hub (`ccqa audit --report-to-hub`) and rendered by the same report
15502
- * UI as `ccqa run`/`ccqa live`. Browser-execution fields (testCounts,
15503
- * evidence, liveRun, ...) don't apply to a drift audit and are always null —
15504
- * which is why `mode` is carried separately: nothing ran, but which surfaces
15505
- * were audited is still a fact about the row.
15518
+ * One audited spec as a report row. Browser-execution fields don't apply to an
15519
+ * audit and stay empty which is why `mode` is carried separately: nothing
15520
+ * ran, but which surfaces were audited is still a fact about the row. The
15521
+ * diagnosis goes into `analysis` because for a `kind: "drift"` report the
15522
+ * diagnosis IS the row's verdict, so it renders through the same card a failed
15523
+ * `ccqa run` spec does.
15506
15524
  *
15507
- * Each result's diagnosis goes into `analysis`: for a `kind: "drift"` report
15508
- * the diagnosis IS the row's verdict, so it renders through the same diagnosis
15509
- * card a failed `ccqa run` spec does. `reasoning` has no drift-audit
15510
- * equivalent (the audit gives one headline, not a deliberation) so it is
15511
- * filled with an empty string to satisfy `FailureAnalysisSchema`.
15525
+ * Shared by the incremental push and the final report: the hub upserts by
15526
+ * feature/spec, so two mappings would let the closing patch rewrite history.
15512
15527
  */
15513
- function driftResultsToReport(results, meta) {
15514
- const specResults = results.map((result) => ({
15515
- feature: result.target.featureName,
15516
- spec: result.target.specName,
15517
- title: result.title ?? null,
15528
+ function driftResultToRow(result, threshold) {
15529
+ return {
15530
+ ...emptySpecRow({
15531
+ feature: result.target.featureName,
15532
+ spec: result.target.specName,
15533
+ title: result.title ?? null,
15534
+ status: specStatus(result, threshold)
15535
+ }),
15518
15536
  ...result.live === void 0 ? {} : { mode: result.live ? "live" : "deterministic" },
15519
- status: specStatus(result, meta.threshold),
15520
- testCounts: null,
15521
- durationMs: null,
15522
- assertions: null,
15523
- analysis: result.drift ? result.drift : null,
15524
- analysisSkipped: null,
15525
- failureLogExcerpt: null,
15526
- diffExcerpt: null,
15527
- specYaml: null,
15528
- evidence: null,
15529
- liveRun: null
15530
- }));
15537
+ analysis: result.drift ?? null
15538
+ };
15539
+ }
15540
+ /**
15541
+ * Adapts `ccqa audit` results into the shared RunReportData shape so they can
15542
+ * be pushed to the hub (`ccqa audit --report-to-hub`) and rendered by the same
15543
+ * report UI as `ccqa run`/`ccqa live`.
15544
+ */
15545
+ function driftResultsToReport(results, meta) {
15546
+ const specResults = results.map((result) => driftResultToRow(result, meta.threshold));
15531
15547
  return {
15532
15548
  schemaVersion: 1,
15533
15549
  kind: "drift",
@@ -15807,8 +15823,13 @@ async function runAudit(specPath, opts) {
15807
15823
  requireReportToHubConnection(opts);
15808
15824
  let results;
15809
15825
  let promptCtx;
15826
+ let push = null;
15810
15827
  try {
15811
15828
  promptCtx = await resolveAuditPromptContext(opts, cwd);
15829
+ if (opts.reportToHub) {
15830
+ push = await openDriftPush(opts, cwd);
15831
+ if (format === "text") info(`hub: incremental drift run opened (${push.runId})`);
15832
+ }
15812
15833
  results = await analyzeDrift({
15813
15834
  targets,
15814
15835
  cwd,
@@ -15819,16 +15840,18 @@ async function runAudit(specPath, opts) {
15819
15840
  guidance: promptCtx.guidance,
15820
15841
  onSpecStart: (t) => {
15821
15842
  if (format === "text") info(`checking ${t.featureName}/${t.specName}`);
15843
+ },
15844
+ onSpecDone: async (r) => {
15845
+ if (push) await sendDriftRow(push, r, threshold);
15822
15846
  }
15823
15847
  });
15824
15848
  } finally {
15825
15849
  if (holder) await releaseSpecs(hub, hubProject, opts.hubProfile, holder);
15826
15850
  }
15827
15851
  process.stdout.write(renderDrift(results, format, cwd));
15828
- if (opts.reportToHub) await pushDriftResults({
15852
+ if (push) await sealDriftPush(push, {
15829
15853
  results,
15830
15854
  threshold,
15831
- cwd,
15832
15855
  opts,
15833
15856
  format,
15834
15857
  baseRef,
@@ -15877,59 +15900,92 @@ function requireReportToHubConnection(opts) {
15877
15900
  process.exit(2);
15878
15901
  }
15879
15902
  /**
15880
- * Push a finished drift audit to a ccqa hub as a `kind: "drift"` run, so it
15881
- * shows up alongside `ccqa run` runs in the hub UI. A missing hub connection
15882
- * is a usage error, not a silent skip a CI job that asked to publish and
15883
- * did not must say so.
15903
+ * Open the drift run this sweep patches into. A failure here is fatal, as it
15904
+ * is for `ccqa run`: a job that asked to publish and cannot reach the hub has
15905
+ * not done what it was told, and the audit has no local artifact to fall back
15906
+ * on the hub is its only output. Raised before any spec is checked, so
15907
+ * nothing is wasted. Not retried: a dropped response after the hub committed
15908
+ * would leave a second orphan running run.
15884
15909
  *
15885
- * `resolveHub` is injectable so tests can supply a fake `HubClient` without
15886
- * a real hub connection; it defaults to the real flag/env resolution.
15910
+ * `resolveHub` is injectable for tests.
15887
15911
  */
15888
- async function pushDriftResults(args, resolveHub = resolveHubClient) {
15889
- const { results, threshold, cwd, opts, format, baseRef, promptCtx } = args;
15912
+ async function openDriftPush(opts, cwd, resolveHub = resolveHubClient) {
15890
15913
  const hub = resolveHub(opts);
15891
- if (!hub) {
15892
- error("--report-to-hub requires a hub connection (--hub-url/--hub-token or CCQA_HUB_URL/CCQA_HUB_TOKEN)");
15893
- process.exit(2);
15914
+ if (!hub) throw new RunUsageError("--report-to-hub requires a hub connection (--hub-url/--hub-token or CCQA_HUB_URL/CCQA_HUB_TOKEN)");
15915
+ const project = resolveProject({
15916
+ project: opts.project,
15917
+ cwd
15918
+ });
15919
+ const [branch, gitHead] = await Promise.all([detectBranch(cwd), getGitHead(cwd)]);
15920
+ const ciRunId = githubRunId();
15921
+ const runUrl = githubRunUrl();
15922
+ try {
15923
+ return {
15924
+ hub,
15925
+ runId: (await hub.openRun({
15926
+ project,
15927
+ kind: "drift",
15928
+ ...branch ? { branch } : {},
15929
+ ...opts.hubProfile ? { profile: opts.hubProfile } : {},
15930
+ ...gitHead ? { gitHead } : {},
15931
+ ...ciRunId ? { ciRunId } : {},
15932
+ ...runUrl ? { runUrl } : {}
15933
+ })).id,
15934
+ gitHead
15935
+ };
15936
+ } catch (err) {
15937
+ throw new RunUsageError(`--report-to-hub: could not open a run on the hub (${errMessage(err)})`);
15894
15938
  }
15939
+ }
15940
+ /**
15941
+ * Send one finished spec. A failure is warned and swallowed rather than
15942
+ * thrown: the seal resends every row, so a dropped patch costs freshness for
15943
+ * the rest of the sweep, not the record.
15944
+ */
15945
+ async function sendDriftRow(push, result, threshold) {
15895
15946
  try {
15896
- const project = resolveProject({
15897
- project: opts.project,
15898
- cwd
15947
+ await push.hub.patchRun(push.runId, {
15948
+ rows: [driftResultToRow(result, threshold)],
15949
+ reportMeta: { cost: currentReportCost() }
15899
15950
  });
15900
- const [branch, head] = await Promise.all([detectBranch(cwd), getGitHead(cwd)]);
15901
- const report = driftResultsToReport(results, {
15902
- threshold,
15903
- git: {
15904
- head,
15905
- base: baseRef ?? null
15906
- },
15907
- customPromptVersion: promptCtx?.customPromptVersion ?? null,
15908
- triageUserPromptHash: promptCtx?.triageUserPromptHash ?? null
15951
+ } catch (err) {
15952
+ warn(`hub: could not push ${result.target.featureName}/${result.target.specName}: ${errMessage(err)}`);
15953
+ }
15954
+ }
15955
+ /**
15956
+ * Close the run with every row and the envelope metadata. Resends all rows
15957
+ * rather than only the unsent ones, so this one call also repairs any row
15958
+ * whose mid-sweep patch failed. Status is left to the hub, which derives it
15959
+ * from the rows.
15960
+ */
15961
+ async function sealDriftPush(push, args) {
15962
+ const { results, threshold, opts, format, baseRef, promptCtx } = args;
15963
+ const report = driftResultsToReport(results, {
15964
+ threshold,
15965
+ git: {
15966
+ head: push.gitHead,
15967
+ base: baseRef ?? null
15968
+ },
15969
+ customPromptVersion: promptCtx?.customPromptVersion ?? null,
15970
+ triageUserPromptHash: promptCtx?.triageUserPromptHash ?? null
15971
+ });
15972
+ try {
15973
+ await push.hub.patchRun(push.runId, {
15974
+ rows: report.results,
15975
+ done: true,
15976
+ reportMeta: {
15977
+ git: report.git,
15978
+ promptVersion: report.promptVersion,
15979
+ customPromptVersion: report.customPromptVersion,
15980
+ ...report.triageUserPromptHash ? { triageUserPromptHash: report.triageUserPromptHash } : {},
15981
+ cost: report.cost
15982
+ }
15909
15983
  });
15910
- const dir = await mkdtemp(join(tmpdir(), "ccqa-drift-push-"));
15911
- try {
15912
- await writeFile(join(dir, "report.json"), JSON.stringify(report, null, 2), "utf8");
15913
- const archive = await packDirToTarGz(dir);
15914
- const run = await hub.pushRun(archive, {
15915
- project,
15916
- ...branch ? { branch } : {},
15917
- kind: "drift"
15918
- });
15919
- if (format === "text") info(`pushed drift result to hub: ${(opts.hubUrl ?? process.env.CCQA_HUB_URL ?? "").replace(/\/+$/, "")}/#/runs/${run.id}`);
15920
- } finally {
15921
- await rm(dir, {
15922
- recursive: true,
15923
- force: true
15924
- });
15925
- }
15926
15984
  } catch (err) {
15927
- if (err instanceof HubApiError) {
15928
- error(`hub request failed (${err.status} ${err.code}): ${err.message}`);
15929
- process.exit(2);
15930
- }
15931
- throw err;
15985
+ error(`hub: could not close the drift run ${push.runId}: ${errMessage(err)}`);
15986
+ process.exit(2);
15932
15987
  }
15988
+ if (format === "text") info(`pushed drift result to hub: ${(opts.hubUrl ?? process.env.CCQA_HUB_URL ?? "").replace(/\/+$/, "")}/#/runs/${push.runId}`);
15933
15989
  }
15934
15990
  /**
15935
15991
  * Adapts `AuditOptions` to `resolveHubContext`'s flat option shape. Unlike
@@ -16755,12 +16811,17 @@ function toLedger(raw) {
16755
16811
  }
16756
16812
  /** Fold `from` into `into` in place, per bucket and key, newest `at` winning. */
16757
16813
  function mergeLedgerInto(into, from) {
16758
- for (const name of BUCKET_NAMES) for (const [key, entry] of Object.entries(from[name])) {
16759
- const prev = into[name][key];
16760
- if (!prev || prev.at <= entry.at) into[name][key] = entry;
16761
- }
16814
+ mergeBucket(into.green, from.green);
16815
+ mergeBucket(into.run, from.run);
16816
+ mergeBucket(into.red, from.red);
16762
16817
  return into;
16763
16818
  }
16819
+ function mergeBucket(into, from) {
16820
+ for (const [key, entry] of Object.entries(from)) {
16821
+ const prev = into[key];
16822
+ if (!prev || prev.at <= entry.at) into[key] = entry;
16823
+ }
16824
+ }
16764
16825
  //#endregion
16765
16826
  //#region src/hub/api/validate.ts
16766
16827
  /**
@@ -16971,7 +17032,7 @@ async function updateSpecLedger(storage, run, results) {
16971
17032
  const key = `${row.feature}/${row.spec}`;
16972
17033
  ledger.run[key] = entry;
16973
17034
  if (row.status === "passed") ledger.green[key] = entry;
16974
- else ledger.red[key] = entry;
17035
+ else ledger.red[key] = redEntry(entry, row);
16975
17036
  }
16976
17037
  if (Object.keys(ledger.run).length === 0) return;
16977
17038
  try {
@@ -16981,6 +17042,23 @@ async function updateSpecLedger(storage, run, results) {
16981
17042
  }
16982
17043
  }
16983
17044
  /**
17045
+ * The red bucket's entry: the coordinate every bucket carries, plus what the
17046
+ * failure analysis concluded, so a reader of the ledger learns why a spec is
17047
+ * red without fetching its report.
17048
+ *
17049
+ * A field with nothing behind it is left out rather than written empty:
17050
+ * `analysis` is null when the run did not ask for one (`--on-fail-explain` is
17051
+ * opt-in), and a model that answered with no headline leaves `headline` "".
17052
+ */
17053
+ function redEntry(entry, row) {
17054
+ if (!row.analysis) return entry;
17055
+ return {
17056
+ ...entry,
17057
+ label: row.analysis.label,
17058
+ ...row.analysis.headline ? { headline: row.analysis.headline } : {}
17059
+ };
17060
+ }
17061
+ /**
16984
17062
  * Advance the drift ledger from a terminal `kind: "drift"` run: each row's
16985
17063
  * `analysis` (the diagnosis `driftResultsToReport` put there) becomes that
16986
17064
  * spec's newest audit entry. No profile — drift asks whether the spec still
@@ -17119,23 +17197,20 @@ function createPatchRunHandler(config) {
17119
17197
  };
17120
17198
  });
17121
17199
  if (evidence) for (const [relPath, b64] of Object.entries(evidence)) await config.storage.artifacts.putFile(id, relPath, Buffer.from(b64, "base64"));
17122
- const patch = done ? {
17123
- status: finalStatus ?? (specs.failed > 0 ? "failed" : "passed"),
17200
+ const patch = {
17124
17201
  specs,
17125
17202
  costUsd,
17126
17203
  ...run.kind === "drift" ? { drift: summarizeDrift(mergedResults) } : {},
17127
- ...reportMeta?.git?.head ? { gitHead: reportMeta.git.head } : {},
17128
- ...reportMeta?.promptVersion ? { promptVersion: reportMeta.promptVersion } : {},
17129
- ...await deployHeadMovedDuringRun(config.storage, run) ? { deployedShaAmbiguous: true } : {}
17130
- } : {
17131
- specs,
17132
- costUsd
17204
+ ...done ? {
17205
+ status: finalStatus ?? (specs.failed > 0 ? "failed" : "passed"),
17206
+ ...reportMeta?.git?.head ? { gitHead: reportMeta.git.head } : {},
17207
+ ...reportMeta?.promptVersion ? { promptVersion: reportMeta.promptVersion } : {},
17208
+ ...await deployHeadMovedDuringRun(config.storage, run) ? { deployedShaAmbiguous: true } : {}
17209
+ } : {}
17133
17210
  };
17134
17211
  const updated = await config.storage.runs.update(id, patch);
17135
- if (done) {
17136
- await updateSpecLedger(config.storage, updated, mergedResults);
17137
- await updateDriftLedger(config.storage, updated, mergedResults);
17138
- }
17212
+ await updateDriftLedger(config.storage, updated, mergedResults);
17213
+ if (done) await updateSpecLedger(config.storage, updated, mergedResults);
17139
17214
  sendJson(ctx.res, 200, updated);
17140
17215
  };
17141
17216
  }
@@ -22205,6 +22280,17 @@ const CLIENT_JS = `
22205
22280
  badge.appendChild(document.createTextNode(" " + t("perspectives.run.state." + runState)));
22206
22281
  td.appendChild(badge);
22207
22282
 
22283
+ // What the failure was, as the run's analysis called it — the same place
22284
+ // the audit column names its drift label. Absent on a red entry the run
22285
+ // never analyzed, and on entries written before the ledger carried it.
22286
+ if (runState === "failed" && rr.lastRed && rr.lastRed.label) {
22287
+ var cause = el("span", "cellsub", labelText(rr.lastRed.label));
22288
+ // The one-line conclusion, for a pointer. The detail panel shows it in
22289
+ // full, so nothing here depends on finding it.
22290
+ if (rr.lastRed.headline) cause.title = rr.lastRed.headline;
22291
+ td.appendChild(cause);
22292
+ }
22293
+
22208
22294
  // The sub-line is the coordinate of the result being reported — the same
22209
22295
  // "when is this from" evidence the audit column carries above, so the two
22210
22296
  // axes read as siblings. Why the currency matters belongs to the verdict
@@ -22423,6 +22509,21 @@ const CLIENT_JS = `
22423
22509
  return wrap;
22424
22510
  }
22425
22511
 
22512
+ // The failure: which run it was, then what the analysis concluded. The
22513
+ // headline is model output, already localized server-side, so it is shown as
22514
+ // written. A run made without failure analysis carries neither field, and the
22515
+ // row is then the coordinate alone — what it has always been.
22516
+ function rerunFailureValue(entry) {
22517
+ var wrap = el("div");
22518
+ wrap.appendChild(ledgerLine(entry));
22519
+ if (entry.label) {
22520
+ var line = el("div", "d-prose", labelText(entry.label));
22521
+ if (entry.headline) line.appendChild(document.createTextNode(" · " + entry.headline));
22522
+ wrap.appendChild(line);
22523
+ }
22524
+ return wrap;
22525
+ }
22526
+
22426
22527
  // Detail row: a definition list of the case's fields plus the note editor.
22427
22528
  // Built with createElement/textContent throughout — every field here is
22428
22529
  // API-derived, so none of it may go through innerHTML.
@@ -22456,7 +22557,7 @@ const CLIENT_JS = `
22456
22557
  var rr = ledgerEntryFor(perspState.rerun, feature, spec);
22457
22558
  if (rr) {
22458
22559
  row(rerunEvidenceLabelKey(rr), rerunEvidenceValue(rr));
22459
- if (rerunHasFailure(rr)) row("perspectives.d.lastRed", ledgerLine(rr.lastRed));
22560
+ if (rerunHasFailure(rr)) row("perspectives.d.lastRed", rerunFailureValue(rr.lastRed));
22460
22561
  }
22461
22562
  frag.appendChild(dl);
22462
22563
 
@@ -314,6 +314,14 @@ declare const RerunReportSchema: z.ZodObject<{
314
314
  at: z.ZodString;
315
315
  deployedSha: z.ZodOptional<z.ZodNullable<z.ZodString>>;
316
316
  deployedShaAmbiguous: z.ZodOptional<z.ZodBoolean>;
317
+ label: z.ZodOptional<z.ZodEnum<{
318
+ TEST_DRIFT: "TEST_DRIFT";
319
+ SPEC_CHANGE: "SPEC_CHANGE";
320
+ PRODUCT_BUG: "PRODUCT_BUG";
321
+ ENVIRONMENT: "ENVIRONMENT";
322
+ UNKNOWN: "UNKNOWN";
323
+ }>>;
324
+ headline: z.ZodOptional<z.ZodString>;
317
325
  }, z.core.$strip>>;
318
326
  touchedBy: z.ZodOptional<z.ZodArray<z.ZodString>>;
319
327
  touchedByDeploy: z.ZodOptional<z.ZodNullable<z.ZodObject<{
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.21.0",
3
+ "version": "1.23.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.21.0",
3
+ "version": "1.23.0",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {