ccqa 1.31.0 → 1.31.1

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
@@ -15523,7 +15523,7 @@ const VALIDATION_MODES = ["lenient", "strict"];
15523
15523
  const recordCommand = addHubOptions(addProfileOption(addLanguageOption(new Command("record").argument("<feature/spec>", "Spec id in '<feature>/<spec>' form (resolves to .ccqa/features/<feature>/test-cases/<spec>/)").description("Record a test from a spec: run agent-browser to collect actions (trace), then compile them into runnable code via the spec's target (generate) — a vitest test.spec.ts for agent-browser, a @playwright/test spec for the playwright target. Recording-backed targets only; spec-input targets like runn have no trace step (use `ccqa generate`), and agent-browser live specs need no recording.").optionsGroup("How to record:").option("-m, --model <name>", "Claude model alias ('sonnet'|'opus'|'haiku') or full ID. Overrides CCQA_MODEL.").option("--instruction <text>", "Extra guidance for the recording agent — e.g. the drift audit's finding when re-recording a drifted spec.").option("--trace-validation <mode>", "What to do with actions that fail post-trace validation: 'lenient' (default) tags them; 'strict' drops them.", (raw) => {
15524
15524
  if (VALIDATION_MODES.includes(raw)) return raw;
15525
15525
  throw new Error(`--trace-validation must be one of ${VALIDATION_MODES.join(" | ")}`);
15526
- }, "lenient").option("--auto-fix <mode>", "Auto-fix behaviour during script generation: 'interactive' (default, prompt y/N; declines on non-TTY), 'auto' (apply without prompt, for CI), 'skip' (agent-browser: apply only high-confidence fixes; external targets like playwright/runn: no fix pass at all).", parseAutoFixFlag, "interactive").option("--auto-fix-max-retries <n>", "Maximum number of auto-fix retries", "3").option("--trace-only", "Stop after the trace step; do not generate test code").option("--no-session-pin", "Don't pin AGENT_BROWSER_SESSION / capture page snapshots after a failure (debug toggle)").optionsGroup("What to do with the result:").option("--overwrite", "Replace an existing test.spec.ts without warning").option("--report-to-hub", "Leave a run (kind: record) on the hub saying this spec was recorded and what the recording spent on Claude, so a budget summed over the hub's runs sees it. It advances no ledger: a recording verifies nothing.").optionsGroup("Learning:").option("--learn-hub-trace-prompt", "After the trace finishes, ask Claude to refresh the \"record.agent\" prompt on the hub from a summary of the run. Requires a hub connection.").optionsGroup("Environment and connection:").option("--cwd <path>", "Working directory containing the .ccqa/ tree (monorepo support). Defaults to the current directory.").option("--project <name>", "Project name for the hub. Defaults to the current directory's name.")))).action(withUsageErrors(async (specPath, opts) => {
15526
+ }, "lenient").option("--auto-fix <mode>", "Auto-fix behaviour during script generation: 'interactive' (default, prompt y/N; declines on non-TTY), 'auto' (apply without prompt, for CI), 'skip' (agent-browser: apply only high-confidence fixes; external targets like playwright/runn: no fix pass at all).", parseAutoFixFlag, "interactive").option("--auto-fix-max-retries <n>", "Maximum number of auto-fix retries", "3").option("--timeout <seconds>", "Abort the recording after this many seconds, wherever it is (trace, generate, auto-fix): reap the browser session, seal the open hub run (--report-to-hub) with a 'timed out' note, and exit 124. Prefer this over wrapping the command in an external `timeout`, whose SIGTERM may never reach this process.", parseTimeoutSeconds).option("--trace-only", "Stop after the trace step; do not generate test code").option("--no-session-pin", "Don't pin AGENT_BROWSER_SESSION / capture page snapshots after a failure (debug toggle)").optionsGroup("What to do with the result:").option("--overwrite", "Replace an existing test.spec.ts without warning").option("--report-to-hub", "Leave a run (kind: record) on the hub saying this spec was recorded and what the recording spent on Claude, so a budget summed over the hub's runs sees it. It advances no ledger: a recording verifies nothing.").optionsGroup("Learning:").option("--learn-hub-trace-prompt", "After the trace finishes, ask Claude to refresh the \"record.agent\" prompt on the hub from a summary of the run. Requires a hub connection.").optionsGroup("Environment and connection:").option("--cwd <path>", "Working directory containing the .ccqa/ tree (monorepo support). Defaults to the current directory.").option("--project <name>", "Project name for the hub. Defaults to the current directory's name.")))).action(withUsageErrors(async (specPath, opts) => {
15527
15527
  await withCostReporting("record", () => runRecord(specPath, opts));
15528
15528
  }));
15529
15529
  async function runRecord(specPath, opts) {
@@ -15578,21 +15578,29 @@ async function runRecord(specPath, opts) {
15578
15578
  let recorded = false;
15579
15579
  let sealed = true;
15580
15580
  let tracingStep;
15581
- let killedBy;
15581
+ let abortCause;
15582
15582
  const teardown = createRunTeardown();
15583
15583
  teardown.onFinalize(async () => {
15584
15584
  if (!push) return;
15585
- const note = killedBy ? `terminated by signal (${killedBy})${tracingStep ? ` during ${tracingStep}` : ""}` : void 0;
15586
- sealed = await sealRecordPush(push, featureName, specName, recorded, note);
15585
+ sealed = await sealRecordPush(push, featureName, specName, recorded, abortCause !== void 0 ? abortNote(abortCause, tracingStep) : void 0);
15587
15586
  });
15588
15587
  const disposeSignalHandlers = installTeardownSignalHandlers(teardown, (sig) => {
15589
- killedBy = sig;
15588
+ abortCause = `terminated by signal (${sig})`;
15590
15589
  });
15590
+ let deadline;
15591
+ if (opts.timeout !== void 0) {
15592
+ const seconds = opts.timeout;
15593
+ deadline = setTimeout(() => {
15594
+ abortCause = `timed out after ${seconds}s`;
15595
+ error(`--timeout: ${abortNote(abortCause, tracingStep)}`);
15596
+ teardown.run().finally(() => process.exit(124));
15597
+ }, seconds * 1e3);
15598
+ deadline.unref();
15599
+ }
15591
15600
  try {
15592
- let traceResult = null;
15593
15601
  let generated = true;
15594
15602
  try {
15595
- traceResult = await runTrace(featureName, specName, opts.model, opts.traceValidation ?? "lenient", language, {
15603
+ const traceResult = await runTrace(featureName, specName, opts.model, opts.traceValidation ?? "lenient", language, {
15596
15604
  cwd: cwdForProfile,
15597
15605
  hubContext,
15598
15606
  ...opts.instruction ? { instruction: opts.instruction } : {},
@@ -15602,6 +15610,15 @@ async function runRecord(specPath, opts) {
15602
15610
  });
15603
15611
  tracingStep = void 0;
15604
15612
  blank();
15613
+ await learnFromTrace({
15614
+ enabled: opts.learnHubTracePrompt === true,
15615
+ featureName,
15616
+ specName,
15617
+ traceResult,
15618
+ hubContext,
15619
+ ...opts.model ? { model: opts.model } : {},
15620
+ ...language ? { language } : {}
15621
+ });
15605
15622
  if (!opts.traceOnly) generated = (await runGenerate(featureName, specName, {
15606
15623
  maxRetries: parseInt(opts.autoFixMaxRetries ?? "3", 10),
15607
15624
  fixMode: toFixMode(opts.autoFix ?? "interactive"),
@@ -15616,25 +15633,52 @@ async function runRecord(specPath, opts) {
15616
15633
  } finally {
15617
15634
  await releaseLock();
15618
15635
  }
15619
- if (opts.learnHubTracePrompt && traceResult !== null) {
15620
- blank();
15621
- await updateAgentPrompt({
15622
- kind: "record",
15623
- flag: "--learn-hub-trace-prompt",
15624
- runSummary: buildRecordRunSummary(featureName, specName, traceResult),
15625
- hubContext,
15626
- ...opts.model ? { model: opts.model } : {},
15627
- ...language ? { language } : {}
15628
- });
15629
- }
15630
15636
  recorded = generated;
15631
15637
  } finally {
15638
+ if (deadline !== void 0) clearTimeout(deadline);
15632
15639
  await teardown.run();
15633
15640
  disposeSignalHandlers();
15634
15641
  }
15635
15642
  if (!sealed) process.exit(2);
15636
15643
  if (!recorded) process.exit(1);
15637
15644
  }
15645
+ /** `--timeout <seconds>`: a positive whole number of seconds. */
15646
+ function parseTimeoutSeconds(raw) {
15647
+ const n = Number(raw);
15648
+ if (!Number.isFinite(n) || n <= 0 || Math.floor(n) !== n) throw new Error(`--timeout must be a positive integer number of seconds, got "${raw}"`);
15649
+ return n;
15650
+ }
15651
+ /**
15652
+ * The one line an aborted recording leaves on its hub row: what ended it
15653
+ * ("terminated by signal (SIGTERM)", "timed out after 900s") plus the spec
15654
+ * step that was tracing, when one was in flight. The signal handlers and the
15655
+ * --timeout deadline both seal through this, so every abort dies with the
15656
+ * same shape of reason.
15657
+ */
15658
+ function abortNote(cause, tracingStep) {
15659
+ return `${cause}${tracingStep !== void 0 ? ` during ${tracingStep}` : ""}`;
15660
+ }
15661
+ /**
15662
+ * The rule for `--learn-hub-trace-prompt`: a browser trace ran → learn from
15663
+ * it; no trace → stay silent. `runRecord` calls this immediately after the
15664
+ * trace, before generate — sequencing it after the generate/auto-fix half
15665
+ * (as record once did) let any death there discard a completed trace's
15666
+ * learnings. Returns whether the refresh fired, and takes the updater as a
15667
+ * seam so the rule is testable without a browser.
15668
+ */
15669
+ async function learnFromTrace(args, update = updateAgentPrompt) {
15670
+ if (!args.enabled || args.traceResult === null) return false;
15671
+ blank();
15672
+ await update({
15673
+ kind: "record",
15674
+ flag: "--learn-hub-trace-prompt",
15675
+ runSummary: buildRecordRunSummary(args.featureName, args.specName, args.traceResult),
15676
+ hubContext: args.hubContext,
15677
+ ...args.model !== void 0 ? { model: args.model } : {},
15678
+ ...args.language !== void 0 ? { language: args.language } : {}
15679
+ });
15680
+ return true;
15681
+ }
15638
15682
  /**
15639
15683
  * Close the record run with the one row this command produced, answering
15640
15684
  * whether it closed. One spec is recorded per invocation, so one row is the
@@ -481,8 +481,8 @@ declare const ReportSpecResultSchema: z.ZodObject<{
481
481
  title: z.ZodNullable<z.ZodString>;
482
482
  target: z.ZodOptional<z.ZodString>;
483
483
  mode: z.ZodOptional<z.ZodEnum<{
484
- live: "live";
485
484
  deterministic: "deterministic";
485
+ live: "live";
486
486
  }>>;
487
487
  status: z.ZodEnum<{
488
488
  passed: "passed";
@@ -665,8 +665,8 @@ declare const RunReportDataSchema: z.ZodObject<{
665
665
  title: z.ZodNullable<z.ZodString>;
666
666
  target: z.ZodOptional<z.ZodString>;
667
667
  mode: z.ZodOptional<z.ZodEnum<{
668
- live: "live";
669
668
  deterministic: "deterministic";
669
+ live: "live";
670
670
  }>>;
671
671
  status: z.ZodEnum<{
672
672
  passed: "passed";
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.31.0",
3
+ "version": "1.31.1",
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.31.0",
3
+ "version": "1.31.1",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {